diff --git a/.claude/skills/software-manual/scripts/assemble_docsify.py b/.claude/skills/software-manual/scripts/assemble_docsify.py
new file mode 100644
index 00000000..d99974d3
--- /dev/null
+++ b/.claude/skills/software-manual/scripts/assemble_docsify.py
@@ -0,0 +1,584 @@
+#!/usr/bin/env python3
+"""
+Docsify-Style HTML Manual Assembly Script Template
+Generates interactive single-file documentation with hierarchical navigation
+
+Usage:
+ 1. Copy this script to your manual output directory
+ 2. Customize MANUAL_META and NAV_STRUCTURE
+ 3. Run: python assemble_docsify.py
+"""
+
+import json
+import base64
+import re
+from pathlib import Path
+from typing import Dict, List, Any
+
+# Try to import markdown library
+try:
+ import markdown
+ from markdown.extensions.codehilite import CodeHiliteExtension
+ from markdown.extensions.fenced_code import FencedCodeExtension
+ from markdown.extensions.tables import TableExtension
+ from markdown.extensions.toc import TocExtension
+ HAS_MARKDOWN = True
+except ImportError:
+ HAS_MARKDOWN = False
+ print("Warning: markdown library not found. Install with: pip install markdown pygments")
+
+
+# ============================================================
+# CONFIGURATION - Customize these for your project
+# ============================================================
+
+# Paths - Update these paths for your environment
+BASE_DIR = Path(__file__).parent
+SECTIONS_DIR = BASE_DIR / "sections"
+SCREENSHOTS_DIR = BASE_DIR / "screenshots"
+
+# Template paths - Point to skill templates directory
+SKILL_DIR = Path(__file__).parent.parent # Adjust based on where script is placed
+TEMPLATE_FILE = SKILL_DIR / "templates" / "docsify-shell.html"
+CSS_BASE_FILE = SKILL_DIR / "templates" / "css" / "docsify-base.css"
+
+# Manual metadata - Customize for your software
+MANUAL_META = {
+ "title": "Your Software",
+ "subtitle": "使用手册",
+ "version": "v1.0.0",
+ "timestamp": "2025-01-01",
+ "language": "zh-CN",
+ "logo_icon": "Y" # First letter or emoji
+}
+
+# Output file
+OUTPUT_FILE = BASE_DIR / f"{MANUAL_META['title']}{MANUAL_META['subtitle']}.html"
+
+# Hierarchical navigation structure
+# Customize groups and items based on your sections
+NAV_STRUCTURE = [
+ {
+ "type": "group",
+ "title": "入门指南",
+ "icon": "📚",
+ "expanded": True,
+ "items": [
+ {"id": "overview", "title": "产品概述", "file": "section-overview.md"},
+ ]
+ },
+ {
+ "type": "group",
+ "title": "使用教程",
+ "icon": "🎯",
+ "expanded": False,
+ "items": [
+ {"id": "ui-guide", "title": "UI操作指南", "file": "section-ui-guide.md"},
+ ]
+ },
+ {
+ "type": "group",
+ "title": "API参考",
+ "icon": "🔧",
+ "expanded": False,
+ "items": [
+ {"id": "api-reference", "title": "API文档", "file": "section-api-reference.md"},
+ ]
+ },
+ {
+ "type": "group",
+ "title": "配置与部署",
+ "icon": "⚙️",
+ "expanded": False,
+ "items": [
+ {"id": "configuration", "title": "配置指南", "file": "section-configuration.md"},
+ ]
+ },
+ {
+ "type": "group",
+ "title": "帮助与支持",
+ "icon": "💡",
+ "expanded": False,
+ "items": [
+ {"id": "troubleshooting", "title": "故障排除", "file": "section-troubleshooting.md"},
+ {"id": "examples", "title": "代码示例", "file": "section-examples.md"},
+ ]
+ }
+]
+
+# Screenshot ID to filename mapping - Customize for your screenshots
+SCREENSHOT_MAPPING = {
+ # "截图ID": "filename.png",
+}
+
+
+# ============================================================
+# CORE FUNCTIONS - Generally don't need to modify
+# ============================================================
+
+# Global cache for embedded images
+_embedded_images = {}
+
+
+def read_file(filepath: Path) -> str:
+ """Read file content with UTF-8 encoding"""
+ return filepath.read_text(encoding='utf-8')
+
+
+# ============================================================
+# MERMAID VALIDATION
+# ============================================================
+
+# Valid Mermaid diagram types
+MERMAID_DIAGRAM_TYPES = [
+ 'graph', 'flowchart', 'sequenceDiagram', 'classDiagram',
+ 'stateDiagram', 'stateDiagram-v2', 'erDiagram', 'journey',
+ 'gantt', 'pie', 'quadrantChart', 'requirementDiagram',
+ 'gitGraph', 'mindmap', 'timeline', 'zenuml', 'sankey-beta',
+ 'xychart-beta', 'block-beta'
+]
+
+# Common Mermaid syntax patterns
+MERMAID_PATTERNS = {
+ 'graph': r'^graph\s+(TB|BT|LR|RL|TD)\s*$',
+ 'flowchart': r'^flowchart\s+(TB|BT|LR|RL|TD)\s*$',
+ 'sequenceDiagram': r'^sequenceDiagram\s*$',
+ 'classDiagram': r'^classDiagram\s*$',
+ 'stateDiagram': r'^stateDiagram(-v2)?\s*$',
+ 'erDiagram': r'^erDiagram\s*$',
+ 'gantt': r'^gantt\s*$',
+ 'pie': r'^pie\s*(showData|title\s+.*)?\s*$',
+ 'journey': r'^journey\s*$',
+}
+
+
+class MermaidBlock:
+ """Represents a mermaid code block found in markdown"""
+ def __init__(self, content: str, file: str, line_num: int, indented: bool = False):
+ self.content = content
+ self.file = file
+ self.line_num = line_num
+ self.indented = indented
+ self.errors: List[str] = []
+ self.warnings: List[str] = []
+ self.diagram_type: str = None
+
+ def __repr__(self):
+ return f"MermaidBlock({self.diagram_type}, {self.file}:{self.line_num})"
+
+
+def extract_mermaid_blocks(markdown_text: str, filename: str) -> List[MermaidBlock]:
+ """Extract all mermaid code blocks from markdown text"""
+ blocks = []
+
+ # More flexible pattern - matches opening fence with optional indent,
+ # then captures content until closing fence (with any indent)
+ pattern = r'^(\s*)(```|~~~)mermaid\s*\n(.*?)\n\s*\2\s*$'
+
+ for match in re.finditer(pattern, markdown_text, re.MULTILINE | re.DOTALL):
+ indent = match.group(1)
+ content = match.group(3)
+ # Calculate line number
+ line_num = markdown_text[:match.start()].count('\n') + 1
+ indented = len(indent) > 0
+
+ block = MermaidBlock(
+ content=content,
+ file=filename,
+ line_num=line_num,
+ indented=indented
+ )
+ blocks.append(block)
+
+ return blocks
+
+
+def validate_mermaid_block(block: MermaidBlock) -> bool:
+ """Validate a mermaid block and populate errors/warnings"""
+ content = block.content.strip()
+ lines = content.split('\n')
+
+ if not lines:
+ block.errors.append("Empty mermaid block")
+ return False
+
+ first_line = lines[0].strip()
+
+ # Detect diagram type
+ for dtype in MERMAID_DIAGRAM_TYPES:
+ if first_line.startswith(dtype):
+ block.diagram_type = dtype
+ break
+
+ if not block.diagram_type:
+ block.errors.append(f"Unknown diagram type: '{first_line[:30]}...'")
+ block.errors.append(f"Valid types: {', '.join(MERMAID_DIAGRAM_TYPES[:8])}...")
+ return False
+
+ # Check for balanced brackets/braces
+ brackets = {'[': ']', '{': '}', '(': ')'}
+ stack = []
+ for i, char in enumerate(content):
+ if char in brackets:
+ stack.append((char, i))
+ elif char in brackets.values():
+ if not stack:
+ block.errors.append(f"Unmatched closing bracket '{char}' at position {i}")
+ else:
+ open_char, _ = stack.pop()
+ if brackets[open_char] != char:
+ block.errors.append(f"Mismatched brackets: '{open_char}' and '{char}'")
+
+ if stack:
+ for open_char, pos in stack:
+ block.warnings.append(f"Unclosed bracket '{open_char}' at position {pos}")
+
+ # Check for common graph/flowchart issues
+ if block.diagram_type in ['graph', 'flowchart']:
+ # Check direction specifier
+ if not re.match(r'^(graph|flowchart)\s+(TB|BT|LR|RL|TD)', first_line):
+ block.warnings.append("Missing or invalid direction (TB/BT/LR/RL/TD)")
+
+ # Check for arrow syntax
+ arrow_count = content.count('-->') + content.count('---') + content.count('-.->') + content.count('==>')
+ if arrow_count == 0 and len(lines) > 1:
+ block.warnings.append("No arrows found - graph may be incomplete")
+
+ # Check for sequenceDiagram issues
+ if block.diagram_type == 'sequenceDiagram':
+ if '->' not in content and '->>' not in content:
+ block.warnings.append("No message arrows found in sequence diagram")
+
+ # Indentation warning
+ if block.indented:
+ block.warnings.append("Indented code block - may not render in some markdown parsers")
+
+ return len(block.errors) == 0
+
+
+def validate_all_mermaid(nav_structure: List[Dict], sections_dir: Path) -> Dict[str, Any]:
+ """Validate all mermaid blocks in all section files"""
+ report = {
+ 'total_blocks': 0,
+ 'valid_blocks': 0,
+ 'error_blocks': 0,
+ 'warning_blocks': 0,
+ 'blocks': [],
+ 'by_file': {},
+ 'by_type': {}
+ }
+
+ for group in nav_structure:
+ for item in group.get("items", []):
+ section_file = item.get("file")
+ if not section_file:
+ continue
+
+ filepath = sections_dir / section_file
+ if not filepath.exists():
+ continue
+
+ content = read_file(filepath)
+ blocks = extract_mermaid_blocks(content, section_file)
+
+ file_report = {'blocks': [], 'errors': 0, 'warnings': 0}
+
+ for block in blocks:
+ report['total_blocks'] += 1
+ is_valid = validate_mermaid_block(block)
+
+ if is_valid:
+ report['valid_blocks'] += 1
+ else:
+ report['error_blocks'] += 1
+ file_report['errors'] += 1
+
+ if block.warnings:
+ report['warning_blocks'] += 1
+ file_report['warnings'] += len(block.warnings)
+
+ # Track by diagram type
+ if block.diagram_type:
+ if block.diagram_type not in report['by_type']:
+ report['by_type'][block.diagram_type] = 0
+ report['by_type'][block.diagram_type] += 1
+
+ report['blocks'].append(block)
+ file_report['blocks'].append(block)
+
+ if blocks:
+ report['by_file'][section_file] = file_report
+
+ return report
+
+
+def print_mermaid_report(report: Dict[str, Any]) -> None:
+ """Print mermaid validation report"""
+ print("\n" + "=" * 60)
+ print("MERMAID DIAGRAM VALIDATION REPORT")
+ print("=" * 60)
+
+ print(f"\nSummary:")
+ print(f" Total blocks: {report['total_blocks']}")
+ print(f" Valid: {report['valid_blocks']}")
+ print(f" With errors: {report['error_blocks']}")
+ print(f" With warnings: {report['warning_blocks']}")
+
+ if report['by_type']:
+ print(f"\nDiagram Types:")
+ for dtype, count in sorted(report['by_type'].items()):
+ print(f" {dtype}: {count}")
+
+ # Print errors and warnings
+ has_issues = False
+ for block in report['blocks']:
+ if block.errors or block.warnings:
+ if not has_issues:
+ print(f"\nIssues Found:")
+ has_issues = True
+
+ print(f"\n [{block.file}:{block.line_num}] {block.diagram_type or 'unknown'}")
+ for error in block.errors:
+ print(f" [ERROR] {error}")
+ for warning in block.warnings:
+ print(f" [WARN] {warning}")
+
+ if not has_issues:
+ print(f"\n No issues found!")
+
+ print("=" * 60 + "\n")
+
+
+def convert_md_to_html(markdown_text: str) -> str:
+ """Convert Markdown to HTML with syntax highlighting"""
+ if not HAS_MARKDOWN:
+ # Fallback: just escape HTML and wrap in pre
+ escaped = markdown_text.replace('&', '&').replace('<', '<').replace('>', '>')
+ return f'
{escaped}'
+
+ md = markdown.Markdown(
+ extensions=[
+ FencedCodeExtension(),
+ TableExtension(),
+ TocExtension(toc_depth=3),
+ CodeHiliteExtension(
+ css_class='highlight',
+ linenums=False,
+ guess_lang=True,
+ use_pygments=True
+ ),
+ ],
+ output_format='html5'
+ )
+ html = md.convert(markdown_text)
+ md.reset()
+ return html
+
+
+def embed_screenshot_base64(screenshot_id: str) -> str:
+ """Embed screenshot as base64, using cache to avoid duplicates"""
+ global _embedded_images
+
+ filename = SCREENSHOT_MAPPING.get(screenshot_id)
+
+ if not filename:
+ return f'📷 {screenshot_id}
'
+
+ filepath = SCREENSHOTS_DIR / filename
+
+ if not filepath.exists():
+ return f'📷 {screenshot_id}
'
+
+ # Check cache
+ if filename not in _embedded_images:
+ try:
+ with open(filepath, 'rb') as f:
+ image_data = base64.b64encode(f.read()).decode('utf-8')
+ ext = filepath.suffix[1:].lower()
+ _embedded_images[filename] = f"data:image/{ext};base64,{image_data}"
+ except Exception as e:
+ return f'📷 {screenshot_id} (加载失败)
'
+
+ return f'''
+
+ {screenshot_id}
+ '''
+
+
+def process_markdown_screenshots(markdown_text: str) -> str:
+ """Replace [[screenshot:xxx]] placeholders with embedded images"""
+ pattern = r'\[\[screenshot:(.*?)\]\]'
+
+ def replacer(match):
+ screenshot_id = match.group(1)
+ return embed_screenshot_base64(screenshot_id)
+
+ return re.sub(pattern, replacer, markdown_text)
+
+
+def generate_sidebar_nav_html(nav_structure: List[Dict]) -> str:
+ """Generate hierarchical sidebar navigation HTML"""
+ html_parts = []
+
+ for group in nav_structure:
+ if group["type"] == "group":
+ expanded_class = "expanded" if group.get("expanded", False) else ""
+ html_parts.append(f'''
+
+
+
''')
+
+ for item in group.get("items", []):
+ html_parts.append(f'''
+
{item['title']}''')
+
+ html_parts.append('''
+
+
''')
+
+ return '\n'.join(html_parts)
+
+
+def generate_sections_html(nav_structure: List[Dict]) -> str:
+ """Generate content sections HTML"""
+ sections_html = []
+
+ for group in nav_structure:
+ for item in group.get("items", []):
+ section_id = item["id"]
+ section_title = item["title"]
+ section_file = item.get("file")
+
+ if not section_file:
+ continue
+
+ filepath = SECTIONS_DIR / section_file
+ if not filepath.exists():
+ print(f"Warning: Section file not found: {filepath}")
+ continue
+
+ # Read and convert markdown
+ markdown_content = read_file(filepath)
+ markdown_content = process_markdown_screenshots(markdown_content)
+ html_content = convert_md_to_html(markdown_content)
+
+ sections_html.append(f'''
+ ''')
+
+ return '\n'.join(sections_html)
+
+
+def generate_search_index(nav_structure: List[Dict]) -> str:
+ """Generate search index JSON"""
+ search_index = {}
+
+ for group in nav_structure:
+ for item in group.get("items", []):
+ section_id = item["id"]
+ section_file = item.get("file")
+
+ if not section_file:
+ continue
+
+ filepath = SECTIONS_DIR / section_file
+ if filepath.exists():
+ content = read_file(filepath)
+ clean_content = re.sub(r'[#*`\[\]()]', '', content)
+ clean_content = re.sub(r'\s+', ' ', clean_content)[:1500]
+
+ search_index[section_id] = {
+ "title": item["title"],
+ "body": clean_content,
+ "group": group["title"]
+ }
+
+ return json.dumps(search_index, ensure_ascii=False, indent=2)
+
+
+def generate_nav_structure_json(nav_structure: List[Dict]) -> str:
+ """Generate navigation structure JSON for client-side"""
+ return json.dumps(nav_structure, ensure_ascii=False, indent=2)
+
+
+def assemble_manual(validate_mermaid: bool = True):
+ """Main assembly function
+
+ Args:
+ validate_mermaid: Whether to validate mermaid diagrams (default: True)
+ """
+ global _embedded_images
+ _embedded_images = {}
+
+ full_title = f"{MANUAL_META['title']} {MANUAL_META['subtitle']}"
+ print(f"Assembling Docsify-style manual: {full_title}")
+
+ # Verify template exists
+ if not TEMPLATE_FILE.exists():
+ print(f"Error: Template not found at {TEMPLATE_FILE}")
+ print("Please update TEMPLATE_FILE path in this script.")
+ return None, 0
+
+ if not CSS_BASE_FILE.exists():
+ print(f"Error: CSS not found at {CSS_BASE_FILE}")
+ print("Please update CSS_BASE_FILE path in this script.")
+ return None, 0
+
+ # Validate Mermaid diagrams
+ mermaid_report = None
+ if validate_mermaid:
+ print("\nValidating Mermaid diagrams...")
+ mermaid_report = validate_all_mermaid(NAV_STRUCTURE, SECTIONS_DIR)
+ print_mermaid_report(mermaid_report)
+
+ # Warn if there are errors (but continue)
+ if mermaid_report['error_blocks'] > 0:
+ print(f"[WARN] {mermaid_report['error_blocks']} mermaid block(s) have errors!")
+ print(" These diagrams may not render correctly.")
+
+ # Read template and CSS
+ template_html = read_file(TEMPLATE_FILE)
+ css_content = read_file(CSS_BASE_FILE)
+
+ # Generate components
+ sidebar_nav_html = generate_sidebar_nav_html(NAV_STRUCTURE)
+ sections_html = generate_sections_html(NAV_STRUCTURE)
+ search_index_json = generate_search_index(NAV_STRUCTURE)
+ nav_structure_json = generate_nav_structure_json(NAV_STRUCTURE)
+
+ # Replace placeholders
+ output_html = template_html
+ output_html = output_html.replace('{{SOFTWARE_NAME}}', full_title)
+ output_html = output_html.replace('{{VERSION}}', MANUAL_META['version'])
+ output_html = output_html.replace('{{TIMESTAMP}}', MANUAL_META['timestamp'])
+ output_html = output_html.replace('{{LOGO_ICON}}', MANUAL_META['logo_icon'])
+ output_html = output_html.replace('{{EMBEDDED_CSS}}', css_content)
+ output_html = output_html.replace('{{SIDEBAR_NAV_HTML}}', sidebar_nav_html)
+ output_html = output_html.replace('{{SECTIONS_HTML}}', sections_html)
+ output_html = output_html.replace('{{SEARCH_INDEX_JSON}}', search_index_json)
+ output_html = output_html.replace('{{NAV_STRUCTURE_JSON}}', nav_structure_json)
+
+ # Write output file
+ OUTPUT_FILE.write_text(output_html, encoding='utf-8')
+
+ file_size = OUTPUT_FILE.stat().st_size
+ file_size_mb = file_size / (1024 * 1024)
+ section_count = sum(len(g.get("items", [])) for g in NAV_STRUCTURE)
+
+ print("[OK] Docsify-style manual generated successfully!")
+ print(f" Output: {OUTPUT_FILE}")
+ print(f" Size: {file_size_mb:.2f} MB ({file_size:,} bytes)")
+ print(f" Navigation Groups: {len(NAV_STRUCTURE)}")
+ print(f" Sections: {section_count}")
+
+ return str(OUTPUT_FILE), file_size
+
+
+if __name__ == "__main__":
+ output_path, size = assemble_manual()
diff --git a/.claude/skills/software-manual/templates/css/docsify-base.css b/.claude/skills/software-manual/templates/css/docsify-base.css
new file mode 100644
index 00000000..906baa4a
--- /dev/null
+++ b/.claude/skills/software-manual/templates/css/docsify-base.css
@@ -0,0 +1,984 @@
+/* ========================================
+ Docsify-Style Documentation CSS
+ Software Manual Skill - Modern Theme
+ ======================================== */
+
+/* ========== CSS Variables ========== */
+:root {
+ /* Light Theme - Teal Accent */
+ --bg-color: #ffffff;
+ --bg-secondary: #f8fafc;
+ --bg-tertiary: #f1f5f9;
+ --text-color: #1e293b;
+ --text-secondary: #64748b;
+ --text-muted: #94a3b8;
+ --border-color: #e2e8f0;
+ --accent-color: #14b8a6;
+ --accent-hover: #0d9488;
+ --accent-light: rgba(20, 184, 166, 0.1);
+ --link-color: #14b8a6;
+ --sidebar-bg: #ffffff;
+ --sidebar-width: 280px;
+ --code-bg: #1e293b;
+ --code-color: #e2e8f0;
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
+
+ /* Callout Colors */
+ --tip-bg: rgba(20, 184, 166, 0.08);
+ --tip-border: #14b8a6;
+ --warning-bg: rgba(245, 158, 11, 0.08);
+ --warning-border: #f59e0b;
+ --danger-bg: rgba(239, 68, 68, 0.08);
+ --danger-border: #ef4444;
+ --info-bg: rgba(59, 130, 246, 0.08);
+ --info-border: #3b82f6;
+
+ /* Typography */
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif;
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
+ --font-size-xs: 0.75rem;
+ --font-size-sm: 0.875rem;
+ --font-size-base: 1rem;
+ --font-size-lg: 1.125rem;
+ --line-height: 1.75;
+
+ /* Spacing */
+ --space-xs: 0.25rem;
+ --space-sm: 0.5rem;
+ --space-md: 1rem;
+ --space-lg: 1.5rem;
+ --space-xl: 2rem;
+ --space-2xl: 3rem;
+
+ /* Border Radius */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+
+ /* Transitions */
+ --transition: 0.2s ease;
+ --transition-slow: 0.3s ease;
+}
+
+/* Dark Theme */
+[data-theme="dark"] {
+ --bg-color: #0f172a;
+ --bg-secondary: #1e293b;
+ --bg-tertiary: #334155;
+ --text-color: #f1f5f9;
+ --text-secondary: #94a3b8;
+ --text-muted: #64748b;
+ --border-color: #334155;
+ --sidebar-bg: #1e293b;
+ --code-bg: #0f172a;
+ --code-color: #e2e8f0;
+ --tip-bg: rgba(20, 184, 166, 0.15);
+ --warning-bg: rgba(245, 158, 11, 0.15);
+ --danger-bg: rgba(239, 68, 68, 0.15);
+ --info-bg: rgba(59, 130, 246, 0.15);
+}
+
+/* ========== Reset ========== */
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html, body {
+ height: 100%;
+}
+
+body {
+ font-family: var(--font-family);
+ font-size: var(--font-size-base);
+ line-height: var(--line-height);
+ color: var(--text-color);
+ background-color: var(--bg-color);
+ -webkit-font-smoothing: antialiased;
+}
+
+/* ========== Layout ========== */
+.docsify-container {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* ========== Sidebar ========== */
+.sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: var(--sidebar-width);
+ height: 100vh;
+ background: var(--sidebar-bg);
+ border-right: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+ z-index: 100;
+ transition: transform var(--transition);
+}
+
+.sidebar-header {
+ padding: var(--space-lg);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+.logo-icon {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, var(--accent-color), #3eaf7c);
+ border-radius: 8px;
+ color: #fff;
+ font-weight: bold;
+ font-size: 1.25rem;
+}
+
+.logo-text h1 {
+ font-size: var(--font-size-base);
+ font-weight: 600;
+ color: var(--text-color);
+ margin: 0;
+ line-height: 1.2;
+}
+
+.logo-text .version {
+ font-size: var(--font-size-sm);
+ color: var(--text-muted);
+}
+
+/* ========== Search ========== */
+.sidebar-search {
+ padding: var(--space-md);
+ position: relative;
+}
+
+.search-box {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.search-icon {
+ position: absolute;
+ left: 10px;
+ color: var(--text-muted);
+ pointer-events: none;
+}
+
+.search-box input {
+ width: 100%;
+ padding: 10px 60px 10px 36px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ font-size: var(--font-size-sm);
+ background: var(--bg-secondary);
+ color: var(--text-color);
+ transition: all var(--transition);
+}
+
+.search-box input:focus {
+ outline: none;
+ border-color: var(--accent-color);
+ box-shadow: 0 0 0 3px var(--accent-light);
+ background: var(--bg-color);
+}
+
+.search-box input::placeholder {
+ color: var(--text-muted);
+}
+
+/* Keyboard shortcut hint */
+.search-box::after {
+ content: 'Ctrl K';
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: var(--font-size-xs);
+ color: var(--text-muted);
+ background: var(--bg-color);
+ padding: 2px 6px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border-color);
+ font-family: var(--font-mono);
+ pointer-events: none;
+}
+
+.search-results {
+ position: absolute;
+ top: 100%;
+ left: var(--space-md);
+ right: var(--space-md);
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ box-shadow: var(--shadow-lg);
+ max-height: 400px;
+ overflow-y: auto;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-4px);
+ transition: all var(--transition);
+ z-index: 200;
+}
+
+.search-results.visible {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.search-result-item {
+ display: block;
+ padding: var(--space-sm) var(--space-md);
+ text-decoration: none;
+ color: var(--text-color);
+ border-bottom: 1px solid var(--border-color);
+ transition: background var(--transition);
+}
+
+.search-result-item:last-child {
+ border-bottom: none;
+}
+
+.search-result-item:hover {
+ background: var(--bg-secondary);
+}
+
+.result-title {
+ font-weight: 600;
+ font-size: var(--font-size-sm);
+ margin-bottom: 2px;
+}
+
+.result-excerpt {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ line-height: 1.4;
+}
+
+.result-excerpt mark {
+ background: var(--accent-light);
+ color: var(--accent-color);
+ padding: 1px 4px;
+ border-radius: var(--radius-sm);
+ font-weight: 500;
+}
+
+.no-results {
+ padding: var(--space-md);
+ text-align: center;
+ color: var(--text-muted);
+ font-size: var(--font-size-sm);
+}
+
+/* ========== Sidebar Navigation ========== */
+.sidebar-nav {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--space-md) 0;
+}
+
+.nav-group {
+ margin-bottom: var(--space-xs);
+}
+
+.nav-group-header {
+ display: flex;
+ align-items: center;
+ padding: var(--space-sm) var(--space-md);
+ cursor: pointer;
+ user-select: none;
+ transition: background var(--transition);
+}
+
+.nav-group-header:hover {
+ background: var(--bg-secondary);
+}
+
+.nav-group-toggle {
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: var(--space-xs);
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: transform var(--transition);
+}
+
+.nav-group-toggle svg {
+ width: 12px;
+ height: 12px;
+}
+
+.nav-group.expanded .nav-group-toggle {
+ transform: rotate(90deg);
+}
+
+.nav-group-title {
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ color: var(--text-color);
+}
+
+.nav-group-items {
+ display: none;
+ padding-left: var(--space-lg);
+}
+
+.nav-group.expanded .nav-group-items {
+ display: block;
+}
+
+.nav-item {
+ display: block;
+ padding: 8px var(--space-md) 8px calc(var(--space-md) + 4px);
+ font-size: var(--font-size-sm);
+ color: var(--text-secondary);
+ text-decoration: none;
+ border-left: 2px solid transparent;
+ margin: 2px 8px 2px 0;
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
+ transition: all var(--transition);
+ cursor: pointer;
+}
+
+.nav-item:hover {
+ color: var(--text-color);
+ background: var(--bg-secondary);
+}
+
+.nav-item.active {
+ color: var(--accent-color);
+ border-left-color: var(--accent-color);
+ background: var(--accent-light);
+ font-weight: 500;
+}
+
+/* Top-level nav items (no group) */
+.nav-item.top-level {
+ padding-left: var(--space-md);
+ border-left: none;
+ margin: 2px 8px;
+ border-radius: var(--radius-md);
+}
+
+.nav-item.top-level.active {
+ background: var(--accent-light);
+}
+
+/* ========== Main Content ========== */
+.main-content {
+ flex: 1;
+ margin-left: var(--sidebar-width);
+ min-height: 100vh;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.mobile-header {
+ display: none;
+ position: sticky;
+ top: 0;
+ padding: var(--space-sm) var(--space-md);
+ background: var(--bg-color);
+ border-bottom: 1px solid var(--border-color);
+ z-index: 50;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+.sidebar-toggle {
+ background: none;
+ border: none;
+ padding: var(--space-xs);
+ color: var(--text-color);
+ cursor: pointer;
+ border-radius: 4px;
+ transition: background var(--transition);
+}
+
+.sidebar-toggle:hover {
+ background: var(--bg-secondary);
+}
+
+.current-section {
+ flex: 1;
+ font-weight: 600;
+ font-size: var(--font-size-sm);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.theme-toggle-mobile {
+ background: none;
+ border: none;
+ padding: var(--space-xs);
+ font-size: 1.25rem;
+ cursor: pointer;
+}
+
+/* ========== Content Sections ========== */
+.content-wrapper {
+ flex: 1;
+ max-width: 860px;
+ margin: 0 auto;
+ padding: var(--space-2xl) var(--space-xl);
+ width: 100%;
+}
+
+.content-section {
+ display: none;
+ animation: fadeIn 0.3s ease;
+}
+
+.content-section.active {
+ display: block;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ========== Content Typography ========== */
+.content-section h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-bottom: var(--space-lg);
+ padding-bottom: var(--space-md);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.content-section h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-top: var(--space-2xl);
+ margin-bottom: var(--space-md);
+ padding-bottom: var(--space-sm);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.content-section h3 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-top: var(--space-xl);
+ margin-bottom: var(--space-sm);
+}
+
+.content-section h4 {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-top: var(--space-lg);
+ margin-bottom: var(--space-sm);
+}
+
+.content-section p {
+ margin-bottom: var(--space-md);
+}
+
+.content-section a {
+ color: var(--link-color);
+ text-decoration: none;
+}
+
+.content-section a:hover {
+ text-decoration: underline;
+}
+
+/* Lists */
+.content-section ul,
+.content-section ol {
+ margin: var(--space-md) 0;
+ padding-left: var(--space-xl);
+}
+
+.content-section li {
+ margin-bottom: var(--space-sm);
+}
+
+.content-section li::marker {
+ color: var(--accent-color);
+}
+
+/* Inline Code */
+.content-section code {
+ font-family: var(--font-mono);
+ font-size: 0.85em;
+ padding: 3px 8px;
+ background: var(--bg-tertiary);
+ color: var(--accent-color);
+ border-radius: var(--radius-sm);
+ font-weight: 500;
+}
+
+/* Code Blocks */
+.code-block-wrapper {
+ position: relative;
+ margin: var(--space-lg) 0;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow-md);
+}
+
+.content-section pre {
+ margin: 0;
+ padding: var(--space-lg);
+ padding-top: calc(var(--space-lg) + 40px);
+ background: var(--code-bg);
+ overflow-x: auto;
+ border-radius: var(--radius-lg);
+}
+
+.content-section pre code {
+ display: block;
+ padding: 0;
+ background: none;
+ color: var(--code-color);
+ font-size: var(--font-size-sm);
+ line-height: 1.7;
+}
+
+/* Code Block Header */
+.code-block-wrapper::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 40px;
+ background: rgba(255,255,255,0.03);
+ border-bottom: 1px solid rgba(255,255,255,0.05);
+}
+
+/* Code Block Actions */
+.code-block-actions {
+ position: absolute;
+ top: 8px;
+ right: 12px;
+ display: flex;
+ gap: 8px;
+ z-index: 10;
+}
+
+.copy-code-btn {
+ position: absolute;
+ top: 8px;
+ right: 12px;
+ padding: 6px 12px;
+ background: rgba(255,255,255,0.08);
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: var(--radius-md);
+ color: var(--code-color);
+ cursor: pointer;
+ opacity: 0;
+ transition: all var(--transition);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: var(--font-size-xs);
+ font-family: var(--font-family);
+}
+
+.code-block-wrapper:hover .copy-code-btn {
+ opacity: 1;
+}
+
+.copy-code-btn:hover {
+ background: rgba(255,255,255,0.15);
+ border-color: rgba(255,255,255,0.2);
+}
+
+.copy-code-btn.copied {
+ background: var(--accent-color);
+ border-color: var(--accent-color);
+ color: #fff;
+}
+
+/* Code syntax colors */
+.content-section pre .keyword { color: #c678dd; }
+.content-section pre .string { color: #98c379; }
+.content-section pre .number { color: #d19a66; }
+.content-section pre .comment { color: #5c6370; font-style: italic; }
+.content-section pre .function { color: #61afef; }
+.content-section pre .operator { color: #56b6c2; }
+
+/* Tables */
+.content-section table {
+ width: 100%;
+ margin: var(--space-lg) 0;
+ border-collapse: collapse;
+ font-size: var(--font-size-sm);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+}
+
+.content-section th {
+ padding: var(--space-md);
+ background: var(--accent-color);
+ color: #fff;
+ font-weight: 600;
+ text-align: left;
+ font-size: var(--font-size-sm);
+ letter-spacing: 0.02em;
+}
+
+.content-section th:first-child {
+ border-top-left-radius: var(--radius-md);
+}
+
+.content-section th:last-child {
+ border-top-right-radius: var(--radius-md);
+}
+
+.content-section td {
+ padding: var(--space-sm) var(--space-md);
+ border-bottom: 1px solid var(--border-color);
+ vertical-align: top;
+}
+
+.content-section tbody tr:nth-child(even) {
+ background: var(--bg-secondary);
+}
+
+.content-section tbody tr:hover {
+ background: var(--accent-light);
+}
+
+.content-section tbody tr:last-child td {
+ border-bottom: none;
+}
+
+.content-section tbody tr:last-child td:first-child {
+ border-bottom-left-radius: var(--radius-md);
+}
+
+.content-section tbody tr:last-child td:last-child {
+ border-bottom-right-radius: var(--radius-md);
+}
+
+/* Blockquote / Callouts */
+.content-section blockquote {
+ position: relative;
+ margin: var(--space-lg) 0;
+ padding: var(--space-md) var(--space-lg);
+ padding-left: calc(var(--space-lg) + 32px);
+ background: var(--tip-bg);
+ border: 1px solid var(--tip-border);
+ border-radius: var(--radius-lg);
+}
+
+.content-section blockquote::before {
+ content: '💡';
+ position: absolute;
+ left: var(--space-md);
+ top: var(--space-md);
+ font-size: 1.25rem;
+ line-height: 1;
+}
+
+.content-section blockquote p:last-child {
+ margin-bottom: 0;
+}
+
+.content-section blockquote p:first-child {
+ font-weight: 500;
+ color: var(--text-color);
+}
+
+/* Warning callout */
+.content-section blockquote.warning,
+.content-section blockquote:has(strong:first-child:contains("警告")),
+.content-section blockquote:has(strong:first-child:contains("Warning")) {
+ background: var(--warning-bg);
+ border-color: var(--warning-border);
+}
+
+.content-section blockquote.warning::before {
+ content: '⚠️';
+}
+
+/* Danger callout */
+.content-section blockquote.danger,
+.content-section blockquote:has(strong:first-child:contains("危险")),
+.content-section blockquote:has(strong:first-child:contains("Danger")) {
+ background: var(--danger-bg);
+ border-color: var(--danger-border);
+}
+
+.content-section blockquote.danger::before {
+ content: '🚨';
+}
+
+/* Info callout */
+.content-section blockquote.info,
+.content-section blockquote:has(strong:first-child:contains("注意")),
+.content-section blockquote:has(strong:first-child:contains("Note")) {
+ background: var(--info-bg);
+ border-color: var(--info-border);
+}
+
+.content-section blockquote.info::before {
+ content: 'ℹ️';
+}
+
+/* Images */
+.content-section img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 8px;
+ box-shadow: var(--shadow-md);
+ margin: var(--space-md) 0;
+}
+
+.screenshot-placeholder {
+ padding: var(--space-xl);
+ background: var(--bg-secondary);
+ border: 2px dashed var(--border-color);
+ border-radius: 8px;
+ text-align: center;
+ color: var(--text-muted);
+ margin: var(--space-md) 0;
+}
+
+/* ========== Footer ========== */
+.main-footer {
+ padding: var(--space-lg);
+ text-align: center;
+ color: var(--text-muted);
+ font-size: var(--font-size-sm);
+ border-top: 1px solid var(--border-color);
+ margin-top: auto;
+}
+
+/* ========== Theme Toggle (Desktop) ========== */
+.theme-toggle {
+ position: fixed;
+ bottom: var(--space-lg);
+ right: var(--space-lg);
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ border: 1px solid var(--border-color);
+ background: var(--bg-color);
+ box-shadow: var(--shadow-md);
+ cursor: pointer;
+ font-size: 1.25rem;
+ z-index: 100;
+ transition: transform var(--transition);
+}
+
+.theme-toggle:hover {
+ transform: scale(1.1);
+}
+
+[data-theme="light"] .moon-icon { display: inline; }
+[data-theme="light"] .sun-icon { display: none; }
+[data-theme="dark"] .moon-icon { display: none; }
+[data-theme="dark"] .sun-icon { display: inline; }
+
+/* ========== Back to Top ========== */
+.back-to-top {
+ position: fixed;
+ bottom: calc(var(--space-lg) + 56px);
+ right: var(--space-lg);
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 1px solid var(--border-color);
+ background: var(--bg-color);
+ box-shadow: var(--shadow-md);
+ color: var(--text-secondary);
+ cursor: pointer;
+ opacity: 0;
+ visibility: hidden;
+ transition: all var(--transition);
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.back-to-top.visible {
+ opacity: 1;
+ visibility: visible;
+}
+
+.back-to-top:hover {
+ color: var(--accent-color);
+ border-color: var(--accent-color);
+}
+
+/* ========== Responsive ========== */
+@media (max-width: 960px) {
+ .sidebar {
+ transform: translateX(-100%);
+ }
+
+ .sidebar.open {
+ transform: translateX(0);
+ box-shadow: var(--shadow-lg);
+ }
+
+ .main-content {
+ margin-left: 0;
+ }
+
+ .mobile-header {
+ display: flex;
+ }
+
+ .content-wrapper {
+ padding: var(--space-lg);
+ }
+
+ .theme-toggle {
+ display: none;
+ }
+}
+
+@media (max-width: 640px) {
+ .content-section h1 {
+ font-size: 1.5rem;
+ }
+
+ .content-section h2 {
+ font-size: 1.25rem;
+ }
+
+ .content-wrapper {
+ padding: var(--space-md);
+ }
+}
+
+/* ========== Print Styles ========== */
+@media print {
+ .sidebar,
+ .mobile-header,
+ .theme-toggle,
+ .back-to-top,
+ .copy-code-btn {
+ display: none !important;
+ }
+
+ .main-content {
+ margin-left: 0;
+ }
+
+ .content-section {
+ display: block !important;
+ page-break-after: always;
+ }
+
+ .content-section pre {
+ background: #f5f5f5 !important;
+ color: #333 !important;
+ }
+}
+
+
+/* ========== Pygments Syntax Highlighting (One Dark Theme) ========== */
+/* Generated for CodeHilite extension */
+.highlight { background: #282c34; border-radius: 8px; padding: 1em; overflow-x: auto; margin: var(--spacing-md) 0; }
+.highlight pre { margin: 0; background: transparent; padding: 0; }
+.highlight code { background: transparent; border: none; padding: 0; color: #abb2bf; font-size: var(--font-size-sm); }
+
+/* Pygments Token Colors - One Dark Theme */
+.highlight .hll { background-color: #3e4451; }
+.highlight .c { color: #5c6370; font-style: italic; } /* Comment */
+.highlight .err { color: #e06c75; } /* Error */
+.highlight .k { color: #c678dd; } /* Keyword */
+.highlight .l { color: #98c379; } /* Literal */
+.highlight .n { color: #abb2bf; } /* Name */
+.highlight .o { color: #56b6c2; } /* Operator */
+.highlight .p { color: #abb2bf; } /* Punctuation */
+.highlight .ch { color: #5c6370; font-style: italic; } /* Comment.Hashbang */
+.highlight .cm { color: #5c6370; font-style: italic; } /* Comment.Multiline */
+.highlight .cp { color: #5c6370; font-style: italic; } /* Comment.Preproc */
+.highlight .cpf { color: #5c6370; font-style: italic; } /* Comment.PreprocFile */
+.highlight .c1 { color: #5c6370; font-style: italic; } /* Comment.Single */
+.highlight .cs { color: #5c6370; font-style: italic; } /* Comment.Special */
+.highlight .gd { color: #e06c75; } /* Generic.Deleted */
+.highlight .ge { font-style: italic; } /* Generic.Emph */
+.highlight .gh { color: #abb2bf; font-weight: bold; } /* Generic.Heading */
+.highlight .gi { color: #98c379; } /* Generic.Inserted */
+.highlight .go { color: #5c6370; } /* Generic.Output */
+.highlight .gp { color: #5c6370; } /* Generic.Prompt */
+.highlight .gs { font-weight: bold; } /* Generic.Strong */
+.highlight .gu { color: #56b6c2; font-weight: bold; } /* Generic.Subheading */
+.highlight .gt { color: #e06c75; } /* Generic.Traceback */
+.highlight .kc { color: #c678dd; } /* Keyword.Constant */
+.highlight .kd { color: #c678dd; } /* Keyword.Declaration */
+.highlight .kn { color: #c678dd; } /* Keyword.Namespace */
+.highlight .kp { color: #c678dd; } /* Keyword.Pseudo */
+.highlight .kr { color: #c678dd; } /* Keyword.Reserved */
+.highlight .kt { color: #e5c07b; } /* Keyword.Type */
+.highlight .ld { color: #98c379; } /* Literal.Date */
+.highlight .m { color: #d19a66; } /* Literal.Number */
+.highlight .s { color: #98c379; } /* Literal.String */
+.highlight .na { color: #d19a66; } /* Name.Attribute */
+.highlight .nb { color: #e5c07b; } /* Name.Builtin */
+.highlight .nc { color: #e5c07b; } /* Name.Class */
+.highlight .no { color: #d19a66; } /* Name.Constant */
+.highlight .nd { color: #e5c07b; } /* Name.Decorator */
+.highlight .ni { color: #abb2bf; } /* Name.Entity */
+.highlight .ne { color: #e06c75; } /* Name.Exception */
+.highlight .nf { color: #61afef; } /* Name.Function */
+.highlight .nl { color: #abb2bf; } /* Name.Label */
+.highlight .nn { color: #e5c07b; } /* Name.Namespace */
+.highlight .nx { color: #abb2bf; } /* Name.Other */
+.highlight .py { color: #abb2bf; } /* Name.Property */
+.highlight .nt { color: #e06c75; } /* Name.Tag */
+.highlight .nv { color: #e06c75; } /* Name.Variable */
+.highlight .ow { color: #56b6c2; } /* Operator.Word */
+.highlight .w { color: #abb2bf; } /* Text.Whitespace */
+.highlight .mb { color: #d19a66; } /* Literal.Number.Bin */
+.highlight .mf { color: #d19a66; } /* Literal.Number.Float */
+.highlight .mh { color: #d19a66; } /* Literal.Number.Hex */
+.highlight .mi { color: #d19a66; } /* Literal.Number.Integer */
+.highlight .mo { color: #d19a66; } /* Literal.Number.Oct */
+.highlight .sa { color: #98c379; } /* Literal.String.Affix */
+.highlight .sb { color: #98c379; } /* Literal.String.Backtick */
+.highlight .sc { color: #98c379; } /* Literal.String.Char */
+.highlight .dl { color: #98c379; } /* Literal.String.Delimiter */
+.highlight .sd { color: #98c379; } /* Literal.String.Doc */
+.highlight .s2 { color: #98c379; } /* Literal.String.Double */
+.highlight .se { color: #d19a66; } /* Literal.String.Escape */
+.highlight .sh { color: #98c379; } /* Literal.String.Heredoc */
+.highlight .si { color: #98c379; } /* Literal.String.Interpol */
+.highlight .sx { color: #98c379; } /* Literal.String.Other */
+.highlight .sr { color: #56b6c2; } /* Literal.String.Regex */
+.highlight .s1 { color: #98c379; } /* Literal.String.Single */
+.highlight .ss { color: #56b6c2; } /* Literal.String.Symbol */
+.highlight .bp { color: #e5c07b; } /* Name.Builtin.Pseudo */
+.highlight .fm { color: #61afef; } /* Name.Function.Magic */
+.highlight .vc { color: #e06c75; } /* Name.Variable.Class */
+.highlight .vg { color: #e06c75; } /* Name.Variable.Global */
+.highlight .vi { color: #e06c75; } /* Name.Variable.Instance */
+.highlight .vm { color: #e06c75; } /* Name.Variable.Magic */
+.highlight .il { color: #d19a66; } /* Literal.Number.Integer.Long */
+
+/* Dark theme override for highlight */
+[data-theme="dark"] .highlight {
+ background: #1e2128;
+ border: 1px solid #3d4450;
+}
diff --git a/.claude/skills/software-manual/templates/docsify-shell.html b/.claude/skills/software-manual/templates/docsify-shell.html
new file mode 100644
index 00000000..38511124
--- /dev/null
+++ b/.claude/skills/software-manual/templates/docsify-shell.html
@@ -0,0 +1,466 @@
+
+
+
+
+
+
+
+ {{SOFTWARE_NAME}} v{{VERSION}} - User Manual
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{SECTIONS_HTML}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css b/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css
index 51bee099..9a53b713 100644
--- a/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css
+++ b/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css
@@ -91,6 +91,71 @@
color: hsl(var(--warning-foreground, white));
}
+/* ===== Search Box ===== */
+.cli-stream-search {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ background: hsl(var(--background));
+ border: 1px solid hsl(var(--border));
+ border-radius: 6px;
+ transition: all 0.2s;
+}
+
+.cli-stream-search:focus-within {
+ border-color: hsl(var(--primary));
+ box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1);
+}
+
+.cli-stream-search-input {
+ width: 140px;
+ padding: 2px 4px;
+ background: transparent;
+ border: none;
+ outline: none;
+ font-size: 0.75rem;
+ color: hsl(var(--foreground));
+}
+
+.cli-stream-search-input::placeholder {
+ color: hsl(var(--muted-foreground));
+}
+
+.cli-stream-search-icon {
+ width: 14px;
+ height: 14px;
+ color: hsl(var(--muted-foreground));
+ flex-shrink: 0;
+}
+
+.cli-stream-search-clear {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ background: transparent;
+ border: none;
+ border-radius: 50%;
+ font-size: 0.75rem;
+ color: hsl(var(--muted-foreground));
+ cursor: pointer;
+ opacity: 0;
+ transition: all 0.15s;
+}
+
+.cli-stream-search:focus-within .cli-stream-search-clear,
+.cli-stream-search-input:not(:placeholder-shown) + .cli-stream-search-clear {
+ opacity: 1;
+}
+
+.cli-stream-search-clear:hover {
+ background: hsl(var(--muted));
+ color: hsl(var(--foreground));
+}
+
.cli-stream-actions {
display: flex;
align-items: center;
@@ -334,6 +399,26 @@
color: hsl(200 80% 70%);
}
+/* Search highlight */
+.cli-stream-highlight {
+ background: hsl(50 100% 50% / 0.4);
+ color: inherit;
+ padding: 0 2px;
+ border-radius: 2px;
+}
+
+/* Filter result info */
+.cli-stream-filter-info {
+ display: inline-block;
+ padding: 4px 10px;
+ margin-bottom: 8px;
+ background: hsl(var(--primary) / 0.15);
+ color: hsl(var(--primary));
+ border-radius: 4px;
+ font-size: 0.6875rem;
+ font-weight: 500;
+}
+
/* Auto-scroll indicator */
.cli-stream-scroll-btn {
position: sticky;
diff --git a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js
index d12036a9..ea893749 100644
--- a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js
+++ b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js
@@ -8,6 +8,7 @@ let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, s
let activeStreamTab = null;
let autoScrollEnabled = true;
let isCliStreamViewerOpen = false;
+let searchFilter = ''; // Search filter for output content
const MAX_OUTPUT_LINES = 5000; // Prevent memory issues
@@ -230,9 +231,9 @@ function renderStreamTabs() {
function renderStreamContent(executionId) {
const contentContainer = document.getElementById('cliStreamContent');
if (!contentContainer) return;
-
+
const exec = executionId ? cliStreamExecutions[executionId] : null;
-
+
if (!exec) {
// Show empty state
contentContainer.innerHTML = `
@@ -245,20 +246,43 @@ function renderStreamContent(executionId) {
if (typeof lucide !== 'undefined') lucide.createIcons();
return;
}
-
+
// Check if should auto-scroll
const wasAtBottom = contentContainer.scrollHeight - contentContainer.scrollTop <= contentContainer.clientHeight + 50;
-
- // Render output lines
- contentContainer.innerHTML = exec.output.map(line =>
- `${escapeHtml(line.content)}
`
- ).join('');
-
+
+ // Filter output lines based on search
+ let filteredOutput = exec.output;
+ if (searchFilter.trim()) {
+ const searchLower = searchFilter.toLowerCase();
+ filteredOutput = exec.output.filter(line =>
+ line.content.toLowerCase().includes(searchLower)
+ );
+ }
+
+ // Render output lines with search highlighting
+ contentContainer.innerHTML = filteredOutput.map(line => {
+ let content = escapeHtml(line.content);
+ // Highlight search matches
+ if (searchFilter.trim()) {
+ const searchRegex = new RegExp(`(${escapeRegex(searchFilter)})`, 'gi');
+ content = content.replace(searchRegex, '$1');
+ }
+ return `${content}
`;
+ }).join('');
+
+ // Show filter result count if filtering
+ if (searchFilter.trim() && filteredOutput.length !== exec.output.length) {
+ const filterInfo = document.createElement('div');
+ filterInfo.className = 'cli-stream-filter-info';
+ filterInfo.textContent = `${filteredOutput.length} / ${exec.output.length} lines`;
+ contentContainer.insertBefore(filterInfo, contentContainer.firstChild);
+ }
+
// Auto-scroll if enabled and was at bottom
if (autoScrollEnabled && wasAtBottom) {
contentContainer.scrollTop = contentContainer.scrollHeight;
}
-
+
// Update status bar
renderStreamStatus(executionId);
}
@@ -408,14 +432,14 @@ function handleStreamContentScroll() {
// ===== Helper Functions =====
function formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
-
+
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
-
+
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
-
+
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
@@ -428,6 +452,25 @@ function escapeHtml(text) {
return div.innerHTML;
}
+function escapeRegex(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+// ===== Search Functions =====
+function handleSearchInput(event) {
+ searchFilter = event.target.value;
+ renderStreamContent(activeStreamTab);
+}
+
+function clearSearch() {
+ searchFilter = '';
+ const searchInput = document.getElementById('cliStreamSearchInput');
+ if (searchInput) {
+ searchInput.value = '';
+ }
+ renderStreamContent(activeStreamTab);
+}
+
// Translation helper with fallback (uses global t from i18n.js)
function _streamT(key) {
// First try global t() from i18n.js
@@ -459,3 +502,16 @@ if (document.readyState === 'loading') {
} else {
initCliStreamViewer();
}
+
+// ===== Global Exposure =====
+window.toggleCliStreamViewer = toggleCliStreamViewer;
+window.handleCliStreamStarted = handleCliStreamStarted;
+window.handleCliStreamOutput = handleCliStreamOutput;
+window.handleCliStreamCompleted = handleCliStreamCompleted;
+window.handleCliStreamError = handleCliStreamError;
+window.switchStreamTab = switchStreamTab;
+window.closeStream = closeStream;
+window.clearCompletedStreams = clearCompletedStreams;
+window.toggleAutoScroll = toggleAutoScroll;
+window.handleSearchInput = handleSearchInput;
+window.clearSearch = clearSearch;