diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee47c30..9c1625b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Non-code commits (documentation, planning, README-only updates) are excluded. ## v3.10.0 - **Description:** Introduced scalable local document storage, complete workspace backup and recovery, and a rebuilt GitHub importer with secure private-repository access. + - **In-App Release Notes:** Added the single extensionless, read-only v3.10.0 release note as a branded workspace tab with first-run visibility, close-as-seen version tracking, a sticky section guide, a Show Release Notes action in About, and a documented Markdown format that scales from one-fix patches to large multi-contributor releases. - **Workspace Storage:** Removed the document-count limit, migrated browser workspaces to per-document IndexedDB records with on-demand loading, a bounded memory cache, and paged Explorer rendering, and stored desktop documents as ordinary Markdown files in the fixed `Documents/Markdown Viewer Vault` location. - **Backup & Recovery:** Added folder-preserving ZIP backup and restore for web and desktop, optional encrypted Secret Workspace files, exact usage and document counts, safer import and reset confirmations, and clearer browser-data and Private mode behavior. - **GitHub Import:** Discovered every Markdown file including truncated repository trees, resolved branches, tags, slash-containing refs, and commits to immutable SHAs, and added up to 50 named fine-grained or classic PAT entries stored in an AES-GCM-encrypted local credential vault. diff --git a/Dockerfile b/Dockerfile index b40fea3b..dc3f73f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ COPY workspace-storage.js /usr/share/nginx/html/ COPY script.js /usr/share/nginx/html/ COPY styles.css /usr/share/nginx/html/ COPY sw.js /usr/share/nginx/html/ +COPY RELEASE_NOTES /usr/share/nginx/html/ COPY manifest.json /usr/share/nginx/html/ COPY robots.txt /usr/share/nginx/html/ COPY sitemap.xml /usr/share/nginx/html/ diff --git a/RELEASE_NOTES b/RELEASE_NOTES new file mode 100644 index 00000000..046ffc1a --- /dev/null +++ b/RELEASE_NOTES @@ -0,0 +1,83 @@ +![Markdown Viewer logo](assets/icon.jpg) **Markdown Viewer release** · `v3.10.0` + +# Welcome to Markdown Viewer 3.10.0 + +_Released August 2, 2026_ + +This release gives your workspace room to grow, makes complete backups practical, and rebuilds GitHub importing for large and private repositories. + +Everything here ships with the web, PWA, Docker, and desktop editions. **Happy coding!** + +[View release on GitHub](https://github.com/ThisIs-Developer/Markdown-Viewer/releases/tag/v3.10.0) [Open the full changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md) + +--- + +## Highlights + +- **A workspace without a document-count ceiling.** Documents now use scalable per-document storage instead of one increasingly large browser record. +- **Complete backup and recovery.** Export and restore folder-preserving ZIP backups on the web and desktop. +- **A rebuilt GitHub importer.** Discover every Markdown file, work with branches and tags reliably, and save private-repository access securely. +- **Polish across the product.** Faster bulk tab closing, clearer staged loading, responsive import selection, and an audit of all 14 interface languages. + +> [!TIP] +> Start with **Workspace storage** if you manage a large library, or jump to **GitHub import** if your documentation lives in repositories. + +## Workspace storage + +Markdown Viewer no longer limits a workspace to a fixed number of documents. Browser documents are stored as individual IndexedDB records, loaded only when needed, retained in a bounded memory cache, and rendered in paged Explorer groups. + +Desktop documents remain ordinary Markdown files in the fixed **Documents/Markdown Viewer Vault** location, so the vault is predictable and easy to inspect outside the application. + +### What changed + +- Removed the former document-count limit. +- Added on-demand document loading and a bounded cache. +- Added paged Explorer rendering for large workspaces. +- Added exact storage usage and document totals. +- Preserved a normal Markdown-file vault for desktop users. + +## Backup and recovery + +Workspace backup now preserves documents and folder organization in a ZIP file. The same archive can restore a workspace on the web or desktop, with clearer confirmations before imports and destructive resets. + +Secret Workspace files can be included as encrypted data, while Private mode continues to pause persistence without silently removing saved work. + +> [!IMPORTANT] +> A workspace reset removes documents, settings, and release-note visibility data. After a reset, Markdown Viewer treats the next launch as a first run and shows Welcome with this release-note tab beside it. + +## GitHub import + +The importer now finds all Markdown files even when GitHub truncates a large repository tree. It resolves branches, tags, slash-containing references, and commit selections to immutable SHAs before importing. + +Private access supports up to 50 named fine-grained or classic personal access tokens. Saved credentials stay in a local AES-GCM-encrypted vault and are sent only to `api.github.com` for the selected repository operation. + +### Import workflow improvements + +- Search and select files in a responsive picker. +- Import files into Explorer without opening a flood of tabs. +- See staged progress and clearer success or failure feedback. +- Distinguish repository folders and their expanded state more easily. +- Keep private credentials named, encrypted, and removable. + +## Interface, languages, and desktop + +The release completes a visual and localization pass across the importer and workspace. All 14 interface catalogs were audited, desktop resources were synchronized with the web build, and tagged prereleases now publish with the correct metadata. + +Bulk tab closing is batched so large sessions remain responsive, and small-screen import selection retains comfortable touch targets. + +## Thank you + +### Contributors + +- **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** — Design, implementation, testing, documentation, and release work. + +### Changes included + +- [#230 — Unlimited storage, backup, reset, and fixed vault](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/230) +- [#231 — Preserve the complete #230 commit history](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/231) +- [#232 — Refine storage documentation highlights](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/232) +- [#233 — Scale GitHub imports and simplify private access](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/233) + +Thank you for using Markdown Viewer and helping make every release more useful. For the complete history, read the [project changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md). + +**Happy coding!** diff --git a/assets/i18n/de.json b/assets/i18n/de.json index 1d6edc36..16763951 100644 --- a/assets/i18n/de.json +++ b/assets/i18n/de.json @@ -90,7 +90,6 @@ "Capturing document": "Dokument erfassen", "Capturing image": "Bild wird aufgenommen", "Change text case": "Text-Groß-/Kleinschreibung ändern", - "Changelog": "Änderungsprotokoll", "Chars": "Zeichen", "Checkbox Map": "Kontrollkästchen-Karte", "Checklist Map": "Checklistenkarte", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Eingebettete Bilder konnten nicht konvertiert werden. Bitte versuchen Sie es erneut.", "expanded.": "erweitert.", "Image conversion was cancelled because the document changed.": "Die Bildkonvertierung wurde abgebrochen, da sich das Dokument geändert hat.", - "MD": "MD", "Open an editable Markdown file before inserting images.": "Öffnen Sie eine bearbeitbare Markdown-Datei, bevor Sie Bilder einfügen.", "uploaded and inserted with short links.": "hochgeladen und mit Kurzlinks eingefügt.", "cancel": "abbrechen", @@ -1018,7 +1016,6 @@ "Paste failed:": "Einfügen fehlgeschlagen:", "Selection copied to clipboard.": "Auswahl in die Zwischenablage kopiert.", "Selection cut to clipboard.": "Auswahl in Zwischenablage ausschneiden.", - "1 file": "1 Datei", "Checking…": "Überprüfen…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "Gelöschte Dokumente werden in den wiederherstellbaren Papierkorb verschoben. Desktop-Änderungen bewahren den aktuellen Verlauf im Tresor auf.", "Document unavailable": "Dokument nicht verfügbar", @@ -1156,5 +1153,20 @@ "Token name": "Tokenname", "Up to 50 GitHub access tokens can be saved.": "Es können bis zu 50 GitHub-Zugriffstokens gespeichert werden.", "Use a unique token name.": "Verwenden Sie einen eindeutigen Tokennamen.", - "Move to…": "Verschieben nach…" + "Move to…": "Verschieben nach…", + "Release notes unavailable": "Versionshinweise nicht verfügbar", + "Show Release Notes": "Versionshinweise anzeigen", + "The release notes could not be opened.": "Die Versionshinweise konnten nicht geöffnet werden.", + "Release notes are read only.": "Versionshinweise sind schreibgeschützt.", + "Release notes opened.": "Versionshinweise geöffnet.", + "Back to the top of the release notes": "Zurück zum Anfang der Versionshinweise", + "Back to top": "Zurück nach oben", + "Close Release Notes: 3.10.0": "Versionshinweise schließen: 3.10.0", + "File options for Release Notes: 3.10.0": "Dateioptionen für Versionshinweise: 3.10.0", + "In this update": "In diesem Update", + "release notes": "Versionshinweise", + "Release notes are available in the tab list.": "Versionshinweise sind in der Registerkartenliste verfügbar.", + "Release Notes: 3.10.0": "Versionshinweise: 3.10.0", + "Skip update navigation": "Update-Navigation überspringen", + "1 file": "1 Datei" } diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 92c024aa..fbce03ac 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -95,6 +95,8 @@ "Area Chart": "Area Chart", "Audio playback is not supported in this browser.": "Audio playback is not supported in this browser.", "Back to content": "Back to content", + "Back to the top of the release notes": "Back to the top of the release notes", + "Back to top": "Back to top", "Back up or restore your workspace": "Back up or restore your workspace", "Backup": "Backup", "Backup complete": "Backup complete", @@ -120,7 +122,6 @@ "Capturing document": "Capturing document", "Capturing image": "Capturing image", "Change text case": "Change text case", - "Changelog": "Changelog", "Changes not saved": "Changes not saved", "Chars": "Chars", "Checkbox Map": "Checkbox Map", @@ -183,6 +184,7 @@ "Close others": "Close others", "Close PDF export dialog": "Close PDF export dialog", "Close reference dialog": "Close reference dialog", + "Close Release Notes: 3.10.0": "Close Release Notes: 3.10.0", "Close rename dialog": "Close rename dialog", "Close reset confirmation dialog": "Close reset confirmation dialog", "Close review deletion dialog": "Close review deletion dialog", @@ -415,6 +417,7 @@ "File not supported": "File not supported", "File options": "File options", "File options for": "File options for", + "File options for Release Notes: 3.10.0": "File options for Release Notes: 3.10.0", "File options for Welcome to Markdown": "File options for Welcome to Markdown", "File renamed": "File renamed", "File tree actions": "File tree actions", @@ -528,6 +531,7 @@ "Importing this backup permanently replaces the current workspace, including its files, folders, settings, and Secret Workspace data.": "Importing this backup permanently replaces the current workspace, including its files, folders, settings, and Secret Workspace data.", "Importing workspace backup": "Importing workspace backup", "in split view": "in split view", + "In this update": "In this update", "Include secure workspace files": "Include secure workspace files", "Incorrect access key or unreadable Secret Workspace data.": "Incorrect access key or unreadable Secret Workspace data.", "Inline code": "Inline code", @@ -620,7 +624,6 @@ "Match Whole Word": "Match Whole Word", "Match Whole Word (W)": "Match Whole Word (W)", "matches": "matches", - "MD": "MD", "Media description": "Media description", "media file": "media file", "Media file": "Media file", @@ -801,6 +804,12 @@ "Reference": "Reference", "Reference Number": "Reference Number", "Reference title": "Reference title", + "release notes": "release notes", + "Release notes are available in the tab list.": "Release notes are available in the tab list.", + "Release notes are read only.": "Release notes are read only.", + "Release notes opened.": "Release notes opened.", + "Release notes unavailable": "Release notes unavailable", + "Release Notes: 3.10.0": "Release Notes: 3.10.0", "Reloading restored workspace…": "Reloading restored workspace…", "Remove from Favorites": "Remove from Favorites", "Remove selected access token": "Remove selected access token", @@ -952,6 +961,7 @@ "Show access key": "Show access key", "Show confirmation access key": "Show confirmation access key", "Show Diff Preview before replace": "Show Diff Preview before replace", + "Show Release Notes": "Show Release Notes", "Show sidebar": "Show sidebar", "Showing": "Showing", "shown.": "shown.", @@ -959,6 +969,7 @@ "Signature and Speed": "Signature and Speed", "Simple Flow": "Simple Flow", "Simple Tune": "Simple Tune", + "Skip update navigation": "Skip update navigation", "Small documents stay encoded in the URL. Large documents use temporary Cloudflare KV for up to 90 days. Shared tabs are not saved locally on the recipient's device.": "Small documents stay encoded in the URL. Large documents use temporary Cloudflare KV for up to 90 days. Shared tabs are not saved locally on the recipient's device.", "Solid": "Solid", "Solid Mode": "Solid Mode", @@ -1034,6 +1045,7 @@ "The live room could not be joined. Please check the invite link or connection.": "The live room could not be joined. Please check the invite link or connection.", "The media file could not be inserted.": "The media file could not be inserted.", "The provided URL does not point to a Markdown file.": "The provided URL does not point to a Markdown file.", + "The release notes could not be opened.": "The release notes could not be opened.", "The repository folder could not be created.": "The repository folder could not be created.", "The selected GitHub access could not be saved.": "The selected GitHub access could not be saved.", "The share link could not be generated.": "The share link could not be generated.", diff --git a/assets/i18n/es.json b/assets/i18n/es.json index e69d939b..53ebfc0b 100644 --- a/assets/i18n/es.json +++ b/assets/i18n/es.json @@ -90,7 +90,6 @@ "Capturing document": "Capturando documento", "Capturing image": "Capturando imagen", "Change text case": "Cambiar mayúsculas y minúsculas del texto", - "Changelog": "Registro de cambios", "Chars": "Caracteres", "Checkbox Map": "Mapa de casillas de verificación", "Checklist Map": "Mapa de lista de verificación", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Las imágenes incrustadas no se pudieron convertir. Por favor inténtalo de nuevo.", "expanded.": "ampliado.", "Image conversion was cancelled because the document changed.": "La conversión de imagen se canceló porque el documento cambió.", - "MD": "MD", "Open an editable Markdown file before inserting images.": "Abra un archivo Markdown editable antes de insertar imágenes.", "uploaded and inserted with short links.": "subido e insertado con enlaces cortos.", "cancel": "cancelar", @@ -1018,7 +1016,6 @@ "Paste failed:": "Error al pegar:", "Selection copied to clipboard.": "Selección copiada al portapapeles.", "Selection cut to clipboard.": "Selección cortada al portapapeles.", - "1 file": "1 archivo", "Checking…": "Comprobando…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "Los documentos eliminados se mueven a la papelera recuperable. Las ediciones de escritorio mantienen el historial reciente dentro de la bóveda.", "Document unavailable": "Documento no disponible", @@ -1156,5 +1153,20 @@ "Token name": "Nombre del token", "Up to 50 GitHub access tokens can be saved.": "Se pueden guardar hasta 50 tokens de acceso de GitHub.", "Use a unique token name.": "Utilice un nombre de token único.", - "Move to…": "Mover a…" + "Move to…": "Mover a…", + "Release notes unavailable": "Notas de la versión no disponibles", + "Show Release Notes": "Mostrar notas de la versión", + "The release notes could not be opened.": "No se pudieron abrir las notas de la versión.", + "Release notes are read only.": "Las notas de la versión son de solo lectura.", + "Release notes opened.": "Notas de la versión abiertas.", + "Back to the top of the release notes": "Volver al principio de las notas de la versión", + "Back to top": "Volver arriba", + "Close Release Notes: 3.10.0": "Cerrar Notas de la versión: 3.10.0", + "File options for Release Notes: 3.10.0": "Opciones de archivo para Notas de la versión: 3.10.0", + "In this update": "En esta actualización", + "release notes": "notas de la versión", + "Release notes are available in the tab list.": "Las notas de la versión están disponibles en la lista de pestañas.", + "Release Notes: 3.10.0": "Notas de la versión: 3.10.0", + "Skip update navigation": "Saltar navegación de actualización", + "1 file": "1 archivo" } diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json index c518d75f..528ded43 100644 --- a/assets/i18n/fr.json +++ b/assets/i18n/fr.json @@ -90,7 +90,6 @@ "Capturing document": "Capture de document", "Capturing image": "Capture d'image", "Change text case": "Modifier la casse du texte", - "Changelog": "Journal des modifications", "Chars": "Caractères", "Checkbox Map": "Carte des cases à cocher", "Checklist Map": "Carte de la liste de contrôle", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Les images intégrées n'ont pas pu être converties. Veuillez réessayer.", "expanded.": "développé.", "Image conversion was cancelled because the document changed.": "La conversion de l'image a été annulée car le document a été modifié.", - "MD": "MD", "Open an editable Markdown file before inserting images.": "Ouvrez un fichier Markdown modifiable avant d'insérer des images.", "uploaded and inserted with short links.": "téléchargé et inséré avec des liens courts.", "cancel": "annuler", @@ -1018,7 +1016,6 @@ "Paste failed:": "Échec du collage :", "Selection copied to clipboard.": "Sélection copiée dans le presse-papiers.", "Selection cut to clipboard.": "Sélection coupée dans le presse-papiers.", - "1 file": "1 fichier", "Checking…": "Vérification…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "Les documents supprimés sont déplacés vers la corbeille récupérable. Les modifications apportées au bureau conservent l’historique récent dans le coffre-fort.", "Document unavailable": "Document indisponible", @@ -1156,5 +1153,20 @@ "Token name": "Nom du jeton", "Up to 50 GitHub access tokens can be saved.": "Jusqu'à 50 jetons d'accès GitHub peuvent être enregistrés.", "Use a unique token name.": "Utilisez un nom de jeton unique.", - "Move to…": "Déplacer vers…" + "Move to…": "Déplacer vers…", + "Release notes unavailable": "Notes de version indisponibles", + "Show Release Notes": "Afficher les notes de version", + "The release notes could not be opened.": "Les notes de version n'ont pas pu être ouvertes.", + "Release notes are read only.": "Les notes de version sont en lecture seule.", + "Release notes opened.": "Notes de version ouvertes.", + "Back to the top of the release notes": "Retour en haut des notes de version", + "Back to top": "Retour en haut", + "Close Release Notes: 3.10.0": "Fermer les notes de version : 3.10.0", + "File options for Release Notes: 3.10.0": "Options de fichier pour les notes de version : 3.10.0", + "In this update": "Dans cette mise à jour", + "release notes": "Notes de version", + "Release notes are available in the tab list.": "Les notes de version sont disponibles dans la liste des onglets.", + "Release Notes: 3.10.0": "Notes de version : 3.10.0", + "Skip update navigation": "Ignorer la navigation de mise à jour", + "1 file": "1 fichier" } diff --git a/assets/i18n/generate-ui-locales.mjs b/assets/i18n/generate-ui-locales.mjs index 3f43cc4c..f2659e35 100644 --- a/assets/i18n/generate-ui-locales.mjs +++ b/assets/i18n/generate-ui-locales.mjs @@ -223,6 +223,7 @@ const EXTRA_STRINGS = [ 'All feedback resolved', 'Everything has been resolved. Switch to Resolved or All to review earlier feedback.', 'Review item deleted.', 'Review item reopened.', 'Review item resolved.', 'Review item updated.', 'Review mode keeps the Markdown source read only.', + 'Release notes are read only.', 'Creating link', 'Creating snapshot link...', 'Room active - waiting for collaborators', 'Ending live room...', 'Live room disconnected', 'Live room ended by the host', 'This Live Share session is view only.', 'This document is read only.', diff --git a/assets/i18n/it.json b/assets/i18n/it.json index f6f94a83..72af1813 100644 --- a/assets/i18n/it.json +++ b/assets/i18n/it.json @@ -90,7 +90,6 @@ "Capturing document": "Acquisizione del documento", "Capturing image": "Cattura dell'immagine", "Change text case": "Cambia maiuscole/minuscole nel testo", - "Changelog": "Registro delle modifiche", "Chars": "Caratteri", "Checkbox Map": "Casella di controllo Mappa", "Checklist Map": "Mappa della lista di controllo", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Impossibile convertire le immagini incorporate. Per favore riprova.", "expanded.": "ampliato.", "Image conversion was cancelled because the document changed.": "La conversione dell'immagine è stata annullata perché il documento è cambiato.", - "MD": "MD", "Open an editable Markdown file before inserting images.": "Apri un file modificabile Markdown prima di inserire le immagini.", "uploaded and inserted with short links.": "caricato e inserito con collegamenti brevi.", "cancel": "annulla", @@ -1018,7 +1016,6 @@ "Paste failed:": "Incolla non riuscita:", "Selection copied to clipboard.": "Selezione copiata negli appunti.", "Selection cut to clipboard.": "Selezione tagliata negli appunti.", - "1 file": "1 file", "Checking…": "Controllo…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "I documenti eliminati vengono spostati nel cestino recuperabile. Le modifiche sul desktop mantengono la cronologia recente all'interno del vault.", "Document unavailable": "Documento non disponibile", @@ -1156,5 +1153,20 @@ "Token name": "Nome del token", "Up to 50 GitHub access tokens can be saved.": "È possibile salvare fino a 50 token di accesso GitHub.", "Use a unique token name.": "Utilizza un nome token univoco.", - "Move to…": "Sposta in..." + "Move to…": "Sposta in...", + "Release notes unavailable": "Note sulla versione non disponibili", + "Show Release Notes": "Mostra le note di rilascio", + "The release notes could not be opened.": "Impossibile aprire le note sulla versione.", + "Release notes are read only.": "Le note di rilascio sono di sola lettura.", + "Release notes opened.": "Note sulla versione aperte.", + "Back to the top of the release notes": "Torna all'inizio delle note sulla versione", + "Back to top": "Torna all'inizio", + "Close Release Notes: 3.10.0": "Chiudi le note sulla versione: 3.10.0", + "File options for Release Notes: 3.10.0": "Opzioni file per le note sulla versione: 3.10.0", + "In this update": "In questo aggiornamento", + "release notes": "note sulla versione", + "Release notes are available in the tab list.": "Le note sulla versione sono disponibili nell'elenco delle schede.", + "Release Notes: 3.10.0": "Note sulla versione: 3.10.0", + "Skip update navigation": "Salta la navigazione degli aggiornamenti", + "1 file": "1 file" } diff --git a/assets/i18n/ja.json b/assets/i18n/ja.json index 75c20609..97c07b1f 100644 --- a/assets/i18n/ja.json +++ b/assets/i18n/ja.json @@ -90,7 +90,6 @@ "Capturing document": "ドキュメントをキャプチャしています", "Capturing image": "画像キャプチャ", "Change text case": "テキストの大文字と小文字を変更する", - "Changelog": "変更履歴", "Chars": "文字", "Checkbox Map": "チェックボックス マップ", "Checklist Map": "チェックリストマップ", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "埋め込み画像を変換できませんでした。もう一度試してください。", "expanded.": "を展開しました。", "Image conversion was cancelled because the document changed.": "ドキュメントが変更されたため、画像変換がキャンセルされました。", - "MD": "MD", "Open an editable Markdown file before inserting images.": "画像を挿入する前に、編集可能な Markdown ファイルを開いてください。", "uploaded and inserted with short links.": "がアップロードされ、短いリンクが挿入されました。", "cancel": "キャンセル", @@ -1018,7 +1016,6 @@ "Paste failed:": "貼り付けに失敗しました:", "Selection copied to clipboard.": "選択範囲がクリップボードにコピーされました。", "Selection cut to clipboard.": "選択範囲がクリップボードに切り取られました。", - "1 file": "1ファイル", "Checking…": "確認中…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "削除されたドキュメントは回復可能なゴミ箱に移動されます。デスクトップ編集では、最近の履歴が Vault 内に保存されます。", "Document unavailable": "ドキュメントが利用できません", @@ -1156,5 +1153,20 @@ "Token name": "トークン名", "Up to 50 GitHub access tokens can be saved.": "GitHub アクセストークンは最大 50 個まで保存できます。", "Use a unique token name.": "一意のトークン名を使用します。", - "Move to…": "移動先…" + "Move to…": "移動先…", + "Release notes unavailable": "リリースノートは利用できません", + "Show Release Notes": "リリースノートを表示", + "The release notes could not be opened.": "リリースノートを開けませんでした。", + "Release notes are read only.": "リリースノートは読み取り専用です。", + "Release notes opened.": "リリースノートを公開しました。", + "Back to the top of the release notes": "リリースノートの先頭に戻る", + "Back to top": "トップに戻る", + "Close Release Notes: 3.10.0": "リリースノートを閉じる: 3.10.0", + "File options for Release Notes: 3.10.0": "リリースノートのファイルオプション: 3.10.0", + "In this update": "今回のアップデートでは", + "release notes": "リリースノート", + "Release notes are available in the tab list.": "リリースノートはタブ一覧からご覧いただけます。", + "Release Notes: 3.10.0": "リリースノート: 3.10.0", + "Skip update navigation": "アップデートナビゲーションをスキップ", + "1 file": "1ファイル" } diff --git a/assets/i18n/ko.json b/assets/i18n/ko.json index 37eda176..6c339969 100644 --- a/assets/i18n/ko.json +++ b/assets/i18n/ko.json @@ -90,7 +90,6 @@ "Capturing document": "문서 캡쳐 중", "Capturing image": "이미지 캡쳐 중", "Change text case": "대소문자 변경", - "Changelog": "변경 내역", "Chars": "문자", "Checkbox Map": "체크박스 맵", "Checklist Map": "체크리스트 맵", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "삽입된 이미지를 변환할 수 없습니다. 다시 시도해 주세요.", "expanded.": "이 확장되었습니다.", "Image conversion was cancelled because the document changed.": "문서가 변경되어 이미지 변환이 취소되었습니다.", - "MD": "MD", "Open an editable Markdown file before inserting images.": "이미지를 삽입하기 전에 편집 가능한 Markdown 파일을 엽니다.", "uploaded and inserted with short links.": "짧은 링크와 함께 업로드 및 삽입되었습니다.", "cancel": "취소", @@ -1018,7 +1016,6 @@ "Paste failed:": "붙여넣기 실패:", "Selection copied to clipboard.": "선택 항목이 클립보드에 복사되었습니다.", "Selection cut to clipboard.": "선택 항목을 클립보드로 잘라냈습니다.", - "1 file": "파일 1개", "Checking…": "확인 중…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "삭제된 문서는 복구 가능한 휴지통으로 이동됩니다. 데스크탑 편집은 Vault 내부에 최근 기록을 유지합니다.", "Document unavailable": "문서를 사용할 수 없습니다.", @@ -1156,5 +1153,20 @@ "Token name": "토큰 이름", "Up to 50 GitHub access tokens can be saved.": "GitHub 액세스 토큰은 최대 50개까지 저장할 수 있습니다.", "Use a unique token name.": "고유한 토큰 이름을 사용하세요.", - "Move to…": "이동…" + "Move to…": "이동…", + "Release notes unavailable": "릴리스 노트를 사용할 수 없습니다.", + "Show Release Notes": "출시 노트 보기", + "The release notes could not be opened.": "릴리스 노트를 열 수 없습니다.", + "Release notes are read only.": "릴리스 노트는 읽기 전용입니다.", + "Release notes opened.": "릴리스 노트가 열렸습니다.", + "Back to the top of the release notes": "릴리스 노트 위로 돌아가기", + "Back to top": "위로 가기", + "Close Release Notes: 3.10.0": "닫기 릴리스 노트: 3.10.0", + "File options for Release Notes: 3.10.0": "릴리스 노트의 파일 옵션: 3.10.0", + "In this update": "이번 업데이트에서는", + "release notes": "릴리스 노트", + "Release notes are available in the tab list.": "릴리스 노트는 탭 목록에서 확인할 수 있습니다.", + "Release Notes: 3.10.0": "출시 노트: 3.10.0", + "Skip update navigation": "업데이트 탐색 건너뛰기", + "1 file": "파일 1개" } diff --git a/assets/i18n/pl.json b/assets/i18n/pl.json index 7dbbe2ea..59fedd68 100644 --- a/assets/i18n/pl.json +++ b/assets/i18n/pl.json @@ -90,7 +90,6 @@ "Capturing document": "Przechwytywanie dokumentu", "Capturing image": "Przechwytywanie obrazu", "Change text case": "Zmień wielkość liter", - "Changelog": "Lista zmian", "Chars": "Znaki", "Checkbox Map": "Mapa pól wyboru", "Checklist Map": "Mapa listy kontrolnej", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Nie można przekonwertować osadzonych obrazów. Spróbuj ponownie.", "expanded.": "rozwinięte.", "Image conversion was cancelled because the document changed.": "Konwersja obrazu została anulowana z powodu zmiany dokumentu.", - "MD": "Lek", "Open an editable Markdown file before inserting images.": "Otwórz edytowalny plik Markdown przed wstawieniem obrazów.", "uploaded and inserted with short links.": "przesłano i wstawiono z krótkimi linkami.", "cancel": "anuluj", @@ -1018,7 +1016,6 @@ "Paste failed:": "Wklejanie nie powiodło się:", "Selection copied to clipboard.": "Zaznaczenie skopiowane do schowka.", "Selection cut to clipboard.": "Zaznaczenie wycięte do schowka.", - "1 file": "1 plik", "Checking…": "Sprawdzam…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "Usunięte dokumenty są przenoszone do kosza, który można odzyskać. Zmiany na pulpicie przechowują najnowszą historię w skarbcu.", "Document unavailable": "Dokument niedostępny", @@ -1156,5 +1153,20 @@ "Token name": "Nazwa tokena", "Up to 50 GitHub access tokens can be saved.": "Można zapisać do 50 tokenów dostępu GitHub.", "Use a unique token name.": "Użyj unikalnej nazwy tokena.", - "Move to…": "Przejdź do…" + "Move to…": "Przejdź do…", + "Release notes unavailable": "Informacje o wydaniu są niedostępne", + "Show Release Notes": "Pokaż informacje o wersji", + "The release notes could not be opened.": "Nie można otworzyć informacji o wersji.", + "Release notes are read only.": "Informacje o wersji są tylko do odczytu.", + "Release notes opened.": "Otwarto informacje o wersji.", + "Back to the top of the release notes": "Powrót na początek informacji o wydaniu", + "Back to top": "Powrót do góry", + "Close Release Notes: 3.10.0": "Zamknij Informacje o wydaniu: 3.10.0", + "File options for Release Notes: 3.10.0": "Opcje plików dla informacji o wydaniu: 3.10.0", + "In this update": "W tej aktualizacji", + "release notes": "informacje o wydaniu", + "Release notes are available in the tab list.": "Informacje o wydaniu są dostępne na liście zakładek.", + "Release Notes: 3.10.0": "Informacje o wydaniu: 3.10.0", + "Skip update navigation": "Pomiń nawigację po aktualizacji", + "1 file": "1 plik" } diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json index e1f6fc2c..b353cbc7 100644 --- a/assets/i18n/pt.json +++ b/assets/i18n/pt.json @@ -90,7 +90,6 @@ "Capturing document": "Capturando documento", "Capturing image": "Capturando imagem", "Change text case": "Alterar maiúsculas e minúsculas do texto", - "Changelog": "Registro de alterações", "Chars": "Caracteres", "Checkbox Map": "Mapa de caixas de seleção", "Checklist Map": "Mapa da lista de verificação", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Não foi possível converter imagens incorporadas. Por favor, tente novamente.", "expanded.": "expandido.", "Image conversion was cancelled because the document changed.": "A conversão da imagem foi cancelada porque o documento foi alterado.", - "MD": "Médico", "Open an editable Markdown file before inserting images.": "Abra um arquivo Markdown editável antes de inserir imagens.", "uploaded and inserted with short links.": "carregado e inserido com links curtos.", "cancel": "cancelar", @@ -1018,7 +1016,6 @@ "Paste failed:": "Falha na colagem:", "Selection copied to clipboard.": "Seleção copiada para a área de transferência.", "Selection cut to clipboard.": "Seleção cortada para a área de transferência.", - "1 file": "1 arquivo", "Checking…": "Verificando…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "Os documentos excluídos são movidos para a lixeira recuperável. As edições na área de trabalho mantêm o histórico recente dentro do cofre.", "Document unavailable": "Documento indisponível", @@ -1156,5 +1153,20 @@ "Token name": "Nome do token", "Up to 50 GitHub access tokens can be saved.": "Até 50 tokens de acesso do GitHub podem ser salvos.", "Use a unique token name.": "Use um nome de token exclusivo.", - "Move to…": "Mover para…" + "Move to…": "Mover para…", + "Release notes unavailable": "Notas de lançamento indisponíveis", + "Show Release Notes": "Mostrar notas de lançamento", + "The release notes could not be opened.": "Não foi possível abrir as notas de lançamento.", + "Release notes are read only.": "As notas de versão são somente leitura.", + "Release notes opened.": "Notas de lançamento abertas.", + "Back to the top of the release notes": "Voltar ao início das notas de lançamento", + "Back to top": "Voltar ao início", + "Close Release Notes: 3.10.0": "Fechar Notas de Lançamento: 3.10.0", + "File options for Release Notes: 3.10.0": "Opções de arquivo para Notas de Lançamento: 3.10.0", + "In this update": "Nesta atualização", + "release notes": "notas de lançamento", + "Release notes are available in the tab list.": "As notas de versão estão disponíveis na lista de guias.", + "Release Notes: 3.10.0": "Notas de versão: 3.10.0", + "Skip update navigation": "Pular navegação de atualização", + "1 file": "1 arquivo" } diff --git a/assets/i18n/ru.json b/assets/i18n/ru.json index 73cdc9bd..b2e6284f 100644 --- a/assets/i18n/ru.json +++ b/assets/i18n/ru.json @@ -90,7 +90,6 @@ "Capturing document": "Захват документа", "Capturing image": "Захват изображения", "Change text case": "Изменить регистр текста", - "Changelog": "Список изменений", "Chars": "Символы", "Checkbox Map": "Карта флажков", "Checklist Map": "Карта контрольного списка", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Не удалось преобразовать встроенные изображения. Пожалуйста, попробуйте еще раз.", "expanded.": "расширено.", "Image conversion was cancelled because the document changed.": "Преобразование изображения было отменено, поскольку документ изменился.", - "MD": "Доктор медицины", "Open an editable Markdown file before inserting images.": "Прежде чем вставлять изображения, откройте редактируемый файл Markdown.", "uploaded and inserted with short links.": "загружено и вставлено с короткими ссылками.", "cancel": "отменить", @@ -1018,7 +1016,6 @@ "Paste failed:": "Не удалось вставить:", "Selection copied to clipboard.": "Выделение скопировано в буфер обмена.", "Selection cut to clipboard.": "Выделенный фрагмент вырезан в буфер обмена.", - "1 file": "1 файл", "Checking…": "Проверка…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "Удаленные документы перемещаются в восстанавливаемую корзину. Изменения на рабочем столе сохраняют недавнюю историю в хранилище.", "Document unavailable": "Документ недоступен", @@ -1156,5 +1153,20 @@ "Token name": "Имя токена", "Up to 50 GitHub access tokens can be saved.": "Можно сохранить до 50 токенов доступа GitHub.", "Use a unique token name.": "Используйте уникальное имя токена.", - "Move to…": "Перейти к…" + "Move to…": "Перейти к…", + "Release notes unavailable": "Примечания к выпуску недоступны", + "Show Release Notes": "Показать примечания к выпуску", + "The release notes could not be opened.": "Не удалось открыть примечания к выпуску.", + "Release notes are read only.": "Примечания к выпуску доступны только для чтения.", + "Release notes opened.": "Открыты примечания к выпуску.", + "Back to the top of the release notes": "Вернуться к началу примечаний к выпуску", + "Back to top": "Вернуться к началу", + "Close Release Notes: 3.10.0": "Закрыть Примечания к выпуску: 3.10.0", + "File options for Release Notes: 3.10.0": "Параметры файла для примечаний к выпуску: 3.10.0", + "In this update": "В этом обновлении", + "release notes": "примечания к выпуску", + "Release notes are available in the tab list.": "Примечания к выпуску доступны в списке вкладок.", + "Release Notes: 3.10.0": "Примечания к выпуску: 3.10.0", + "Skip update navigation": "Пропустить навигацию по обновлению", + "1 file": "1 файл" } diff --git a/assets/i18n/tr.json b/assets/i18n/tr.json index 88341f2e..9b683e55 100644 --- a/assets/i18n/tr.json +++ b/assets/i18n/tr.json @@ -90,7 +90,6 @@ "Capturing document": "Belge yakalanıyor", "Capturing image": "Görüntü yakalanıyor", "Change text case": "Metnin büyük/küçük harflerini değiştir", - "Changelog": "Değişiklik Günlüğü", "Chars": "Karakterler", "Checkbox Map": "Onay Kutusu Haritası", "Checklist Map": "Kontrol Listesi Haritası", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Gömülü görüntüler dönüştürülemedi. Lütfen tekrar deneyin.", "expanded.": "genişletildi.", "Image conversion was cancelled because the document changed.": "Belge değiştiği için görüntü dönüştürme iptal edildi.", - "MD": "MD", "Open an editable Markdown file before inserting images.": "Görüntüleri eklemeden önce düzenlenebilir bir Markdown dosyasını açın.", "uploaded and inserted with short links.": "kısa bağlantılarla yüklendi ve eklendi.", "cancel": "iptal et", @@ -1018,7 +1016,6 @@ "Paste failed:": "Yapıştırma başarısız oldu:", "Selection copied to clipboard.": "Seçim panoya kopyalandı.", "Selection cut to clipboard.": "Seçim panoya kesildi.", - "1 file": "1 dosya", "Checking…": "Kontrol ediliyor…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "Silinen belgeler kurtarılabilir çöp kutusuna taşınır. Masaüstü düzenlemeleri yakın geçmişi kasanın içinde tutar.", "Document unavailable": "Belge kullanılamıyor", @@ -1156,5 +1153,20 @@ "Token name": "Jeton adı", "Up to 50 GitHub access tokens can be saved.": "En fazla 50 GitHub erişim belirteci kaydedilebilir.", "Use a unique token name.": "Benzersiz bir belirteç adı kullanın.", - "Move to…": "Şuraya git…" + "Move to…": "Şuraya git…", + "Release notes unavailable": "Sürüm notları mevcut değil", + "Show Release Notes": "Sürüm Notlarını Göster", + "The release notes could not be opened.": "Sürüm notları açılamadı.", + "Release notes are read only.": "Sürüm notları salt okunurdur.", + "Release notes opened.": "Sürüm notları açıldı.", + "Back to the top of the release notes": "Sürüm notlarının başına dön", + "Back to top": "Başa dön", + "Close Release Notes: 3.10.0": "Sürüm Notlarını Kapat: 3.10.0", + "File options for Release Notes: 3.10.0": "Sürüm Notları için dosya seçenekleri: 3.10.0", + "In this update": "Bu güncellemede", + "release notes": "sürüm notları", + "Release notes are available in the tab list.": "Sürüm notları sekme listesinde mevcuttur.", + "Release Notes: 3.10.0": "Sürüm Notları: 3.10.0", + "Skip update navigation": "Güncelleme gezinmesini atla", + "1 file": "1 dosya" } diff --git a/assets/i18n/tw.json b/assets/i18n/tw.json index 45eb939e..8b2d93fe 100644 --- a/assets/i18n/tw.json +++ b/assets/i18n/tw.json @@ -90,7 +90,6 @@ "Capturing document": "擷取文檔", "Capturing image": "拍攝影像", "Change text case": "更改文字大小寫", - "Changelog": "變更日誌", "Chars": "字符", "Checkbox Map": "複選框圖", "Checklist Map": "清單地圖", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "無法轉換嵌入影像。請再試一次。", "expanded.": "擴展。", "Image conversion was cancelled because the document changed.": "由於文件更改,影像轉換被取消。", - "MD": "MD", "Open an editable Markdown file before inserting images.": "在插入影像之前開啟可編輯的 Markdown 檔案。", "uploaded and inserted with short links.": "已上傳並插入短連結。", "cancel": "取消", @@ -1018,7 +1016,6 @@ "Paste failed:": "貼上失敗:", "Selection copied to clipboard.": "所選內容已複製到剪貼簿。", "Selection cut to clipboard.": "所選內容剪下到剪貼簿。", - "1 file": "1 份文件", "Checking…": "正在檢查...", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "已刪除的文件將移至可回收垃圾箱。桌面編輯將最近的歷史記錄保留在保管庫內。", "Document unavailable": "文件不可用", @@ -1156,5 +1153,20 @@ "Token name": "代幣名稱", "Up to 50 GitHub access tokens can be saved.": "最多可以保存 50 個 GitHub 存取權杖。", "Use a unique token name.": "使用唯一的令牌名稱。", - "Move to…": "移至..." + "Move to…": "移至...", + "Release notes unavailable": "發行說明不可用", + "Show Release Notes": "顯示發行說明", + "The release notes could not be opened.": "無法開啟發行說明。", + "Release notes are read only.": "發行說明是唯讀的。", + "Release notes opened.": "發行說明已開啟。", + "Back to the top of the release notes": "返回發行說明頂部", + "Back to top": "回頂部", + "Close Release Notes: 3.10.0": "關閉發行說明:3.10.0", + "File options for Release Notes: 3.10.0": "發行說明的檔案選項:3.10.0", + "In this update": "在本次更新中", + "release notes": "發行說明", + "Release notes are available in the tab list.": "發行說明可在選項卡清單中找到。", + "Release Notes: 3.10.0": "發行說明:3.10.0", + "Skip update navigation": "跳過更新導航", + "1 file": "1 份文件" } diff --git a/assets/i18n/uk.json b/assets/i18n/uk.json index 914b3295..c9adfedf 100644 --- a/assets/i18n/uk.json +++ b/assets/i18n/uk.json @@ -90,7 +90,6 @@ "Capturing document": "Запис документа", "Capturing image": "Захоплення зображення", "Change text case": "Змінити регістр тексту", - "Changelog": "Журнал змін", "Chars": "Символи", "Checkbox Map": "Карта прапорців", "Checklist Map": "Карта контрольного списку", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "Вбудовані зображення не вдалося конвертувати. Спробуйте ще раз.", "expanded.": "розширено.", "Image conversion was cancelled because the document changed.": "Перетворення зображення скасовано, оскільки документ змінено.", - "MD": "MD", "Open an editable Markdown file before inserting images.": "Відкрийте редагований файл Markdown перед вставленням зображень.", "uploaded and inserted with short links.": "завантажено та вставлено з короткими посиланнями.", "cancel": "скасувати", @@ -1018,7 +1016,6 @@ "Paste failed:": "Не вдалося вставити:", "Selection copied to clipboard.": "Виділення скопійовано в буфер обміну.", "Selection cut to clipboard.": "Виділення вирізано в буфер обміну.", - "1 file": "1 файл", "Checking…": "Перевірка…", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "Видалені документи переміщуються до кошика для відновлення. Редагування на робочому столі зберігає нещодавню історію в сховищі.", "Document unavailable": "Документ недоступний", @@ -1156,5 +1153,20 @@ "Token name": "Назва маркера", "Up to 50 GitHub access tokens can be saved.": "Можна зберегти до 50 токенів доступу GitHub.", "Use a unique token name.": "Використовуйте унікальну назву маркера.", - "Move to…": "Перейти до…" + "Move to…": "Перейти до…", + "Release notes unavailable": "Примітки до випуску недоступні", + "Show Release Notes": "Показати примітки до випуску", + "The release notes could not be opened.": "Не вдалося відкрити примітки до випуску.", + "Release notes are read only.": "Примітки до випуску доступні лише для читання.", + "Release notes opened.": "Відкрито примітки до випуску.", + "Back to the top of the release notes": "Повернутися до початку приміток до випуску", + "Back to top": "На початок", + "Close Release Notes: 3.10.0": "Закрити Примітки до випуску: 3.10.0", + "File options for Release Notes: 3.10.0": "Параметри файлу для приміток до випуску: 3.10.0", + "In this update": "У цьому оновленні", + "release notes": ".", + "Release notes are available in the tab list.": "Примітки до випуску доступні у списку вкладок.", + "Release Notes: 3.10.0": "Примітки до випуску: 3.10.0", + "Skip update navigation": "Пропустити оновлення навігації", + "1 file": "1 файл" } diff --git a/assets/i18n/zh.json b/assets/i18n/zh.json index 260897be..e16c6555 100644 --- a/assets/i18n/zh.json +++ b/assets/i18n/zh.json @@ -90,7 +90,6 @@ "Capturing document": "捕获文档", "Capturing image": "拍摄图像", "Change text case": "更改文本大小写", - "Changelog": "变更日志", "Chars": "字符", "Checkbox Map": "复选框图", "Checklist Map": "清单地图", @@ -971,7 +970,6 @@ "Embedded images could not be converted. Please try again.": "无法转换嵌入图像。请再试一次。", "expanded.": "扩展。", "Image conversion was cancelled because the document changed.": "由于文档更改,图像转换被取消。", - "MD": "MD", "Open an editable Markdown file before inserting images.": "在插入图像之前打开可编辑的 Markdown 文件。", "uploaded and inserted with short links.": "已上传并插入短链接。", "cancel": "取消", @@ -1018,7 +1016,6 @@ "Paste failed:": "粘贴失败:", "Selection copied to clipboard.": "所选内容已复制到剪贴板。", "Selection cut to clipboard.": "所选内容剪切到剪贴板。", - "1 file": "1 个文件", "Checking…": "正在检查...", "Deleted documents are moved to recoverable trash. Desktop edits keep recent history inside the vault.": "已删除的文档将移至可回收垃圾箱。桌面编辑将最近的历史记录保留在保管库内。", "Document unavailable": "文档不可用", @@ -1156,5 +1153,20 @@ "Token name": "代币名称", "Up to 50 GitHub access tokens can be saved.": "最多可以保存 50 个 GitHub 访问令牌。", "Use a unique token name.": "使用唯一的令牌名称。", - "Move to…": "移至..." + "Move to…": "移至...", + "Release notes unavailable": "发行说明不可用", + "Show Release Notes": "显示发行说明", + "The release notes could not be opened.": "无法打开发行说明。", + "Release notes are read only.": "发行说明是只读的。", + "Release notes opened.": "发行说明已打开。", + "Back to the top of the release notes": "返回发行说明顶部", + "Back to top": "回到顶部", + "Close Release Notes: 3.10.0": "关闭发行说明:3.10.0", + "File options for Release Notes: 3.10.0": "发行说明的文件选项:3.10.0", + "In this update": "在本次更新中", + "release notes": "发行说明", + "Release notes are available in the tab list.": "发行说明可在选项卡列表中找到。", + "Release Notes: 3.10.0": "发行说明:3.10.0", + "Skip update navigation": "跳过更新导航", + "1 file": "1 个文件" } diff --git a/desktop-app/prepare.js b/desktop-app/prepare.js index 9e1f2d5a..cdef4414 100644 --- a/desktop-app/prepare.js +++ b/desktop-app/prepare.js @@ -55,6 +55,9 @@ console.log("Copied preview-worker.js to resources/js/preview-worker.js"); fs.copyFileSync(path.join(ROOT_DIR, "styles.css"), path.join(RESOURCES_DIR, "styles.css")); console.log("✓ Copied styles.css → resources/styles.css"); +fs.copyFileSync(path.join(ROOT_DIR, "RELEASE_NOTES"), path.join(RESOURCES_DIR, "RELEASE_NOTES")); +console.log("Copied RELEASE_NOTES to resources/RELEASE_NOTES"); + // PERF-027: Exclude large demo assets (GIFs) from desktop build to reduce binary size copyDirSync(path.join(ROOT_DIR, "assets"), path.join(RESOURCES_DIR, "assets"), [/\.gif$/i]); console.log("✓ Copied assets/ → resources/assets/ (excluding GIF demos)"); diff --git a/desktop-app/resources/RELEASE_NOTES b/desktop-app/resources/RELEASE_NOTES new file mode 100644 index 00000000..046ffc1a --- /dev/null +++ b/desktop-app/resources/RELEASE_NOTES @@ -0,0 +1,83 @@ +![Markdown Viewer logo](assets/icon.jpg) **Markdown Viewer release** · `v3.10.0` + +# Welcome to Markdown Viewer 3.10.0 + +_Released August 2, 2026_ + +This release gives your workspace room to grow, makes complete backups practical, and rebuilds GitHub importing for large and private repositories. + +Everything here ships with the web, PWA, Docker, and desktop editions. **Happy coding!** + +[View release on GitHub](https://github.com/ThisIs-Developer/Markdown-Viewer/releases/tag/v3.10.0) [Open the full changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md) + +--- + +## Highlights + +- **A workspace without a document-count ceiling.** Documents now use scalable per-document storage instead of one increasingly large browser record. +- **Complete backup and recovery.** Export and restore folder-preserving ZIP backups on the web and desktop. +- **A rebuilt GitHub importer.** Discover every Markdown file, work with branches and tags reliably, and save private-repository access securely. +- **Polish across the product.** Faster bulk tab closing, clearer staged loading, responsive import selection, and an audit of all 14 interface languages. + +> [!TIP] +> Start with **Workspace storage** if you manage a large library, or jump to **GitHub import** if your documentation lives in repositories. + +## Workspace storage + +Markdown Viewer no longer limits a workspace to a fixed number of documents. Browser documents are stored as individual IndexedDB records, loaded only when needed, retained in a bounded memory cache, and rendered in paged Explorer groups. + +Desktop documents remain ordinary Markdown files in the fixed **Documents/Markdown Viewer Vault** location, so the vault is predictable and easy to inspect outside the application. + +### What changed + +- Removed the former document-count limit. +- Added on-demand document loading and a bounded cache. +- Added paged Explorer rendering for large workspaces. +- Added exact storage usage and document totals. +- Preserved a normal Markdown-file vault for desktop users. + +## Backup and recovery + +Workspace backup now preserves documents and folder organization in a ZIP file. The same archive can restore a workspace on the web or desktop, with clearer confirmations before imports and destructive resets. + +Secret Workspace files can be included as encrypted data, while Private mode continues to pause persistence without silently removing saved work. + +> [!IMPORTANT] +> A workspace reset removes documents, settings, and release-note visibility data. After a reset, Markdown Viewer treats the next launch as a first run and shows Welcome with this release-note tab beside it. + +## GitHub import + +The importer now finds all Markdown files even when GitHub truncates a large repository tree. It resolves branches, tags, slash-containing references, and commit selections to immutable SHAs before importing. + +Private access supports up to 50 named fine-grained or classic personal access tokens. Saved credentials stay in a local AES-GCM-encrypted vault and are sent only to `api.github.com` for the selected repository operation. + +### Import workflow improvements + +- Search and select files in a responsive picker. +- Import files into Explorer without opening a flood of tabs. +- See staged progress and clearer success or failure feedback. +- Distinguish repository folders and their expanded state more easily. +- Keep private credentials named, encrypted, and removable. + +## Interface, languages, and desktop + +The release completes a visual and localization pass across the importer and workspace. All 14 interface catalogs were audited, desktop resources were synchronized with the web build, and tagged prereleases now publish with the correct metadata. + +Bulk tab closing is batched so large sessions remain responsive, and small-screen import selection retains comfortable touch targets. + +## Thank you + +### Contributors + +- **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** — Design, implementation, testing, documentation, and release work. + +### Changes included + +- [#230 — Unlimited storage, backup, reset, and fixed vault](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/230) +- [#231 — Preserve the complete #230 commit history](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/231) +- [#232 — Refine storage documentation highlights](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/232) +- [#233 — Scale GitHub imports and simplify private access](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/233) + +Thank you for using Markdown Viewer and helping make every release more useful. For the complete history, read the [project changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md). + +**Happy coding!** diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index cfc05dc4..36e655ce 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -845,12 +845,12 @@

