From ebb595c1b835bfd4d8d592800b77126eb07b242b Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Mon, 3 Aug 2026 03:44:59 +0530 Subject: [PATCH 1/6] feat(releases): add in-app release notes --- CHANGELOG.md | 11 ++ Dockerfile | 1 + RELEASE_NOTES | 23 +++ assets/i18n/de.json | 9 +- assets/i18n/en.json | 7 +- assets/i18n/es.json | 9 +- assets/i18n/fr.json | 9 +- assets/i18n/generate-ui-locales.mjs | 1 + assets/i18n/it.json | 9 +- assets/i18n/ja.json | 9 +- assets/i18n/ko.json | 9 +- assets/i18n/pl.json | 9 +- assets/i18n/pt.json | 9 +- assets/i18n/ru.json | 9 +- assets/i18n/tr.json | 9 +- assets/i18n/tw.json | 9 +- assets/i18n/uk.json | 9 +- assets/i18n/zh.json | 9 +- desktop-app/neutralino.config.json | 2 +- desktop-app/package-lock.json | 4 +- desktop-app/package.json | 2 +- desktop-app/prepare.js | 3 + desktop-app/resources/RELEASE_NOTES | 23 +++ desktop-app/resources/index.html | 6 +- desktop-app/resources/js/script.js | 226 ++++++++++++++++++++++------ desktop-app/resources/styles.css | 9 ++ index.html | 6 +- script.js | 226 ++++++++++++++++++++++------ styles.css | 9 ++ sw.js | 3 +- 30 files changed, 531 insertions(+), 148 deletions(-) create mode 100644 RELEASE_NOTES create mode 100644 desktop-app/resources/RELEASE_NOTES diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee47c30..20fa65b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable code changes to **Markdown Viewer** are documented here. Non-code commits (documentation, planning, README-only updates) are excluded. +## v3.11.0 + +- **Description:** Added a VS Code-style, once-per-version release-note experience inside the existing document workspace. + - **Update Experience:** Opens the current release note automatically after a version change and remembers dismissal only after the user closes the tab. + - **Tabs & About:** Presents the extensionless, read-only `RELEASE_NOTES` document in the normal tab list with the Markdown Viewer app logo, keeps it out of Explorer and document storage, and replaces the external Changelog action in About with Show Release Notes. + - **Offline & Desktop:** Precached the release note for the PWA, bundled it into Neutralino desktop builds, synchronized the application/package/cache version at 3.11.0, and persisted version visibility across desktop launches. +- **Date:** 2026-08-03 +- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/releases/tag/v3.11.0 + +--- + ## v3.10.0 - **Description:** Introduced scalable local document storage, complete workspace backup and recovery, and a rebuilt GitHub importer with secure private-repository access. 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..a25a97df --- /dev/null +++ b/RELEASE_NOTES @@ -0,0 +1,23 @@ +# Markdown Viewer 3.11.0 + +_Released August 3, 2026_ + +This update makes it easier to understand what changed without leaving your workspace. + +## Release notes in your workspace + +- Release notes now open automatically in the tab list after Markdown Viewer updates to a new version. +- The note opens once per version. Close its tab after reading it and it stays dismissed until the next update. +- The release-note tab is read only, uses Markdown Viewer's app logo, and behaves like a temporary file without being added to Explorer or saved with your documents. + +## Easier to find again + +- **About Markdown Viewer** now includes **Show Release Notes**, which reopens the current version's note in the tab list at any time. +- The bundled `RELEASE_NOTES` document has no file extension and remains available offline in the web, PWA, and desktop builds. + +## Consistent versioning + +- The web app, offline cache, and Neutralino desktop packages now report version **3.11.0**. +- Release-note visibility is tracked locally per version and does not require an account or network request. + +For the complete project history, see the [changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md). diff --git a/assets/i18n/de.json b/assets/i18n/de.json index 1d6edc36..1e412a48 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", @@ -1156,5 +1154,10 @@ "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." } diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 92c024aa..66b080b3 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -120,7 +120,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", @@ -620,7 +619,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 +799,9 @@ "Reference": "Reference", "Reference Number": "Reference Number", "Reference title": "Reference title", + "Release notes are read only.": "Release notes are read only.", + "Release notes opened.": "Release notes opened.", + "Release notes unavailable": "Release notes unavailable", "Reloading restored workspace…": "Reloading restored workspace…", "Remove from Favorites": "Remove from Favorites", "Remove selected access token": "Remove selected access token", @@ -952,6 +953,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.", @@ -1034,6 +1036,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..0033dd45 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", @@ -1156,5 +1154,10 @@ "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." } diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json index c518d75f..9d1e6e15 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", @@ -1156,5 +1154,10 @@ "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." } 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..e69bc363 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", @@ -1156,5 +1154,10 @@ "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." } diff --git a/assets/i18n/ja.json b/assets/i18n/ja.json index 75c20609..d49f2a4f 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": "キャンセル", @@ -1156,5 +1154,10 @@ "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.": "リリースノートを公開しました。" } diff --git a/assets/i18n/ko.json b/assets/i18n/ko.json index 37eda176..0aaee751 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": "취소", @@ -1156,5 +1154,10 @@ "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.": "릴리스 노트가 열렸습니다." } diff --git a/assets/i18n/pl.json b/assets/i18n/pl.json index 7dbbe2ea..54d3c0bf 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", @@ -1156,5 +1154,10 @@ "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." } diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json index e1f6fc2c..3b2772ef 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", @@ -1156,5 +1154,10 @@ "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." } diff --git a/assets/i18n/ru.json b/assets/i18n/ru.json index 73cdc9bd..480b8137 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": "отменить", @@ -1156,5 +1154,10 @@ "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.": "Открыты примечания к выпуску." } diff --git a/assets/i18n/tr.json b/assets/i18n/tr.json index 88341f2e..a7abb6fc 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", @@ -1156,5 +1154,10 @@ "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ı." } diff --git a/assets/i18n/tw.json b/assets/i18n/tw.json index 45eb939e..243b8f18 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": "取消", @@ -1156,5 +1154,10 @@ "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.": "發行說明已開啟。" } diff --git a/assets/i18n/uk.json b/assets/i18n/uk.json index 914b3295..b0b275b0 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": "скасувати", @@ -1156,5 +1154,10 @@ "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.": "Відкрито примітки до випуску." } diff --git a/assets/i18n/zh.json b/assets/i18n/zh.json index 260897be..eed575c0 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": "取消", @@ -1156,5 +1154,10 @@ "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.": "发行说明已打开。" } diff --git a/desktop-app/neutralino.config.json b/desktop-app/neutralino.config.json index 8a7badc4..13a16853 100644 --- a/desktop-app/neutralino.config.json +++ b/desktop-app/neutralino.config.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json", "applicationId": "com.markdownviewer.desktop", - "version": "3.10.0", + "version": "3.11.0", "defaultMode": "window", "port": 0, "documentRoot": "/resources/", diff --git a/desktop-app/package-lock.json b/desktop-app/package-lock.json index 0b6f8e96..3f37c60e 100644 --- a/desktop-app/package-lock.json +++ b/desktop-app/package-lock.json @@ -1,12 +1,12 @@ { "name": "markdown-viewer-desktop", - "version": "3.10.0", + "version": "3.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "markdown-viewer-desktop", - "version": "3.10.0" + "version": "3.11.0" } } } diff --git a/desktop-app/package.json b/desktop-app/package.json index ff75c5ec..76809ed7 100644 --- a/desktop-app/package.json +++ b/desktop-app/package.json @@ -1,6 +1,6 @@ { "name": "markdown-viewer-desktop", - "version": "3.10.0", + "version": "3.11.0", "private": true, "description": "A lightweight Neutralinojs desktop build for Markdown Viewer with live preview, diagrams, math, and PDF/HTML/PNG exports.", "scripts": { 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..a25a97df --- /dev/null +++ b/desktop-app/resources/RELEASE_NOTES @@ -0,0 +1,23 @@ +# Markdown Viewer 3.11.0 + +_Released August 3, 2026_ + +This update makes it easier to understand what changed without leaving your workspace. + +## Release notes in your workspace + +- Release notes now open automatically in the tab list after Markdown Viewer updates to a new version. +- The note opens once per version. Close its tab after reading it and it stays dismissed until the next update. +- The release-note tab is read only, uses Markdown Viewer's app logo, and behaves like a temporary file without being added to Explorer or saved with your documents. + +## Easier to find again + +- **About Markdown Viewer** now includes **Show Release Notes**, which reopens the current version's note in the tab list at any time. +- The bundled `RELEASE_NOTES` document has no file extension and remains available offline in the web, PWA, and desktop builds. + +## Consistent versioning + +- The web app, offline cache, and Neutralino desktop packages now report version **3.11.0**. +- Release-note visibility is tracked locally per version and does not require an account or network request. + +For the complete project history, see the [changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md). diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index cfc05dc4..564447dc 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -845,9 +845,9 @@

