diff --git a/README.md b/README.md index a0109c1e..6e1098f5 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ pnpm test packages/tools # tools package ### Format ```bash -npm run format # format all files +npm run format # format source/config files npm run format:check # check format (CI) ``` diff --git a/package.json b/package.json index a6ac0084..5c44cf18 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "web:dev": "pnpm --filter @memo-code/web dev", "web:build": "pnpm --filter @memo-code/web build", "web:start": "pnpm --filter @memo-code/web start", - "format": "prettier --write .", - "format:check": "prettier --check .", + "format": "prettier --write \"{packages,web}/**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,mdx}\" \"{package.json,pnpm-workspace.yaml,tsconfig.json,tsup.config.ts,vitest.config.ts,vitest.setup.ts,.prettierrc}\"", + "format:check": "prettier --check \"{packages,web}/**/*.{ts,tsx,js,jsx,mjs,cjs,json,css,mdx}\" \"{package.json,pnpm-workspace.yaml,tsconfig.json,tsup.config.ts,vitest.config.ts,vitest.setup.ts,.prettierrc}\"", "test": "vitest run", "test:core": "vitest run packages/core", "test:tools": "vitest run packages/tools", diff --git a/packages/cli/src/tui/components/layout/HeaderBar.tsx b/packages/cli/src/tui/components/layout/HeaderBar.tsx index c859a7cc..3aaafa6b 100644 --- a/packages/cli/src/tui/components/layout/HeaderBar.tsx +++ b/packages/cli/src/tui/components/layout/HeaderBar.tsx @@ -1,38 +1,12 @@ import { Box, Text } from 'ink' import { memo } from 'react' -import os from 'node:os' type HeaderBarProps = { providerName: string model: string - cwd: string - sessionId?: string } -function formatCwd(cwd: string) { - const home = os.homedir() - if (!home) return cwd - return cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd -} - -function formatSessionId(id: string | undefined): string { - if (!id) return 'N/A' - // Show first 8 chars and last 4 chars - if (id.length > 16) { - return `${id.slice(0, 8)}...${id.slice(-4)}` - } - return id -} - -export const HeaderBar = memo(function HeaderBar({ - providerName, - model, - cwd, - sessionId, -}: HeaderBarProps) { - const displayCwd = formatCwd(cwd) - const displaySession = formatSessionId(sessionId) - +export const HeaderBar = memo(function HeaderBar({ providerName, model }: HeaderBarProps) { return ( - - Directory: - {displayCwd} - - - Session: - {displaySession} - Model: {model} diff --git a/packages/cli/src/tui/components/layout/MainContent.tsx b/packages/cli/src/tui/components/layout/MainContent.tsx index 49d52a80..7b6bb7dc 100644 --- a/packages/cli/src/tui/components/layout/MainContent.tsx +++ b/packages/cli/src/tui/components/layout/MainContent.tsx @@ -1,6 +1,5 @@ import { Box, Static, Text } from 'ink' import { memo } from 'react' -import os from 'node:os' import type { SystemMessage, TurnView as TurnViewType } from '../../types' import { SystemMessageView } from '../messages/SystemMessageView' import { TurnView } from '../turns/TurnView' @@ -19,20 +18,6 @@ type MainContentProps = { } } -function formatCwd(cwd: string) { - const home = os.homedir() - if (!home) return cwd - return cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd -} - -function formatSessionId(id: string): string { - // Show first 8 chars and last 4 chars - if (id.length > 16) { - return `${id.slice(0, 8)}...${id.slice(-4)}` - } - return id -} - // Custom comparison to prevent re-render when arrays haven't actually changed function arePropsEqual(prevProps: MainContentProps, nextProps: MainContentProps): boolean { // Check header info (shouldn't change often) @@ -144,16 +129,6 @@ export const MainContent = memo(function MainContent({ - - Directory: - {formatCwd(header.cwd)} - - - Session: - - {formatSessionId(header.sessionId)} - - Model: {header.model} diff --git a/packages/cli/src/tui/components/turns/StepView.test.ts b/packages/cli/src/tui/components/turns/StepView.test.ts new file mode 100644 index 00000000..f3b1f92e --- /dev/null +++ b/packages/cli/src/tui/components/turns/StepView.test.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert' +import path from 'node:path' +import { describe, test } from 'vitest' +import { getMainParam } from './StepView' + +describe('getMainParam', () => { + test('formats absolute file paths as workspace-relative paths', () => { + const cwd = path.join(path.sep, 'tmp', 'memo-cli') + const filePath = path.join(cwd, 'web', 'public', 'logo.svg') + + const param = getMainParam({ file_path: filePath }, cwd) + assert.strictEqual(param, 'web/public/logo.svg') + }) + + test('keeps absolute paths unchanged when outside workspace', () => { + const cwd = path.join(path.sep, 'tmp', 'memo-cli') + const outsidePath = path.join(path.sep, 'tmp', 'other', 'logo.svg') + + const param = getMainParam({ file_path: outsidePath }, cwd) + assert.strictEqual(param, outsidePath) + }) + + test('formats absolute string input as workspace-relative path', () => { + const cwd = path.join(path.sep, 'tmp', 'memo-cli') + const filePath = path.join(cwd, 'README.md') + + const param = getMainParam(filePath, cwd) + assert.strictEqual(param, 'README.md') + }) + + test('formats cwd absolute path as dot', () => { + const cwd = path.join(path.sep, 'tmp', 'memo-cli') + const param = getMainParam({ dir_path: cwd }, cwd) + assert.strictEqual(param, '.') + }) +}) diff --git a/packages/cli/src/tui/components/turns/StepView.tsx b/packages/cli/src/tui/components/turns/StepView.tsx index 14e38a2d..e170b401 100644 --- a/packages/cli/src/tui/components/turns/StepView.tsx +++ b/packages/cli/src/tui/components/turns/StepView.tsx @@ -1,5 +1,6 @@ import { Box, Text } from 'ink' import { memo } from 'react' +import path from 'node:path' import type { StepView as StepViewType } from '../../types' import { MarkdownMessage } from '../messages/MarkdownMessage' @@ -8,20 +9,35 @@ type StepViewProps = { hideAssistantText?: boolean } +const PATH_PARAM_KEYS = new Set(['dir_path', 'file_path', 'path', 'file', 'filename', 'cwd', 'dir']) + +function truncate(value: string): string { + return value.length > 50 ? `${value.slice(0, 47)}...` : value +} + +function toDisplayPath(value: string, cwd: string): string { + if (!path.isAbsolute(value)) return value + + const relative = path.relative(cwd, value) + if (!relative) return '.' + if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) { + return value + } + + return relative.split(path.sep).join('/') +} + // Extract the most important parameter to display -function getMainParam(toolInput: unknown): string | undefined { +export function getMainParam(toolInput: unknown, cwd = process.cwd()): string | undefined { if (!toolInput) return undefined if (typeof toolInput === 'string') { - if (toolInput.length > 50) { - return toolInput.slice(0, 47) + '...' - } - return toolInput + return truncate(toDisplayPath(toolInput, cwd)) } if (typeof toolInput !== 'object' || Array.isArray(toolInput)) return undefined - const input = toolInput as Record + const input = toolInput as Record const priorityKeys = [ 'cmd', @@ -41,21 +57,19 @@ function getMainParam(toolInput: unknown): string | undefined { ] for (const key of priorityKeys) { - if (input[key]) { - const value = String(input[key]) - if (value.length > 50) { - return value.slice(0, 47) + '...' - } - return value - } + const rawValue = input[key] + if (rawValue === undefined || rawValue === null || rawValue === '') continue + const value = String(rawValue) + const displayValue = + PATH_PARAM_KEYS.has(key) || key.includes('path') ? toDisplayPath(value, cwd) : value + return truncate(displayValue) } for (const [key, value] of Object.entries(input)) { if (typeof value === 'string' && key !== 'description') { - if (value.length > 50) { - return value.slice(0, 47) + '...' - } - return value + const displayValue = + PATH_PARAM_KEYS.has(key) || key.includes('path') ? toDisplayPath(value, cwd) : value + return truncate(displayValue) } } diff --git a/web/app/docs/[slug]/page.tsx b/web/app/docs/[slug]/page.tsx index 20f393d9..06175e37 100644 --- a/web/app/docs/[slug]/page.tsx +++ b/web/app/docs/[slug]/page.tsx @@ -61,16 +61,16 @@ export default async function DocDetailPage({ params }: DocDetailPageProps) { {previous ? ( -
- +
+
-

