Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f1d4b07
fix(deps): update YAML and sanitizer security patches
Kechos23 Oct 11, 2026
fb28777
fix(storage): retain newer recovery journals after saves
Kechos23 Oct 11, 2026
4353a54
fix(review): recover comment metadata without losing newer replies
Kechos23 Oct 11, 2026
855d593
fix(editor): prevent reference chooser from stealing input focus
Kechos23 Oct 11, 2026
de4f5e8
fix(storage): coordinate deletions with queued document saves
Kechos23 Oct 11, 2026
07db99f
fix(storage): refuse destructive desktop document move collisions
Kechos23 Oct 11, 2026
d5d28ec
fix(storage): guard workspace replacement against pending operations
Kechos23 Oct 11, 2026
21905e6
docs(storage): explain recovery and workspace maintenance boundaries
Kechos23 Oct 11, 2026
bf0e404
test(storage): seed persisted document for stale writer move
Kechos23 Oct 11, 2026
3596f33
test(theme): compare final tokens after animated theme changes
Kechos23 Oct 11, 2026
54fcbae
test(trash): synchronize persistence and allow selection order
Kechos23 Oct 11, 2026
042c08f
fix(editor): prevent deferred media chooser focus from redirecting input
Kechos23 Oct 11, 2026
42d6c06
fix(editor): prevent deferred table chooser focus from changing row i…
Kechos23 Oct 11, 2026
f1b4041
feat(storage): isolate linked recovery hierarchy and detach imported …
Kechos23 Oct 11, 2026
8c3c1cf
feat(desktop): integrate reviewed linked workflows and draft safety g…
Kechos23 Oct 11, 2026
b3a1a72
test(desktop): cover linked safety and folder workflows without custo…
Kechos23 Oct 11, 2026
eee37bd
docs(desktop): explain linked workflows and filesystem safety boundaries
Kechos23 Oct 11, 2026
9e8b706
feat(i18n): complete reviewed Linked Workspace UI catalogs
Kechos23 Oct 11, 2026
73f5567
feat(desktop): refresh opt-in single-instance and native-drop demo on…
Kechos23 Oct 11, 2026
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
10 changes: 9 additions & 1 deletion assets/i18n/audit-ui-locales.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { readFile, readdir } from 'node:fs/promises';

