diff --git a/CHANGELOG.md b/CHANGELOG.md index e47304b0..c835940e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Leafdown uses lightweight [Keep a Changelog](https://keepachangelog.com/en/1.1.0 ### Changed +- Rewrite the bundled help guides with numbered window illustrations, task-by-task instructions, menu paths and shortcuts, and Markdown examples pairing source with its rendered result. The guides and illustrations work offline and follow the current appearance. - Redesign the help pages, keyboard shortcuts, What's new, and Changelog as one family. Each opens larger, taking most of the window's height and as much width as its text needs, with its title in a header and its text at 15 px. A help page with several sections, and the Changelog, list them beside the text in a wide window to jump between. What's new alternates its highlights' drawings from side to side, starts each change with its first sentence in bold, and folds a release's fixes behind a count you can open. The Changelog lays out every release the same way, with its fixes listed, and the shortcuts sit in cards side by side. - Show shortcuts and key hints as raised keycaps across menus, the command palette, the welcome screen, the shortcuts reference, help, and release notes. Keycaps name the platform's keys: Ctrl on Windows and Linux, and modifier symbols on macOS, with full names for screen readers. - Write an inserted horizontal rule with the run the document's breaks agree on, including its length and internal spacing. Where the breaks disagree or there are none, use `***`. Typed markers keep their spelling, and a run that would be read as another construct still uses a safe fallback. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 82436627..f91d4e57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,16 @@ A release's section may name a few highlights, which What's new shows first, eac If a proposed implementation would change product direction, architecture, or release scope, reach agreement in an issue before substantial implementation. Update the owning documentation as part of the accepted change. +### Bundled Help Authoring + +Keep user guides as Markdown under `docs/help/`. Lead with a task, explain the result of each action, and write menu paths in bold with shortcuts in inline code. Use `Mod` for the platform's primary modifier. A command without a default shortcut should not acquire an invented one. Link to another guide with its filename, or to a section on the current page with its GitHub heading fragment. + +For a source/result card, use a `text` code fence with exactly `example=commonmark`, `example=gfm`, or `example=leafdown` after the language. Use a longer outer fence when the example contains fences. The `text` language keeps formatters from rewriting extension syntax as standard Markdown. GitHub shows ordinary source; the app adds a syntax-family badge and a read-only rendering using the document editor. Put a description of the result and any menu path or shortcut in surrounding prose. Results are visual illustrations, with inert controls, no document or folder context, and no remote image loading. The source and explanation remain available to assistive technology. Keep examples small; do not rely on an actual local file, interactive control, or network resource for their meaning. + +Put line drawings in `docs/help/illustrations/` and embed them as `![Description](illustrations/name.svg)`. Only these bundled SVGs render as images in Help; other image destinations show their alt text without loading. Use the empty-state drawing style and `--art-*`, `--primary`, `--primary-foreground`, `--card`, or `--foreground` colors with light-theme fallbacks. SVGs are trusted repository assets: do not include scripts, external references, or event handlers. Give informative drawings alt text and explain numbered areas in the prose; use empty alt text for purely decorative drawings. + +Use an ordinary blockquote beginning with **Tip:** for advice. A list whose items each start with a link to a bundled guide becomes navigation cards in the app and remains a readable list on GitHub. Preview changes in light and dark appearances at narrow and wide sizes. + ### AI-Assisted Contributions AI assistance is allowed, but the human author remains responsible for understanding, reviewing, verifying, and explaining the contribution and for addressing review feedback. Do not submit generated changes that you cannot evaluate or maintain. diff --git a/docs/architecture.md b/docs/architecture.md index 62a141c0..aae2fde6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -32,6 +32,8 @@ Application composition lives under `src/components/` in `layout/` and `screens/ The `release-notes` feature owns the changelog bundled into the frontend, the highlight illustrations bundled with it, current-version selection, and persisted launch and seen-version history. It renders notes with the `help` feature's bundled-page Markdown renderer, so both Help surfaces share one link policy, and it does not access document or session state. +The `help` feature bundles guide Markdown and trusted SVG illustrations. Example code fences request the `editor` feature's read-only Markdown preview, created when the example enters view and destroyed when it leaves the page. Each preview has an isolated editor with no document path, folder context, or session bridge; its controls are inert and subsequent content and selection transactions are rejected. The authored source and surrounding explanation provide the accessible equivalent of the visual result. Only illustrations imported from `docs/help/illustrations/` are inserted as SVG; arbitrary Markdown image destinations remain text and cannot trigger a fetch. + The `session` feature owns the window's open documents and their relationship to the folder context, plus workflows spanning multiple features. Dependencies flow left to right: `application components -> commands -> session -> domain features -> shared UI/lib` diff --git a/docs/help/file-and-folder-workflows.md b/docs/help/file-and-folder-workflows.md index 516ef7b1..5152bef2 100644 --- a/docs/help/file-and-folder-workflows.md +++ b/docs/help/file-and-folder-workflows.md @@ -1,65 +1,115 @@ # File and folder workflows -Leafdown keeps your open documents and the folder context separate. The folder context pins the article navigator to one folder until you open or close another folder. +Browse notes without losing your place, keep the documents you need, and search or export your work. Your open documents and the navigator's folder are separate: opening a file elsewhere does not replace an already open folder. ## Open and revisit -**File > Open** opens one Markdown file: `.md`, `.markdown`, `.mdown`, or `.mkd`. If no folder is open, Leafdown also opens its parent folder as context. If a folder is already pinned, opening a file elsewhere leaves that context in place. **File > Open folder** scans the folder for Markdown articles and opens a matching root-level `readme` or `index` file when present, keeping the documents you already have open. Ignored directories do not appear in the navigator. +- **Open one document:** **File > Open...** (`Mod+O`) accepts `.md`, `.markdown`, `.mdown`, and `.mkd`. With no folder open, its parent becomes the navigator's folder. +- **Browse a folder:** **File > Open folder...** (`Mod+Shift+O`) scans for Markdown articles. It opens a matching root-level index file, such as `readme` or `index`, if present. Ignored directories are skipped. +- **Start a note:** **File > New** (`Mod+N`) creates an untitled tab. No file is written until you save. +- **Revisit a path:** Choose it on the welcome screen or under **File > Open recent**. Recent lists are recorded when **Record recent files and folders** is on. Removing an entry or clearing the lists does not delete files. + +On Windows, Leafdown is available in File Explorer's **Open with** menu and **Settings > Apps > Default apps** after installation. Installing does not change your default app. Opening an associated file starts Leafdown or brings it forward; starting Leafdown again without a file opens a new window. ## Work with tabs -Every document you open gets a tab above the document, after the one you are on, and **File > New** adds an untitled tab. Opening a file that is already open switches to its tab, so opening never closes or replaces a document. Each tab keeps its own unsaved edits, undo history, place in the document, and search. +Opening a file through Open, recent items, a drop, or the operating system gives it an ordinary tab after the active one. Opening an already open file switches to its tab. -Choose a tab to switch to it. `Mod+Tab` and `Mod+Shift+Tab`, or `Mod+PageDown` and `Mod+PageUp`, move to the next and previous tab, and `Mod+1` to `Mod+8` jump to a tab by position, `Mod+9` to the last one. A dot on a tab marks unsaved changes. **File > Close document** (`Mod+W`), a tab's close button, or a middle click closes it, asking first if it has unsaved changes. Closing the window or the folder asks once about every tab with unsaved changes. +Choosing an article in the navigator, a folder-search result, a link, or a folder's index opens a **preview tab**, named in italics. Choosing another such document can reuse that tab. Editing, saving, double-clicking the tab or navigator row, or **View > Keep tab** (`Mod+Shift+K`) makes it an ordinary tab. Unsaved work is never replaced by a preview. -A newly opened document starts at the top. Following a heading link or choosing a search result instead brings its target into view. +![An italic preview tab beside a kept tab; choosing another article reuses only the preview.](illustrations/tabs.svg) -## Work in more than one window +Each tab keeps its unsaved edits, undo history, position, and search. A dot marks unsaved changes. Use one action at a time: + +- **Next tab:** **View > Next tab**, `Mod+Tab` or `Mod+PageDown`. +- **Previous tab:** **View > Previous tab**, `Mod+Shift+Tab` or `Mod+PageUp`. +- **Jump by position:** `Mod+1` through `Mod+8`; `Mod+9` selects the last tab. These commands are also in the command palette, not a menu. +- **Close the active tab:** **File > Close document**, `Mod+W`. Its close button and a middle click work too. Unsaved changes prompt before the tab closes. + +Newly opened documents begin at the top. Heading links and search results bring their target into view instead. -**File > New window** (`Mod+Shift+N`) opens another Leafdown window, to keep two documents side by side or two folders open at once. Each window has its own tabs, folder, navigator, folder search, and zoom, while settings and recent items are shared and change in every window at once. A file is open in only one window at a time: opening a file that another window already has switches to its tab there and brings that window forward. +To compare documents side by side, see [Work in more than one window](#work-in-more-than-one-window). -Renaming a file or folder in the navigator moves its tabs in every window, keeping their unsaved edits. Deleting one closes its tabs in the window you deleted it from; other windows keep its tabs open with their text and show the file as missing. **Replace in folder** leaves alone a file that another window has with unsaved edits, and says so. +## Work in more than one window + +1. Choose **File > New window** (`Mod+Shift+N`). +2. Open another folder or document there. Tabs, folder, navigator, folder search, and zoom belong to each window. Settings and recent items are shared. +3. Arrange the windows side by side with your operating system. -**File > Close window** (`Alt+F4`) or the window's close button asks only about that window's unsaved edits, and closing the last window quits Leafdown. **File > Quit** (`Mod+Q`) closes every window in turn. With **Restore session** on, as it is by default, quitting keeps every window's tabs and unsaved edits without asking, and the next launch reopens them, warning you about any file that changed or went missing in the meantime. With it off, a window with unsaved edits comes to the front to ask about them, and canceling there stops quitting with that window and those not yet asked still open. The first window reopens where you left it; other windows open offset from the one you opened them from. +A file is open in only one window at a time. Opening it again brings its existing tab and window forward. A file opened from the operating system otherwise goes to the Leafdown window you used last. -On Windows, installing Leafdown adds it to File Explorer's **Open with** menu for these files and to **Settings > Apps > Default apps**, without changing which app opens them by default. Choose Leafdown there to open Markdown files from File Explorer. Files associated with Leafdown show a folded-page icon with the green leaf and Markdown symbol. If Leafdown is not running, it starts with the file, using the file's folder as context. If it is, the file opens as a new tab in the Leafdown window you used last, which comes to the front; a file that is already open switches to its tab and window instead. Starting Leafdown again without a file opens a new window. **File > Open** works either way. +Renaming a file or folder moves its open tabs in every window and preserves edits. Deleting closes its tabs in the deleting window; other windows keep their text and mark the file missing. Folder replacement skips a file with unsaved edits in another window and reports it. -The welcome screen and **File > Open recent** show previously opened files and folders when **Record recent files and folders** is on. You can remove one recent item or clear both lists without deleting files. See [Settings reference](settings-reference.md) for the setting. +**File > Close window** (`Alt+F4`) closes one window. **File > Quit** (`Mod+Q`) closes them all. With **Restore session** on, quitting or closing the last window keeps the windows, tabs, and unsaved edits for next launch without asking, unless saving that session fails. Closing a window while others remain open asks about its unsaved changes as usual. With restoration off, unsaved changes prompt before closing. Canceling a quit stops there, leaving that window and those not yet asked open. ## Work in the article navigator -Choose a Markdown file row to open it. A row's context menu can create a file or folder, rename or delete an entry, reveal its location in the system file manager, or copy its absolute or folder-relative path. Empty navigator space offers actions for the folder root. New files open in a new tab. Deleting asks for confirmation and moves an entry to the system Recycle Bin or Trash; Leafdown does not permanently delete it on its own. Deleting an open file closes its tab. Renaming an open file keeps its unsaved edits and updates its recent path. Both reach tabs in other windows too, as [Work in more than one window](#work-in-more-than-one-window) describes. Links in other files are not rewritten. +Choose a Markdown row to preview it. Expand folders to reach nested notes. Keyboard focus moves with the arrow keys; `Enter` or `Space` opens the focused article. **View > Collapse all folders** and **Expand all folders** change the tree at once. + +Right-click a row, or press `Shift+F10` on it, to open its context menu: + +- **New file / New folder:** Create beside an article or inside a folder. A new file opens in an ordinary tab. +- **Rename:** Change the name while keeping open edits. Other files' links are not rewritten. +- **Delete:** Confirm before moving an entry to the system Recycle Bin or Trash. Leafdown does not permanently delete it on its own. +- **Open file location / Open folder location:** Reveal it in the system file manager. +- **Copy path / Copy relative path:** Copy an absolute path or one relative to the open folder. -The navigator lists Markdown articles and the folders that hold them, along with empty folders so a folder you just made stays in view. Turn on **View > Show all files** to list the folder's other files too, such as images and PDFs, dimmed and marked with an icon for their type. Leafdown can only open Markdown files, so choosing one of them does nothing, and its context menu does not rename or delete it. With that setting on, a folder holding only such files can be expanded, while an empty folder has no arrow either way. +Right-click empty navigator space for actions at the folder root. These context-menu actions have no default shortcuts. + +**View > Show all files** includes images, PDFs, and other unsupported files as dimmed rows. They cannot be opened, renamed, or deleted here. Empty folders remain visible, including one you just created. **File > Reveal in sidebar** locates the active document when it belongs to this folder. ## Search the folder -**Edit > Find and replace > Find in folder** (`Mod+Shift+F`) turns the sidebar into a search of the open folder's Markdown files, the same files the navigator opens. Type to search; matching works as **Find** does inside a document, with **Match case** and **Whole word**, and every open document is searched as you have it, unsaved edits included. Matches are grouped by file, with the text around each one. Choose a match, or move to it with the arrow keys and press `Enter`, to open its file in a tab and highlight the match while you stay in the results. Files that are too large or not valid UTF-8 or UTF-16 are skipped and listed. `Escape` returns to the navigator and to the document, with the match selected. +1. Choose **Edit > Find and replace > Find in folder...** (`Mod+Shift+F`). +2. Type your query. **Match case** and **Whole word** narrow matches. Search covers the folder's Markdown articles and includes open documents' unsaved edits. +3. Choose a result to open its file and highlight the match. The arrow keys move between result rows; `Enter` opens a match. +4. Press `Escape` to return to the navigator and document, with the match selected. + +Files too large to search or not valid UTF-8 or UTF-16 are skipped and listed. To search only the active note, use **Edit > Find and replace > Find...** (`Mod+F`). ## Replace across the folder -**Edit > Find and replace > Replace in folder** (`Mod+Shift+H`), or the arrow before the search field, shows a **Replace with** field. While it shows, the view previews every change instead of searching: each match is shown struck through with its replacement beside it, across every file, however many there are. Nothing is written until you choose **Replace all**, which appears once the preview is complete; changing the search, the replacement, or an option first changes nothing. +1. Choose **Edit > Find and replace > Replace in folder...** (`Mod+Shift+H`). +2. Enter the search text and **Replace with** text. The preview strikes out each match and shows its replacement beside it. +3. Review the complete preview. Changing a field or option only updates this preview; it writes nothing. +4. Choose **Replace all** once the preview is complete. If a previewed open document has unsaved edits, save or discard them when asked; the preview refreshes before writing. -**Replace all** writes each file in turn and then reports what it did. Only the lines holding a replacement change; a file marked with a warning sign is written as **Save** would write it, which can also change other lines, such as the padding of a table. If open documents with unsaved changes are part of the preview, you are asked to save or discard each of them first, and the preview updates before anything is written. A file that changed since the preview is left alone and listed, as is any file that cannot be written, and the other files are still replaced. +Leafdown writes files in turn and reports results. A file changed since the preview, one that cannot be written, or one edited in another window is skipped and listed; other files can still be replaced. Normally only lines with replacements change. A warning-marked file is written as Save would write it and can also change formatting elsewhere, such as table padding. -A replacement cannot be undone across files. In an open document, **Undo** undoes it for that document only; other files keep their new text, so commit or back up your notes first if you may want them back. +> **Tip:** Back up or commit your notes before a replacement you may want to reverse. Undo affects only an open document; it cannot undo the whole folder operation. ## Drop a file or folder -Drop one supported Markdown file or folder onto the app to open it. A preview shows the configured action while dragging. In **Preferences > Files**, each drop type can instead insert a link into the active document. A saved document receives a relative link when the paths share a filesystem root; an untitled document receives an absolute link. Multiple or unsupported items are not opened. +Drop one supported Markdown file or folder onto Leafdown. The drag preview tells you what will happen. Multiple or unsupported items are not opened. + +Under **File > Preferences... > Files**, choose whether each drop type opens the item or inserts a link. A saved document gets a relative link when the paths share a filesystem root; an untitled one gets an absolute link. ## Links and images -Relative links and image paths start from the saved document's location. An untitled document cannot resolve them until it is saved. Local Markdown links open in Leafdown without replacing the pinned folder. Rest the pointer on one, or press `Mod+Alt+P` with the caret in it, to preview the start of the linked file, or the section its heading fragment names, without opening it. A local non-Markdown link asks before opening with the system default app. Web links open in the system browser. +Relative paths begin at the saved document's location. For example, `notes/today.md` can link to a sibling with `[Tomorrow](tomorrow.md)` and to an image one folder up with `![Seedling](../images/seedling.png)`. Save an untitled note before expecting relative paths to resolve. + +Local Markdown links open in Leafdown and keep the navigator's folder pinned. Hover a link, or press `Mod+Alt+P` with the caret in it, to preview its target or named heading. Non-Markdown local links ask before opening with the system default app. Web links open in your browser. -Local images render when available. A remote `https:` image remains a placeholder until you choose **Load image**; merely opening a document does not request it. Images outside the folder context may ask for approval before rendering. See [Markdown reference](markdown-reference.md) for link syntax. +Local images render when available. Remote HTTPS images remain placeholders until **Load image** is chosen; opening a document does not request them. Images outside the folder may need approval. See [Markdown reference](markdown-reference.md) for syntax. ## Export as HTML -**File > Export > Export as HTML...** writes the open document, including unsaved edits, as a single HTML page you can read in any browser without Leafdown or a network connection. Local images, math, diagrams, and highlighted code are embedded in the page. A remote image is included only if you loaded it in the document first, and an image outside the folder only if you loaded it there. Anything left out is listed when the export finishes. Exporting does not save or change your Markdown file. +1. Open the document you want to share; unsaved edits are included. +2. Choose **File > Export > Export as HTML...** (no default shortcut). +3. Choose the output location, then review any reported omissions. + +The result is one HTML page that works in a browser offline. Local images, math, diagrams, and highlighted code are embedded. A remote or outside-folder image is included only if you loaded it in the document first. Exporting does not save or change the Markdown file. ## Save and external changes -Saving keeps the blank lines a file ends with, though the document shows no empty lines for them. They stay at the end as you edit, and selecting everything and then typing or deleting removes them with the rest of the text. +Use **File > Save** (`Mod+S`) for the current file, or **File > Save as...** (`Mod+Shift+S`) for another path. Save as cannot overwrite a file open in another tab and does not switch the navigator's folder. + +Leafdown watches every open file, including those outside the pinned folder: + +- **Clean document changed on disk:** It reloads. +- **Edited document changed on disk:** Your edits remain and a warning appears. Saving over the external change asks first. +- **File removed:** The document remains available; Save offers Save as. + +Saving retains the file's trailing blank lines, though they are not displayed as empty document lines. Selecting everything and replacing or deleting it removes those lines too. -**File > Save as** writes the document to a chosen path. Saving outside the current folder does not switch that folder, and a file open in another tab cannot be saved over. Leafdown watches every open file: a clean document reloads after another program changes it, while an edited document keeps its changes and warns. Saving over an externally changed file asks first. If the file has disappeared, Save offers Save as. These checks also apply to a file opened outside the pinned folder. +Continue with [Settings reference](settings-reference.md) for restoration, folder scanning, and file defaults. diff --git a/docs/help/getting-started.md b/docs/help/getting-started.md index a7382714..c499229c 100644 --- a/docs/help/getting-started.md +++ b/docs/help/getting-started.md @@ -1,23 +1,62 @@ # Getting started -Leafdown edits ordinary Markdown files on your computer. Your work stays in the document you choose; opening Help does not change it. +Open a folder of notes, choose an article, and make your first edit. Leafdown works with ordinary Markdown files on your computer. Help leaves your document and unsaved work in place. -## Start a document +## Find your way around -Choose **File > New** to write an untitled document. Nothing is created on disk until you save it. Choose **File > Open** to edit an existing `.md` or `.markdown` file, or **File > Open folder** to browse a folder of Markdown files. Opening a folder also opens its first matching root-level index file, if one exists; otherwise choose an article in the navigator. +![Leafdown window with six numbered areas, described below.](illustrations/window.svg) -You can open an individual file while a folder is already open. The folder remains pinned in the navigator, even if that file is elsewhere. +1. **Menus:** File opens and saves notes; Edit, Insert, and Format help you write. View changes the workspace, and Help opens these guides. +2. **Article navigator:** Browse the open folder and expand its subfolders. The folder stays here even if you open a document elsewhere. +3. **Document tabs:** Switch between open notes. An italic name marks a reusable preview; a dot marks unsaved changes. +4. **Document:** Write and read formatted Markdown. Moving into an element can reveal its source for editing. +5. **Outline:** Point at the heading dots on the right to see the document's headings, then choose one to jump there. +6. **Status bar:** Check unsaved state, word count, reading time, encoding, and line ending. -## Find and edit articles +## Your first session -The article navigator lists the Markdown articles in the open folder and the folders that hold them. Choose an article to open it. **View > Show all files** lists the folder's other files too, such as images, dimmed because Leafdown cannot open them. Expand folders to reach nested articles, and use **View > Collapse all folders** or **Expand all folders** to change the tree at once. The sidebar toggle hides the navigator without closing its folder. +### 1. Open your notes -Click in the document and type. Leafdown shows Markdown as formatted text while keeping its source in the file. Formatting and insertion commands are in **Insert** and **Format**; **Edit** contains Undo, Redo, and Find and replace. See [Markdown reference](markdown-reference.md) for supported source forms. +Choose **File > Open folder...** (`Mod+Shift+O`) and select a folder. Leafdown lists its Markdown articles and opens a root-level `readme` or `index` when one matches your configured index names. -## Save and close +For a single file, use **File > Open...** (`Mod+O`). Leafdown accepts `.md`, `.markdown`, `.mdown`, and `.mkd`. If no folder is open, that file's parent becomes the navigator's folder. -Choose **File > Save** to write changes. Saving an untitled document opens **Save as**, where you choose its filename and location. **File > Save as** also writes an existing document to a new path. A file saved outside the open folder does not replace that pinned folder context. +### 2. Choose an article -Opening a document never closes another: each opens in its own tab. When you close a tab, the folder, or a window with unsaved changes, Leafdown asks before discarding them, and canceling keeps everything open. Quitting Leafdown keeps your windows, tabs, and unsaved changes for the next time it starts instead of asking, unless you turn off **Restore session** in Preferences > Files. Your files change only when you save. If another program has changed or removed the file, Leafdown warns before a save can overwrite it or offers Save as for a missing file. +Click an article in the navigator, or use the arrow keys and press `Enter`. This opens it in the preview tab; choosing another article can replace that preview. Double-click the article or choose **View > Keep tab** (`Mod+Shift+K`) to keep it open. Editing or saving it keeps it too. -Continue with [File and folder workflows](file-and-folder-workflows.md) or [Settings reference](settings-reference.md). +To start a fresh note instead, choose **File > New** (`Mod+N`). It stays untitled until you save it. Hide or show the navigator with **View > Toggle sidebar** (`Mod+Shift+E`). + +### 3. Write something + +Click the document and type. Try a heading and a short paragraph: + +```text example=commonmark +# Today's notes + +One small thing worth remembering. +``` + +Select text and choose **Format > Strong** (`Mod+B`) or **Format > Emphasis** (`Mod+I`). Turn a paragraph into a heading with **Format > Heading > Heading 1** (`Mod+Alt+1`). **Edit > Undo** (`Mod+Z`) reverses an edit. + +> **Tip:** Use **View > Command palette...** (`Mod+Shift+P`) to find a command by its name or menu path. In these guides, Mod means Ctrl on Windows and Linux, or Command on macOS. + +### 4. Save your work + +Choose **File > Save** (`Mod+S`). For an untitled note, choose a name and location in Save as. To write an existing note to another path, choose **File > Save as...** (`Mod+Shift+S`). Saving outside the open folder leaves the navigator's folder in place. + +The tab's dot disappears when its changes have been saved. Session restoration keeps unsaved work between launches, but your Markdown file changes only when you save it. + +## Leave and come back + +Close one tab with **File > Close document** (`Mod+W`). Leafdown asks before discarding that tab's unsaved changes. Closing a folder or a window while other Leafdown windows remain open also asks about unsaved work. Closing the last window keeps its session instead when restoration is on. + +**File > Quit** (`Mod+Q`) keeps your windows, tabs, and unsaved edits for the next launch when **Preferences > Files > Restore session** is on, as it is by default. Turn it off to have quitting ask about unsaved changes instead. + +If another program changed a file, Leafdown warns before overwriting it. If the file went missing, Save offers Save as. + +## Choose your next step + +- [Markdown reference](markdown-reference.md) — Compare what you type with what Leafdown displays, from headings to diagrams. +- [File and folder workflows](file-and-folder-workflows.md) — Keep tabs, search a folder, follow links, and export your work. +- [Settings reference](settings-reference.md) — Set up your workspace, file defaults, and reading appearance. diff --git a/docs/help/illustrations/preferences.svg b/docs/help/illustrations/preferences.svg new file mode 100644 index 00000000..ce5d41eb --- /dev/null +++ b/docs/help/illustrations/preferences.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/help/illustrations/tabs.svg b/docs/help/illustrations/tabs.svg new file mode 100644 index 00000000..13a2bdd0 --- /dev/null +++ b/docs/help/illustrations/tabs.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/help/illustrations/window.svg b/docs/help/illustrations/window.svg new file mode 100644 index 00000000..fb91e7be --- /dev/null +++ b/docs/help/illustrations/window.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + 123456 + + diff --git a/docs/help/markdown-reference.md b/docs/help/markdown-reference.md index ac87fb33..1fc78940 100644 --- a/docs/help/markdown-reference.md +++ b/docs/help/markdown-reference.md @@ -1,56 +1,163 @@ # Markdown reference -Leafdown opens `.md` and `.markdown` files and saves ordinary Markdown. Type these forms in a document or paste them as Markdown. Editing a rendered element may reveal its source markers. +Compare what you type with what Leafdown displays. Copy an example into your document using **Edit > Paste as > Markdown** to try it. The results here are read-only; editing an element in your own document can reveal its source. -## CommonMark and GitHub Flavored Markdown +Each card names its syntax family: **CommonMark**, **GFM** (GitHub Flavored Markdown), or **Leafdown extension** (additional supported syntax, not necessarily invented by Leafdown). A command with no shortcut listed has no default shortcut. Mod means Ctrl on Windows/Linux or Command on macOS. -Use headings, paragraphs, emphasis, strong text, links, images, blockquotes, lists, thematic breaks, inline code, and fenced or indented code blocks. Leafdown also supports GFM tables, task lists, strikethrough, autolinks, and footnotes. +## Headings and paragraphs -Start a bullet list by typing `- `, `+ `, or `* `, and an ordered list by typing `1. ` or `1) `. Saving keeps the marker you typed, except where two adjacent lists need different markers to stay separate. Lists created through Format use the marker your document's lists agree on, or the default where they disagree or there are none. +Use one to six `#` characters followed by a space for heading levels. A blank line starts another paragraph. Headings also appear in the document outline. -```text -# A heading +```text example=commonmark +# Garden notes -**Strong**, _emphasis_, ~~struck text~~, and `inline code`. +## This week -> A quotation with a [link](https://example.com). +Plant the seeds. -- One item -- [x] A completed task -- [ ] An open task +Keep the soil damp. +``` + +**Try it:** **Format > Heading > Heading 1–6** (`Mod+Alt+1` through `Mod+Alt+6`); **Format > Paragraph** (`Mod+Alt+0`). **Insert > Heading** and **Insert > Paragraph** add new blocks. + +A normal newline within a paragraph becomes a space. End a line with a backslash for a hard line break; two trailing spaces work too. + +```text example=commonmark +First line\ +Second line +``` + +**Try it:** Press `Shift+Enter` for a hard break while writing a paragraph. + +## Emphasis and inline code + +Use pairs of markers around the words you want to format. Backticks keep code literal, so its stars are not emphasis. + +```text example=commonmark +**Strong**, _emphasis_, and `a * b`. +``` + +**Try it:** Select text, then **Format > Strong** (`Mod+B`), **Emphasis** (`Mod+I`), or **Inline code** (`Mod+E`). **Format > Clear inline formatting** (`Mod+\`) removes supported inline formatting. + +```text example=gfm +~~Old plan~~ New plan. +``` + +**Try it:** **Format > Strikethrough** (`Mod+Alt+X`). + +Escape punctuation with a backslash to write it literally. Character references also work. + +```text example=commonmark +\*Not emphasis\* & a plain ampersand. +``` + +## Lists and tasks + +Start a bullet list with `- `, `+ `, or `* `. Start an ordered list with `1. ` or `1) `. Saving preserves your marker except where adjacent lists need different markers to remain separate. + +```text example=commonmark +- Seeds +- Water + +1. Fill the pot +2. Plant the seeds +``` -1. First -2. Second +**Try it:** **Format > Unordered list** (`Mod+Alt+8`) or **Ordered list** (`Mod+Alt+7`). Use **Format > Increase list indent** (`Tab`) or **Decrease list indent** (`Shift+Tab`) within a list. The Insert menu also creates lists. Commands use the marker your document's lists agree on, or the default if they disagree. -| Name | Value | -| ---- | ----- | -| One | Two | +```text example=gfm +- [x] Plant seeds +- [ ] Water the pots +``` + +**Try it:** **Format > Task list** (`Mod+Alt+9`), then **Format > Toggle task checked** (`Mod+Enter`), or click the checkbox in your document. **Insert > Task list** adds a new list. -![Description](images/photo.png) +## Quotes and separators -A footnote[^note] and its definition. +Prefix a paragraph with `>` to quote it. A line containing three or more matching stars, hyphens, or underscores creates a horizontal rule; surround it with blank lines to keep it separate from other blocks. -[^note]: More detail. +```text example=commonmark +> Start small and keep notes. --- + +Next topic. ``` -Use three backticks or tildes around a fenced code block. An optional language name enables highlighting when Leafdown knows it. The language badge on a block can change that name without editing its fence by hand. +**Try it:** **Format > Blockquote** (`Mod+Shift+B`), **Insert > Blockquote**, or **Insert > Horizontal rule**. -## Leafdown-supported extensions +## Links and images -These forms work in Leafdown alongside the CommonMark and GFM content above. They are not all part of standard GFM. +Write a label in square brackets followed by a destination in parentheses. An image adds `!` before the label; its label describes the image. Save your document before using relative paths. -**Wiki links** target Markdown files relative to the current saved document. A label changes the displayed text, and a heading fragment jumps to the first exact matching heading. Missing targets stay unresolved instead of creating files. +```text example=commonmark +[Example website](https://example.com) -```text -[[notes/meeting]] -[[notes/meeting#Decisions|Meeting decisions]] +![A seedling](images/seedling.png) +``` + +The sample image has no file behind it here, so it shows a placeholder. In your saved document a local image renders when available. Remote images wait for **Load image**, and images outside the folder may need approval. See [File and folder workflows](file-and-folder-workflows.md). + +**Try it:** **Insert > Link** (`Mod+K`) or **Insert > Image**. **Edit > Replace image...** changes the standalone image in context. + +Reference links share a destination through a definition. A CommonMark autolink uses angle brackets; GFM also recognizes bare web addresses. + +```text example=commonmark +[Read more][guide] + +[guide]: https://example.com "Example guide" + + +``` + +```text example=gfm +Visit https://example.com for details. ``` -**Definition lists** use Pandoc-style `:` or `~` markers with a space before the definition text. +**Try it:** Type or paste these forms. Hover a local Markdown link, or use `Mod+Alt+P` with the caret in it, to preview its document without opening it. -```text +## Tables + +Use pipes between cells and a separator row after the header. Colons in that separator set column alignment. + +```text example=gfm +| Plant | Pots | +| :---- | ---: | +| Basil | 2 | +| Mint | 1 | +``` + +**Try it:** **Insert > Table** creates a table. **Format > Table** adds, moves, or deletes rows and columns. In the document, table controls provide those actions beside the table too. + +## Code blocks + +Wrap code in three backticks or tildes. An optional language name enables highlighting when Leafdown knows it. Four leading spaces also make an indented code block, without a language badge. + +````text example=commonmark +```js +const pots = 3; +``` + + plain indented code +```` + +**Try it:** **Insert > Code block** or **Format > Code block** (`Mod+Alt+C`). Click a fenced block's language badge or use **Format > Code block language...** to change its language. Line numbers and soft wrap change presentation, never the saved code. + +## Footnotes and definitions + +Footnotes are supported in addition to CommonMark and GFM. A reference names a definition elsewhere in the document. + +```text example=leafdown +Keep a record[^note]. + +[^note]: More detail about this observation. +``` + +**Try it:** **Insert > Footnote** creates a reference and moves to its new definition. Hover a reference, or press `Mod+Alt+P` there, to preview the definition. **Edit > Jump > Jump to footnote definition** moves to it; **Edit > Rename footnote** selects its label for replacement. + +Definition lists use Pandoc-style `:` or `~` markers followed by a space. + +```text example=leafdown Term : Its definition. @@ -58,9 +165,25 @@ Another term ~ Another definition. ``` -**Math** uses dollar delimiters, or a fenced code block whose language is exactly `math`. A `$$` span alone in its paragraph is displayed as a centered block. TeX is rendered locally; it never loads an external resource. +**Try it:** Type the term and definition on successive lines; there is no definition-list command. -````text +## Wiki links + +Wiki links target Markdown files relative to the saved document. Add a heading fragment to jump to the first exact matching heading, and a pipe to change the displayed label. Missing targets stay unresolved; they do not create files. + +```text example=leafdown +[[notes/meeting]] + +[[notes/meeting#Decisions|Meeting decisions]] +``` + +**Try it:** Type `[[` to start a wiki link. These examples have no folder context, so their destinations remain unresolved here. + +## Math + +Dollar delimiters enclose TeX. A `$$` span alone in its paragraph displays as a centered block. A fenced block with the language `math` also displays a formula. Rendering is local and loads no external resource. + +````text example=leafdown Inline math: $a^2+b^2=c^2$. $$ @@ -72,46 +195,94 @@ $$ ``` ```` -**Citations** use Pandoc's bracketed form: `@` and a citation key inside square brackets, with items separated by semicolons. An item can carry a prefix before its key, a `-` before `@` to suppress the author, and a locator or other suffix after the key. Leafdown shows the group exactly as written; it does not read a bibliography or format the citation. Click a citation or arrow into it to edit its source. To keep bracketed text with an `@` as plain text, write `\@`. +**Try it:** Type dollar delimiters, or use **Insert > Code block** and choose `math` from its language badge. Moving into the formula reveals its source; invalid TeX stays visible for correction. + +## Citations -```text -As shown before [@doe2026, pp. 4-6], and later [see -@roe2024; @smith2025, chap. 2]. +Pandoc-style bracketed citations keep their authored text. Leafdown does not read a bibliography or format a reference list. Semicolons separate items; a prefix, locator, or `-` before `@` can qualify an item. -Keys with other characters use braces: [@{https://example.com/bib?id=7}, p. 3]. +```text example=leafdown +As shown before [@doe2026, pp. 4-6]. + +[see -@roe2024; @smith2025, chap. 2] + +[@{https://example.com/bib?id=7}, p. 3] ``` -**Mermaid diagrams** use a fenced code block whose first info word is `mermaid`. The diagram shows while the caret is elsewhere; click it or arrow into it to edit its syntax-highlighted code, and the diagram updates below once you pause. Wide diagrams scroll sideways at their natural size. Diagrams use Leafdown's light or dark colors, render locally, and never load an external resource. Configuration directives and frontmatter are kept but not previewed. +**Try it:** Type the bracketed form, then click or arrow into it to edit. Escape the `@` as `\@` to keep bracketed text literal instead. + +## Diagrams -````text +A fenced code block whose first info word is `mermaid` shows a diagram while the caret is elsewhere. It uses the current light or dark colors. Wide diagrams scroll horizontally. Configuration directives and frontmatter stay in the file but are not previewed. + +````text example=leafdown ```mermaid -flowchart LR +flowchart TD Draft --> Review --> Publish ``` ```` -**Callouts** support GitHub alert blocks, Material for MkDocs admonitions, and Docusaurus or VitePress colon fences. Supported types and titles depend on the dialect. **Insert > Callout**, also in the context popup, lists the types of the dialect your document's callouts share, or GitHub alert types when there are none or more than one dialect. Below a separator, **GitHub alert**, **MkDocs**, **Docusaurus**, and **VitePress** submenus offer the other dialects; the dialect listed first has no submenu of its own. Choose a type to insert an empty callout in that dialect, with no title. MkDocs starts with `!!!`, and Docusaurus and VitePress use a three-colon fence. Adding a second dialect makes later insertions list GitHub alert types first. The command palette's **Callout...** keeps offering only the inferred dialect. Choose a type from **Format > Callout**, or press the icon at the start of a callout's header, to change its type within its own dialect, and use **Format > Callout > Callout opens expanded** to choose whether a collapsible MkDocs admonition opens expanded. For example: +**Try it:** **Insert > Code block**, then choose `mermaid` from its language badge. Click the diagram to edit its highlighted source; the preview updates after you pause. Rendering is local and loads no external resource. + +## Callouts + +Callouts distinguish notes, tips, and warnings. Leafdown supports GitHub alerts, Material for MkDocs admonitions, and Docusaurus or VitePress colon fences. These are extensions beyond standard GFM; each dialect has its own types and title rules. -```text +```text example=leafdown > [!NOTE] > Keep a copy of the source file. +``` +```text example=leafdown !!! warning "Before you save" Check changes made by another program. +``` +```text example=leafdown ::: tip Keep related articles in one folder. ::: ``` -**Frontmatter** is a metadata block at the very start of a document: YAML between `---` lines, TOML between `+++` lines, or JSON between `;;;` lines. Edit it as source. Invalid syntax shows an error but never blocks saving, and the block is saved as you left it. **Insert > Frontmatter** adds an empty block. Leafdown gives its keys no special meaning. +**Try it:** **Insert > Callout** lists your document's shared dialect first, or GitHub alert types when there is none or more than one. Submenus offer the other dialects. Choose a type to insert an empty callout with no title. The command palette's **Callout...** offers only the inferred dialect. + +Change a callout within its dialect through **Format > Callout** or its header icon. For an MkDocs collapsible admonition, `???` starts closed and `???+` starts open; **Format > Callout > Callout opens expanded** switches that choice. Ordinary MkDocs insertion starts with `!!!`. + +## Frontmatter + +Frontmatter must be the first block. YAML uses `---`, TOML uses `+++`, and JSON uses `;;;` delimiters. Leafdown displays the metadata as source and gives its keys no special meaning. Invalid syntax shows an error but never blocks saving. -```text +```text example=leafdown --- title: Garden report tags: [notes, garden] --- ``` -Leafdown also displays self-contained raw HTML from a restricted set of elements when there are no attributes. Unsupported HTML remains source text. The [Specification](https://github.com/Azganoth/leafdown/blob/main/docs/specification.md) describes the exact supported elements and extension rules. +```text example=leafdown ++++ +title = "Garden report" ++++ +``` + +```text example=leafdown +;;; +{"title": "Garden report"} +;;; +``` + +**Try it:** **Insert > Frontmatter > YAML**, **TOML**, or **JSON** adds an empty block if the document has none. Edit it directly; it saves as you left it. + +## Raw HTML + +Leafdown displays a restricted set of self-contained HTML elements with no attributes. Unsupported HTML remains visible source. This safe subset is narrower than CommonMark's raw HTML syntax. + +```text example=leafdown +Water is H2O. + +
MoreA small note.
+``` + +**Try it:** Type supported HTML directly. The [Specification](https://github.com/Azganoth/leafdown/blob/main/docs/specification.md) lists the exact supported elements and extension rules. The example results in this guide are illustrations, so controls and links in them do not activate. -For where links and images resolve, see [File and folder workflows](file-and-folder-workflows.md). +Continue with [File and folder workflows](file-and-folder-workflows.md) for paths, saving, and export, or [Settings reference](settings-reference.md) to adjust document typography. diff --git a/docs/help/settings-reference.md b/docs/help/settings-reference.md index 01739660..212be98c 100644 --- a/docs/help/settings-reference.md +++ b/docs/help/settings-reference.md @@ -1,43 +1,79 @@ # Settings reference -Open **File > Preferences** to change the settings below. Changes persist across launches. **Restore defaults** returns them to their defaults. The Preferences dialog groups settings as General, Files, Editor, and Appearance. +Make Leafdown comfortable to read and choose how it opens, saves, and restores your work. Open **File > Preferences...** (`Mod+,`). Changes persist across launches and are shared by your windows. **Restore defaults** returns settings to their defaults. + +![Preferences has a section list on the left and controls on the right, with a document preview beneath typography choices.](illustrations/preferences.svg) + +Choose **General**, **Files**, **Editor**, or **Appearance** on the left, then change a control on the right. Switches turn a behavior on or off; choice controls select one value. The drawing is a guide to the controls, not a second Preferences dialog. ## General -- **Language:** Use the operating system language or a language Leafdown ships. English is currently available. -- **Record recent files and folders:** Keep separate recent lists, with up to 10 items each. Turning this off stops new entries from being recorded. -- **Sidebar visibility:** Show or hide the article navigator when a folder is open. -- **Status bar visibility:** Show or hide document status when a document is open. -- **Article sort order:** Sort the navigator by name, modified date, or type. -- **Show all files:** List files Leafdown cannot open, dimmed, in the navigator alongside articles. Off by default; **View > Show all files** switches it too. +### Arrange your workspace + +- **Sidebar visibility:** Show or hide the article navigator while a folder is open. **View > Toggle sidebar** (`Mod+Shift+E`) does the same. +- **Status bar visibility:** Show or hide document status, counts, encoding, and line ending. Also available as **View > Toggle status bar**. +- **Article sort order:** Choose name, modified date, or type. **View > Sort articles by** offers the same choices. +- **Show all files:** Include dimmed rows for files Leafdown cannot open. Off by default; **View > Show all files** switches it too. -**View > Always on top** is another persistent window setting; it keeps every Leafdown window above other windows until switched off. +Two related settings live in the workspace itself: **View > Always on top** keeps Leafdown windows above other windows, and the outline's **H1 to H6** control chooses the deepest heading level listed (H1–H3 by default). Both choices are remembered. -The **H1 to H6** control at the top of the document outline is also remembered across launches. It chooses how deep the outline lists headings, H1–H3 by default. +### Choose language and recent history + +**Language** uses the operating system language or a shipped language. English is currently available. + +**Record recent files and folders** keeps up to 10 entries in each list. Turning it off stops recording new entries; use the welcome screen to remove an existing entry or **File > Open recent > Clear recent items** to clear both lists. Your files are untouched. ## Files -- **Default extension for new documents:** Choose `.md` or `.markdown`. -- **Default line ending for new documents:** Choose LF or CRLF. The default follows your operating system. -- **Insert final newline on save:** End the last line of saved text with a newline when enabled. Blank lines at the end of a document are saved either way. -- **Restore session:** Reopen your windows, tabs, and unsaved changes the next time Leafdown starts, so quitting does not ask about them. On by default. Turn it off to have quitting ask about unsaved changes and start with the welcome screen. -- **When dropping a folder / Markdown file:** Open the item or insert a link into the active document. Each item type has its own choice. -- **Index file names:** Set the ordered names Leafdown tries when opening a folder. The defaults are `readme` and `index`. -- **Ignored directories:** Set directory names to skip during folder scans. The defaults include version-control and build directories. +### Return to unfinished work + +Leave **Restore session** on to reopen windows, tabs, and unsaved text after quitting. It is on by default. Turn it off to have quitting ask about unsaved changes and the next launch start at the welcome screen. + +> **Tip:** Restoring a session does not save your Markdown files. Use **File > Save** (`Mod+S`) when you want the file on disk to contain your changes. + +### Set defaults for new notes + +- **Default extension for new documents:** `.md` (default) or `.markdown`. Existing `.mdown` and `.mkd` files work too. +- **Default line ending for new documents:** LF or CRLF. The default follows the operating system: CRLF on Windows, LF on macOS/Linux. To change the current note, use **Edit > Line ending** or its status-bar control. +- **Insert final newline on save:** End the last content line with a newline. On by default; trailing blank lines are preserved either way. Also available under **Edit > Line ending**. + +### Choose what a folder opens + +**Index file names** is an ordered list of base names. The defaults are `readme`, then `index`: Leafdown tries them when opening a folder, so put your preferred landing page first. Use names without their Markdown extension. + +**Ignored directories** skips named directories and their contents during scans. Defaults include `.git`, `.hg`, `.svn`, `node_modules`, `target`, `dist`, `build`, and `.cache`. Matching ignores case on Windows and respects it on Unix-like systems. An ignored directory's notes do not appear in the navigator or folder search. + +### Choose what a drop does + +**When dropping a folder** and **When dropping a Markdown file** each choose between opening the item and inserting a link into the active document. Both default to opening. The drag preview shows the configured action before you drop. ## Editor -- **Auto pair brackets and quotes:** Insert the matching closing character while typing when enabled. -- **Display line numbers for code blocks:** Show visual line numbers that never enter saved text or copied content. -- **Soft wrap for code blocks:** Wrap long code lines visually without changing the Markdown. +### Reduce typing + +**Auto pair brackets and quotes** inserts a matching closing character while you type. It is on by default. Turn it off if you prefer to type both sides yourself. + +### Read code more easily + +**Display line numbers for code blocks** numbers lines beside code. Numbers never become saved or copied text. A wrapped line still has only one number. + +**Soft wrap for code blocks** wraps long lines visually instead of requiring horizontal scrolling. It does not insert newlines into your code. Both settings are off by default and can also be changed with the controls on a code block. ## Appearance -- **Accent color:** Choose Neutral, Red, Orange, Amber, Emerald, Cyan, Blue, Violet, or Fuchsia. -- **Appearance theme:** Follow the system or choose Light or Dark. -- **File icons:** Keep the navigator's file and folder icons gray, or choose Colored to give folders and each type of file a color of their own. -- **Document font:** Choose Inter, IBM Plex Sans, Atkinson Hyperlegible Next, Literata, or the system font. Code keeps its monospace face. -- **Text size:** Choose 14, 16, 18, or 20 px for document text. -- **Line spacing:** Choose Compact, Default, or Relaxed for document paragraphs, lists, and quotes. +### Choose colors + +**Appearance theme** follows System or selects Light or Dark. **View > Appearance** offers the same choices. **Accent color** selects Neutral, Red, Orange, Amber, Emerald, Cyan, Blue, Violet, or Fuchsia. + +**File icons** chooses Neutral gray icons or Colored icons with amber folders and a color for each file type. This is separate from the accent color. + +### Tune document reading + +1. Choose a **Document font**: Inter, IBM Plex Sans, Atkinson Hyperlegible Next, Literata, or System. All except System are bundled for offline use; code keeps its monospace font. +2. Choose **Text size**: 14, 16, 18, or 20 px. Headings and other document elements scale with it. +3. Choose **Line spacing**: Compact, Default, or Relaxed. It changes paragraphs, lists, and quotes; headings and code keep their own line height. +4. Read the sample beneath those controls to compare your choices before returning to the document. + +These choices affect document typography, not the interface font or saved Markdown. To enlarge the whole interface instead, use **View > Zoom in** (`Mod+=`), **Zoom out** (`Mod+-`), or **Reset zoom** (`Mod+0`). -Document typography settings affect the document surface and its preview, leaving the interface font and the file's Markdown alone. For the complete command and settings inventory, see the [repository Reference](https://github.com/Azganoth/leafdown/blob/main/docs/reference.md). +Continue with [Getting started](getting-started.md) or [File and folder workflows](file-and-folder-workflows.md). The [repository Reference](https://github.com/Azganoth/leafdown/blob/main/docs/reference.md) contains the complete settings and command inventory. diff --git a/e2e/desktop/specs/help-pages.spec.ts b/e2e/desktop/specs/help-pages.spec.ts index 2f9e3437..f91a74e1 100644 --- a/e2e/desktop/specs/help-pages.spec.ts +++ b/e2e/desktop/specs/help-pages.spec.ts @@ -1,4 +1,4 @@ -import { $, browser, expect } from "@wdio/globals"; +import { $, $$, browser, expect } from "@wdio/globals"; import { Key } from "webdriverio"; import { waitForDiagnosticRecord } from "../support/diagnostics.js"; @@ -77,12 +77,13 @@ describe("desktop Help pages", () => { throw new Error(`Help did not scroll: ${JSON.stringify(scrolling)}`); } - await $("article button").click(); + await $("article").$("button=Markdown reference").click(); await expect($("[data-slot='dialog-title']")).toHaveText("Markdown reference"); await expect($("article pre")).toBeDisplayed(); expect(await browser.getUrl()).not.toContain("github.com"); - await $("article button").click(); + await $("article").$("button=Specification").scrollIntoView(); + await $("article").$("button=Specification").click(); await waitForDiagnosticRecord( (record) => record.event === "desktopE2eOpenerSuppressed" && @@ -110,4 +111,81 @@ describe("desktop Help pages", () => { await browser.keys([Key.Ctrl, "z", Key.NULL]); await expect(editor).not.toHaveText(expect.stringContaining("Help draft")); }); + + for (const dark of [false, true]) { + for (const width of [640, 1280]) { + for (const page of [ + "Getting started", + "Markdown reference", + "File and folder workflows", + "Settings reference", + ]) { + it(`keeps ${page} illustrations and examples within the page at ${width}px in ${dark ? "dark" : "light"} appearance`, async () => { + const originallyDark = await browser.execute(() => + document.documentElement.classList.contains("dark"), + ); + try { + await browser.execute( + (enabled) => document.documentElement.classList.toggle("dark", enabled), + dark, + ); + await browser.setWindowSize(width, 800); + await openMenu("Help"); + await $(`aria/${page}`).click(); + for await (const example of $$("article [data-help-example]")) { + await example.scrollIntoView(); + await browser.waitUntil( + () => + browser.execute((element) => { + const diagram = element.querySelector( + '[data-language="mermaid"] .leafdown-code-mermaid-image', + ); + return ( + Boolean(element.querySelector('[data-ready="true"]')) && + (!diagram || + (diagram.complete && + diagram.naturalWidth > 0 && + diagram.getBoundingClientRect().height > 0)) + ); + }, example), + { timeoutMsg: `An example in ${page} did not finish rendering.` }, + ); + const dimensions = await browser.execute((element) => { + const card = element.getBoundingClientRect(); + const article = element.closest("article")!.getBoundingClientRect(); + const panels = element.querySelectorAll(":scope > div > div"); + return { + editable: Boolean(element.querySelector('[contenteditable="true"]')), + height: card.height, + left: card.left, + right: card.right, + articleLeft: article.left, + articleRight: article.right, + sourceTop: panels[0].getBoundingClientRect().top, + resultTop: panels[1].getBoundingClientRect().top, + }; + }, example); + expect(dimensions.editable).toBe(false); + expect(dimensions.height).toBeGreaterThan(0); + expect(dimensions.left).toBeGreaterThanOrEqual(dimensions.articleLeft); + expect(dimensions.right).toBeLessThanOrEqual(dimensions.articleRight + 1); + if (width === 640) expect(dimensions.resultTop).toBeGreaterThan(dimensions.sourceTop); + else expect(dimensions.resultTop).toEqual(dimensions.sourceTop); + } + const overflow = await browser.execute(() => { + const article = document.querySelector("article")!; + return article.scrollWidth - article.clientWidth; + }); + expect(overflow).toBeLessThanOrEqual(1); + } finally { + await browser.keys("Escape"); + await browser.execute( + (enabled) => document.documentElement.classList.toggle("dark", enabled), + originallyDark, + ); + } + }); + } + } + } }); diff --git a/src/features/editor/components/markdown-example-preview.css b/src/features/editor/components/markdown-example-preview.css new file mode 100644 index 00000000..82a129bf --- /dev/null +++ b/src/features/editor/components/markdown-example-preview.css @@ -0,0 +1,25 @@ +@reference "../../../app.css"; + +.leafdown-editor.leafdown-example-preview { + @apply min-h-16 p-0; + + .ProseMirror { + @apply min-h-0 max-w-none px-0; + + > p:last-child:has(> br:only-child) { + @apply hidden; + } + + pre, + pre[data-mermaid-mode] .leafdown-code-mermaid-panel:not([hidden]), + pre .leafdown-code-math-preview:not([hidden]) { + @apply overflow-visible; + } + } + + button, + .leafdown-code-controls, + .leafdown-block-gutter-layer { + @apply hidden; + } +} diff --git a/src/features/editor/components/markdown-example-preview.test.tsx b/src/features/editor/components/markdown-example-preview.test.tsx new file mode 100644 index 00000000..8e6a08c5 --- /dev/null +++ b/src/features/editor/components/markdown-example-preview.test.tsx @@ -0,0 +1,47 @@ +// @vitest-environment happy-dom + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { act, render, waitFor } from "@/test/utils/react"; + +import { MarkdownExamplePreview } from "./markdown-example-preview"; + +afterEach(() => vi.unstubAllGlobals()); + +describe("MarkdownExamplePreview", () => { + it("waits for visibility, renders document syntax read-only, and removes its view", async () => { + let reveal: () => void = () => {}; + const disconnect = vi.fn(); + vi.stubGlobal( + "IntersectionObserver", + class { + constructor(callback: (entries: { isIntersecting: boolean }[]) => void) { + reveal = () => callback([{ isIntersecting: true }]); + } + observe() {} + disconnect = disconnect; + }, + ); + const { container, unmount } = render( + [!NOTE]\n> A note." + } + />, + ); + expect(container.querySelector(".ProseMirror")).toBeNull(); + act(reveal); + await waitFor(() => expect(container.querySelector('[data-ready="true"]')).not.toBeNull()); + const editor = container.querySelector(".ProseMirror")!; + expect(editor).toHaveAttribute("contenteditable", "false"); + expect(editor.querySelector("h1")).toHaveTextContent("A heading"); + expect(editor.querySelector("strong")).toHaveTextContent("Strong"); + expect(editor.querySelector("table")).toHaveTextContent("One"); + expect(editor.querySelector(".katex")).not.toBeNull(); + expect(editor).toHaveTextContent("A note."); + expect(editor.closest("[inert]")).not.toBeNull(); + unmount(); + expect(editor.isConnected).toBe(false); + expect(disconnect).toHaveBeenCalled(); + }); +}); diff --git a/src/features/editor/components/markdown-example-preview.tsx b/src/features/editor/components/markdown-example-preview.tsx new file mode 100644 index 00000000..933584be --- /dev/null +++ b/src/features/editor/components/markdown-example-preview.tsx @@ -0,0 +1,78 @@ +import "katex/dist/katex.min.css"; +import "./milkdown-editor.css"; +import "./markdown-example-preview.css"; +import { editorViewCtx, editorViewOptionsCtx } from "@milkdown/kit/core"; +import { TextSelection } from "@milkdown/kit/prose/state"; +import { useEffect, useRef } from "react"; + +import { handleUnexpectedError } from "@/lib/errors"; +import { useLocalization } from "@/lib/i18n"; + +import { SOURCE_PROJECTION_ENTRY_SUPPRESSION_META } from "../plugins/sourceProjection"; +import { createMilkdownEditor, type MilkdownEditorInstance } from "../utils/createMilkdownEditor"; + +export function MarkdownExamplePreview({ source }: { source: string }) { + const { t } = useLocalization(); + const root = useRef(null); + + useEffect(() => { + const container = root.current!; + const host = document.createElement("div"); + container.append(host); + let disposed = false; + let instance: MilkdownEditorInstance | undefined; + const start = async () => { + const editor = await createMilkdownEditor({ root: host, initialMarkdown: source }); + editor.config((ctx) => { + ctx.update(editorViewOptionsCtx, (options) => ({ ...options, editable: () => false })); + }); + try { + await editor.create(); + } catch (error) { + await editor.destroy(); + throw error; + } + if (disposed) { + await editor.destroy(); + return; + } + instance = editor; + const view = editor.ctx.get(editorViewCtx); + const transaction = view.state.tr.insert( + view.state.doc.content.size, + view.state.schema.nodes.paragraph.create(), + ); + transaction.setSelection(TextSelection.atEnd(transaction.doc)); + view.dispatch(transaction.setMeta(SOURCE_PROJECTION_ENTRY_SUPPRESSION_META, true)); + view.setProps({ + dispatchTransaction: (next) => { + if (!next.docChanged && !next.selectionSet) { + view.updateState(view.state.apply(next)); + } + }, + }); + host.dataset.ready = "true"; + }; + const observer = new IntersectionObserver((entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + observer.disconnect(); + void start().catch((error) => { + if (!disposed) { + host.dataset.failed = "true"; + host.textContent = t("editor.example.failed"); + } + handleUnexpectedError(error, "editor.markdownExamplePreview"); + }); + } + }); + observer.observe(container); + return () => { + disposed = true; + observer.disconnect(); + host.remove(); + void instance?.destroy(); + }; + }, [source, t]); + + return
; +} diff --git a/src/features/editor/index.ts b/src/features/editor/index.ts index 756a138b..12ba253f 100644 --- a/src/features/editor/index.ts +++ b/src/features/editor/index.ts @@ -14,6 +14,7 @@ export type { CalloutTypeOption, } from "./commands/formatting/callouts"; export { DocumentTypographyPreview } from "./components/document-typography-preview"; +export { MarkdownExamplePreview } from "./components/markdown-example-preview"; export { HeadingOutline } from "./components/heading-outline"; export { MilkdownEditor, diff --git a/src/features/help/components/help-markdown.test.tsx b/src/features/help/components/help-markdown.test.tsx index 4cd69631..19812742 100644 --- a/src/features/help/components/help-markdown.test.tsx +++ b/src/features/help/components/help-markdown.test.tsx @@ -1,14 +1,69 @@ // @vitest-environment happy-dom import { openUrl } from "@tauri-apps/plugin-opener"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { withMacUserAgent, withWindowsUserAgent } from "@/test/utils/platform"; -import { render, screen, within } from "@/test/utils/react"; +import { render, renderWithUser, screen, within } from "@/test/utils/react"; import { HelpMarkdown } from "./help-markdown"; describe("HelpMarkdown", () => { + it("renders only bundled illustrations, with informative and decorative alternatives", () => { + render( + , + ); + expect(screen.getByRole("img", { name: "Window legend" }).querySelector("svg")).not.toBeNull(); + const article = screen.getByRole("article"); + expect(article.querySelectorAll("svg")).toHaveLength(2); + expect(article.querySelector('[aria-hidden="true"] svg')).not.toBeNull(); + expect(article.querySelector("img")).toBeNull(); + expect(article).toHaveTextContent("Missing"); + expect(article).toHaveTextContent("Local"); + }); + + it("pairs explicit example fences with an inert result and keeps ordinary fences as code", () => { + render( + , + ); + const article = screen.getByRole("article"); + expect(article.querySelectorAll("figure")).toHaveLength(2); + expect(article.querySelectorAll("pre code")).toHaveLength(4); + expect(article.querySelector('[data-help-example="gfm"]')).toHaveTextContent("What you type"); + expect(article.querySelector('[data-help-example="leafdown"]')).toHaveTextContent( + "Leafdown extension", + ); + expect(article.querySelectorAll("[inert][aria-hidden=true]")).toHaveLength(2); + expect(article.querySelector("pre code")).toHaveTextContent("- [x] Done"); + }); + + it("keeps navigation cards keyboard reachable and preserves ordinary lists and tips", async () => { + const onPageLink = vi.fn(); + const { user } = renderWithUser( + **Tip:** Keep a copy.\n\n- Ordinary item" + } + />, + ); + const link = screen.getByRole("button", { name: "Read Markdown" }); + link.focus(); + await user.keyboard("{Enter}"); + expect(onPageLink).toHaveBeenCalledWith("markdown-reference"); + expect(link.closest("ul")).toHaveClass("grid"); + expect(screen.getByText("Ordinary item").closest("ul")).toHaveClass("list-disc"); + expect(screen.getByText("Tip:").closest("blockquote")).toHaveTextContent("Keep a copy."); + }); + it.each([ ["Windows", withWindowsUserAgent, "Ctrl+Shift+F", ["Ctrl", "Shift", "F"]], ["macOS", withMacUserAgent, "Shift+Command+F", ["⇧", "⌘", "F"]], diff --git a/src/features/help/components/help-markdown.tsx b/src/features/help/components/help-markdown.tsx index 5dff79f4..78949bad 100644 --- a/src/features/help/components/help-markdown.tsx +++ b/src/features/help/components/help-markdown.tsx @@ -1,6 +1,14 @@ import { openUrl } from "@tauri-apps/plugin-opener"; import { ExternalLinkIcon } from "lucide-react"; -import { createContext, useContext, type ComponentProps, type ReactNode, type Ref } from "react"; +import { + createContext, + lazy, + Suspense, + useContext, + type ComponentProps, + type ReactNode, + type Ref, +} from "react"; import ReactMarkdown, { type Components, type ExtraProps } from "react-markdown"; import { ShortcutKeys } from "@/components/ui/shortcut-keys"; @@ -34,11 +42,122 @@ interface TextNode { children?: TextNode[]; } +interface ExampleNode extends TextNode { + children?: ExampleNode[]; + meta?: string | null; + data?: { hProperties?: Record }; +} + const nodeText = (node: TextNode | undefined): string => node?.type === "text" ? (node.value ?? "") : (node?.children ?? []).map(nodeText).join(""); const HelpPageLinkContext = createContext<((page: HelpPageId) => void) | null>(null); const CodeBlockContext = createContext(false); +const MarkdownExamplePreview = lazy(async () => ({ + default: (await import("@/features/editor")).MarkdownExamplePreview, +})); + +const HELP_ILLUSTRATIONS = import.meta.glob("../../../../docs/help/illustrations/*.svg", { + eager: true, + query: "?raw", + import: "default", +}); + +const remarkExamples = () => (tree: ExampleNode) => { + const visit = (node: ExampleNode) => { + if (node.type === "code") { + const kind = /^example=(commonmark|gfm|leafdown)$/u.exec(node.meta ?? "")?.[1]; + if (kind) node.data = { hProperties: { "data-example": kind } }; + } + node.children?.forEach(visit); + }; + visit(tree); +}; + +function HelpList({ node, ...props }: MarkdownProps<"ul">) { + const items = node?.children.filter((child) => child.type === "element"); + const cards = + items?.length && + items.every((item) => { + const first = item.children.find((child) => child.type === "element"); + const link = first?.type === "element" && first.tagName === "p" ? first.children[0] : first; + return ( + link?.type === "element" && + link.tagName === "a" && + typeof link.properties.href === "string" && + getHelpLinkTarget(link.properties.href)?.kind === "internal" + ); + }); + return ( +
    li]:rounded-lg [&>li]:border [&>li]:p-4" + : "my-3 list-disc space-y-2 pl-5 marker:text-muted-foreground" + } + {...props} + /> + ); +} + +function HelpImage({ src, alt }: MarkdownProps<"img">) { + const illustration = + typeof src === "string" && /^illustrations\/[a-z-]+\.svg$/u.test(src) + ? HELP_ILLUSTRATIONS[`../../../../docs/help/${src}`] + : undefined; + return illustration ? ( + + ) : ( + {alt} + ); +} + +function HelpPre({ node, children, ...props }: MarkdownProps<"pre">) { + const { t } = useLocalization(); + const code = node?.children[0]; + const kind = code?.type === "element" ? code.properties["data-example"] : undefined; + const source = nodeText(node).replace(/\n$/u, ""); + const sample = ( +
    +      {children}
    +    
    + ); + if (!kind) return sample; + return ( +
    +
    + {kind === "commonmark" + ? "CommonMark" + : kind === "gfm" + ? "GFM" + : t("help.example.extension")} +
    +
    +
    +

    {t("help.example.source")}

    + {sample} +
    +
    +

    + {t("help.example.result")} +

    + }> + + +
    +
    +
    + ); +} function HelpCode({ node: _node, className, children, ...props }: MarkdownProps<"code">) { const block = useContext(CodeBlockContext); @@ -101,7 +220,7 @@ function HelpLink({ href, children }: MarkdownProps<"a">) { const HELP_MARKDOWN_COMPONENTS = { a: HelpLink, - img: ({ alt }: MarkdownProps<"img">) => {alt}, + img: HelpImage, h1: () => null, h2: ({ node, ...props }: MarkdownProps<"h2">) => (

    ), p: ({ node: _node, ...props }: MarkdownProps<"p">) =>

    , - ul: ({ node: _node, ...props }: MarkdownProps<"ul">) => ( -

      - ), + ul: HelpList, ol: ({ node: _node, ...props }: MarkdownProps<"ol">) => (
        ), @@ -131,14 +248,7 @@ const HELP_MARKDOWN_COMPONENTS = {
        ), code: HelpCode, - pre: ({ node: _node, children, ...props }: MarkdownProps<"pre">) => ( -
        -      {children}
        -    
        - ), + pre: HelpPre, } satisfies Components; const INLINE_COMPONENTS = { @@ -154,7 +264,11 @@ export function HelpMarkdownFragment({ inline?: boolean; }) { return ( - + {source} ); diff --git a/src/features/help/services/pages.test.ts b/src/features/help/services/pages.test.ts index d723facd..0bd12cf3 100644 --- a/src/features/help/services/pages.test.ts +++ b/src/features/help/services/pages.test.ts @@ -48,6 +48,31 @@ describe("bundled Help pages", () => { } }); + it("resolves every authored guide and section link outside source examples", () => { + for (const source of Object.values(HELP_PAGES)) { + const sections = getHelpPageSections(source); + let fence: string | undefined; + for (const line of source.split(/\r?\n/u)) { + const marker = /^(`{3,}|~{3,})/u.exec(line)?.[1]; + if (marker) { + if (!fence) fence = marker; + else if (marker[0] === fence[0] && marker.length >= fence.length) fence = undefined; + continue; + } + if (fence) continue; + for (const match of line + .replace(/`[^`]*`/gu, "") + .matchAll(/(? id === target.id)).toBe( + true, + ); + } + } + } + }); + it("routes a fragment to the section heading it names", () => { expect(getHelpLinkTarget("#work-in-more-than-one-window")).toEqual({ kind: "section", diff --git a/src/locales/en.json b/src/locales/en.json index bc7012bc..34380485 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -247,6 +247,10 @@ "command.help.diagnostics": "Diagnostics...", "command.help.about": "About", "help.onThisPage": "On this page", + "help.example.source": "What you type", + "help.example.result": "What you see", + "help.example.extension": "Leafdown extension", + "editor.example.failed": "Preview unavailable. Read the source example beside it.", "shortcuts.description": "Leafdown keyboard shortcuts, grouped by menu. Shortcuts may be unavailable until their command is available.", "shortcuts.alternative": "or", "shortcuts.searchLabel": "Search shortcuts",