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