const CATALOG_DIR = new URL('./', import.meta.url);
const EXPECTED_LOCALES = ['bg', 'de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'tr', 'tw', 'uk', 'zh'];
// Keyboard key names and bare capacity values may legitimately remain identical.
// Keep the exception finite: ordinary untranslated UI labels must still fail.
const SHARED_TECHNICAL_LITERALS = ['Alt', 'Tab', 'Cmd', 'F11', 'F3', '10 MB.', '20 MiB.'];
const REVIEWED_IDENTICAL_TRANSLATIONS = {
de: ['Option'],
it: ['$$ formula $$', ...Array.from({ length: 6 }, (_, index) => `Desktop: Ctrl/Cmd+${index + 1}`)]
};
const ALLOWED_IDENTICAL_TRANSLATIONS = {
bg: ['{{0}}m', '{{0}}m {{1}}s', '{{0}}s', 'Ab', 'Ctrl', 'Git Graph', 'GitHub', 'GitHub URL', 'h1, h2, h3, h4, h5, h6', 'HTML', 'JavaScript', 'Markdown (.md)', 'Markdown Viewer', 'MD', 'PDF', 'PNG', 'Shift', 'SVG', 'ThisIs-Developer'],
de: ['{{0}}m', '{{0}}m {{1}}s', '{{0}}s', 'Ab', 'Explorer', 'FAQ', 'h1, h2, h3, h4, h5, h6', 'HTML', 'JavaScript', 'Link', 'Markdown (.md)', 'Markdown Viewer', 'MD', 'Name', 'PDF', 'PNG', 'SVG', 'ThisIs-Developer', 'Version', 'Wrap'],
Expand Down Expand Up @@ -54,6 +61,7 @@ addFailures(failures, 'en', 'non-English source pollution', englishKeys.filter(s
addFailures(failures, 'en', 'non-interface source key', englishKeys.filter(source =>
/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/i.test(source)
|| /^bi(?:\s+bi-[\w-]+)+$/i.test(source)
|| /\[[\w-]+[~|^$*]?=[^\]]*\]/.test(source)
|| /(?:^|\s)(?:style|class|id|aria-[\w-]+|data-[\w-]+|role|tabindex)=/i.test(source)
));

Expand All @@ -75,7 +83,7 @@ for (const locale of files) {
|| (key.includes('Markdown') && !catalog[key].includes('Markdown'))
));
if (locale !== 'en') {
const allowed = new Set(ALLOWED_IDENTICAL_TRANSLATIONS[locale] || []);
const allowed = new Set([...SHARED_TECHNICAL_LITERALS, ...(ALLOWED_IDENTICAL_TRANSLATIONS[locale] || []), ...(REVIEWED_IDENTICAL_TRANSLATIONS[locale] || [])]);
addFailures(failures, locale, 'unexpected English fallback', keys.filter(key =>
catalog[key] === key && !allowed.has(key)
));
Expand Down
500 changes: 439 additions & 61 deletions assets/i18n/bg.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/de.json

Large diffs are not rendered by default.

564 changes: 471 additions & 93 deletions assets/i18n/en.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/es.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/fr.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/it.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/ja.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/ko.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/pl.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/pt.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/ru.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/tr.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/tw.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/uk.json

Large diffs are not rendered by default.

500 changes: 439 additions & 61 deletions assets/i18n/zh.json

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions assets/linked-workspace-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Welcome to Linked Workspace

Linked Workspace lets you edit Markdown files where they already live on your computer.

## Link a file or folder

Use **Link Markdown files** or **Link a Markdown folder** in the Explorer toolbar or the New menu. A linked folder includes Markdown files in its subfolders.

**Create new linked file…** creates a new external Markdown file and opens its link. Dropping an external Markdown file imports an independent Vault copy. Use the explicit linking actions when you want to keep an original file linked.

Linked documents use a file-with-arrow icon; linked folders use a folder-with-arrow icon. Hover over a linked document to see its original path.

## Drafts and saving

Editing automatically stores a recovery draft inside the application's **Linked Workspace** storage folder. It does **not** automatically overwrite your original file.

Linked-folder recovery drafts mirror the source subfolders under a separate recovery root. ID suffixes distinguish roots and documents with identical names. These are internal recovery copies, not the original files; do not manually reorganize this storage.

- **Ctrl+S** or **Save** updates the original linked file.
- **Save As** creates a new external file and a separate link. The previous document and original remain unchanged.
- Saving a Vault document externally keeps its independent Vault copy and activates the new linked document.

The Save shortcut works across keyboard layouts, including Greek. Saving to an original normally asks for confirmation and shows its full path. **Don't show this warning again** disables that ordinary warning for all linked files in this installation; **Confirm saving to original** in Settings enables it again. Conflict and missing-original warnings still require confirmation.

## Independent Vault copies

**Convert to Workspace copy** turns a linked draft into an independent Vault document without changing the original. You can also drag a linked document into the Vault or one of its folders.

Reopening the original creates or activates its link; it never replaces the independent Vault copy.

You can also **Convert to Workspace copy** on a linked folder or subfolder, or drag it into the Vault or one of its folders. This converts the currently linked drafts and preserves their subfolder structure. Excluded files are not included. A complete root conversion removes the folder link; a subfolder conversion keeps the root linked.

If a conversion stops partway through, completed copies remain in the Vault and the remaining drafts stay linked. The notification reports partial completion. Retrying uses the remembered destination when it is still available. Existing destination folders are not silently merged; a separate copy name is used when needed.

## Save a Vault folder externally

On a normal Vault folder, **Save As linked folder…** lets you choose an external parent directory. It creates a new folder inside that directory, preserves the subfolder structure, adds the new linked folder, and activates a linked copy. The Vault folder and documents remain independent and unchanged.

Existing destination names get a copy suffix rather than being overwritten. This exports Markdown documents, not a complete archive of images or other assets. If writing or linking stops partway through, the notification identifies the destination; partial external copies are kept, and Vault drafts remain available.

## External changes and rescanning

The application checks linked sources for external changes. If your local draft conflicts with a changed original, it keeps the draft and shows a warning.

Hover over the warning icon to see the source status. **Reload from disk** replaces your draft with the original and asks before discarding unsaved changes. Saving over a conflicting original also requires confirmation. If the original is missing or unreadable, your draft is retained; do not assume the source was deleted just because access failed.

**Rescan folder** discovers new files and restores links previously removed or converted, if the originals still exist. Re-linking the same folder does the same. Automatic background refreshes respect removed links instead of restoring them.

## Closing and removing

Closing a tab keeps its document in the workspace. **Remove linked file** or **Remove linked folder** removes links from the application, not the originals from disk.

## Backups and safety

Workspace backups include internal recovery drafts, not copies taken from external originals. Restoring a backup keeps linked drafts as recovered documents without reconnecting their original paths or enabling writes to those originals. Link originals explicitly when you want to reconnect them.

Multi-file conversion and external folder saving are not all-or-nothing operations. The application validates drafts and destinations and verifies writes, but the desktop filesystem APIs cannot make a whole folder write atomic or eliminate every race with another program. Keep backups of important originals, and review any incomplete-operation message before retrying.

This guide is an internal, editable document, not a link to an external file. You can delete it without deleting any source file; it will not reappear on every launch.
11 changes: 11 additions & 0 deletions assets/lucide-icons.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion desktop-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ neutralino.js
.tmp

# Docker build output
/output
/output

# Explicit local runtime experiment (not a distribution asset)
neutralino.experimental.config.json
resources/js/desktop-runtime-features.js
9 changes: 9 additions & 0 deletions desktop-app/neutralino.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"nativeAllowList": [
"app.exit",
"os.showOpenDialog",
"os.showFolderDialog",
"os.showSaveDialog",
"os.showMessageBox",
"os.getPath",
Expand All @@ -32,9 +33,17 @@
"filesystem.writeBinaryFile",
"filesystem.getStats",
"filesystem.getJoinedPath",
"filesystem.getNormalizedPath",
"filesystem.copy",
"filesystem.move",
"filesystem.remove",
"filesystem.createWatcher",
"filesystem.removeWatcher",
"filesystem.getWatchers",
"window.focus",
"window.show",
"window.isMinimized",
"window.unminimize",
"storage.setData",
"storage.getData",
"storage.getKeys",
Expand Down
22 changes: 22 additions & 0 deletions desktop-app/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ fs.copyFileSync(path.join(ROOT_DIR, "script.js"), path.join(jsDest, "script.js")
console.log("✓ Copied script.js → resources/js/script.js");

fs.copyFileSync(path.join(ROOT_DIR, "workspace-storage.js"), path.join(jsDest, "workspace-storage.js"));
fs.copyFileSync(path.join(ROOT_DIR, "native-markdown-source.js"), path.join(jsDest, "native-markdown-source.js"));
console.log("✓ Copied workspace-storage.js → resources/js/workspace-storage.js");

fs.copyFileSync(path.join(ROOT_DIR, "preview-worker.js"), path.join(jsDest, "preview-worker.js"));
Expand Down Expand Up @@ -331,12 +332,33 @@ async function prepareOfflineDependencies() {
html = html.replace(/<script\b[^>]*type="application\/ld\+json"[^>]*>[\s\S]*?<\/script>/i, '');
html = html.replace(/\s*<!-- Browser fallback for hosts that do not run the Cloudflare Pages SEO middleware\. -->\s*<script type="module" src="assets\/seo-metadata\.mjs"><\/script>/i, '');

html = html.replace(/<script src="native-markdown-source\.js" defer><\/script>/g, '<script src="/js/native-markdown-source.js"></script>');

// Inject Neutralino script tags
html = html.replace(
/<script\s+src="workspace-storage\.js"[^>]*><\/script>\s*<script\s+src="script\.js"[^>]*><\/script>/i,
'<script src="/js/neutralino.js"></script>\n <script src="/js/main.js"></script>\n <script src="/js/workspace-storage.js"></script>\n <script src="/js/script.js"></script>',
);

// Explicit desktop demo opt-in; the official runtime pin remains unchanged.
// Default desktop and web builds
// retain the upstream HTML drag-and-drop mechanism.
if (process.argv.includes('--experimental-neutralino-runtime')) {
// External same-origin scripts are allowed by the existing CSP. An inline
// flag would be blocked and silently disable the custom drag workaround.
fs.writeFileSync(path.join(RESOURCES_DIR, 'js', 'desktop-runtime-features.js'),
'window.MARKDOWN_VIEWER_DESKTOP_RUNTIME_FEATURES = { sidebarPointerDrag: true, nativeSingleInstance: true, nativeFileDrop: true };\n', 'utf-8');
html = html.replace('</head>', '<script src="/js/desktop-runtime-features.js"></script>\n</head>');
const demoConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'neutralino.config.json'), 'utf-8'));
demoConfig.singleInstance = true;
demoConfig.modes.window.emitDropEvents = true;
fs.writeFileSync(path.join(__dirname, 'neutralino.experimental.config.json'), JSON.stringify(demoConfig, null, 2));
} else {
// Do not leave custom opt-in resources behind after a standard preparation.
fs.rmSync(path.join(RESOURCES_DIR, 'js', 'desktop-runtime-features.js'), { force: true });
fs.rmSync(path.join(__dirname, 'neutralino.experimental.config.json'), { force: true });
}

// Inject app-info element
html = html.replace(
'<div class="app-container">',
Expand Down
Loading
Loading