docs: add VitePress documentation site

- Add docs directory with VitePress configuration
- Add GitHub Actions workflow for docs build and deploy
- Support bilingual (English/Chinese) documentation
- Include search, custom theme, and responsive design
This commit is contained in:
catlog22
2026-02-28 16:14:09 +08:00
parent ab65caec45
commit c3ddf7e322
136 changed files with 34486 additions and 0 deletions

363
docs/.vitepress/config.ts Normal file
View File

@@ -0,0 +1,363 @@
import { defineConfig } from 'vitepress'
const repoName = process.env.GITHUB_REPOSITORY?.split('/')[1]
const isUserOrOrgSite = Boolean(repoName && repoName.endsWith('.github.io'))
const base =
process.env.CCW_DOCS_BASE ||
(process.env.GITHUB_ACTIONS && repoName && !isUserOrOrgSite ? `/${repoName}/` : '/')
export default defineConfig({
title: 'CCW Documentation',
description: 'Claude Code Workspace - Advanced AI-Powered Development Environment',
lang: 'zh-CN',
base,
// Ignore dead links for incomplete docs
ignoreDeadLinks: true,
head: [
['link', { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' }],
[
'script',
{},
`(() => {
try {
const theme = localStorage.getItem('ccw-theme') || 'blue'
document.documentElement.setAttribute('data-theme', theme)
const mode = localStorage.getItem('ccw-color-mode') || 'auto'
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const isDark = mode === 'dark' || (mode === 'auto' && prefersDark)
document.documentElement.classList.toggle('dark', isDark)
} catch {}
})()`
],
['meta', { name: 'theme-color', content: '#3b82f6' }],
['meta', { name: 'og:type', content: 'website' }],
['meta', { name: 'og:locale', content: 'en_US' }],
['meta', { name: 'og:locale:alternate', content: 'zh_CN' }]
],
// Appearance
appearance: false,
// Vite build/dev optimizations
vite: {
optimizeDeps: {
include: ['flexsearch']
},
build: {
target: 'es2019',
cssCodeSplit: true
}
},
// Theme configuration
themeConfig: {
logo: '/logo.svg',
// Right-side table of contents (outline)
outline: {
level: [2, 3],
label: 'On this page'
},
// Navigation - 按照 Trellis 风格组织
nav: [
{ text: 'Guide', link: '/guide/ch01-what-is-claude-dms3' },
{ text: 'Commands', link: '/commands/claude/' },
{ text: 'Skills', link: '/skills/' },
{ text: 'Features', link: '/features/spec' },
{
text: 'Languages',
items: [
{ text: '简体中文', link: '/zh/guide/ch01-what-is-claude-dms3' }
]
}
],
// Sidebar - 按照 Trellis 风格组织
sidebar: {
'/guide/': [
{
text: 'Guide',
items: [
{ text: 'What is Claude_dms3', link: '/guide/ch01-what-is-claude-dms3' },
{ text: 'Getting Started', link: '/guide/ch02-getting-started' },
{ text: 'Core Concepts', link: '/guide/ch03-core-concepts' },
{ text: 'Workflow Basics', link: '/guide/ch04-workflow-basics' },
{ text: 'Advanced Tips', link: '/guide/ch05-advanced-tips' },
{ text: 'Best Practices', link: '/guide/ch06-best-practices' }
]
}
],
'/commands/': [
{
text: 'Claude Commands',
collapsible: true,
items: [
{ text: 'Overview', link: '/commands/claude/' },
{ text: 'Core Orchestration', link: '/commands/claude/core-orchestration' },
{ text: 'Workflow', link: '/commands/claude/workflow' },
{ text: 'Session', link: '/commands/claude/session' },
{ text: 'Issue', link: '/commands/claude/issue' },
{ text: 'Memory', link: '/commands/claude/memory' },
{ text: 'CLI', link: '/commands/claude/cli' },
{ text: 'UI Design', link: '/commands/claude/ui-design' }
]
},
{
text: 'Codex Prompts',
collapsible: true,
items: [
{ text: 'Overview', link: '/commands/codex/' },
{ text: 'Prep', link: '/commands/codex/prep' },
{ text: 'Review', link: '/commands/codex/review' }
]
}
],
'/skills/': [
{
text: 'Claude Skills',
collapsible: true,
items: [
{ text: 'Overview', link: '/skills/claude-index' },
{ text: 'Collaboration', link: '/skills/claude-collaboration' },
{ text: 'Workflow', link: '/skills/claude-workflow' },
{ text: 'Memory', link: '/skills/claude-memory' },
{ text: 'Review', link: '/skills/claude-review' },
{ text: 'Meta', link: '/skills/claude-meta' }
]
},
{
text: 'Codex Skills',
collapsible: true,
items: [
{ text: 'Overview', link: '/skills/codex-index' },
{ text: 'Lifecycle', link: '/skills/codex-lifecycle' },
{ text: 'Workflow', link: '/skills/codex-workflow' },
{ text: 'Specialized', link: '/skills/codex-specialized' }
]
}
],
'/features/': [
{
text: 'Core Features',
items: [
{ text: 'Spec System', link: '/features/spec' },
{ text: 'Memory System', link: '/features/memory' },
{ text: 'CLI Call', link: '/features/cli' },
{ text: 'Dashboard', link: '/features/dashboard' },
{ text: 'CodexLens', link: '/features/codexlens' },
{ text: 'API Settings', link: '/features/api-settings' },
{ text: 'System Settings', link: '/features/system-settings' }
]
}
],
'/mcp/': [
{
text: 'MCP Tools',
collapsible: true,
items: [
{ text: 'Overview', link: '/mcp/tools' }
]
}
],
'/agents/': [
{
text: 'Agents',
collapsible: true,
items: [
{ text: 'Overview', link: '/agents/' },
{ text: 'Built-in Agents', link: '/agents/builtin' },
{ text: 'Custom Agents', link: '/agents/custom' }
]
}
],
'/workflows/': [
{
text: 'Workflow System',
collapsible: true,
items: [
{ text: 'Overview', link: '/workflows/' },
{ text: '4-Level System', link: '/workflows/4-level' },
{ text: 'Best Practices', link: '/workflows/best-practices' }
]
}
]
},
// Social links
socialLinks: [
{ icon: 'github', link: 'https://github.com/catlog22/Claude-Code-Workflow' }
],
// Footer
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2025-present CCW Contributors'
},
// Edit link
editLink: {
pattern: 'https://github.com/catlog22/Claude-Code-Workflow/edit/main/docs/:path',
text: 'Edit this page on GitHub'
},
// Last updated
lastUpdated: {
text: 'Last updated',
formatOptions: {
dateStyle: 'full',
timeStyle: 'short'
}
},
// Search (handled by custom FlexSearch DocSearch component)
search: false
},
// Markdown configuration
markdown: {
lineNumbers: true,
theme: {
light: 'github-light',
dark: 'github-dark'
},
languages: [
'bash',
'powershell',
'json',
'yaml',
'toml',
'javascript',
'typescript',
'vue',
'markdown'
],
config: (md) => {
// Add markdown-it plugins if needed
}
},
// locales
locales: {
root: {
label: 'English',
lang: 'en-US'
},
zh: {
label: '简体中文',
lang: 'zh-CN',
title: 'CCW 文档',
description: 'Claude Code Workspace - 高级 AI 驱动开发环境',
themeConfig: {
outline: {
level: [2, 3],
label: '本页目录'
},
nav: [
{ text: '指南', link: '/zh/guide/ch01-what-is-claude-dms3' },
{ text: '命令', link: '/zh/commands/claude/' },
{ text: '技能', link: '/skills/' },
{ text: '功能', link: '/zh/features/spec' },
{
text: '语言',
items: [
{ text: 'English', link: '/guide/ch01-what-is-claude-dms3' }
]
}
],
sidebar: {
'/zh/guide/': [
{
text: '指南',
items: [
{ text: 'Claude_dms3 是什么', link: '/zh/guide/ch01-what-is-claude-dms3' },
{ text: '快速开始', link: '/zh/guide/ch02-getting-started' },
{ text: '核心概念', link: '/zh/guide/ch03-core-concepts' },
{ text: '工作流基础', link: '/zh/guide/ch04-workflow-basics' },
{ text: '高级技巧', link: '/zh/guide/ch05-advanced-tips' },
{ text: '最佳实践', link: '/zh/guide/ch06-best-practices' }
]
}
],
'/zh/commands/': [
{
text: 'Claude 命令',
collapsible: true,
items: [
{ text: '概述', link: '/zh/commands/claude/' },
{ text: '核心编排', link: '/zh/commands/claude/core-orchestration' },
{ text: '工作流', link: '/zh/commands/claude/workflow' },
{ text: '会话管理', link: '/zh/commands/claude/session' },
{ text: 'Issue', link: '/zh/commands/claude/issue' },
{ text: 'Memory', link: '/zh/commands/claude/memory' },
{ text: 'CLI', link: '/zh/commands/claude/cli' },
{ text: 'UI 设计', link: '/zh/commands/claude/ui-design' }
]
},
{
text: 'Codex Prompts',
collapsible: true,
items: [
{ text: '概述', link: '/zh/commands/codex/' },
{ text: 'Prep', link: '/zh/commands/codex/prep' },
{ text: 'Review', link: '/zh/commands/codex/review' }
]
}
],
'/zh/skills/': [
{
text: 'Claude Skills',
collapsible: true,
items: [
{ text: '概述', link: '/zh/skills/claude-index' },
{ text: '协作', link: '/zh/skills/claude-collaboration' },
{ text: '工作流', link: '/zh/skills/claude-workflow' },
{ text: '记忆', link: '/zh/skills/claude-memory' },
{ text: '审查', link: '/zh/skills/claude-review' },
{ text: '元技能', link: '/zh/skills/claude-meta' }
]
},
{
text: 'Codex Skills',
collapsible: true,
items: [
{ text: '概述', link: '/zh/skills/codex-index' },
{ text: '生命周期', link: '/zh/skills/codex-lifecycle' },
{ text: '工作流', link: '/zh/skills/codex-workflow' },
{ text: '专项', link: '/zh/skills/codex-specialized' }
]
}
],
'/zh/features/': [
{
text: '核心功能',
items: [
{ text: 'Spec 规范系统', link: '/zh/features/spec' },
{ text: 'Memory 记忆系统', link: '/zh/features/memory' },
{ text: 'CLI 调用', link: '/zh/features/cli' },
{ text: 'Dashboard 面板', link: '/zh/features/dashboard' },
{ text: 'CodexLens', link: '/zh/features/codexlens' },
{ text: 'API 设置', link: '/zh/features/api-settings' },
{ text: '系统设置', link: '/zh/features/system-settings' }
]
}
],
'/zh/workflows/': [
{
text: '工作流系统',
collapsible: true,
items: [
{ text: '概述', link: '/zh/workflows/' },
{ text: '四级体系', link: '/zh/workflows/4-level' },
{ text: '最佳实践', link: '/zh/workflows/best-practices' }
]
}
]
}
}
}
}
})

View File

@@ -0,0 +1,25 @@
export const FLEXSEARCH_INDEX_VERSION = 1
export function flexsearchEncode(text) {
const normalized = String(text ?? '')
.toLowerCase()
.normalize('NFKC')
const tokens = normalized.match(
/[a-z0-9]+|[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af]/g
)
return tokens ?? []
}
export const FLEXSEARCH_OPTIONS = {
tokenize: 'forward',
resolution: 9,
cache: 100,
encode: flexsearchEncode
}
export function createFlexSearchIndex(FlexSearch) {
return new FlexSearch.Index(FLEXSEARCH_OPTIONS)
}

View File

@@ -0,0 +1,216 @@
<template>
<div class="agent-orchestration">
<div class="orchestration-title">🤖 Agent Orchestration</div>
<div class="agent-flow">
<!-- CLI Layer -->
<div class="flow-layer cli-layer">
<div class="layer-label">CLI Tools</div>
<div class="agents-row">
<div class="agent-card cli" @mouseenter="showTooltip('cli-explore')" @mouseleave="hideTooltip">🔍 Explore</div>
<div class="agent-card cli" @mouseenter="showTooltip('cli-plan')" @mouseleave="hideTooltip">📋 Plan</div>
<div class="agent-card cli" @mouseenter="showTooltip('cli-exec')" @mouseleave="hideTooltip"> Execute</div>
<div class="agent-card cli" @mouseenter="showTooltip('cli-discuss')" @mouseleave="hideTooltip">💬 Discuss</div>
</div>
</div>
<!-- Flow Arrow -->
<div class="flow-arrow"></div>
<!-- Development Layer -->
<div class="flow-layer dev-layer">
<div class="layer-label">Development</div>
<div class="agents-row">
<div class="agent-card dev" @mouseenter="showTooltip('code-dev')" @mouseleave="hideTooltip">👨💻 Code</div>
<div class="agent-card dev" @mouseenter="showTooltip('tdd')" @mouseleave="hideTooltip">🧪 TDD</div>
<div class="agent-card dev" @mouseenter="showTooltip('test-fix')" @mouseleave="hideTooltip">🔧 Fix</div>
</div>
</div>
<!-- Flow Arrow -->
<div class="flow-arrow"></div>
<!-- Output Layer -->
<div class="flow-layer output-layer">
<div class="layer-label">Output</div>
<div class="agents-row">
<div class="agent-card doc" @mouseenter="showTooltip('doc-gen')" @mouseleave="hideTooltip">📄 Docs</div>
<div class="agent-card ui" @mouseenter="showTooltip('ui-design')" @mouseleave="hideTooltip">🎨 UI</div>
<div class="agent-card universal" @mouseenter="showTooltip('universal')" @mouseleave="hideTooltip">🌐 Universal</div>
</div>
</div>
</div>
<div class="tooltip" v-if="tooltip" :class="{ visible: tooltip }">
{{ tooltipText }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const tooltip = ref(false)
const tooltipText = ref('')
const tooltips = {
'cli-explore': 'cli-explore-agent: 代码库探索和语义搜索',
'cli-plan': 'cli-planning-agent: 任务规划和分解',
'cli-exec': 'cli-execution-agent: 命令执行和结果处理',
'cli-discuss': 'cli-discuss-agent: 多视角讨论和共识达成',
'code-dev': 'code-developer: 代码实现和开发',
'tdd': 'tdd-developer: 测试驱动开发',
'test-fix': 'test-fix-agent: 测试修复循环',
'doc-gen': 'doc-generator: 文档自动生成',
'ui-design': 'ui-design-agent: UI设计和设计令牌',
'universal': 'universal-executor: 通用任务执行器'
}
function showTooltip(key) {
tooltipText.value = tooltips[key] || ''
tooltip.value = true
}
function hideTooltip() {
tooltip.value = false
}
</script>
<style scoped>
.agent-orchestration {
padding: 3rem 2rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 24px;
margin: 2rem 0;
position: relative;
overflow: hidden;
}
.orchestration-title {
text-align: center;
font-size: 1.75rem;
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 2.5rem;
}
.agent-flow {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.flow-layer {
width: 100%;
max-width: 600px;
}
.layer-label {
font-size: 0.7rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--vp-c-text-3);
margin-bottom: 0.75rem;
text-align: center;
}
.agents-row {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 1rem;
}
.agent-card {
padding: 0.8rem 1.5rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
border: 1px solid transparent;
}
.agent-card.cli {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
border-color: rgba(59, 130, 246, 0.2);
}
.agent-card.dev {
background: rgba(16, 185, 129, 0.1);
color: #10B981;
border-color: rgba(16, 185, 129, 0.2);
}
.agent-card.doc {
background: rgba(139, 92, 246, 0.1);
color: #8B5CF6;
border-color: rgba(139, 92, 246, 0.2);
}
.agent-card.ui {
background: rgba(245, 158, 11, 0.1);
color: #F59E0B;
border-color: rgba(245, 158, 11, 0.2);
}
.agent-card.universal {
background: rgba(239, 68, 68, 0.1);
color: #EF4444;
border-color: rgba(239, 68, 68, 0.2);
}
.agent-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.flow-arrow {
color: var(--vp-c-divider);
font-size: 1.25rem;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); opacity: 0.5; }
50% { transform: translateY(8px); opacity: 1; }
}
.tooltip {
position: absolute;
top: 1rem;
right: 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-brand-1);
color: var(--vp-c-text-1);
padding: 0.75rem 1.25rem;
border-radius: 10px;
font-size: 0.85rem;
font-weight: 500;
opacity: 0;
transition: all 0.3s;
pointer-events: none;
box-shadow: var(--vp-shadow-md);
z-index: 10;
}
.tooltip.visible {
opacity: 1;
}
@media (max-width: 640px) {
.agent-orchestration {
padding: 2rem 1rem;
}
.agent-card {
padding: 0.6rem 1rem;
font-size: 0.8rem;
}
}
</style>

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
const { page } = useData()
interface BreadcrumbItem {
text: string
link?: string
}
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
const items: BreadcrumbItem[] = [
{ text: 'Home', link: '/' }
]
const pathSegments = page.value.relativePath.split('/')
const fileName = pathSegments.pop()?.replace(/\.md$/, '')
// Build breadcrumb from path
let currentPath = ''
for (const segment of pathSegments) {
currentPath += `${segment}/`
items.push({
text: formatTitle(segment),
link: `/${currentPath}`
})
}
// Add current page
if (fileName && fileName !== 'index') {
items.push({
text: formatTitle(fileName)
})
}
return items
})
const formatTitle = (str: string): string => {
return str
.split(/[-_]/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
</script>
<template>
<nav v-if="breadcrumbs.length > 1" class="breadcrumb" aria-label="Breadcrumb">
<ol class="breadcrumb-list">
<li v-for="(item, index) in breadcrumbs" :key="index" class="breadcrumb-item">
<router-link v-if="item.link && index < breadcrumbs.length - 1" :to="item.link" class="breadcrumb-link">
{{ item.text }}
</router-link>
<span v-else class="breadcrumb-current">{{ item.text }}</span>
<span v-if="index < breadcrumbs.length - 1" class="breadcrumb-separator">/</span>
</li>
</ol>
</nav>
</template>
<style scoped>
.breadcrumb {
padding: 12px 0;
margin-bottom: 16px;
}
.breadcrumb-list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
list-style: none;
margin: 0;
padding: 0;
}
.breadcrumb-item {
display: flex;
align-items: center;
gap: 4px;
}
.breadcrumb-link {
color: var(--vp-c-text-2);
font-size: var(--vp-font-size-sm);
text-decoration: none;
transition: color var(--vp-transition-color);
}
.breadcrumb-link:hover {
color: var(--vp-c-primary);
text-decoration: underline;
}
.breadcrumb-current {
color: var(--vp-c-text-1);
font-size: var(--vp-font-size-sm);
font-weight: 500;
}
.breadcrumb-separator {
color: var(--vp-c-text-3);
font-size: var(--vp-font-size-sm);
}
@media (max-width: 768px) {
.breadcrumb {
padding: 8px 0;
margin-bottom: 12px;
}
.breadcrumb-link,
.breadcrumb-current,
.breadcrumb-separator {
font-size: 12px;
}
.breadcrumb-list {
gap: 2px;
}
}
</style>

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
// This component is integrated into ThemeSwitcher
// Kept as separate component for modularity
const emit = defineEmits<{
(e: 'select', scheme: string): void
}>()
const schemes = [
{ id: 'blue', name: 'Blue', color: '#3b82f6' },
{ id: 'green', name: 'Green', color: '#10b981' },
{ id: 'orange', name: 'Orange', color: '#f59e0b' },
{ id: 'purple', name: 'Purple', color: '#8b5cf6' }
]
const selectScheme = (schemeId: string) => {
emit('select', schemeId)
}
</script>
<template>
<div class="color-scheme-selector">
<button
v-for="scheme in schemes"
:key="scheme.id"
:class="['scheme-option']"
:style="{ '--scheme-color': scheme.color }"
:aria-label="scheme.name"
@click="selectScheme(scheme.id)"
>
<span class="scheme-indicator"></span>
<span class="scheme-name">{{ scheme.name }}</span>
</button>
</div>
</template>
<style scoped>
.color-scheme-selector {
display: flex;
flex-direction: column;
gap: 4px;
}
.scheme-option {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border: 1px solid var(--vp-c-border);
border-radius: var(--vp-radius-md);
background: var(--vp-c-bg);
cursor: pointer;
transition: all var(--vp-transition-color);
}
.scheme-option:hover {
border-color: var(--vp-c-primary);
background: var(--vp-c-bg-soft);
}
.scheme-indicator {
width: 16px;
height: 16px;
border-radius: var(--vp-radius-full);
background: var(--scheme-color);
border: 2px solid var(--vp-c-border);
}
.scheme-name {
font-size: var(--vp-font-size-sm);
color: var(--vp-c-text-1);
}
</style>

View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
code: string
}>()
const emit = defineEmits<{
(e: 'copy'): void
}>()
const copied = ref(false)
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(props.code)
copied.value = true
emit('copy')
setTimeout(() => {
copied.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
// Also handle Ctrl+C
const handleKeydown = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 'c') {
copyToClipboard()
}
}
</script>
<template>
<button
class="copy-code-button"
:class="{ copied }"
:aria-label="copied ? 'Copied!' : 'Copy code'"
:title="copied ? 'Copied!' : 'Copy code (Ctrl+C)'"
@click="copyToClipboard"
@keydown="handleKeydown"
>
<svg v-if="!copied" class="icon copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
<svg v-else class="icon check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span v-if="copied" class="copy-feedback">Copied!</span>
</button>
</template>
<style scoped>
.copy-code-button {
position: absolute;
top: 12px;
right: 12px;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--vp-c-border);
border-radius: var(--vp-radius-md);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
font-size: var(--vp-font-size-sm);
cursor: pointer;
opacity: 0;
transition: all var(--vp-transition-color);
z-index: 10;
}
.copy-code-button:hover {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border-color: var(--vp-c-primary);
}
.copy-code-button.copied {
background: var(--vp-c-secondary-500);
color: white;
border-color: var(--vp-c-secondary-500);
}
.copy-code-button .icon {
width: 16px;
height: 16px;
}
.copy-feedback {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
padding: 4px 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-border);
border-radius: var(--vp-radius-md);
font-size: 12px;
white-space: nowrap;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Show button on code block hover */
div[class*='language-']:hover .copy-code-button,
.copy-code-button:focus {
opacity: 1;
}
@media (max-width: 768px) {
.copy-code-button {
opacity: 1;
top: 8px;
right: 8px;
padding: 8px;
}
.copy-feedback {
display: none;
}
}
</style>