Markdown Viewer

GitHub Repository - - Changelog - + Apache License 2.0 diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index c47a5626..df5113fc 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -1,6 +1,11 @@ document.addEventListener("DOMContentLoaded", async function () { window.alert = function(message) { showAppToast(message); }; const PRIVATE_MODE_KEY = 'markdownViewerPrivateMode'; + const APP_VERSION = '3.11.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_SEEN_VERSION_KEY = 'markdownViewerReleaseNotesSeenVersion'; const DOCUMENT_STORAGE_KEYS = new Set([ 'markdownViewerGlobalState', 'markdownViewerTabs', @@ -46,6 +51,9 @@ document.addEventListener("DOMContentLoaded", async function () { 'markdownViewerUntitledCounter', 'markdownViewerDocumentOrganization', 'markdownViewerSecretWorkspace', + RELEASE_NOTES_LAST_VERSION_KEY, + RELEASE_NOTES_PENDING_VERSION_KEY, + RELEASE_NOTES_SEEN_VERSION_KEY, 'find-replace-docked', 'app-lang' ]; @@ -288,7 +296,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 +436,88 @@ 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); + } + } + + 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); + const versionChanged = Boolean(previousVersion && previousVersion !== APP_VERSION); + const featureUpgrade = Boolean(!previousVersion && hadExistingWorkspace); + + if ((versionChanged || featureUpgrade) && seenVersion !== APP_VERSION) { + pendingVersion = APP_VERSION; + saveStorageItem(RELEASE_NOTES_PENDING_VERSION_KEY, APP_VERSION); + } + saveStorageItem(RELEASE_NOTES_LAST_VERSION_KEY, APP_VERSION); + return pendingVersion === APP_VERSION && seenVersion !== APP_VERSION; + } + + let releaseNotesContentPromise = null; + + 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 existing = tabs.find(function(tab) { + return isReleaseNotesTab(tab) && tab.releaseVersion === APP_VERSION; + }); + if (existing) { + await switchTab(existing.id); + updateLiveEditorAccess(); + 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); + await switchTab(tab.id); + updateLiveEditorAccess(); + if (!settings.silent) { + announceToScreenReader('Release notes opened.'); + } + return tab; + } + function stripTemporaryTabs(tabsArr) { return (tabsArr || []).filter(function(tab) { return !isShareSnapshotTab(tab) && tab.temporary !== true; @@ -481,6 +570,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 +684,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"); @@ -5404,7 +5497,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'; @@ -7725,29 +7820,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 +7945,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 +7991,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 +8002,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 +8089,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 +8103,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 +8393,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 +8528,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 +8587,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 +8609,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 +8792,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 +8805,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 +9115,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 +9216,7 @@ document.addEventListener("DOMContentLoaded", async function () { openDocumentSurfaceContextMenu(surface, event); }); }); + return hadExistingWorkspace; } // Late-load callback hook for Neutralino command-line files @@ -13080,7 +13194,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 +18087,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 +18812,11 @@ ${selector} .arrowheadPath { }); } - await initTabs(); + const hadExistingWorkspace = await initTabs(); initReviewMode(); + if (queueCurrentReleaseNotes(hadExistingWorkspace)) { + await openReleaseNotes(); + } if (loadGlobalState().syncScrollingEnabled === false) toggleSyncScrolling(); updateMobileStats(); updateFindHighlights(); @@ -21773,7 +21902,7 @@ ${selector} .arrowheadPath { } function canMutateEditor() { - return hasActiveOpenDocument() && !reviewModeActive && !isLiveViewOnlyParticipant() && !isShareSnapshotViewOnlyActive(); + return hasActiveOpenDocument() && !reviewModeActive && !isLiveViewOnlyParticipant() && !isShareSnapshotViewOnlyActive() && !isReleaseNotesActive(); } function isLiveMutatingAction(action) { @@ -21808,10 +21937,11 @@ ${selector} .arrowheadPath { 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 +21952,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 +21967,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; diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index fe3f611f..826b24e8 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -4464,6 +4464,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 +5167,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; diff --git a/index.html b/index.html index 4761d597..9942d736 100644 --- a/index.html +++ b/index.html @@ -938,9 +938,9 @@

Markdown Viewer

