feat: interactive checkboxes in list note viewer

- renderMarkdown() accepts interactiveCheckboxes option: removes disabled=""
  and stamps data-task-index on each checkbox in the marked HTML output
- NoteViewerView detects list notes by body content (- [ ] / - [x] pattern)
  and passes interactiveCheckboxes: true when rendering
- onBodyChange() handles checkbox change events: toggles the matching line
  in the body, optimistically updates the store, then PATCHes the note
- prose.css adds .prose--checklist rules for marked output: no bullet,
  flex row, accent-color, line-through on checked items via :has()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 09:31:37 -04:00
parent aec7a910f0
commit 749a60b9fd
3 changed files with 84 additions and 5 deletions
+15 -3
View File
@@ -38,7 +38,7 @@ const headingRenderer = {
marked.use({ renderer: headingRenderer });
const PURIFY_OPTS_FULL = {
ADD_ATTR: ["data-tag", "data-title"],
ADD_ATTR: ["data-tag", "data-title", "data-task-index"],
FORCE_BODY: true,
};
@@ -47,10 +47,22 @@ const PURIFY_OPTS_PREVIEW = {
FORCE_BODY: true,
};
export function renderMarkdown(text: string): string {
export function renderMarkdown(text: string, { interactiveCheckboxes = false } = {}): string {
const stripped = stripFirstLineTags(text);
const decoded = decodeEntities(stripped);
const html = marked(decoded) as string;
let html = marked(decoded) as string;
if (interactiveCheckboxes) {
// marked renders task-list checkboxes as <input ... disabled="" ...>.
// Remove disabled and stamp a sequential data-task-index so the viewer
// can identify which item was toggled regardless of attribute order.
html = html.replace(/ disabled=""/g, "");
let taskIdx = 0;
html = html.replace(/<input ([^>]*type="checkbox"[^>]*)>/g, (_m, attrs) => {
return `<input data-task-index="${taskIdx++}" ${attrs}>`;
});
}
const withTags = linkifyTags(html);
const withLinks = linkifyWikilinks(withTags);
const sanitized = DOMPurify.sanitize(withLinks, PURIFY_OPTS_FULL);