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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Releases up to 0.3.5 are described on the [GitHub releases page](https://github.

## [Unreleased]

### Added
- Excalidraw drawings inside notes. **Drawing** in the editor toolbar adds one and opens a full-screen canvas; the preview shows it and opens it on click. Drawings are stored as `excalidraw` code blocks, sync and merge like the rest of the note, and are embedded in Word and PDF exports ([#98](https://github.com/LowBloat/LowNotes/issues/98)).

## [0.4.2] - 2026-10-24

### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ On Linux, updates follow the installation format. Arch, DEB and RPM packages sho
### Notes and organization

- Editable Markdown with preview, tasks, tables, footnotes, syntax extensions and Mermaid diagrams.
- Hand-drawn diagrams with [Excalidraw](https://excalidraw.com): **Drawing** in the editor toolbar adds one at the cursor and opens a full-screen canvas. The drawing lives inside the note as an `excalidraw` code block, so it syncs, collaborates and keeps history like the rest of the note; strokes from another device merge into an open drawing. Click a drawing in the preview to edit it. Pictures cannot be pasted into drawings, and Chinese, Japanese and Korean text in drawings uses system fonts.
- Check or uncheck tasks directly in **Split** or **Preview** view. The matching `[ ]` / `[x]` marker is edited in the Markdown, autosaved and synchronized through the editor's collaborative history. Each toggle is a separate local undo step; code examples and checkboxes in assistant responses stay read-only.
- **Ctrl + click** a Markdown link, image URL or plain web URL in the editor to open it in your default browser (**Cmd + click** on macOS). Regular clicks continue to edit the note; local image references stay in the vault.
- **Ctrl + F** (**Cmd + F** on macOS) opens a floating finder in the upper-right corner of the note area. Editor mode searches Markdown, Preview mode searches rendered text, and Split mode searches both panes and scrolls them together when navigating results. Use Enter / Shift + Enter, F3 / Shift + F3 or Ctrl + G / Ctrl + Shift + G to move between matches; Esc closes the finder. Case, whole-word and regular-expression options are available, with Markdown replacement in Editor and Split modes.
Expand All @@ -106,7 +107,7 @@ On Linux, updates follow the installation format. Arch, DEB and RPM packages sho
- Local recovery data is kept forever by default. Under **History and trash → Retention**, optional day limits are checked at startup and every 15 minutes while the vault is active. A cleanup preview shows expired versions/archives and protected items before manual removal. Deletion archives awaiting acknowledgement from a paired device, external recovery files and unsafe/incomplete archives stay protected. The structural deletion history and immutable image blobs are retained.
- Long editor lines wrap visually by default, preserving the file's line breaks and numbering. Disable **Visual line wrapping** in **Settings → General** to use horizontal scrolling.
- Folders, note search, `[[links between notes]]` and local Markdown links. The link map combines relations written in notes with relations added manually or by the assistant.
- Export notes and drafts to Word (`.docx`) and PDF using the same Markdown dialect as the preview. Rich text, lists, tasks, aligned tables, links and footnotes are preserved; Mermaid diagrams and images are embedded as visuals. PDF text remains selectable and Word text remains editable. Images can come from the vault, HTTP(S) addresses or data URLs.
- Export notes and drafts to Word (`.docx`) and PDF using the same Markdown dialect as the preview. Rich text, lists, tasks, aligned tables, links and footnotes are preserved; Mermaid diagrams, drawings and images are embedded as visuals. PDF text remains selectable and Word text remains editable. Images can come from the vault, HTTP(S) addresses or data URLs.
- Settings gathered on one screen: general preferences, themes, AI, providers, web search sources and app information.

### Optional assistant
Expand Down
322 changes: 316 additions & 6 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ flowchart LR
| Right-click menus | `src/lib/context-menu.svelte.ts`, `menu.ts`, `components/ContextMenu.svelte`, `MenuList.svelte` |
| Editor and preview, collaboration, presence | `src/lib/components/Editor.svelte`, `editor-collaboration.ts`, `presence.ts` |
| Markdown rendering (markdown-it, Mermaid) | `src/lib/markdown.ts`, `mermaid-renderer.ts`, `preview-links.ts` |
| Excalidraw drawings: blocks in notes, the canvas (React, loaded on demand), fonts served by `vite.config.js` | `src/lib/drawing-blocks.ts`, `excalidraw.ts`, `components/DrawingEditor.svelte` |
| Native bridge (every `invoke`) | `src/lib/api.ts`, `src/lib/types.ts` |
| Translations | `src/lib/i18n/` (en-US, pt-BR, es-ES) |
| Word/PDF export, loaded on demand | `document-export.ts`, `docx-export.ts`, `pdf-export.ts` |
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
"version:check": "bun scripts/check-version.mjs"
},
"overrides": {
"lodash-es": "4.18.1"
"lodash-es": "4.18.1",
"nanoid": "3.3.20"
},
"license": "AGPL-3.0-only",
"dependencies": {
"@codemirror/lang-markdown": "^6.5.2",
"@excalidraw/excalidraw": "^0.18.1",
"@tauri-apps/api": "^2.12.0",
"@tauri-apps/plugin-dialog": "^2.8.0",
"@tauri-apps/plugin-opener": "^2.7.0",
Expand All @@ -58,6 +60,8 @@
"markdown-it-sup": "^2.0.0",
"markdown-it-task-lists": "^2.1.1",
"mermaid": "^12.0.0",
"react": "^19.2.9",
"react-dom": "^19.2.9",
"y-codemirror.next": "^0.3.6",
"y-protocols": "^1.0.7",
"yjs": "^13.6.33"
Expand All @@ -70,6 +74,8 @@
"@tailwindcss/vite": "^4.3.3",
"@tauri-apps/cli": "^2",
"@types/bun": "^1.4.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"svelte": "^5.56.3",
"svelte-check": "^4.6.0",
"tailwindcss": "^4.3.3",
Expand Down
88 changes: 88 additions & 0 deletions src/lib/components/DrawingEditor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script lang="ts">
import { LoaderCircle, X } from 'lucide-svelte';
import { onDestroy, onMount, untrack } from 'svelte';
import { openUrl } from '@tauri-apps/plugin-opener';
import { locale, t } from '$lib/i18n';
import type { DrawingEditorHandle } from '$lib/excalidraw';
import type { AppTheme } from '../types';

let { content, theme, onChange, onClose } = $props<{
content: string;
theme: AppTheme;
onChange: (content: string) => void;
onClose: () => void;
}>();

let host: HTMLDivElement;
let handle: DrawingEditorHandle | null = null;
let status = $state<'loading' | 'ready' | 'error'>('loading');
let disposed = false;

/** Excalidraw's own languages: Portuguese and Spanish match, English is plain `en`. */
const LANGUAGES: Record<string, string> = { 'pt-BR': 'pt-BR', 'es-ES': 'es-ES' };

export function merge(next: string) { handle?.merge(next); }
export function flush() { handle?.flush(); }

function openLink(url: string) { void openUrl(url).catch(() => {}); }

// Links in Excalidraw's help dialog would otherwise navigate the app window.
function handleLinkClick(event: MouseEvent) {
const link = (event.target as Element | null)?.closest('a[href]');
if (!link || !host.contains(link)) return;
event.preventDefault();
const href = link.getAttribute('href') ?? '';
if (/^https?:\/\//i.test(href)) openLink(href);
}

onMount(async () => {
try {
const { mountDrawingEditor } = await import('$lib/excalidraw');
const mounted = await mountDrawingEditor(host, {
content: untrack(() => content),
theme: untrack(() => theme),
langCode: LANGUAGES[$locale] ?? 'en',
onChange: (next) => onChange(next),
onOpenLink: openLink,
});
if (disposed) { mounted.destroy(); return; }
handle = mounted;
status = 'ready';
} catch (error) {
console.error('Failed to open the drawing editor:', error);
if (!disposed) status = 'error';
}
});

onDestroy(() => {
disposed = true;
handle?.destroy();
handle = null;
});
</script>

<!-- Below the title bar, which keeps the window buttons. -->
<div data-modal-backdrop class="fixed inset-x-0 bottom-0 top-[var(--titlebar-height)] z-50 flex flex-col bg-[var(--bg-main)]" role="dialog" aria-modal="true" aria-labelledby="drawing-editor-title">
<header class="app-topbar flex items-center gap-3 px-4 border-b border-[var(--border)] bg-[var(--bg-sidebar)] select-none">
<h2 id="drawing-editor-title" class="text-sm font-semibold text-[var(--text-main)]">{$t('drawing.title')}</h2>
<span class="text-xs text-[var(--text-dim)]">{$t('drawing.autosave')}</span>
<button
type="button"
onclick={onClose}
class="ml-auto px-2.5 py-1 text-xs rounded transition flex items-center gap-1.5 border border-[var(--border)] bg-[var(--bg-card)] hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-main)]"
>
<X size={14} /> {$t('drawing.close')}
</button>
</header>
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
<div class="relative flex-1 min-h-0" onclickcapture={handleLinkClick}>
<div bind:this={host} class="drawing-editor-canvas absolute inset-0"></div>
{#if status === 'loading'}
<div role="status" class="absolute inset-0 flex items-center justify-center gap-2 text-sm text-[var(--text-muted)]">
<LoaderCircle size={16} class="animate-spin motion-reduce:animate-none" aria-hidden="true" /> {$t('drawing.loading')}
</div>
{:else if status === 'error'}
<div role="alert" class="absolute inset-0 flex items-center justify-center text-sm text-[var(--danger)]">{$t('drawing.loadError')}</div>
{/if}
</div>
</div>
Loading
Loading