Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
25 changes: 25 additions & 0 deletions e2e/desktop/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -146,6 +149,9 @@ const main = async () => {
savedMarkdown: `${savedMarker}\n`,
savedMarker,
},
images: {
path: imagesPath,
},
folder: {
addedFileName: addedFolderFileName,
addedFilePath: addedFolderFilePath,
Expand All @@ -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,
'<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" viewBox="0 0 1 1"><rect width="1" height="1" fill="transparent" /></svg>',
);
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" },
Expand Down
63 changes: 63 additions & 0 deletions e2e/desktop/specs/rendered-images.spec.ts
Original file line number Diff line number Diff line change
@@ -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<typeof $>): Promise<ElementBox> => {
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);
});
});
3 changes: 3 additions & 0 deletions e2e/desktop/support/runContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export interface DesktopE2ERunContext {
savedMarkdown: string;
savedMarker: string;
};
images: {
path: string;
};
folder: {
addedFileName: string;
addedFilePath: string;
Expand Down
8 changes: 4 additions & 4 deletions src/features/editor/components/milkdown-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -296,27 +296,27 @@
}

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 {
@apply my-0 block;
}

.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;
}
}

.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 {
Expand Down