Skip to content

Commit 313c451

Browse files
thorocbacknotpropclaude
authored
feat: TOC sidebar, sticky actions, and settings redesign (#122)
* 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>
1 parent 1642934 commit 313c451

13 files changed

Lines changed: 2067 additions & 326 deletions

bun.lock

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,8 @@
2929
},
3030
"dependencies": {
3131
"@pierre/diffs": "^1.0.4"
32+
},
33+
"devDependencies": {
34+
"happy-dom": "^20.5.0"
3235
}
3336
}

packages/editor/App.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ import { ModeSwitcher } from '@plannotator/ui/components/ModeSwitcher';
1111
import { TaterSpriteRunning } from '@plannotator/ui/components/TaterSpriteRunning';
1212
import { TaterSpritePullup } from '@plannotator/ui/components/TaterSpritePullup';
1313
import { Settings } from '@plannotator/ui/components/Settings';
14+
import { TableOfContents } from '@plannotator/ui/components/TableOfContents';
1415
import { useSharing } from '@plannotator/ui/hooks/useSharing';
1516
import { useAgents } from '@plannotator/ui/hooks/useAgents';
17+
import { useActiveSection } from '@plannotator/ui/hooks/useActiveSection';
1618
import { storage } from '@plannotator/ui/utils/storage';
1719
import { UpdateBanner } from '@plannotator/ui/components/UpdateBanner';
1820
import { getObsidianSettings, getEffectiveVaultPath, CUSTOM_PATH_SENTINEL } from '@plannotator/ui/utils/obsidian';
1921
import { getBearSettings } from '@plannotator/ui/utils/bear';
2022
import { getAgentSwitchSettings, getEffectiveAgentName } from '@plannotator/ui/utils/agentSwitch';
2123
import { getPlanSaveSettings } from '@plannotator/ui/utils/planSave';
24+
import { getUIPreferences, type UIPreferences } from '@plannotator/ui/utils/uiPreferences';
2225
import { getEditorMode, saveEditorMode } from '@plannotator/ui/utils/editorMode';
2326
import {
2427
getPermissionModeSettings,
@@ -338,6 +341,7 @@ const App: React.FC = () => {
338341
const stored = storage.getItem('plannotator-tater-mode');
339342
return stored === 'true';
340343
});
344+
const [uiPrefs, setUiPrefs] = useState(() => getUIPreferences());
341345
const [isApiMode, setIsApiMode] = useState(false);
342346
const [origin, setOrigin] = useState<'claude-code' | 'opencode' | null>(null);
343347
const [globalAttachments, setGlobalAttachments] = useState<string[]>([]);
@@ -350,6 +354,11 @@ const App: React.FC = () => {
350354
const [sharingEnabled, setSharingEnabled] = useState(true);
351355
const [repoInfo, setRepoInfo] = useState<{ display: string; branch?: string } | null>(null);
352356
const viewerRef = useRef<ViewerHandle>(null);
357+
const containerRef = useRef<HTMLElement>(null);
358+
359+
// Track active section for TOC highlighting
360+
const headingCount = useMemo(() => blocks.filter(b => b.type === 'heading').length, [blocks]);
361+
const activeSection = useActiveSection(containerRef, headingCount);
353362

354363
// URL-based sharing
355364
const {
@@ -657,6 +666,11 @@ const App: React.FC = () => {
657666
setGlobalAttachments(prev => prev.filter(p => p !== path));
658667
};
659668

669+
const handleTocNavigate = (blockId: string) => {
670+
// Navigation handled by TableOfContents component
671+
// This is just a placeholder for future custom logic
672+
};
673+
660674
const diffOutput = useMemo(() => exportDiff(blocks, annotations, globalAttachments), [blocks, annotations, globalAttachments]);
661675

662676
const agentName = useMemo(() => {
@@ -671,7 +685,7 @@ const App: React.FC = () => {
671685
{/* Tater sprites */}
672686
{taterMode && <TaterSpriteRunning />}
673687
{/* Minimal Header */}
674-
<header className="h-12 flex items-center justify-between px-2 md:px-4 border-b border-border/50 bg-card/50 backdrop-blur-xl z-50">
688+
<header className="h-12 flex items-center justify-between px-2 md:px-4 border-b border-border/50 bg-card/50 backdrop-blur-xl sticky top-0 z-20">
675689
<div className="flex items-center gap-2 md:gap-3">
676690
<a
677691
href="https://plannotator.ai"
@@ -772,7 +786,7 @@ const App: React.FC = () => {
772786
)}
773787

774788
<ModeToggle />
775-
<Settings taterMode={taterMode} onTaterModeChange={handleTaterModeChange} onIdentityChange={handleIdentityChange} origin={origin} />
789+
<Settings taterMode={taterMode} onTaterModeChange={handleTaterModeChange} onIdentityChange={handleIdentityChange} origin={origin} onUIPreferencesChange={setUiPrefs} />
776790

777791
<button
778792
onClick={() => setIsPanelOpen(!isPanelOpen)}
@@ -802,8 +816,19 @@ const App: React.FC = () => {
802816

803817
{/* Main Content */}
804818
<div className="flex-1 flex overflow-hidden">
819+
{/* Table of Contents */}
820+
{uiPrefs.tocEnabled && (
821+
<TableOfContents
822+
blocks={blocks}
823+
annotations={annotations}
824+
activeId={activeSection}
825+
onNavigate={handleTocNavigate}
826+
className="hidden lg:block w-60 sticky top-12 h-[calc(100vh-3rem)] flex-shrink-0"
827+
/>
828+
)}
829+
805830
{/* Document Area */}
806-
<main className="flex-1 overflow-y-auto bg-grid">
831+
<main ref={containerRef} className="flex-1 overflow-y-auto bg-grid">
807832
<div className="min-h-full flex flex-col items-center px-4 py-3 md:px-10 md:py-8 xl:px-16">
808833
{/* Mode Switcher */}
809834
<div className="w-full max-w-[832px] 2xl:max-w-5xl mb-3 md:mb-4 flex justify-start">
@@ -825,6 +850,7 @@ const App: React.FC = () => {
825850
onAddGlobalAttachment={handleAddGlobalAttachment}
826851
onRemoveGlobalAttachment={handleRemoveGlobalAttachment}
827852
repoInfo={repoInfo}
853+
stickyActions={uiPrefs.stickyActionsEnabled}
828854
/>
829855
</div>
830856
</main>

0 commit comments

Comments
 (0)