View File

@@ -0,0 +1,133 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
type ColorMode = 'light' | 'dark' | 'auto'
const colorMode = ref<ColorMode>('auto')
const modes: { id: ColorMode; name: string; icon: string }[] = [
{ id: 'light', name: 'Light', icon: 'sun' },
{ id: 'dark', name: 'Dark', icon: 'moon' },
{ id: 'auto', name: 'Auto', icon: 'computer' }
]
const setMode = (mode: ColorMode) => {
colorMode.value = mode
localStorage.setItem('ccw-color-mode', mode)
applyMode(mode)
}
const applyMode = (mode: ColorMode) => {
const html = document.documentElement
if (mode === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
html.classList.toggle('dark', prefersDark)
} else {
html.classList.toggle('dark', mode === 'dark')
}
}
onMounted(() => {
const savedMode = localStorage.getItem('ccw-color-mode') as ColorMode
if (savedMode && modes.find(m => m.id === savedMode)) {
setMode(savedMode)
} else {
setMode('auto')
}
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (colorMode.value === 'auto') {
applyMode('auto')
}
})
})
</script>
<template>
<div class="dark-mode-toggle">
<button
v-for="mode in modes"
:key="mode.id"
:class="['mode-button', { active: colorMode === mode.id }]"
:aria-label="`Switch to ${mode.name} mode`"
:title="mode.name"
@click="setMode(mode.id)"
>
<svg v-if="mode.icon === 'sun'" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<svg v-else-if="mode.icon === 'moon'" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<svg v-else class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</button>
</div>
</template>
<style scoped>
.dark-mode-toggle {
display: flex;
align-items: center;
gap: 2px;
padding: 4px;
background: var(--vp-c-bg-soft);
border-radius: var(--vp-radius-full);
}
.mode-button {
position: relative;
width: 36px;
height: 32px;
border: none;
background: transparent;
border-radius: var(--vp-radius-md);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-2);
transition: all var(--vp-transition-color);
}
.mode-button:hover {
color: var(--vp-c-text-1);
background: var(--vp-c-bg-mute);
}
.mode-button.active {
background: var(--vp-c-bg);
color: var(--vp-c-primary);
box-shadow: var(--vp-shadow-sm);
}
.mode-button .icon {
width: 18px;
height: 18px;
}
@media (max-width: 768px) {
.mode-button {
width: 40px;
height: 36px;
}
.mode-button .icon {
width: 20px;
height: 20px;
}
}
</style>

