From e24f75a1cb961ebb338aa1f7bc4c427577830831 Mon Sep 17 00:00:00 2001
From: Julien Calixte
Date: Mon, 15 Jun 2026 00:01:55 +0200
Subject: [PATCH 1/4] feat(todo): show archived tasks from done.txt in a
collapsible section
---
src/views/TodoNotes.vue | 74 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 74 insertions(+)
diff --git a/src/views/TodoNotes.vue b/src/views/TodoNotes.vue
index 9aca62d..1123977 100644
--- a/src/views/TodoNotes.vue
+++ b/src/views/TodoNotes.vue
@@ -23,6 +23,7 @@ type Prop = {
}
const TODO_PATH = "todo.txt"
+const DONE_PATH = "done.txt"
const FluxNote = defineAsyncComponent(() => import("@/components/FluxNote.vue"))
@@ -58,6 +59,34 @@ watch(
{ immediate: true }
)
+const doneFile = computed(() => store.files.find((f) => f.path === DONE_PATH))
+const doneSha = computed(() => doneFile.value?.sha ?? "")
+const donePath = computed(() => doneFile.value?.path ?? DONE_PATH)
+
+// Reuse useTodoTxtCommit as a read-only loader. We never call its `mutate`,
+// so nothing is ever written to done.txt from this view — the file is
+// produced by an external tool.
+const { items: doneItems, syncContent: syncDoneContent } = useTodoTxtCommit({
+ user: props.user,
+ repo: props.repo,
+ path: donePath,
+ initialContent: "",
+ initialSha: doneSha,
+ debounceMs: 1000
+})
+
+watch(
+ doneSha,
+ async (newSha) => {
+ if (!newSha) return
+ const base64 = await queryFileContent(props.user, props.repo, newSha)
+ if (base64) {
+ syncDoneContent(decodeBase64ToUTF8(base64), newSha)
+ }
+ },
+ { immediate: true }
+)
+
// Rank `A` = 65 .. `Z` = 90; absent priority is Infinity so it sorts last
// regardless of locale collation quirks.
const priorityRank = (p?: string): number =>
@@ -75,6 +104,10 @@ const taskEntries = computed(() => {
return out
})
+const archivedTasks = computed(() =>
+ doneItems.value.filter((line): line is Task => !isBlank(line))
+)
+
const allProjects = computed(() => {
const set = new Set()
items.value.forEach((line) => {
@@ -431,6 +464,15 @@ const createTodoFile = async () => {
>
Your todo list is empty. Add a task above.
+
+
+ Archived ({{ archivedTasks.length }})
+
+
@@ -531,5 +573,37 @@ const createTodoFile = async () => {
.todo-filter-chip {
cursor: pointer;
}
+
+ .todo-archive {
+ margin-top: 1rem;
+
+ > summary {
+ cursor: pointer;
+ opacity: 0.7;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ list-style: none;
+ }
+
+ > summary::-webkit-details-marker {
+ display: none;
+ }
+ }
+
+ .todo-archive-list {
+ list-style: none;
+ padding-left: 0;
+ margin: 0.5rem 0 0;
+ display: flex;
+ flex-direction: column;
+
+ li {
+ list-style: none;
+ opacity: 0.7;
+ padding-bottom: 0.25rem;
+ }
+ }
}
From 826f7a4ec9c3b5ed5ae11fdd2b7f17b5b4a9574b Mon Sep 17 00:00:00 2001
From: Julien Calixte
Date: Thu, 18 Jun 2026 00:19:38 +0200
Subject: [PATCH 2/4] fix(svg): cover full viewBox with export background
The white background rect was anchored at 0,0, but a diagram's viewBox
origin can be negative (content drawn above y=0, e.g. a QFD house roof),
leaving that content outside the fill. Anchor the rect to the viewBox
origin so the whole drawing sits on white in PNG/SVG exports.
---
src/utils/svgDownload.ts | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/src/utils/svgDownload.ts b/src/utils/svgDownload.ts
index 76099a7..3b7d052 100644
--- a/src/utils/svgDownload.ts
+++ b/src/utils/svgDownload.ts
@@ -71,17 +71,22 @@ const buildExportableSvgString = (svg: SVGSVGElement): string => {
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink")
const { width, height } = getSvgPixelSize(svg)
+ const viewBox = svg.viewBox?.baseVal
+ const originX = viewBox && viewBox.width > 0 ? viewBox.x : 0
+ const originY = viewBox && viewBox.height > 0 ? viewBox.y : 0
if (!clone.getAttribute("viewBox")) {
- clone.setAttribute("viewBox", `0 0 ${width} ${height}`)
+ clone.setAttribute("viewBox", `${originX} ${originY} ${width} ${height}`)
}
clone.setAttribute("width", String(width))
clone.setAttribute("height", String(height))
+ // Cover the full viewBox (its origin can be negative — e.g. a diagram whose
+ // content extends above y=0), otherwise that content falls outside the fill.
const bg = document.createElementNS(SVG_NS, "rect")
- bg.setAttribute("x", "0")
- bg.setAttribute("y", "0")
- bg.setAttribute("width", "100%")
- bg.setAttribute("height", "100%")
+ bg.setAttribute("x", String(originX))
+ bg.setAttribute("y", String(originY))
+ bg.setAttribute("width", String(width))
+ bg.setAttribute("height", String(height))
bg.setAttribute("fill", "#ffffff")
clone.insertBefore(bg, clone.firstChild)
From 0f19482cf2aa0c44f3f5626625830c68b41f7946 Mon Sep 17 00:00:00 2001
From: Julien Calixte
Date: Thu, 18 Jun 2026 00:19:51 +0200
Subject: [PATCH 3/4] feat: fullscreen image lightbox on image/diagram click
Click a note image or a mermaid/tikz diagram to view it fullscreen over
a blurred dim backdrop, with a fade/scale animation and rounded white
frame. Works on desktop and mobile; diagram SVGs are serialized to a
data URL via svgToDataUrl. Closes on click or Escape.
---
src/App.vue | 2 +
src/components/ImageLightbox.vue | 54 +++++++++++++++++++++++
src/hooks/useImageLightbox.hook.ts | 19 ++++++++
src/styles/app.css | 69 ++++++++++++++++++++++++++++++
src/utils/svgDownload.ts | 3 ++
5 files changed, 147 insertions(+)
create mode 100644 src/components/ImageLightbox.vue
create mode 100644 src/hooks/useImageLightbox.hook.ts
diff --git a/src/App.vue b/src/App.vue
index 9d7363a..9b54610 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,4 +1,5 @@
+
+
+
+
diff --git a/src/hooks/useImageLightbox.hook.ts b/src/hooks/useImageLightbox.hook.ts
new file mode 100644
index 0000000..edf2ed8
--- /dev/null
+++ b/src/hooks/useImageLightbox.hook.ts
@@ -0,0 +1,19 @@
+import { ref } from "vue"
+
+const isOpen = ref(false)
+const src = ref("")
+const alt = ref("")
+
+export const useImageLightbox = () => {
+ const open = (imageSrc: string, imageAlt = "") => {
+ src.value = imageSrc
+ alt.value = imageAlt
+ isOpen.value = true
+ }
+
+ const close = () => {
+ isOpen.value = false
+ }
+
+ return { isOpen, src, alt, open, close }
+}
diff --git a/src/styles/app.css b/src/styles/app.css
index 657c9ab..058bd4c 100644
--- a/src/styles/app.css
+++ b/src/styles/app.css
@@ -344,3 +344,72 @@ iframe {
padding-bottom: 0.2rem;
padding-inline-start: 0.2rem;
}
+
+/* Image lightbox — desktop click-to-fullscreen */
+
+/* Affordance: zoomable content images and diagrams (desktop only). */
+@media (min-width: 769px) {
+ .note-content img,
+ .note-display img,
+ .tikz svg,
+ .mermaid svg {
+ cursor: zoom-in;
+ }
+}
+
+/* DaisyUI hides the native ::backdrop and uses the .modal element's own
+ background-color for the dim, so the blurred overlay lives on the element. */
+.image-lightbox {
+ background-color: rgb(0 0 0 / 0);
+ -webkit-backdrop-filter: blur(0);
+ backdrop-filter: blur(0);
+ transition:
+ visibility 0.3s allow-discrete,
+ background-color 0.3s ease-out,
+ -webkit-backdrop-filter 0.3s ease-out,
+ backdrop-filter 0.3s ease-out;
+}
+
+.image-lightbox[open] {
+ background-color: rgb(0 0 0 / 0.7);
+ -webkit-backdrop-filter: blur(8px);
+ backdrop-filter: blur(8px);
+}
+
+@starting-style {
+ .image-lightbox[open] {
+ background-color: rgb(0 0 0 / 0);
+ -webkit-backdrop-filter: blur(0);
+ backdrop-filter: blur(0);
+ }
+}
+
+.image-lightbox img {
+ transition:
+ opacity 0.3s ease-out,
+ scale 0.3s ease-out;
+}
+
+.image-lightbox[open] img {
+ opacity: 1;
+ scale: 1;
+}
+
+.image-lightbox:not([open]) img {
+ opacity: 0;
+ scale: 0.95;
+}
+
+@starting-style {
+ .image-lightbox[open] img {
+ opacity: 0;
+ scale: 0.95;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .image-lightbox,
+ .image-lightbox img {
+ transition: none;
+ }
+}
diff --git a/src/utils/svgDownload.ts b/src/utils/svgDownload.ts
index 3b7d052..fe48ab0 100644
--- a/src/utils/svgDownload.ts
+++ b/src/utils/svgDownload.ts
@@ -101,6 +101,9 @@ const buildExportableSvgString = (svg: SVGSVGElement): string => {
return new XMLSerializer().serializeToString(clone)
}
+export const svgToDataUrl = (svg: SVGSVGElement): string =>
+ `data:image/svg+xml;charset=utf-8,${encodeURIComponent(buildExportableSvgString(svg))}`
+
const triggerBlobDownload = (blob: Blob, filename: string): void => {
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
From 7575e34f8b634f8fcef39807e5874ab7f565480f Mon Sep 17 00:00:00 2001
From: Julien Calixte
Date: Thu, 18 Jun 2026 00:23:38 +0200
Subject: [PATCH 4/4] style: inset lightbox image with a small dim margin
Cap the fullscreen image at 96vh/96vw so a thin dim margin frames the
white image instead of bleeding to the viewport edges.
---
src/components/ImageLightbox.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/ImageLightbox.vue b/src/components/ImageLightbox.vue
index 644de05..bd63792 100644
--- a/src/components/ImageLightbox.vue
+++ b/src/components/ImageLightbox.vue
@@ -48,7 +48,7 @@ watch(isOpen, (value) => {
v-if="src"
:src="src"
:alt="alt"
- class="max-h-[100vh] max-w-[100vw] rounded-lg bg-white p-3 cursor-zoom-out"
+ class="max-h-[96vh] max-w-[96vw] rounded-lg bg-white p-3 cursor-zoom-out"
/>