GitHub Repository - - Changelog - + Apache License 2.0 diff --git a/script.js b/script.js index c47a5626..df5113fc 100644 --- a/script.js +++ b/script.js @@ -1,6 +1,11 @@ document.addEventListener("DOMContentLoaded", async function () { window.alert = function(message) { showAppToast(message); }; const PRIVATE_MODE_KEY = 'markdownViewerPrivateMode'; + const APP_VERSION = '3.11.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_SEEN_VERSION_KEY = 'markdownViewerReleaseNotesSeenVersion'; const DOCUMENT_STORAGE_KEYS = new Set([ 'markdownViewerGlobalState', 'markdownViewerTabs', @@ -46,6 +51,9 @@ document.addEventListener("DOMContentLoaded", async function () { 'markdownViewerUntitledCounter', 'markdownViewerDocumentOrganization', 'markdownViewerSecretWorkspace', + RELEASE_NOTES_LAST_VERSION_KEY, + RELEASE_NOTES_PENDING_VERSION_KEY, + RELEASE_NOTES_SEEN_VERSION_KEY, 'find-replace-docked', 'app-lang' ]; @@ -288,7 +296,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 +436,88 @@ 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); + } + } + + 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); + const versionChanged = Boolean(previousVersion && previousVersion !== APP_VERSION); + const featureUpgrade = Boolean(!previousVersion && hadExistingWorkspace); + + if ((versionChanged || featureUpgrade) && seenVersion !== APP_VERSION) { + pendingVersion = APP_VERSION; + saveStorageItem(RELEASE_NOTES_PENDING_VERSION_KEY, APP_VERSION); + } + saveStorageItem(RELEASE_NOTES_LAST_VERSION_KEY, APP_VERSION); + return pendingVersion === APP_VERSION && seenVersion !== APP_VERSION; + } + + let releaseNotesContentPromise = null; + + 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 existing = tabs.find(function(tab) { + return isReleaseNotesTab(tab) && tab.releaseVersion === APP_VERSION; + }); + if (existing) { + await switchTab(existing.id); + updateLiveEditorAccess(); + 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); + await switchTab(tab.id); + updateLiveEditorAccess(); + if (!settings.silent) { + announceToScreenReader('Release notes opened.'); + } + return tab; + } + function stripTemporaryTabs(tabsArr) { return (tabsArr || []).filter(function(tab) { return !isShareSnapshotTab(tab) && tab.temporary !== true; @@ -481,6 +570,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 +684,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"); @@ -5404,7 +5497,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'; @@ -7725,29 +7820,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 +7945,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 +7991,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 +8002,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 +8089,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 +8103,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 +8393,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 +8528,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 +8587,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 +8609,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 +8792,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 +8805,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 +9115,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 +9216,7 @@ document.addEventListener("DOMContentLoaded", async function () { openDocumentSurfaceContextMenu(surface, event); }); }); + return hadExistingWorkspace; } // Late-load callback hook for Neutralino command-line files @@ -13080,7 +13194,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 +18087,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 +18812,11 @@ ${selector} .arrowheadPath { }); } - await initTabs(); + const hadExistingWorkspace = await initTabs(); initReviewMode(); + if (queueCurrentReleaseNotes(hadExistingWorkspace)) { + await openReleaseNotes(); + } if (loadGlobalState().syncScrollingEnabled === false) toggleSyncScrolling(); updateMobileStats(); updateFindHighlights(); @@ -21773,7 +21902,7 @@ ${selector} .arrowheadPath { } function canMutateEditor() { - return hasActiveOpenDocument() && !reviewModeActive && !isLiveViewOnlyParticipant() && !isShareSnapshotViewOnlyActive(); + return hasActiveOpenDocument() && !reviewModeActive && !isLiveViewOnlyParticipant() && !isShareSnapshotViewOnlyActive() && !isReleaseNotesActive(); } function isLiveMutatingAction(action) { @@ -21808,10 +21937,11 @@ ${selector} .arrowheadPath { 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 +21952,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 +21967,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; diff --git a/styles.css b/styles.css index fe3f611f..826b24e8 100644 --- a/styles.css +++ b/styles.css @@ -4464,6 +4464,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 +5167,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; diff --git a/sw.js b/sw.js index 2c2af8bf..d3436ef8 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'markdown-viewer-cache-v3.10.0'; +const CACHE_NAME = 'markdown-viewer-cache-v3.11.0'; // PERF-011: Split precache into critical (local files) and lazy (CDN libraries) // Critical assets are precached during SW install for instant offline startup @@ -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' From ce3d310dac5fe25c9a710d0a861b94df85851cfc Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Mon, 3 Aug 2026 21:00:37 +0530 Subject: [PATCH 2/6] feat(releases): refine v3.10 release experience --- CHANGELOG.md | 12 +- RELEASE_NOTES | 111 +++++- assets/i18n/de.json | 13 +- assets/i18n/en.json | 9 + assets/i18n/es.json | 13 +- assets/i18n/fr.json | 13 +- assets/i18n/it.json | 13 +- assets/i18n/ja.json | 13 +- assets/i18n/ko.json | 13 +- assets/i18n/pl.json | 13 +- assets/i18n/pt.json | 13 +- assets/i18n/ru.json | 13 +- assets/i18n/tr.json | 13 +- assets/i18n/tw.json | 13 +- assets/i18n/uk.json | 13 +- assets/i18n/zh.json | 13 +- desktop-app/neutralino.config.json | 2 +- desktop-app/package-lock.json | 4 +- desktop-app/package.json | 2 +- desktop-app/resources/RELEASE_NOTES | 111 +++++- desktop-app/resources/js/script.js | 198 ++++++++++- desktop-app/resources/styles.css | 507 ++++++++++++++++++++++++++++ script.js | 198 ++++++++++- styles.css | 507 ++++++++++++++++++++++++++++ sw.js | 2 +- 25 files changed, 1738 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20fa65b5..3da75b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,20 +3,10 @@ All notable code changes to **Markdown Viewer** are documented here. Non-code commits (documentation, planning, README-only updates) are excluded. -## v3.11.0 - -- **Description:** Added a VS Code-style, once-per-version release-note experience inside the existing document workspace. - - **Update Experience:** Opens the current release note automatically after a version change and remembers dismissal only after the user closes the tab. - - **Tabs & About:** Presents the extensionless, read-only `RELEASE_NOTES` document in the normal tab list with the Markdown Viewer app logo, keeps it out of Explorer and document storage, and replaces the external Changelog action in About with Show Release Notes. - - **Offline & Desktop:** Precached the release note for the PWA, bundled it into Neutralino desktop builds, synchronized the application/package/cache version at 3.11.0, and persisted version visibility across desktop launches. -- **Date:** 2026-08-03 -- **URL:** https://github.com/ThisIs-Developer/Markdown-Viewer/releases/tag/v3.11.0 - ---- - ## 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 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, and a Show Release Notes action in About. - **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/RELEASE_NOTES b/RELEASE_NOTES index a25a97df..1f1ded66 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -1,23 +1,106 @@ -# Markdown Viewer 3.11.0 +

Markdown Viewer releasev3.10.0

-_Released August 3, 2026_ +# Welcome to Markdown Viewer 3.10.0 -This update makes it easier to understand what changed without leaving your workspace. +

Released August 2, 202640 commits4 merged pull requests

