Files
inkwell/frontend/src/components/MarkdownText.vue
T
bvandeusen 3cab054684
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 9s
CI & Build / integration (push) Successful in 25s
CI & Build / Build & push image (push) Successful in 41s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m25s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m55s
Desktop (Tauri) / Update manifest (push) Successful in 4s
web: task lines render as checkboxes where they sit in the note
M304 step 5. The server already returns a derived `items` array, so the web kept
working across the last two commits — but it was rendering every list TWICE: once
as literal `- [ ] milk` bullets inside the body, and again as the separate
NoteChecklist block underneath. This is the commit that makes the body the only
place a checklist appears.

markdown.ts gains a `task` block, matched BEFORE the plain bullet — which would
otherwise swallow the marker and leave the brackets showing, the same ordering
reason `code` is matched before emphasis in INLINE_RE. Each item carries its
ordinal across the WHOLE document, because that is what an item's id means
everywhere else now; counting per block would have made the second list's
checkboxes toggle the first list's items.

The card's preview clamp is why that ordinal is safe there: it only ever drops
lines from the end, so a visible item's index is the same whether or not the body
was truncated.

MarkdownText emits a toggle rather than reaching for the store. Ticking a box
rewrites a line of someone's note, and a renderer used in several places should not
be the thing deciding that is allowed — the card passes `toggleable` and wires it,
a read-only render does not and the boxes are inert. Not wrapped in a <label>
either: on a card the text is the note's own words and clicking it opens the note,
so only the box toggles.

In the editor, the toolbar button stops revealing a section and inserts `- [ ] ` at
the caret. That makes it the one toolbar action needing no persisted note to hang
anything off — ensureDraft is gone from it, and it works on an empty compose box
the moment it opens. Enter on a task line continues the list, and on an EMPTY one
clears the marker; without that second half a list would be impossible to get out
of. Indent and bullet are carried over rather than normalised, because continuing
someone's `*` list with a `-` is an edit they did not ask for.

NoteChecklist.vue is deleted (rule 22). The store's item methods stay: they are the
repository seam the REST routes and Tauri commands both implement, not the old path.

CI cannot check any of this beyond types — there are no frontend tests, only
vue-tsc. It wants a real browser pass.
2026-08-24 08:10:20 -04:00

64 lines
3.1 KiB
Vue

<script setup lang="ts">
import { computed } from "vue";
import { parseMarkdown } from "../notes/markdown";
import MarkdownInline from "./MarkdownInline.vue";
const props = defineProps<{ text: string; toggleable?: boolean }>();
// Ticking a box rewrites a line of the note's body, which is a thing only the owner
// of that note can do — so this renders the checkbox and hands the intent up rather
// than reaching for the store itself. The card wires it; a read-only render does not
// pass `toggleable` and the boxes are inert.
const emit = defineEmits<{ toggle: [index: number, checked: boolean] }>();
const blocks = computed(() => parseMarkdown(props.text));
</script>
<template>
<div class="space-y-1.5 break-words">
<template v-for="(b, i) in blocks" :key="i">
<h3 v-if="b.type === 'h1'" class="text-base font-bold"><MarkdownInline :tokens="b.inline ?? []" /></h3>
<h4 v-else-if="b.type === 'h2'" class="text-sm font-bold"><MarkdownInline :tokens="b.inline ?? []" /></h4>
<h5 v-else-if="b.type === 'h3'" class="text-sm font-semibold"><MarkdownInline :tokens="b.inline ?? []" /></h5>
<blockquote
v-else-if="b.type === 'quote'"
class="whitespace-pre-wrap border-l-2 border-neutral-300 pl-2 text-neutral-600 dark:border-neutral-600 dark:text-neutral-400"
>
<MarkdownInline :tokens="b.inline ?? []" />
</blockquote>
<div v-else-if="b.type === 'task'" class="flex flex-col gap-1">
<div v-for="(it, j) in b.items ?? []" :key="j" class="flex items-start gap-2">
<!-- Not wrapped in a <label>: on a card the text is the note's own words and
clicking it opens the note, so only the box itself toggles. `.stop` for
the same reason — the card is a click target underneath. -->
<input
type="checkbox"
class="mt-0.5 h-4 w-4 shrink-0 accent-brand"
:checked="b.tasks?.[j]?.checked ?? false"
:disabled="!toggleable"
:aria-label="toggleable ? 'Toggle item' : undefined"
@click.stop
@change="emit('toggle', b.tasks?.[j]?.index ?? 0, ($event.target as HTMLInputElement).checked)"
/>
<span
class="min-w-0 flex-1"
:class="b.tasks?.[j]?.checked ? 'text-neutral-400 line-through' : ''"
>
<MarkdownInline :tokens="it" />
</span>
</div>
</div>
<ul v-else-if="b.type === 'ul'" class="list-disc space-y-0.5 pl-5">
<li v-for="(it, j) in b.items ?? []" :key="j"><MarkdownInline :tokens="it" /></li>
</ul>
<ol v-else-if="b.type === 'ol'" class="list-decimal space-y-0.5 pl-5">
<li v-for="(it, j) in b.items ?? []" :key="j"><MarkdownInline :tokens="it" /></li>
</ol>
<pre
v-else-if="b.type === 'pre'"
class="overflow-x-auto whitespace-pre-wrap rounded-md bg-black/5 p-2 font-mono text-xs dark:bg-white/10"
>{{ b.value ?? "" }}</pre
>
<p v-else class="whitespace-pre-wrap"><MarkdownInline :tokens="b.inline ?? []" /></p>
</template>
</div>
</template>