View File

@@ -0,0 +1,492 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import { useData, useRouter, withBase } from 'vitepress'
type LocaleKey = 'root' | 'zh'
interface SearchDoc {
id: number
title: string
url: string
excerpt?: string
}
interface SearchIndexPayload {
version: number
locale: LocaleKey
index: Record<string, string>
docs: SearchDoc[]
}
const { page } = useData()
const router = useRouter()
const localeKey = computed<LocaleKey>(() =>
page.value.relativePath.startsWith('zh/') ? 'zh' : 'root'
)
const isOpen = ref(false)
const isLoading = ref(false)
const error = ref<string | null>(null)
const query = ref('')
const results = ref<SearchDoc[]>([])
const activeIndex = ref(0)
const inputRef = ref<HTMLInputElement | null>(null)
const buttonRef = ref<HTMLButtonElement | null>(null)
const modifierKey = ref('Ctrl')
const loadedIndex = shallowRef<any | null>(null)
const loadedDocsById = shallowRef<Map<number, SearchDoc> | null>(null)
const cache = new Map<LocaleKey, { index: any; docsById: Map<number, SearchDoc> }>()
const placeholder = computed(() => (localeKey.value === 'zh' ? '搜索文档' : 'Search docs'))
const cancelText = computed(() => (localeKey.value === 'zh' ? '关闭' : 'Close'))
const loadingText = computed(() => (localeKey.value === 'zh' ? '正在加载索引…' : 'Loading index…'))
const hintText = computed(() => (localeKey.value === 'zh' ? '输入关键词开始搜索' : 'Type to start searching'))
const noResultsText = computed(() => (localeKey.value === 'zh' ? '未找到结果' : 'No results'))
function isEditableTarget(target: EventTarget | null) {
const el = target as HTMLElement | null
if (!el) return false
const tag = el.tagName?.toLowerCase()
if (!tag) return false
return tag === 'input' || tag === 'textarea' || tag === 'select' || el.isContentEditable
}
async function loadLocaleIndex(key: LocaleKey) {
const cached = cache.get(key)
if (cached) {
loadedIndex.value = cached.index
loadedDocsById.value = cached.docsById
return
}
isLoading.value = true
error.value = null
try {
const res = await fetch(withBase(`/search-index.${key}.json`))
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const payload = (await res.json()) as SearchIndexPayload
if (!payload || payload.locale !== key) throw new Error('Invalid index payload')
const [{ default: FlexSearch }, { createFlexSearchIndex }] = await Promise.all([
import('flexsearch'),
import('../../search/flexsearch.mjs')
])
const index = createFlexSearchIndex(FlexSearch)
await Promise.all(Object.entries(payload.index).map(([k, v]) => index.import(k, v)))
const docsById = new Map<number, SearchDoc>()
for (const doc of payload.docs) docsById.set(doc.id, doc)
cache.set(key, { index, docsById })
loadedIndex.value = index
loadedDocsById.value = docsById
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
loadedIndex.value = null
loadedDocsById.value = null
} finally {
isLoading.value = false
}
}
async function ensureReady() {
await loadLocaleIndex(localeKey.value)
}
let searchTimer: number | undefined
async function runSearch() {
const q = query.value.trim()
if (!q) {
results.value = []
activeIndex.value = 0
return
}
await ensureReady()
if (!loadedIndex.value || !loadedDocsById.value) {
results.value = []
activeIndex.value = 0
return
}
const ids = loadedIndex.value.search(q, 12) as number[]
const docsById = loadedDocsById.value
results.value = ids
.map((id) => docsById.get(id))
.filter((d): d is SearchDoc => Boolean(d))
activeIndex.value = 0
}
function navigate(url: string) {
router.go(withBase(url))
close()
}
async function open() {
isOpen.value = true
document.body.style.overflow = 'hidden'
await ensureReady()
await nextTick()
inputRef.value?.focus()
}
function close() {
isOpen.value = false
query.value = ''
results.value = []
activeIndex.value = 0
error.value = null
document.body.style.overflow = ''
buttonRef.value?.focus()
}
function onInputKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.preventDefault()
close()
return
}
if (e.key === 'ArrowDown') {
e.preventDefault()
if (results.value.length > 0) {
activeIndex.value = (activeIndex.value + 1) % results.value.length
}
return
}
if (e.key === 'ArrowUp') {
e.preventDefault()
if (results.value.length > 0) {
activeIndex.value =
(activeIndex.value - 1 + results.value.length) % results.value.length
}
return
}
if (e.key === 'Enter') {
const hit = results.value[activeIndex.value]
if (hit) {
e.preventDefault()
navigate(hit.url)
}
}
}
let onGlobalKeydown: ((e: KeyboardEvent) => void) | null = null
onMounted(() => {
modifierKey.value = /mac/i.test(navigator.platform) ? '⌘' : 'Ctrl'
onGlobalKeydown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
e.preventDefault()
if (!isOpen.value) open()
return
}
if (e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey) {
if (isEditableTarget(e.target)) return
e.preventDefault()
if (!isOpen.value) open()
}
}
window.addEventListener('keydown', onGlobalKeydown)
})
onBeforeUnmount(() => {
if (onGlobalKeydown) window.removeEventListener('keydown', onGlobalKeydown)
})
watch(query, () => {
if (searchTimer) window.clearTimeout(searchTimer)
searchTimer = window.setTimeout(() => {
runSearch()
}, 60)
})
watch(
() => page.value.relativePath,
() => {
if (isOpen.value) close()
}
)
</script>
<template>
<div class="DocSearch">
<button
ref="buttonRef"
type="button"
class="DocSearch-Button"
:aria-label="placeholder"
@click="open"
>
<svg class="DocSearch-Button-Icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<span class="DocSearch-Button-Placeholder">{{ placeholder }}</span>
<span class="DocSearch-Button-Keys" aria-hidden="true">
<kbd class="DocSearch-Button-Key">{{ modifierKey }}</kbd>
<kbd class="DocSearch-Button-Key">K</kbd>
</span>
</button>
<Teleport to="body">
<div v-if="isOpen" class="DocSearch-Modal">
<div class="DocSearch-Overlay" @click="close" />
<div class="DocSearch-Container" role="dialog" aria-modal="true">
<div class="DocSearch-SearchBar">
<svg class="DocSearch-SearchIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<input
ref="inputRef"
v-model="query"
class="DocSearch-Input"
type="search"
:placeholder="placeholder"
:aria-label="placeholder"
@keydown="onInputKeydown"
/>
<button type="button" class="DocSearch-Cancel" @click="close">{{ cancelText }}</button>
</div>
<div class="DocSearch-Body">
<div v-if="isLoading" class="DocSearch-Status">{{ loadingText }}</div>
<div v-else-if="error" class="DocSearch-Status DocSearch-Status--error">{{ error }}</div>
<div v-else-if="query.trim().length === 0" class="DocSearch-Status">{{ hintText }}</div>
<div v-else>
<ul v-if="results.length > 0" class="DocSearch-Results">
<li
v-for="(item, i) in results"
:key="item.id"
:class="['DocSearch-Result', { active: i === activeIndex }]"
@mousemove="activeIndex = i"
>
<a
class="DocSearch-Result-Link"
:href="withBase(item.url)"
@click.prevent="navigate(item.url)"
>
<div class="DocSearch-Result-Title">{{ item.title }}</div>
<div v-if="item.excerpt" class="DocSearch-Result-Excerpt">
{{ item.excerpt }}
</div>
</a>
</li>
</ul>
<div v-else class="DocSearch-Status">{{ noResultsText }}</div>
</div>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<style scoped>
.DocSearch {
display: flex;
align-items: center;
}
.DocSearch-Button {
display: inline-flex;
align-items: center;
gap: 10px;
height: 36px;
padding: 0 12px;
border: 1px solid var(--vp-c-border);
border-radius: var(--vp-radius-full);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
cursor: pointer;
transition: all var(--vp-transition-color);
}
.DocSearch-Button:hover {
border-color: var(--vp-c-primary);
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
}
.DocSearch-Button-Icon {
width: 16px;
height: 16px;
flex: 0 0 auto;
}
.DocSearch-Button-Placeholder {
font-size: var(--vp-font-size-sm);
white-space: nowrap;
}
.DocSearch-Button-Keys {
display: inline-flex;
gap: 4px;
margin-left: 8px;
}
.DocSearch-Button-Key {
font-family: var(--vp-font-family-mono);
font-size: 12px;
line-height: 1;
padding: 4px 6px;
border-radius: var(--vp-radius-sm);
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.DocSearch-Modal {
position: fixed;
inset: 0;
z-index: var(--vp-z-index-modal);
}
.DocSearch-Overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
.DocSearch-Container {
position: relative;
width: min(720px, calc(100vw - 32px));
margin: 10vh auto 0;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: var(--vp-radius-xl);
box-shadow: var(--vp-shadow-xl);
overflow: hidden;
}
.DocSearch-SearchBar {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.DocSearch-SearchIcon {
width: 18px;
height: 18px;
color: var(--vp-c-text-3);
flex: 0 0 auto;
}
.DocSearch-Input {
flex: 1;
height: 40px;
padding: 0 12px;
border: 1px solid var(--vp-c-border);
border-radius: var(--vp-radius-md);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
}
.DocSearch-Input:focus-visible {
outline: 2px solid var(--vp-c-primary);
outline-offset: 2px;
}
.DocSearch-Cancel {
height: 40px;
padding: 0 12px;
border: 1px solid var(--vp-c-border);
border-radius: var(--vp-radius-md);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
transition: all var(--vp-transition-color);
}
.DocSearch-Cancel:hover {
border-color: var(--vp-c-primary);
color: var(--vp-c-text-1);
background: var(--vp-c-bg-soft);
}
.DocSearch-Body {
max-height: 60vh;
overflow: auto;
}
.DocSearch-Status {
padding: 18px 16px;
color: var(--vp-c-text-2);
font-size: var(--vp-font-size-sm);
}
.DocSearch-Status--error {
color: #ef4444;
}
.DocSearch-Results {
list-style: none;
margin: 0;
padding: 8px;
}
.DocSearch-Result {
border-radius: var(--vp-radius-lg);
transition: background var(--vp-transition-color);
}
.DocSearch-Result.active,
.DocSearch-Result:hover {
background: var(--vp-c-bg-soft);
}
.DocSearch-Result-Link {
display: block;
padding: 12px 12px;
text-decoration: none;
}
.DocSearch-Result-Title {
font-weight: 600;
color: var(--vp-c-text-1);
font-size: 14px;
}
.DocSearch-Result-Excerpt {
margin-top: 4px;
color: var(--vp-c-text-3);
font-size: 12px;
line-height: 1.4;
}
@media (max-width: 768px) {
.DocSearch-Button-Placeholder,
.DocSearch-Button-Keys {
display: none;
}
.DocSearch-Button {
width: 40px;
justify-content: center;
padding: 0;
}
.DocSearch-Container {
margin-top: 6vh;
}
}
</style>

View File

@@ -0,0 +1,214 @@
<template>
<div class="hero-animation-container" :class="{ 'is-visible': isVisible }">
<div class="glow-bg"></div>
<svg viewBox="0 0 400 320" class="hero-svg" preserveAspectRatio="xMidYMid meet">
<defs>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3.5" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<linearGradient id="pathGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="var(--vp-c-brand-1)" stop-opacity="0" />
<stop offset="50%" stop-color="var(--vp-c-brand-1)" stop-opacity="0.5" />
<stop offset="100%" stop-color="var(--vp-c-brand-1)" stop-opacity="0" />
</linearGradient>
</defs>
<!-- Connection Lines -->
<g class="data-paths">
<path v-for="(path, i) in paths" :key="'path-'+i" :d="path" class="connection-path" />
<circle v-for="(path, i) in paths" :key="'dot-'+i" r="2" class="data-pulse">
<animateMotion :dur="2 + i * 0.4 + 's'" repeatCount="indefinite" :path="path" />
</circle>
</g>
<!-- Orbit Rings -->
<g class="orbit-rings">
<circle cx="200" cy="160" r="130" class="orbit-ring ring-outer" />
<circle cx="200" cy="160" r="95" class="orbit-ring ring-inner" />
</g>
<!-- Agent Nodes -->
<g v-for="(agent, i) in agents" :key="'agent-'+i" class="agent-node" :style="{ '--delay': i * 0.4 + 's' }">
<g class="agent-group" :style="{ transform: `translate(${agent.x}px, ${agent.y}px)` }">
<circle r="8" :fill="agent.color" class="agent-circle" filter="url(#glow)" />
<circle r="12" :stroke="agent.color" fill="none" class="agent-halo" />
<text y="22" text-anchor="middle" class="agent-label">{{ agent.name }}</text>
</g>
</g>
<!-- Central Core -->
<g class="central-core" transform="translate(200, 160)">
<circle r="40" class="core-bg" />
<circle r="32" fill="var(--vp-c-brand-1)" filter="url(#glow)" class="core-inner" />
<text y="8" text-anchor="middle" class="core-text">CCW</text>
<!-- Scanning Effect -->
<path d="M-32 0 A32 32 0 0 1 32 0" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" class="core-scanner" />
</g>
</svg>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const isVisible = ref(false)
onMounted(() => {
setTimeout(() => {
isVisible.value = true
}, 100)
})
const agents = [
{ name: 'Analyze', x: 200, y: 35, color: '#3B82F6' },
{ name: 'Plan', x: 315, y: 110, color: '#10B981' },
{ name: 'Code', x: 285, y: 245, color: '#8B5CF6' },
{ name: 'Test', x: 115, y: 245, color: '#F59E0B' },
{ name: 'Review', x: 85, y: 110, color: '#EF4444' }
]
const paths = [
'M200,160 L200,35',
'M200,160 L315,110',
'M200,160 L285,245',
'M200,160 L115,245',
'M200,160 L85,110',
'M200,35 Q260,35 315,110',
'M315,110 Q315,180 285,245',
'M285,245 Q200,285 115,245',
'M115,245 Q85,180 85,110',
'M85,110 Q85,35 200,35'
]
</script>
<style scoped>
.hero-animation-container {
width: 100%;
max-width: 480px;
position: relative;
opacity: 0;
transform: scale(0.95);
transition: all 1s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.hero-animation-container.is-visible {
opacity: 1;
transform: scale(1);
}
.glow-bg {
position: absolute;
top: 50%;
left: 50%;
width: 150px;
height: 150px;
background: var(--vp-c-brand-1);
filter: blur(80px);
opacity: 0.15;
transform: translate(-50%, -50%);
pointer-events: none;
}
.hero-svg {
width: 100%;
height: auto;
overflow: visible;
}
.orbit-ring {
fill: none;
stroke: var(--vp-c-brand-1);
stroke-width: 0.5;
opacity: 0.1;
stroke-dasharray: 4 4;
}
.ring-outer { animation: rotate 60s linear infinite; transform-origin: 200px 160px; }
.ring-inner { animation: rotate 40s linear infinite reverse; transform-origin: 200px 160px; }
.connection-path {
fill: none;
stroke: var(--vp-c-brand-1);
stroke-width: 0.8;
opacity: 0.05;
}
.data-pulse {
fill: var(--vp-c-brand-2);
filter: drop-shadow(0 0 4px var(--vp-c-brand-2));
}
.agent-group {
transition: all 0.3s ease;
}
.agent-circle {
transition: all 0.3s ease;
}
.agent-halo {
opacity: 0.2;
animation: agent-pulse 2s ease-in-out infinite;
transform-origin: center;
}
.agent-label {
font-size: 10px;
fill: var(--vp-c-text-2);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.7;
}
.core-bg {
fill: var(--vp-c-bg-soft);
stroke: var(--vp-c-brand-soft);
stroke-width: 1;
}
.core-inner {
opacity: 0.8;
}
.core-text {
font-size: 14px;
font-weight: 800;
fill: white;
letter-spacing: 0.05em;
}
.core-scanner {
animation: rotate 3s linear infinite;
opacity: 0.6;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes agent-pulse {
0%, 100% { transform: scale(1); opacity: 0.2; }
50% { transform: scale(1.3); opacity: 0.1; }
}
.agent-node {
animation: agent-float 4s ease-in-out infinite;
animation-delay: var(--delay);
}
@keyframes agent-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
.hero-animation-container:hover .agent-circle {
filter: blur(2px) brightness(1.5);
}
</style>

View File

@@ -0,0 +1,184 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useData } from 'vitepress'
const { page } = useData()
interface TocItem {
id: string
text: string
level: number
children?: TocItem[]
}
const toc = computed<TocItem[]>(() => {
return page.value.headers || []
})
const activeId = ref('')
const onItemClick = (id: string) => {
activeId.value = id
const element = document.getElementById(id)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
onMounted(() => {
// Update active heading on scroll
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
activeId.value = entry.target.id
}
})
},
{ rootMargin: '-80px 0px -80% 0px' }
)
page.value.headers.forEach((header) => {
const element = document.getElementById(header.id)
if (element) {
observer.observe(element)
}
})
return () => observer.disconnect()
})
</script>
<template>
<nav v-if="toc.length > 0" class="page-toc" aria-label="Page navigation">
<div class="toc-header">On this page</div>
<ul class="toc-list">
<li
v-for="item in toc"
:key="item.id"
:class="['toc-item', `toc-level-${item.level}`, { active: item.id === activeId }]"
>
<a
:href="`#${item.id}`"
class="toc-link"
@click.prevent="onItemClick(item.id)"
>
{{ item.text }}
</a>
<ul v-if="item.children && item.children.length > 0" class="toc-list toc-sublist">
<li
v-for="child in item.children"
:key="child.id"
:class="['toc-item', `toc-level-${child.level}`, { active: child.id === activeId }]"
>
<a
:href="`#${child.id}`"
class="toc-link"
@click.prevent="onItemClick(child.id)"
>
{{ child.text }}
</a>
</li>
</ul>
</li>
</ul>
</nav>
</template>
<style scoped>
.page-toc {
position: sticky;
top: calc(var(--vp-nav-height) + 24px);
max-height: calc(100vh - var(--vp-nav-height) - 48px);
overflow-y: auto;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: var(--vp-radius-lg);
border: 1px solid var(--vp-c-divider);
}
.toc-header {
font-size: var(--vp-font-size-sm);
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.toc-list {
list-style: none;
margin: 0;
padding: 0;
}
.toc-sublist {
margin-left: 12px;
padding-left: 12px;
border-left: 1px solid var(--vp-c-divider);
}
.toc-item {
margin: 4px 0;
}
.toc-link {
display: block;
padding: 4px 8px;
color: var(--vp-c-text-2);
font-size: var(--vp-font-size-sm);
text-decoration: none;
border-left: 2px solid transparent;
transition: all var(--vp-transition-color);
border-radius: 0 var(--vp-radius-sm) var(--vp-radius-sm) 0;
}
.toc-link:hover {
color: var(--vp-c-primary);
background: var(--vp-c-bg-soft);
}
.toc-item.active > .toc-link {
color: var(--vp-c-primary);
border-left-color: var(--vp-c-primary);
background: var(--vp-c-bg-mute);
font-weight: 500;
}
.toc-level-3 .toc-link {
font-size: 13px;
padding-left: 16px;
}
.toc-level-4 .toc-link {
font-size: 12px;
padding-left: 20px;
}
/* Hide on mobile */
@media (max-width: 1024px) {
.page-toc {
display: none;
}
}
/* Scrollbar styling for TOC */
.page-toc::-webkit-scrollbar {
width: 4px;
}
.page-toc::-webkit-scrollbar-track {
background: transparent;
}
.page-toc::-webkit-scrollbar-thumb {
background: var(--vp-c-divider);
border-radius: var(--vp-radius-full);
}
.page-toc::-webkit-scrollbar-thumb:hover {
background: var(--vp-c-text-3);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
/**
* Skip Link Component
* Accessibility feature allowing keyboard users to skip to main content
*/
</script>
<template>
<a href="#VPContent" class="skip-link">
Skip to main content
</a>
</template>
<style scoped>
.skip-link {
position: absolute;
top: -100px;
left: 0;
padding: 8px 16px;
background: var(--vp-c-primary);
color: white;
text-decoration: none;
z-index: 9999;
transition: top 0.3s ease;
font-size: 14px;
font-weight: 500;
}
.skip-link:focus {
top: 0;
}
.skip-link:hover {
background: var(--vp-c-primary-600);
}
@media print {
.skip-link {
display: none;
}
}
</style>

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const currentTheme = ref<'blue' | 'green' | 'orange' | 'purple'>('blue')
const themes = [
{ id: 'blue', name: 'Blue', color: '#3b82f6' },
{ id: 'green', name: 'Green', color: '#10b981' },
{ id: 'orange', name: 'Orange', color: '#f59e0b' },
{ id: 'purple', name: 'Purple', color: '#8b5cf6' }
]
const setTheme = (themeId: typeof currentTheme.value) => {
currentTheme.value = themeId
document.documentElement.setAttribute('data-theme', themeId)
localStorage.setItem('ccw-theme', themeId)
}
onMounted(() => {
const savedTheme = localStorage.getItem('ccw-theme') as typeof currentTheme.value
if (savedTheme && themes.find(t => t.id === savedTheme)) {
setTheme(savedTheme)
}
})
</script>
<template>
<div class="theme-switcher">
<div class="theme-buttons">
<button
v-for="theme in themes"
:key="theme.id"
:class="['theme-button', { active: currentTheme === theme.id }]"
:style="{ '--theme-color': theme.color }"
:aria-label="`Switch to ${theme.name} theme`"
:title="theme.name"
@click="setTheme(theme.id)"
>
<span class="theme-dot"></span>
</button>
</div>
</div>
</template>
<style scoped>
.theme-switcher {
display: flex;
align-items: center;
gap: 8px;
}
.theme-buttons {
display: flex;
gap: 4px;
padding: 4px;
background: var(--vp-c-bg-soft);
border-radius: var(--vp-radius-full);
}
.theme-button {
position: relative;
width: 32px;
height: 32px;
border: none;
background: transparent;
border-radius: var(--vp-radius-full);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--vp-transition-color);
}
.theme-button:hover {
background: var(--vp-c-bg-mute);
}
.theme-button.active {
background: var(--vp-c-bg);
box-shadow: var(--vp-shadow-sm);
}
.theme-dot {
width: 16px;
height: 16px;
border-radius: var(--vp-radius-full);
background: var(--theme-color);
border: 2px solid transparent;
transition: all var(--vp-transition-color);
}
.theme-button.active .theme-dot {
border-color: var(--vp-c-text-1);
transform: scale(1.1);
}
@media (max-width: 768px) {
.theme-button {
width: 36px;
height: 36px;
}
.theme-dot {
width: 20px;
height: 20px;
}
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<div class="workflow-animation">
<div class="workflow-container">
<div class="workflow-node coordinator">
<div class="node-icon">🎯</div>
<div class="node-label">Coordinator</div>
</div>
<div class="workflow-paths">
<svg class="path-svg" viewBox="0 0 400 200">
<!-- Spec Path -->
<path class="flow-path path-spec" d="M50,100 Q150,20 250,50" fill="none" stroke="#3B82F6" stroke-width="2"/>
<circle class="flow-dot dot-spec" r="6" fill="#3B82F6">
<animateMotion dur="3s" repeatCount="indefinite" path="M50,100 Q150,20 250,50"/>
</circle>
<!-- Impl Path -->
<path class="flow-path path-impl" d="M50,100 Q150,100 250,100" fill="none" stroke="#10B981" stroke-width="2"/>
<circle class="flow-dot dot-impl" r="6" fill="#10B981">
<animateMotion dur="2.5s" repeatCount="indefinite" path="M50,100 Q150,100 250,100"/>
</circle>
<!-- Test Path -->
<path class="flow-path path-test" d="M50,100 Q150,180 250,150" fill="none" stroke="#F59E0B" stroke-width="2"/>
<circle class="flow-dot dot-test" r="6" fill="#F59E0B">
<animateMotion dur="3.5s" repeatCount="indefinite" path="M50,100 Q150,180 250,150"/>
</circle>
</svg>
</div>
<div class="workflow-nodes">
<div class="workflow-node analyst">
<div class="node-icon">📊</div>
<div class="node-label">Analyst</div>
</div>
<div class="workflow-node writer">
<div class="node-icon"></div>
<div class="node-label">Writer</div>
</div>
<div class="workflow-node executor">
<div class="node-icon"></div>
<div class="node-label">Executor</div>
</div>
<div class="workflow-node tester">
<div class="node-icon">🧪</div>
<div class="node-label">Tester</div>
</div>
</div>
</div>
<div class="workflow-legend">
<div class="legend-item"><span class="dot spec"></span> Spec Phase</div>
<div class="legend-item"><span class="dot impl"></span> Impl Phase</div>
<div class="legend-item"><span class="dot test"></span> Test Phase</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
// Add animation class after mount
document.querySelector('.workflow-animation')?.classList.add('animate')
})
</script>
<style scoped>
.workflow-animation {
padding: 2rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 24px;
margin: 2rem 0;
overflow: hidden;
}
.workflow-container {
display: flex;
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
gap: 2rem;
min-height: 200px;
}
.workflow-paths {
flex: 1.2;
min-width: 280px;
max-width: 450px;
}
.path-svg {
width: 100%;
height: auto;
}
.flow-path {
stroke-dasharray: 6, 6;
opacity: 0.3;
animation: dash 30s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: -120;
}
}
.flow-dot {
filter: drop-shadow(0 0 4px currentColor);
}
.workflow-nodes {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
flex: 0.8;
min-width: 200px;
}
.workflow-node {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.25rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
box-shadow: var(--vp-shadow-sm);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.workflow-node:hover {
transform: translateY(-4px);
border-color: var(--vp-c-brand-1);
box-shadow: var(--vp-shadow-md);
}
.node-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.node-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.workflow-node.coordinator {
grid-column: span 2;
background: linear-gradient(135deg, var(--vp-c-brand-1), var(--vp-c-brand-2));
border: none;
color: white;
}
.workflow-node.coordinator .node-label {
color: white;
}
.workflow-legend {
display: flex;
justify-content: center;
gap: 2.5rem;
margin-top: 2rem;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.85rem;
font-weight: 500;
color: var(--vp-c-text-2);
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot.spec { background: var(--vp-c-brand-1); }
.dot.impl { background: var(--vp-c-secondary-500); }
.dot.test { background: var(--vp-c-accent-400); }
@media (max-width: 768px) {
.workflow-animation {
padding: 1.5rem;
}
.workflow-container {
flex-direction: column;
}
.workflow-nodes {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,25 @@
import DefaultTheme from 'vitepress/theme'
import ThemeSwitcher from './components/ThemeSwitcher.vue'
import DocSearch from './components/DocSearch.vue'
import DarkModeToggle from './components/DarkModeToggle.vue'
import CopyCodeButton from './components/CopyCodeButton.vue'
import Breadcrumb from './components/Breadcrumb.vue'
import PageToc from './components/PageToc.vue'
import Layout from './layouts/Layout.vue'
import './styles/variables.css'
import './styles/custom.css'
import './styles/mobile.css'
export default {
extends: DefaultTheme,
Layout,
enhanceApp({ app, router, siteData }) {
// Register global components
app.component('ThemeSwitcher', ThemeSwitcher)
app.component('DocSearch', DocSearch)
app.component('DarkModeToggle', DarkModeToggle)
app.component('CopyCodeButton', CopyCodeButton)
app.component('Breadcrumb', Breadcrumb)
app.component('PageToc', PageToc)
}
}

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import { onBeforeUnmount, onMounted } from 'vue'
let mediaQuery: MediaQueryList | null = null
let systemThemeChangeHandler: (() => void) | null = null
let storageHandler: ((e: StorageEvent) => void) | null = null
function applyTheme() {
const savedTheme = localStorage.getItem('ccw-theme') || 'blue'
document.documentElement.setAttribute('data-theme', savedTheme)
}
function applyColorMode() {
const mode = localStorage.getItem('ccw-color-mode') || 'auto'
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const isDark = mode === 'dark' || (mode === 'auto' && prefersDark)
document.documentElement.classList.toggle('dark', isDark)
}
onMounted(() => {
applyTheme()
applyColorMode()
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
systemThemeChangeHandler = () => {
const mode = localStorage.getItem('ccw-color-mode') || 'auto'
if (mode === 'auto') applyColorMode()
}
mediaQuery.addEventListener('change', systemThemeChangeHandler)
storageHandler = (e: StorageEvent) => {
if (e.key === 'ccw-theme') applyTheme()
if (e.key === 'ccw-color-mode') applyColorMode()
}
window.addEventListener('storage', storageHandler)
})
onBeforeUnmount(() => {
if (mediaQuery && systemThemeChangeHandler) {
mediaQuery.removeEventListener('change', systemThemeChangeHandler)
}
if (storageHandler) window.removeEventListener('storage', storageHandler)
})
</script>
<template>
<DefaultTheme.Layout>
<template #home-hero-after>
<div class="hero-extensions">
<div class="hero-stats">
<div class="stat-item">
<div class="stat-value">27+</div>
<div class="stat-label">Built-in Skills</div>
</div>
<div class="stat-item">
<div class="stat-value">10+</div>
<div class="stat-label">Agent Types</div>
</div>
<div class="stat-item">
<div class="stat-value">4</div>
<div class="stat-label">Workflow Levels</div>
</div>
</div>
</div>
</template>
<template #layout-top>
<a href="#VPContent" class="skip-link">Skip to main content</a>
</template>
<template #nav-bar-content-after>
<div class="nav-extensions">
<DocSearch />
<DarkModeToggle />
<ThemeSwitcher />
</div>
</template>
</DefaultTheme.Layout>
</template>
<style scoped>
.hero-extensions {
margin-top: 40px;
text-align: center;
}
.hero-stats {
display: flex;
justify-content: center;
gap: 48px;
flex-wrap: wrap;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: var(--vp-c-primary);
}
.stat-label {
font-size: 14px;
color: var(--vp-c-text-2);
margin-top: 4px;
}
.nav-extensions {
display: flex;
align-items: center;
gap: 12px;
margin-left: auto;
padding-left: 16px;
}
.skip-link {
position: absolute;
top: -100px;
left: 0;
padding: 8px 16px;
background: var(--vp-c-primary);
color: white;
text-decoration: none;
z-index: 9999;
transition: top 0.3s;
}
.skip-link:focus {
top: 0;
}
@media (max-width: 768px) {
.hero-stats {
gap: 24px;
}
.stat-value {
font-size: 24px;
}
.stat-label {
font-size: 12px;
}
.nav-extensions {
gap: 8px;
padding-left: 8px;
}
}
</style>

View File

@@ -0,0 +1,352 @@
/**
* VitePress Custom Styles
* Overrides and extensions for default VitePress theme
* Design System: ui-ux-pro-max — dark-mode-first, developer-focused
*/
/* ============================================
* Global Theme Variables
* ============================================ */
:root {
--vp-c-brand: var(--vp-c-primary);
--vp-c-brand-light: var(--vp-c-primary-300);
--vp-c-brand-lighter: var(--vp-c-primary-200);
--vp-c-brand-dark: var(--vp-c-primary-700);
--vp-c-brand-darker: var(--vp-c-primary-800);
--vp-home-hero-name-color: var(--vp-c-primary);
--vp-home-hero-name-background: linear-gradient(120deg, var(--vp-c-primary-500) 30%, var(--vp-c-secondary-500));
--vp-button-brand-bg: var(--vp-c-primary);
--vp-button-brand-hover-bg: var(--vp-c-primary-600);
--vp-button-brand-active-bg: var(--vp-c-primary-700);
--vp-custom-block-tip-bg: var(--vp-c-primary-50);
--vp-custom-block-tip-border: var(--vp-c-primary-200);
--vp-custom-block-tip-text: var(--vp-c-primary-700);
--vp-custom-block-warning-bg: var(--vp-c-accent-50);
--vp-custom-block-warning-border: var(--vp-c-accent-200);
--vp-custom-block-warning-text: var(--vp-c-accent-700);
--vp-custom-block-danger-bg: #fef2f2;
--vp-custom-block-danger-border: #fecaca;
--vp-custom-block-danger-text: #b91c1c;
/* Layout Width Adjustments */
--vp-layout-max-width: 1600px;
--vp-content-width: 1000px;
--vp-sidebar-width: 272px;
}
.dark {
--vp-custom-block-tip-bg: rgba(59, 130, 246, 0.1);
--vp-custom-block-tip-border: rgba(59, 130, 246, 0.3);
--vp-custom-block-tip-text: var(--vp-c-primary-300);
--vp-custom-block-warning-bg: rgba(217, 119, 6, 0.1);
--vp-custom-block-warning-border: rgba(217, 119, 6, 0.3);
--vp-custom-block-warning-text: var(--vp-c-accent-300);
--vp-custom-block-danger-bg: rgba(185, 28, 28, 0.1);
--vp-custom-block-danger-border: rgba(185, 28, 28, 0.3);
--vp-custom-block-danger-text: #fca5a5;
}
/* ============================================
* Layout Container Adjustments
* ============================================ */
.VPDoc .content-container {
max-width: var(--vp-content-width);
padding: 0 32px;
}
/* Adjust sidebar and content layout */
.VPDoc {
padding-left: var(--vp-sidebar-width);
}
/* Right side outline (TOC) adjustments */
.VPDocOutline {
padding-left: 24px;
}
.VPDocOutline .outline-link {
font-size: 13px;
line-height: 1.6;
padding: 4px 8px;
}
/* ============================================
* Home Page Override
* ============================================ */
.VPHome {
padding-bottom: 0;
}
.VPHomeHero {
padding: 80px 24px;
background: linear-gradient(180deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
}
/* ============================================
* Documentation Content Typography
* ============================================ */
.vp-doc h1 {
font-weight: 800;
letter-spacing: -0.02em;
margin-bottom: 1.5rem;
}
.vp-doc h2 {
font-weight: 700;
margin-top: 3rem;
padding-top: 2rem;
letter-spacing: -0.01em;
border-top: 1px solid var(--vp-c-divider);
}
.vp-doc h2:first-of-type {
margin-top: 1.5rem;
border-top: none;
}
.vp-doc h3 {
font-weight: 600;
margin-top: 2.5rem;
}
.vp-doc h4 {
font-weight: 600;
margin-top: 2rem;
}
.vp-doc p {
line-height: 1.8;
margin: 1.25rem 0;
}
.vp-doc ul,
.vp-doc ol {
margin: 1.25rem 0;
padding-left: 1.5rem;
}
.vp-doc li {
line-height: 1.8;
margin: 0.5rem 0;
}
.vp-doc li + li {
margin-top: 0.5rem;
}
/* Better spacing for code blocks in lists */
.vp-doc li > code {
margin: 0 2px;
}
/* ============================================
* Command Reference Specific Styles
* ============================================ */
.vp-doc h3[id^="ccw"],
.vp-doc h3[id^="workflow"],
.vp-doc h3[id^="issue"],
.vp-doc h3[id^="cli"],
.vp-doc h3[id^="memory"] {
scroll-margin-top: 80px;
position: relative;
}
/* Add subtle separator between command sections */
.vp-doc hr {
border: none;
border-top: 1px solid var(--vp-c-divider);
margin: 3rem 0;
}
/* ============================================
* Custom Container Blocks
* ============================================ */
.custom-container {
margin: 20px 0;
padding: 16px 20px;
border-radius: 12px;
border-left: 4px solid;
}
.custom-container.info {
background: var(--vp-c-bg-soft);
border-color: var(--vp-c-primary);
}
.custom-container.success {
background: var(--vp-c-secondary-50);
border-color: var(--vp-c-secondary);
}
.dark .custom-container.success {
background: rgba(16, 185, 129, 0.1);
}
.custom-container.tip {
border-radius: 12px;
}
.custom-container.warning {
border-radius: 12px;
}
.custom-container.danger {
border-radius: 12px;
}
/* ============================================
* Code Block Improvements
* ============================================ */
.vp-code-group {
margin: 20px 0;
border-radius: 12px;
overflow: hidden;
}
.vp-code-group .tabs {
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.vp-code-group div[class*='language-'] {
margin: 0;
border-radius: 0;
}
div[class*='language-'] {
border-radius: 12px;
margin: 20px 0;
}
div[class*='language-'] pre {
line-height: 1.65;
}
/* Inline code */
.vp-doc :not(pre) > code {
border-radius: 6px;
padding: 2px 6px;
font-size: 0.875em;
font-weight: 500;
}
/* ============================================
* Table Styling
* ============================================ */
table {
border-collapse: collapse;
width: 100%;
margin: 20px 0;
border-radius: 12px;
overflow: hidden;
}
table th,
table td {
padding: 12px 16px;
border: 1px solid var(--vp-c-divider);
text-align: left;
}
table th {
background: var(--vp-c-bg-soft);
font-weight: 600;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--vp-c-text-2);
}
table tr:hover {
background: var(--vp-c-bg-soft);
}
/* ============================================
* Sidebar Polish
* ============================================ */
.VPSidebar .group + .group {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--vp-c-divider);
}
/* ============================================
* Scrollbar Styling
* ============================================ */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--vp-c-surface-2);
border-radius: var(--vp-radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--vp-c-surface-3);
}
/* ============================================
* Link Improvements
* ============================================ */
a {
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
text-decoration: underline;
}
/* ============================================
* Focus States — Accessibility
* ============================================ */
:focus-visible {
outline: 2px solid var(--vp-c-primary);
outline-offset: 2px;
}
/* ============================================
* Skip Link — Accessibility
* ============================================ */
.skip-link {
position: absolute;
top: -100px;
left: 0;
background: var(--vp-c-bg);
padding: 8px 16px;
z-index: 9999;
transition: top 0.3s;
}
.skip-link:focus {
top: 0;
}
/* ============================================
* Print Styles
* ============================================ */
@media print {
.VPNav,
.VPSidebar,
.skip-link {
display: none;
}
.VPContent {
margin: 0 !important;
padding: 0 !important;
}
}

View File

@@ -0,0 +1,346 @@
/**
* Mobile-Responsive Styles
* Breakpoints: 320px-768px (mobile), 768px-1024px (tablet), 1024px+ (desktop)
* WCAG 2.1 AA compliant
*/
/* ============================================
* Mobile First Approach
* ============================================ */
/* Base Mobile Styles (320px+) */
@media (max-width: 768px) {
/* Typography */
:root {
--vp-font-size-base: 14px;
--vp-content-width: 100%;
}
/* Container */
.container {
padding: 0 16px;
}
/* Navigation */
.VPNav {
height: 56px;
}
.VPNavBar {
padding: 0 16px;
}
/* Sidebar */
.VPSidebar {
width: 100%;
max-width: 320px;
}
/* Content */
.VPContent {
padding: 16px;
}
/* Doc content adjustments */
.VPDoc .content-container {
padding: 0 16px;
}
/* Hide outline on mobile */
.VPDocOutline {
display: none;
}
/* Hero Section */
.VPHomeHero {
padding: 40px 16px;
}
.VPHomeHero h1 {
font-size: 28px;
line-height: 1.2;
}
.VPHomeHero p {
font-size: 14px;
}
/* Code Blocks */
div[class*='language-'] {
margin: 12px -16px;
border-radius: 0;
}
div[class*='language-'] pre {
padding: 12px 16px;
font-size: 12px;
}
/* Tables - make them scrollable */
.vp-doc table {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
font-size: 12px;
}
table th,
table td {
padding: 8px 12px;
}
/* Buttons */
.VPButton {
padding: 8px 16px;
font-size: 14px;
}
/* Cards */
.VPFeature {
padding: 16px;
}
/* Touch-friendly tap targets (min 44x44px per WCAG) */
button,
a,
input,
select,
textarea {
min-height: 44px;
min-width: 44px;
}
/* Search */
.DocSearch {
width: 100%;
}
/* Theme Switcher */
.theme-switcher {
padding: 12px;
}
/* Breadcrumbs */
.breadcrumb {
padding: 8px 0;
font-size: 12px;
}
/* Table of Contents - hidden on mobile */
.page-toc {
display: none;
}
/* Typography adjustments for mobile */
.vp-doc h1 {
font-size: 1.75rem;
margin-bottom: 1rem;
}
.vp-doc h2 {
font-size: 1.375rem;
margin-top: 2rem;
padding-top: 1.5rem;
}
.vp-doc h3 {
font-size: 1.125rem;
margin-top: 1.5rem;
}
.vp-doc p {
line-height: 1.7;
margin: 1rem 0;
}
.vp-doc ul,
.vp-doc ol {
margin: 1rem 0;
padding-left: 1.25rem;
}
.vp-doc li {
margin: 0.375rem 0;
}
}
/* ============================================
* Tablet Styles (768px - 1024px)
* ============================================ */
@media (min-width: 768px) and (max-width: 1024px) {
:root {
--vp-content-width: 760px;
--vp-sidebar-width: 240px;
}
.VPContent {
padding: 24px;
}
.VPDoc .content-container {
padding: 0 24px;
max-width: var(--vp-content-width);
}
.VPHomeHero {
padding: 60px 24px;
}
.VPHomeHero h1 {
font-size: 36px;
}
div[class*='language-'] {
margin: 12px 0;
}
/* Outline visible but narrower */
.VPDocOutline {
width: 200px;
padding-left: 16px;
}
.VPDocOutline .outline-link {
font-size: 12px;
}
}
/* ============================================
* Desktop Styles (1024px+)
* ============================================ */
@media (min-width: 1024px) {
:root {
--vp-layout-max-width: 1600px;
--vp-content-width: 960px;
--vp-sidebar-width: 272px;
}
.VPContent {
padding: 32px 48px;
max-width: var(--vp-layout-max-width);
}
.VPDoc .content-container {
max-width: var(--vp-content-width);
padding: 0 40px;
}
/* Outline - sticky on desktop with good width */
.VPDocOutline {
position: sticky;
top: calc(var(--vp-nav-height) + 24px);
width: 256px;
padding-left: 24px;
max-height: calc(100vh - var(--vp-nav-height) - 48px);
overflow-y: auto;
}
.VPDocOutline .outline-marker {
display: block;
}
.VPDocOutline .outline-link {
font-size: 13px;
line-height: 1.6;
padding: 4px 12px;
transition: color 0.2s ease;
}
.VPDocOutline .outline-link:hover {
color: var(--vp-c-primary);
}
/* Two-column layout for content + TOC */
.content-with-toc {
display: grid;
grid-template-columns: 1fr 280px;
gap: 32px;
}
}
/* ============================================
* Large Desktop (1440px+)
* ============================================ */
@media (min-width: 1440px) {
:root {
--vp-content-width: 1040px;
--vp-sidebar-width: 280px;
}
.VPDoc .content-container {
padding: 0 48px;
}
.VPDocOutline {
width: 280px;
}
}
/* ============================================
* Landscape Orientation
* ============================================ */
@media (max-height: 500px) and (orientation: landscape) {
.VPNav {
height: 48px;
}
.VPHomeHero {
padding: 20px 16px;
}
}
/* ============================================
* High DPI Displays
* ============================================ */
@media (-webkit-min-device-pixel-ratio: 2),
(min-resolution: 192dpi) {
/* Optimize images for retina displays */
img {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
}
/* ============================================
* Reduced Motion (Accessibility)
* ============================================ */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* ============================================
* Dark Mode Specific
* ============================================ */
@media (max-width: 768px) {
.dark {
--vp-c-bg: #0f172a;
--vp-c-text-1: #f1f5f9;
}
}
/* ============================================
* Print Styles for Mobile
* ============================================ */
@media print and (max-width: 768px) {
.VPContent {
font-size: 10pt;
}
h1 {
font-size: 14pt;
}
h2 {
font-size: 12pt;
}
}

View File

@@ -0,0 +1,221 @@
/**
* Design Tokens for CCW Documentation
* Based on ui-ux-pro-max design system
* 8 themes: 4 colors × 2 modes (light/dark)
*/
:root {
/* ============================================
* Color Scheme: Blue (Default)
* ============================================ */
/* Primary Colors */
--vp-c-primary-50: #eff6ff;
--vp-c-primary-100: #dbeafe;
--vp-c-primary-200: #bfdbfe;
--vp-c-primary-300: #93c5fd;
--vp-c-primary-400: #60a5fa;
--vp-c-primary-500: #3b82f6;
--vp-c-primary-600: #2563eb;
--vp-c-primary-700: #1d4ed8;
--vp-c-primary-800: #1e40af;
--vp-c-primary-900: #1e3a8a;
--vp-c-primary: var(--vp-c-primary-500);
--vp-c-brand-1: var(--vp-c-primary-500);
--vp-c-brand-2: var(--vp-c-secondary-500);
--vp-c-brand-soft: rgba(59, 130, 246, 0.1);
/* Secondary Colors (Green) */
--vp-c-secondary-50: #ecfdf5;
--vp-c-secondary-100: #d1fae5;
--vp-c-secondary-200: #a7f3d0;
--vp-c-secondary-300: #6ee7b7;
--vp-c-secondary-400: #34d399;
--vp-c-secondary-500: #10b981;
--vp-c-secondary-600: #059669;
--vp-c-secondary-700: #047857;
--vp-c-secondary-800: #065f46;
--vp-c-secondary-900: #064e3b;
--vp-c-secondary: var(--vp-c-secondary-500);
/* Accent Colors */
--vp-c-accent-50: #fef3c7;
--vp-c-accent-100: #fde68a;
--vp-c-accent-200: #fcd34d;
--vp-c-accent-300: #fbbf24;
--vp-c-accent-400: #f59e0b;
--vp-c-accent-500: #d97706;
--vp-c-accent: var(--vp-c-accent-400);
/* Background Colors (Light Mode) */
--vp-c-bg: #ffffff;
--vp-c-bg-soft: #f9fafb;
--vp-c-bg-mute: #f3f4f6;
--vp-c-bg-alt: #ffffff;
/* Surface Colors */
--vp-c-surface: #f9fafb;
--vp-c-surface-1: #f3f4f6;
--vp-c-surface-2: #e5e7eb;
--vp-c-surface-3: #d1d5db;
/* Border Colors */
--vp-c-border: #e5e7eb;
--vp-c-border-soft: #f3f4f6;
--vp-c-divider: #e5e7eb;
/* Text Colors */
--vp-c-text-1: #111827;
--vp-c-text-2: #374151;
--vp-c-text-3: #6b7280;
--vp-c-text-4: #9ca3af;
--vp-c-text-code: #ef4444;
/* Typography */
--vp-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
--vp-font-family-mono: 'Fira Code', 'Cascadia Code', 'JetBrains Mono', Consolas, 'Courier New', monospace;
/* Font Sizes */
--vp-font-size-base: 16px;
--vp-font-size-sm: 14px;
--vp-font-size-lg: 18px;
--vp-font-size-xl: 20px;
/* Spacing */
--vp-spacing-xs: 0.25rem; /* 4px */
--vp-spacing-sm: 0.5rem; /* 8px */
--vp-spacing-md: 1rem; /* 16px */
--vp-spacing-lg: 1.5rem; /* 24px */
--vp-spacing-xl: 2rem; /* 32px */
--vp-spacing-2xl: 3rem; /* 48px */
/* Border Radius */
--vp-radius-sm: 0.25rem; /* 4px */
--vp-radius-md: 0.375rem; /* 6px */
--vp-radius-lg: 0.5rem; /* 8px */
--vp-radius-xl: 0.75rem; /* 12px */
--vp-radius-full: 9999px;
/* Shadows */
--vp-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--vp-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--vp-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--vp-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
/* Transitions */
--vp-transition-color: 0.2s ease;
--vp-transition-transform: 0.2s ease;
--vp-transition-all: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* Z-Index */
--vp-z-index-base: 1;
--vp-z-index-dropdown: 10;
--vp-z-index-sticky: 20;
--vp-z-index-fixed: 50;
--vp-z-index-modal: 100;
--vp-z-index-toast: 200;
}
/* ============================================
* Dark Mode
* ============================================ */
.dark {
/* Background Colors (Dark Mode) */
--vp-c-bg: #111827;
--vp-c-bg-soft: #1f2937;
--vp-c-bg-mute: #374151;
--vp-c-bg-alt: #0f172a;
/* Surface Colors */
--vp-c-surface: #1f2937;
--vp-c-surface-1: #374151;
--vp-c-surface-2: #4b5563;
--vp-c-surface-3: #6b7280;
/* Border Colors */
--vp-c-border: #374151;
--vp-c-border-soft: #1f2937;
--vp-c-divider: #374151;
/* Text Colors */
--vp-c-text-1: #f9fafb;
--vp-c-text-2: #e5e7eb;
--vp-c-text-3: #d1d5db;
--vp-c-text-4: #9ca3af;
--vp-c-text-code: #fca5a5;
/* Primary Colors (adjusted for dark mode) */
--vp-c-primary-50: #1e3a8a;
--vp-c-primary-100: #1e40af;
--vp-c-primary-200: #1d4ed8;
--vp-c-primary-300: #2563eb;
--vp-c-primary-400: #3b82f6;
--vp-c-primary-500: #60a5fa;
--vp-c-primary-600: #93c5fd;
--vp-c-primary-700: #bfdbfe;
--vp-c-primary-800: #dbeafe;
--vp-c-primary-900: #eff6ff;
--vp-c-primary: var(--vp-c-primary-400);
--vp-c-brand-1: var(--vp-c-primary-400);
--vp-c-brand-2: var(--vp-c-secondary-400);
--vp-c-brand-soft: rgba(96, 165, 250, 0.2);
/* Secondary Colors (adjusted for dark mode) */
--vp-c-secondary-50: #064e3b;
--vp-c-secondary-100: #065f46;
--vp-c-secondary-200: #047857;
--vp-c-secondary-300: #059669;
--vp-c-secondary-400: #10b981;
--vp-c-secondary-500: #34d399;
--vp-c-secondary-600: #6ee7b7;
--vp-c-secondary-700: #a7f3d0;
--vp-c-secondary-800: #d1fae5;
--vp-c-secondary-900: #ecfdf5;
--vp-c-secondary: var(--vp-c-secondary-400);
}
/* ============================================
* Color Scheme: Green
* ============================================ */
[data-theme="green"] {
--vp-c-primary: var(--vp-c-secondary-500);
}
[data-theme="green"].dark {
--vp-c-primary: var(--vp-c-secondary-400);
}
/* ============================================
* Color Scheme: Orange
* ============================================ */
[data-theme="orange"] {
--vp-c-primary: var(--vp-c-accent-500);
}
[data-theme="orange"].dark {
--vp-c-primary: var(--vp-c-accent-400);
}
/* ============================================
* Color Scheme: Purple
* ============================================ */
[data-theme="purple"] {
--vp-c-primary-500: #8b5cf6;
--vp-c-primary-600: #7c3aed;
--vp-c-primary-700: #6d28d9;
--vp-c-primary: var(--vp-c-primary-500);
}
[data-theme="purple"].dark {
--vp-c-primary-400: #a78bfa;
--vp-c-primary-500: #8b5cf6;
--vp-c-primary: var(--vp-c-primary-400);
}
/* ============================================
* Utility Classes
* ============================================ */
.text-primary { color: var(--vp-c-primary); }
.text-secondary { color: var(--vp-c-secondary); }
.bg-surface { background-color: var(--vp-c-surface); }
.border-primary { border-color: var(--vp-c-primary); }
.rounded-md { border-radius: var(--vp-radius-md); }
.shadow-md { box-shadow: var(--vp-shadow-md); }
.transition { transition: var(--vp-transition-all); }