-## Release notes in your workspace +This release gives your workspace room to grow, makes complete backups practical, and rebuilds GitHub importing for large and private repositories. -- Release notes now open automatically in the tab list after Markdown Viewer updates to a new version. -- The note opens once per version. Close its tab after reading it and it stays dismissed until the next update. -- The release-note tab is read only, uses Markdown Viewer's app logo, and behaves like a temporary file without being added to Explorer or saved with your documents. +Everything here ships with the web, PWA, Docker, and desktop editions. **Happy coding!** -## Easier to find again + -- **About Markdown Viewer** now includes **Show Release Notes**, which reopens the current version's note in the tab list at any time. -- The bundled `RELEASE_NOTES` document has no file extension and remains available offline in the web, PWA, and desktop builds. +--- -## Consistent versioning +## Highlights -- The web app, offline cache, and Neutralino desktop packages now report version **3.11.0**. -- Release-note visibility is tracked locally per version and does not require an account or network request. +- **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. -For the complete project history, see the [changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md). +> [!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. + +## How this release-note tab works + +On a first run, **Welcome to Markdown** stays selected and **Release Notes: 3.10.0** appears beside it. Select the release-note tab whenever you are ready. + +For an existing workspace, Markdown Viewer compares its current application version with the locally stored version. When they differ, the current note opens automatically. + +Closing this tab marks version 3.10.0 as seen. It will stay closed on future launches and return only after another version change. You can always reopen the current note from **About Markdown Viewer → Show Release Notes**. + +## Release figures + +| Figure | v3.10.0 | +| --- | ---: | +| Merged pull requests | 4 | +| Commits since v3.9.4 | 40 | +| Files changed | 53 | +| Lines added | 12,584 | +| Lines removed | 2,094 | +| Contributors | 1 | + +The figures come from GitHub's comparison of `v3.9.4...v3.10.0`. No standalone issues were linked to this release. + +### Pull requests included + +| PR | Change | Contributor | +| --- | --- | --- | +| [#230](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/230) | Unlimited storage, backup, reset, and fixed vault | @ThisIs-Developer | +| [#231](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/231) | Preserve the complete #230 commit history | @ThisIs-Developer | +| [#232](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/232) | Refine storage documentation highlights | @ThisIs-Developer | +| [#233](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/233) | Scale GitHub imports and simplify private access | @ThisIs-Developer | + +## Contributor + +Thank you to **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** for the design, implementation, testing, documentation, and release work across all four pull requests. + +## Thank you + +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 1e412a48..16763951 100644 --- a/assets/i18n/de.json +++ b/assets/i18n/de.json @@ -1016,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", @@ -1159,5 +1158,15 @@ "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." + "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 66b080b3..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", @@ -182,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", @@ -414,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", @@ -527,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", @@ -799,9 +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", @@ -961,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", diff --git a/assets/i18n/es.json b/assets/i18n/es.json index 0033dd45..53ebfc0b 100644 --- a/assets/i18n/es.json +++ b/assets/i18n/es.json @@ -1016,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", @@ -1159,5 +1158,15 @@ "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." + "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 9d1e6e15..528ded43 100644 --- a/assets/i18n/fr.json +++ b/assets/i18n/fr.json @@ -1016,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", @@ -1159,5 +1158,15 @@ "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." + "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/it.json b/assets/i18n/it.json index e69bc363..72af1813 100644 --- a/assets/i18n/it.json +++ b/assets/i18n/it.json @@ -1016,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", @@ -1159,5 +1158,15 @@ "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." + "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 d49f2a4f..97c07b1f 100644 --- a/assets/i18n/ja.json +++ b/assets/i18n/ja.json @@ -1016,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": "ドキュメントが利用できません", @@ -1159,5 +1158,15 @@ "Show Release Notes": "リリースノートを表示", "The release notes could not be opened.": "リリースノートを開けませんでした。", "Release notes are read only.": "リリースノートは読み取り専用です。", - "Release notes opened.": "リリースノートを公開しました。" + "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 0aaee751..6c339969 100644 --- a/assets/i18n/ko.json +++ b/assets/i18n/ko.json @@ -1016,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": "문서를 사용할 수 없습니다.", @@ -1159,5 +1158,15 @@ "Show Release Notes": "출시 노트 보기", "The release notes could not be opened.": "릴리스 노트를 열 수 없습니다.", "Release notes are read only.": "릴리스 노트는 읽기 전용입니다.", - "Release notes opened.": "릴리스 노트가 열렸습니다." + "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 54d3c0bf..59fedd68 100644 --- a/assets/i18n/pl.json +++ b/assets/i18n/pl.json @@ -1016,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", @@ -1159,5 +1158,15 @@ "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." + "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 3b2772ef..b353cbc7 100644 --- a/assets/i18n/pt.json +++ b/assets/i18n/pt.json @@ -1016,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", @@ -1159,5 +1158,15 @@ "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." + "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 480b8137..b2e6284f 100644 --- a/assets/i18n/ru.json +++ b/assets/i18n/ru.json @@ -1016,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": "Документ недоступен", @@ -1159,5 +1158,15 @@ "Show Release Notes": "Показать примечания к выпуску", "The release notes could not be opened.": "Не удалось открыть примечания к выпуску.", "Release notes are read only.": "Примечания к выпуску доступны только для чтения.", - "Release notes opened.": "Открыты примечания к выпуску." + "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 a7abb6fc..9b683e55 100644 --- a/assets/i18n/tr.json +++ b/assets/i18n/tr.json @@ -1016,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", @@ -1159,5 +1158,15 @@ "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ı." + "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 243b8f18..8b2d93fe 100644 --- a/assets/i18n/tw.json +++ b/assets/i18n/tw.json @@ -1016,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": "文件不可用", @@ -1159,5 +1158,15 @@ "Show Release Notes": "顯示發行說明", "The release notes could not be opened.": "無法開啟發行說明。", "Release notes are read only.": "發行說明是唯讀的。", - "Release notes opened.": "發行說明已開啟。" + "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 b0b275b0..c9adfedf 100644 --- a/assets/i18n/uk.json +++ b/assets/i18n/uk.json @@ -1016,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": "Документ недоступний", @@ -1159,5 +1158,15 @@ "Show Release Notes": "Показати примітки до випуску", "The release notes could not be opened.": "Не вдалося відкрити примітки до випуску.", "Release notes are read only.": "Примітки до випуску доступні лише для читання.", - "Release notes opened.": "Відкрито примітки до випуску." + "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 eed575c0..e16c6555 100644 --- a/assets/i18n/zh.json +++ b/assets/i18n/zh.json @@ -1016,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": "文档不可用", @@ -1159,5 +1158,15 @@ "Show Release Notes": "显示发行说明", "The release notes could not be opened.": "无法打开发行说明。", "Release notes are read only.": "发行说明是只读的。", - "Release notes opened.": "发行说明已打开。" + "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/neutralino.config.json b/desktop-app/neutralino.config.json index 13a16853..8a7badc4 100644 --- a/desktop-app/neutralino.config.json +++ b/desktop-app/neutralino.config.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json", "applicationId": "com.markdownviewer.desktop", - "version": "3.11.0", + "version": "3.10.0", "defaultMode": "window", "port": 0, "documentRoot": "/resources/", diff --git a/desktop-app/package-lock.json b/desktop-app/package-lock.json index 3f37c60e..0b6f8e96 100644 --- a/desktop-app/package-lock.json +++ b/desktop-app/package-lock.json @@ -1,12 +1,12 @@ { "name": "markdown-viewer-desktop", - "version": "3.11.0", + "version": "3.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "markdown-viewer-desktop", - "version": "3.11.0" + "version": "3.10.0" } } } diff --git a/desktop-app/package.json b/desktop-app/package.json index 76809ed7..ff75c5ec 100644 --- a/desktop-app/package.json +++ b/desktop-app/package.json @@ -1,6 +1,6 @@ { "name": "markdown-viewer-desktop", - "version": "3.11.0", + "version": "3.10.0", "private": true, "description": "A lightweight Neutralinojs desktop build for Markdown Viewer with live preview, diagrams, math, and PDF/HTML/PNG exports.", "scripts": { diff --git a/desktop-app/resources/RELEASE_NOTES b/desktop-app/resources/RELEASE_NOTES index a25a97df..1f1ded66 100644 --- a/desktop-app/resources/RELEASE_NOTES +++ b/desktop-app/resources/RELEASE_NOTES @@ -1,23 +1,106 @@ -# Markdown Viewer 3.11.0 +

Markdown Viewer releasev3.10.0

-_Released August 3, 2026_ +# Welcome to Markdown Viewer 3.10.0 -This update makes it easier to understand what changed without leaving your workspace. +

Released August 2, 202640 commits4 merged pull requests