+

Previous

-

+

{previous.title}

@@ -82,18 +82,18 @@ export default async function DocDetailPage({ params }: DocDetailPageProps) { {next ? (
-

+

Next

-

+

{next.title}

-
- +
+
) : ( diff --git a/web/app/docs/page.tsx b/web/app/docs/page.tsx index 606a1ed1..d2dbdf34 100644 --- a/web/app/docs/page.tsx +++ b/web/app/docs/page.tsx @@ -1,175 +1,122 @@ import Link from 'next/link' +import { ArrowRight, ExternalLink } from 'lucide-react' import { DocsShell } from '@/components/docs-shell' import { listDocPages } from '@/lib/docs' -import { - ArrowRight, - BookOpen, - Zap, - Sparkles, - Terminal, - ExternalLink, - Puzzle, - Settings, -} from 'lucide-react' const quickStartGuides = [ { slug: 'getting-started', title: 'Getting Started', description: 'Installation and basic setup', - icon: Terminal, }, { slug: 'cli-tui', title: 'CLI & TUI', description: 'Interactive terminal interface', - icon: Zap, }, { slug: 'configuration', title: 'Configuration', description: 'API keys and settings', - icon: BookOpen, }, ] -const categoryConfig: Record = { - 'Getting Started': { title: 'Getting Started', icon: Terminal }, - 'Core Features': { title: 'Core Features', icon: Zap }, - Extensions: { title: 'Extensions', icon: Puzzle }, - Operations: { title: 'Operations', icon: Settings }, -} +const categoryOrder = ['Getting Started', 'Core Features', 'Extensions', 'Operations'] as const export default async function DocsIndexPage() { const pages = await listDocPages() - // Group pages by category - const grouped = { - 'Getting Started': pages.filter((p) => p.category === 'Getting Started'), - 'Core Features': pages.filter((p) => p.category === 'Core Features'), - Extensions: pages.filter((p) => p.category === 'Extensions'), - Operations: pages.filter((p) => p.category === 'Operations'), - } + const grouped = categoryOrder.map((category) => ({ + category, + pages: pages.filter((page) => page.category === category), + })) return (
- {/* Quick Start Section */} -
-
-
- -
-

Quick Start

-
- -
- {quickStartGuides.map((guide, index) => ( +
+

+ Quick Start +

+
+ {quickStartGuides.map((guide) => ( -
-
-
- - 0{index + 1} - - -
-

{guide.title}

-

- {guide.description} +

+

+ {guide.title}

+
+

+ {guide.description} +

))}
- {/* All Docs Grid */} -
-
-
- -
-

All Guides

-
- -
- {Object.entries(grouped).map(([category, categoryPages]) => { - const config = categoryConfig[category] - const Icon = config?.icon || BookOpen - return ( -
-
- -

- {config?.title || category} -

-
-
- {categoryPages.map((page) => ( - -

- {page.title} -

-

- {page.summary} -

- - ))} -
+
+

+ All Guides +

+
+ {grouped.map((group) => ( +
+

+ {group.category} +

+
+ {group.pages.map((page) => ( + +

+ {page.title} +

+

+ {page.summary} +

+ + ))}
- ) - })} +
+ ))}
- {/* Help Section */} -
-
-
-
- -
-
-

Need help?

-

- Can't find what you're looking for? Check our - troubleshooting guide or open an issue on GitHub. -

-
- - Troubleshooting - - - Open an issue - - -
-
-
+
+

+ Need help? +

+

+ Check troubleshooting or open an issue if something is unclear. +

+
+ + Troubleshooting + + + Open an issue + +
diff --git a/web/app/globals.css b/web/app/globals.css index ccd33fb2..a81fe1c9 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -2,24 +2,24 @@ :root { /* Linear-inspired dark color palette */ - --bg-primary: #0a0a0f; - --bg-secondary: #111118; - --bg-tertiary: #1a1a24; - --bg-elevated: #232330; + --bg-primary: #09090b; + --bg-secondary: #111113; + --bg-tertiary: #18181b; + --bg-elevated: #222226; - --text-primary: #fafafa; + --text-primary: #f4f4f5; --text-secondary: #a1a1aa; --text-tertiary: #71717a; - --accent-primary: #5e6ad2; - --accent-secondary: #8b5cf6; - --accent-gradient: linear-gradient(135deg, #5e6ad2 0%, #8b5cf6 100%); + --accent-primary: #f4f4f5; + --accent-secondary: #d4d4d8; + --accent-gradient: linear-gradient(180deg, #fafafa 0%, #d4d4d8 100%); --border-default: rgba(255, 255, 255, 0.08); --border-hover: rgba(255, 255, 255, 0.12); - --glow-primary: rgba(94, 106, 210, 0.4); - --glow-secondary: rgba(139, 92, 246, 0.3); + --glow-primary: rgba(255, 255, 255, 0.14); + --glow-secondary: rgba(255, 255, 255, 0.08); } @theme inline { @@ -63,9 +63,8 @@ body::before { pointer-events: none; z-index: 0; background: - radial-gradient(ellipse 80% 50% at 50% -20%, rgba(94, 106, 210, 0.15), transparent), - radial-gradient(ellipse 60% 40% at 80% 50%, rgba(139, 92, 246, 0.08), transparent), - radial-gradient(ellipse 50% 30% at 20% 80%, rgba(94, 106, 210, 0.06), transparent); + radial-gradient(ellipse 80% 45% at 50% -20%, rgba(255, 255, 255, 0.1), transparent 65%), + radial-gradient(ellipse 65% 35% at 80% 10%, rgba(255, 255, 255, 0.04), transparent 70%); } a { @@ -74,7 +73,7 @@ a { } ::selection { - background: rgba(94, 106, 210, 0.4); + background: rgba(255, 255, 255, 0.24); color: var(--text-primary); } @@ -105,12 +104,7 @@ a { inset: 0; padding: 1px; border-radius: 16px; - background: linear-gradient( - 135deg, - rgba(94, 106, 210, 0.5), - rgba(139, 92, 246, 0.3), - rgba(255, 255, 255, 0.1) - ); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.06)); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); @@ -137,9 +131,9 @@ a { } .badge-accent { - background: rgba(94, 106, 210, 0.15); - border-color: rgba(94, 106, 210, 0.3); - color: #818cf8; + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.18); + color: var(--text-primary); } /* Primary button */ @@ -148,20 +142,20 @@ a { align-items: center; justify-content: center; padding: 0.75rem 1.5rem; - background: var(--accent-gradient); - color: white; + background: var(--bg-elevated); + color: var(--text-primary); font-weight: 600; font-size: 0.875rem; border-radius: 12px; - border: none; + border: 1px solid rgba(255, 255, 255, 0.18); cursor: pointer; transition: all 0.2s ease; - box-shadow: 0 4px 20px rgba(94, 106, 210, 0.3); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); } .btn-primary:hover { - transform: translateY(-1px); - box-shadow: 0 8px 30px rgba(94, 106, 210, 0.4); + background: #2a2a30; + border-color: rgba(255, 255, 255, 0.26); } /* Secondary button */ @@ -204,7 +198,7 @@ code { padding: 0.2rem 0.4rem; background: var(--bg-tertiary); border-radius: 6px; - color: #c4b5fd; + color: var(--text-primary); } /* Section divider */ @@ -292,52 +286,97 @@ code { min-height: 100vh; } +.doc-prose { + color: var(--text-secondary); + font-size: 0.97rem; + line-height: 1.72; +} + .doc-prose h2 { - margin: 2.5rem 0 1rem; - font-size: 1.5rem; + margin: 2.25rem 0 0.9rem; + font-size: 1.45rem; font-weight: 600; color: var(--text-primary); - letter-spacing: -0.02em; + letter-spacing: -0.01em; } .doc-prose h3 { - margin: 2rem 0 0.75rem; - font-size: 1.25rem; + margin: 1.6rem 0 0.7rem; + font-size: 1.18rem; font-weight: 600; color: var(--text-primary); } .doc-prose p { - margin: 1rem 0; + margin: 0.9rem 0; color: var(--text-secondary); - line-height: 1.75; + line-height: 1.72; } .doc-prose ul { - margin: 1rem 0; - padding-left: 1.5rem; + margin: 0.9rem 0; + padding-left: 1.2rem; + color: var(--text-secondary); +} + +.doc-prose ol { + margin: 0.9rem 0; + padding-left: 1.2rem; color: var(--text-secondary); } .doc-prose li { - margin: 0.5rem 0; + margin: 0.35rem 0; } .doc-prose a { - color: #818cf8; + color: #c7d2fe; + text-decoration: underline; + text-underline-offset: 3px; transition: color 0.2s; } .doc-prose a:hover { - color: #a5b4fc; + color: #e2e8f0; +} + +.doc-prose blockquote { + margin: 1rem 0; + border-left: 2px solid var(--border-default); + padding: 0.4rem 0 0.4rem 0.9rem; + color: var(--text-secondary); +} + +.doc-prose table { + width: 100%; + margin: 1.1rem 0; + border-collapse: collapse; + font-size: 0.92rem; +} + +.doc-prose th, +.doc-prose td { + border: 1px solid var(--border-default); + padding: 0.55rem 0.65rem; + text-align: left; +} + +.doc-prose th { + color: var(--text-primary); + background: var(--bg-tertiary); +} + +.doc-prose td { + padding: 0.55rem 0.65rem; + color: var(--text-secondary); } .doc-prose pre { - margin: 1.25rem 0; - background: #0d0d12; + margin: 1.15rem 0; + background: #0b0f16; border: 1px solid var(--border-default); - border-radius: 12px; - padding: 1.25rem; + border-radius: 8px; + padding: 0.95rem 1rem; overflow-x: auto; } @@ -347,6 +386,15 @@ code { color: var(--text-primary); } +.doc-prose :not(pre) > code { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-default); + border-radius: 5px; + padding: 0.12rem 0.35rem; + font-size: 0.85em; +} + /* Scrollbar styling */ ::-webkit-scrollbar { width: 8px; diff --git a/web/app/page.tsx b/web/app/page.tsx index 02d90e38..f531c1e7 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -2,106 +2,85 @@ import Link from 'next/link' import Image from 'next/image' import { SiteHeader } from '@/components/site-header' import { MemoHeroRemotion } from '@/components/memo-hero-remotion' -import { ArrowRight, Zap, Shield, Terminal, Cpu, GitBranch, Sparkles } from 'lucide-react' +import { ArrowRight, Terminal, Cpu, Shield, GitBranch, Layers, Gauge } from 'lucide-react' const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '' -// Feature data with icons const features = [ { icon: Terminal, - title: 'Terminal-Native', - description: - 'Runs in your terminal, seamlessly integrated with your existing development workflow without context switching.', + title: 'Terminal-native', + description: 'Works where you already ship: directly inside your terminal workflow.', }, { - icon: Zap, - title: 'Lightning Fast', - description: - 'Built on an efficient architecture for rapid responses and a buttery-smooth coding agent experience.', + icon: Gauge, + title: 'Fast feedback', + description: 'Optimized for quick iteration, from reading code to applying changes.', }, { icon: Cpu, - title: 'Built-in Tools', - description: - 'File operations, code search, command execution, and more — ready to use right out of the box.', + title: 'Built-in tools', + description: 'File I/O, grep, commands, and patching are available out of the box.', }, { icon: Shield, - title: 'Safety First', - description: - 'Critical operations require confirmation, giving you full control while enjoying automation.', + title: 'Approval controls', + description: 'Dangerous operations can require confirmation before execution.', }, { icon: GitBranch, - title: 'MCP Ready', - description: - 'Supports Model Context Protocol to connect external tools and extend capabilities.', + title: 'MCP ready', + description: 'Connect external capabilities through the Model Context Protocol.', }, { - icon: Sparkles, - title: 'Multi-Agent', - description: - 'Subagent mode enables multi-agent collaboration for handling complex tasks with ease.', + icon: Layers, + title: 'Multi-agent mode', + description: 'Delegate subtasks to subagents when work needs to fan out.', }, ] -// Workflow steps const workflowSteps = [ { step: '01', title: 'Install', code: 'npm install -g @memo-code/memo', - description: 'Install globally and get started instantly', + description: 'Install once and run anywhere.', }, { step: '02', title: 'Configure', code: 'export OPENAI_API_KEY=sk-...', - description: 'Set your API key, supports multiple models', + description: 'Use OpenAI-compatible or DeepSeek providers.', }, { step: '03', title: 'Launch', code: 'memo', - description: 'Start the interactive TUI interface', + description: 'Start the interactive TUI session.', }, ] export default function Home() { return ( -
+
-
- {/* Hero Section */} -
+
+
- {/* Badge */} -
-
- - Now available on npm -
+
+ Open-source terminal coding agent
- {/* Main headline */} -

- Your coding agent, -
- - in the terminal - +

+ A lightweight coding agent that runs in your terminal.

- - {/* Subtitle */} -

- Memo is a lightweight, open-source coding agent that understands your - project context and helps you code faster through natural language. +

+ Memo understands your project structure, runs tools safely, and helps + you move from prompt to patch without leaving the shell.

- {/* CTA Buttons */} -
+
- {/* Hero Terminal Preview */} -
+
- {/* Features Grid */} -
+
-
-
Features
-

- Everything you need -

-

- A complete toolkit for AI-assisted development, right in your - terminal -

-
+

+ Built for real engineering workflows +

+

+ Keep the stack simple: clean output, explicit steps, and tooling that + fits daily development. +

-
- {features.map((feature, index) => ( +
+ {features.map((feature) => (
-
- +
+
-

+

{feature.title}

-

+

{feature.description}

@@ -162,149 +135,127 @@ export default function Home() {
- {/* Workflow Section */} -
-
-
-
-
-
Quick Start
-

- Up and running in seconds -

-

- No complex setup required. Install, configure, and start coding - with AI assistance in under a minute. -

+
+
+
+
Quick Start
+

+ Get productive in under a minute +

+

+ Install, configure a provider, and launch the interactive CLI. +

-
- {workflowSteps.map((item) => ( -
-
- {item.step} -
-
-

- {item.title} -

-

- {item.description} -

-
+
+ {workflowSteps.map((item) => ( +
+
+ {item.step}
- ))} -
-
- -
-
-
-
-
-
- - install.sh - +
+

+ {item.title} +

+

+ {item.description} +

-
-
- {workflowSteps.map((item) => ( -
-

- # {item.title} -

-

- ${' '} - {item.code} -

-
- ))} -
+ ))} +
+
+ +
+
+ terminal +
+
+ {workflowSteps.map((item) => ( +
+

+ # {item.title} +

+

+ ${' '} + {item.code} +

-
+ ))}
- {/* CTA Section */} -
-
-
-
-
-
-
- -
-

- Ready to get started? -

-

- Install Memo CLI and start coding with AI assistance today. -

-
- - Read Documentation - - - - npm install - -
-
+
+
+

+ Ready to try Memo CLI? +

+

+ Install Memo and start working with an agent that stays inside your + terminal. +

+
+ + Read Documentation + + + + npm install +
- {/* Footer */} -
-
-
-
- Memo Logo - Memo CLI -
-
- - Documentation - - - GitHub - - - npm - -
-

- © 2025 Memo CLI. Open source under MIT. -

+
+
+
+ Memo Logo + + Memo CLI + +
+
+ + Documentation + + + GitHub + + + npm +
+

+ © 2025 Memo CLI · MIT License +

diff --git a/web/components/doc-toc.tsx b/web/components/doc-toc.tsx index 1cbfee77..80f9bdfa 100644 --- a/web/components/doc-toc.tsx +++ b/web/components/doc-toc.tsx @@ -8,14 +8,8 @@ type DocTocProps = { sections: DocSectionAnchor[] } -function clamp(value: number, min: number, max: number) { - return Math.min(max, Math.max(min, value)) -} - export function DocToc({ sections }: DocTocProps) { const [activeId, setActiveId] = useState(sections[0]?.id ?? '') - const [progress, setProgress] = useState(0) - const sectionIds = useMemo(() => sections.map((section) => section.id), [sections]) useEffect(() => { @@ -54,96 +48,35 @@ export function DocToc({ sections }: DocTocProps) { return () => observer.disconnect() }, [sectionIds]) - useEffect(() => { - const updateProgress = () => { - const article = document.querySelector('article') - if (!article) { - setProgress(0) - return - } - - const rect = article.getBoundingClientRect() - const top = window.scrollY + rect.top - const height = article.offsetHeight - const viewport = window.innerHeight - - const start = top - viewport * 0.16 - const end = top + height - viewport * 0.72 - const ratio = (window.scrollY - start) / Math.max(end - start, 1) - - setProgress(clamp(ratio, 0, 1)) - } - - updateProgress() - window.addEventListener('scroll', updateProgress, { passive: true }) - window.addEventListener('resize', updateProgress) - - return () => { - window.removeEventListener('scroll', updateProgress) - window.removeEventListener('resize', updateProgress) - } - }, []) - return (