From 3d8ba1b890521b612d8c0ac5a3b994d632fb29ce Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 13:15:18 +0000 Subject: [PATCH 1/4] Render Markdown in the webapp's editable list-item rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A list-item row now shows its Markdown rendered until it holds the caret, and its source for exactly as long as it does. Closes the gap where a card read **Milk** and the editor read `**Milk**`, which looked like a rendering bug rather than a deliberate limit. Focused and editing are the same state. That is the decision the rest follows from: every keystroke handler stays on a real textarea, because a row with the caret *is* one — no render-mode duplicate of Tab, Enter or the suggestion arrows, and no focusable non-interactive element for axe to flag. Four things that fall out of it: - The textarea is never unmounted, only moved out of flow at opacity-0, so NoteModal's imperative focus paths keep working through itemInputRefs and the field comes back at a height measured while it was on screen. - A row only swaps when rendering changes something. `buy milk` renders to itself, so a list with no Markdown behaves exactly as it did before. - An editable row's links are inert and a read-only row's are live: a row that owns a caret does not own a link. - Completed rows render, accepting the ~~strike~~ / line-through collision that the spec already accepted for display surfaces. A click maps back to a source offset rather than landing at 0: nodes now carry source spans and inlineSourceOffset walks them, both in shared/ so mobile can reuse them. Both forms share one class list for layout, including the inline-block/overflow pairing that keeps a span's baseline where a textarea's is — without it every row silently lost 7px when unfocused. The grip prevents its default mousedown so a drag never collapses the row dnd-kit just measured. Mobile's editable row is unchanged; #867 tracks it and records these decisions so only the keyboard-churn question is left open there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QuVeVda1EiJrJrdzsybRAr --- docs/specs/markdown-rendering.md | 95 ++++++- mobile/__tests__/ListItem.test.tsx | 2 +- mobile/src/components/ListItem.tsx | 4 +- mobile/src/utils/inlineMarkdown.ts | 2 +- shared/src/__tests__/inlineMarkdown.test.ts | 119 +++++++++ shared/src/inlineMarkdown.ts | 240 ++++++++++++++++-- webapp/e2e/tests/keyboard-focus.spec.ts | 65 +++++ webapp/e2e/tests/markdown.spec.ts | 129 +++++++++- webapp/src/components/SortableItem.tsx | 155 ++++++++++- .../__tests__/SortableItem.test.tsx | 112 +++++++- .../src/utils/__tests__/inlineCaret.test.ts | 66 +++++ webapp/src/utils/inlineCaret.ts | 95 +++++++ webapp/src/utils/markdown.ts | 45 +++- 13 files changed, 1076 insertions(+), 53 deletions(-) create mode 100644 webapp/src/utils/__tests__/inlineCaret.test.ts create mode 100644 webapp/src/utils/inlineCaret.ts diff --git a/docs/specs/markdown-rendering.md b/docs/specs/markdown-rendering.md index 7ea8ea4c6..ecfa81ff8 100644 --- a/docs/specs/markdown-rendering.md +++ b/docs/specs/markdown-rendering.md @@ -11,11 +11,12 @@ Markdown applies to the **`content` of text notes** in full, and to **list-note item text** in an inline-only subset (§2.1): - **Note titles are plain.** They are rendered as text everywhere. -- **List-item Markdown renders on display surfaces only.** Note cards, mobile's - read-only editor and the collapsed-completed parent label render it; the - editable row still shows its source, because it is an always-live input with no - preview mode. Closing that gap is - [#824](https://github.com/hanzei/jot/issues/824). +- **List-item Markdown renders everywhere on the webapp, and on display surfaces + on mobile.** Note cards, mobile's read-only editor and the collapsed-completed + parent label render it on both clients. The webapp's *editable* row renders it + too, swapping to source while it holds the caret (§1.2). Mobile's editable row + still shows source; closing that gap is + [#867](https://github.com/hanzei/jot/issues/867). ### 1.1 Note cards render links as text @@ -35,15 +36,77 @@ colour that only signals "link" fails anyone who cannot use it. So the label renders exactly as the surrounding text. The open note is unaffected: the webapp's modal preview and mobile's editor both -render live links, which is where a reader who wants the link already is. One -consequence worth naming: because the *editable* list-item row shows source -(above), a link typed into a list item now has no live surface on the webapp at -all until [#824](https://github.com/hanzei/jot/issues/824) gives that row a view -mode. Mobile still has one, in its read-only editor. +render live links, which is where a reader who wants the link already is. An +*editable* list-item row is the one other place a link stays inert, for a +different reason — §1.2. Both clients render the same feature set from the same source string, so a note written on a phone reads identically in a browser and the other way round. +### 1.2 The webapp's editable row swaps between rendered and source + +A list-item row on the webapp shows its Markdown rendered until it holds the +caret, and its source for exactly as long as it does. Type `**Milk**`, move +away, and the row reads **Milk**; click back into it and it reads `**Milk**` +again, with the caret where you clicked. + +**Focused and editing are the same state.** That is the decision the rest +follows from. Every keystroke a row handles — Tab to indent, Enter to split, +arrows to move between rows and through the completed-item suggestions — stays +on a real `