-## Release notes in your workspace +This release gives your workspace room to grow, makes complete backups practical, and rebuilds GitHub importing for large and private repositories. -- Release notes now open automatically in the tab list after Markdown Viewer updates to a new version. -- The note opens once per version. Close its tab after reading it and it stays dismissed until the next update. -- The release-note tab is read only, uses Markdown Viewer's app logo, and behaves like a temporary file without being added to Explorer or saved with your documents. +Everything here ships with the web, PWA, Docker, and desktop editions. **Happy coding!** -## Easier to find again + -- **About Markdown Viewer** now includes **Show Release Notes**, which reopens the current version's note in the tab list at any time. -- The bundled `RELEASE_NOTES` document has no file extension and remains available offline in the web, PWA, and desktop builds. +--- -## Consistent versioning +## Highlights -- The web app, offline cache, and Neutralino desktop packages now report version **3.11.0**. -- Release-note visibility is tracked locally per version and does not require an account or network request. +- **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. -For the complete project history, see the [changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md). +> [!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. + +## How this release-note tab works + +On a first run, **Welcome to Markdown** stays selected and **Release Notes: 3.10.0** appears beside it. Select the release-note tab whenever you are ready. + +For an existing workspace, Markdown Viewer compares its current application version with the locally stored version. When they differ, the current note opens automatically. + +Closing this tab marks version 3.10.0 as seen. It will stay closed on future launches and return only after another version change. You can always reopen the current note from **About Markdown Viewer → Show Release Notes**. + +## Release figures + +| Figure | v3.10.0 | +| --- | ---: | +| Merged pull requests | 4 | +| Commits since v3.9.4 | 40 | +| Files changed | 53 | +| Lines added | 12,584 | +| Lines removed | 2,094 | +| Contributors | 1 | + +The figures come from GitHub's comparison of `v3.9.4...v3.10.0`. No standalone issues were linked to this release. + +### Pull requests included + +| PR | Change | Contributor | +| --- | --- | --- | +| [#230](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/230) | Unlimited storage, backup, reset, and fixed vault | @ThisIs-Developer | +| [#231](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/231) | Preserve the complete #230 commit history | @ThisIs-Developer | +| [#232](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/232) | Refine storage documentation highlights | @ThisIs-Developer | +| [#233](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/233) | Scale GitHub imports and simplify private access | @ThisIs-Developer | + +## Contributor + +Thank you to **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** for the design, implementation, testing, documentation, and release work across all four pull requests. + +## Thank you + +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/js/script.js b/desktop-app/resources/js/script.js index df5113fc..ea9c1584 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -1,10 +1,11 @@ document.addEventListener("DOMContentLoaded", async function () { window.alert = function(message) { showAppToast(message); }; const PRIVATE_MODE_KEY = 'markdownViewerPrivateMode'; - const APP_VERSION = '3.11.0'; + 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', @@ -53,6 +54,7 @@ document.addEventListener("DOMContentLoaded", async function () { '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' @@ -460,6 +462,7 @@ document.addEventListener("DOMContentLoaded", async function () { 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); } } @@ -467,18 +470,171 @@ document.addEventListener("DOMContentLoaded", async function () { 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 ((versionChanged || featureUpgrade) && seenVersion !== APP_VERSION) { + 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 pendingVersion === APP_VERSION && seenVersion !== 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 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) { @@ -495,12 +651,17 @@ document.addEventListener("DOMContentLoaded", async function () { 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) { - await switchTab(existing.id); - updateLiveEditorAccess(); + if (shouldActivate) { + await switchTab(existing.id); + updateLiveEditorAccess(); + } else { + renderTabBar(tabs, activeTabId); + } return existing; } @@ -510,10 +671,14 @@ document.addEventListener("DOMContentLoaded", async function () { tab.temporary = true; tab.releaseVersion = APP_VERSION; tabs.push(tab); - await switchTab(tab.id); - updateLiveEditorAccess(); + if (shouldActivate) { + await switchTab(tab.id); + updateLiveEditorAccess(); + } else { + renderTabBar(tabs, activeTabId); + } if (!settings.silent) { - announceToScreenReader('Release notes opened.'); + announceToScreenReader(shouldActivate ? 'Release notes opened.' : 'Release notes are available in the tab list.'); } return tab; } @@ -3949,7 +4114,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) { @@ -5664,6 +5830,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); @@ -10902,6 +11069,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() : ''; @@ -18814,9 +18983,7 @@ ${selector} .arrowheadPath { const hadExistingWorkspace = await initTabs(); initReviewMode(); - if (queueCurrentReleaseNotes(hadExistingWorkspace)) { - await openReleaseNotes(); - } + const releaseNotesLaunch = queueCurrentReleaseNotes(hadExistingWorkspace); if (loadGlobalState().syncScrollingEnabled === false) toggleSyncScrolling(); updateMobileStats(); updateFindHighlights(); @@ -26482,4 +26649,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 826b24e8..22ae83a3 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -11738,3 +11738,510 @@ 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(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(22px, 4vw, 46px) 0 clamp(34px, 6vw, 64px); + 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: 0 0 30px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 650; +} + +.release-note-brandline img { + width: 32px; + height: 32px; + border: 1px solid var(--border-color); + border-radius: 8px; + object-fit: cover; +} + +.release-note-brandline strong { + 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-size: 10px; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.markdown-body .release-note-intro h1 { + max-width: 860px; + padding: 0; + margin: 0 0 18px; + border: 0; + color: var(--preview-text-color); + font-size: clamp(34px, 5vw, 58px); + font-weight: 690; + letter-spacing: -0.045em; + line-height: 1.04; + text-wrap: balance; +} + +.release-note-release-meta, +.markdown-body .release-note-release-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0 0 24px; +} + +.release-note-release-meta span { + display: inline-flex; + min-height: 28px; + align-items: center; + padding: 4px 9px; + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-secondary); + background: var(--release-note-surface); + font-size: 11px; +} + +.release-note-intro > p:not(.release-note-brandline):not(.release-note-release-meta), +.markdown-body .release-note-intro > p:not(.release-note-brandline):not(.release-note-release-meta) { + max-width: 75ch; + margin: 0 0 14px; + color: var(--text-secondary); + font-size: 17px; +} + +.release-note-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 26px; +} + +.markdown-body .release-note-actions a { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + 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: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); +} + +.release-note-section table { + display: block; + width: 100%; + max-width: 100%; + margin: 22px 0; + overflow-x: auto; + border-spacing: 0; + scrollbar-width: thin; +} + +.release-note-section table th, +.release-note-section table td { + min-width: 118px; + padding: 10px 12px; + font-size: 12px; + line-height: 1.45; +} + +.release-note-section table th { + color: var(--preview-text-color); + font-weight: 650; +} + +.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: 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(31px, 10vw, 42px); + } + + .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, + .release-note-section table { + break-inside: avoid; + } +} diff --git a/script.js b/script.js index df5113fc..ea9c1584 100644 --- a/script.js +++ b/script.js @@ -1,10 +1,11 @@ document.addEventListener("DOMContentLoaded", async function () { window.alert = function(message) { showAppToast(message); }; const PRIVATE_MODE_KEY = 'markdownViewerPrivateMode'; - const APP_VERSION = '3.11.0'; + 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', @@ -53,6 +54,7 @@ document.addEventListener("DOMContentLoaded", async function () { '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' @@ -460,6 +462,7 @@ document.addEventListener("DOMContentLoaded", async function () { 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); } } @@ -467,18 +470,171 @@ document.addEventListener("DOMContentLoaded", async function () { 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 ((versionChanged || featureUpgrade) && seenVersion !== APP_VERSION) { + 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 pendingVersion === APP_VERSION && seenVersion !== 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 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) { @@ -495,12 +651,17 @@ document.addEventListener("DOMContentLoaded", async function () { 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) { - await switchTab(existing.id); - updateLiveEditorAccess(); + if (shouldActivate) { + await switchTab(existing.id); + updateLiveEditorAccess(); + } else { + renderTabBar(tabs, activeTabId); + } return existing; } @@ -510,10 +671,14 @@ document.addEventListener("DOMContentLoaded", async function () { tab.temporary = true; tab.releaseVersion = APP_VERSION; tabs.push(tab); - await switchTab(tab.id); - updateLiveEditorAccess(); + if (shouldActivate) { + await switchTab(tab.id); + updateLiveEditorAccess(); + } else { + renderTabBar(tabs, activeTabId); + } if (!settings.silent) { - announceToScreenReader('Release notes opened.'); + announceToScreenReader(shouldActivate ? 'Release notes opened.' : 'Release notes are available in the tab list.'); } return tab; } @@ -3949,7 +4114,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) { @@ -5664,6 +5830,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); @@ -10902,6 +11069,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() : ''; @@ -18814,9 +18983,7 @@ ${selector} .arrowheadPath { const hadExistingWorkspace = await initTabs(); initReviewMode(); - if (queueCurrentReleaseNotes(hadExistingWorkspace)) { - await openReleaseNotes(); - } + const releaseNotesLaunch = queueCurrentReleaseNotes(hadExistingWorkspace); if (loadGlobalState().syncScrollingEnabled === false) toggleSyncScrolling(); updateMobileStats(); updateFindHighlights(); @@ -26482,4 +26649,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 826b24e8..22ae83a3 100644 --- a/styles.css +++ b/styles.css @@ -11738,3 +11738,510 @@ 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(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(22px, 4vw, 46px) 0 clamp(34px, 6vw, 64px); + 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: 0 0 30px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 650; +} + +.release-note-brandline img { + width: 32px; + height: 32px; + border: 1px solid var(--border-color); + border-radius: 8px; + object-fit: cover; +} + +.release-note-brandline strong { + 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-size: 10px; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.markdown-body .release-note-intro h1 { + max-width: 860px; + padding: 0; + margin: 0 0 18px; + border: 0; + color: var(--preview-text-color); + font-size: clamp(34px, 5vw, 58px); + font-weight: 690; + letter-spacing: -0.045em; + line-height: 1.04; + text-wrap: balance; +} + +.release-note-release-meta, +.markdown-body .release-note-release-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0 0 24px; +} + +.release-note-release-meta span { + display: inline-flex; + min-height: 28px; + align-items: center; + padding: 4px 9px; + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-secondary); + background: var(--release-note-surface); + font-size: 11px; +} + +.release-note-intro > p:not(.release-note-brandline):not(.release-note-release-meta), +.markdown-body .release-note-intro > p:not(.release-note-brandline):not(.release-note-release-meta) { + max-width: 75ch; + margin: 0 0 14px; + color: var(--text-secondary); + font-size: 17px; +} + +.release-note-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 26px; +} + +.markdown-body .release-note-actions a { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + 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: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); +} + +.release-note-section table { + display: block; + width: 100%; + max-width: 100%; + margin: 22px 0; + overflow-x: auto; + border-spacing: 0; + scrollbar-width: thin; +} + +.release-note-section table th, +.release-note-section table td { + min-width: 118px; + padding: 10px 12px; + font-size: 12px; + line-height: 1.45; +} + +.release-note-section table th { + color: var(--preview-text-color); + font-weight: 650; +} + +.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: 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(31px, 10vw, 42px); + } + + .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, + .release-note-section table { + break-inside: avoid; + } +} diff --git a/sw.js b/sw.js index d3436ef8..423d8dd7 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'markdown-viewer-cache-v3.11.0'; +const CACHE_NAME = 'markdown-viewer-cache-v3.10.0'; // PERF-011: Split precache into critical (local files) and lazy (CDN libraries) // Critical assets are precached during SW install for instant offline startup From 02fb55820d9ebdcfcbe026677434d979611b9722 Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Mon, 3 Aug 2026 21:28:46 +0530 Subject: [PATCH 3/6] fix(releases): refine v3.10 release note layout --- RELEASE_NOTES | 43 +++--------- desktop-app/resources/RELEASE_NOTES | 43 +++--------- desktop-app/resources/js/script.js | 27 ++++++++ desktop-app/resources/styles.css | 102 ++++++++++++---------------- script.js | 27 ++++++++ styles.css | 102 ++++++++++++---------------- 6 files changed, 160 insertions(+), 184 deletions(-) diff --git a/RELEASE_NOTES b/RELEASE_NOTES index 1f1ded66..0463b6de 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -1,14 +1,14 @@ -

