mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
313c45138c
* 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 * fix: Correct typo in opencode.json (CALUDE -> CLAUDE) * docs: Update testing documentation - separate UI tests from integration/utility tests * fix(ui): prevent code block annotation from breaking syntax highlighting - Replace surroundContents() with plain text wrapper approach - Add syntax highlighting restoration in removeHighlight() - Fixes issue reported by kkharji in PR #122 The old approach used range.surroundContents() which wrapped syntax-highlighted <span> elements, creating nested structure that broke the layout. The new approach replaces code block innerHTML with plain text wrapped in <mark>, then restores syntax highlighting when the annotation is removed. Test coverage: 15 tests pass with edge cases for empty blocks, special chars, large blocks (10k), unicode, and multiple annotation cycles. * test(ui): add comprehensive code block annotation regression tests - Add 15 test cases covering code block annotation behavior - Test plain text wrapper approach vs nested span approach - Verify syntax highlighting restoration on annotation removal - Cover edge cases: empty blocks, special chars, large blocks, unicode - Add happy-dom dev dependency for DOM testing * feat: update opencode.json to include additional documentation instructions and configure Playwright MCP * chore: stop tracking docs directory and opencode.json * chore: move UI-TESTING.md to tests directory * docs: extract UI testing checklist into separate file - Move comprehensive feature checklists to UI-TESTING-CHECKLIST.md - Keep main UI-TESTING.md focused on development workflow and setup - Add reference link to checklist file for easy navigation * docs: update UI testing documentation * fix(ui): fix TOC active section tracking IntersectionObserver was using root: null (viewport) instead of the actual scroll container (<main>), and the effect only ran on mount before headings were rendered. Pass the container as root and re-run the observer when heading count changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * revert: remove Playwright MCP from marketplace.json and stale README links Playwright MCP is dev tooling, not for end users. README referenced docs/UI-TESTING.md and docs/CODE-STYLE.md which don't exist at those paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(ui): compact TOC styling and fix layout shifts - Smaller text (text-xs) and tighter padding for compact feel - Remove border-l-2 and font-medium from active state to prevent layout shifts that pushed text into multiline - Annotation count badges are now perfect circles (w-5 h-5) - Add left padding to root-level heading Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(ui): add optional TOC and sticky actions settings - New uiPreferences.ts utility for cookie-based persistence - TOC toggle: conditionally renders sidebar in App.tsx - Sticky Actions toggle: conditionally applies sticky positioning in Viewer.tsx - Both default to enabled, persisted via cookies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(ui): redesign Settings dialog with sidebar navigation Replace flat settings list with sidebar + content panel layout: - General tab: Identity, Permission Mode, Agent Switching - Display tab: TOC, Sticky Actions, Tater Mode - Saving tab: Plan Saving, Obsidian, Bear Notes - Scrollable content area (max-h-[70vh]) - Sidebar hidden in review mode (single tab) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ui): fix sticky action bar positioning and add scroll-aware card background Match main's button positioning across all breakpoints with responsive margins that account for article padding. Use IntersectionObserver sentinel to detect when the bar is stuck, revealing the card background only on scroll. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
102 lines
2.7 KiB
TypeScript
102 lines
2.7 KiB
TypeScript
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<string, number> {
|
|
const counts = new Map<string, number>();
|
|
|
|
// 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<string, number>
|
|
): 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;
|
|
}
|