From 1aae1b8f6b5d4b83ae63ab8ab5a72445723c019e Mon Sep 17 00:00:00 2001 From: thoroc Date: Fri, 30 Jan 2026 19:08:15 +0000 Subject: [PATCH 1/2] feat: Add Table of Contents sidebar with sticky action buttons - Add hierarchical Table of Contents component with clickable navigation - Implement useActiveSection hook for real-time section highlighting - Add annotationHelpers utility for block identification - Make Images, Global comment, and Copy plan buttons sticky during scroll - Fix navigation scrolling to work within scrollable main container --- .claude-plugin/marketplace.json | 7 + README.md | 12 + bun.lock | 5 +- docs/CODE-STYLE.md | 565 ++++++++++++++++++++ docs/UI-TESTING.md | 566 +++++++++++++++++++++ packages/editor/App.tsx | 24 +- packages/ui/components/TableOfContents.tsx | 213 ++++++++ packages/ui/components/Viewer.tsx | 4 +- packages/ui/hooks/useActiveSection.ts | 71 +++ packages/ui/utils/annotationHelpers.ts | 101 ++++ 10 files changed, 1562 insertions(+), 6 deletions(-) create mode 100644 docs/CODE-STYLE.md create mode 100644 docs/UI-TESTING.md create mode 100644 packages/ui/components/TableOfContents.tsx create mode 100644 packages/ui/hooks/useActiveSection.ts create mode 100644 packages/ui/utils/annotationHelpers.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b453132..2239773 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -3,6 +3,13 @@ "owner": { "name": "backnotprop" }, + "mcp": { + "playwright": { + "type": "local", + "command": ["npx", "@playwright/mcp@latest"], + "enabled": true + } + }, "plugins": [ { "name": "plannotator", diff --git a/README.md b/README.md index 3405d3d..ac51e9c 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,18 @@ When your AI agent finishes planning, Plannotator: --- +## Contributing + +We welcome contributions! Please see: + +- **[CONTRIBUTING.md](./CONTRIBUTING.md)** - Legal requirements (License, CLA) +- **[docs/UI-TESTING.md](./docs/UI-TESTING.md)** - How to test UI changes +- **[docs/CODE-STYLE.md](./docs/CODE-STYLE.md)** - Code style guidelines + +For bug reports and feature requests, please [open an issue](https://github.com/backnotprop/plannotator/issues). + +--- + ## License **Copyright (c) 2025 backnotprop.** diff --git a/bun.lock b/bun.lock index 8f92591..a518a08 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "plannotator", @@ -47,7 +48,7 @@ }, "apps/opencode-plugin": { "name": "@plannotator/opencode", - "version": "0.6.7", + "version": "0.6.8", "dependencies": { "@opencode-ai/plugin": "^1.1.10", }, @@ -117,7 +118,7 @@ }, "packages/server": { "name": "@plannotator/server", - "version": "0.6.7", + "version": "0.6.8", "peerDependencies": { "bun": ">=1.0.0", }, diff --git a/docs/CODE-STYLE.md b/docs/CODE-STYLE.md new file mode 100644 index 0000000..50fcb1a --- /dev/null +++ b/docs/CODE-STYLE.md @@ -0,0 +1,565 @@ +# Code Style Guide + +This guide documents the coding standards and conventions used in Plannotator. Following these guidelines ensures consistency and maintainability. + +## Table of Contents + +1. [TypeScript Guidelines](#typescript-guidelines) +2. [React Patterns](#react-patterns) +3. [Tailwind CSS](#tailwind-css) +4. [File Naming Conventions](#file-naming-conventions) +5. [Component Structure Template](#component-structure-template) +6. [Commit Message Format](#commit-message-format) + +--- + +## TypeScript Guidelines + +### Strict Mode + +TypeScript strict mode is enabled in this project. Always use explicit types. + +### Type Annotations + +**Do:** Provide explicit types for function parameters and return values + +```typescript +function calculateAnnotationCount(blocks: Block[]): number { + return blocks.filter(b => b.type === 'heading').length; +} +``` + +**Don't:** Rely on type inference for function signatures + +```typescript +function calculateAnnotationCount(blocks) { // ❌ No type + return blocks.filter(b => b.type === 'heading').length; +} +``` + +### Avoid `any` + +**Do:** Use specific types or `unknown` when type is truly unknown + +```typescript +interface ApiResponse { + data: unknown; + error?: string; +} +``` + +**Don't:** Use `any` as a shortcut + +```typescript +function handleData(data: any) { // ❌ Avoid any + // ... +} +``` + +### Interface Usage + +**Do:** Define interfaces for component props and complex data structures + +```typescript +interface TableOfContentsProps { + blocks: Block[]; + annotations: Annotation[]; + activeId: string | null; + onNavigate: (blockId: string) => void; +} +``` + +**Do:** Use `type` for unions, intersections, and utility types + +```typescript +type AnnotationType = 'DELETION' | 'REPLACEMENT' | 'COMMENT' | 'INSERTION'; +type BlockWithAnnotations = Block & { annotationCount: number }; +``` + +### Null vs Undefined + +**Prefer:** `null` for intentional absence, `undefined` for uninitialized + +```typescript +const [activeId, setActiveId] = useState(null); // ✓ +const [data, setData] = useState(); // undefined initially ✓ +``` + +--- + +## React Patterns + +### Functional Components + +Use functional components with hooks exclusively. Class components are not used in this codebase. + +```typescript +export function MyComponent({ prop1, prop2 }: MyComponentProps) { + // Component logic + return
...
; +} +``` + +### Props Interface Naming + +Name props interfaces with the pattern `ComponentNameProps`: + +```typescript +interface TableOfContentsProps { + blocks: Block[]; + onNavigate: (blockId: string) => void; +} + +export function TableOfContents({ blocks, onNavigate }: TableOfContentsProps) { + // ... +} +``` + +### Hook Ordering + +Order hooks consistently: + +1. Context hooks (`useContext`) +2. State hooks (`useState`) +3. Ref hooks (`useRef`) +4. Effect hooks (`useEffect`) +5. Memoization (`useMemo`, `useCallback`) +6. Custom hooks + +```typescript +export function MyComponent({ data }: MyComponentProps) { + const theme = useContext(ThemeContext); // 1. Context + const [count, setCount] = useState(0); // 2. State + const inputRef = useRef(null); // 3. Refs + + useEffect(() => { // 4. Effects + // Side effect + }, [data]); + + const processedData = useMemo(() => { // 5. Memoization + return data.map(item => transform(item)); + }, [data]); + + const handleClick = useCallback(() => { + setCount(c => c + 1); + }, []); + + return
...
; +} +``` + +### Memoization + +**Use `useMemo`** for expensive computations: + +```typescript +const tocHierarchy = useMemo( + () => buildTocHierarchy(blocks, annotationCounts), + [blocks, annotationCounts] +); +``` + +**Use `useCallback`** for event handlers passed as props: + +```typescript +const handleNavigate = useCallback( + (blockId: string) => { + onNavigate(blockId); + scrollToBlock(blockId); + }, + [onNavigate] +); +``` + +**Don't overuse:** Only memoize when there's a clear performance benefit. + +### Conditional Rendering + +**Prefer:** Ternary or logical AND for simple conditions + +```typescript +// Ternary for if/else +{isLoading ? : } + +// Logical AND for conditional render +{error && } +``` + +**Use early returns** for complex conditions: + +```typescript +function MyComponent({ data }: Props) { + if (!data) { + return ; + } + + if (data.length === 0) { + return ; + } + + return ; +} +``` + +--- + +## Tailwind CSS + +### Utility-First Approach + +Use Tailwind utility classes directly in JSX: + +```typescript +
+ Title + +
+``` + +### Responsive Modifiers + +Use responsive prefixes for breakpoint-specific styles: + +```typescript + +``` + +**Breakpoints:** +- `sm:` - 640px +- `md:` - 768px +- `lg:` - 1024px +- `xl:` - 1280px +- `2xl:` - 1536px + +### Dark Mode + +Use the `dark:` prefix for dark mode styles: + +```typescript +
+ Dark mode ready! +
+``` + +### State Variants + +Use state variants for interactive elements: + +```typescript + +``` + +### Custom CSS (Use Sparingly) + +**Prefer Tailwind** utilities over custom CSS. Only use custom CSS for: +- Complex animations +- Global styles +- Patterns not expressible with utilities + +If you must use custom CSS, use `@apply` in CSS files: + +```css +.custom-button { + @apply px-4 py-2 bg-primary text-white rounded hover:bg-primary/90; +} +``` + +### Conditional Classes + +Use template literals for conditional classes: + +```typescript +
+ Content +
+``` + +Or use a helper function for complex logic: + +```typescript +import { clsx } from 'clsx'; + +
+ Content +
+``` + +--- + +## File Naming Conventions + +### Components + +Use **PascalCase** for component files: + +``` +TableOfContents.tsx +AnnotationPanel.tsx +ConfirmDialog.tsx +``` + +### Utilities + +Use **camelCase** for utility files: + +``` +annotationHelpers.ts +parser.ts +storage.ts +sharing.ts +``` + +### Hooks + +Use **camelCase** with `use` prefix: + +``` +useActiveSection.ts +useSharing.ts +useAgents.ts +``` + +### Types + +Type-only files use **camelCase** with `.d.ts` or placed in main file: + +``` +types.ts +index.d.ts +``` + +### Constants + +Use **UPPER_SNAKE_CASE** for constant values: + +```typescript +export const MAX_ANNOTATION_LENGTH = 1000; +export const DEFAULT_PORT = 19432; +export const CUSTOM_PATH_SENTINEL = '__CUSTOM__'; +``` + +### Test Files + +Use `.test.ts` or `.spec.ts` suffix: + +``` +annotationHelpers.test.ts +parser.spec.ts +``` + +--- + +## Component Structure Template + +Use this template for new components: + +```typescript +// 1. Imports - External libraries first, then internal +import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import type { Block, Annotation } from '../types'; +import { buildHierarchy } from '../utils/helpers'; + +// 2. Props interface - Define props type +interface MyComponentProps { + blocks: Block[]; + annotations: Annotation[]; + onNavigate: (id: string) => void; + className?: string; // Optional props use ? +} + +// 3. Helper interfaces/types (if needed) +interface InternalState { + expanded: boolean; + selectedId: string | null; +} + +// 4. Main component - Named export +export function MyComponent({ + blocks, + annotations, + onNavigate, + className = '' // Default values in destructure +}: MyComponentProps) { + // 5. Hooks - Ordered by type (state, refs, effects, memoization) + const [state, setState] = useState({ + expanded: true, + selectedId: null, + }); + + const containerRef = useRef(null); + + useEffect(() => { + // Side effects + }, [blocks]); + + const hierarchy = useMemo( + () => buildHierarchy(blocks, annotations), + [blocks, annotations] + ); + + // 6. Event handlers - Use useCallback for handlers passed as props + const handleClick = useCallback((id: string) => { + setState(prev => ({ ...prev, selectedId: id })); + onNavigate(id); + }, [onNavigate]); + + // 7. Helper functions - Local functions don't need useCallback + const isSelected = (id: string) => state.selectedId === id; + + // 8. Early returns for special cases + if (blocks.length === 0) { + return
No content
; + } + + // 9. Main render + return ( +
+ {hierarchy.map(item => ( +
handleClick(item.id)} + className={isSelected(item.id) ? 'selected' : ''} + > + {item.content} +
+ ))} +
+ ); +} + +// 10. Sub-components (if small and only used here) +function SubComponent({ data }: { data: string }) { + return {data}; +} +``` + +### Key Points + +- **Named exports** for components (not default exports) +- **Props destructuring** in function signature +- **Optional props** use `?` and provide defaults +- **Memoization** only when needed for performance +- **Event handlers** use arrow functions or useCallback +- **Early returns** for edge cases before main render + +--- + +## Commit Message Format + +### Conventional Commits + +Use conventional commits format when possible: + +``` +(): + +[optional body] + +[optional footer] +``` + +### Types + +- `feat:` - New feature +- `fix:` - Bug fix +- `docs:` - Documentation only +- `style:` - Code style (formatting, missing semicolons, etc.) +- `refactor:` - Code refactoring +- `perf:` - Performance improvement +- `test:` - Adding or updating tests +- `chore:` - Maintenance tasks (build, deps, etc.) + +### Examples + +**Feature:** +``` +feat: add table of contents sidebar + +- Shows H1-H3 headings hierarchically +- Active section highlighting with scroll tracking +- Smooth scroll navigation +- Annotation count badges per section +``` + +**Bug Fix:** +``` +fix: correct TOC scroll offset for sticky header + +Previously TOC navigation would scroll sections under the sticky header. +Now accounts for 80px offset to show section at proper position. +``` + +**Documentation:** +``` +docs: add UI testing guide + +Includes: +- Development setup instructions +- Feature testing checklists +- Common debugging tips +``` + +**Refactor:** +``` +refactor: extract annotation counting logic + +Move annotation count calculation to annotationHelpers.ts for reuse +in TOC and annotation panel components. +``` + +### Guidelines + +**Do:** +- Use present tense ("add feature" not "added feature") +- Use imperative mood ("move cursor" not "moves cursor") +- Keep first line under 72 characters +- Capitalize first letter after type +- No period at end of first line +- Add body for complex changes + +**Don't:** +- Don't use vague descriptions ("fix bug", "update code") +- Don't include issue numbers in first line (put in body/footer) +- Don't make massive commits (break into logical pieces) + +--- + +## Questions? + +If something isn't covered here: + +1. Look at existing code for patterns +2. Check `CLAUDE.md` for architecture details +3. Ask in your PR for clarification +4. Suggest updates to this guide via PR diff --git a/docs/UI-TESTING.md b/docs/UI-TESTING.md new file mode 100644 index 0000000..4dfc748 --- /dev/null +++ b/docs/UI-TESTING.md @@ -0,0 +1,566 @@ +# UI Testing Guide + +This guide helps you test UI changes in Plannotator. Whether you're adding new features or fixing bugs, follow these steps to ensure your changes work correctly. + +## Table of Contents + +1. [Development Setup](#development-setup) +2. [Development Workflow](#development-workflow) +3. [Quick Testing Guide](#quick-testing-guide) +4. [UI Feature Testing Checklists](#ui-feature-testing-checklists) +5. [Debugging Common Issues](#debugging-common-issues) + +--- + +## Development Setup + +### Prerequisites + +- **Bun** - JavaScript runtime and package manager ([install](https://bun.sh)) +- **Git** - Version control +- **Modern browser** - Chrome, Firefox, Safari, or Edge (latest version) + +### Installation + +```bash +git clone https://github.com/backnotprop/plannotator.git +cd plannotator +bun install +``` + +### Monorepo Structure + +The project uses a monorepo structure: + +- **`packages/`** - Shared code + - `ui/` - Reusable React components, hooks, utilities + - `server/` - Server implementation (plan/review servers) + - `editor/` - Plan review application logic + - `review-editor/` - Code review application logic + +- **`apps/`** - Deployable applications + - `hook/` - Claude Code plugin (plan review) + - `opencode-plugin/` - OpenCode plugin + - `review/` - Standalone review app + - `portal/` - Share portal (share.plannotator.ai) + - `marketing/` - Marketing site (plannotator.ai) + +### First Build Test + +Verify your setup works: + +```bash +bun run build:hook +``` + +If successful, you'll see `apps/hook/dist/index.html` created. + +--- + +## Development Workflow + +### Making UI Changes + +**Shared components** (used by both plan and review UIs): +- Location: `packages/ui/components/` +- Examples: `TableOfContents.tsx`, `AnnotationToolbar.tsx`, `Viewer.tsx` + +**Plan editor** (plan review UI): +- Location: `packages/editor/App.tsx` +- Main application logic for plan review + +**Code review editor** (code review UI): +- Location: `packages/review-editor/App.tsx` +- Main application logic for code review + +**Utilities and hooks**: +- Location: `packages/ui/utils/`, `packages/ui/hooks/` +- Examples: `parser.ts`, `useActiveSection.ts`, `annotationHelpers.ts` + +### Development Servers (Hot Reload) + +For rapid iteration, use development servers with hot reload: + +```bash +# Plan review UI (most common) +bun run dev:hook +# Opens http://localhost:5173 + +# Code review UI +bun run dev:review +# Opens http://localhost:5174 +``` + +**Note:** Development servers run standalone without plugin integration. Changes appear instantly without rebuild. + +### Building for Testing + +When you're ready to test with actual plugin integration: + +```bash +# Build plan review UI +bun run build:hook +# Output: apps/hook/dist/index.html + +# Build code review UI +bun run build:review +# Output: apps/review/dist/index.html + +# Build OpenCode plugin +bun run build:opencode +# Copies HTML from hook/review dist folders + +# Build everything +bun run build +# Runs build:hook && build:opencode +``` + +### Important Build Note + +**The OpenCode plugin copies pre-built HTML files from hook and review dist folders.** + +When making UI changes: + +✅ **Correct:** +```bash +bun run build:hook && bun run build:opencode +``` + +❌ **Incorrect:** +```bash +bun run build:opencode # Uses stale HTML from previous build! +``` + +Always rebuild hook/review apps BEFORE building OpenCode if you changed UI code. + +--- + +## Quick Testing Guide + +### Test Scripts + +Three test scripts simulate plugin behavior locally: + +```bash +# Test plan review UI (Claude Code simulation) +./tests/manual/local/test-hook.sh + +# Test OpenCode origin badge +./tests/manual/local/test-hook-2.sh + +# Test code review UI +./tests/manual/local/test-opencode-review.sh +``` + +### What Each Script Does + +**`test-hook.sh`** +1. Builds the hook plugin (`bun run build:hook`) +2. Pipes sample plan JSON (includes title, SQL/TypeScript code, checklist) +3. Starts local server +4. Opens browser with plan review UI +5. Prints approve/deny decision to terminal + +**`test-hook-2.sh`** +1. Builds the hook plugin +2. Starts server with `opencode` origin flag +3. Verifies blue "OpenCode" badge appears in UI +4. Tests origin detection logic + +**`test-opencode-review.sh`** +1. Builds review app (`bun run build:review`) +2. Starts review server with sample git diff +3. Opens browser with code review UI +4. Verifies "OpenCode" badge + "Send Feedback" button (not "Copy Feedback") +5. Tests feedback submission flow + +### Manual Testing Workflow + +1. **Make your changes** in `packages/ui/` or `packages/editor/` + +2. **Choose testing method:** + - **Option A:** Dev server (fast iteration) + ```bash + bun run dev:hook + ``` + - **Option B:** Build and test with script (integration test) + ```bash + bun run build:hook && ./tests/manual/local/test-hook.sh + ``` + +3. **Verify your changes** work correctly + +4. **Test responsive design:** + - Desktop (>1024px): Full layout with TOC + - Tablet (768-1024px): TOC hidden + - Mobile (<768px): Touch-optimized + - Use browser DevTools (F12) → Device Toolbar (Cmd+Shift+M / Ctrl+Shift+M) + +5. **Check browser console** for errors: + - Open DevTools (F12) + - Console tab + - Look for red errors + +6. **Test on multiple browsers** (Chrome, Firefox, Safari, Edge) + +--- + +## UI Feature Testing Checklists + +Use these checklists to verify features work correctly before submitting a PR. + +### Table of Contents (TOC) Feature + +The TOC sidebar provides navigation for long documents. + +**Visual & Layout:** +- [ ] TOC visible on left sidebar on desktop (≥1024px width) +- [ ] TOC hidden on mobile/tablet (<1024px) +- [ ] Width is 240px on desktop +- [ ] Background: semi-transparent card with backdrop blur +- [ ] Border on right side separates TOC from content + +**Hierarchy & Structure:** +- [ ] Shows H1-H3 headings in hierarchical structure +- [ ] H1 headings have no left indent +- [ ] H2 headings indented one level (pl-4) +- [ ] H3 headings indented two levels (pl-6) +- [ ] Nested items appear under parent headings + +**Active Section Highlighting:** +- [ ] Active section highlights as you scroll +- [ ] Highlight uses primary color +- [ ] Left border (2px) appears on active item +- [ ] Background tint (primary/10) on active item +- [ ] Only one item active at a time + +**Navigation:** +- [ ] Clicking TOC item scrolls smoothly to that section +- [ ] Scroll accounts for sticky header offset (no overlap) +- [ ] Smooth scroll animation (behavior: smooth) +- [ ] Navigation works for all heading levels + +**Annotation Badges:** +- [ ] Annotation count badges show correct numbers +- [ ] Badges only appear when annotations exist in section +- [ ] Badge style: rounded-full with accent color +- [ ] Badge position: right side of TOC item + +**Collapse/Expand:** +- [ ] Chevron button appears for headings with children +- [ ] Chevron rotates correctly (down = expanded, right = collapsed) +- [ ] Clicking chevron toggles child visibility +- [ ] Expand/collapse state persists while scrolling +- [ ] Multiple sections can be collapsed independently + +**Interactions:** +- [ ] Hover state shows background change +- [ ] Long heading text wraps properly (line-clamp-2) +- [ ] Text remains readable when truncated +- [ ] Hover shows full text via title attribute (if truncated) + +**Keyboard Navigation:** +- [ ] Tab key focuses TOC items +- [ ] Enter key navigates to section +- [ ] Space key also navigates to section +- [ ] Focus indicators visible on all items +- [ ] Chevron buttons keyboard accessible + +**Scrolling & Performance:** +- [ ] TOC scrolls independently from main content +- [ ] Sticky positioning works (stays visible while scrolling) +- [ ] No horizontal scrollbar appears +- [ ] No lag with 100+ headings +- [ ] Active section updates smoothly while scrolling + +### Annotation Features + +**Text Selection & Toolbar:** +- [ ] Selecting text shows annotation toolbar +- [ ] Toolbar appears near selection (above or below) +- [ ] Toolbar has correct z-index (above other content) +- [ ] Toolbar shows all annotation type options + +**Annotation Types:** +- [ ] **DELETION:** Red highlight, text appears crossed out in export +- [ ] **REPLACEMENT:** Shows old text → new text +- [ ] **COMMENT:** Comment bubble/tooltip appears +- [ ] **INSERTION:** New text shown at cursor position with insertion point +- [ ] **GLOBAL_COMMENT:** Accessible from sticky header button + +**Annotation Panel:** +- [ ] Panel lists all annotations +- [ ] Annotations grouped correctly +- [ ] Can click annotation in panel to highlight in document +- [ ] Can edit annotation text +- [ ] Can delete annotations +- [ ] Deletion count shows in panel header + +**Export & Formatting:** +- [ ] Export diff shows annotations in readable format +- [ ] Format follows: "- **DELETION** (line X): [original text]" +- [ ] Global comments appear at top of export +- [ ] All annotation types exported correctly + +**Code Blocks:** +- [ ] Can annotate text in code blocks +- [ ] Code block annotations use manual mark wrapping +- [ ] Syntax highlighting preserved after annotation +- [ ] Multi-line code annotations work + +**Images:** +- [ ] Can attach images to annotations +- [ ] Image annotation tools available (pen, arrow, circle) +- [ ] Annotated images display in review UI +- [ ] Can remove image attachments + +### Sticky Header + +**Position & Behavior:** +- [ ] Header stays at top while scrolling (position: sticky) +- [ ] Header has proper z-index (z-20) +- [ ] Header appears above content but below annotation toolbar (z-100) +- [ ] Sticky behavior works on all screen sizes + +**Buttons & Controls:** +- [ ] Global comment button always accessible +- [ ] Global comment button opens annotation input +- [ ] Theme toggle works (dark/light/system) +- [ ] Mode switcher works (selection mode / redline mode) +- [ ] Share button generates valid URL +- [ ] Settings button opens modal +- [ ] All buttons have proper hover states + +**Settings Modal:** +- [ ] Settings modal opens correctly +- [ ] Identity settings save (name/email) +- [ ] Plan save settings persist (enabled/path) +- [ ] Obsidian settings work (vault selection) +- [ ] Bear settings work (enable/disable) +- [ ] Settings persist across sessions (stored in cookies) + +**Visual Polish:** +- [ ] Header background: semi-transparent with backdrop blur +- [ ] Border on bottom separates header from content +- [ ] Header height: 48px (h-12) +- [ ] Content doesn't overlap with header when scrolling + +### Responsive Design + +**Desktop (≥1024px):** +- [ ] TOC visible on left (240px width) +- [ ] Main content area uses remaining width +- [ ] Document centered with max-width constraint +- [ ] All buttons and controls accessible +- [ ] Layout doesn't feel cramped + +**Tablet (768-1023px):** +- [ ] TOC hidden completely +- [ ] Document uses full width +- [ ] Touch targets sized appropriately +- [ ] No horizontal scrolling + +**Mobile (<768px):** +- [ ] TOC hidden completely +- [ ] Content optimized for small screens +- [ ] Font sizes readable (not too small) +- [ ] Touch targets minimum 44x44px +- [ ] Buttons don't overlap +- [ ] Modal dialogs fit on screen +- [ ] No pinch-zoom required to read text + +**General Responsive:** +- [ ] No horizontal scrolling on any screen size +- [ ] Touch gestures work (scroll, tap, swipe) +- [ ] Viewport meta tag configured correctly +- [ ] Layout reflows smoothly when resizing + +### Accessibility + +**Keyboard Navigation:** +- [ ] All interactive elements keyboard accessible (Tab, Enter, Space) +- [ ] Tab order logical (top to bottom, left to right) +- [ ] Escape key closes modals/dialogs +- [ ] Arrow keys work in appropriate contexts + +**Focus Indicators:** +- [ ] Focus indicators visible on all interactive elements +- [ ] Focus indicator has sufficient contrast +- [ ] Focus indicator not hidden by CSS + +**Screen Readers:** +- [ ] Headings announce with correct levels (H1, H2, H3) +- [ ] Links and buttons have descriptive labels +- [ ] Icon-only buttons have aria-label +- [ ] Images have alt text or aria-label +- [ ] Form inputs have associated labels + +**ARIA Attributes:** +- [ ] `aria-current="location"` on active TOC item +- [ ] `aria-label` on icon-only buttons +- [ ] `aria-expanded` on collapsible elements +- [ ] `role="navigation"` on TOC (implicit with nav element) + +**Color & Contrast:** +- [ ] Color contrast meets WCAG AA (4.5:1 for text, 3:1 for UI) +- [ ] Information not conveyed by color alone +- [ ] Links distinguishable from text (not just color) +- [ ] Dark mode has sufficient contrast + +### Performance & Browser Compatibility + +**Performance:** +- [ ] Large documents (100+ headings, 10+ pages) load quickly (<2s) +- [ ] Scrolling is smooth (60fps) +- [ ] No jank when highlighting active section +- [ ] Annotation creation is instant (no lag) +- [ ] No memory leaks (check DevTools Memory tab) +- [ ] Page load time acceptable (<3s on fast connection) + +**Browser Compatibility:** +- [ ] Works in Chrome (latest) +- [ ] Works in Firefox (latest) +- [ ] Works in Safari (latest) +- [ ] Works in Edge (latest) +- [ ] Fallbacks for unsupported features (if any) + +--- + +## Debugging Common Issues + +### Browser DevTools + +Open DevTools to inspect and debug: +- **Mac:** Cmd+Option+I +- **Windows/Linux:** F12 or Ctrl+Shift+I + +**Useful tabs:** +- **Console:** JavaScript errors and logs +- **Network:** Failed requests, slow resources +- **Elements:** Inspect DOM and CSS +- **Performance:** Profile rendering performance +- **Memory:** Check for memory leaks + +**Recommended extensions:** +- React DevTools - Inspect component tree and props +- Redux DevTools - If using Redux (not currently) + +### Common Issues & Solutions + +#### Port Already in Use + +**Error:** +``` +Error: listen EADDRINUSE: address already in use :::5173 +``` + +**Solution:** Kill the process using that port + +**macOS/Linux:** +```bash +lsof -ti:5173 | xargs kill -9 +``` + +**Windows:** +```powershell +netstat -ano | findstr :5173 +taskkill /PID /F +``` + +#### Module Not Found + +**Error:** +``` +Error: Cannot find module '@plannotator/ui' +``` + +**Solution:** Clean install dependencies +```bash +rm -rf node_modules +bun install +``` + +#### Hot Reload Not Working + +**Symptom:** Changes don't appear in browser after saving file + +**Solutions:** +1. Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows/Linux) +2. Restart dev server: Ctrl+C then `bun run dev:hook` +3. Clear browser cache +4. Check terminal for errors + +#### CSS Not Applying + +**Symptom:** Tailwind classes not working or styles look wrong + +**Solutions:** +1. Check for typos in class names (Tailwind is strict) +2. Verify Tailwind config includes your file paths +3. Try rebuilding: `bun run build:hook` +4. Check if another CSS rule is overriding (use DevTools Elements tab) +5. Ensure you're using correct responsive prefixes (`sm:`, `md:`, `lg:`) + +#### TypeScript/LSP Errors + +**Symptom:** Editor shows red squiggles, but code works + +**Important:** Many LSP errors in this codebase are warnings, not blockers. + +**Solutions:** +1. Focus on fixing errors in files YOU changed +2. Run `bun run build` to see actual compilation errors +3. Existing files may have warnings - that's okay +4. If new errors appear in your files, fix them + +**Common LSP warnings you can ignore:** +- "Alternative text title element cannot be empty" (SVG icons) +- "This hook does not specify its dependency" (known) +- "Provide an explicit type prop for button" (existing code) + +#### Build Fails + +**Error:** +``` +Build failed with X errors +``` + +**Solutions:** +1. Read the error message carefully (shows file and line) +2. Check for syntax errors in your changes +3. Verify imports are correct +4. Run `bun install` to ensure dependencies are up to date +5. Check that file paths are correct (case-sensitive on Linux/macOS) + +### Viewing Logs + +**Server logs:** +- Check terminal where `bun` is running +- Server prints requests and errors +- Hook output shows approve/deny decisions + +**Browser logs:** +- DevTools → Console tab +- Network tab shows request/response details +- Preserve log checkbox keeps logs across page loads + +**Test script output:** +- Test scripts print to terminal +- Shows build output, server startup, and hook decisions +- Use `echo` statements to add debug output to scripts + +--- + +## Need Help? + +If you're stuck: + +1. Check this guide again +2. Review existing code for patterns +3. Look at `CLAUDE.md` for architecture details +4. Check `tests/README.md` for test script details +5. Open an issue on GitHub with: + - What you're trying to do + - What you've tried + - Error messages (full text) + - Browser and OS version diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 9b64f81..5eb16ec 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -11,8 +11,10 @@ import { ModeSwitcher } from '@plannotator/ui/components/ModeSwitcher'; import { TaterSpriteRunning } from '@plannotator/ui/components/TaterSpriteRunning'; import { TaterSpritePullup } from '@plannotator/ui/components/TaterSpritePullup'; import { Settings } from '@plannotator/ui/components/Settings'; +import { TableOfContents } from '@plannotator/ui/components/TableOfContents'; import { useSharing } from '@plannotator/ui/hooks/useSharing'; import { useAgents } from '@plannotator/ui/hooks/useAgents'; +import { useActiveSection } from '@plannotator/ui/hooks/useActiveSection'; import { storage } from '@plannotator/ui/utils/storage'; import { UpdateBanner } from '@plannotator/ui/components/UpdateBanner'; import { getObsidianSettings, getEffectiveVaultPath, CUSTOM_PATH_SENTINEL } from '@plannotator/ui/utils/obsidian'; @@ -349,6 +351,10 @@ const App: React.FC = () => { const [sharingEnabled, setSharingEnabled] = useState(true); const [repoInfo, setRepoInfo] = useState<{ display: string; branch?: string } | null>(null); const viewerRef = useRef(null); + const containerRef = useRef(null); + + // Track active section for TOC highlighting + const activeSection = useActiveSection(containerRef); // URL-based sharing const { @@ -651,6 +657,11 @@ const App: React.FC = () => { setGlobalAttachments(prev => prev.filter(p => p !== path)); }; + const handleTocNavigate = (blockId: string) => { + // Navigation handled by TableOfContents component + // This is just a placeholder for future custom logic + }; + const diffOutput = useMemo(() => exportDiff(blocks, annotations, globalAttachments), [blocks, annotations, globalAttachments]); const agentName = useMemo(() => { @@ -665,7 +676,7 @@ const App: React.FC = () => { {/* Tater sprites */} {taterMode && } {/* Minimal Header */} -
+
{ {/* Main Content */}
+ {/* Table of Contents */} + + {/* Document Area */} -
+
{/* Mode Switcher */}
diff --git a/packages/ui/components/TableOfContents.tsx b/packages/ui/components/TableOfContents.tsx new file mode 100644 index 0000000..2419ad1 --- /dev/null +++ b/packages/ui/components/TableOfContents.tsx @@ -0,0 +1,213 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import type { Block, Annotation } from '../types'; +import { + buildTocHierarchy, + getAnnotationCountBySection, + type TocItem, +} from '../utils/annotationHelpers'; + +interface TableOfContentsProps { + blocks: Block[]; + annotations: Annotation[]; + activeId: string | null; + onNavigate: (blockId: string) => void; + className?: string; +} + +interface TocItemProps { + item: TocItem; + activeId: string | null; + onNavigate: (blockId: string) => void; + isExpanded: boolean; + onToggle: () => void; + hasChildren: boolean; +} + +function TocItemComponent({ + item, + activeId, + onNavigate, + isExpanded, + onToggle, + hasChildren, +}: TocItemProps) { + const isActive = item.id === activeId; + const indent = item.level === 1 ? 'pl-0' : item.level === 2 ? 'pl-4' : 'pl-6'; + + return ( +
  • +
    + {hasChildren && ( + + )} + + +
    + + {hasChildren && isExpanded && ( +
      + {item.children.map((child) => ( + + ))} +
    + )} +
  • + ); +} + +function TocItemWithState({ + item, + activeId, + onNavigate, +}: { + item: TocItem; + activeId: string | null; + onNavigate: (blockId: string) => void; +}) { + const [isExpanded, setIsExpanded] = useState(true); + const hasChildren = item.children.length > 0; + + return ( + setIsExpanded(!isExpanded)} + hasChildren={hasChildren} + /> + ); +} + +export function TableOfContents({ + blocks, + annotations, + activeId, + onNavigate, + className = '', +}: TableOfContentsProps) { + // Calculate annotation counts per section + const annotationCounts = useMemo( + () => getAnnotationCountBySection(blocks, annotations), + [blocks, annotations] + ); + + // Build hierarchical TOC structure + const tocItems = useMemo( + () => buildTocHierarchy(blocks, annotationCounts), + [blocks, annotationCounts] + ); + + // Handle navigation with smooth scroll + const handleNavigate = useCallback( + (blockId: string) => { + onNavigate(blockId); + + // Find target element and scroll to it + const target = document.querySelector(`[data-block-id="${blockId}"]`); + if (target) { + // Find the scrollable main container + const scrollContainer = document.querySelector('main'); + if (!scrollContainer) return; + + // Account for sticky header (48px = h-12) + const headerOffset = 80; + const containerRect = scrollContainer.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + const scrollTop = scrollContainer.scrollTop; + const relativeTop = targetRect.top - containerRect.top; + const offsetPosition = scrollTop + relativeTop - headerOffset; + + scrollContainer.scrollTo({ + top: offsetPosition, + behavior: 'smooth', + }); + } + }, + [onNavigate] + ); + + if (tocItems.length === 0) { + return null; + } + + return ( + + ); +} diff --git a/packages/ui/components/Viewer.tsx b/packages/ui/components/Viewer.tsx index 63bb8aa..5040a5a 100644 --- a/packages/ui/components/Viewer.tsx +++ b/packages/ui/components/Viewer.tsx @@ -648,7 +648,7 @@ export const Viewer = forwardRef(({ )} {/* Header buttons - top right */} -
    +
    {/* Attachments button */} {onAddGlobalAttachment && onRemoveGlobalAttachment && ( = ({ block }) => { 3: 'text-base font-semibold mb-2 mt-6 text-foreground/80', }[block.level || 1] || 'text-base font-semibold mb-2 mt-4'; - return ; + return ; case 'blockquote': return ( diff --git a/packages/ui/hooks/useActiveSection.ts b/packages/ui/hooks/useActiveSection.ts new file mode 100644 index 0000000..9a83c0d --- /dev/null +++ b/packages/ui/hooks/useActiveSection.ts @@ -0,0 +1,71 @@ +import { useEffect, useState, useRef } from 'react'; + +/** + * Track which heading section is currently visible in the viewport + * Uses Intersection Observer to detect when headings enter/leave view + */ +export function useActiveSection(containerRef: React.RefObject) { + const [activeId, setActiveId] = useState(null); + const observerRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + // Find all heading elements with data-block-id + const headings = container.querySelectorAll('[data-block-type="heading"]'); + if (headings.length === 0) return; + + // Track which headings are currently intersecting + const intersectingHeadings = new Map(); + + // Create observer with configuration to detect headings near top of viewport + observerRef.current = new IntersectionObserver( + (entries) => { + // Update intersection state + entries.forEach(entry => { + intersectingHeadings.set(entry.target, entry.isIntersecting); + }); + + // Find the first intersecting heading (topmost in viewport) + let topHeading: Element | null = null; + let topPosition = Infinity; + + for (const [heading, isIntersecting] of intersectingHeadings) { + if (isIntersecting) { + const rect = heading.getBoundingClientRect(); + if (rect.top < topPosition) { + topPosition = rect.top; + topHeading = heading; + } + } + } + + // Update active ID + if (topHeading) { + const blockId = topHeading.getAttribute('data-block-id'); + if (blockId) { + setActiveId(blockId); + } + } + }, + { + root: null, // viewport + rootMargin: '-80px 0px -80% 0px', // Activate when heading is near top + threshold: [0, 0.1, 0.5, 1.0], + } + ); + + // Observe all headings + headings.forEach(heading => { + observerRef.current?.observe(heading); + }); + + // Cleanup + return () => { + observerRef.current?.disconnect(); + }; + }, [containerRef]); + + return activeId; +} diff --git a/packages/ui/utils/annotationHelpers.ts b/packages/ui/utils/annotationHelpers.ts new file mode 100644 index 0000000..fc7a568 --- /dev/null +++ b/packages/ui/utils/annotationHelpers.ts @@ -0,0 +1,101 @@ +import type { Block, Annotation } from '../types'; + +/** + * Calculate annotation counts per section (grouped by headings) + * A section includes all blocks from a heading until the next heading of same/higher level + */ +export function getAnnotationCountBySection( + blocks: Block[], + annotations: Annotation[] +): Map { + const counts = new Map(); + + // Find all headings + const headings = blocks.filter(b => b.type === 'heading' && (b.level ?? 0) <= 3); + + if (headings.length === 0) return counts; + + // For each heading, determine which blocks belong to its section + for (let i = 0; i < headings.length; i++) { + const heading = headings[i]; + const currentLevel = heading.level ?? 1; + const startLine = heading.startLine; + + // Find the end of this section (next heading of same/higher level) + let endLine = Infinity; + for (let j = i + 1; j < headings.length; j++) { + const nextHeading = headings[j]; + const nextLevel = nextHeading.level ?? 1; + if (nextLevel <= currentLevel) { + endLine = nextHeading.startLine; + break; + } + } + + // Count annotations in blocks within this section + let count = 0; + for (const block of blocks) { + if (block.startLine >= startLine && block.startLine < endLine) { + // Count annotations that belong to this block + const blockAnnotations = annotations.filter(a => a.blockId === block.id); + count += blockAnnotations.length; + } + } + + counts.set(heading.id, count); + } + + return counts; +} + +/** + * Build hierarchical TOC structure from flat blocks array + */ +export interface TocItem { + id: string; + content: string; + level: number; + order: number; + children: TocItem[]; + annotationCount: number; +} + +export function buildTocHierarchy( + blocks: Block[], + annotationCounts: Map +): TocItem[] { + const headings = blocks + .filter(b => b.type === 'heading' && (b.level ?? 0) <= 3) + .sort((a, b) => a.order - b.order); + + const root: TocItem[] = []; + const stack: TocItem[] = []; + + for (const heading of headings) { + const item: TocItem = { + id: heading.id, + content: heading.content, + level: heading.level ?? 1, + order: heading.order, + children: [], + annotationCount: annotationCounts.get(heading.id) ?? 0, + }; + + // Find the correct parent based on level + while (stack.length > 0 && stack[stack.length - 1].level >= item.level) { + stack.pop(); + } + + if (stack.length === 0) { + // Top-level item + root.push(item); + } else { + // Child of the last item in stack + stack[stack.length - 1].children.push(item); + } + + stack.push(item); + } + + return root; +} From 7137cfa28d25d7623aaab7e44a9294838f30c8d2 Mon Sep 17 00:00:00 2001 From: thoroc Date: Fri, 30 Jan 2026 19:09:29 +0000 Subject: [PATCH 2/2] fix: Correct typo in opencode.json (CALUDE -> CLAUDE) --- opencode.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 opencode.json diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..8bcbdd7 --- /dev/null +++ b/opencode.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://opencode.ai/config.json", + "instructions": ["README.md", "CLAUDE.md"], + "plugin": [ + "@tarquinen/opencode-dcp@latest", + "@ramtinj95/opencode-tokenscope@latest", + "opencode-todo-reminder@latest", + "opencode-command-hooks@latest", + "@plannotator/opencode@latest" + ] +}