Markdown Viewer releasev3.10.0

+![Markdown Viewer logo](assets/icon.jpg) **Markdown Viewer release** `v3.10.0` # Welcome to Markdown Viewer 3.10.0 -

Released August 2, 202640 commits4 merged pull requests

+_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) --- @@ -65,41 +65,16 @@ The release completes a visual and localization pass across the importer and wor Bulk tab closing is batched so large sessions remain responsive, and small-screen import selection retains comfortable touch targets. -## How this release-note tab works - -On a first run, **Welcome to Markdown** stays selected and **Release Notes: 3.10.0** appears beside it. Select the release-note tab whenever you are ready. - -For an existing workspace, Markdown Viewer compares its current application version with the locally stored version. When they differ, the current note opens automatically. - -Closing this tab marks version 3.10.0 as seen. It will stay closed on future launches and return only after another version change. You can always reopen the current note from **About Markdown Viewer → Show Release Notes**. - -## Release figures - -| Figure | v3.10.0 | -| --- | ---: | -| Merged pull requests | 4 | -| Commits since v3.9.4 | 40 | -| Files changed | 53 | -| Lines added | 12,584 | -| Lines removed | 2,094 | -| Contributors | 1 | +## Thank you -The figures come from GitHub's comparison of `v3.9.4...v3.10.0`. No standalone issues were linked to this release. +Thank you to **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** for the design, implementation, testing, documentation, and release work. ### Pull requests included -| PR | Change | Contributor | -| --- | --- | --- | -| [#230](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/230) | Unlimited storage, backup, reset, and fixed vault | @ThisIs-Developer | -| [#231](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/231) | Preserve the complete #230 commit history | @ThisIs-Developer | -| [#232](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/232) | Refine storage documentation highlights | @ThisIs-Developer | -| [#233](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/233) | Scale GitHub imports and simplify private access | @ThisIs-Developer | - -## Contributor - -Thank you to **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** for the design, implementation, testing, documentation, and release work across all four pull requests. - -## Thank you +- [#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). diff --git a/desktop-app/resources/RELEASE_NOTES b/desktop-app/resources/RELEASE_NOTES index 1f1ded66..0463b6de 100644 --- a/desktop-app/resources/RELEASE_NOTES +++ b/desktop-app/resources/RELEASE_NOTES @@ -1,14 +1,14 @@ -

Markdown Viewer releasev3.10.0

+![Markdown Viewer logo](assets/icon.jpg) **Markdown Viewer release** `v3.10.0` # Welcome to Markdown Viewer 3.10.0 -

Released August 2, 202640 commits4 merged pull requests

