From 7b888775f100c2bb0ebe3d10a4e21de015fce302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ademir=20Jos=C3=A9=20Ferreira=20J=C3=BAnior?= Date: Sun, 20 Sep 2026 23:04:48 -0300 Subject: [PATCH] Keep rendered images visible --- CHANGELOG.md | 1 + docs/specification.md | 1 + e2e/desktop/run.ts | 25 ++++++++ e2e/desktop/specs/rendered-images.spec.ts | 63 +++++++++++++++++++ e2e/desktop/support/runContext.ts | 3 + .../editor/components/milkdown-editor.css | 8 +-- 6 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 e2e/desktop/specs/rendered-images.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7105a500..2763a208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Leafdown uses lightweight [Keep a Changelog](https://keepachangelog.com/en/1.1.0 ### Changed +- Keep local SVG images visible in rendered Markdown, and give tiny or transparent images a non-collapsing editor surface so they can still be selected without enlarging their authored pixels. - Rebuild the welcome screen as a launcher. `New document` joins `Open file` and `Open folder`, so the fastest way to start writing is no longer the least visible one, and each of the three names its keyboard shortcut the way the menus do. A recent item is listed by its own name with the folder holding it beside it, where the row used to be the full path cut off at the end, which is where the file name is. The recent lists stay away entirely until there is a recent item to list. `Clear recent items` is a small action under the lists it clears instead of a full-size button in the row beside `Open file`. - Show the article navigator only while a folder context is open. Without one it had nothing to list and stood beside the welcome screen repeating what that screen already said, so the document surface now spans the workspace instead. `Toggle sidebar` is unavailable until a folder opens, in the View menu and in the titlebar alike, and leaves the sidebar visibility setting as it found it, so a sidebar hidden before one folder closed is still hidden when the next one opens. - Refine how the editor draws blocks. A block quote's accent bar is half its former width. A task list checkbox stands as an unfilled outline with a stronger edge until it is checked, then carries the same filled box and tick as the checkboxes elsewhere in the interface, and a checked item's own text is muted. Table cells are separated by single rules that stop at the table's own edge rather than boxing each cell, cells have more room around their text, and rows no longer alternate their shading. A fenced block's language sits in a badge in the block's corner instead of floating as bare text, and the block itself no longer carries an inner shadow. The badge beside a heading that names its level is filled and lettered like one too, where it was an outline holding the page's own colour. diff --git a/docs/specification.md b/docs/specification.md index 526b463f..1e5d221f 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -372,6 +372,7 @@ Confirmations, warnings, and security blocks affect editor rendering only; sourc - Remote image Markdown is preserved, but network images are currently blocked completely; loading them is deferred. - Local images that resolve outside the current folder context require explicit confirmation before rendering. Instead of a blocking modal, the editor displays an inline placeholder in place of the image, prompting the user to click to load/render it. - Selecting a rendered or placeholder image exposes the raw image Markdown for editing the description and target path. +- A rendered image keeps its intrinsic aspect ratio and stays within the document width. Its editor surface never collapses, so a tiny or transparent image keeps its authored pixels while remaining reliably selectable with the usual selected-node treatment. - An image description keeps the Markdown it was written with, including emphasis, strong, inline code, strikethrough, a link, and a nested image. It is source the image carries rather than content the document holds, so a nested image is written back as it was authored rather than rendered as a second image, and the image is named by the text its description spells. - The raw image Markdown of a focused image is the source the file holds. Editing the destination or the title leaves the description as it was written; editing the description replaces it with the text typed there, which is written back escaped. diff --git a/e2e/desktop/run.ts b/e2e/desktop/run.ts index f82b910a..9a649ff5 100644 --- a/e2e/desktop/run.ts +++ b/e2e/desktop/run.ts @@ -132,6 +132,9 @@ const main = async () => { const temporaryRoot = await mkdtemp(path.join(tmpdir(), "leafdown-desktop-e2e-")); const documentPath = path.join(temporaryRoot, "document-lifecycle.md"); + const imagesPath = path.join(temporaryRoot, "rendered-images.md"); + const leafImagePath = path.join(temporaryRoot, "leaf.svg"); + const tinyImagePath = path.join(temporaryRoot, "tiny-transparent.svg"); const folderPath = path.join(temporaryRoot, "folder-context"); const initialFolderFileName = "readme.md"; const initialFolderFilePath = path.join(folderPath, initialFolderFileName); @@ -146,6 +149,9 @@ const main = async () => { savedMarkdown: `${savedMarker}\n`, savedMarker, }, + images: { + path: imagesPath, + }, folder: { addedFileName: addedFolderFileName, addedFilePath: addedFolderFilePath, @@ -169,12 +175,31 @@ const main = async () => { path.join(repositoryRoot, "e2e", "desktop", "fixtures", "folder-context", "readme.md"), initialFolderFilePath, ); + await copyFile(path.join(repositoryRoot, "corpus", "assets", "leaf.svg"), leafImagePath); + await writeFile( + tinyImagePath, + '', + ); + await writeFile( + imagesPath, + [ + "![Missing SVG](./missing.svg)", + "", + "![Visible SVG](./leaf.svg)", + "", + "[![Linked SVG](./leaf.svg)](https://example.com)", + "", + "![Tiny transparent SVG](./tiny-transparent.svg)", + "", + ].join("\n"), + ); await writeJson(contextPath, context); const scenarios: Scenario[] = [ { name: "diagnostics" }, { name: "document-lifecycle", recentFiles: [documentPath] }, { name: "folder-watcher", recentFolders: [folderPath] }, + { name: "rendered-images", recentFiles: [imagesPath] }, { name: "missing-document-error", recentFiles: [missingDocumentPath] }, { name: "persistence-write", recentFolders: [folderPath] }, { name: "persistence-restart", continues: "persistence-write" }, diff --git a/e2e/desktop/specs/rendered-images.spec.ts b/e2e/desktop/specs/rendered-images.spec.ts new file mode 100644 index 00000000..72f95c24 --- /dev/null +++ b/e2e/desktop/specs/rendered-images.spec.ts @@ -0,0 +1,63 @@ +import { $, expect } from "@wdio/globals"; + +import { getDesktopE2ERunContext } from "../support/runContext.js"; +import { openRecentPath } from "../support/ui.js"; + +interface ElementBox { + height: number; + width: number; +} + +const getBox = async (element: ReturnType): Promise => { + const result = await element.execute((node) => { + const { height, width } = node.getBoundingClientRect(); + + return { height, width }; + }); + + return result as ElementBox; +}; + +describe("desktop rendered images", () => { + it("keeps decoded SVGs visible and tiny images on an editor surface", async () => { + const { images } = await getDesktopE2ERunContext(); + + await openRecentPath(images.path); + + const visibleImage = $('img[alt="Visible SVG"]'); + const linkedImage = $('img[alt="Linked SVG"]'); + const tinyImage = $('img[alt="Tiny transparent SVG"]'); + const missingPlaceholder = $(".leafdown-image-placeholder"); + + await expect(visibleImage).toBeDisplayed(); + await expect(linkedImage).toBeDisplayed(); + await expect(tinyImage).toBeDisplayed(); + await expect(missingPlaceholder).toBeDisplayed(); + + const [visibleBox, linkedBox, tinyBox] = await Promise.all([ + getBox(visibleImage), + getBox(linkedImage), + getBox(tinyImage), + ]); + + expect(visibleBox.width).toBeGreaterThan(0); + expect(visibleBox.height).toBeGreaterThan(0); + expect(linkedBox.width).toBeGreaterThan(0); + expect(linkedBox.height).toBeGreaterThan(0); + expect(tinyBox.width).toBeGreaterThan(0); + expect(tinyBox.height).toBeGreaterThan(0); + expect(tinyBox.width).toBeLessThanOrEqual(3); + expect(tinyBox.height).toBeLessThanOrEqual(3); + + const tinyImageView = tinyImage.$(".."); + await expect(tinyImageView).toHaveElementClass("leafdown-image-view"); + const [selectionSurface, placeholderBox] = await Promise.all([ + getBox(tinyImageView), + getBox(missingPlaceholder), + ]); + + expect(selectionSurface.width).toBeGreaterThanOrEqual(32); + expect(selectionSurface.height).toBe(28); + expect(placeholderBox.height).toBe(28); + }); +}); diff --git a/e2e/desktop/support/runContext.ts b/e2e/desktop/support/runContext.ts index 8921bb86..6b471280 100644 --- a/e2e/desktop/support/runContext.ts +++ b/e2e/desktop/support/runContext.ts @@ -7,6 +7,9 @@ export interface DesktopE2ERunContext { savedMarkdown: string; savedMarker: string; }; + images: { + path: string; + }; folder: { addedFileName: string; addedFilePath: string; diff --git a/src/features/editor/components/milkdown-editor.css b/src/features/editor/components/milkdown-editor.css index 02ec7fd0..3357fcdc 100644 --- a/src/features/editor/components/milkdown-editor.css +++ b/src/features/editor/components/milkdown-editor.css @@ -296,11 +296,11 @@ } img { - @apply my-5 max-w-full rounded-md border border-border bg-card; + @apply my-5 max-w-full rounded-md bg-card; } .leafdown-image-view { - @apply my-5 inline-flex max-w-full flex-col gap-2 align-middle; + @apply my-5 block min-h-7 max-w-full rounded-md; } .leafdown-markdown-image { @@ -308,7 +308,7 @@ } .leafdown-image-markdown-input { - @apply w-full min-w-64 rounded-sm border border-border bg-muted px-2 py-1 font-mono text-sm text-foreground outline-none; + @apply mb-2 w-full min-w-64 rounded-sm border border-border bg-muted px-2 py-1 font-mono text-sm text-foreground outline-none; &:focus-visible { @apply border-primary ring-2 ring-primary/25; @@ -316,7 +316,7 @@ } .leafdown-image-placeholder { - @apply inline-flex max-w-full flex-wrap items-center gap-2 rounded-md border border-dashed border-border bg-card/70 px-3 py-2 text-sm text-muted-foreground; + @apply inline-flex max-w-full flex-wrap items-center gap-2 rounded-md border border-dashed border-border bg-card/70 px-3 py-[3px] text-sm text-muted-foreground; } .leafdown-image-placeholder__message {