From 72dee337b70b8825e8a6f660c3ed6e2e7cbf5186 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sat, 13 Jun 2026 21:23:19 +0200 Subject: [PATCH] feat(todo): switch /todo view to todo.txt format Replace the markdown _todo/todo.md view with a structured todo.txt UI at the repo root: priority dropdown, inline body edit, project/context/due/rec chips with Tabler icons, add input as a join-suffix, project/context filter columns with TransitionGroup animation, priority-sorted list. Drops the now-unused .todo-notes markdown checkbox styles from app.css. --- src/components/TodoTxtItem.vue | 470 +++++++++++++++++++++++++++++ src/hooks/useTodoTxtCommit.hook.ts | 87 ++++++ src/styles/app.css | 9 - src/views/TodoNotes.vue | 420 ++++++++++++++++++++++---- 4 files changed, 920 insertions(+), 66 deletions(-) create mode 100644 src/components/TodoTxtItem.vue create mode 100644 src/hooks/useTodoTxtCommit.hook.ts diff --git a/src/components/TodoTxtItem.vue b/src/components/TodoTxtItem.vue new file mode 100644 index 0000000..b93e646 --- /dev/null +++ b/src/components/TodoTxtItem.vue @@ -0,0 +1,470 @@ + + + + + diff --git a/src/hooks/useTodoTxtCommit.hook.ts b/src/hooks/useTodoTxtCommit.hook.ts new file mode 100644 index 0000000..a7f2697 --- /dev/null +++ b/src/hooks/useTodoTxtCommit.hook.ts @@ -0,0 +1,87 @@ +import { useDebounceFn } from "@vueuse/core" +import { onUnmounted, Ref, ref, toValue } from "vue" + +import { useGitHubContent } from "@/hooks/useGitHubContent.hook" +import { FileLine, parseFile, serializeFile } from "@/utils/todotxt" + +export const useTodoTxtCommit = ({ + user, + repo, + path, + initialContent, + initialSha, + debounceMs = 1000 +}: { + user: string + repo: string + path: Ref | string | undefined + initialContent: Ref | string + initialSha: Ref | string + debounceMs?: number +}) => { + const { updateFile } = useGitHubContent({ user, repo }) + + const items = ref(parseFile(toValue(initialContent))) + const currentSha = ref(toValue(initialSha)) + const isCommitting = ref(false) + const hasPendingChanges = ref(false) + + // Re-seed from a freshly fetched file. We must not blow away in-flight edits. + const syncContent = (content: string, sha: string) => { + if (!hasPendingChanges.value) { + items.value = parseFile(content) + currentSha.value = sha + } + } + + const commitChanges = async () => { + const pathValue = toValue(path) + if (!pathValue || !hasPendingChanges.value) { + return + } + + if (isCommitting.value) { + debouncedCommit() + return + } + + isCommitting.value = true + + const { sha: newSha } = await updateFile({ + content: serializeFile(items.value), + path: pathValue, + sha: currentSha.value + }) + + if (newSha) { + currentSha.value = newSha + hasPendingChanges.value = false + } + + isCommitting.value = false + } + + const debouncedCommit = useDebounceFn(commitChanges, debounceMs) + + const mutate = (mutator: (current: FileLine[]) => FileLine[]) => { + items.value = mutator(items.value) + hasPendingChanges.value = true + debouncedCommit() + } + + onUnmounted(() => { + // Flush any pending edit on unmount so navigation doesn't lose work. + if (hasPendingChanges.value) { + commitChanges() + } + }) + + return { + items, + currentSha, + isCommitting, + hasPendingChanges, + syncContent, + mutate + } +} diff --git a/src/styles/app.css b/src/styles/app.css index 34d2399..657c9ab 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -336,15 +336,6 @@ iframe { height: 400px; } -/* TODO page */ -.todo-notes input[type="checkbox"] { - @apply checkbox checkbox-success; -} - -.todo-notes li:has(> input[type="checkbox"]) { - list-style: none; -} - .tabs :where(pre):not(:where([class~="not-prose"], [class~="not-prose"] *)) { margin-top: 0; margin-bottom: 0; diff --git a/src/views/TodoNotes.vue b/src/views/TodoNotes.vue index 27c1ab3..da74d0d 100644 --- a/src/views/TodoNotes.vue +++ b/src/views/TodoNotes.vue @@ -1,80 +1,180 @@ -