From b59e810e76d8f7b063e957a308680c6ddff37d9d Mon Sep 17 00:00:00 2001 From: kyasuda Date: Tue, 10 Feb 2026 13:20:59 -0800 Subject: [PATCH] docs: add Mermaid architecture diagrams and VitePress renderer --- docs/.vitepress/theme/index.ts | 75 +++++++++++++++++++++++++++++++++- docs/architecture.md | 40 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index a8ae85d..cb9f3f2 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,4 +1,77 @@ import DefaultTheme from 'vitepress/theme'; +import { useRoute } from 'vitepress'; +import { nextTick, onMounted, watch } from 'vue'; import '@catppuccin/vitepress/theme/macchiato/mauve.css'; -export default DefaultTheme; +let mermaidLoader: Promise | null = null; + +async function getMermaid() { + if (!mermaidLoader) { + mermaidLoader = import( + /* @vite-ignore */ 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs' + ).then((mod) => { + const mermaid = mod.default ?? mod; + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'loose', + }); + return mermaid; + }); + } + return mermaidLoader; +} + +async function renderMermaidBlocks() { + if (typeof document === 'undefined') { + return; + } + const blocks = Array.from( + document.querySelectorAll('div.language-mermaid'), + ); + if (blocks.length === 0) { + return; + } + + const mermaid = await getMermaid(); + const nodes: HTMLElement[] = []; + + for (const block of blocks) { + if (block.dataset.mermaidRendered === 'true') { + continue; + } + const code = block.querySelector('pre code'); + const source = code?.textContent?.trim(); + if (!source) { + continue; + } + + const mount = document.createElement('div'); + mount.className = 'mermaid'; + mount.textContent = source; + + block.replaceChildren(mount); + block.dataset.mermaidRendered = 'true'; + nodes.push(mount); + } + + if (nodes.length > 0) { + await mermaid.run({ nodes }); + } +} + +export default { + ...DefaultTheme, + setup() { + const route = useRoute(); + const render = () => { + nextTick(() => { + renderMermaidBlocks().catch((error) => { + console.error('Failed to render Mermaid diagram:', error); + }); + }); + }; + + onMounted(render); + watch(() => route.path, render); + }, +}; diff --git a/docs/architecture.md b/docs/architecture.md index 07908a3..b18cf18 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -35,6 +35,33 @@ SubMiner uses a service-oriented Electron main-process architecture where `src/m - `src/jimaku/*`, `src/subsync/*` - Domain-specific integration helpers. +## Flow Diagram + +```mermaid +flowchart TD + Main["src/main.ts\n(composition root)"] --> Startup["runStartupBootstrapRuntimeService"] + Main --> Lifecycle["startAppLifecycleService"] + Lifecycle --> AppReady["runAppReadyRuntimeService"] + + Main --> OverlayMgr["overlay-manager-service"] + Main --> Ipc["ipc-service / ipc-command-service"] + Main --> Mpv["mpv-service / mpv-control-service"] + Main --> Shortcuts["shortcut-service / overlay-shortcut-service"] + Main --> RuntimeOpts["runtime-options-ipc-service"] + Main --> Subtitle["subtitle-ws-service / secondary-subtitle-service"] + + Main --> Config["src/config/*"] + Main --> Cli["src/cli/*"] + Main --> Trackers["src/window-trackers/*"] + Main --> Integrations["src/jimaku/* + src/subsync/*"] + + OverlayMgr --> OverlayWindow["overlay-window-service"] + OverlayMgr --> OverlayVisibility["overlay-visibility-service"] + Mpv --> Subtitle + Ipc --> RuntimeOpts + Shortcuts --> OverlayMgr +``` + ## Composition Pattern Most runtime code follows a dependency-injection pattern: @@ -59,6 +86,19 @@ This keeps side effects explicit and makes behavior easy to unit-test with fakes - Shutdown: - `startAppLifecycleService` registers cleanup hooks (`will-quit`) while teardown behavior stays delegated to focused services from `main.ts`. +```mermaid +flowchart LR + Args["CLI args"] --> Bootstrap["runStartupBootstrapRuntimeService"] + Bootstrap -->|generate-config| Exit["exit"] + Bootstrap -->|normal start| AppLifecycle["startAppLifecycleService"] + AppLifecycle --> Ready["runAppReadyRuntimeService"] + Ready --> Runtime["IPC + shortcuts + mpv events"] + Runtime --> Overlay["overlay visibility + mining actions"] + Runtime --> Subsync["subsync + secondary sub flows"] + Runtime --> WillQuit["app will-quit"] + WillQuit --> Cleanup["service-level cleanup + unregister"] +``` + ## Why This Design - Smaller blast radius: changing one feature usually touches one service.