From fa4da03b7aa19a95265ca270e3565e065fcc405d Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 9 Apr 2026 15:13:24 -0700 Subject: [PATCH 1/6] fix: pin bullet markers to top of multi-line list items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `items-start` to list item flex container so markers align to the first line instead of centering vertically across the full item height - Use consistent bullet glyph (•) across all nesting levels - Add test fixture 10 covering all bullet/list types and inline gap cases For provenance purposes, this commit was AI assisted. --- packages/ui/components/ListMarker.tsx | 2 +- packages/ui/components/Viewer.tsx | 2 +- .../10-inline-gaps-and-bullets.md | 192 ++++++++++++++++++ 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 tests/test-fixtures/10-inline-gaps-and-bullets.md diff --git a/packages/ui/components/ListMarker.tsx b/packages/ui/components/ListMarker.tsx index b5507d97..056dd774 100644 --- a/packages/ui/components/ListMarker.tsx +++ b/packages/ui/components/ListMarker.tsx @@ -25,7 +25,7 @@ interface ListMarkerProps { onToggle?: () => void; } -const BULLET_BY_LEVEL = ['\u2022', '\u25E6', '\u25AA']; +const BULLET_BY_LEVEL = ['\u2022', '\u2022', '\u2022']; export const ListMarker: React.FC = ({ level, diff --git a/packages/ui/components/Viewer.tsx b/packages/ui/components/Viewer.tsx index 38e6a7f6..34d51368 100644 --- a/packages/ui/components/Viewer.tsx +++ b/packages/ui/components/Viewer.tsx @@ -1020,7 +1020,7 @@ const BlockRenderer: React.FC<{ const isInteractive = isCheckbox && !!onToggleCheckbox; return (
diff --git a/tests/test-fixtures/10-inline-gaps-and-bullets.md b/tests/test-fixtures/10-inline-gaps-and-bullets.md new file mode 100644 index 00000000..a3fec142 --- /dev/null +++ b/tests/test-fixtures/10-inline-gaps-and-bullets.md @@ -0,0 +1,192 @@ +# Inline Gaps & Bullet List Coverage + +## Inline Formatting — Known Gaps + +### Strikethrough + +This feature is ~~deprecated~~ and should be removed. + +The old ~~`legacyMode` flag~~ has been replaced by `modernMode`. + +### Nested and Combined Emphasis + +This is ***bold and italic*** at the same time. + +Here is _**underscore italic wrapping bold**_ text. + +And **_bold wrapping underscore italic_** text. + +Single *italic* and **bold** beside each other with no space: *italic***bold**. + +### Backslash Escaping + +Literal asterisk: \*not italic\* + +Literal underscore: \_not italic\_ + +Literal backtick: \`not code\` + +Literal bracket: \[not a link\] + +### Autolinks + +Visit for more info. + +Send mail to . + +### HTML Entities + +Trademark symbol: ™ + +Copyright: © 2024 + +Non-breaking space between these words. + +Ampersand: AT&T + +Less-than sign: 2 < 3 + +### Link Reference Definitions + +Here is a [link to the docs][docs] and another [link to the repo][repo]. + +Also a [bare reference][docs] used twice. + +[docs]: https://plannotator.ai/docs +[repo]: https://github.com/backnotprop/plannotator + +--- + +## Bullet Lists — All Types + +### Unordered — Dash + +- First item +- Second item +- Third item + +### Unordered — Asterisk + +* Alpha +* Beta +* Gamma + +### Ordered — Basic + +1. First step +2. Second step +3. Third step + +### Ordered — Arbitrary Numbers (should renumber 1, 2, 3) + +1. First +99. Second +3. Third + +### Ordered — Starting at Non-1 + +5. Item five +6. Item six +7. Item seven + +### Checkboxes — Unchecked + +- [ ] Write tests +- [ ] Review PR +- [ ] Deploy to staging + +### Checkboxes — Mixed + +- [x] Install dependencies +- [x] Configure environment +- [ ] Run integration tests +- [ ] Write release notes + +### Ordered + Checkbox (GitHub style) + +1. [x] Design spec approved +2. [x] Implementation complete +3. [ ] QA signoff +4. [ ] Shipped + +--- + +## Nested Bullets + +### Two Levels — Unordered + +- Top level item A with enough text that it will wrap across multiple lines when viewed in a normal browser window at typical zoom levels + - Nested under A, also with a longer description so we can confirm the bullet stays pinned to the top of the text and does not drift to the vertical center of the whole item + - Another nested item under A with similar verbosity to make the wrapping behavior clearly visible during manual testing +- Top level item B which also has a fair amount of text to ensure it wraps and we can see how the marker aligns relative to the content block beneath it + - Nested under B with extra detail: this item intentionally runs long so the bullet alignment fix is obvious even at wide viewport widths + +### Three Levels — Unordered + +- Level one — this is a long item that spans multiple lines so we can verify the top-alignment fix holds at the outermost nesting level, not just for deeply nested content + - Level two — also written with enough words to cause line wrapping, confirming the fix applies consistently across all indentation levels and is not just a one-off + - Level three — the deepest level in this section, with enough prose to wrap at least once so the bullet position relative to the first line is clearly observable + - Another level three item with similar length to the one above, included so we have two consecutive wrapping items at the deepest level + - Back to level two with a longer description than before so this item also wraps and we can spot any regression between siblings at the same nesting depth +- Another level one item, intentionally verbose so it wraps and gives us a second data point at the top level for the alignment check + +### Mixed Ordered and Unordered + +1. First ordered item written with enough detail to push it past a single line, confirming that the top-alignment fix works for ordered markers as well as plain bullets + - Unordered child with a long enough description that it wraps, so we can see the bullet sit at the top of the wrapped content rather than floating in the middle + - Another child item, equally verbose, to verify consistent alignment across sibling items within the same parent +2. Second ordered item, also long enough to wrap, giving us a second ordered marker to inspect for correct top alignment + - Child of second ordered item, written long so the bullet wraps and the alignment is testable without squinting +3. Third ordered item to round out the list with a similarly verbose description for consistency + +### Ordered Nested + +1. Phase one — introductory work that sets up the environment, configures dependencies, and establishes the baseline from which all subsequent phases will build + 1. Task A involves researching the current implementation and documenting all the edge cases that need to be addressed before moving forward + 2. Task B covers writing the initial draft of the solution, including inline comments explaining the rationale behind each non-obvious decision +2. Phase two — the main implementation phase where the bulk of the work happens and the majority of the codebase changes are introduced + 1. Task C is the core refactor, touching the parser, the viewer, and the list marker component in a coordinated way to avoid regressions + 2. Task D is the follow-up cleanup pass that removes dead code, updates tests, and ensures the diff engine still produces correct output after the changes + 1. Sub-task D1: update parser tests to cover the new list continuation logic added during the refactor + 2. Sub-task D2: rebuild all build targets in the correct order and run a full manual smoke test against the test fixtures before merging + +### Deep Nesting with Checkboxes + +- [ ] Top-level task with a long description that wraps to confirm the checkbox icon stays at the top of the item rather than centering itself relative to all lines of text + - [x] Subtask that has been completed, written with extra detail so the checked state and the strikethrough styling are both visible across multiple lines of wrapped content + - [ ] Subtask still pending, also written long enough to wrap so we can compare the visual alignment of checked versus unchecked items at the same nesting level + - [ ] Sub-subtask at the deepest level, long enough to wrap and confirm the fix holds even at three levels of nesting with checkbox markers + - [x] Another sub-subtask that is done, included so we have both checked and unchecked examples at this depth for a thorough visual comparison + - [ ] Another subtask at level two, written verbosely to wrap and complete the set of alignment test cases for this nesting structure +- [x] Another top-level task that is complete, with a long description so the strikethrough and muted text styles are visible across multiple wrapped lines + +### Multi-line List Items (continuation lines) + +- This is a list item with a genuinely long description that continues across several lines. It covers enough ground that even on a wide screen it should wrap at least once, giving us a real-world example of the bullet alignment fix in action without relying on artificial line breaks. +- Short item. +- Another item with continuation that also runs long enough to wrap naturally in the browser, so we can confirm the fix applies to continuation-style items the same way it applies to items written as a single long string. + +### Bullets with Inline Formatting + +- Item with **bold text** that also runs long enough to wrap so we can confirm the bullet aligns to the top even when the first line contains a bold span that changes the line's visual weight +- Item with *italic text* and enough surrounding prose that the line wraps and the bullet position relative to the first line is clearly observable during the manual test +- Item with `inline code` embedded partway through a longer sentence so the item wraps and we can see that the code span does not affect bullet alignment in any unexpected way +- Item with a [link to plannotator.ai](https://plannotator.ai) embedded in a longer description that wraps, confirming that anchor elements inside list items do not disrupt the top-alignment behavior +- Item with ~~strikethrough~~ text in a long enough sentence that the item wraps and the strikethrough styling is visible on the first line while the bullet stays pinned to the top +- Item with ***bold italic*** combined emphasis inside a sentence that continues long enough to wrap, giving us a combined formatting case to inspect for alignment +- **Bold label:** a longer description following the bold label, intentionally verbose so the item wraps and we can see the bold marker sit correctly at the top of the block + +### Nested Bullets with Inline Formatting + +- **Phase 1:** Initial setup — this phase covers installing all required runtimes, verifying the environment, and ensuring every developer on the team can reproduce the build locally without additional configuration steps + - Install `bun` runtime by following the official installation guide; confirm the version matches the one pinned in `.tool-versions` or the project README before proceeding + - Run `bun install` in the monorepo root to pull all workspace dependencies in a single pass; this may take a minute on a cold cache but subsequent runs will be fast + - Verify with `bun --version` that the installed version is correct and matches CI; mismatches here have historically caused subtle build failures that are hard to diagnose +- **Phase 2:** Configuration — set all required environment variables and confirm that the local server starts cleanly before attempting any integration or end-to-end tests + - Set `PLANNOTATOR_PORT=19432` to match the port expected by the VS Code extension and the remote tunnel configuration used in devcontainer environments + - Export `PLANNOTATOR_REMOTE=1` when running inside a devcontainer or over SSH; without this flag the server will attempt to open a browser on the remote host which will silently fail + - Confirm the SSH tunnel is *active* and forwarding the correct port before running any test that depends on the browser opening automatically on the local machine +- **Phase 3:** Deployment — build all targets in the correct order, verify the output, and notify the team before tagging the release so there is time to catch any last-minute issues + - Run `bun run build` from the monorepo root, which executes the review build first and then the hook build in the correct sequence to avoid stale HTML being copied into the dist folder + - ~~Upload to S3~~ (replaced by CDN push via GitHub Actions on merge to main; manual uploads are no longer part of the release process and should not be performed directly) + - Notify the team via **Slack** in the `#releases` channel with the version number, a link to the changelog, and a brief summary of what changed so reviewers know what to look for From e91c7b1055780e78578e1eebb803bbc71646db7d Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 9 Apr 2026 15:20:16 -0700 Subject: [PATCH 2/6] fix: improve inline markdown rendering - Add strikethrough support (~~text~~) - Fix ***bold italic*** by checking triple-asterisk before double - Nudge checkbox icons down 3px to align with text baseline For provenance purposes, this commit was AI assisted. --- packages/ui/components/ListMarker.tsx | 4 ++-- packages/ui/components/Viewer.tsx | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/ui/components/ListMarker.tsx b/packages/ui/components/ListMarker.tsx index 056dd774..f092f62b 100644 --- a/packages/ui/components/ListMarker.tsx +++ b/packages/ui/components/ListMarker.tsx @@ -57,11 +57,11 @@ export const ListMarker: React.FC = ({ )} {isCheckbox ? ( checked ? ( - + ) : ( - + ) diff --git a/packages/ui/components/Viewer.tsx b/packages/ui/components/Viewer.tsx index 34d51368..eaf42e6c 100644 --- a/packages/ui/components/Viewer.tsx +++ b/packages/ui/components/Viewer.tsx @@ -739,8 +739,26 @@ const InlineMarkdown: React.FC<{ text: string; onOpenLinkedDoc?: (path: string) let previousChar = ''; while (remaining.length > 0) { + // Strikethrough: ~~text~~ + let match = remaining.match(/^~~([\s\S]+?)~~/); + if (match) { + parts.push(); + remaining = remaining.slice(match[0].length); + previousChar = match[0][match[0].length - 1] || previousChar; + continue; + } + + // Bold + italic: ***text*** + match = remaining.match(/^\*\*\*([\s\S]+?)\*\*\*/); + if (match) { + parts.push(); + remaining = remaining.slice(match[0].length); + previousChar = match[0][match[0].length - 1] || previousChar; + continue; + } + // Bold: **text** ([\s\S]+? allows matching across hard line breaks) - let match = remaining.match(/^\*\*([\s\S]+?)\*\*/); + match = remaining.match(/^\*\*([\s\S]+?)\*\*/); if (match) { parts.push(); remaining = remaining.slice(match[0].length); @@ -906,7 +924,7 @@ const InlineMarkdown: React.FC<{ text: string; onOpenLinkedDoc?: (path: string) } // Find next special character or consume one regular character - const nextSpecial = remaining.slice(1).search(/[\*_`\[!]/); + const nextSpecial = remaining.slice(1).search(/[\*_`\[!~]/); if (nextSpecial === -1) { parts.push(remaining); previousChar = remaining[remaining.length - 1] || previousChar; From 824634ee331ba63ba00287292aa1a336e44d3ec3 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 9 Apr 2026 15:28:33 -0700 Subject: [PATCH 3/6] fix: add backslash escaping to inline markdown renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add \* \_ \` \[ \~ \\ escape sequences — consume backslash and emit literal character - Add \ to nextSpecial scanner so the escape handler fires correctly mid-text For provenance purposes, this commit was AI assisted. --- packages/ui/components/Viewer.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/ui/components/Viewer.tsx b/packages/ui/components/Viewer.tsx index eaf42e6c..fcb2dbbd 100644 --- a/packages/ui/components/Viewer.tsx +++ b/packages/ui/components/Viewer.tsx @@ -739,8 +739,17 @@ const InlineMarkdown: React.FC<{ text: string; onOpenLinkedDoc?: (path: string) let previousChar = ''; while (remaining.length > 0) { + // Backslash escaping: \* \_ \` \[ \~ etc. — emit literal char, hide backslash + let match = remaining.match(/^\\([*_`\[\]~!\\])/); + if (match) { + parts.push(match[1]); + remaining = remaining.slice(2); + previousChar = match[1]; + continue; + } + // Strikethrough: ~~text~~ - let match = remaining.match(/^~~([\s\S]+?)~~/); + match = remaining.match(/^~~([\s\S]+?)~~/); if (match) { parts.push(); remaining = remaining.slice(match[0].length); @@ -924,7 +933,7 @@ const InlineMarkdown: React.FC<{ text: string; onOpenLinkedDoc?: (path: string) } // Find next special character or consume one regular character - const nextSpecial = remaining.slice(1).search(/[\*_`\[!~]/); + const nextSpecial = remaining.slice(1).search(/[\*_`\[!~\\]/); if (nextSpecial === -1) { parts.push(remaining); previousChar = remaining[remaining.length - 1] || previousChar; From de7a0215fafc2e5d4f9012b5e221292722072f59 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 9 Apr 2026 15:29:18 -0700 Subject: [PATCH 4/6] fix: add autolink support to inline markdown renderer - Handle as clickable external links - Handle as mailto links - Add < to nextSpecial scanner so autolinks are detected mid-text For provenance purposes, this commit was AI assisted. --- packages/ui/components/Viewer.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/ui/components/Viewer.tsx b/packages/ui/components/Viewer.tsx index fcb2dbbd..147e2001 100644 --- a/packages/ui/components/Viewer.tsx +++ b/packages/ui/components/Viewer.tsx @@ -748,6 +748,24 @@ const InlineMarkdown: React.FC<{ text: string; onOpenLinkedDoc?: (path: string) continue; } + // Autolinks: or + match = remaining.match(/^<(https?:\/\/[^>]+)>/); + if (match) { + const url = match[1]; + parts.push({url}); + remaining = remaining.slice(match[0].length); + previousChar = '>'; + continue; + } + match = remaining.match(/^<([^@>\s]+@[^>\s]+)>/); + if (match) { + const email = match[1]; + parts.push({email}); + remaining = remaining.slice(match[0].length); + previousChar = '>'; + continue; + } + // Strikethrough: ~~text~~ match = remaining.match(/^~~([\s\S]+?)~~/); if (match) { @@ -933,7 +951,7 @@ const InlineMarkdown: React.FC<{ text: string; onOpenLinkedDoc?: (path: string) } // Find next special character or consume one regular character - const nextSpecial = remaining.slice(1).search(/[\*_`\[!~\\]/); + const nextSpecial = remaining.slice(1).search(/[\*_`\[!~\\<]/); if (nextSpecial === -1) { parts.push(remaining); previousChar = remaining[remaining.length - 1] || previousChar; From 9256352a12a049003a565c62d0f2763a1093e59c Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 9 Apr 2026 15:29:53 -0700 Subject: [PATCH 5/6] chore: remove HTML entities from test fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not a supported feature — matches Obsidian behavior. For provenance purposes, this commit was AI assisted. --- tests/test-fixtures/10-inline-gaps-and-bullets.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test-fixtures/10-inline-gaps-and-bullets.md b/tests/test-fixtures/10-inline-gaps-and-bullets.md index a3fec142..be7afc38 100644 --- a/tests/test-fixtures/10-inline-gaps-and-bullets.md +++ b/tests/test-fixtures/10-inline-gaps-and-bullets.md @@ -34,18 +34,6 @@ Visit for more info. Send mail to . -### HTML Entities - -Trademark symbol: ™ - -Copyright: © 2024 - -Non-breaking space between these words. - -Ampersand: AT&T - -Less-than sign: 2 < 3 - ### Link Reference Definitions Here is a [link to the docs][docs] and another [link to the repo][repo]. From 17b464fea95abb51a42b4fd82e5b8dab75beee94 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Thu, 9 Apr 2026 16:41:20 -0700 Subject: [PATCH 6/6] fix: prevent empty DocBadges wrapper in sticky header row layout In row layout, check hasPreviousVersion && planDiffStats (what PlanDiffBadge actually needs) instead of onPlanDiffToggle (always truthy). Prevents an empty wrapper div that created phantom width via gap-x-3 in the sticky bar. For provenance purposes, this commit was AI assisted. --- packages/ui/components/DocBadges.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ui/components/DocBadges.tsx b/packages/ui/components/DocBadges.tsx index c55ada70..0aa99dcb 100644 --- a/packages/ui/components/DocBadges.tsx +++ b/packages/ui/components/DocBadges.tsx @@ -37,12 +37,16 @@ export const DocBadges: React.FC = ({ archiveInfo, linkedDocInfo, }) => { - const anything = - repoInfo || hasPreviousVersion || showDemoBadge || linkedDocInfo || archiveInfo; - if (!anything) return null; - const isRow = layout === 'row'; + // In row layout, only PlanDiffBadge (when it has stats to show) and + // archiveInfo actually render — everything else is hidden. Check what + // will truly produce visible output to avoid an empty wrapper div. + const anything = isRow + ? (!linkedDocInfo && ((hasPreviousVersion && planDiffStats) || archiveInfo)) + : repoInfo || hasPreviousVersion || showDemoBadge || linkedDocInfo || archiveInfo; + if (!anything) return null; + // Row layout: single horizontal line. Column layout: stacked rows. const outerClass = isRow ? 'flex flex-row items-center gap-1.5 text-[9px] text-muted-foreground/70 font-mono'