Files
dgrissen2 5a41a21861 fix(ui): in-page anchor navigation for headings (#605)
* fix(ui): in-page anchor navigation for headings

Intercept `#`-prefixed link clicks and smooth-scroll inside the sticky
scroll viewport instead of letting the browser jump (which broke when the
content lives inside a scrollable container). Headings now expose both a
canonical slug and a legacy slug via `data-anchor-aliases` so older shared
URLs keep resolving. Also listen for `hashchange` / initial hash so direct
links land on the right heading.

Adds `packages/ui/utils/anchors.ts` with `normalizeAnchorText`,
`decodeAnchorHash`, `slugifyHeadingAnchor`, `legacySlugifyHeadingAnchor`,
and `getHeadingAnchorAliases` (+ tests).

Also ignore `.serena/` tooling state.

* refactor(ui): drop unjustified slug aliasing

Anchor navigation didn't work before this branch, so no shared URLs exist
using any slug format — the legacy slug alias was preserving nothing.

The canonical slug was also a redefinition: heading ids are already
written by `utils/slugify.ts` (unicode-aware, `\p{L}\p{N}`). The new
ASCII-only `slugifyHeadingAnchor` would miss unicode headings ("Café"
writes `id="café"` but the nav code looked for `cafe`).

Nav path is now just: decode the hash and `getElementById`. The
sticky-header offset math — the part that actually mattered — stays.

* fix(ui): anchor-nav edge cases — share-hash collision and raw HTML blocks

Two issues flagged by cross-model review of 9978a40:

1. `useSharing` treated any non-empty `window.location.hash` as a share
   payload. Plain anchor hashes like `#section-overview` fell into
   `parseShareHash`, failed decompression, and popped the "Shared Plan
   Could Not Be Loaded" dialog. Added an `isPlainAnchorHash` guard
   (lowercase ASCII + digits + hyphen) that short-circuits before the
   share path in both the initial load and the hashchange listener.
   Share payloads are base64url-encoded deflate output, so they'll
   always contain uppercase/`=`/`_` and never match the guard.

2. `HtmlBlock` (raw `<details>`, `<summary>`, etc.) skipped the new
   nav handler. `rewriteRelativeRefs` returned early for any `#` href,
   so anchors inside raw HTML fell back to native browser jump which
   doesn't target the OverlayScrollArea. Now intercepts those clicks
   and routes through `onNavigateAnchor` to match the `InlineMarkdown`
   path. Threaded the callback through `BlockRenderer`.

* fix(ui): widen share-payload detection to cover Unicode + uppercase ids

Previous guard rejected any hash that wasn't lowercase ASCII + digits +
hyphen, so Unicode heading ids (`#café`, `#中文-标题`) and raw HTML ids
(`#MySection`) still fell into `parseShareHash` and popped the shared-
plan error dialog.

Flip the predicate: a share payload is base64url (`[A-Za-z0-9_-]`),
realistically ≥30 chars, and virtually always contains at least one
uppercase letter because deflate output has high entropy. Anything else
is handed back to Viewer to scroll to (or silently ignored).

Also add tests/test-fixtures/14-anchor-links.md exercising the full
character range — ASCII, Unicode (accented, CJK, Cyrillic), numeric
start, raw HTML ids — across paragraph, list, blockquote, alert,
directive, table, and raw `<details>` contexts.
2026-04-23 13:39:22 -07:00
..

Tests

This directory contains manual testing scripts for Plannotator.

UI Tests (manual/local/)

These scripts test the UI components and require a browser.

Plan review UI:

./tests/manual/local/test-hook.sh          # Claude Code simulation
./tests/manual/local/test-hook-2.sh        # OpenCode origin badge test

Code review UI:

./tests/manual/local/test-opencode-review.sh  # Code review UI test
./tests/manual/local/test-worktree-review.sh  # Worktree support test (creates sandbox with 4 worktrees)

See UI-TESTING.md for detailed UI testing documentation.

Integration & Utility Tests (manual/local/)

These scripts test integrations, releases, and provide utilities.

Binary release testing:

./tests/manual/local/test-binary.sh        # Test installed binary from ~/.local/bin/

Tests the installed plannotator binary to verify releases work correctly.

Bulk plan testing (Obsidian integration):

./tests/manual/local/test-bulk-plans.sh    # Iterate through ~/.claude/plans/

Opens each .md file from ~/.claude/plans/ in Plannotator. Great for testing Obsidian integration with multiple plans.

OpenCode integration sandbox:

./tests/manual/local/sandbox-opencode.sh [--disable-sharing] [--keep] [--no-git]

Creates a temporary sandbox with a sample React/TypeScript project, initializes git with uncommitted changes, sets up the local OpenCode plugin, and launches OpenCode for full integration testing.

Options:

  • --disable-sharing: Creates opencode.json with sharing disabled
  • --keep: Don't clean up sandbox on exit
  • --no-git: Skip git initialization (tests non-git fallback)

Obsidian utility:

./tests/manual/local/fix-vault-links.sh /path/to/vault/plannotator

Adds Obsidian backlinks ([[Plannotator Plans]]) to existing plan files in your vault.

SSH Remote Testing (manual/ssh/)

Tests SSH session detection and port forwarding for remote development scenarios.

cd tests/manual/ssh/
docker-compose up -d
./test-ssh.sh

See manual/ssh/DOCKER_SSH_TEST.md for detailed setup instructions.