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" />