+_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) --- @@ -65,41 +65,16 @@ The release completes a visual and localization pass across the importer and wor Bulk tab closing is batched so large sessions remain responsive, and small-screen import selection retains comfortable touch targets. -## How this release-note tab works - -On a first run, **Welcome to Markdown** stays selected and **Release Notes: 3.10.0** appears beside it. Select the release-note tab whenever you are ready. - -For an existing workspace, Markdown Viewer compares its current application version with the locally stored version. When they differ, the current note opens automatically. - -Closing this tab marks version 3.10.0 as seen. It will stay closed on future launches and return only after another version change. You can always reopen the current note from **About Markdown Viewer → Show Release Notes**. - -## Release figures - -| Figure | v3.10.0 | -| --- | ---: | -| Merged pull requests | 4 | -| Commits since v3.9.4 | 40 | -| Files changed | 53 | -| Lines added | 12,584 | -| Lines removed | 2,094 | -| Contributors | 1 | +## Thank you -The figures come from GitHub's comparison of `v3.9.4...v3.10.0`. No standalone issues were linked to this release. +Thank you to **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** for the design, implementation, testing, documentation, and release work. ### Pull requests included -| PR | Change | Contributor | -| --- | --- | --- | -| [#230](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/230) | Unlimited storage, backup, reset, and fixed vault | @ThisIs-Developer | -| [#231](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/231) | Preserve the complete #230 commit history | @ThisIs-Developer | -| [#232](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/232) | Refine storage documentation highlights | @ThisIs-Developer | -| [#233](https://github.com/ThisIs-Developer/Markdown-Viewer/pull/233) | Scale GitHub imports and simplify private access | @ThisIs-Developer | - -## Contributor - -Thank you to **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** for the design, implementation, testing, documentation, and release work across all four pull requests. - -## Thank you +- [#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). diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index ea9c1584..563baa23 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -527,6 +527,33 @@ document.addEventListener("DOMContentLoaded", async function () { 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'); diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index 22ae83a3..33343715 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -11764,7 +11764,7 @@ html[data-theme="dark"] .mermaid svg { #markdown-preview.release-notes-preview { min-height: 100%; - padding: clamp(24px, 4vw, 52px); + padding: clamp(18px, 2.4vw, 28px) clamp(24px, 4vw, 52px); background: var(--preview-bg); } @@ -11796,7 +11796,7 @@ html[data-theme="dark"] .mermaid svg { } .release-note-intro { - padding: clamp(22px, 4vw, 46px) 0 clamp(34px, 6vw, 64px); + padding: clamp(4px, 1vw, 10px) 0 clamp(30px, 4vw, 44px); border-bottom: 1px solid var(--border-color); } @@ -11806,66 +11806,62 @@ html[data-theme="dark"] .mermaid svg { width: fit-content; align-items: center; gap: 10px; - margin: 0 0 30px; + margin: 0 0 18px; color: var(--text-secondary); font-size: 12px; font-weight: 650; } .release-note-brandline img { - width: 32px; - height: 32px; + width: 28px; + height: 28px; border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: 7px; object-fit: cover; } -.release-note-brandline strong { +.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: 860px; + max-width: none; padding: 0; - margin: 0 0 18px; + margin: 0 0 12px; border: 0; color: var(--preview-text-color); - font-size: clamp(34px, 5vw, 58px); + font-size: clamp(32px, 3.7vw, 44px); font-weight: 690; letter-spacing: -0.045em; - line-height: 1.04; - text-wrap: balance; -} - -.release-note-release-meta, -.markdown-body .release-note-release-meta { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0 0 24px; + line-height: 1.08; + text-wrap: nowrap; + white-space: nowrap; } -.release-note-release-meta span { - display: inline-flex; - min-height: 28px; - align-items: center; - padding: 4px 9px; - border: 1px solid var(--border-color); - border-radius: 6px; +.release-note-release-date, +.markdown-body .release-note-release-date { + margin: 0 0 18px; color: var(--text-secondary); - background: var(--release-note-surface); - font-size: 11px; + font-size: 12px; } -.release-note-intro > p:not(.release-note-brandline):not(.release-note-release-meta), -.markdown-body .release-note-intro > p:not(.release-note-brandline):not(.release-note-release-meta) { +.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); @@ -11874,9 +11870,10 @@ html[data-theme="dark"] .mermaid svg { .release-note-actions { display: flex; + max-width: none; flex-wrap: wrap; gap: 10px; - margin-top: 26px; + margin: 24px 0 0; } .markdown-body .release-note-actions a { @@ -11884,6 +11881,7 @@ html[data-theme="dark"] .mermaid svg { min-height: 44px; align-items: center; justify-content: center; + gap: 7px; padding: 8px 14px; border: 1px solid var(--border-color); border-radius: 7px; @@ -11895,6 +11893,11 @@ html[data-theme="dark"] .mermaid svg { 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); @@ -12063,29 +12066,6 @@ html[data-theme="dark"] .mermaid svg { color: var(--preview-text-color); } -.release-note-section table { - display: block; - width: 100%; - max-width: 100%; - margin: 22px 0; - overflow-x: auto; - border-spacing: 0; - scrollbar-width: thin; -} - -.release-note-section table th, -.release-note-section table td { - min-width: 118px; - padding: 10px 12px; - font-size: 12px; - line-height: 1.45; -} - -.release-note-section table th { - color: var(--preview-text-color); - font-weight: 650; -} - .release-note-section:last-child { padding: clamp(24px, 4vw, 36px); margin-bottom: 0; @@ -12121,6 +12101,13 @@ html[data-theme="dark"] .mermaid svg { 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; @@ -12192,7 +12179,7 @@ html[data-theme="dark"] .mermaid svg { @media (max-width: 520px) { .markdown-body .release-note-intro h1 { - font-size: clamp(31px, 10vw, 42px); + font-size: clamp(30px, 9vw, 40px); } .release-note-brandline { @@ -12240,8 +12227,7 @@ html[data-theme="dark"] .mermaid svg { display: block; } - .release-note-section, - .release-note-section table { + .release-note-section { break-inside: avoid; } } diff --git a/script.js b/script.js index ea9c1584..563baa23 100644 --- a/script.js +++ b/script.js @@ -527,6 +527,33 @@ document.addEventListener("DOMContentLoaded", async function () { 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'); diff --git a/styles.css b/styles.css index 22ae83a3..33343715 100644 --- a/styles.css +++ b/styles.css @@ -11764,7 +11764,7 @@ html[data-theme="dark"] .mermaid svg { #markdown-preview.release-notes-preview { min-height: 100%; - padding: clamp(24px, 4vw, 52px); + padding: clamp(18px, 2.4vw, 28px) clamp(24px, 4vw, 52px); background: var(--preview-bg); } @@ -11796,7 +11796,7 @@ html[data-theme="dark"] .mermaid svg { } .release-note-intro { - padding: clamp(22px, 4vw, 46px) 0 clamp(34px, 6vw, 64px); + padding: clamp(4px, 1vw, 10px) 0 clamp(30px, 4vw, 44px); border-bottom: 1px solid var(--border-color); } @@ -11806,66 +11806,62 @@ html[data-theme="dark"] .mermaid svg { width: fit-content; align-items: center; gap: 10px; - margin: 0 0 30px; + margin: 0 0 18px; color: var(--text-secondary); font-size: 12px; font-weight: 650; } .release-note-brandline img { - width: 32px; - height: 32px; + width: 28px; + height: 28px; border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: 7px; object-fit: cover; } -.release-note-brandline strong { +.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: 860px; + max-width: none; padding: 0; - margin: 0 0 18px; + margin: 0 0 12px; border: 0; color: var(--preview-text-color); - font-size: clamp(34px, 5vw, 58px); + font-size: clamp(32px, 3.7vw, 44px); font-weight: 690; letter-spacing: -0.045em; - line-height: 1.04; - text-wrap: balance; -} - -.release-note-release-meta, -.markdown-body .release-note-release-meta { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0 0 24px; + line-height: 1.08; + text-wrap: nowrap; + white-space: nowrap; } -.release-note-release-meta span { - display: inline-flex; - min-height: 28px; - align-items: center; - padding: 4px 9px; - border: 1px solid var(--border-color); - border-radius: 6px; +.release-note-release-date, +.markdown-body .release-note-release-date { + margin: 0 0 18px; color: var(--text-secondary); - background: var(--release-note-surface); - font-size: 11px; + font-size: 12px; } -.release-note-intro > p:not(.release-note-brandline):not(.release-note-release-meta), -.markdown-body .release-note-intro > p:not(.release-note-brandline):not(.release-note-release-meta) { +.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); @@ -11874,9 +11870,10 @@ html[data-theme="dark"] .mermaid svg { .release-note-actions { display: flex; + max-width: none; flex-wrap: wrap; gap: 10px; - margin-top: 26px; + margin: 24px 0 0; } .markdown-body .release-note-actions a { @@ -11884,6 +11881,7 @@ html[data-theme="dark"] .mermaid svg { min-height: 44px; align-items: center; justify-content: center; + gap: 7px; padding: 8px 14px; border: 1px solid var(--border-color); border-radius: 7px; @@ -11895,6 +11893,11 @@ html[data-theme="dark"] .mermaid svg { 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); @@ -12063,29 +12066,6 @@ html[data-theme="dark"] .mermaid svg { color: var(--preview-text-color); } -.release-note-section table { - display: block; - width: 100%; - max-width: 100%; - margin: 22px 0; - overflow-x: auto; - border-spacing: 0; - scrollbar-width: thin; -} - -.release-note-section table th, -.release-note-section table td { - min-width: 118px; - padding: 10px 12px; - font-size: 12px; - line-height: 1.45; -} - -.release-note-section table th { - color: var(--preview-text-color); - font-weight: 650; -} - .release-note-section:last-child { padding: clamp(24px, 4vw, 36px); margin-bottom: 0; @@ -12121,6 +12101,13 @@ html[data-theme="dark"] .mermaid svg { 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; @@ -12192,7 +12179,7 @@ html[data-theme="dark"] .mermaid svg { @media (max-width: 520px) { .markdown-body .release-note-intro h1 { - font-size: clamp(31px, 10vw, 42px); + font-size: clamp(30px, 9vw, 40px); } .release-note-brandline { @@ -12240,8 +12227,7 @@ html[data-theme="dark"] .mermaid svg { display: block; } - .release-note-section, - .release-note-section table { + .release-note-section { break-inside: avoid; } } From 2dabc989fdcba2b67abe7aff39bd735fa25d51e9 Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Mon, 3 Aug 2026 21:47:33 +0530 Subject: [PATCH 4/6] feat(releases): add universal release note format --- CHANGELOG.md | 2 +- RELEASE_NOTES | 6 ++-- RELEASE_NOTES_TEMPLATE.md | 44 +++++++++++++++++++++++++++++ desktop-app/resources/RELEASE_NOTES | 6 ++-- desktop-app/resources/styles.css | 4 +++ styles.css | 4 +++ wiki/Contributing.md | 43 ++++++++++++++++++++++++++++ 7 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 RELEASE_NOTES_TEMPLATE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da75b3b..64bb74d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +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 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, and a Show Release Notes action in About. + - **In-App Release Notes:** Added the 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 canonical Markdown template 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/RELEASE_NOTES b/RELEASE_NOTES index 0463b6de..8a077359 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -67,9 +67,11 @@ Bulk tab closing is batched so large sessions remain responsive, and small-scree ## Thank you -Thank you to **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** for the design, implementation, testing, documentation, and release work. +### Contributors -### Pull requests included +- **[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) diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md new file mode 100644 index 00000000..adc720d4 --- /dev/null +++ b/RELEASE_NOTES_TEMPLATE.md @@ -0,0 +1,44 @@ +![Markdown Viewer logo](assets/icon.jpg) **Markdown Viewer release** `v{{VERSION}}` + +# Welcome to Markdown Viewer {{VERSION}} + +_Released {{MONTH DAY, YEAR}}_ + +{{ONE-SENTENCE SUMMARY OF THE USER-FACING VALUE.}} + +{{OPTIONAL SECOND SENTENCE ABOUT AVAILABILITY OR COMPATIBILITY.}} **Happy coding!** + +[View release on GitHub](https://github.com/ThisIs-Developer/Markdown-Viewer/releases/tag/v{{VERSION}}) [Open the full changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md) + +--- + +## Highlights + +- **{{HIGHLIGHT TITLE}}.** {{ONE OR TWO SENTENCES ABOUT THE USER IMPACT.}} +- **{{HIGHLIGHT TITLE}}.** {{DELETE OR DUPLICATE HIGHLIGHT BULLETS AS NEEDED.}} + +## {{OPTIONAL DETAIL SECTION}} + +{{EXPLAIN ONE USER-FACING TOPIC. DELETE THIS SECTION FOR A SHORT RELEASE OR DUPLICATE IT FOR A LARGE RELEASE.}} + +### {{OPTIONAL SUBTOPIC}} + +- {{SUPPORTING CHANGE OR INSTRUCTION.}} +- {{SUPPORTING CHANGE OR INSTRUCTION.}} + +> [!IMPORTANT] +> {{ADD ONLY WHEN USERS NEED A MIGRATION STEP, COMPATIBILITY NOTE, OR SAFETY WARNING. OTHERWISE DELETE THIS ALERT.}} + +## Thank you + +### Contributors + +- [{{CONTRIBUTOR NAME}} (@{{GITHUB HANDLE}})](https://github.com/{{GITHUB HANDLE}}) — {{CONTRIBUTION.}} + +### Changes included + +- [{{PR, ISSUE, OR COMMIT REFERENCE}} — {{SHORT USER-FACING DESCRIPTION}}]({{URL}}) + +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/RELEASE_NOTES b/desktop-app/resources/RELEASE_NOTES index 0463b6de..8a077359 100644 --- a/desktop-app/resources/RELEASE_NOTES +++ b/desktop-app/resources/RELEASE_NOTES @@ -67,9 +67,11 @@ Bulk tab closing is batched so large sessions remain responsive, and small-scree ## Thank you -Thank you to **[Baivab Sarkar (@ThisIs-Developer)](https://github.com/ThisIs-Developer)** for the design, implementation, testing, documentation, and release work. +### Contributors -### Pull requests included +- **[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) diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index 33343715..45c312da 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -12066,6 +12066,10 @@ html[data-theme="dark"] .mermaid svg { 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; diff --git a/styles.css b/styles.css index 33343715..45c312da 100644 --- a/styles.css +++ b/styles.css @@ -12066,6 +12066,10 @@ html[data-theme="dark"] .mermaid svg { 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; diff --git a/wiki/Contributing.md b/wiki/Contributing.md index 948ec750..f272a811 100644 --- a/wiki/Contributing.md +++ b/wiki/Contributing.md @@ -88,6 +88,47 @@ 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 [`RELEASE_NOTES_TEMPLATE.md`](../RELEASE_NOTES_TEMPLATE.md) as the canonical source for every release. Copy it to the extensionless `RELEASE_NOTES` file, replace every `{{PLACEHOLDER}}`, and delete unused optional sections. Keep the source as standard Markdown; the application adds the branded layout, action icons, section navigation, and active-section state after 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: + +- Describe user impact instead of copying commit subjects. +- Do not show commit, PR, issue, file, or line counts in the introduction. +- Do not add empty headings. Delete any optional section that has no useful content. +- Keep **Highlights** to five bullets or fewer. Combine related changes instead of creating a card for every commit. +- Use `##` for navigation topics and `###` only for supporting content inside a topic. +- Use alerts only for migration steps, compatibility notes, data-loss risks, or other actions the user must notice. +- Keep references as Markdown bullets, not tables. For a very large release, list the most important references and rely on the release/changelog link for the complete history. +- With one contributor, use one bullet. With multiple contributors, use one bullet per person in display-name order and state each contribution briefly. +- When no individual credit is appropriate, replace the Contributors subsection with a short maintainer acknowledgement. Never leave a placeholder or empty list. +- Keep **Thank you** last because the application gives the final section its closing-card treatment. +- Preserve the standard release and changelog URLs so the application can add their icons. + +Before publishing a release: + +1. Verify the version and date in `RELEASE_NOTES`, `CHANGELOG.md`, `script.js`, `sw.js`, and the desktop package/configuration files. +2. Verify every contributor and PR, issue, or commit link against GitHub. +3. Search for unreplaced `{{PLACEHOLDER}}` values and remove all unused template content. +4. Run `node assets/i18n/generate-ui-locales.mjs` and `node assets/i18n/audit-ui-locales.mjs` when visible labels or the version title change. +5. Run `node desktop-app/prepare.js`, `npm run build`, and the focused release-note lifecycle/responsive tests. + ## Testing Choose checks that match the change: @@ -162,6 +203,8 @@ Include the affected version/commit, required preconditions, impact, minimal rep | `preview-worker.js` | Worker Markdown rendering path. | | `styles.css` | Layout, themes, renderer styles, modals, responsive UI. | | `sw.js` | PWA/service-worker cache behavior. | +| `RELEASE_NOTES` | Current extensionless release note bundled with web/PWA and desktop builds. | +| `RELEASE_NOTES_TEMPLATE.md` | Canonical Markdown structure and placeholders for every release size. | | `assets/i18n/` | Interface catalogs, generator, and catalog-audit tool. | | `functions/api/image/[[id]].js` | Content-addressed managed raster image and GIF API. | | `functions/api/media/[[id]].js` | Route alias for content-addressed managed video uploads and delivery. | From 6561fe39c8c254144918b5173c433d11e42e00b5 Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Mon, 3 Aug 2026 21:58:20 +0530 Subject: [PATCH 5/6] refactor(releases): keep a single Markdown source --- CHANGELOG.md | 2 +- RELEASE_NOTES_TEMPLATE.md | 44 --------------------------------------- wiki/Contributing.md | 11 +++++----- 3 files changed, 7 insertions(+), 50 deletions(-) delete mode 100644 RELEASE_NOTES_TEMPLATE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 64bb74d7..9c1625b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +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 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 canonical Markdown template that scales from one-fix patches to large multi-contributor releases. + - **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/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md deleted file mode 100644 index adc720d4..00000000 --- a/RELEASE_NOTES_TEMPLATE.md +++ /dev/null @@ -1,44 +0,0 @@ -![Markdown Viewer logo](assets/icon.jpg) **Markdown Viewer release** `v{{VERSION}}` - -# Welcome to Markdown Viewer {{VERSION}} - -_Released {{MONTH DAY, YEAR}}_ - -{{ONE-SENTENCE SUMMARY OF THE USER-FACING VALUE.}} - -{{OPTIONAL SECOND SENTENCE ABOUT AVAILABILITY OR COMPATIBILITY.}} **Happy coding!** - -[View release on GitHub](https://github.com/ThisIs-Developer/Markdown-Viewer/releases/tag/v{{VERSION}}) [Open the full changelog](https://github.com/ThisIs-Developer/Markdown-Viewer/blob/main/CHANGELOG.md) - ---- - -## Highlights - -- **{{HIGHLIGHT TITLE}}.** {{ONE OR TWO SENTENCES ABOUT THE USER IMPACT.}} -- **{{HIGHLIGHT TITLE}}.** {{DELETE OR DUPLICATE HIGHLIGHT BULLETS AS NEEDED.}} - -## {{OPTIONAL DETAIL SECTION}} - -{{EXPLAIN ONE USER-FACING TOPIC. DELETE THIS SECTION FOR A SHORT RELEASE OR DUPLICATE IT FOR A LARGE RELEASE.}} - -### {{OPTIONAL SUBTOPIC}} - -- {{SUPPORTING CHANGE OR INSTRUCTION.}} -- {{SUPPORTING CHANGE OR INSTRUCTION.}} - -> [!IMPORTANT] -> {{ADD ONLY WHEN USERS NEED A MIGRATION STEP, COMPATIBILITY NOTE, OR SAFETY WARNING. OTHERWISE DELETE THIS ALERT.}} - -## Thank you - -### Contributors - -- [{{CONTRIBUTOR NAME}} (@{{GITHUB HANDLE}})](https://github.com/{{GITHUB HANDLE}}) — {{CONTRIBUTION.}} - -### Changes included - -- [{{PR, ISSUE, OR COMMIT REFERENCE}} — {{SHORT USER-FACING DESCRIPTION}}]({{URL}}) - -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/wiki/Contributing.md b/wiki/Contributing.md index f272a811..e3f0c9b2 100644 --- a/wiki/Contributing.md +++ b/wiki/Contributing.md @@ -90,7 +90,7 @@ Detailed Wiki pages are maintained in English. When no localized page exists, la ## Release Notes Format -Use [`RELEASE_NOTES_TEMPLATE.md`](../RELEASE_NOTES_TEMPLATE.md) as the canonical source for every release. Copy it to the extensionless `RELEASE_NOTES` file, replace every `{{PLACEHOLDER}}`, and delete unused optional sections. Keep the source as standard Markdown; the application adds the branded layout, action icons, section navigation, and active-section state after rendering. +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: @@ -109,11 +109,13 @@ Scale the same format to the release size: 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, `