Markdown Viewer

GitHub Repository - - Changelog - Apache License 2.0 + FAQ diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index c47a5626..b2ae2e6e 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -1,6 +1,12 @@ document.addEventListener("DOMContentLoaded", async function () { window.alert = function(message) { showAppToast(message); }; const PRIVATE_MODE_KEY = 'markdownViewerPrivateMode'; + const APP_VERSION = '3.10.0'; + const RELEASE_NOTES_TAB_KIND = 'release-notes'; + const RELEASE_NOTES_LAST_VERSION_KEY = 'markdownViewerLastVersion'; + const RELEASE_NOTES_PENDING_VERSION_KEY = 'markdownViewerPendingReleaseNotesVersion'; + const RELEASE_NOTES_PENDING_MODE_KEY = 'markdownViewerPendingReleaseNotesMode'; + const RELEASE_NOTES_SEEN_VERSION_KEY = 'markdownViewerReleaseNotesSeenVersion'; const DOCUMENT_STORAGE_KEYS = new Set([ 'markdownViewerGlobalState', 'markdownViewerTabs', @@ -46,6 +52,10 @@ document.addEventListener("DOMContentLoaded", async function () { 'markdownViewerUntitledCounter', 'markdownViewerDocumentOrganization', 'markdownViewerSecretWorkspace', + RELEASE_NOTES_LAST_VERSION_KEY, + RELEASE_NOTES_PENDING_VERSION_KEY, + RELEASE_NOTES_PENDING_MODE_KEY, + RELEASE_NOTES_SEEN_VERSION_KEY, 'find-replace-docked', 'app-lang' ]; @@ -288,7 +298,6 @@ document.addEventListener("DOMContentLoaded", async function () { let currentViewMode = 'split'; // 'editor', 'split', or 'preview' const shareSnapshotViewOnlyTabIds = new Set(); const SHARE_SNAPSHOT_TAB_KIND = 'share-snapshot'; - const APP_VERSION = '3.10.0'; const REVIEW_TARGET_SELECTOR = 'h1, h2, h3, h4, h5, h6, p, pre, .frontmatter-table, .diagram-viewer, .geojson-container, .topojson-container, .stl-container'; const REVIEW_TEXT_LIMIT = 2000; let reviewModeActive = false; @@ -429,6 +438,278 @@ document.addEventListener("DOMContentLoaded", async function () { return Boolean(activeTabId && isShareSnapshotTabId(activeTabId)); } + function isReleaseNotesTab(tab) { + return Boolean(tab && tab.kind === RELEASE_NOTES_TAB_KIND); + } + + function isReleaseNotesActive() { + return Boolean(activeTabId && tabs.some(function(tab) { + return tab.id === activeTabId && isReleaseNotesTab(tab); + })); + } + + function readReleaseNotesStorage(key) { + try { + return localStorage.getItem(key) || ''; + } catch (_) { + return ''; + } + } + + function markReleaseNotesSeen(tab) { + if (!isReleaseNotesTab(tab)) return; + const version = tab.releaseVersion || APP_VERSION; + saveStorageItem(RELEASE_NOTES_SEEN_VERSION_KEY, version); + if (readReleaseNotesStorage(RELEASE_NOTES_PENDING_VERSION_KEY) === version) { + removeStorageItem(RELEASE_NOTES_PENDING_VERSION_KEY); + removeStorageItem(RELEASE_NOTES_PENDING_MODE_KEY); + } + } + + function queueCurrentReleaseNotes(hadExistingWorkspace) { + const previousVersion = readReleaseNotesStorage(RELEASE_NOTES_LAST_VERSION_KEY); + const seenVersion = readReleaseNotesStorage(RELEASE_NOTES_SEEN_VERSION_KEY); + let pendingVersion = readReleaseNotesStorage(RELEASE_NOTES_PENDING_VERSION_KEY); + let pendingMode = readReleaseNotesStorage(RELEASE_NOTES_PENDING_MODE_KEY); + const firstRun = Boolean(!previousVersion && !hadExistingWorkspace); + const versionChanged = Boolean(previousVersion && previousVersion !== APP_VERSION); + const featureUpgrade = Boolean(!previousVersion && hadExistingWorkspace); + + if ((firstRun || versionChanged || featureUpgrade) && seenVersion !== APP_VERSION) { + pendingVersion = APP_VERSION; + pendingMode = firstRun ? 'background' : 'foreground'; + saveStorageItem(RELEASE_NOTES_PENDING_VERSION_KEY, APP_VERSION); + saveStorageItem(RELEASE_NOTES_PENDING_MODE_KEY, pendingMode); + } + saveStorageItem(RELEASE_NOTES_LAST_VERSION_KEY, APP_VERSION); + return { + shouldOpen: pendingVersion === APP_VERSION && seenVersion !== APP_VERSION, + activate: pendingMode !== 'background' + }; + } + + let releaseNotesContentPromise = null; + let releaseNotesPreviewCleanup = null; + let releaseNotesScrollFrame = null; + + function clearReleaseNotesPreviewEnhancements() { + if (releaseNotesPreviewCleanup) { + releaseNotesPreviewCleanup(); + releaseNotesPreviewCleanup = null; + } + if (releaseNotesScrollFrame !== null) { + cancelAnimationFrame(releaseNotesScrollFrame); + releaseNotesScrollFrame = null; + } + if (previewPane) previewPane.classList.remove('release-notes-surface'); + if (markdownPreview) markdownPreview.classList.remove('release-notes-preview'); + } + + function enhanceReleaseNotesPreview() { + clearReleaseNotesPreviewEnhancements(); + if (!isReleaseNotesActive() || !markdownPreview || !previewPane) return; + + let shell = markdownPreview.querySelector('.release-note-shell'); + if (!shell) { + const sourceNodes = Array.from(markdownPreview.childNodes); + const firstSectionIndex = sourceNodes.findIndex(function(node) { + return node.nodeType === Node.ELEMENT_NODE && node.tagName === 'H2'; + }); + if (firstSectionIndex < 0) return; + + shell = document.createElement('article'); + shell.className = 'release-note-shell'; + shell.setAttribute('aria-label', 'Markdown Viewer ' + APP_VERSION + ' release notes'); + + const intro = document.createElement('header'); + intro.className = 'release-note-intro'; + sourceNodes.slice(0, firstSectionIndex).forEach(function(node) { + intro.appendChild(node); + }); + + const introParagraphs = Array.from(intro.children).filter(function(node) { + return node.tagName === 'P'; + }); + const brandLine = introParagraphs.find(function(paragraph) { + return Boolean(paragraph.querySelector('img[src$="assets/icon.jpg"]')); + }); + if (brandLine) brandLine.classList.add('release-note-brandline'); + + const releaseDate = introParagraphs.find(function(paragraph) { + return /^Released\s/i.test(paragraph.textContent.trim()); + }); + if (releaseDate) releaseDate.classList.add('release-note-release-date'); + + const releaseLink = intro.querySelector('a[href*="/releases/tag/"]'); + const changelogLink = intro.querySelector('a[href*="/CHANGELOG.md"]'); + if (releaseLink && changelogLink && releaseLink.parentElement === changelogLink.parentElement) { + releaseLink.parentElement.classList.add('release-note-actions'); + const releaseIcon = document.createElement('i'); + releaseIcon.className = 'bi bi-github'; + releaseIcon.setAttribute('aria-hidden', 'true'); + releaseLink.prepend(releaseIcon); + const changelogIcon = document.createElement('i'); + changelogIcon.className = 'lucide lucide-history'; + changelogIcon.setAttribute('aria-hidden', 'true'); + changelogLink.prepend(changelogIcon); + } + + const layout = document.createElement('div'); + layout.className = 'release-note-layout'; + const navigation = document.createElement('nav'); + navigation.className = 'release-note-navigation'; + navigation.setAttribute('aria-label', 'In this update'); + const navigationLabel = document.createElement('p'); + navigationLabel.className = 'release-note-navigation-label'; + navigationLabel.textContent = 'In this update'; + navigation.appendChild(navigationLabel); + + const content = document.createElement('div'); + content.className = 'release-note-content'; + content.id = 'release-note-content'; + let currentSection = null; + sourceNodes.slice(firstSectionIndex).forEach(function(node) { + if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'H2') { + currentSection = document.createElement('section'); + currentSection.className = 'release-note-section'; + currentSection.id = node.id || ('release-note-section-' + content.children.length); + node.id = currentSection.id + '-heading'; + currentSection.setAttribute('aria-labelledby', node.id); + currentSection.appendChild(node); + content.appendChild(currentSection); + + const link = document.createElement('a'); + link.href = '#' + currentSection.id; + link.textContent = node.textContent || 'Section'; + navigation.appendChild(link); + return; + } + if (currentSection) currentSection.appendChild(node); + }); + + const skipLink = document.createElement('a'); + skipLink.className = 'release-note-skip-link'; + skipLink.href = '#release-note-content'; + skipLink.textContent = 'Skip update navigation'; + layout.appendChild(skipLink); + layout.appendChild(navigation); + layout.appendChild(content); + shell.appendChild(intro); + shell.appendChild(layout); + + const topLink = document.createElement('a'); + topLink.className = 'release-note-back-to-top'; + const topTarget = intro.querySelector('h1'); + if (topTarget && !topTarget.id) topTarget.id = 'release-note-top'; + topLink.href = '#' + (topTarget && topTarget.id ? topTarget.id : 'release-note-content'); + topLink.setAttribute('aria-label', 'Back to the top of the release notes'); + topLink.title = 'Back to top'; + topLink.innerHTML = ''; + shell.appendChild(topLink); + markdownPreview.replaceChildren(shell); + } + + previewPane.classList.add('release-notes-surface'); + markdownPreview.classList.add('release-notes-preview'); + const links = Array.from(shell.querySelectorAll('.release-note-navigation a[href^="#"]')); + const sections = links.map(function(link) { + const id = decodeURIComponent((link.getAttribute('href') || '').slice(1)); + return { link: link, section: id ? shell.querySelector('#' + CSS.escape(id)) : null }; + }).filter(function(item) { + return Boolean(item.section); + }); + + function updateActiveReleaseNotesSection() { + releaseNotesScrollFrame = null; + if (!isReleaseNotesActive() || !document.body.contains(shell) || sections.length === 0) return; + const paneTop = previewPane.getBoundingClientRect().top; + const activationLine = paneTop + Math.min(112, previewPane.clientHeight * 0.24); + let current = sections[0]; + sections.forEach(function(item) { + if (item.section.getBoundingClientRect().top <= activationLine) current = item; + }); + if (previewPane.scrollTop + previewPane.clientHeight >= previewPane.scrollHeight - 8) { + current = sections[sections.length - 1]; + } + sections.forEach(function(item) { + const active = item === current; + item.link.classList.toggle('is-active', active); + if (active) item.link.setAttribute('aria-current', 'location'); + else item.link.removeAttribute('aria-current'); + }); + } + + function scheduleReleaseNotesSectionUpdate() { + if (releaseNotesScrollFrame !== null) return; + releaseNotesScrollFrame = requestAnimationFrame(updateActiveReleaseNotesSection); + } + + const backToTopLink = shell.querySelector('.release-note-back-to-top'); + function handleReleaseNotesBackToTop(event) { + event.preventDefault(); + event.stopPropagation(); + const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + previewPane.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' }); + } + + previewPane.addEventListener('scroll', scheduleReleaseNotesSectionUpdate, { passive: true }); + window.addEventListener('resize', scheduleReleaseNotesSectionUpdate); + if (backToTopLink) backToTopLink.addEventListener('click', handleReleaseNotesBackToTop); + releaseNotesPreviewCleanup = function() { + previewPane.removeEventListener('scroll', scheduleReleaseNotesSectionUpdate); + window.removeEventListener('resize', scheduleReleaseNotesSectionUpdate); + if (backToTopLink) backToTopLink.removeEventListener('click', handleReleaseNotesBackToTop); + }; + scheduleReleaseNotesSectionUpdate(); + } + + function loadCurrentReleaseNotes() { + if (!releaseNotesContentPromise) { + releaseNotesContentPromise = fetch('RELEASE_NOTES').then(function(response) { + if (!response.ok) throw new Error('Release notes returned ' + response.status); + return response.text(); + }).catch(function(error) { + console.warn('Failed to load release notes:', error); + return '# Markdown Viewer ' + APP_VERSION + '\n\nThe release notes could not be loaded. Visit the [project releases](https://github.com/ThisIs-Developer/Markdown-Viewer/releases) for the latest changes.'; + }); + } + return releaseNotesContentPromise; + } + + async function openReleaseNotes(options) { + const settings = options || {}; + const shouldActivate = settings.activate !== false; + const existing = tabs.find(function(tab) { + return isReleaseNotesTab(tab) && tab.releaseVersion === APP_VERSION; + }); + if (existing) { + if (shouldActivate) { + await switchTab(existing.id); + updateLiveEditorAccess(); + } else { + renderTabBar(tabs, activeTabId); + } + return existing; + } + + const content = await loadCurrentReleaseNotes(); + const tab = createTab(content, 'Release Notes: ' + APP_VERSION, 'preview'); + tab.kind = RELEASE_NOTES_TAB_KIND; + tab.temporary = true; + tab.releaseVersion = APP_VERSION; + tabs.push(tab); + if (shouldActivate) { + await switchTab(tab.id); + updateLiveEditorAccess(); + } else { + renderTabBar(tabs, activeTabId); + } + if (!settings.silent) { + announceToScreenReader(shouldActivate ? 'Release notes opened.' : 'Release notes are available in the tab list.'); + } + return tab; + } + function stripTemporaryTabs(tabsArr) { return (tabsArr || []).filter(function(tab) { return !isShareSnapshotTab(tab) && tab.temporary !== true; @@ -481,6 +762,9 @@ document.addEventListener("DOMContentLoaded", async function () { if (isShareSnapshotViewOnlyActive()) { return 'This shared snapshot is view only.'; } + if (isReleaseNotesActive()) { + return 'Release notes are read only.'; + } if (isLiveViewOnlyParticipant()) { return 'This Live Share session is view only.'; } @@ -592,6 +876,7 @@ document.addEventListener("DOMContentLoaded", async function () { const aboutModalClose = document.getElementById("about-modal-close"); const aboutModalCloseIcon = document.getElementById("about-modal-close-icon"); const aboutVersion = document.getElementById("about-version"); + const aboutReleaseNotes = document.getElementById("about-release-notes"); const privateModeToggle = document.getElementById("private-mode-toggle"); const storageSettingsButton = document.getElementById("storage-settings-button"); const storageSettingsModal = document.getElementById("storage-settings-modal"); @@ -3856,7 +4141,8 @@ document.addEventListener("DOMContentLoaded", async function () { } function getDocumentCountForLimit() { - return tabs.length + (isSecretWorkspaceUnlocked() ? 0 : secretWorkspaceDocumentCount); + return tabs.filter(function(tab) { return !isReleaseNotesTab(tab); }).length + + (isSecretWorkspaceUnlocked() ? 0 : secretWorkspaceDocumentCount); } function getWorkspaceById(workspaceId) { @@ -5404,7 +5690,9 @@ document.addEventListener("DOMContentLoaded", async function () { function renderWorkspaceTree(tree) { let renderedDocuments = 0; - const temporaryTabs = tabs.filter(isTemporaryDocument).filter(documentMatchesSidebarSearch); + const temporaryTabs = tabs.filter(function(tab) { + return isTemporaryDocument(tab) && !isReleaseNotesTab(tab); + }).filter(documentMatchesSidebarSearch); if (temporaryTabs.length) { const temporaryLabel = document.createElement('p'); temporaryLabel.className = 'document-tree-section-label'; @@ -5569,6 +5857,7 @@ document.addEventListener("DOMContentLoaded", async function () { let renderedCount = filter === 'all' ? renderWorkspaceTree(tree) : renderFlatDocumentView(tree, filter); documentSidebarRenderBudget = null; let visibleDocumentCount = tabs.filter(function(tab) { + if (isReleaseNotesTab(tab)) return false; if (filter !== 'all' && isTemporaryDocument(tab)) return false; if (filter === 'favorites' && !tab.favorite) return false; return documentMatchesSidebarSearch(tab); @@ -7725,29 +8014,33 @@ document.addEventListener("DOMContentLoaded", async function () { dropdown.className = 'tab-menu-dropdown'; dropdown.setAttribute('data-tab-menu-dropdown', 'true'); dropdown.setAttribute('role', 'menu'); - const duplicateAction = isShareSnapshotTab(tab) - ? '' - : ''; - const downloadAction = isShareSnapshotTab(tab) - ? '' - : ''; - const favoriteAction = isShareSnapshotTab(tab) - ? '' - : ''; - const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); - const splitAction = isCombinedSplitTab - ? '' - : tabs.length > 1 - ? '' - : ''; - dropdown.innerHTML = - '' + - duplicateAction + - favoriteAction + - splitAction + - downloadAction + - '' + - ''; + if (isReleaseNotesTab(tab)) { + dropdown.innerHTML = ''; + } else { + const duplicateAction = isShareSnapshotTab(tab) + ? '' + : ''; + const downloadAction = isShareSnapshotTab(tab) + ? '' + : ''; + const favoriteAction = isShareSnapshotTab(tab) + ? '' + : ''; + const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); + const splitAction = isCombinedSplitTab + ? '' + : tabs.length > 1 + ? '' + : ''; + dropdown.innerHTML = + '' + + duplicateAction + + favoriteAction + + splitAction + + downloadAction + + '' + + ''; + } menuBtn.addEventListener('click', function(e) { e.preventDefault(); @@ -7846,6 +8139,7 @@ document.addEventListener("DOMContentLoaded", async function () { if (!idsToClose.has(tab.id) || !isTabOpen(tab)) return; if (isTemporaryDocument(tab)) { temporaryIds.add(tab.id); + markReleaseNotesSeen(tab); shareSnapshotViewOnlyTabIds.delete(tab.id); if (tabHistories[tab.id]) delete tabHistories[tab.id]; return; @@ -7891,14 +8185,7 @@ document.addEventListener("DOMContentLoaded", async function () { menu.setAttribute('aria-label', 'Tab actions for ' + (tab.title || 'Untitled')); const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); - const actions = [{ - id: isCombinedSplitTab ? 'split-close' : 'split', - icon: isCombinedSplitTab ? 'lucide-panel-right-close' : 'lucide-columns-2', - label: isCombinedSplitTab ? 'Exit split view' : 'Open in split view', - disabled: !isCombinedSplitTab && tabs.length < 2 - }, { - separator: true - }, { + const closeActions = [{ id: 'close', icon: 'lucide-x', label: 'Close' }, { id: 'others', icon: 'lucide-circle-x', label: 'Close others', disabled: openTabs.length < 2 @@ -7909,6 +8196,14 @@ document.addEventListener("DOMContentLoaded", async function () { }, { id: 'all', icon: 'lucide-square-x', label: 'Close all' }]; + const actions = isReleaseNotesTab(tab) ? closeActions : [{ + id: isCombinedSplitTab ? 'split-close' : 'split', + icon: isCombinedSplitTab ? 'lucide-panel-right-close' : 'lucide-columns-2', + label: isCombinedSplitTab ? 'Exit split view' : 'Open in split view', + disabled: !isCombinedSplitTab && tabs.length < 2 + }, { + separator: true + }].concat(closeActions); actions.forEach(function(action) { if (action.separator) { @@ -7988,7 +8283,7 @@ document.addEventListener("DOMContentLoaded", async function () { ? tabs.find(function(item) { return item.id === secondarySplitTabId && isTabOpen(item); }) : null; const item = document.createElement('div'); - item.className = 'tab-item' + (tab.id === currentActiveTabId ? ' active' : '') + (splitPartner ? ' is-document-split' : ''); + item.className = 'tab-item' + (tab.id === currentActiveTabId ? ' active' : '') + (splitPartner ? ' is-document-split' : '') + (isReleaseNotesTab(tab) ? ' release-notes-tab' : ''); item.setAttribute('data-tab-id', tab.id); if (splitPartner) item.setAttribute('data-split-tab-id', splitPartner.id); item.setAttribute('role', 'tab'); @@ -8002,9 +8297,18 @@ document.addEventListener("DOMContentLoaded", async function () { item.style.setProperty('--split-tab-preferred-width', Math.round(preferredWidth) + 'px'); } - const fileIcon = document.createElement('i'); - fileIcon.className = 'lucide ' + (splitPartner ? 'lucide-columns-2' : 'lucide-file-text') + ' tab-file-icon'; - fileIcon.setAttribute('aria-hidden', 'true'); + let fileIcon; + if (isReleaseNotesTab(tab) && !splitPartner) { + fileIcon = document.createElement('img'); + fileIcon.className = 'tab-file-icon tab-app-icon'; + fileIcon.src = 'assets/icon.jpg'; + fileIcon.alt = ''; + fileIcon.setAttribute('aria-hidden', 'true'); + } else { + fileIcon = document.createElement('i'); + fileIcon.className = 'lucide ' + (splitPartner ? 'lucide-columns-2' : 'lucide-file-text') + ' tab-file-icon'; + fileIcon.setAttribute('aria-hidden', 'true'); + } const titleSpan = document.createElement('span'); titleSpan.className = 'tab-title' + (splitPartner ? ' tab-split-titles' : ''); @@ -8283,7 +8587,7 @@ document.addEventListener("DOMContentLoaded", async function () { if (contentChanged && !isTemporaryDocument(tab)) { tab.lastEditedAt = Date.now(); } - saveTabsToStorage(tabs, [tab.id]); + if (!isTemporaryDocument(tab)) saveTabsToStorage(tabs, [tab.id]); if (contentChanged) renderDocumentSidebar(); } @@ -8418,7 +8722,7 @@ document.addEventListener("DOMContentLoaded", async function () { documentSplitEditor.hidden = previewMode; documentSplitPreview.hidden = !previewMode; if (documentSplitEditor.value !== tab.content) documentSplitEditor.value = tab.content || ''; - documentSplitEditor.readOnly = shareSnapshotViewOnlyTabIds.has(tab.id); + documentSplitEditor.readOnly = shareSnapshotViewOnlyTabIds.has(tab.id) || isReleaseNotesTab(tab); documentSplitEditor.setAttribute('aria-label', 'Edit ' + (tab.title || 'Untitled') + ' in split view'); documentSplitPreview.setAttribute('aria-label', 'Preview ' + (tab.title || 'Untitled') + ' in split view'); if (previewMode) renderDocumentSplitPreview(tab); @@ -8477,7 +8781,7 @@ document.addEventListener("DOMContentLoaded", async function () { async function openDocumentSplitView(sourceTabId, secondTabId) { const sourceTab = tabs.find(function(item) { return item.id === sourceTabId; }); const secondTab = tabs.find(function(item) { return item.id === secondTabId; }); - if (!sourceTab || !secondTab || sourceTab.id === secondTab.id) return; + if (!sourceTab || !secondTab || sourceTab.id === secondTab.id || isReleaseNotesTab(sourceTab) || isReleaseNotesTab(secondTab)) return; try { await Promise.all([ensureTabContent(sourceTab), ensureTabContent(secondTab)]); } catch (_) { @@ -8499,8 +8803,8 @@ document.addEventListener("DOMContentLoaded", async function () { function openDocumentSplitPicker(sourceTabId) { const sourceTab = tabs.find(function(item) { return item.id === sourceTabId; }); - const candidates = tabs.filter(function(item) { return item.id !== sourceTabId; }); - if (!sourceTab || candidates.length === 0) { + const candidates = tabs.filter(function(item) { return item.id !== sourceTabId && !isReleaseNotesTab(item); }); + if (!sourceTab || isReleaseNotesTab(sourceTab) || candidates.length === 0) { alert('Open or create another document before starting split view.'); return; } @@ -8682,6 +8986,7 @@ document.addEventListener("DOMContentLoaded", async function () { const idx = tabs.findIndex(function(t) { return t.id === tabId; }); if (idx === -1) return; + const tab = tabs[idx]; const wasActive = activeTabId === tabId; const splitPartnerId = wasActive ? secondarySplitTabId : null; if (wasActive) { @@ -8694,6 +8999,7 @@ document.addEventListener("DOMContentLoaded", async function () { closeDocumentSplitView({ silent: true, renderTabs: false }); } shareSnapshotViewOnlyTabIds.delete(tabId); + markReleaseNotesSeen(tab); // Clean up history of the closed tab if (tabHistories[tabId]) { @@ -9003,6 +9309,7 @@ document.addEventListener("DOMContentLoaded", async function () { documentOrganization = await loadDocumentOrganization(); initializeSecretWorkspaceState(); tabs = await loadTabsFromStorage(); + const hadExistingWorkspace = tabs.length > 0; activeTabId = loadActiveTabId(); // Check if Neutralino passed an initial file via command line (early load) @@ -9103,6 +9410,7 @@ document.addEventListener("DOMContentLoaded", async function () { openDocumentSurfaceContextMenu(surface, event); }); }); + return hadExistingWorkspace; } // Late-load callback hook for Neutralino command-line files @@ -10788,6 +11096,8 @@ ${selector} .arrowheadPath { enhancePreviewCodeBlocks(root); }); + enhanceReleaseNotesPreview(); + queryPreviewRoots(roots, 'input[type="checkbox"]').forEach(function(input) { if (!input.hasAttribute('aria-label')) { const parentText = input.parentElement ? input.parentElement.textContent.trim() : ''; @@ -13080,7 +13390,7 @@ ${selector} .arrowheadPath { // View Mode Functions - Story 1.1 & 1.2 function setViewMode(mode) { if (secondarySplitTabId && mode === 'split') mode = 'editor'; - if (isShareSnapshotViewOnlyActive() && mode !== 'preview') { + if ((isShareSnapshotViewOnlyActive() || isReleaseNotesActive()) && mode !== 'preview') { mode = 'preview'; announceToScreenReader(getEditorReadOnlyMessage()); } @@ -17973,6 +18283,18 @@ ${selector} .arrowheadPath { if (aboutModalCloseIcon) { aboutModalCloseIcon.addEventListener('click', function() { closeAppModal(aboutModal); }); } + if (aboutReleaseNotes) { + aboutReleaseNotes.addEventListener('click', function() { + closeAppModal(aboutModal); + openReleaseNotes().catch(function(error) { + console.warn('Failed to open release notes:', error); + showAppToast('The release notes could not be opened.', { + tone: 'error', + title: 'Release notes unavailable' + }); + }); + }); + } } function openHelpModal() { @@ -18686,8 +19008,9 @@ ${selector} .arrowheadPath { }); } - await initTabs(); + const hadExistingWorkspace = await initTabs(); initReviewMode(); + const releaseNotesLaunch = queueCurrentReleaseNotes(hadExistingWorkspace); if (loadGlobalState().syncScrollingEnabled === false) toggleSyncScrolling(); updateMobileStats(); updateFindHighlights(); @@ -19201,6 +19524,7 @@ ${selector} .arrowheadPath { exportMd.addEventListener("click", function () { if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; if (blockShareSnapshotSourceAccess()) return; if (typeof Neutralino !== 'undefined') { nativeSaveMarkdown(); @@ -19219,6 +19543,7 @@ ${selector} .arrowheadPath { exportHtml.addEventListener("click", function () { if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; try { const { frontmatter, body } = parseFrontmatter(markdownEditor.value); const tableHtml = frontmatter ? renderFrontmatterTable(frontmatter) : ''; @@ -20581,6 +20906,7 @@ ${selector} .arrowheadPath { exportPdf.addEventListener("click", function (event) { event.preventDefault(); if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; openAppModal(pdfExportModal); }); @@ -20933,6 +21259,7 @@ ${selector} .arrowheadPath { exportPng.addEventListener("click", async function (event) { event.preventDefault(); if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; logPdfExportDebug("PNG export button clicked!"); if (activePdfExport) { logPdfExportDebug("Export already active, ignoring click"); @@ -21432,6 +21759,7 @@ ${selector} .arrowheadPath { function openShareModal() { if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; if (blockTemporarySnapshotShare()) return; if (blockLiveDocumentShareSnapshot()) return; // PERF-002: Lazy-load pako on first share @@ -21773,7 +22101,7 @@ ${selector} .arrowheadPath { } function canMutateEditor() { - return hasActiveOpenDocument() && !reviewModeActive && !isLiveViewOnlyParticipant() && !isShareSnapshotViewOnlyActive(); + return hasActiveOpenDocument() && !reviewModeActive && !isLiveViewOnlyParticipant() && !isShareSnapshotViewOnlyActive() && !isReleaseNotesActive(); } function isLiveMutatingAction(action) { @@ -21806,12 +22134,86 @@ ${selector} .arrowheadPath { setSelectedLiveAccessMode(mode); } + function updateReleaseNotesActionAvailability(releaseNotesActive) { + const actions = [ + exportDropdown, + exportMd, + exportHtml, + exportPdf, + exportPng, + shareButton, + liveShareButton, + mobileExportToggle, + mobileExportMd, + mobileExportHtml, + mobileExportPdf, + mobileExportPng, + mobileShareButton, + mobileLiveShareButton + ]; + + actions.forEach(function(action) { + if (!action) return; + if (releaseNotesActive) { + if (action.dataset.releaseNotesDisabled !== 'true') { + action.dataset.releaseNotesWasDisabled = action.disabled ? 'true' : 'false'; + if (action.tagName === 'A') { + action.dataset.releaseNotesOriginalTabindex = action.hasAttribute('tabindex') + ? action.getAttribute('tabindex') + : '__missing__'; + } + } + action.dataset.releaseNotesDisabled = 'true'; + if ('disabled' in action) action.disabled = true; + action.classList.add('disabled'); + action.setAttribute('aria-disabled', 'true'); + if (action.tagName === 'A') action.setAttribute('tabindex', '-1'); + return; + } + + if (action.dataset.releaseNotesDisabled !== 'true') return; + const wasDisabled = action.dataset.releaseNotesWasDisabled === 'true'; + const hasAnotherDisabledState = Object.keys(action.dataset).some(function(key) { + return key !== 'releaseNotesDisabled' && + key !== 'releaseNotesWasDisabled' && + /Disabled$/.test(key) && + action.dataset[key] === 'true'; + }); + const remainsDisabled = wasDisabled || hasAnotherDisabledState; + delete action.dataset.releaseNotesDisabled; + delete action.dataset.releaseNotesWasDisabled; + if ('disabled' in action) action.disabled = remainsDisabled; + action.classList.toggle('disabled', remainsDisabled); + action.setAttribute('aria-disabled', remainsDisabled ? 'true' : 'false'); + if (action.tagName === 'A') { + const originalTabindex = action.dataset.releaseNotesOriginalTabindex; + delete action.dataset.releaseNotesOriginalTabindex; + if (originalTabindex && originalTabindex !== '__missing__') { + action.setAttribute('tabindex', originalTabindex); + } else { + action.removeAttribute('tabindex'); + } + } + }); + + if (!releaseNotesActive) return; + if (exportDropdown) { + exportDropdown.setAttribute('aria-expanded', 'false'); + const exportMenu = exportDropdown.parentElement && exportDropdown.parentElement.querySelector('.dropdown-menu'); + if (exportMenu) exportMenu.classList.remove('show'); + } + const mobileExportPanel = document.getElementById('mobile-menu-export-panel'); + if (mobileExportToggle) mobileExportToggle.setAttribute('aria-expanded', 'false'); + if (mobileExportPanel) mobileExportPanel.hidden = true; + } + function updateLiveEditorAccess() { const snapshotViewOnly = isShareSnapshotViewOnlyActive(); + const releaseNotesActive = isReleaseNotesActive(); const snapshotActive = isShareSnapshotActive(); const liveShareDocumentActive = isLiveShareDocumentActive(); const liveShareGuestDocumentActive = liveShareDocumentActive && !isLiveShareHostDocumentActive(); - const viewOnly = isLiveViewOnlyParticipant() || snapshotViewOnly; + const viewOnly = isLiveViewOnlyParticipant() || snapshotViewOnly || releaseNotesActive; const sourceReadOnly = viewOnly || reviewModeActive; if (markdownEditor) { markdownEditor.readOnly = sourceReadOnly; @@ -21822,7 +22224,7 @@ ${selector} .arrowheadPath { viewModeButtons.forEach(function(button) { const mode = button.getAttribute('data-view-mode'); - const shouldDisable = (snapshotViewOnly || reviewModeActive) && mode !== 'preview'; + const shouldDisable = (snapshotViewOnly || releaseNotesActive || reviewModeActive) && mode !== 'preview'; if (shouldDisable) { button.dataset.shareSnapshotDisabled = 'true'; button.disabled = true; @@ -21837,7 +22239,7 @@ ${selector} .arrowheadPath { mobileViewModeButtons.forEach(function(button) { const mode = button.getAttribute('data-mode'); - const shouldDisable = (snapshotViewOnly || reviewModeActive) && mode !== 'preview'; + const shouldDisable = (snapshotViewOnly || releaseNotesActive || reviewModeActive) && mode !== 'preview'; if (shouldDisable) { button.dataset.shareSnapshotDisabled = 'true'; button.disabled = true; @@ -21950,6 +22352,7 @@ ${selector} .arrowheadPath { } } updateDocumentToolbarAvailability(hasActiveOpenDocument()); + updateReleaseNotesActionAvailability(releaseNotesActive); } function getLiveRoomSocketUrl(roomId, secret, auth) { @@ -23443,6 +23846,7 @@ ${selector} .arrowheadPath { function openLiveShareModal() { if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; if (!liveShareModal) return; if (blockTemporarySnapshotLiveShare()) return; if (liveShareDisplayName && !liveShareDisplayName.value) { @@ -26352,4 +26756,11 @@ ${selector} .arrowheadPath { }); }); } + + // Wait until all editor and collaboration dependencies exist before adding the update tab. + if (releaseNotesLaunch.shouldOpen) { + await openReleaseNotes({ + activate: releaseNotesLaunch.activate + }); + } }); diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index fe3f611f..fd95dc38 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -2390,6 +2390,13 @@ body.document-sidebar-collapsed .document-sidebar { outline-offset: -2px; } +.app-menu-item.disabled, +.app-menu-item[aria-disabled="true"] { + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; +} + .app-menu-item > i:first-child { color: currentColor; font-size: var(--ui-icon-size-md); @@ -4464,6 +4471,13 @@ a:focus { font-size: 13px; } +.tab-app-icon { + width: 16px; + height: 16px; + border-radius: 3px; + object-fit: cover; +} + .tab-item.active .tab-file-icon { color: var(--accent-color); } @@ -5160,7 +5174,9 @@ a:focus { border-radius: 8px; background: var(--button-bg); color: var(--text-color); + cursor: pointer; font-size: var(--ui-font-md); + font-family: inherit; line-height: 1.2; text-decoration: none; transition: background-color 0.15s ease, border-color 0.15s ease; @@ -11729,3 +11745,500 @@ html[data-theme="dark"] .mermaid svg { background: white !important; } } + +/* ======================================== + VERSIONED RELEASE NOTES + ======================================== */ +.tab-item.release-notes-tab { + width: max-content; + min-width: max-content; + max-width: none; + flex: 0 0 auto; +} + +.tab-item.release-notes-tab .tab-title { + overflow: visible; + flex: 0 0 auto; + max-width: none; + text-overflow: clip; +} + +.preview-pane.release-notes-surface { + padding: 0; + scroll-behavior: smooth; + overscroll-behavior: contain; +} + +#markdown-preview.release-notes-preview { + min-height: 100%; + padding: clamp(18px, 2.4vw, 28px) clamp(24px, 4vw, 52px); + background: var(--preview-bg); +} + +.release-note-shell { + --release-note-accent-hsl: 212 92% 45%; + --release-note-accent: hsl(var(--release-note-accent-hsl)); + --release-note-accent-soft: hsl(var(--release-note-accent-hsl) / 0.1); + --release-note-surface: color-mix(in srgb, var(--header-bg) 74%, var(--preview-bg)); + width: min(100%, 1120px); + margin: 0 auto; + padding-bottom: 72px; + color: var(--preview-text-color); + font-size: 15px; + line-height: 1.7; +} + +[data-theme="dark"] .release-note-shell { + --release-note-accent-hsl: 210 100% 68%; +} + +.release-note-shell *, +.release-note-shell *::before, +.release-note-shell *::after { + box-sizing: border-box; +} + +.release-note-shell a { + touch-action: manipulation; +} + +.release-note-intro { + padding: clamp(4px, 1vw, 10px) 0 clamp(30px, 4vw, 44px); + border-bottom: 1px solid var(--border-color); +} + +.release-note-brandline, +.markdown-body .release-note-brandline { + display: flex; + width: fit-content; + align-items: center; + gap: 10px; + margin: 8px 0 18px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 650; +} + +.release-note-brandline img { + width: 28px; + height: 28px; + border: 1px solid var(--border-color); + border-radius: 7px; + object-fit: cover; +} + +.markdown-body .release-note-brandline > strong { + color: var(--preview-text-color); + font-weight: 650; +} + +.markdown-body .release-note-brandline code { + padding: 3px 8px; + border: 1px solid hsl(var(--release-note-accent-hsl) / 0.28); + border-radius: 999px; + color: var(--release-note-accent); + background: var(--release-note-accent-soft); + font-family: inherit; + font-size: 10px; + font-weight: 650; + letter-spacing: 0.07em; + line-height: 1.4; + text-transform: uppercase; +} + +.markdown-body .release-note-intro h1 { + max-width: none; + padding: 0; + margin: 0 0 12px; + border: 0; + color: var(--preview-text-color); + font-size: clamp(32px, 3.7vw, 44px); + font-weight: 690; + letter-spacing: -0.045em; + line-height: 1.08; + text-wrap: nowrap; + white-space: nowrap; +} + +.release-note-release-date, +.markdown-body .release-note-release-date { + margin: 0 0 18px; + color: var(--text-secondary); + font-size: 12px; +} + +.release-note-intro > p:not(.release-note-brandline):not(.release-note-release-date):not(.release-note-actions), +.markdown-body .release-note-intro > p:not(.release-note-brandline):not(.release-note-release-date):not(.release-note-actions) { + max-width: 75ch; + margin: 0 0 14px; + color: var(--text-secondary); + font-size: 17px; +} + +.release-note-actions { + display: flex; + max-width: none; + flex-wrap: wrap; + gap: 10px; + margin: 24px 0 0; +} + +.markdown-body .release-note-actions a { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + gap: 7px; + padding: 8px 14px; + border: 1px solid var(--border-color); + border-radius: 7px; + color: var(--preview-text-color); + background: var(--button-bg); + font-size: 12px; + font-weight: 650; + text-decoration: none; + transition: border-color 0.18s ease, background-color 0.18s ease; +} + +.markdown-body .release-note-actions a i { + flex: 0 0 auto; + font-size: 15px; +} + +.markdown-body .release-note-actions a:first-child { + border-color: hsl(var(--release-note-accent-hsl) / 0.54); + color: var(--release-note-accent); + background: var(--release-note-accent-soft); +} + +.markdown-body .release-note-actions a:hover, +.markdown-body .release-note-actions a:focus-visible { + border-color: var(--release-note-accent); + background: var(--button-hover); +} + +.release-note-intro > hr { + display: none; +} + +.release-note-layout { + position: relative; + display: grid; + grid-template-columns: minmax(170px, 210px) minmax(0, 1fr); + align-items: start; + gap: clamp(34px, 6vw, 72px); + padding-top: clamp(42px, 7vw, 76px); +} + +.release-note-skip-link { + position: absolute; + z-index: 6; + top: 8px; + left: 0; + padding: 8px 10px; + border-radius: 6px; + color: var(--preview-text-color); + background: var(--button-bg); + opacity: 0; + pointer-events: none; + transform: translateY(-8px); +} + +.release-note-skip-link:focus { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.release-note-navigation { + position: sticky; + top: 18px; + max-height: calc(100vh - 132px); + overflow-y: auto; + padding-bottom: 12px; + scrollbar-width: thin; +} + +.release-note-navigation-label, +.markdown-body .release-note-navigation-label { + margin: 0 0 13px; + color: var(--text-secondary); + font-size: 10px; + font-weight: 760; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.markdown-body .release-note-navigation a { + position: relative; + display: flex; + min-height: 38px; + align-items: center; + padding: 7px 10px 7px 15px; + border-radius: 6px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.4; + text-decoration: none; + transition: color 0.16s ease, background-color 0.16s ease; +} + +.markdown-body .release-note-navigation a::before { + position: absolute; + top: 9px; + bottom: 9px; + left: 0; + width: 2px; + border-radius: 2px; + background: transparent; + content: ''; +} + +.markdown-body .release-note-navigation a:hover, +.markdown-body .release-note-navigation a:focus-visible { + color: var(--preview-text-color); + background: var(--button-hover); +} + +.markdown-body .release-note-navigation a.is-active, +.markdown-body .release-note-navigation a[aria-current="location"] { + color: var(--release-note-accent); + background: var(--release-note-accent-soft); + font-weight: 650; +} + +.markdown-body .release-note-navigation a.is-active::before, +.markdown-body .release-note-navigation a[aria-current="location"]::before { + background: var(--release-note-accent); +} + +.release-note-content { + min-width: 0; + max-width: 790px; +} + +.release-note-section { + padding-bottom: clamp(48px, 7vw, 78px); + margin-bottom: clamp(48px, 7vw, 78px); + border-bottom: 1px solid var(--border-color); + scroll-margin-top: 28px; +} + +.markdown-body .release-note-section h2 { + max-width: 720px; + padding: 0; + margin: 0 0 18px; + border: 0; + color: var(--preview-text-color); + font-size: clamp(25px, 3.2vw, 36px); + font-weight: 680; + letter-spacing: -0.035em; + line-height: 1.15; + text-wrap: balance; +} + +.markdown-body .release-note-section h3 { + margin: 30px 0 10px; + color: var(--preview-text-color); + font-size: 16px; + font-weight: 650; +} + +.release-note-section > p, +.markdown-body .release-note-section > p { + max-width: 75ch; + color: var(--text-secondary); +} + +.markdown-body .release-note-section:first-child > ul { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + padding: 0; + margin: 26px 0; + list-style: none; +} + +.markdown-body .release-note-section:first-child > ul > li { + min-width: 0; + padding: 17px; + border: 1px solid var(--border-color); + border-radius: 10px; + color: var(--text-secondary); + background: var(--release-note-surface); + font-size: 13px; +} + +.markdown-body .release-note-section:first-child > ul > li strong { + color: var(--preview-text-color); +} + +.markdown-body .release-note-section:first-child > ul > li:last-child:nth-child(odd) { + grid-column: 1 / -1; +} + +.release-note-section:last-child { + padding: clamp(24px, 4vw, 36px); + margin-bottom: 0; + border: 1px solid hsl(var(--release-note-accent-hsl) / 0.3); + border-radius: 14px; + background: var(--release-note-accent-soft); +} + +.markdown-body .release-note-back-to-top { + position: fixed; + z-index: 12; + right: 26px; + bottom: 46px; + display: inline-flex; + width: 44px; + height: 44px; + align-items: center; + justify-content: center; + border: 1px solid var(--border-color); + border-radius: 50%; + color: var(--preview-text-color); + background: color-mix(in srgb, var(--button-bg) 92%, transparent); + box-shadow: 0 8px 24px color-mix(in srgb, var(--text-color) 16%, transparent); + text-decoration: none; + backdrop-filter: blur(10px); + transition: border-color 0.18s ease, background-color 0.18s ease; +} + +.markdown-body .release-note-back-to-top:hover, +.markdown-body .release-note-back-to-top:focus-visible { + border-color: var(--release-note-accent); + color: var(--release-note-accent); + background: var(--button-hover); +} + +@media (max-width: 900px) { + .markdown-body .release-note-intro h1 { + text-wrap: balance; + white-space: normal; + } +} + +@media (max-width: 760px) { + #markdown-preview.release-notes-preview { + padding: 18px; + } + + .release-note-layout { + display: block; + padding-top: 34px; + } + + .release-note-navigation { + top: 0; + z-index: 5; + display: flex; + max-height: none; + gap: 3px; + margin: 0 -18px 44px; + padding: 9px 18px; + overflow-x: auto; + overflow-y: hidden; + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + background: color-mix(in srgb, var(--preview-bg) 94%, transparent); + backdrop-filter: blur(10px); + scrollbar-width: none; + } + + .release-note-navigation::-webkit-scrollbar { + display: none; + } + + .release-note-navigation-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + .markdown-body .release-note-navigation a { + flex: 0 0 auto; + min-height: 44px; + padding: 9px 12px; + white-space: nowrap; + } + + .markdown-body .release-note-navigation a::before { + top: auto; + right: 12px; + bottom: 3px; + left: 12px; + width: auto; + height: 2px; + } + + .release-note-section { + scroll-margin-top: 72px; + } + + .markdown-body .release-note-back-to-top { + right: 18px; + bottom: 42px; + } +} + +@media (max-width: 520px) { + .markdown-body .release-note-intro h1 { + font-size: clamp(30px, 9vw, 40px); + } + + .release-note-brandline { + flex-wrap: wrap; + } + + .release-note-actions { + align-items: stretch; + flex-direction: column; + } + + .markdown-body .release-note-actions a { + width: 100%; + } + + .markdown-body .release-note-section:first-child > ul { + grid-template-columns: 1fr; + } + + .tab-item.release-notes-tab { + max-width: none !important; + } +} + +@media (prefers-reduced-motion: reduce) { + .preview-pane.release-notes-surface { + scroll-behavior: auto; + } + + .release-note-actions a, + .release-note-navigation a, + .release-note-back-to-top { + transition: none !important; + } +} + +@media print { + .release-note-navigation, + .release-note-skip-link, + .release-note-back-to-top { + display: none !important; + } + + .release-note-layout { + display: block; + } + + .release-note-section { + break-inside: avoid; + } +} diff --git a/index.html b/index.html index 4761d597..040f3773 100644 --- a/index.html +++ b/index.html @@ -938,12 +938,12 @@

Markdown Viewer

GitHub Repository - - Changelog - Apache License 2.0 + FAQ diff --git a/script.js b/script.js index c47a5626..b2ae2e6e 100644 --- a/script.js +++ b/script.js @@ -1,6 +1,12 @@ document.addEventListener("DOMContentLoaded", async function () { window.alert = function(message) { showAppToast(message); }; const PRIVATE_MODE_KEY = 'markdownViewerPrivateMode'; + const APP_VERSION = '3.10.0'; + const RELEASE_NOTES_TAB_KIND = 'release-notes'; + const RELEASE_NOTES_LAST_VERSION_KEY = 'markdownViewerLastVersion'; + const RELEASE_NOTES_PENDING_VERSION_KEY = 'markdownViewerPendingReleaseNotesVersion'; + const RELEASE_NOTES_PENDING_MODE_KEY = 'markdownViewerPendingReleaseNotesMode'; + const RELEASE_NOTES_SEEN_VERSION_KEY = 'markdownViewerReleaseNotesSeenVersion'; const DOCUMENT_STORAGE_KEYS = new Set([ 'markdownViewerGlobalState', 'markdownViewerTabs', @@ -46,6 +52,10 @@ document.addEventListener("DOMContentLoaded", async function () { 'markdownViewerUntitledCounter', 'markdownViewerDocumentOrganization', 'markdownViewerSecretWorkspace', + RELEASE_NOTES_LAST_VERSION_KEY, + RELEASE_NOTES_PENDING_VERSION_KEY, + RELEASE_NOTES_PENDING_MODE_KEY, + RELEASE_NOTES_SEEN_VERSION_KEY, 'find-replace-docked', 'app-lang' ]; @@ -288,7 +298,6 @@ document.addEventListener("DOMContentLoaded", async function () { let currentViewMode = 'split'; // 'editor', 'split', or 'preview' const shareSnapshotViewOnlyTabIds = new Set(); const SHARE_SNAPSHOT_TAB_KIND = 'share-snapshot'; - const APP_VERSION = '3.10.0'; const REVIEW_TARGET_SELECTOR = 'h1, h2, h3, h4, h5, h6, p, pre, .frontmatter-table, .diagram-viewer, .geojson-container, .topojson-container, .stl-container'; const REVIEW_TEXT_LIMIT = 2000; let reviewModeActive = false; @@ -429,6 +438,278 @@ document.addEventListener("DOMContentLoaded", async function () { return Boolean(activeTabId && isShareSnapshotTabId(activeTabId)); } + function isReleaseNotesTab(tab) { + return Boolean(tab && tab.kind === RELEASE_NOTES_TAB_KIND); + } + + function isReleaseNotesActive() { + return Boolean(activeTabId && tabs.some(function(tab) { + return tab.id === activeTabId && isReleaseNotesTab(tab); + })); + } + + function readReleaseNotesStorage(key) { + try { + return localStorage.getItem(key) || ''; + } catch (_) { + return ''; + } + } + + function markReleaseNotesSeen(tab) { + if (!isReleaseNotesTab(tab)) return; + const version = tab.releaseVersion || APP_VERSION; + saveStorageItem(RELEASE_NOTES_SEEN_VERSION_KEY, version); + if (readReleaseNotesStorage(RELEASE_NOTES_PENDING_VERSION_KEY) === version) { + removeStorageItem(RELEASE_NOTES_PENDING_VERSION_KEY); + removeStorageItem(RELEASE_NOTES_PENDING_MODE_KEY); + } + } + + function queueCurrentReleaseNotes(hadExistingWorkspace) { + const previousVersion = readReleaseNotesStorage(RELEASE_NOTES_LAST_VERSION_KEY); + const seenVersion = readReleaseNotesStorage(RELEASE_NOTES_SEEN_VERSION_KEY); + let pendingVersion = readReleaseNotesStorage(RELEASE_NOTES_PENDING_VERSION_KEY); + let pendingMode = readReleaseNotesStorage(RELEASE_NOTES_PENDING_MODE_KEY); + const firstRun = Boolean(!previousVersion && !hadExistingWorkspace); + const versionChanged = Boolean(previousVersion && previousVersion !== APP_VERSION); + const featureUpgrade = Boolean(!previousVersion && hadExistingWorkspace); + + if ((firstRun || versionChanged || featureUpgrade) && seenVersion !== APP_VERSION) { + pendingVersion = APP_VERSION; + pendingMode = firstRun ? 'background' : 'foreground'; + saveStorageItem(RELEASE_NOTES_PENDING_VERSION_KEY, APP_VERSION); + saveStorageItem(RELEASE_NOTES_PENDING_MODE_KEY, pendingMode); + } + saveStorageItem(RELEASE_NOTES_LAST_VERSION_KEY, APP_VERSION); + return { + shouldOpen: pendingVersion === APP_VERSION && seenVersion !== APP_VERSION, + activate: pendingMode !== 'background' + }; + } + + let releaseNotesContentPromise = null; + let releaseNotesPreviewCleanup = null; + let releaseNotesScrollFrame = null; + + function clearReleaseNotesPreviewEnhancements() { + if (releaseNotesPreviewCleanup) { + releaseNotesPreviewCleanup(); + releaseNotesPreviewCleanup = null; + } + if (releaseNotesScrollFrame !== null) { + cancelAnimationFrame(releaseNotesScrollFrame); + releaseNotesScrollFrame = null; + } + if (previewPane) previewPane.classList.remove('release-notes-surface'); + if (markdownPreview) markdownPreview.classList.remove('release-notes-preview'); + } + + function enhanceReleaseNotesPreview() { + clearReleaseNotesPreviewEnhancements(); + if (!isReleaseNotesActive() || !markdownPreview || !previewPane) return; + + let shell = markdownPreview.querySelector('.release-note-shell'); + if (!shell) { + const sourceNodes = Array.from(markdownPreview.childNodes); + const firstSectionIndex = sourceNodes.findIndex(function(node) { + return node.nodeType === Node.ELEMENT_NODE && node.tagName === 'H2'; + }); + if (firstSectionIndex < 0) return; + + shell = document.createElement('article'); + shell.className = 'release-note-shell'; + shell.setAttribute('aria-label', 'Markdown Viewer ' + APP_VERSION + ' release notes'); + + const intro = document.createElement('header'); + intro.className = 'release-note-intro'; + sourceNodes.slice(0, firstSectionIndex).forEach(function(node) { + intro.appendChild(node); + }); + + const introParagraphs = Array.from(intro.children).filter(function(node) { + return node.tagName === 'P'; + }); + const brandLine = introParagraphs.find(function(paragraph) { + return Boolean(paragraph.querySelector('img[src$="assets/icon.jpg"]')); + }); + if (brandLine) brandLine.classList.add('release-note-brandline'); + + const releaseDate = introParagraphs.find(function(paragraph) { + return /^Released\s/i.test(paragraph.textContent.trim()); + }); + if (releaseDate) releaseDate.classList.add('release-note-release-date'); + + const releaseLink = intro.querySelector('a[href*="/releases/tag/"]'); + const changelogLink = intro.querySelector('a[href*="/CHANGELOG.md"]'); + if (releaseLink && changelogLink && releaseLink.parentElement === changelogLink.parentElement) { + releaseLink.parentElement.classList.add('release-note-actions'); + const releaseIcon = document.createElement('i'); + releaseIcon.className = 'bi bi-github'; + releaseIcon.setAttribute('aria-hidden', 'true'); + releaseLink.prepend(releaseIcon); + const changelogIcon = document.createElement('i'); + changelogIcon.className = 'lucide lucide-history'; + changelogIcon.setAttribute('aria-hidden', 'true'); + changelogLink.prepend(changelogIcon); + } + + const layout = document.createElement('div'); + layout.className = 'release-note-layout'; + const navigation = document.createElement('nav'); + navigation.className = 'release-note-navigation'; + navigation.setAttribute('aria-label', 'In this update'); + const navigationLabel = document.createElement('p'); + navigationLabel.className = 'release-note-navigation-label'; + navigationLabel.textContent = 'In this update'; + navigation.appendChild(navigationLabel); + + const content = document.createElement('div'); + content.className = 'release-note-content'; + content.id = 'release-note-content'; + let currentSection = null; + sourceNodes.slice(firstSectionIndex).forEach(function(node) { + if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'H2') { + currentSection = document.createElement('section'); + currentSection.className = 'release-note-section'; + currentSection.id = node.id || ('release-note-section-' + content.children.length); + node.id = currentSection.id + '-heading'; + currentSection.setAttribute('aria-labelledby', node.id); + currentSection.appendChild(node); + content.appendChild(currentSection); + + const link = document.createElement('a'); + link.href = '#' + currentSection.id; + link.textContent = node.textContent || 'Section'; + navigation.appendChild(link); + return; + } + if (currentSection) currentSection.appendChild(node); + }); + + const skipLink = document.createElement('a'); + skipLink.className = 'release-note-skip-link'; + skipLink.href = '#release-note-content'; + skipLink.textContent = 'Skip update navigation'; + layout.appendChild(skipLink); + layout.appendChild(navigation); + layout.appendChild(content); + shell.appendChild(intro); + shell.appendChild(layout); + + const topLink = document.createElement('a'); + topLink.className = 'release-note-back-to-top'; + const topTarget = intro.querySelector('h1'); + if (topTarget && !topTarget.id) topTarget.id = 'release-note-top'; + topLink.href = '#' + (topTarget && topTarget.id ? topTarget.id : 'release-note-content'); + topLink.setAttribute('aria-label', 'Back to the top of the release notes'); + topLink.title = 'Back to top'; + topLink.innerHTML = ''; + shell.appendChild(topLink); + markdownPreview.replaceChildren(shell); + } + + previewPane.classList.add('release-notes-surface'); + markdownPreview.classList.add('release-notes-preview'); + const links = Array.from(shell.querySelectorAll('.release-note-navigation a[href^="#"]')); + const sections = links.map(function(link) { + const id = decodeURIComponent((link.getAttribute('href') || '').slice(1)); + return { link: link, section: id ? shell.querySelector('#' + CSS.escape(id)) : null }; + }).filter(function(item) { + return Boolean(item.section); + }); + + function updateActiveReleaseNotesSection() { + releaseNotesScrollFrame = null; + if (!isReleaseNotesActive() || !document.body.contains(shell) || sections.length === 0) return; + const paneTop = previewPane.getBoundingClientRect().top; + const activationLine = paneTop + Math.min(112, previewPane.clientHeight * 0.24); + let current = sections[0]; + sections.forEach(function(item) { + if (item.section.getBoundingClientRect().top <= activationLine) current = item; + }); + if (previewPane.scrollTop + previewPane.clientHeight >= previewPane.scrollHeight - 8) { + current = sections[sections.length - 1]; + } + sections.forEach(function(item) { + const active = item === current; + item.link.classList.toggle('is-active', active); + if (active) item.link.setAttribute('aria-current', 'location'); + else item.link.removeAttribute('aria-current'); + }); + } + + function scheduleReleaseNotesSectionUpdate() { + if (releaseNotesScrollFrame !== null) return; + releaseNotesScrollFrame = requestAnimationFrame(updateActiveReleaseNotesSection); + } + + const backToTopLink = shell.querySelector('.release-note-back-to-top'); + function handleReleaseNotesBackToTop(event) { + event.preventDefault(); + event.stopPropagation(); + const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + previewPane.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' }); + } + + previewPane.addEventListener('scroll', scheduleReleaseNotesSectionUpdate, { passive: true }); + window.addEventListener('resize', scheduleReleaseNotesSectionUpdate); + if (backToTopLink) backToTopLink.addEventListener('click', handleReleaseNotesBackToTop); + releaseNotesPreviewCleanup = function() { + previewPane.removeEventListener('scroll', scheduleReleaseNotesSectionUpdate); + window.removeEventListener('resize', scheduleReleaseNotesSectionUpdate); + if (backToTopLink) backToTopLink.removeEventListener('click', handleReleaseNotesBackToTop); + }; + scheduleReleaseNotesSectionUpdate(); + } + + function loadCurrentReleaseNotes() { + if (!releaseNotesContentPromise) { + releaseNotesContentPromise = fetch('RELEASE_NOTES').then(function(response) { + if (!response.ok) throw new Error('Release notes returned ' + response.status); + return response.text(); + }).catch(function(error) { + console.warn('Failed to load release notes:', error); + return '# Markdown Viewer ' + APP_VERSION + '\n\nThe release notes could not be loaded. Visit the [project releases](https://github.com/ThisIs-Developer/Markdown-Viewer/releases) for the latest changes.'; + }); + } + return releaseNotesContentPromise; + } + + async function openReleaseNotes(options) { + const settings = options || {}; + const shouldActivate = settings.activate !== false; + const existing = tabs.find(function(tab) { + return isReleaseNotesTab(tab) && tab.releaseVersion === APP_VERSION; + }); + if (existing) { + if (shouldActivate) { + await switchTab(existing.id); + updateLiveEditorAccess(); + } else { + renderTabBar(tabs, activeTabId); + } + return existing; + } + + const content = await loadCurrentReleaseNotes(); + const tab = createTab(content, 'Release Notes: ' + APP_VERSION, 'preview'); + tab.kind = RELEASE_NOTES_TAB_KIND; + tab.temporary = true; + tab.releaseVersion = APP_VERSION; + tabs.push(tab); + if (shouldActivate) { + await switchTab(tab.id); + updateLiveEditorAccess(); + } else { + renderTabBar(tabs, activeTabId); + } + if (!settings.silent) { + announceToScreenReader(shouldActivate ? 'Release notes opened.' : 'Release notes are available in the tab list.'); + } + return tab; + } + function stripTemporaryTabs(tabsArr) { return (tabsArr || []).filter(function(tab) { return !isShareSnapshotTab(tab) && tab.temporary !== true; @@ -481,6 +762,9 @@ document.addEventListener("DOMContentLoaded", async function () { if (isShareSnapshotViewOnlyActive()) { return 'This shared snapshot is view only.'; } + if (isReleaseNotesActive()) { + return 'Release notes are read only.'; + } if (isLiveViewOnlyParticipant()) { return 'This Live Share session is view only.'; } @@ -592,6 +876,7 @@ document.addEventListener("DOMContentLoaded", async function () { const aboutModalClose = document.getElementById("about-modal-close"); const aboutModalCloseIcon = document.getElementById("about-modal-close-icon"); const aboutVersion = document.getElementById("about-version"); + const aboutReleaseNotes = document.getElementById("about-release-notes"); const privateModeToggle = document.getElementById("private-mode-toggle"); const storageSettingsButton = document.getElementById("storage-settings-button"); const storageSettingsModal = document.getElementById("storage-settings-modal"); @@ -3856,7 +4141,8 @@ document.addEventListener("DOMContentLoaded", async function () { } function getDocumentCountForLimit() { - return tabs.length + (isSecretWorkspaceUnlocked() ? 0 : secretWorkspaceDocumentCount); + return tabs.filter(function(tab) { return !isReleaseNotesTab(tab); }).length + + (isSecretWorkspaceUnlocked() ? 0 : secretWorkspaceDocumentCount); } function getWorkspaceById(workspaceId) { @@ -5404,7 +5690,9 @@ document.addEventListener("DOMContentLoaded", async function () { function renderWorkspaceTree(tree) { let renderedDocuments = 0; - const temporaryTabs = tabs.filter(isTemporaryDocument).filter(documentMatchesSidebarSearch); + const temporaryTabs = tabs.filter(function(tab) { + return isTemporaryDocument(tab) && !isReleaseNotesTab(tab); + }).filter(documentMatchesSidebarSearch); if (temporaryTabs.length) { const temporaryLabel = document.createElement('p'); temporaryLabel.className = 'document-tree-section-label'; @@ -5569,6 +5857,7 @@ document.addEventListener("DOMContentLoaded", async function () { let renderedCount = filter === 'all' ? renderWorkspaceTree(tree) : renderFlatDocumentView(tree, filter); documentSidebarRenderBudget = null; let visibleDocumentCount = tabs.filter(function(tab) { + if (isReleaseNotesTab(tab)) return false; if (filter !== 'all' && isTemporaryDocument(tab)) return false; if (filter === 'favorites' && !tab.favorite) return false; return documentMatchesSidebarSearch(tab); @@ -7725,29 +8014,33 @@ document.addEventListener("DOMContentLoaded", async function () { dropdown.className = 'tab-menu-dropdown'; dropdown.setAttribute('data-tab-menu-dropdown', 'true'); dropdown.setAttribute('role', 'menu'); - const duplicateAction = isShareSnapshotTab(tab) - ? '' - : ''; - const downloadAction = isShareSnapshotTab(tab) - ? '' - : ''; - const favoriteAction = isShareSnapshotTab(tab) - ? '' - : ''; - const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); - const splitAction = isCombinedSplitTab - ? '' - : tabs.length > 1 - ? '' - : ''; - dropdown.innerHTML = - '' + - duplicateAction + - favoriteAction + - splitAction + - downloadAction + - '' + - ''; + if (isReleaseNotesTab(tab)) { + dropdown.innerHTML = ''; + } else { + const duplicateAction = isShareSnapshotTab(tab) + ? '' + : ''; + const downloadAction = isShareSnapshotTab(tab) + ? '' + : ''; + const favoriteAction = isShareSnapshotTab(tab) + ? '' + : ''; + const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); + const splitAction = isCombinedSplitTab + ? '' + : tabs.length > 1 + ? '' + : ''; + dropdown.innerHTML = + '' + + duplicateAction + + favoriteAction + + splitAction + + downloadAction + + '' + + ''; + } menuBtn.addEventListener('click', function(e) { e.preventDefault(); @@ -7846,6 +8139,7 @@ document.addEventListener("DOMContentLoaded", async function () { if (!idsToClose.has(tab.id) || !isTabOpen(tab)) return; if (isTemporaryDocument(tab)) { temporaryIds.add(tab.id); + markReleaseNotesSeen(tab); shareSnapshotViewOnlyTabIds.delete(tab.id); if (tabHistories[tab.id]) delete tabHistories[tab.id]; return; @@ -7891,14 +8185,7 @@ document.addEventListener("DOMContentLoaded", async function () { menu.setAttribute('aria-label', 'Tab actions for ' + (tab.title || 'Untitled')); const isCombinedSplitTab = tab.id === activeTabId && Boolean(secondarySplitTabId); - const actions = [{ - id: isCombinedSplitTab ? 'split-close' : 'split', - icon: isCombinedSplitTab ? 'lucide-panel-right-close' : 'lucide-columns-2', - label: isCombinedSplitTab ? 'Exit split view' : 'Open in split view', - disabled: !isCombinedSplitTab && tabs.length < 2 - }, { - separator: true - }, { + const closeActions = [{ id: 'close', icon: 'lucide-x', label: 'Close' }, { id: 'others', icon: 'lucide-circle-x', label: 'Close others', disabled: openTabs.length < 2 @@ -7909,6 +8196,14 @@ document.addEventListener("DOMContentLoaded", async function () { }, { id: 'all', icon: 'lucide-square-x', label: 'Close all' }]; + const actions = isReleaseNotesTab(tab) ? closeActions : [{ + id: isCombinedSplitTab ? 'split-close' : 'split', + icon: isCombinedSplitTab ? 'lucide-panel-right-close' : 'lucide-columns-2', + label: isCombinedSplitTab ? 'Exit split view' : 'Open in split view', + disabled: !isCombinedSplitTab && tabs.length < 2 + }, { + separator: true + }].concat(closeActions); actions.forEach(function(action) { if (action.separator) { @@ -7988,7 +8283,7 @@ document.addEventListener("DOMContentLoaded", async function () { ? tabs.find(function(item) { return item.id === secondarySplitTabId && isTabOpen(item); }) : null; const item = document.createElement('div'); - item.className = 'tab-item' + (tab.id === currentActiveTabId ? ' active' : '') + (splitPartner ? ' is-document-split' : ''); + item.className = 'tab-item' + (tab.id === currentActiveTabId ? ' active' : '') + (splitPartner ? ' is-document-split' : '') + (isReleaseNotesTab(tab) ? ' release-notes-tab' : ''); item.setAttribute('data-tab-id', tab.id); if (splitPartner) item.setAttribute('data-split-tab-id', splitPartner.id); item.setAttribute('role', 'tab'); @@ -8002,9 +8297,18 @@ document.addEventListener("DOMContentLoaded", async function () { item.style.setProperty('--split-tab-preferred-width', Math.round(preferredWidth) + 'px'); } - const fileIcon = document.createElement('i'); - fileIcon.className = 'lucide ' + (splitPartner ? 'lucide-columns-2' : 'lucide-file-text') + ' tab-file-icon'; - fileIcon.setAttribute('aria-hidden', 'true'); + let fileIcon; + if (isReleaseNotesTab(tab) && !splitPartner) { + fileIcon = document.createElement('img'); + fileIcon.className = 'tab-file-icon tab-app-icon'; + fileIcon.src = 'assets/icon.jpg'; + fileIcon.alt = ''; + fileIcon.setAttribute('aria-hidden', 'true'); + } else { + fileIcon = document.createElement('i'); + fileIcon.className = 'lucide ' + (splitPartner ? 'lucide-columns-2' : 'lucide-file-text') + ' tab-file-icon'; + fileIcon.setAttribute('aria-hidden', 'true'); + } const titleSpan = document.createElement('span'); titleSpan.className = 'tab-title' + (splitPartner ? ' tab-split-titles' : ''); @@ -8283,7 +8587,7 @@ document.addEventListener("DOMContentLoaded", async function () { if (contentChanged && !isTemporaryDocument(tab)) { tab.lastEditedAt = Date.now(); } - saveTabsToStorage(tabs, [tab.id]); + if (!isTemporaryDocument(tab)) saveTabsToStorage(tabs, [tab.id]); if (contentChanged) renderDocumentSidebar(); } @@ -8418,7 +8722,7 @@ document.addEventListener("DOMContentLoaded", async function () { documentSplitEditor.hidden = previewMode; documentSplitPreview.hidden = !previewMode; if (documentSplitEditor.value !== tab.content) documentSplitEditor.value = tab.content || ''; - documentSplitEditor.readOnly = shareSnapshotViewOnlyTabIds.has(tab.id); + documentSplitEditor.readOnly = shareSnapshotViewOnlyTabIds.has(tab.id) || isReleaseNotesTab(tab); documentSplitEditor.setAttribute('aria-label', 'Edit ' + (tab.title || 'Untitled') + ' in split view'); documentSplitPreview.setAttribute('aria-label', 'Preview ' + (tab.title || 'Untitled') + ' in split view'); if (previewMode) renderDocumentSplitPreview(tab); @@ -8477,7 +8781,7 @@ document.addEventListener("DOMContentLoaded", async function () { async function openDocumentSplitView(sourceTabId, secondTabId) { const sourceTab = tabs.find(function(item) { return item.id === sourceTabId; }); const secondTab = tabs.find(function(item) { return item.id === secondTabId; }); - if (!sourceTab || !secondTab || sourceTab.id === secondTab.id) return; + if (!sourceTab || !secondTab || sourceTab.id === secondTab.id || isReleaseNotesTab(sourceTab) || isReleaseNotesTab(secondTab)) return; try { await Promise.all([ensureTabContent(sourceTab), ensureTabContent(secondTab)]); } catch (_) { @@ -8499,8 +8803,8 @@ document.addEventListener("DOMContentLoaded", async function () { function openDocumentSplitPicker(sourceTabId) { const sourceTab = tabs.find(function(item) { return item.id === sourceTabId; }); - const candidates = tabs.filter(function(item) { return item.id !== sourceTabId; }); - if (!sourceTab || candidates.length === 0) { + const candidates = tabs.filter(function(item) { return item.id !== sourceTabId && !isReleaseNotesTab(item); }); + if (!sourceTab || isReleaseNotesTab(sourceTab) || candidates.length === 0) { alert('Open or create another document before starting split view.'); return; } @@ -8682,6 +8986,7 @@ document.addEventListener("DOMContentLoaded", async function () { const idx = tabs.findIndex(function(t) { return t.id === tabId; }); if (idx === -1) return; + const tab = tabs[idx]; const wasActive = activeTabId === tabId; const splitPartnerId = wasActive ? secondarySplitTabId : null; if (wasActive) { @@ -8694,6 +8999,7 @@ document.addEventListener("DOMContentLoaded", async function () { closeDocumentSplitView({ silent: true, renderTabs: false }); } shareSnapshotViewOnlyTabIds.delete(tabId); + markReleaseNotesSeen(tab); // Clean up history of the closed tab if (tabHistories[tabId]) { @@ -9003,6 +9309,7 @@ document.addEventListener("DOMContentLoaded", async function () { documentOrganization = await loadDocumentOrganization(); initializeSecretWorkspaceState(); tabs = await loadTabsFromStorage(); + const hadExistingWorkspace = tabs.length > 0; activeTabId = loadActiveTabId(); // Check if Neutralino passed an initial file via command line (early load) @@ -9103,6 +9410,7 @@ document.addEventListener("DOMContentLoaded", async function () { openDocumentSurfaceContextMenu(surface, event); }); }); + return hadExistingWorkspace; } // Late-load callback hook for Neutralino command-line files @@ -10788,6 +11096,8 @@ ${selector} .arrowheadPath { enhancePreviewCodeBlocks(root); }); + enhanceReleaseNotesPreview(); + queryPreviewRoots(roots, 'input[type="checkbox"]').forEach(function(input) { if (!input.hasAttribute('aria-label')) { const parentText = input.parentElement ? input.parentElement.textContent.trim() : ''; @@ -13080,7 +13390,7 @@ ${selector} .arrowheadPath { // View Mode Functions - Story 1.1 & 1.2 function setViewMode(mode) { if (secondarySplitTabId && mode === 'split') mode = 'editor'; - if (isShareSnapshotViewOnlyActive() && mode !== 'preview') { + if ((isShareSnapshotViewOnlyActive() || isReleaseNotesActive()) && mode !== 'preview') { mode = 'preview'; announceToScreenReader(getEditorReadOnlyMessage()); } @@ -17973,6 +18283,18 @@ ${selector} .arrowheadPath { if (aboutModalCloseIcon) { aboutModalCloseIcon.addEventListener('click', function() { closeAppModal(aboutModal); }); } + if (aboutReleaseNotes) { + aboutReleaseNotes.addEventListener('click', function() { + closeAppModal(aboutModal); + openReleaseNotes().catch(function(error) { + console.warn('Failed to open release notes:', error); + showAppToast('The release notes could not be opened.', { + tone: 'error', + title: 'Release notes unavailable' + }); + }); + }); + } } function openHelpModal() { @@ -18686,8 +19008,9 @@ ${selector} .arrowheadPath { }); } - await initTabs(); + const hadExistingWorkspace = await initTabs(); initReviewMode(); + const releaseNotesLaunch = queueCurrentReleaseNotes(hadExistingWorkspace); if (loadGlobalState().syncScrollingEnabled === false) toggleSyncScrolling(); updateMobileStats(); updateFindHighlights(); @@ -19201,6 +19524,7 @@ ${selector} .arrowheadPath { exportMd.addEventListener("click", function () { if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; if (blockShareSnapshotSourceAccess()) return; if (typeof Neutralino !== 'undefined') { nativeSaveMarkdown(); @@ -19219,6 +19543,7 @@ ${selector} .arrowheadPath { exportHtml.addEventListener("click", function () { if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; try { const { frontmatter, body } = parseFrontmatter(markdownEditor.value); const tableHtml = frontmatter ? renderFrontmatterTable(frontmatter) : ''; @@ -20581,6 +20906,7 @@ ${selector} .arrowheadPath { exportPdf.addEventListener("click", function (event) { event.preventDefault(); if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; openAppModal(pdfExportModal); }); @@ -20933,6 +21259,7 @@ ${selector} .arrowheadPath { exportPng.addEventListener("click", async function (event) { event.preventDefault(); if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; logPdfExportDebug("PNG export button clicked!"); if (activePdfExport) { logPdfExportDebug("Export already active, ignoring click"); @@ -21432,6 +21759,7 @@ ${selector} .arrowheadPath { function openShareModal() { if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; if (blockTemporarySnapshotShare()) return; if (blockLiveDocumentShareSnapshot()) return; // PERF-002: Lazy-load pako on first share @@ -21773,7 +22101,7 @@ ${selector} .arrowheadPath { } function canMutateEditor() { - return hasActiveOpenDocument() && !reviewModeActive && !isLiveViewOnlyParticipant() && !isShareSnapshotViewOnlyActive(); + return hasActiveOpenDocument() && !reviewModeActive && !isLiveViewOnlyParticipant() && !isShareSnapshotViewOnlyActive() && !isReleaseNotesActive(); } function isLiveMutatingAction(action) { @@ -21806,12 +22134,86 @@ ${selector} .arrowheadPath { setSelectedLiveAccessMode(mode); } + function updateReleaseNotesActionAvailability(releaseNotesActive) { + const actions = [ + exportDropdown, + exportMd, + exportHtml, + exportPdf, + exportPng, + shareButton, + liveShareButton, + mobileExportToggle, + mobileExportMd, + mobileExportHtml, + mobileExportPdf, + mobileExportPng, + mobileShareButton, + mobileLiveShareButton + ]; + + actions.forEach(function(action) { + if (!action) return; + if (releaseNotesActive) { + if (action.dataset.releaseNotesDisabled !== 'true') { + action.dataset.releaseNotesWasDisabled = action.disabled ? 'true' : 'false'; + if (action.tagName === 'A') { + action.dataset.releaseNotesOriginalTabindex = action.hasAttribute('tabindex') + ? action.getAttribute('tabindex') + : '__missing__'; + } + } + action.dataset.releaseNotesDisabled = 'true'; + if ('disabled' in action) action.disabled = true; + action.classList.add('disabled'); + action.setAttribute('aria-disabled', 'true'); + if (action.tagName === 'A') action.setAttribute('tabindex', '-1'); + return; + } + + if (action.dataset.releaseNotesDisabled !== 'true') return; + const wasDisabled = action.dataset.releaseNotesWasDisabled === 'true'; + const hasAnotherDisabledState = Object.keys(action.dataset).some(function(key) { + return key !== 'releaseNotesDisabled' && + key !== 'releaseNotesWasDisabled' && + /Disabled$/.test(key) && + action.dataset[key] === 'true'; + }); + const remainsDisabled = wasDisabled || hasAnotherDisabledState; + delete action.dataset.releaseNotesDisabled; + delete action.dataset.releaseNotesWasDisabled; + if ('disabled' in action) action.disabled = remainsDisabled; + action.classList.toggle('disabled', remainsDisabled); + action.setAttribute('aria-disabled', remainsDisabled ? 'true' : 'false'); + if (action.tagName === 'A') { + const originalTabindex = action.dataset.releaseNotesOriginalTabindex; + delete action.dataset.releaseNotesOriginalTabindex; + if (originalTabindex && originalTabindex !== '__missing__') { + action.setAttribute('tabindex', originalTabindex); + } else { + action.removeAttribute('tabindex'); + } + } + }); + + if (!releaseNotesActive) return; + if (exportDropdown) { + exportDropdown.setAttribute('aria-expanded', 'false'); + const exportMenu = exportDropdown.parentElement && exportDropdown.parentElement.querySelector('.dropdown-menu'); + if (exportMenu) exportMenu.classList.remove('show'); + } + const mobileExportPanel = document.getElementById('mobile-menu-export-panel'); + if (mobileExportToggle) mobileExportToggle.setAttribute('aria-expanded', 'false'); + if (mobileExportPanel) mobileExportPanel.hidden = true; + } + function updateLiveEditorAccess() { const snapshotViewOnly = isShareSnapshotViewOnlyActive(); + const releaseNotesActive = isReleaseNotesActive(); const snapshotActive = isShareSnapshotActive(); const liveShareDocumentActive = isLiveShareDocumentActive(); const liveShareGuestDocumentActive = liveShareDocumentActive && !isLiveShareHostDocumentActive(); - const viewOnly = isLiveViewOnlyParticipant() || snapshotViewOnly; + const viewOnly = isLiveViewOnlyParticipant() || snapshotViewOnly || releaseNotesActive; const sourceReadOnly = viewOnly || reviewModeActive; if (markdownEditor) { markdownEditor.readOnly = sourceReadOnly; @@ -21822,7 +22224,7 @@ ${selector} .arrowheadPath { viewModeButtons.forEach(function(button) { const mode = button.getAttribute('data-view-mode'); - const shouldDisable = (snapshotViewOnly || reviewModeActive) && mode !== 'preview'; + const shouldDisable = (snapshotViewOnly || releaseNotesActive || reviewModeActive) && mode !== 'preview'; if (shouldDisable) { button.dataset.shareSnapshotDisabled = 'true'; button.disabled = true; @@ -21837,7 +22239,7 @@ ${selector} .arrowheadPath { mobileViewModeButtons.forEach(function(button) { const mode = button.getAttribute('data-mode'); - const shouldDisable = (snapshotViewOnly || reviewModeActive) && mode !== 'preview'; + const shouldDisable = (snapshotViewOnly || releaseNotesActive || reviewModeActive) && mode !== 'preview'; if (shouldDisable) { button.dataset.shareSnapshotDisabled = 'true'; button.disabled = true; @@ -21950,6 +22352,7 @@ ${selector} .arrowheadPath { } } updateDocumentToolbarAvailability(hasActiveOpenDocument()); + updateReleaseNotesActionAvailability(releaseNotesActive); } function getLiveRoomSocketUrl(roomId, secret, auth) { @@ -23443,6 +23846,7 @@ ${selector} .arrowheadPath { function openLiveShareModal() { if (!hasActiveOpenDocument()) return; + if (isReleaseNotesActive()) return; if (!liveShareModal) return; if (blockTemporarySnapshotLiveShare()) return; if (liveShareDisplayName && !liveShareDisplayName.value) { @@ -26352,4 +26756,11 @@ ${selector} .arrowheadPath { }); }); } + + // Wait until all editor and collaboration dependencies exist before adding the update tab. + if (releaseNotesLaunch.shouldOpen) { + await openReleaseNotes({ + activate: releaseNotesLaunch.activate + }); + } }); diff --git a/styles.css b/styles.css index fe3f611f..fd95dc38 100644 --- a/styles.css +++ b/styles.css @@ -2390,6 +2390,13 @@ body.document-sidebar-collapsed .document-sidebar { outline-offset: -2px; } +.app-menu-item.disabled, +.app-menu-item[aria-disabled="true"] { + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; +} + .app-menu-item > i:first-child { color: currentColor; font-size: var(--ui-icon-size-md); @@ -4464,6 +4471,13 @@ a:focus { font-size: 13px; } +.tab-app-icon { + width: 16px; + height: 16px; + border-radius: 3px; + object-fit: cover; +} + .tab-item.active .tab-file-icon { color: var(--accent-color); } @@ -5160,7 +5174,9 @@ a:focus { border-radius: 8px; background: var(--button-bg); color: var(--text-color); + cursor: pointer; font-size: var(--ui-font-md); + font-family: inherit; line-height: 1.2; text-decoration: none; transition: background-color 0.15s ease, border-color 0.15s ease; @@ -11729,3 +11745,500 @@ html[data-theme="dark"] .mermaid svg { background: white !important; } } + +/* ======================================== + VERSIONED RELEASE NOTES + ======================================== */ +.tab-item.release-notes-tab { + width: max-content; + min-width: max-content; + max-width: none; + flex: 0 0 auto; +} + +.tab-item.release-notes-tab .tab-title { + overflow: visible; + flex: 0 0 auto; + max-width: none; + text-overflow: clip; +} + +.preview-pane.release-notes-surface { + padding: 0; + scroll-behavior: smooth; + overscroll-behavior: contain; +} + +#markdown-preview.release-notes-preview { + min-height: 100%; + padding: clamp(18px, 2.4vw, 28px) clamp(24px, 4vw, 52px); + background: var(--preview-bg); +} + +.release-note-shell { + --release-note-accent-hsl: 212 92% 45%; + --release-note-accent: hsl(var(--release-note-accent-hsl)); + --release-note-accent-soft: hsl(var(--release-note-accent-hsl) / 0.1); + --release-note-surface: color-mix(in srgb, var(--header-bg) 74%, var(--preview-bg)); + width: min(100%, 1120px); + margin: 0 auto; + padding-bottom: 72px; + color: var(--preview-text-color); + font-size: 15px; + line-height: 1.7; +} + +[data-theme="dark"] .release-note-shell { + --release-note-accent-hsl: 210 100% 68%; +} + +.release-note-shell *, +.release-note-shell *::before, +.release-note-shell *::after { + box-sizing: border-box; +} + +.release-note-shell a { + touch-action: manipulation; +} + +.release-note-intro { + padding: clamp(4px, 1vw, 10px) 0 clamp(30px, 4vw, 44px); + border-bottom: 1px solid var(--border-color); +} + +.release-note-brandline, +.markdown-body .release-note-brandline { + display: flex; + width: fit-content; + align-items: center; + gap: 10px; + margin: 8px 0 18px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 650; +} + +.release-note-brandline img { + width: 28px; + height: 28px; + border: 1px solid var(--border-color); + border-radius: 7px; + object-fit: cover; +} + +.markdown-body .release-note-brandline > strong { + color: var(--preview-text-color); + font-weight: 650; +} + +.markdown-body .release-note-brandline code { + padding: 3px 8px; + border: 1px solid hsl(var(--release-note-accent-hsl) / 0.28); + border-radius: 999px; + color: var(--release-note-accent); + background: var(--release-note-accent-soft); + font-family: inherit; + font-size: 10px; + font-weight: 650; + letter-spacing: 0.07em; + line-height: 1.4; + text-transform: uppercase; +} + +.markdown-body .release-note-intro h1 { + max-width: none; + padding: 0; + margin: 0 0 12px; + border: 0; + color: var(--preview-text-color); + font-size: clamp(32px, 3.7vw, 44px); + font-weight: 690; + letter-spacing: -0.045em; + line-height: 1.08; + text-wrap: nowrap; + white-space: nowrap; +} + +.release-note-release-date, +.markdown-body .release-note-release-date { + margin: 0 0 18px; + color: var(--text-secondary); + font-size: 12px; +} + +.release-note-intro > p:not(.release-note-brandline):not(.release-note-release-date):not(.release-note-actions), +.markdown-body .release-note-intro > p:not(.release-note-brandline):not(.release-note-release-date):not(.release-note-actions) { + max-width: 75ch; + margin: 0 0 14px; + color: var(--text-secondary); + font-size: 17px; +} + +.release-note-actions { + display: flex; + max-width: none; + flex-wrap: wrap; + gap: 10px; + margin: 24px 0 0; +} + +.markdown-body .release-note-actions a { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + gap: 7px; + padding: 8px 14px; + border: 1px solid var(--border-color); + border-radius: 7px; + color: var(--preview-text-color); + background: var(--button-bg); + font-size: 12px; + font-weight: 650; + text-decoration: none; + transition: border-color 0.18s ease, background-color 0.18s ease; +} + +.markdown-body .release-note-actions a i { + flex: 0 0 auto; + font-size: 15px; +} + +.markdown-body .release-note-actions a:first-child { + border-color: hsl(var(--release-note-accent-hsl) / 0.54); + color: var(--release-note-accent); + background: var(--release-note-accent-soft); +} + +.markdown-body .release-note-actions a:hover, +.markdown-body .release-note-actions a:focus-visible { + border-color: var(--release-note-accent); + background: var(--button-hover); +} + +.release-note-intro > hr { + display: none; +} + +.release-note-layout { + position: relative; + display: grid; + grid-template-columns: minmax(170px, 210px) minmax(0, 1fr); + align-items: start; + gap: clamp(34px, 6vw, 72px); + padding-top: clamp(42px, 7vw, 76px); +} + +.release-note-skip-link { + position: absolute; + z-index: 6; + top: 8px; + left: 0; + padding: 8px 10px; + border-radius: 6px; + color: var(--preview-text-color); + background: var(--button-bg); + opacity: 0; + pointer-events: none; + transform: translateY(-8px); +} + +.release-note-skip-link:focus { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.release-note-navigation { + position: sticky; + top: 18px; + max-height: calc(100vh - 132px); + overflow-y: auto; + padding-bottom: 12px; + scrollbar-width: thin; +} + +.release-note-navigation-label, +.markdown-body .release-note-navigation-label { + margin: 0 0 13px; + color: var(--text-secondary); + font-size: 10px; + font-weight: 760; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.markdown-body .release-note-navigation a { + position: relative; + display: flex; + min-height: 38px; + align-items: center; + padding: 7px 10px 7px 15px; + border-radius: 6px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.4; + text-decoration: none; + transition: color 0.16s ease, background-color 0.16s ease; +} + +.markdown-body .release-note-navigation a::before { + position: absolute; + top: 9px; + bottom: 9px; + left: 0; + width: 2px; + border-radius: 2px; + background: transparent; + content: ''; +} + +.markdown-body .release-note-navigation a:hover, +.markdown-body .release-note-navigation a:focus-visible { + color: var(--preview-text-color); + background: var(--button-hover); +} + +.markdown-body .release-note-navigation a.is-active, +.markdown-body .release-note-navigation a[aria-current="location"] { + color: var(--release-note-accent); + background: var(--release-note-accent-soft); + font-weight: 650; +} + +.markdown-body .release-note-navigation a.is-active::before, +.markdown-body .release-note-navigation a[aria-current="location"]::before { + background: var(--release-note-accent); +} + +.release-note-content { + min-width: 0; + max-width: 790px; +} + +.release-note-section { + padding-bottom: clamp(48px, 7vw, 78px); + margin-bottom: clamp(48px, 7vw, 78px); + border-bottom: 1px solid var(--border-color); + scroll-margin-top: 28px; +} + +.markdown-body .release-note-section h2 { + max-width: 720px; + padding: 0; + margin: 0 0 18px; + border: 0; + color: var(--preview-text-color); + font-size: clamp(25px, 3.2vw, 36px); + font-weight: 680; + letter-spacing: -0.035em; + line-height: 1.15; + text-wrap: balance; +} + +.markdown-body .release-note-section h3 { + margin: 30px 0 10px; + color: var(--preview-text-color); + font-size: 16px; + font-weight: 650; +} + +.release-note-section > p, +.markdown-body .release-note-section > p { + max-width: 75ch; + color: var(--text-secondary); +} + +.markdown-body .release-note-section:first-child > ul { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + padding: 0; + margin: 26px 0; + list-style: none; +} + +.markdown-body .release-note-section:first-child > ul > li { + min-width: 0; + padding: 17px; + border: 1px solid var(--border-color); + border-radius: 10px; + color: var(--text-secondary); + background: var(--release-note-surface); + font-size: 13px; +} + +.markdown-body .release-note-section:first-child > ul > li strong { + color: var(--preview-text-color); +} + +.markdown-body .release-note-section:first-child > ul > li:last-child:nth-child(odd) { + grid-column: 1 / -1; +} + +.release-note-section:last-child { + padding: clamp(24px, 4vw, 36px); + margin-bottom: 0; + border: 1px solid hsl(var(--release-note-accent-hsl) / 0.3); + border-radius: 14px; + background: var(--release-note-accent-soft); +} + +.markdown-body .release-note-back-to-top { + position: fixed; + z-index: 12; + right: 26px; + bottom: 46px; + display: inline-flex; + width: 44px; + height: 44px; + align-items: center; + justify-content: center; + border: 1px solid var(--border-color); + border-radius: 50%; + color: var(--preview-text-color); + background: color-mix(in srgb, var(--button-bg) 92%, transparent); + box-shadow: 0 8px 24px color-mix(in srgb, var(--text-color) 16%, transparent); + text-decoration: none; + backdrop-filter: blur(10px); + transition: border-color 0.18s ease, background-color 0.18s ease; +} + +.markdown-body .release-note-back-to-top:hover, +.markdown-body .release-note-back-to-top:focus-visible { + border-color: var(--release-note-accent); + color: var(--release-note-accent); + background: var(--button-hover); +} + +@media (max-width: 900px) { + .markdown-body .release-note-intro h1 { + text-wrap: balance; + white-space: normal; + } +} + +@media (max-width: 760px) { + #markdown-preview.release-notes-preview { + padding: 18px; + } + + .release-note-layout { + display: block; + padding-top: 34px; + } + + .release-note-navigation { + top: 0; + z-index: 5; + display: flex; + max-height: none; + gap: 3px; + margin: 0 -18px 44px; + padding: 9px 18px; + overflow-x: auto; + overflow-y: hidden; + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + background: color-mix(in srgb, var(--preview-bg) 94%, transparent); + backdrop-filter: blur(10px); + scrollbar-width: none; + } + + .release-note-navigation::-webkit-scrollbar { + display: none; + } + + .release-note-navigation-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + .markdown-body .release-note-navigation a { + flex: 0 0 auto; + min-height: 44px; + padding: 9px 12px; + white-space: nowrap; + } + + .markdown-body .release-note-navigation a::before { + top: auto; + right: 12px; + bottom: 3px; + left: 12px; + width: auto; + height: 2px; + } + + .release-note-section { + scroll-margin-top: 72px; + } + + .markdown-body .release-note-back-to-top { + right: 18px; + bottom: 42px; + } +} + +@media (max-width: 520px) { + .markdown-body .release-note-intro h1 { + font-size: clamp(30px, 9vw, 40px); + } + + .release-note-brandline { + flex-wrap: wrap; + } + + .release-note-actions { + align-items: stretch; + flex-direction: column; + } + + .markdown-body .release-note-actions a { + width: 100%; + } + + .markdown-body .release-note-section:first-child > ul { + grid-template-columns: 1fr; + } + + .tab-item.release-notes-tab { + max-width: none !important; + } +} + +@media (prefers-reduced-motion: reduce) { + .preview-pane.release-notes-surface { + scroll-behavior: auto; + } + + .release-note-actions a, + .release-note-navigation a, + .release-note-back-to-top { + transition: none !important; + } +} + +@media print { + .release-note-navigation, + .release-note-skip-link, + .release-note-back-to-top { + display: none !important; + } + + .release-note-layout { + display: block; + } + + .release-note-section { + break-inside: avoid; + } +} diff --git a/sw.js b/sw.js index 2c2af8bf..423d8dd7 100644 --- a/sw.js +++ b/sw.js @@ -10,6 +10,7 @@ const CRITICAL_ASSETS = [ './preview-worker.js', './styles.css', './assets/lucide-icons.css', + './RELEASE_NOTES', './sample.md', './assets/icon.jpg', './manifest.json' diff --git a/wiki/Contributing.md b/wiki/Contributing.md index 948ec750..e3f0c9b2 100644 --- a/wiki/Contributing.md +++ b/wiki/Contributing.md @@ -88,6 +88,49 @@ When changing managed media, snapshot, or live behavior, update the relevant fil Detailed Wiki pages are maintained in English. When no localized page exists, label the English destination instead of creating a broken localized link. +## Release Notes Format + +Use the single extensionless [`RELEASE_NOTES`](../RELEASE_NOTES) file as both the current release note and the canonical example. Update it in place for each version; do not add a duplicate template or a versioned copy. Keep its source entirely in Markdown. The application adds the branded layout, action icons, section navigation, and active-section state after Markdown rendering. + +Every release note must contain these parts in this order: + +1. Brand line, version heading, unambiguous release date, one- or two-sentence summary, and the release/changelog links. +2. **Highlights** with at least one user-facing bullet. +3. Zero or more detail sections, each covering one user-facing topic. +4. **Thank you** as the final section, with contributor and change-reference bullets when available. + +Scale the same format to the release size: + +| Release scope | Highlights | Detail sections | Change references | +| :--- | :--- | :--- | :--- | +| Single fix or one commit | 1 | 0–1 | Link the issue, PR, or commit when useful | +| Small feature or maintenance update | 2–3 | 1–2 | List only the related PRs/issues | +| Large feature release | 3–5 | 2–6 | Curate the important PRs/issues; link the release for the full history | + +Authoring rules: + +- Use the Markdown syntax supported by the editor: headings, paragraphs, emphasis, inline or fenced code, links, images, blockquotes, GitHub-style alerts, ordered or unordered lists, task lists, tables, definition lists, footnotes, and horizontal rules. Use a construct only when it helps explain the release. +- Do not embed HTML elements, inline styles, `