diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md index 4f6a355..34a3cc1 100644 --- a/.claude/commands/code-review.md +++ b/.claude/commands/code-review.md @@ -1,6 +1,7 @@ # Code Review Command ## Trigger Phrases + - "code review" - "review the code" - "perform code review" @@ -10,13 +11,16 @@ ## Instructions ### Prerequisites + 1. **Always read CLAUDE.md first** - This contains the tech stack and coding standards 2. **Never modify AGENTS.md** - This file only contains instructions for other coding assistants ### Code Review Process #### 1. Initial Scan + Analyze the entire codebase focusing on: + - `/src/` - All React/TypeScript frontend code (components, hooks, types) - `/src-tauri/src/` - All Rust backend code (menu handling, IPC) - Configuration files: `tauri.conf.json`, `vite.config.ts`, `tsconfig.json` @@ -24,6 +28,7 @@ Analyze the entire codebase focusing on: #### 2. Review Categories (in order of criticality) **CRITICAL Issues:** + - Security vulnerabilities (XSS in markdown rendering, path traversal in file operations, exposed secrets) - Tauri command permissions too broad or missing validation - Unhandled promise rejections or uncaught errors in async operations @@ -33,6 +38,7 @@ Analyze the entire codebase focusing on: - External link handling without security validation **HIGH Priority:** + - Missing TypeScript documentation comments (TSDoc) on functions and types - Functions longer than 50 lines that should be split - Code duplication (same logic in multiple places) @@ -43,6 +49,7 @@ Analyze the entire codebase focusing on: - useState/useEffect hooks that could be extracted to custom hooks **MEDIUM Priority:** + - Poor naming conventions (not following camelCase/PascalCase/snake_case standards) - Lack of reusability (logic that could be extracted to utilities/hooks) - Inconsistent React patterns (mixing class and functional components) @@ -53,6 +60,7 @@ Analyze the entire codebase focusing on: - Large bundle size issues that could benefit from code-splitting **LOW Priority:** + - Code style inconsistencies (should be caught by ESLint/Prettier) - Missing comments for complex business logic - Outdated TODO comments @@ -64,6 +72,7 @@ Analyze the entire codebase focusing on: #### 3. Specific Technology Checks **Rust Backend (src-tauri/src/):** + - All public functions have Rust doc comments (`///`) - Tauri commands use proper Result return types - Menu handles stored in State and accessed safely @@ -75,6 +84,7 @@ Analyze the entire codebase focusing on: - Menu event handlers are registered correctly **React Frontend (src/):** + - All components are functional (no class components) - TypeScript interfaces defined for all component props - Custom hooks extracted for reusable logic (see src/hooks/) @@ -87,6 +97,7 @@ Analyze the entire codebase focusing on: - Event listeners properly removed in cleanup **TypeScript:** + - No `any` types used (use `unknown` and type guards if needed) - All interfaces/types properly exported from src/types/ - Strict mode enabled in tsconfig.json @@ -96,6 +107,7 @@ Analyze the entire codebase focusing on: - Enums used for fixed sets of values **Configuration & Build:** + - tauri.conf.json settings appropriate for security and UX - Vite config optimized for bundle size - ESLint rules enforced and passing @@ -104,10 +116,12 @@ Analyze the entire codebase focusing on: - Bundle identifier follows conventions (not ending in .app) **Testing:** + - Note: No test suite currently exists for this project - Future testing should cover: file operations, menu interactions, markdown rendering, editor functionality #### 4. Architecture & Patterns + - React components focused on UI (business logic in hooks/utilities) - TypeScript interfaces/types are just data definitions (no logic) - Tauri commands are thin (just IPC handling) @@ -120,6 +134,7 @@ Analyze the entire codebase focusing on: - File operations handled by Tauri backend (not in React components) #### 5. Modern JavaScript/TypeScript Patterns + - ES modules used exclusively (import/export) - Async/await preferred over .then/.catch - Destructuring used appropriately @@ -133,16 +148,17 @@ Analyze the entire codebase focusing on: - Readonly arrays/objects for immutable data #### 6. Documentation & Comments + - TypeScript functions have TSDoc comments with: - - Description - - @param for each parameter (type inferred from TypeScript) - - @returns with description (type inferred from TypeScript) - - @throws for error conditions + - Description + - @param for each parameter (type inferred from TypeScript) + - @returns with description (type inferred from TypeScript) + - @throws for error conditions - Rust functions have doc comments (`///`) with: - - Description - - `# Arguments` section for parameters - - `# Returns` section - - `# Errors` section for fallible functions + - Description + - `# Arguments` section for parameters + - `# Returns` section + - `# Errors` section for fallible functions - Complex logic has inline comments explaining "why" not "what" - README and CLAUDE.md are up to date - TypeScript interfaces documented with purpose and usage examples @@ -150,10 +166,12 @@ Analyze the entire codebase focusing on: ### Output Format Generate a review document at `/docs/reviews/.md` with this structure: + ````markdown # Code Review - ## Executive Summary + - Total Issues Found: X - Critical: X | High: X | Medium: X | Low: X - Estimated Total Remediation Time: X hours @@ -164,29 +182,37 @@ Generate a review document at `/docs/reviews/.md` with this structure: ### CRITICAL Issues #### [CRIT-001] Issue Title + **File:** `path/to/file.js:lineNumber` **Category:** Security/Performance/Error Handling **Description:** Clear explanation of the issue **Impact:** What problems this causes **Current Code:** + ```typescript // Show problematic code ``` + **Recommended Fix:** + ```typescript // Show corrected code ``` + **Effort:** X minutes --- ### HIGH Priority Issues + [Same format as CRITICAL] ### MEDIUM Priority Issues + [Same format as CRITICAL] ### LOW Priority Issues + [Same format as CRITICAL] --- @@ -194,12 +220,15 @@ Generate a review document at `/docs/reviews/.md` with this structure: ## Coding Sessions ### Session 1: Critical Security & Error Handling (Est. X hours) + **Focus:** Address all CRITICAL issues first **Issues to Fix:** + - [CRIT-001] Issue Title - `file.js:line` (X min) - [CRIT-002] Issue Title - `file.js:line` (X min) **Instructions for Agent:** + 1. Fix [CRIT-001] by implementing the recommended solution 2. Fix [CRIT-002] by implementing the recommended solution 3. Run `npm run build` and `npm run lint` to validate @@ -209,12 +238,15 @@ Generate a review document at `/docs/reviews/.md` with this structure: --- ### Session 2: High Priority - TSDoc & Code Structure (Est. X hours) + **Focus:** Documentation and code organization **Issues to Fix:** + - [HIGH-001] Issue Title - `file.ts:line` (X min) - [HIGH-002] Issue Title - `file.ts:line` (X min) **Instructions for Agent:** + 1. Add TSDoc comments to all TypeScript functions identified 2. Add Rust doc comments to all public Rust functions 3. Refactor large functions into smaller utilities @@ -225,19 +257,23 @@ Generate a review document at `/docs/reviews/.md` with this structure: --- ### Session 3: Medium Priority - Refactoring & Reusability (Est. X hours) + [Continue pattern] ### Session 4: Low Priority - Polish & Optimization (Est. X hours) + [Continue pattern] --- ## Positive Findings + - List things done well - Areas that follow best practices - Well-structured code examples ## Recommendations for Future Development + - Architectural improvements - Tools or libraries to consider - Process improvements @@ -246,6 +282,7 @@ Generate a review document at `/docs/reviews/.md` with this structure: --- ## Notes + - This review was generated based on CLAUDE.md standards - Run `npm run build` and `npm run lint` after each session to ensure quality - Test functionality with `npm run tauri dev` before committing @@ -256,12 +293,14 @@ Generate a review document at `/docs/reviews/.md` with this structure: ### Review Guidelines **Criticality Scoring:** + - **CRITICAL**: Security risks, data loss potential, production blockers - **HIGH**: Maintainability issues, missing docs, poor error handling - **MEDIUM**: Code smells, duplication, suboptimal patterns - **LOW**: Style issues, minor optimizations, nice-to-haves **Session Sizing:** + - Target 2-4 hours per session maximum - Group related issues together (e.g., all TSDoc additions, all type fixes) - Order by file/directory to minimize context switching @@ -269,6 +308,7 @@ Generate a review document at `/docs/reviews/.md` with this structure: - Separate TypeScript and Rust changes when practical **Agent Instructions:** + - Be specific about which files and functions to modify - Include line numbers when possible - Provide concrete code examples in TypeScript/Rust @@ -277,12 +317,14 @@ Generate a review document at `/docs/reviews/.md` with this structure: - Include semantic commit messages (conventional commits format) **Balance:** + - Don't be overly pedantic on LOW issues - Focus on actionable, specific feedback - Highlight good practices when found - Consider effort vs. value trade-offs ### Quality Checks Before Output + 1. All issues have clear descriptions and fixes 2. Estimated times are realistic 3. Sessions are balanced and logical diff --git a/.claude/commands/update-docs.md b/.claude/commands/update-docs.md index 659b1a3..ae85c14 100644 --- a/.claude/commands/update-docs.md +++ b/.claude/commands/update-docs.md @@ -1,41 +1,42 @@ ### Prerequisites + 1. **Always read CLAUDE.md first** - This is the single source of truth for all developer information 2. **Never modify AGENTS.md** - This file only contains instructions for other coding assistants to read CLAUDE.md ### Documentation Update Process 1. **Analyze Recent Changes** - - Check git log for recent commits (last 7-14 days or since last release) - - Identify modified/added/removed files in all source code folders - - Note any changes to APIs, functions, configuration, or architecture + - Check git log for recent commits (last 7-14 days or since last release) + - Identify modified/added/removed files in all source code folders + - Note any changes to APIs, functions, configuration, or architecture 2. **Review Current Documentation** - - Read existing documentation files: - - `README.md` - - `CLAUDE.md` (the single source of truth) - - Any files in `/docs` directory - - Any README.md files in any source code folder - - Identify sections that are now outdated or incomplete + - Read existing documentation files: + - `README.md` + - `CLAUDE.md` (the single source of truth) + - Any files in `/docs` directory + - Any README.md files in any source code folder + - Identify sections that are now outdated or incomplete 3. **Update Documentation** For each documentation file: **README.md:** - - Update installation instructions if dependencies changed - - Update usage examples if APIs changed - - Update feature list if functionality added/removed - - Ensure quick start guide reflects current setup + - Update installation instructions if dependencies changed + - Update usage examples if APIs changed + - Update feature list if functionality added/removed + - Ensure quick start guide reflects current setup **CLAUDE.md:** - - Add new architectural decisions or patterns - - Update coding conventions if they've evolved - - Document new dependencies or tools - - Add any new gotchas or important context + - Add new architectural decisions or patterns + - Update coding conventions if they've evolved + - Document new dependencies or tools + - Add any new gotchas or important context **AGENTS.md:** - - **DO NOT MODIFY** - Leave this file untouched + - **DO NOT MODIFY** - Leave this file untouched **/docs directory:** - - Update API documentation for changed endpoints/functions - - Refresh architecture diagrams if structure changed - - Update configuration guides if settings changed + - Update API documentation for changed endpoints/functions + - Refresh architecture diagrams if structure changed + - Update configuration guides if settings changed diff --git a/.github/actions-scripts/update-versions.js b/.github/actions-scripts/update-versions.js index 1fe5049..0bd4268 100644 --- a/.github/actions-scripts/update-versions.js +++ b/.github/actions-scripts/update-versions.js @@ -18,9 +18,7 @@ const versionType = process.argv[2]; const validVersionTypes = ['major', 'minor', 'patch']; if (!validVersionTypes.includes(versionType)) { - console.error( - `Invalid version type. Use one of: ${validVersionTypes.join(', ')}` - ); + console.error(`Invalid version type. Use one of: ${validVersionTypes.join(', ')}`); process.exit(1); } @@ -58,10 +56,7 @@ try { try { let cargoToml = fs.readFileSync(cargoTomlPath, 'utf8'); // Replace version line in TOML file - cargoToml = cargoToml.replace( - /^version = ".*"$/m, - `version = "${newVersion}"` - ); + cargoToml = cargoToml.replace(/^version = ".*"$/m, `version = "${newVersion}"`); fs.writeFileSync(cargoTomlPath, cargoToml); console.log(`Updated ${cargoTomlPath} to version ${newVersion}`); } catch (error) { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d6ecf7b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + frontend: + name: Frontend (lint, typecheck, unit, e2e) + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Prettier check + run: npm run format:check + + - name: Unit tests + run: npm run test:unit + + - name: E2E tests + run: npm run test:e2e + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report + retention-days: 7 + + rust: + name: Rust (check, clippy, test) + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install Frontend Dependencies + run: npm ci + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Install Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + libssl-dev \ + patchelf + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> target + + - name: cargo check + working-directory: src-tauri + run: cargo check --locked + + - name: cargo clippy + working-directory: src-tauri + run: cargo clippy --locked -- -D warnings + + - name: cargo test + working-directory: src-tauri + run: cargo test --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5865a87..9f849e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,9 +92,75 @@ jobs: branch: ${{ github.ref }} tags: true + verify: + name: Verify (lint, typecheck, unit, e2e, rust) + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Install Linux system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libappindicator3-dev \ + librsvg2-dev \ + libssl-dev \ + patchelf + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> target + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Prettier check + run: npm run format:check + + - name: Unit tests + run: npm run test:unit + + - name: E2E tests + run: npm run test:e2e + + - name: cargo check + working-directory: src-tauri + run: cargo check --locked + + - name: cargo clippy + working-directory: src-tauri + run: cargo clippy --locked -- -D warnings + + - name: cargo test + working-directory: src-tauri + run: cargo test --locked + build-tauri: name: Build Tauri App (${{ matrix.platform }}) - needs: prepare-release + needs: [prepare-release, verify] strategy: fail-fast: false matrix: diff --git a/.gitignore b/.gitignore index 4d8282b..8b4680b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,12 @@ docs/ai/ *.sw? .playwright-mcp/ +# Ad-hoc Playwright MCP screenshots dumped at repo root during UI review. +/welcome-*.png +/viewer-*.png +/page-*.png + +# Test output +playwright-report/ +test-results/ +coverage/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..231296f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,28 @@ +dist/ +src-tauri/target/ +src-tauri/gen/ +src-tauri/wix/ +node_modules/ +package-lock.json +coverage/ +playwright-report/ +test-results/ + +# Rust / Cargo +src-tauri/Cargo.lock +src-tauri/Cargo.toml +*.rs + +# Binary and non-source files +*.wxs +*.xml +*.ico +*.icns +*.png +*.jpg +*.jpeg +*.gif +*.svg +*.pdf +*.woff +*.woff2 diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..044758e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 100, + "arrowParens": "always" +} diff --git a/AGENTS.md b/AGENTS.md index 26a5655..9e65275 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,4 +7,3 @@ It contains the canonical project notes, gotchas, and rules of engagement for th Direct link (from repo root): `CLAUDE.md` — A fellow agent - diff --git a/CHANGELOG.md b/CHANGELOG.md index 8094e9d..c3a8458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,20 +5,48 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unreleased] - 2026-04-23 -### Planned -- Export to HTML/PDF -- Syntax highlighting for code blocks -- Search within document -- Word count and reading time statistics -- Custom CSS themes -- Multiple document tabs -- Drag-and-drop file opening +### Changed + +- **View-only refactor.** The editor, split-pane, synchronized scrolling, tabs, save / save-as, unsaved-changes dialogs, detached windows, and session restoration have all been removed. MarkDoc is now a dedicated markdown viewer. +- **One window per file.** The app now runs on a multi-window model: a single `main` window renders the Welcome screen, and additional files open as their own `viewer-` windows. All file-open entry points (File menu, recents, OS "Open With…", `tauri-plugin-single-instance`) route through the Rust command `open_file_in_window`, which canonicalises the path and focuses the existing window if the file is already open. +- **Welcome screen.** New recents-driven landing page (up to 20 entries) with Open / Help actions and the current version + build hash. +- **File associations.** `.md` / `.markdown` now advertise MarkDoc with `role: "Viewer"` across macOS, Windows (WiX fragment), and Linux (`.desktop`). +- **Menus.** Trimmed to View-only: File (Open, Open Recent, Close Window, Export ▸ HTML / PDF), Edit (Copy, Select All), View (Zoom, Theme, Toggle Sidebar, Toggle Auto-resize), Window (dynamic list of open file windows), Help. + +### Removed + +- Editor (CodeMirror), split-pane, synchronized scrolling, edit-mode toggle. +- Multi-tab UI: TabBar, tab scroll controls, open-tabs dropdown, detached windows, per-tab toolbar. +- Save / Save As, unsaved-changes dialogs, `prevent_close` plumbing. +- Rust commands: `update_document_content`, `mark_document_saved`, `detach_document`, and associated types (`Document`, `DocumentId`, `TabInfo`). +- Tailwind toolchain (migrated fully to vanilla CSS during the earlier refactor; residual dependencies now removed). +- Session restoration of open tabs. + +### Added + +- Export to HTML (standalone, theme CSS inlined). +- Export to PDF via `headless_chrome` with progress overlay + cancel. +- Rust window registry (`src-tauri/src/window_registry.rs`) with bidirectional `PathBuf ↔ WindowLabel` mapping, monotonic viewer labels, and canonicalised-path collision handling. Unit-tested with `cargo test`. +- Test harness: + - **Vitest** unit tests (`src/**/__tests__/`), currently 96%+ line coverage on `src/hooks` and `src/utils`. + - **Playwright** e2e via the new `web-mode` harness (`npm run dev:web`) — the Tauri platform bridge is swapped for an in-memory `MockBackend`, with fixtures under `src/fixtures/` and stable `data-testid` selectors on Welcome and Viewer. + - `cargo test` for the Rust window registry. +- CI workflow `.github/workflows/ci.yml`: typecheck, lint, format:check, Vitest, Playwright (headless chromium) + cargo check / clippy `-D warnings` / cargo test. +- New npm scripts: `typecheck`, `format`, `format:check`, `test:unit`, `test:unit:watch`, `test:coverage`, `test:e2e`, `test:e2e:headed`, `test:all`, `dev:web`. + +### Dependency cleanup + +- Removed CodeMirror 6 packages. +- Removed Tailwind / PostCSS toolchain. +- Bumped to Tauri 2.10, React 19, TypeScript 5.9. +- Added Vitest + Testing Library, Playwright, and prettier. ## [0.1.0] - 2024-10-17 ### Added + - Initial release of MarkDoc - Dual mode interface (viewer and editor modes) - Native menu integration with keyboard shortcuts @@ -35,6 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features in Detail #### Viewer Mode + - Clean, centered layout for reading - Renders all CommonMark elements - Supports tables, code blocks, blockquotes @@ -42,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Responsive to OS theme changes #### Editor Mode + - Split-pane layout with live preview - Full markdown syntax highlighting - Line numbers and active line highlighting @@ -51,11 +81,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Line wrapping #### Menus + - FILE menu: NEW, OPEN, CLOSE, SAVE, SAVE AS, RECENT FILES - EDIT menu: Standard actions + EDIT MODE toggle - All menu items have keyboard shortcuts #### Technical + - Built with Tauri v2.4 for minimal size and native performance - React 18 + TypeScript frontend - Vite 7 build system @@ -64,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Vanilla CSS for styling (no framework overhead) ### Known Issues + - Recent Files menu shows placeholder (tracking works, menu needs implementation) - Bundle identifier ends with `.app` (harmless warning on macOS) - Main JavaScript bundle is ~1MB (consider code-splitting in future) @@ -74,26 +107,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **0.1.0** - Initial release (2024-10-17) -## Future Roadmap - -### v0.2.0 (Planned) -- Export functionality (HTML, PDF) -- Enhanced syntax highlighting for code blocks -- Search within document -- Performance optimizations - -### v0.3.0 (Planned) -- Custom themes and styling -- Word count and statistics -- Multiple document tabs -- Improved recent files menu - -### v1.0.0 (Planned) -- All core features complete and stable -- Comprehensive test coverage -- Full documentation -- Windows and Linux builds verified - --- For more details on each release, see the [Releases](../../releases) page. diff --git a/CLAUDE.md b/CLAUDE.md index 003bf50..ef9b28e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,471 +2,360 @@ ## Project Overview -MarkDoc is a lightweight, cross-platform desktop application for viewing and editing Markdown files. Built with Tauri v2, React, and TypeScript, it provides a native-looking UI with minimal dependencies. By Stravica. +MarkDoc is a lightweight, cross-platform desktop application for **viewing** Markdown files. Each opened file lives in its own native window — there are no tabs and no editing capabilities. Built with Tauri v2, React 19, and TypeScript. By Stravica. ## Technology Stack -- **Framework**: Tauri v2.4 (Rust + Web) -- **Frontend**: React 18 + TypeScript +- **Framework**: Tauri v2.10 (Rust + Web) +- **Frontend**: React 19 + TypeScript 5.9 - **Build Tool**: Vite 7 -- **Markdown Rendering**: markdown-it (CommonMark compliant) -- **Code Editor**: CodeMirror 6 +- **Markdown Rendering**: markdown-it (CommonMark compliant) + Prism syntax highlighting - **Styling**: Vanilla CSS (no frameworks) +- **Testing**: Vitest + Testing Library (unit), Playwright (e2e), `cargo test` (Rust) - **Plugins**: - - `tauri-plugin-dialog` - File dialogs - - `tauri-plugin-fs` - File system access - - `tauri-plugin-opener` - External links + - `tauri-plugin-dialog` — File dialogs + - `tauri-plugin-fs` — File system access (read-only surface — see capabilities) + - `tauri-plugin-opener` — External links + - `tauri-plugin-os` — OS/theme detection + - `tauri-plugin-single-instance` — Routes second-instance file-open requests into the live process ## Versioning and Build Numbers -MarkDoc uses a **semantic version + git commit hash** system to ensure exact build traceability: +MarkDoc uses a **semantic version + git commit hash** system to ensure exact build traceability. -- **Version**: Defined in 3 locations (must be kept in sync): - - `package.json` (line 4): `"version": "0.1.0"` - - `src-tauri/Cargo.toml` (line 3): `version = "0.1.0"` - - `src-tauri/tauri.conf.json` (line 4): `"version": "0.1.0"` +- **Version**: Defined in 3 locations — keep in sync: + - `package.json` → `"version"` + - `src-tauri/Cargo.toml` → `version` + - `src-tauri/tauri.conf.json` → `"version"` -- **Build Hash**: Automatically captured during compilation - - Captured in `src-tauri/build.rs` using `git rev-parse --short=7 HEAD` - - Stored as compile-time environment variable `GIT_COMMIT_HASH` - - Falls back to "unknown" if git is unavailable (e.g., built from source archive) - - Displayed during `npm run tauri:build` as: `warning: Building MarkDoc version 0.1.0 (commit: d729697)` +- **Build Hash**: Captured at compile time + - `src-tauri/build.rs` runs `git rev-parse --short=7 HEAD` and sets the `GIT_COMMIT_HASH` env var. + - Falls back to `unknown` when git is unavailable (e.g., source archive). -- **About Dialog**: Shows full version with build hash (e.g., "0.1.0 (build d729697)") - - Location: MarkDoc menu → About MarkDoc (macOS) or Help → About (Windows/Linux) - - Implementation: `src-tauri/src/lib.rs` line ~917 +- **Version API**: `invoke('get_app_version')` returns `{ version, build_hash, full_version }`. Used by `WelcomeWindow` to render `full_version` in the header. -- **Version API**: Frontend can query version info programmatically - - Command: `invoke('get_app_version')` - - Returns: `{ version: "0.1.0", build_hash: "d729697", full_version: "0.1.0 (build d729697)" }` - - Useful for displaying version in help dialogs, footer, or debugging +## Architecture -**Why Git Commit Hash?** -The git commit hash ties each build to exact source code, allowing you to: -1. Verify you're working with the correct build -2. Reproduce bugs by checking out the exact commit -3. Track which features/fixes are in a given build -4. Ensure consistency across development, testing, and production +### Multi-window model -## Project Structure +MarkDoc is a **one-window-per-file** application. -``` -markdoc/ -├── src/ # React frontend -│ ├── components/ -│ │ ├── Viewer.tsx # Markdown preview with themes -│ │ ├── Editor.tsx # Split-pane editor with sync scroll -│ │ ├── TabBar.tsx # Multi-tab interface -│ │ ├── TabScrollControls.tsx # Tab overflow handling -│ │ ├── OpenTabsDropdown.tsx # Quick tab navigation -│ │ ├── DocumentSidebar.tsx # Document outline -│ │ ├── PerTabToolbar.tsx # Per-document controls -│ │ ├── DetachedWindow.tsx # Detached window support -│ │ └── Footer.tsx # Word count & stats display -│ ├── hooks/ -│ │ ├── useTheme.ts # OS theme detection -│ │ ├── useMarkdownTheme.ts # Markdown theme management -│ │ ├── useSyncScrollSimple.ts # Editor-preview sync -│ │ ├── useSidebarState.ts # Sidebar state management -│ │ └── useDocumentOutline.ts # Document structure parsing -│ ├── utils/ -│ │ ├── fileOpening.ts # Centralized file opening -│ │ ├── pdfExport.ts # PDF export functionality -│ │ └── linkHandler.ts # External link handling -│ ├── types/ -│ │ └── index.ts # TypeScript interfaces -│ ├── App.tsx # Main app with multi-tab support -│ ├── App.css # Vanilla CSS with system fonts -│ └── main.tsx # React entry point -├── src-tauri/ # Rust backend -│ ├── src/ -│ │ ├── lib.rs # Native menus and event handling -│ │ ├── document.rs # Document state management -│ │ └── export.rs # Export functionality -│ ├── wix/ # Windows installer customization -│ │ └── file-associations.wxs # WiX fragment for registry entries -│ ├── build.rs # Build script for version tracking -│ ├── icons/ # App icons (Stravica branding) -│ ├── Cargo.toml # Rust dependencies -│ └── tauri.conf.json # Tauri configuration -├── public/ -│ └── themes/ # Markdown theme CSS files -├── docs/ -│ └── techspecs/ # Technical specifications -└── dist/ # Vite build output -``` +- **`main` window** — created by Tauri at launch. Initially renders the `WelcomeWindow` (recents grid + Open/Help actions). When a file is opened from the welcome screen, the same window transitions in-place to `ViewerWindow` — the window label remains `main`. +- **`viewer-` windows** — spawned by the Rust backend for any _additional_ file opened while the `main` window already hosts a file. URL: `/?path=`. The React tree detects the `path` query param and renders `ViewerWindow` directly. + +The React entry is a single HTML page; `WindowRouter` picks between Welcome and Viewer based on the URL and a small in-memory state machine. + +### File-open routing (single source of truth) + +All file opens — dialog-driven, recents, OS "Open With", second-instance args — route through the Rust command `open_file_in_window(path)`. The routing logic: + +1. Canonicalise the path (handles `..`, symlinks). +2. If the canonical path is already registered to a live window → focus that window and return its label. +3. Else, if the `main` window exists and is in welcome mode (empty), emit `viewer://open-path` to main and register it under label `main`. +4. Else, allocate a new `viewer-` label, spawn the webview via `WebviewWindowBuilder`, and register. + +The frontend utility `src/utils/openFileInWindow.ts` is the only caller. It also updates recents. + +### State / preferences -## Key Implementation Details +- **Recents** — `localStorage['markdoc-recent-files']`. Up to 20 entries of `{ path, title, openedAt }`. See `src/utils/recentFiles.ts`. +- **Preferences** — `localStorage['markdoc-preferences']`. Global defaults for theme, zoom, sidebarOpen, sidebarWidth, autosize. See `src/hooks/usePreferences.ts`. Prefs do **not** propagate live between open windows; each new window reads them as its _starting_ values and mutations are local until the window is reopened. -### Native Menus (src-tauri/src/lib.rs) +When recents change, the frontend calls `invoke('refresh_menus', { recents })` so the native _File → Open Recent_ submenu stays in sync. The Rust side stores the list in `State` and rebuilds the menu. -Menus are built using Tauri's MenuBuilder API and emit events to the React frontend: +### Native menus -- **FILE Menu**: NEW, OPEN, CLOSE, SAVE, SAVE AS, RECENT FILES -- **EDIT Menu**: Standard actions (undo, redo, cut, copy, paste) + EDIT MODE toggle +Built in `src-tauri/src/lib.rs::build_menu()` using Tauri's MenuBuilder API. -Events emitted: -- `menu-file-new` -- `menu-file-open` -- `menu-file-close` -- `menu-file-save` -- `menu-file-save-as` -- `menu-edit-mode-toggle` +| Menu | Items | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| **MarkDoc** (macOS) | About, Services, Hide / Hide Others / Show All, Quit | +| **File** | Open… (⌘O), Open Recent ▸ _(dynamic)_, Close Window (⌘W), Export ▸ (HTML ⌘⇧H, PDF ⌘⇧P) | +| **Edit** | Copy, Select All (native roles only) | +| **View** | Zoom In / Out / Actual Size, Theme ▸ (Default / Cobalt / Sage / Amber / Slate), Toggle Sidebar (⌘\\), Toggle Auto-resize | +| **Window** | Minimize, Maximize, _(dynamic list of open file windows)_ | +| **Help** | User Guide | -### File Operations (src/App.tsx & src/utils/fileOpening.ts) +Menu items emit `menu:///` events that the React windows subscribe to. Window-menu clicks call `set_focus()` directly on the target window. -**CRITICAL: Centralized File Opening Architecture** +Dynamic menu items (Open Recent list, Window list) are rebuilt: -All file opening operations MUST use the centralized utility in `src/utils/fileOpening.ts`. This ensures: -1. **No duplicate tabs**: Files are checked against existing documents before opening -2. **Main window only**: Files from OS (Finder/Explorer, "Open With", drag-and-drop) ALWAYS open as tabs in the main window, never in detached windows -3. **Consistent behavior**: Manual File > Open, OS file actions, session restore, and recent files all use the same logic +- On startup (via `refresh_menus`). +- On `WindowEvent::Destroyed` when any file window closes. +- On every recents mutation (frontend calls `refresh_menus` from `src/utils/recentFiles.ts`). -**Key Functions:** -- `openFileInMainWindow(filePath, content, timestamp, activeDoc)` - Core function with duplicate detection -- `openFileByPath(filePath, activeDoc)` - Convenience wrapper that reads file content +### Rust commands -**Usage Locations:** -- Manual file opening: `openFile()` callback in App.tsx -- OS file opening (runtime): `file://open-request` event listener in App.tsx -- OS file opening (startup): Pending files handler in initialization -- Session restore: Session restoration loop -- Single-instance plugin: Emits `file://open-request` when second instance attempted +Kept minimal. See `src-tauri/src/lib.rs`. -**Backend Integration:** -- `RunEvent::Opened` in lib.rs: Handles OS file associations, emits `file://open-request` -- `tauri-plugin-single-instance`: Prevents multiple instances, emits `file://open-request` for second instance file opens +- `get_app_version()` — version + build hash. +- `get_pending_opened_files()` — drain files queued by `RunEvent::Opened` before the frontend was ready. +- `open_file_in_window(path)` — central routing (see above). +- `list_open_file_windows()` — used by the Window menu rebuild. +- `close_file_window(label)` — destroy a window by label. +- `refresh_menus(recents)` — frontend→backend sync for Open Recent submenu. +- `get_file_modified_time(path)` — millis since epoch; used for external-change detection. +- `export_html_command`, `export_pdf_command`, `cancel_export` — export flow driven by `src-tauri/src/export.rs` (PDF goes through `headless_chrome`). -Recent files (max 10) persist to localStorage using key: `markdoc-recent-files` - -### Multi-Tab Support +### Window registry (Rust) -The application supports multiple document tabs with intelligent tab management: +`src-tauri/src/window_registry.rs` — a small `Mutex` behind a Tauri `State`. Bidirectional `HashMap` with a monotonic `viewer-` allocator. Unit tests (`cargo test`) cover register/lookup/release round-trips and canonical-path collapsing (including symlinks). -- **Tab Bar**: Horizontal tabs with overflow handling -- **Tab Scrolling**: Automatic scroll buttons when tabs overflow -- **Open Tabs Dropdown**: Quick access to all open documents (Cmd/Ctrl+Shift+T) -- **Tab Switching**: Keyboard shortcuts (Cmd/Ctrl+1-9, Tab/Shift+Tab) -- **Duplicate Prevention**: Files checked before opening to prevent duplicates -- **Session Restoration**: Automatically reopens documents from previous session -- **Detached Windows**: Support for floating document windows +### Tauri Menu State Caveats (macOS) + +When manipulating native menus on macOS with Tauri 2: + +- `AppHandle::menu().get("id")` only searches direct children (top-level items). Submenu entries are not reachable through this API; attempts to `set_enabled` on them will silently fail with _"Item … not found"_. +- The reliable pattern is to clone the `MenuItem`/`Submenu` handles at creation time and retain them in shared state (`State`). Use those handles later for `set_enabled`, `append`, `remove_at`, etc. +- All menu mutations must occur on the main thread. Wrap in `app.run_on_main_thread`. +- `PredefinedMenuItem::bring_all_to_front` is not yet re-exported in Tauri 2.10 (the upstream muda 0.17 has it). Omitted from the Window menu until Tauri bumps. -### Markdown Themes (public/themes/) +### Themes -Five built-in themes for rendered markdown: -- **Default**: Clean, minimal styling -- **Sage**: Green-tinted professional theme -- **Cobalt**: Blue-tinted dark theme -- **Amber**: Warm, golden theme -- **Slate**: Modern gray theme +Five themes under `public/themes/`: `default`, `cobalt`, `sage`, `amber`, `slate`. Each ships as a standalone CSS file plus `base.css`. `useMarkdownTheme` (hook) and `Viewer` (component) apply the theme class; user preference persists in `markdoc-preferences`. -Each theme provides custom styling for: -- Headers with colored accents -- Code blocks with syntax-appropriate backgrounds -- Blockquotes with themed borders -- Tables with alternating row colors +Export bundles the theme CSS inline so exported HTML renders standalone. -### Synchronized Scrolling (src/hooks/useSyncScrollSimple.ts) +### Windows File Associations -Editor mode features percentage-based synchronized scrolling: -- Calculates scroll position as percentage (0-1) -- Applies same percentage to opposite pane -- Prevents feedback loops with source tracking -- Position preserved when switching modes -- See `docs/techspecs/editor-sync-scrolling.md` for details - -### Document Outline (src/components/DocumentSidebar.tsx) - -Collapsible sidebar showing document structure: -- Extracts headers from markdown -- Click to navigate to section -- Auto-hides on small screens -- Toggle with sidebar button +Configured in three places: -### Export Features (src/utils/pdfExport.ts) +- `src-tauri/tauri.conf.json` → `bundle.fileAssociations[0]` — registers `.md` / `.markdown`, `role: "Viewer"`, `mimeType: "text/markdown"`. This drives macOS `CFBundleTypeRole`, Linux `.desktop` file generation, and the base Windows ProgId setup. +- `src-tauri/wix/file-associations.wxs` — custom WiX fragment that adds Windows "Default Programs" integration (`HKLM\SOFTWARE\RegisteredApplications` → MarkDoc capability keys, FriendlyAppName entries, OpenWithProgIds). Component ID `FileAssociationRegistryEntries`. +- `src-tauri/tauri.conf.json` → `bundle.windows.wix.fragmentPaths` — references the fragment above. -PDF export with custom styling: -- Preserves markdown formatting -- Applies selected theme to export -- Optimized page breaks -- Print-friendly layout +The Windows installer declares MarkDoc as _"MarkDoc - A simple markdown viewer by Stravica"_. -### Theme Detection (src/hooks/useTheme.ts) +### Window close handling -Uses `window.matchMedia('(prefers-color-scheme: dark)')` to detect OS theme preference. -CSS applies theme via `@media (prefers-color-scheme: dark)` queries. +`getCurrentWindow().onCloseRequested` is _not_ used in the view-only app. There are no unsaved-changes dialogs — closing a window just destroys it. Known Tauri bugs affecting `prevent_close()` on Windows (issues #12334, #9504) are therefore not a concern for MarkDoc going forward. -### Styling Approach (src/App.css) - -Vanilla CSS with: -- System font stack: `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI"` -- Dark mode support via CSS media queries -- No CSS framework dependencies (Tailwind was removed due to v4 PostCSS compatibility issues) - -### Windows File Associations (src-tauri/wix/file-associations.wxs) - -MarkDoc uses a custom WiX fragment to register as a recommended app on Windows: - -**Basic File Association:** -- Configured in `tauri.conf.json` under `bundle.fileAssociations` -- Tauri automatically creates basic ProgId entries (`MarkDoc.md`, `MarkDoc.markdown`) -- Allows double-clicking .md/.markdown files to open in MarkDoc - -**Windows "Recommended Apps" Integration:** -- Custom WiX fragment adds registry entries for proper Windows integration -- Registers under `HKLM\SOFTWARE\RegisteredApplications` pointing to Capabilities -- Creates `Capabilities` keys with application metadata: - - `ApplicationName`: "MarkDoc" - - `ApplicationDescription`: "MarkDoc - A simple markdown viewer and editor by Stravica" - - `ApplicationIcon`: Path to MarkDoc.exe - - `FileAssociations`: Maps .md and .markdown to their ProgIds -- Adds `FriendlyAppName` to ProgId keys for proper display in "Open With" dialog -- Adds entries to `OpenWithProgids` for existing file type associations - -**Configuration:** -- Fragment path: `src-tauri/wix/file-associations.wxs` -- Referenced in `tauri.conf.json` under `bundle.windows.wix` -- Component ID: `FileAssociationRegistryEntries` -- GUID: `8F9C4A5B-2D3E-4C1F-9A8B-7E6D5C4B3A21` - -**Result:** -After installing the MSI, MarkDoc appears: -- In Windows "Recommended Apps" when right-clicking .md/.markdown files -- In "Open With" → "Choose another app" with proper branding -- In Windows Settings → Default Apps - -**Platform Notes:** -- macOS: File associations work through `CFBundleDocumentTypes` in Info.plist (handled by Tauri) -- Linux: File associations work through .desktop files (handled by Tauri) -- Windows: Requires additional registry entries (custom WiX fragment) - -### Window Close Handling (Cross-Platform) - -MarkDoc implements unsaved changes protection when closing windows, with special consideration for Windows-specific issues: - -**Implementation:** -- JavaScript close handlers in `App.tsx` (main window) and `DetachedWindow.tsx` (detached windows) -- Uses `getCurrentWindow().onCloseRequested()` to intercept close events -- Shows async `ask()` dialog if unsaved changes exist -- Calls `event.preventDefault()` to prevent close, then `window.destroy()` if user confirms - -**Known Windows Issues:** - -Several Tauri v2 bugs affect window closing on Windows: - -1. **Issue #12334**: `prevent_close()` / `event.preventDefault()` doesn't work reliably in some scenarios -2. **Issue #9504**: Close handler loses scope after minimize/maximize operations -3. **Async dialog race conditions**: Timing issues between `preventDefault()` and async dialog completion - -**Current Status:** -- Works reliably on macOS and Linux -- May experience issues on Windows in edge cases (minimize/maximize sequences, rapid close attempts) -- Rust-side utility command `respond_to_close_request` available as backup mechanism -- Known issues are upstream Tauri bugs being tracked by the Tauri team - -**Recommendations:** -- Keep Tauri dependencies updated as bugs are fixed upstream -- Test on Windows after Tauri updates to verify if issues are resolved -- For Windows-specific testing, verify close behavior after: - - Normal close (X button) - - Close after minimize/maximize sequences - - Close with multiple windows open - - Close with/without unsaved changes - -**Code Locations:** -- Main window handler: `src/App.tsx:1364-1407` -- Detached window handler: `src/components/DetachedWindow.tsx:101-142` -- Utility command: `src-tauri/src/lib.rs:776-800` +## Project Structure + +``` +markdoc/ +├── src/ # React frontend +│ ├── components/ +│ │ ├── Viewer.tsx # Markdown preview (slimmed view-only props) +│ │ ├── DocumentSidebar.tsx # Document outline +│ │ ├── Footer.tsx # Word count / reading time +│ │ ├── ExportOverlay.tsx # Export progress UI +│ │ └── Tooltip.tsx +│ ├── windows/ +│ │ ├── WelcomeWindow.tsx # Recents + Open/Help actions +│ │ ├── ViewerWindow.tsx # Per-file viewer (toolbar + content + footer) +│ │ └── WindowRouter.tsx # Picks Welcome vs Viewer +│ ├── hooks/ +│ │ ├── useTheme.ts # OS theme detection +│ │ ├── useMarkdownTheme.ts # Markdown theme selection +│ │ ├── useSidebarState.ts +│ │ ├── useDocumentOutline.ts # Heading extraction from rendered HTML +│ │ ├── useWindowResize.ts # Autosize behaviour +│ │ ├── useZoom.ts +│ │ └── usePreferences.ts # Global prefs (localStorage-backed) +│ ├── utils/ +│ │ ├── openFileInWindow.ts # Central file-open router +│ │ ├── recentFiles.ts # Recents store + native-menu sync +│ │ ├── pdfExport.ts # HTML generation for PDF export +│ │ ├── linkHandler.ts # External link routing + XSS guards +│ │ └── fileUtils.ts +│ ├── platform/ +│ │ ├── index.ts # Selects tauri vs web target +│ │ ├── tauri.ts # Real platform bridge +│ │ └── web.ts # In-memory mock for web-mode tests +│ ├── fixtures/ # sample-one.md / sample-two.md for tests +│ ├── constants/userguide.ts # Inline user guide content +│ ├── types/index.ts # Preferences, RecentFileEntry, ThemeName, etc. +│ ├── App.tsx # Small root — global listeners + WindowRouter +│ ├── App.css # Vanilla CSS +│ └── main.tsx # React entry point +├── src-tauri/ # Rust backend +│ ├── src/ +│ │ ├── lib.rs # Commands, menus, event routing +│ │ ├── window_registry.rs # PathBuf ↔ WindowLabel registry +│ │ └── export.rs # PDF rendering via headless_chrome +│ ├── wix/file-associations.wxs # Windows installer registry entries +│ ├── build.rs # GIT_COMMIT_HASH at build time +│ ├── icons/ # App icons +│ ├── Cargo.toml +│ └── tauri.conf.json +├── tests/e2e/ # Playwright specs (web-mode harness) +├── public/themes/ # Markdown theme CSS (+ base.css) +├── docs/ +└── .github/workflows/ # ci.yml + release.yml +``` ## Development Commands ```bash -# Install dependencies +# Install npm install -# Development mode (hot reload) -npm run tauri dev - -# Build frontend only -npm run build +# Desktop dev (hot reload) +npm run tauri:dev + +# Web-mode dev (for Playwright; no Tauri IPC — uses mock platform bridge) +npm run dev:web # → http://localhost:1420 + +# Build +npm run build # Frontend only (tsc + vite build) +npm run tauri:build # Full desktop bundle + +# Quality gates +npm run typecheck # tsc --noEmit +npm run lint # eslint . +npm run lint:fix +npm run format +npm run format:check +npm run test:unit # Vitest (watch: test:unit:watch) +npm run test:coverage # Vitest + v8 coverage +npm run test:e2e # Playwright (web-mode) +npm run test:e2e:headed +npm run test:all # typecheck + lint + unit + e2e +``` -# Build production app -npm run tauri build +### Rust gates -# Lint -npm run lint +```bash +cd src-tauri +cargo check --locked +cargo clippy --locked -- -D warnings +cargo test --locked ``` ## CI/CD Pipeline -### GitHub Actions Release Workflow (.github/workflows/release.yml) +### PR CI (`.github/workflows/ci.yml`) + +Triggered on every `pull_request` and `push` to `main`. Two jobs: + +- **frontend** — typecheck, lint, format:check, Vitest, Playwright (headless chromium) +- **rust** — cargo check, cargo clippy `-D warnings`, cargo test -Automated release process triggered by version tags (e.g., `v0.1.4`): +Both run on `ubuntu-latest` with `Swatinem/rust-cache@v2` + `actions/setup-node@v4` caching. -1. **Version Update**: Script updates version in all 3 locations -2. **Multi-Platform Build**: Builds for macOS (x64 & ARM64), Windows, Linux -3. **Asset Generation**: Creates DMG, MSI, DEB, AppImage installers -4. **Release Notes**: Auto-generates from commit messages -5. **GitHub Release**: Creates release with all artifacts +### Release workflow (`.github/workflows/release.yml`) + +Triggered by `workflow_dispatch` with `version_type` input: + +1. `prepare-release` — bumps version in all 3 sync locations, generates release notes, tags. +2. `verify` — runs all frontend + rust gates on Ubuntu (prevents shipping a broken release). +3. `build-tauri` — matrix (macOS arm64, macOS x86_64, Ubuntu, Windows). `needs: [prepare-release, verify]`. +4. `attach-app-bundles` — zips macOS .app artifacts into the release. ### Release Process ```bash -# 1. Update version in package.json, Cargo.toml, tauri.conf.json -# 2. Commit changes -git add -A && git commit -m "chore: bump version to v0.1.4" +# Bump versions +# Commit +git commit -am "chore: bump version to v0.1.6" +# Tag + push — GitHub Actions will verify, build, and release +git tag v0.1.6 && git push origin main --tags +``` -# 3. Create and push tag -git tag v0.1.4 -git push origin main --tags +## Build Output Locations -# 4. GitHub Actions automatically builds and releases +- macOS App: `src-tauri/target/release/bundle/macos/MarkDoc.app` +- macOS DMG: `src-tauri/target/release/bundle/dmg/MarkDoc__aarch64.dmg` +- Windows MSI: `src-tauri/target/release/bundle/msi/MarkDoc__x64_en-US.msi` +- Linux DEB: `src-tauri/target/release/bundle/deb/markdoc__amd64.deb` +- Linux AppImage: `src-tauri/target/release/bundle/appimage/markdoc__amd64.AppImage` + +## Testing + +### Agent / UI testing workflow + +The app ships a **web-mode** harness for fast UI e2e without Tauri. `npm run dev:web` swaps in `src/platform/web.ts` — a full in-memory `MockBackend` that records invoke calls and emits events. The mock handles `open_file_in_window`, `list_open_file_windows`, `get_app_version`, `export_html_command`, `refresh_menus`, and the theme/zoom/sidebar event bus. + +Stable `data-testid` selectors are present across Welcome (`welcome-root`, `welcome-open-button`, `welcome-recent-item`, …) and Viewer (`viewer-root`, `viewer-toolbar`, `viewer-zoom-in`, `viewer-theme-select`, `viewer-help-button`, …). + +Drive menu events from Playwright via: + +```ts +await page.evaluate(() => window.__MARKDOC_MOCK__?.emit('menu://file/open')); ``` -## Build Output Locations +Inspect recorded invoke calls via: -- **macOS App**: `src-tauri/target/release/bundle/macos/MarkDoc.app` -- **macOS DMG**: `src-tauri/target/release/bundle/dmg/MarkDoc_0.1.4_aarch64.dmg` -- **Windows MSI**: `src-tauri/target/release/bundle/msi/MarkDoc_0.1.4_x64_en-US.msi` -- **Linux DEB**: `src-tauri/target/release/bundle/deb/markdoc_0.1.4_amd64.deb` -- **Linux AppImage**: `src-tauri/target/release/bundle/appimage/markdoc_0.1.4_amd64.AppImage` +```ts +const calls = await page.evaluate(() => window.__MARKDOC_MOCK__?.backend.calls); +``` + +### Test coverage + +- **Unit (Vitest)** — 93 tests across 9 files. `src/hooks` 99% lines, `src/utils` 96% lines. +- **E2E (Playwright)** — 16 tests across 8 specs (welcome, viewer-open, theme, zoom, sidebar, export, help, autoresize). +- **Rust** — 10 tests in `window_registry` covering register/lookup/release/canonical-path collapsing. + +### Pre-release smoke checklist + +- [ ] Fresh launch shows Welcome with empty or persisted recents +- [ ] Welcome "Open File…" transitions the same window into viewer mode +- [ ] Opening a second file spawns a new `viewer-` window +- [ ] Opening an already-open file focuses the existing window +- [ ] File > Open via Cmd+O works from both Welcome and Viewer +- [ ] Export HTML + Export PDF produce correct output +- [ ] Theme switch cycles all 5 themes and persists after reload +- [ ] Zoom in/out/reset via toolbar + Cmd± / Cmd0 +- [ ] Cmd+\\ toggles the outline sidebar +- [ ] Auto-resize toggle reflects in window sizing +- [ ] Help icon / Help menu > User Guide shows bundled markdown +- [ ] Window menu lists all open viewers; click focuses +- [ ] Cmd+W closes the current window; macOS keeps the app running with `RunEvent::Reopen` re-opening welcome on dock click +- [ ] Finder "Open With MarkDoc" (macOS) + Explorer "Open With" (Windows) route to the live process and open in a window +- [ ] About dialog shows version + short git commit hash +- [ ] Windows: installer shows MarkDoc as "Viewer" in Default Apps + Open With dialog ## Known Issues & Gotchas -### Bundle Identifier -The bundle identifier is `com.stravica.markdoc`. Avoid using identifiers that end with `.app`, as that conflicts with the macOS bundle extension. - -### Chunk Size Warning -The main JS bundle is ~1MB due to CodeMirror and markdown-it. Consider code-splitting if this becomes an issue: -```javascript -// vite.config.ts -build: { - rollupOptions: { - output: { - manualChunks: { - 'codemirror': ['@codemirror/view', '@codemirror/state', '@codemirror/lang-markdown'], - 'markdown': ['markdown-it'] - } - } - } -} -``` +### Bundle size + +The main JS bundle is ~630 kB (gzipped 204 kB), dominated by `markdown-it` and `prismjs` (~300 language grammars). If chunk-size warnings become actionable, pull Prism + markdown-it into their own chunks via `vite.config.ts` `build.rollupOptions.output.manualChunks`. + +### Tailwind is not used + +Class names like `h-full`, `mx-auto` in components are **vanilla CSS** defined in `src/App.css`, not Tailwind. The Tailwind toolchain was removed in the view-only refactor (no `@apply` or `@tailwind` directives remain). + +### Rust Toolchain + +Dev requires Rust stable: -### Rust Toolchain Required -Development requires Rust stable toolchain. Install via: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup default stable ``` -### Tailwind CSS Was Removed -Originally attempted to use Tailwind CSS v4, but PostCSS compatibility issues led to switching to vanilla CSS. Do not re-add Tailwind without careful consideration. +### Major version bumps pending -### Tauri Menu State Caveats (macOS) +These were intentionally deferred in the last dependency sweep — review separately before accepting: -When manipulating native menus on macOS with Tauri 2: +- TypeScript 5.9 → 6.0 +- Vite 7 → 8 (+ `@vitejs/plugin-react` 5 → 6) +- ESLint 9 → 10 (held by `eslint-plugin-jsx-a11y` peer range) +- `isomorphic-dompurify` 2 → 3 -- `AppHandle::menu()` returns the top-level `Menu`, but calling `menu.get("item_id")` only searches direct children (top-level items). Submenu entries are not reachable through this API; attempts to enable/disable them will silently fail with "Item … not found". -- The reliable approach is to clone the `MenuItem`/`Submenu` handles returned at creation time and retain them in shared state (`State`). Use these handles later when calling `set_enabled`, `append`, `remove_at`, etc. (See `src-tauri/src/lib.rs` for the pattern.) -- All menu mutations must occur on the main thread. Wrap calls in `app.run_on_main_thread` and propagate results back to the caller (otherwise macOS ignores the update). -- Guard frontend RPCs (`invoke`) until the backend has initialised the menu and populated the stored handles. The current app exposes an `is_menu_ready` command and emits a `menu://ready` event once setup completes—reuse this pattern when adding new commands. -- Keep recent-file mappings on the Rust side (we use a `HashMap` keyed by the generated menu item ids) so menu event handlers can map back to filesystem paths without recomputing or relying on frontend state. -- If you introduce new menu items, update the `MenuHandles` struct and the match arms inside `update_menu_state`; missing entries will yield "Unknown menu item" errors. +### Single-instance plugin -## Configuration Files +`tauri-plugin-single-instance` is enabled. When a second copy of MarkDoc is launched, it forwards its argv to the first instance, which routes each argument through `open_file_in_window`. Do not remove the plugin — doing so would let duplicate processes coexist and break routing. -### tauri.conf.json -- Product name, version, bundle identifier -- Window settings (1200x800 default, 600x400 minimum) -- Build commands and paths -- Plugin configuration (must be empty object `{}`) - -### Cargo.toml -- Rust dependencies (all version "2.4") -- Library name must be `markdoc_lib` to avoid Windows conflicts - -### postcss.config.js -Currently configured for potential future needs: -```javascript -export default { - plugins: { - '@tailwindcss/postcss': {}, - autoprefixer: {}, - }, -} -``` +## Configuration Files -## Testing Checklist - -Before releases, verify: -- [ ] All menu items work (FILE, EDIT, VIEW menus) -- [ ] File operations (NEW, OPEN, CLOSE, SAVE, SAVE AS) -- [ ] Recent files tracking and persistence -- [ ] Edit mode toggle (Cmd+E / Ctrl+E) with synchronized scrolling -- [ ] Split-pane editor with live preview -- [ ] Markdown rendering (headers, lists, code blocks, links, tables, blockquotes) -- [ ] All 5 markdown themes render correctly -- [ ] Document outline sidebar navigation -- [ ] Multi-tab interface with overflow handling -- [ ] Tab switching keyboard shortcuts (Cmd/Ctrl+1-9, Tab navigation) -- [ ] Open tabs dropdown functionality -- [ ] Session restoration on app restart -- [ ] PDF export with theme preservation -- [ ] Word count and reading time display -- [ ] Zoom controls (in/out/reset) -- [ ] Dark mode switches with OS preference -- [ ] Native unsaved changes dialogs -- [ ] Detached window support -- [ ] File duplicate prevention -- [ ] OS file associations ("Open With" functionality) -- [ ] Windows: MarkDoc appears in "Recommended Apps" list -- [ ] Windows: MarkDoc shows proper name (not .exe) in "Open With" dialog -- [ ] Cross-platform compatibility (macOS, Windows, Linux) - -### Agent/UI testing workflow (recommended) -- **Web-mode harness for fast UI e2e**: Run `npm run dev:web` (uses `.env.web` to set `VITE_TARGET=web`) to swap in the mock platform bridge. Dialogs, FS, and backend commands are mocked with deterministic fixtures (`src/fixtures/sample-one.md`, `src/fixtures/sample-two.md`). Use Playwright MCP against `http://localhost:5173` and drive stable selectors (`data-testid` across tabs, toolbar, editor/viewer, sidebar, export overlay). You can trigger menu events via `window.__MARKDOC_MOCK__?.emit('menu://file_open')` in the page context to open fixtures without native dialogs. -- **Native desktop coverage**: For Tauri-specific behavior (native menus, real dialogs, file associations), use `tauri-driver`/WebDriver after the fast web-mode pass. - -## Recent Features (v0.1.4) - -Successfully implemented: -- ✅ Export to PDF with custom styling -- ✅ Multiple document tabs with intelligent management -- ✅ Custom CSS themes (5 built-in themes) -- ✅ Word count and reading time statistics -- ✅ Document outline sidebar -- ✅ Session restoration -- ✅ Synchronized scrolling in edit mode -- ✅ Zoom controls -- ✅ Tab overflow handling -- ✅ Open tabs dropdown -- ✅ Native unsaved changes dialogs -- ✅ Centralized file opening (prevents duplicates) -- ✅ Detached window support - -## Future Enhancements - -Potential features to consider: -- Export to HTML -- Custom user-defined themes -- Markdown table editor -- Syntax highlighting for code blocks in preview -- Search within document (Cmd/Ctrl+F) -- Vim/Emacs keybindings option -- Drag-and-drop file opening -- Plugin system for extensions -- Markdown extensions (mermaid diagrams, math equations) -- Split view for comparing documents -- Version control integration -- Live collaboration features +- `tauri.conf.json` — product name, identifiers, window config, capabilities, bundle settings. +- `Cargo.toml` — lib name must be `markdoc_lib` (avoids Windows binary conflict). +- `vite.config.ts` — dev server + build config; single HTML entry. +- `vitest.config.ts` — jsdom env + `tests/setup.ts` import. +- `eslint.config.js` — flat config, TypeScript strict + React + jsx-a11y; existing code has `warn`-level noise that should be cleaned up opportunistically. +- `playwright.config.ts` — launches `npm run dev:web` on port 1420. +- `.prettierrc.json` / `.prettierignore` — 100-col, single quotes, trailing commas. ## Dependencies to Monitor -Key dependencies that should be kept up-to-date: -- `tauri` and all `tauri-plugin-*` packages -- `@codemirror/*` packages -- `markdown-it` -- `react` and `react-dom` -- `vite` +Keep these current: + +- `tauri` + `tauri-plugin-*` (currently 2.10.x) +- `react`, `react-dom`, `@types/react(-dom)` (19.x) +- `vite`, `@vitejs/plugin-react` (7.x / 5.x) +- `markdown-it`, `prismjs`, `isomorphic-dompurify` +- Vitest + Testing Library + Playwright — keep aligned -Check for updates regularly: `npm outdated` +Run `npm outdated` and `cd src-tauri && cargo update --dry-run` periodically. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f17aab6..f47fe17 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,16 +11,18 @@ By participating in this project, you agree to maintain a respectful and inclusi ### Reporting Bugs If you find a bug, please create an issue with: + - A clear, descriptive title - Steps to reproduce the issue - Expected behavior vs actual behavior - Screenshots if applicable -- Your OS version and app version +- Your OS version and app version (the Welcome screen shows the full build hash) - Any error messages from the console ### Suggesting Features -Feature suggestions are welcome! Please: +Feature suggestions are welcome. MarkDoc is intentionally a **view-only** markdown reader with a one-window-per-file model, so please keep that scope in mind. Suggestions that would reintroduce in-app editing, tabs, or multi-document windows are out of scope. + - Check existing issues to avoid duplicates - Clearly describe the feature and its use case - Explain why this feature would be useful to most users @@ -28,65 +30,85 @@ Feature suggestions are welcome! Please: ### Pull Requests 1. **Fork the repository** and create your branch from `main` + ```bash git checkout -b feature/my-new-feature ``` 2. **Set up your development environment** + ```bash npm install - npm run tauri dev + + # Desktop dev with hot reload (requires Rust toolchain) + npm run tauri:dev + + # Or — fast UI loop without Tauri, using the mock backend + npm run dev:web ``` 3. **Make your changes** - Write clear, commented code - Follow the existing code style - Keep commits atomic and well-described - - Update documentation if needed + - Update documentation (`README.md`, `CLAUDE.md`, `docs/`) when behavior changes + +4. **Run the quality gates locally** -4. **Test your changes** - - Test in both viewer and editor modes - - Verify all menu items work - - Test file operations (open, save, close) - - Check both light and dark themes - - Test on your target platform (macOS/Windows/Linux) + ```bash + npm run typecheck # tsc --noEmit + npm run lint # eslint . + npm run format:check # prettier --check . + npm run test:unit # Vitest + npm run test:e2e # Playwright (auto-starts dev:web) + npm run test:all # typecheck + lint + unit + e2e + ``` + + Rust-side checks live under `src-tauri/`: -5. **Run the linter** ```bash - npm run lint + cd src-tauri + cargo check --locked + cargo clippy --locked -- -D warnings + cargo test --locked ``` -6. **Build the app** +5. **Build the app** (optional, but recommended for PRs touching native code) + ```bash - npm run tauri build + npm run tauri:build ``` -7. **Submit your pull request** +6. **Submit your pull request** - Provide a clear description of the changes - Reference any related issues - Include screenshots for UI changes +CI (`.github/workflows/ci.yml`) re-runs typecheck, lint, `format:check`, Vitest, Playwright (headless chromium), `cargo check --locked`, `cargo clippy --locked -- -D warnings`, and `cargo test --locked` on every PR. Keeping those green locally will save review round-trips. + ## Development Guidelines ### Code Style -- **TypeScript**: Use TypeScript for all frontend code -- **React**: Use functional components with hooks -- **Naming**: Use descriptive variable and function names -- **Comments**: Add comments for complex logic -- **Formatting**: Code will be auto-formatted (consider running `npm run lint --fix`) +- **TypeScript**: all frontend code is TS. `npm run typecheck` must pass with no errors. +- **React**: functional components with hooks. The top level is split into per-window entry points (`src/windows/WelcomeWindow.tsx`, `src/windows/ViewerWindow.tsx`) wired up by `WindowRouter.tsx`. +- **Naming**: descriptive variable and function names; match existing patterns in sibling files. +- **Formatting**: prettier owns formatting for `.ts`, `.tsx`, `.css`, `.md`, `.json`. Run `npm run format` before pushing; CI runs `format:check`. +- **Linting**: `npm run lint` (eslint with `typescript-eslint`, `react`, `react-hooks`, `jsx-a11y`, and `prettier` configs). ### Commit Messages Use clear, descriptive commit messages: + ``` feat: Add export to PDF functionality fix: Resolve dark mode code block styling docs: Update README with new keyboard shortcuts -refactor: Simplify file state management +refactor: Simplify window registry key lookup ``` Prefixes: + - `feat:` - New features - `fix:` - Bug fixes - `docs:` - Documentation changes @@ -99,75 +121,106 @@ Prefixes: ``` src/ -├── components/ # React components -├── hooks/ # Custom React hooks -├── types/ # TypeScript type definitions -└── App.tsx # Main application component +├── windows/ # Per-window React entry points +│ ├── WelcomeWindow.tsx +│ ├── ViewerWindow.tsx +│ └── WindowRouter.tsx # Picks the right window based on the Tauri label +├── components/ # Shared UI (Viewer, DocumentSidebar, Footer, ExportOverlay, …) +├── hooks/ # useZoom, useTheme, useMarkdownTheme, useSidebarState, … +│ └── __tests__/ # Colocated Vitest specs +├── utils/ # openFileInWindow, recentFiles, pdfExport, fileUtils, … +│ └── __tests__/ +├── platform/ # Platform bridge: tauri.ts (native) + web.ts (mock) +├── fixtures/ # sample-one.md, sample-two.md (seeded by the web mock) +└── types/ # Shared TypeScript types src-tauri/ └── src/ - └── lib.rs # Rust backend with native menus + ├── lib.rs # Native menus, event emission, Tauri commands + ├── window_registry.rs # PathBuf ↔ WindowLabel registry, cargo-tested + └── export.rs # HTML + PDF export helpers ``` +### Architecture notes + +MarkDoc is view-only and one-window-per-file. When adding features, keep the following in mind: + +- **All file opens go through `open_file_in_window(path)`** (Rust command in `src-tauri/src/lib.rs`, frontend wrapper in `src/utils/openFileInWindow.ts`). This covers the File menu, recents, OS "Open With…", and second-instance forwarding via `tauri-plugin-single-instance`. Do not bypass it — path canonicalisation and duplicate-window detection live inside the registry. +- **`src-tauri/src/window_registry.rs`** keeps the bidirectional `PathBuf ↔ WindowLabel` mapping and allocates monotonic `viewer-` labels. Any new command that creates or destroys file windows must update the registry. +- **Menu events use the `menu:///` namespace.** Native menu items emit events; React windows subscribe via `listen('menu://…')`. When adding a menu item, update the Rust menu definition in `lib.rs`, the mock in `src/platform/web.ts` if e2e needs it, and the listener in the target window component. +- **The platform bridge (`src/platform/index.ts`)** selects `tauri.ts` or `web.ts` based on `VITE_TARGET`. New `invoke` commands must be added to both the Rust command list and the `MockBackend` in `web.ts`; otherwise the web-mode harness and Playwright runs will break. + ### Key Files -- `src/App.tsx` - Main app logic, file operations, menu event handlers -- `src/components/Viewer.tsx` - Markdown rendering component -- `src/components/Editor.tsx` - Split-pane editor with CodeMirror -- `src-tauri/src/lib.rs` - Native menu definitions and event emission -- `src-tauri/tauri.conf.json` - Tauri configuration -- `CLAUDE.md` - Development notes (update when making significant changes) +- `src/windows/WelcomeWindow.tsx` - Welcome screen with recent files +- `src/windows/ViewerWindow.tsx` - Viewer chrome + toolbar + outline +- `src/components/Viewer.tsx` - Markdown rendering (markdown-it + Prism + DOMPurify) +- `src/utils/openFileInWindow.ts` - Frontend entry point that wraps the Rust command +- `src-tauri/src/lib.rs` - Native menus, Tauri commands, RunEvent handling +- `src-tauri/src/window_registry.rs` - Path-keyed window registry (with cargo tests) +- `src-tauri/tauri.conf.json` - Tauri configuration and file associations +- `CLAUDE.md` - Architecture notes (update when making significant changes) ### Adding Dependencies Before adding new dependencies: + 1. Check if the functionality can be implemented without a new dependency 2. Verify the package is actively maintained 3. Check the package size (keep the bundle small) -4. Ensure it's compatible with Tauri +4. Ensure it's compatible with Tauri and React 19 5. Update `package.json` with `npm install ` 6. Document the dependency in `CLAUDE.md` if significant -### Testing Checklist +### Testing Guidance + +- **Unit tests** — [Vitest](https://vitest.dev/) with jsdom + `@testing-library/react`. Colocate specs in `__tests__/` next to the module under test (see `src/hooks/__tests__/` and `src/utils/__tests__/` for the pattern). `npm run test:unit` runs them all; `npm run test:coverage` produces a v8 coverage report. +- **Coverage targets** — `src/hooks` and `src/utils` should stay above 70% line coverage; current coverage is 96%+. New hooks/utils should ship with specs. +- **E2E tests** — [Playwright](https://playwright.dev/) driven by the web-mode harness. `npm run test:e2e` boots `npm run dev:web` under the hood. Use stable `data-testid` selectors; avoid text-based selectors where the string may change. See `docs/testing.md` for the full list. +- **Rust tests** — `cd src-tauri && cargo test --locked`. The window registry ships with integration tests; any new shared Rust logic should be covered similarly. + +### Pre-PR Checklist Before submitting a PR, verify: -- [ ] App runs in dev mode: `npm run tauri dev` -- [ ] App builds successfully: `npm run tauri build` -- [ ] All menu items work correctly -- [ ] File operations work (NEW, OPEN, SAVE, SAVE AS, CLOSE) -- [ ] Edit mode toggle works (Cmd/Ctrl+E) -- [ ] Markdown renders correctly in viewer mode -- [ ] Split-pane editor shows live preview -- [ ] Dark/light theme detection works -- [ ] Recent files tracking works + +- [ ] App runs in dev mode: `npm run tauri:dev` +- [ ] Mock-backed UI runs: `npm run dev:web` and the app renders at `http://localhost:5173` +- [ ] `npm run test:all` passes (typecheck + lint + Vitest + Playwright) +- [ ] `cargo check --locked`, `cargo clippy --locked -- -D warnings`, and `cargo test --locked` pass inside `src-tauri/` +- [ ] `npm run format:check` is clean +- [ ] All menu items emit/handle the right `menu://…` events +- [ ] Opening the same file twice focuses the existing window (duplicate detection) +- [ ] Light/dark themes both render correctly +- [ ] Recent files update and persist across relaunches - [ ] No console errors or warnings -- [ ] Linter passes: `npm run lint` ## Areas for Contribution Looking for where to start? Here are some areas that need help: ### High Priority -- Export to HTML/PDF functionality -- Syntax highlighting for code blocks in rendered view + - Search within document -- Comprehensive test coverage +- Drag-and-drop file opening +- Richer syntax highlighting (languages beyond the current Prism bundle) +- Additional Playwright coverage around export flows ### Medium Priority -- Custom CSS themes -- Word count and reading time statistics -- Drag-and-drop file opening -- Multiple document tabs + +- User-defined custom themes +- Mermaid diagrams / math equations in rendered markdown +- Print support beyond PDF export +- Accessibility polish (focus trapping, screen-reader verification) ### Low Priority -- Vim/Emacs keybindings -- Plugin system for extensions -- Custom keyboard shortcuts -- Table editor + +- Additional keyboard shortcut customisation +- Further documentation / tutorials ## Questions? If you have questions about contributing: + - Check existing [issues](../../issues) and [discussions](../../discussions) - Open a new discussion for general questions - Create an issue for specific bugs or features diff --git a/README.md b/README.md index e31bdff..2894c9b 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,103 @@ # MarkDoc -A lightweight, cross-platform desktop application for viewing and editing Markdown files with a clean, native-looking interface. By Stravica. +A lightweight, cross-platform desktop application for **viewing** Markdown files with a clean, native-looking interface. One window per file. No editor. By Stravica. ## Features -- **Multi-Tab Interface** - - Multiple document tabs with easy switching - - Tab overflow controls with scroll buttons - - Open tabs dropdown for quick navigation - - Detached window support for multi-monitor workflows - - Automatic session restoration (reopens last open documents) - -- **Dual Mode Interface** - - Default viewer mode for distraction-free reading - - Split-pane editor mode with live preview - - Toggle between modes with `Cmd/Ctrl+E` - - Synchronized scrolling between editor and preview - -- **Native Experience** - - True native menus (not web-based) - - System fonts (San Francisco on macOS, Segoe UI on Windows) - - Automatic dark/light theme based on OS preference - - Tiny bundle size (3.6 MB DMG vs 80-120 MB for Electron apps) - - OS-compliant menu structure for each platform - -- **Markdown Themes** - - Multiple built-in themes (Default, Sage, Cobalt, Amber, Slate) - - Theme selector per document - - Custom CSS styling for rendered markdown - - Zoom controls for better readability - -- **Full Markdown Support** - - CommonMark compliant rendering - - GitHub Flavored Markdown features - - Syntax highlighting in editor - - Tables, code blocks, blockquotes, and more - - Collapsible document outline sidebar - -- **File Management** - - Open and save Markdown files - - Recent files tracking (last 10 files) - - Auto-save support - - Native unsaved changes dialogs - - Centralized file opening (prevents duplicates) - - OS file associations ("Open With" support) - -- **Export Options** - - Export to PDF with custom styling - - Print support with optimized layout - - Copy formatted HTML to clipboard - -- **Document Analysis** - - Word count and character count - - Reading time estimation - - Document outline navigation +- **One window per file** + - Opening a file either transitions the Welcome window into a viewer in place, or spawns a new viewer window. + - Opening an already-open file focuses the existing window (path-based duplicate detection, canonicalised so `..`, symlinks, and equivalent paths collapse to the same window). + - OS "Open With…", `tauri-plugin-single-instance`, recents, and the File menu all route through the same Rust command (`open_file_in_window`). + +- **Welcome screen** + - Shown when the app launches with no file, or when you open the app with no arguments via the dock/taskbar. + - Lists recent files (up to 20) with title + path, click-to-open, remove-one, and clear-all. + - "Open File…" and "Help" actions alongside the current version + build-hash. + +- **Native experience** + - True native menus (File, Edit, View, Window, Help — plus the MarkDoc app menu on macOS). + - System fonts (San Francisco on macOS, Segoe UI on Windows). + - Automatic dark/light theme following OS preference. + - Tiny bundle — Tauri ships the OS WebView rather than bundling Chromium. + +- **Rendering** + - CommonMark-compliant via `markdown-it`. + - Prism syntax highlighting in code blocks. + - Sanitised HTML + external-link routing via `tauri-plugin-opener`. + - Collapsible document outline sidebar auto-built from headings. + +- **Themes** + - Five built-in themes: Default, Cobalt, Sage, Amber, Slate. + - Choice persists in `localStorage` and applies to every new window opened after the change. + +- **Zoom + Auto-resize** + - `Cmd/Ctrl +` / `-` / `0` for zoom in/out/reset; persisted per viewer. + - Optional auto-resize fits the window to document width on load. + +- **Export** + - Export to HTML (standalone, theme CSS inlined). + - Export to PDF via headless Chromium (`headless_chrome`). + - Export progress overlay with cancel support. + +- **OS integration** + - File associations for `.md` / `.markdown` on macOS, Windows, and Linux. + - Windows installer registers MarkDoc in Default Apps / Open With as a "Viewer". + - Second-instance launches forward their argv into the live process. + + ## Screenshots -### Viewer Mode -Clean, centered layout for reading rendered Markdown. +### Welcome screen -### Editor Mode -Split-pane with live preview for editing and writing. +Recents grid with Open / Help actions and the current build version. -## Installation +### Viewer -### Direct Downloads (v0.1.4) +Rendered markdown with optional outline sidebar, theme selector, zoom, and export controls. -#### macOS -- **[Download .app Bundle](https://github.com/Stravica/markdoc/releases/download/v0.1.4/MarkDoc_x86_64.app.zip)** (Intel/Apple Silicon Universal) -- **[Download DMG Installer](https://github.com/Stravica/markdoc/releases/download/v0.1.4/MarkDoc_0.1.4_x64_darwin.dmg)** (Intel x86_64) +## Installation -#### Windows -- **[Download Installer](https://github.com/Stravica/markdoc/releases/download/v0.1.4/MarkDoc_0.1.4_x64_en-US_windows.msi)** (64-bit MSI) +### Direct Downloads -#### Linux -- **[Download .deb Package](https://github.com/Stravica/markdoc/releases/download/v0.1.4/MarkDoc_0.1.4_amd64_linux.deb)** (Debian/Ubuntu) -- Also available: AppImage from the [Releases](../../releases) page +See the [Releases](../../releases) page for the latest installers. + +- **macOS** — `.app.zip` (Apple Silicon) and `.dmg` (Intel) +- **Windows** — `.msi` installer (64-bit) +- **Linux** — `.deb` (Debian/Ubuntu) and `.AppImage` ### Installation Instructions #### macOS -**Option 1 - App Bundle (Recommended for quick access):** -1. Download the `.app.zip` file -2. Extract the zip file -3. Drag `MarkDoc.app` to your Applications folder -4. Right-click and select "Open" the first time (to bypass Gatekeeper) +**Option 1 — App Bundle:** + +1. Download the `.app.zip` file. +2. Extract and drag `MarkDoc.app` to your Applications folder. +3. Right-click → "Open" the first time to bypass Gatekeeper. -**Option 2 - DMG Installer:** -1. Download the `.dmg` file -2. Open the DMG -3. Drag MarkDoc to your Applications folder +**Option 2 — DMG Installer:** + +1. Download the `.dmg` file. +2. Open the DMG and drag MarkDoc to your Applications folder. #### Windows -1. Download the `.msi` installer -2. Run the installer -3. Follow the installation wizard +1. Download the `.msi` installer. +2. Run it and follow the wizard. + +After install, MarkDoc appears in Settings → Default Apps and in the right-click "Open With" menu as a registered Markdown viewer. #### Linux **For Debian/Ubuntu (using .deb):** + ```bash -sudo dpkg -i MarkDoc_0.1.4_amd64_linux.deb +sudo dpkg -i MarkDoc__amd64.deb ``` **For other distributions (using AppImage):** + ```bash chmod +x MarkDoc_*.AppImage ./MarkDoc_*.AppImage @@ -114,43 +105,34 @@ chmod +x MarkDoc_*.AppImage ## Usage -### Keyboard Shortcuts - -- `Cmd/Ctrl+N` - New document -- `Cmd/Ctrl+O` - Open file -- `Cmd/Ctrl+T` - New tab -- `Cmd/Ctrl+W` - Close tab -- `Cmd/Ctrl+S` - Save -- `Cmd/Ctrl+Shift+S` - Save As -- `Cmd/Ctrl+E` - Toggle Edit Mode -- `Cmd/Ctrl+P` - Print/Export to PDF -- `Cmd/Ctrl+Plus` - Zoom in -- `Cmd/Ctrl+Minus` - Zoom out -- `Cmd/Ctrl+0` - Reset zoom -- `Cmd/Ctrl+Shift+T` - Toggle open tabs dropdown -- `Cmd/Ctrl+1-9` - Switch to tab 1-9 -- `Cmd/Ctrl+Tab` - Next tab -- `Cmd/Ctrl+Shift+Tab` - Previous tab - -### Menu Options - -**File Menu** -- NEW - Create a new document -- OPEN - Open an existing Markdown file -- CLOSE - Close the current document -- SAVE - Save changes -- SAVE AS - Save with a new filename -- RECENT FILES - Quick access to recently opened files - -**Edit Menu** -- Standard editing commands (Undo, Redo, Cut, Copy, Paste, Select All) -- EDIT MODE - Toggle between viewer and editor modes - -## Building from Source +### Keyboard shortcuts + +- `Cmd/Ctrl+O` — Open file +- `Cmd/Ctrl+W` — Close window +- `Cmd/Ctrl+Shift+H` — Export to HTML +- `Cmd/Ctrl+Shift+P` — Export to PDF +- `Cmd/Ctrl++` / `=` — Zoom in +- `Cmd/Ctrl+-` — Zoom out +- `Cmd/Ctrl+0` — Reset zoom +- `Cmd/Ctrl+\` — Toggle outline sidebar + +### Menus + +**File** — Open…, Open Recent ▸ _(dynamic)_, Close Window, Export ▸ (HTML / PDF) + +**Edit** — Copy, Select All (native roles) + +**View** — Zoom In / Out / Actual Size, Theme ▸ (Default / Cobalt / Sage / Amber / Slate), Toggle Sidebar, Toggle Auto-resize + +**Window** — Minimize, Maximize, _(dynamic list of open file windows — click to focus)_ + +**Help** — User Guide + +## Building from source ### Prerequisites -- [Node.js](https://nodejs.org/) 18+ and npm +- [Node.js](https://nodejs.org/) 20+ and npm - [Rust](https://rustup.rs/) stable toolchain ### Setup @@ -163,92 +145,96 @@ cd markdoc # Install dependencies npm install -# Run in development mode -npm run tauri dev +# Desktop dev (hot reload) +npm run tauri:dev + +# Build frontend only +npm run build -# Build for production -npm run tauri build +# Full desktop bundle +npm run tauri:build ``` -### End-to-End Tests (Playwright) +### Quality gates ```bash -# Install dev dependencies (adds Playwright) and its browsers once -npm install -npx playwright install --with-deps +npm run typecheck # tsc --noEmit +npm run lint # eslint . +npm run format # prettier --write . +npm run format:check # prettier --check . +npm run test:unit # Vitest +npm run test:coverage # Vitest + v8 coverage +npm run test:e2e # Playwright (starts dev:web automatically) +npm run test:all # typecheck + lint + unit + e2e +``` + +Rust-side checks live under `src-tauri/`: -# Run the suite (starts dev:web on port 1420 unless already running) -npm run test:e2e # headless -npm run test:e2e:headed # headed/debug +```bash +cd src-tauri +cargo check --locked +cargo clippy --locked -- -D warnings +cargo test --locked ``` -### Build Output +### Web-mode harness + +`npm run dev:web` launches Vite with `VITE_TARGET=web`, swapping the Tauri platform bridge for an in-memory mock (`src/platform/web.ts`). This is what Playwright drives, and what agents should use for fast UI iteration without needing the Tauri shell. See `docs/testing.md`. -After building, you'll find the installers in: -- **macOS**: `src-tauri/target/release/bundle/dmg/` +### Build output + +After building, installers end up under: + +- **macOS**: `src-tauri/target/release/bundle/dmg/` (and `macos/` for the raw `.app`) - **Windows**: `src-tauri/target/release/bundle/msi/` -- **Linux**: `src-tauri/target/release/bundle/appimage/` +- **Linux**: `src-tauri/target/release/bundle/deb/` and `appimage/` -## Technology Stack +## Technology stack -- **[Tauri](https://tauri.app/)** - Rust-based desktop framework -- **[React](https://react.dev/)** - UI library -- **[TypeScript](https://www.typescriptlang.org/)** - Type-safe JavaScript -- **[Vite](https://vite.dev/)** - Build tool -- **[markdown-it](https://github.com/markdown-it/markdown-it)** - Markdown parser -- **[CodeMirror 6](https://codemirror.net/)** - Code editor +- **[Tauri 2.10](https://tauri.app/)** — Rust + OS WebView desktop framework +- **[React 19](https://react.dev/)** + **[TypeScript 5.9](https://www.typescriptlang.org/)** +- **[Vite 7](https://vite.dev/)** — dev server and bundler +- **[markdown-it](https://github.com/markdown-it/markdown-it)** — CommonMark rendering +- **[Prism](https://prismjs.com/)** — syntax highlighting in rendered code blocks +- **[isomorphic-dompurify](https://github.com/kkomelin/isomorphic-dompurify)** — HTML sanitisation +- **[Vitest](https://vitest.dev/)** + **[Testing Library](https://testing-library.com/)** — unit tests +- **[Playwright](https://playwright.dev/)** — end-to-end tests via the web-mode harness ## Why Tauri? -Tauri was chosen over Electron for several key advantages: - -- **Smaller Bundle Size**: 3.6 MB vs 80-120 MB -- **Better Performance**: Uses OS native WebView instead of bundling Chromium -- **Native Menus**: True OS-native menu bars, not HTML/CSS approximations -- **Lower Memory Usage**: Significantly less RAM consumption -- **Security**: Rust backend with minimal attack surface +- **Smaller bundle** — tens of MB vs 100+ MB for Electron. +- **Lower memory** — uses the OS WebView. +- **Native menus** — real OS menu bars rather than HTML approximations. +- **Rust backend** — narrower attack surface with capability-scoped FS access. ## Contributing -Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. +Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md). ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +MIT — see [LICENSE](LICENSE). ## Roadmap -### Recently Completed (v0.1.4) -- [x] Export to PDF -- [x] Multiple document tabs -- [x] Custom CSS themes (5 built-in themes) -- [x] Word count and reading time statistics -- [x] Document outline sidebar -- [x] Session restoration -- [x] Synchronized scrolling in edit mode -- [x] Zoom controls - -### Planned Features -- [ ] Export to HTML -- [ ] Syntax highlighting for code blocks in preview -- [ ] Search within document -- [ ] Drag-and-drop file opening -- [ ] Vim/Emacs keybindings option -- [ ] Custom user themes -- [ ] Table editor -- [ ] Markdown extensions (mermaid diagrams, math equations) +Potential enhancements: + +- Search within document +- Drag-and-drop file opening +- Custom user-defined themes +- Mermaid diagrams / math equations +- Print support (beyond PDF export) ## Acknowledgments - Built with [Tauri](https://tauri.app/) -- Markdown rendering by [markdown-it](https://github.com/markdown-it/markdown-it) -- Editor powered by [CodeMirror](https://codemirror.net/) +- Rendering by [markdown-it](https://github.com/markdown-it/markdown-it) +- Syntax highlighting by [Prism](https://prismjs.com/) ## Support -If you encounter any issues or have questions: - Open an [issue](../../issues) -- Check existing [discussions](../../discussions) +- Browse [discussions](../../discussions) --- diff --git a/detached.html b/detached.html deleted file mode 100644 index 41a9232..0000000 --- a/detached.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - MarkDoc - Detached - - - -
- - - diff --git a/docs/ai/multi-window-tabs-progress.md b/docs/ai/multi-window-tabs-progress.md deleted file mode 100644 index 669e91d..0000000 --- a/docs/ai/multi-window-tabs-progress.md +++ /dev/null @@ -1,973 +0,0 @@ -# Multi-Window Tab Management Implementation Progress - -**Date Started:** 2025-10-19 -**Current Status:** Phase 3 - COMPLETE (Detached Windows Implemented!) -**Completion:** ~85% (Backend + Frontend + Tab Management + Detached Windows complete) - -## Overview - -Implementation of WebStorm-style tabbed interface with tear-off functionality for the Markdown Viewer application. This allows users to: -- Manage multiple markdown documents in tabs within the main window -- Drag tabs to reorder them -- Drag tabs away from the window to create detached windows -- Re-attach detached windows back to the main window as tabs -- Native OS Window menu showing all open windows - -## Requirements & Design Decisions - -### Technology Stack Chosen -- **Backend:** Tauri v2 Rust with custom document registry -- **Frontend Tabs:** `@uiw/react-tabs-draggable` (installed) -- **State Management:** Rust backend state with event-based sync -- **Window Communication:** Tauri events (`emit`, `listen`) - -### Architecture Pattern -1. **Single source of truth:** Rust backend maintains document registry -2. **Event-driven sync:** Frontend windows listen for state updates -3. **Location tracking:** Documents know if they're in main window (tab) or detached window -4. **Menu integration:** Native OS menus built with Tauri MenuBuilder API - -## Completed Work - -### ✅ Phase 1: Rust Backend Infrastructure (COMPLETE) - -#### 1.1 Permissions (`src-tauri/tauri.conf.json`) -Added to capabilities: -```json -"core:event:allow-emit", -"core:event:allow-emit-to", -"core:window:allow-get-all-windows", -"core:webview:allow-create-webview-window", -"core:webview:allow-get-all-webviews", -"core:webview:allow-webview-close", -"core:webview:default" -``` - -#### 1.2 Document State Management (`src-tauri/src/document.rs`) -Created comprehensive document registry system: - -**Key Types:** -- `Document`: Stores content, file path, dirty state, timestamps -- `DocumentLocation`: Enum tracking if doc is in MainWindow tab or DetachedWindow -- `DocumentRegistry`: Central registry with tab ordering and active index -- `ManagedDocumentRegistry`: Thread-safe wrapper using `Arc>` - -**Key Functions:** -- `create_document()` - Create new document in main window -- `update_document()` - Update content, marks dirty -- `mark_saved()` - Clear dirty flag, set timestamp -- `close_document()` - Remove from registry, adjust indices -- `detach_document()` - Move from main window to detached window -- `reattach_document()` - Move back to main window -- `reorder_tabs()` - Drag-and-drop tab reordering -- `get_main_window_documents()` - Get all tabs in order -- `get_active_document_id()` - Current active tab - -**Broadcasting:** -- `broadcast_document_update()` - Emit to all windows when doc changes -- `broadcast_registry_state()` - Emit full tab list to main window - -#### 1.3 Rust Commands (`src-tauri/src/lib.rs`) -Added 13 new commands, all registered in `invoke_handler`: -```rust -create_document(content, file_path) -> DocumentId -get_document(doc_id) -> Document -get_all_documents() -> Vec -get_active_document_id() -> Option -update_document_content(doc_id, content) -mark_document_saved(doc_id, timestamp) -update_document_file_path(doc_id, file_path) -close_document(doc_id) -set_active_document(doc_id) -reorder_tabs(from_index, to_index) -detach_document(doc_id, window_label) -reattach_document(doc_id) -get_detached_windows() -> Vec<(WindowLabel, DocumentId)> -``` - -State initialized in `setup()`: -```rust -.manage(ManagedDocumentRegistry::new()) -``` - -#### 1.4 WINDOW Menu (`src-tauri/src/lib.rs`) -Added native OS Window menu with: -- Minimize (platform standard) -- Maximize (platform standard) -- Separator -- "Open Windows" submenu (dynamic, placeholder for Phase 4) - -Updated `MenuHandles` struct to include: -```rust -window_menu: Submenu, -window_list_submenu: Submenu, -``` - -Updated `set_edit_menu_visible()` to always include window menu. - -**Compilation Status:** ✅ All Rust code compiles successfully (3 warnings for unused functions) - -### ✅ Phase 2.1: TypeScript Types (`src/types/index.ts`) - -Added comprehensive types: -```typescript -type DocumentId = string; -type WindowLabel = string; - -interface Document { - id: DocumentId; - content: string; - file_path: string | null; - has_unsaved_changes: boolean; - last_saved_at: number | null; -} - -interface TabInfo { - id: DocumentId; - title: string; - isDirty: boolean; - isActive: boolean; -} - -interface RegistryState { - documents: Document[]; - active_index: number | null; -} - -interface DocumentUpdate { - docId: DocumentId; - document: Document; -} - -interface DetachedWindowInfo { - label: WindowLabel; - documentId: DocumentId; - title: string; -} -``` - -**Dependencies Installed:** -- ✅ `@uiw/react-tabs-draggable@^1.0.1` - -### ✅ Phase 2.2: TabBar Component (COMPLETE) - -**File:** `src/components/TabBar.tsx` (CREATED) - -Created fully functional draggable tab bar with: - -**Features Implemented:** -- ✅ Display list of tabs from `RegistryState` -- ✅ Show tab title (filename or "Untitled") -- ✅ Show dirty indicator (•) for unsaved changes -- ✅ Active tab highlighting with blue underline -- ✅ Close button (X) on each tab -- ✅ Click to switch tabs → calls `invoke('set_active_document')` -- ✅ Drag to reorder → calls `invoke('reorder_tabs')` -- ✅ Tear-off detection (detects drag distance, shows alert for Phase 3) - -**Styling:** Vanilla CSS added to `src/App.css` (lines 546-693) -- Follows existing app design patterns -- Full dark mode support -- Smooth transitions and hover effects -- Tab bar height: 36px, sits between Toolbar and content area - -### ✅ Phase 2.3: App.tsx Refactor (COMPLETE) - -**File:** `src/App.tsx` (MAJOR REFACTOR COMPLETE) - -Successfully migrated from single-document to multi-document state: - -**Changes Implemented:** - -1. **State Management Replaced:** - ```tsx - // OLD: Single fileState - const [fileState, setFileState] = useState({...}); - - // NEW: Multi-document registry state - const [documents, setDocuments] = useState([]); - const [activeDocumentId, setActiveDocumentId] = useState(null); - ``` - -2. **Registry Sync Listeners Added:** - - `registry://state` listener - Updates full document list - - `document://updated` listener - Updates individual documents - - Both listeners properly clean up on unmount - -3. **Document Initialization:** - - Checks for existing documents on mount - - Creates initial empty document if none exist - - Sets active document from backend - -4. **File Operations Refactored:** - - `handleNew()` → Creates new document via `create_document` command - - `handleOpen()` → Creates new document with file content - - `handleSave()` → Writes file then calls `mark_document_saved` - - `handleSaveAs()` → Writes file, updates path, marks saved - - `handleClose()` → Closes active document via `close_document` - - Content changes → Calls `update_document_content` - -5. **Tab Handlers Added:** - - `handleTabClick(tabId)` - Switches active document - - `handleTabClose(tabId)` - Closes tab with unsaved changes confirmation - - `handleTabReorder(from, to)` - Reorders tabs - - `handleTabDetach(id, x, y)` - Placeholder for Phase 3 (shows alert) - -6. **Render Logic Updated:** - - TabBar component added (renders when documents.length > 0) - - Active document computed from documents array - - Viewer/Editor use active document content - - Footer shows active document stats - -**Testing Status:** ✅ App compiles and runs successfully -- Rust builds with only expected warnings (unused Phase 3/4 functions) -- No TypeScript errors -- Dev server running successfully - -## Current Position - -**Phase 2 is COMPLETE!** 🎉 - -The frontend tab management system is now fully functional: -- Multiple documents can be created and managed -- Tabs display properly with titles and dirty indicators -- Tab switching, creation, closing all work -- Drag-to-reorder tabs works -- Backend-frontend synchronization via events works - -You can now: -- Create multiple tabs (Cmd+N) -- Open files in new tabs -- Switch between tabs by clicking -- Close tabs with the X button -- Drag tabs to reorder them -- See dirty indicators on unsaved tabs - -## Design Change: Per-Tab Toolbars (BEFORE PHASE 3) - -**Decision Date:** 2025-10-19 11:00 AM - -### Rationale -Each tab should be independent with its own toolbar containing document-specific controls. This better reflects the multi-document nature of the application. - -### Changes Required - -#### 1. Toolbar Architecture Refactor -**Current:** Single global toolbar at top of window -**New:** Per-tab toolbar, showing toolbar for active tab only - -#### 2. Button Relocation -Move these buttons FROM global toolbar TO per-tab toolbar: -- ✅ **SAVE** (Cmd+S) - Document-specific -- ✅ **SAVE AS** (Cmd+Shift+S) - Document-specific -- ✅ **AUTO-SIZE** - Per-document preference (or keep global?) -- ✅ **EDIT/VIEW MODE TOGGLE** (Cmd+E) - Per-document mode - -Keep these buttons in global toolbar: -- ✅ **NEW** (Cmd+N) - Creates new tab -- ✅ **OPEN** (Cmd+O) - Opens file in new tab - -Remove from toolbar entirely: -- ❌ **CLOSE** (Cmd+W) - Now handled by tab close button (X) - -#### 3. Visual Layout - -``` -┌────────────────────────────────────────────────────────────┐ -│ Global Toolbar (40px) │ -│ [NEW] [OPEN] │ -├────────────────────────────────────────────────────────────┤ -│ Tab Bar (36px) │ -│ [Tab 1 •] [Tab 2] [Tab 3 •] │ -├────────────────────────────────────────────────────────────┤ -│ Per-Tab Toolbar (40px) - shows for ACTIVE tab only │ -│ [SAVE] [SAVE AS] [DIRTY?] [AUTO] [EDIT/VIEW]│ -├────────────────────────────────────────────────────────────┤ -│ │ -│ Content Area (Viewer or Editor) │ -│ │ -├────────────────────────────────────────────────────────────┤ -│ Footer (24px) │ -│ Words: 123 | Characters: 456 | Last saved: 10:30 AM │ -└────────────────────────────────────────────────────────────┘ -``` - -#### 4. Implementation Steps - -**Phase 2.4: Per-Tab Toolbar Refactor** (NEW - BEFORE PHASE 3) - -1. **Create PerTabToolbar Component** (`src/components/PerTabToolbar.tsx`) - ```tsx - interface PerTabToolbarProps { - document: Document; - editMode: boolean; - autosize: boolean; - onSave: () => void; - onSaveAs: () => void; - onToggleMode: () => void; - onToggleAutosize: () => void; - } - ``` - -2. **Refactor Toolbar Component** (`src/components/Toolbar.tsx`) - - Remove: CLOSE, SAVE, SAVE AS, AUTO-SIZE, EDIT/VIEW MODE buttons - - Keep: NEW, OPEN buttons - - Simplify to just file creation actions - -3. **Update App.tsx** - - Add `` component between TabBar and content area - - Only render when activeDocument exists - - Pass active document's state to PerTabToolbar - - Move mode state from App-level to per-document level (?) - -4. **Consider: Per-Document Edit Mode** - - **Option A:** Keep edit mode global (simpler, current behavior) - - **Option B:** Store edit mode per-document (more flexible) - - **Recommendation:** Start with Option A (global mode) for now, can enhance later - -5. **Update CSS** - - Reuse existing `.toolbar` styles for both toolbars - - Add wrapper class like `.global-toolbar` and `.tab-toolbar` if needed - -#### 5. Open Design Questions - -1. **Auto-size:** Should this be global or per-document? - - **Recommendation:** Keep global - it's a window preference, not document preference - -2. **Edit mode:** Should each document remember if it was last in edit or view mode? - - **Recommendation:** Start global, enhance later if needed - -3. **Toolbar visibility:** Should per-tab toolbar be collapsible? - - **Recommendation:** Always visible for now, add collapse later if requested - -#### 6. Files to Modify - -``` -src/ -├── components/ -│ ├── Toolbar.tsx (REFACTOR - remove buttons) -│ └── PerTabToolbar.tsx (NEW - create per-tab toolbar) -└── App.tsx (MODIFY - add PerTabToolbar) -``` - -## Remaining Work - -### ✅ Phase 2.4: Per-Tab Toolbar Refactor (COMPLETE!) - -**Status:** Fully implemented and tested - -Successfully refactored the toolbar structure to have: -- **Global Toolbar** - Contains only NEW and OPEN buttons (file creation actions) -- **Per-Tab Toolbar** - Shows for active tab only, contains SAVE, SAVE AS, AUTO-SIZE, and EDIT/VIEW MODE buttons -- Edit mode and autosize remain global settings for now - -**Files Created:** -- `src/components/PerTabToolbar.tsx` - New component with document-specific controls - -**Files Modified:** -- `src/components/Toolbar.tsx` - Simplified to only NEW and OPEN buttons -- `src/App.tsx` - Added PerTabToolbar component between TabBar and content area -- `src/App.css` - Added `.tab-toolbar` styling with secondary visual hierarchy - -**Layout Structure:** -``` -┌────────────────────────────────────────────────────────────┐ -│ Global Toolbar (40px) - NEW, OPEN │ -│ Background: #f5f5f5 (light) / #2a2a2a (dark) │ -├────────────────────────────────────────────────────────────┤ -│ Tab Bar (36px) - [Tab 1 •] [Tab 2] [Tab 3] │ -│ Background: #e8e8e8 (light) / #252525 (dark) │ -├────────────────────────────────────────────────────────────┤ -│ Per-Tab Toolbar (32px) - SAVE, SAVE AS, AUTO, EDIT/VIEW │ -│ Background: #fdfdfd (light) / #1d1d1d (dark) │ -│ Smaller buttons (28px vs 32px), icons (16px vs 18px) │ -├────────────────────────────────────────────────────────────┤ -│ Content Area (Viewer or Editor) │ -│ Background: #ffffff (light) / #1a1a1a (dark) │ -├────────────────────────────────────────────────────────────┤ -│ Footer (24px) - Document stats │ -└────────────────────────────────────────────────────────────┘ -``` - -**Visual Hierarchy:** -- **Primary:** Global toolbar (darker background, standard size buttons) -- **Secondary:** Per-tab toolbar (lighter background closer to content, smaller buttons) -- Creates clear visual distinction between app-level and document-level controls - -### ✅ Phase 3: Detached Windows (COMPLETE!) - -**Status:** Fully implemented and tested - -#### 3.1 Create DetachedWindow Component - -**File:** `src/components/DetachedWindow.tsx` (NEW) - -Lightweight window UI for torn-off tabs: - -**Features:** -- Parse `?docId=xxx` from window URL to know which document to display -- Subscribe to `document://updated` events for this docId -- Lean toolbar with: - - File name + dirty indicator - - SAVE button (if dirty) - - SAVE AS button - - Auto-size toggle - - Edit/View mode toggle -- Same `` and `` components as main window -- "Merge to Main Window" option (re-attach) - -**Window Title:** Set via `getCurrentWindow().setTitle(fileName || 'Untitled')` - -#### 3.2 Implement Window Creation Logic (Tear-off) - -**Location:** `src/components/TabBar.tsx` and `src/utils/windowManager.ts` (NEW) - -**Tear-off Flow:** -1. User drags tab beyond threshold -2. Calculate drop position (screen coordinates) -3. Generate unique window label: `detached_${docId}` -4. Call Rust to detach: - ```tsx - await invoke('detach_document', { - doc_id: docId, - window_label: windowLabel - }); - ``` -5. Create new WebviewWindow: - ```tsx - import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; - - const window = new WebviewWindow(windowLabel, { - url: `/detached.html?docId=${docId}`, - title: fileName || 'Untitled', - width: 1000, - height: 800, - x: dropX, - y: dropY, - }); - ``` - -**New Files Needed:** -- `detached.html` - Entry point for detached windows -- `src/detached.tsx` - Mount point rendering `` - -#### 3.3 Implement Re-attachment - -**Location:** `src/components/DetachedWindow.tsx` - -Add "Merge to Main Window" button/menu item: -```tsx -const handleReattach = async () => { - await invoke('reattach_document', { doc_id: docId }); - await getCurrentWindow().close(); // Close detached window -}; -``` - -Main window automatically updates via `registry://state` event. - -### 🚧 Phase 4: Dynamic Menus & Full Synchronization - -#### 4.1 Dynamic WINDOW Menu Updates - -**File:** `src-tauri/src/lib.rs` (ADD COMMAND) - -Create command to update window list in menu: -```rust -#[tauri::command] -fn update_window_list_menu( - app: AppHandle, - state: State, - windows: Vec<(String, String)>, // (label, title) pairs -) -> Result<(), String> { - // Similar pattern to update_recent_files_menu - // Clear window_list_submenu - // Add menu items for each window - // On click, focus that window -} -``` - -**Frontend:** Call this whenever documents/windows change - -#### 4.2 Event-Based Document Synchronization - -Already implemented in backend! Frontend just needs to: -- Listen to `document://updated` events -- Listen to `registry://state` events -- Update local state accordingly - -**Testing needed:** -- Open same document in 2 detached windows (future: prevent this or show warning) -- Edit in one window, verify other updates -- Save in one window, verify dirty flag clears in all - -### 🚧 Phase 5: Polish & Edge Cases - -#### 5.1 Keyboard Shortcuts - -Add to menu items or handle in frontend: -- `Cmd/Ctrl+T` → New tab -- `Cmd/Ctrl+W` → Close current tab/window -- `Cmd/Ctrl+Shift+T` → Reopen last closed (needs history tracking) -- `Cmd/Ctrl+Tab` → Next tab -- `Cmd/Ctrl+Shift+Tab` → Previous tab -- `Cmd/Ctrl+1-9` → Jump to tab by index - -#### 5.2 Edge Cases to Handle - -**Last Tab Closes:** -- Option A: Show empty state "No documents open" -- Option B: Always keep one empty "Untitled" document - -**Detached Window Closes:** -- Current behavior: Document removed from registry -- Consider: Auto-reattach to main window instead? - -**Unsaved Changes:** -- Confirm before closing dirty tabs -- Confirm before closing window with dirty documents -- Maybe: Show list of unsaved documents in dialog - -**File Already Open:** -- Detect if file path already open in another tab -- Switch to that tab instead of opening duplicate - -**Session Restore:** -- Save open tabs to localStorage on close -- Restore on next launch (optional feature) - -#### 5.3 Testing Checklist - -- [ ] Create new tab (Cmd+N) -- [ ] Open file in tab -- [ ] Switch between tabs -- [ ] Close tabs -- [ ] Drag to reorder tabs -- [ ] Drag tab out to create detached window -- [ ] Edit in detached window -- [ ] Save from detached window -- [ ] Re-attach detached window -- [ ] Close detached window -- [ ] Multiple detached windows -- [ ] Window menu shows all windows -- [ ] Window menu can focus windows -- [ ] Edit mode persists per-document -- [ ] Autosize persists per-window -- [ ] Dark mode works in detached windows -- [ ] All keyboard shortcuts work - -## Technical Notes for Resuming - -### Important Patterns - -**Calling Rust Commands:** -```tsx -import { invoke } from '@tauri-apps/api/core'; - -const docId = await invoke('create_document', { - content: 'Hello', - file_path: null -}); -``` - -**Listening to Events:** -```tsx -import { listen } from '@tauri-apps/api/event'; - -const unlisten = await listen('registry://state', (event) => { - console.log('Registry updated:', event.payload); -}); - -// Cleanup -return () => { unlisten.then(fn => fn()); }; -``` - -**Creating Windows:** -```tsx -import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; - -const win = new WebviewWindow('unique-label', { - url: '/path.html', - title: 'Window Title', - width: 800, - height: 600, -}); - -await win.once('tauri://created', () => { - console.log('Window created'); -}); - -await win.once('tauri://error', (e) => { - console.error('Window creation failed:', e); -}); -``` - -### Current Menu Structure - -``` -FILE -├── New (Cmd+N) -├── Open (Cmd+O) -├── Close (Cmd+W) -├── ─────────── -├── Save (Cmd+S) -├── Save As (Cmd+Shift+S) -├── ─────────── -├── Toggle Edit/View Mode (Cmd+E) -├── ─────────── -└── Recent Files ▶ - -EDIT (only visible in edit mode) -├── Undo -├── Redo -├── ─────────── -├── Cut -├── Copy -├── Paste -└── Select All - -WINDOW -├── Minimize -├── Maximize -├── ─────────── -└── Open Windows ▶ -``` - -### Event Flow Diagram - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Rust Backend │ -│ ┌────────────────────────────────────────────────────────┐ │ -│ │ DocumentRegistry (Mutex) │ │ -│ │ - documents: HashMap │ │ -│ │ - tab_order: Vec │ │ -│ │ - active_tab_index: Option │ │ -│ └────────────────────────────────────────────────────────┘ │ -│ ↓ ↑ │ -│ Rust Commands │ -│ (invoke handlers) │ -│ ↓ ↑ │ -│ Event Broadcasting │ -│ ┌────────────────┴────────────────┐ │ -│ ↓ ↓ │ -│ registry://state document://updated │ -└─────────┼──────────────────────────────────┼─────────────────┘ - ↓ ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Frontend (React) │ -│ │ -│ ┌──────────────────────────┐ ┌──────────────────────┐ │ -│ │ Main Window (App.tsx) │ │ Detached Window │ │ -│ │ ┌────────────────────┐ │ │ (DetachedWindow) │ │ -│ │ │ TabBar │ │ │ │ │ -│ │ └────────────────────┘ │ │ listens to: │ │ -│ │ ┌────────────────────┐ │ │ - document://updated│ │ -│ │ │ Viewer/Editor │ │ │ │ │ -│ │ └────────────────────┘ │ │ renders: │ │ -│ │ │ │ - Viewer/Editor │ │ -│ │ listens to: │ │ - Lean toolbar │ │ -│ │ - registry://state │ └──────────────────────┘ │ -│ │ - document://updated │ │ -│ └──────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### File Structure Changes - -**New Files to Create:** -``` -src/ -├── components/ -│ ├── TabBar.tsx (NEW - Phase 2.2) -│ ├── DetachedWindow.tsx (NEW - Phase 3.1) -│ └── ...existing files -├── utils/ -│ └── windowManager.ts (NEW - Phase 3.2) -├── detached.tsx (NEW - Phase 3.2) -└── ...existing files - -public/ -└── detached.html (NEW - Phase 3.2) - -src-tauri/ -└── src/ - ├── document.rs (✅ DONE) - ├── lib.rs (✅ DONE, may add window menu update cmd) - └── ...existing files -``` - -## Estimated Remaining Time - -- ~~**Phase 2.2-2.3:** 2-3 hours (TabBar + App.tsx refactor)~~ ✅ **COMPLETE** -- ~~**Phase 2.4:** 1-2 hours (Per-tab toolbar refactor)~~ ✅ **COMPLETE** -- ~~**Phase 3:** 2-3 hours (Detached windows + tear-off logic)~~ ✅ **COMPLETE** -- **Phase 4:** 1-2 hours (Menu updates + sync testing) - OPTIONAL -- **Phase 5:** 2-3 hours (Polish, edge cases, keyboard shortcuts) - OPTIONAL - -**Total:** 3-5 hours remaining (~15% optional polish work left) - -**Core functionality is now COMPLETE!** The app is fully functional with multi-window tab management. - -## Commands for Next Session - -```bash -# Start dev server -source ~/.cargo/env && npm run tauri dev - -# Check Rust compilation -source ~/.cargo/env && cargo check --manifest-path=src-tauri/Cargo.toml - -# Build production -npm run tauri build -``` - -## Known Working Features (As of Session 2) - -✅ **Multi-Document Tab Management:** -- Create new tabs (Cmd+N) -- Open files in new tabs (Cmd+O) -- Switch between tabs by clicking -- Close tabs with X button or Cmd+W -- Drag tabs to reorder within tab bar -- Dirty indicators (•) on unsaved tabs -- Recent files menu - -✅ **Detached Windows:** -- Drag tab >100px vertically to tear off -- Creates independent window at drag position -- Full document editing in detached window -- Merge back to main window via "Merge to Main" button -- Synchronized content between windows via events -- Window title updates with filename - -✅ **Document Operations:** -- Save (Cmd+S) and Save As (Cmd+Shift+S) -- Edit/View mode toggle (Cmd+E) -- Auto-size window toggle -- Markdown rendering with CommonMark -- Split-pane editor with live preview -- Dark mode support (follows OS preference) - -✅ **UI/UX:** -- Global toolbar (NEW, OPEN) -- Per-tab toolbar (SAVE, SAVE AS, AUTO, EDIT/VIEW) -- Tab bar with drag-to-reorder -- Footer with document stats -- Native OS menus (FILE, EDIT, WINDOW) - -## Testing Checklist - -- [x] Create new tab (Cmd+N) -- [x] Open file in tab -- [x] Switch between tabs -- [x] Close tabs -- [x] Drag to reorder tabs -- [x] Drag tab out to create detached window -- [x] Edit in detached window -- [x] Save from detached window -- [x] Re-attach detached window (Merge to Main) -- [ ] Close detached window (test document cleanup) -- [ ] Multiple detached windows simultaneously -- [ ] Window menu shows all windows (Phase 4) -- [ ] Window menu can focus windows (Phase 4) -- [x] Dark mode works in detached windows -- [x] All keyboard shortcuts work (Cmd+N, O, W, S, Shift+S, E) - -## Questions/Decisions for Next Session - -1. **Last tab behavior:** Keep one empty document or show empty state? - - **Current**: App starts with one empty "Untitled" document - - **Works well**, no changes needed - -2. **Detached window close:** Remove from registry or auto-reattach? - - **Current**: Document removed from registry when window closes - - **Consider**: Auto-reattach to main window instead? - -3. **Duplicate files:** Prevent opening same file in multiple tabs? - - **Not yet implemented** - same file can be opened multiple times - - **Recommend**: Detect and switch to existing tab instead - -4. **Session restore:** Save/restore tab state on app restart? - - **Not yet implemented** - could save tab state to localStorage - - **Nice-to-have** for future enhancement - -5. **Tab close confirmation:** Always ask if dirty, or only on last tab? - - **Current**: Always asks if document has unsaved changes - - **Works well**, but could be improved with "Don't ask again" option - -## Production Build Notes - -The app is ready for production builds with: -- Multi-page Vite configuration working -- All TypeScript compiles without errors -- Rust compiles successfully (3 harmless warnings for unused Phase 4 code) -- All core features tested and working - -To build: `npm run tauri build` - -Output locations: -- macOS: `src-tauri/target/release/bundle/macos/Markdown Viewer.app` -- macOS DMG: `src-tauri/target/release/bundle/dmg/Markdown Viewer_0.1.0_aarch64.dmg` - ---- - -**Last Updated:** 2025-10-19 11:35 AM -**Next Step:** Phase 4 (Dynamic menus) or Phase 5 (Polish & edge cases) - -## Session Summary (Latest - Session 2) - -### What Was Accomplished This Session: - -**Phase 3: Detached Windows - COMPLETE!** 🎉 -**PLUS: Critical Bug Fixes** ✅ - -1. **Created detached.html Entry Point** - - New HTML entry point for detached windows - - Points to `/src/detached.tsx` React entry - -2. **Created detached.tsx React Entry** - - Mounts `DetachedWindow` component - - Imports shared App.css for consistent styling - -3. **Created DetachedWindow Component** (`src/components/DetachedWindow.tsx`) - - Parses `?docId=xxx` from URL query params - - Loads document via `get_document` command - - Listens to `document://updated` events - - Lean toolbar with: - - "Merge to Main" button for re-attachment - - File name + dirty indicator - - SAVE and SAVE AS buttons - - AUTO-SIZE toggle - - EDIT/VIEW mode toggle - - Reuses existing `` and `` components - - Reuses existing `