From fdec7fcef484a361d418c2dbab04c19975d93b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=AAsufian=20nasser=E2=80=AC=E2=80=8F?= <166199800+sufyanaser@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:01:23 +0300 Subject: [PATCH] enable NASbook V07 automatic updates --- .github/workflows/github-release.yml | 30 ++++-- .github/workflows/windows-release.yml | 17 +++- design-qa.md | 48 ++++++++++ electron/main/index.ts | 5 +- electron/main/updateService.ts | 75 +++++++++++++++ package-lock.json | 95 +++++++++++++++++-- package.json | 17 +++- .../styles/editor-interaction-stability.css | 15 --- src/renderer/styles/editor-productivity.css | 55 +++++++---- tests/auto-update.test.mjs | 46 +++++++++ tests/editor-productivity-features.test.mjs | 19 ++-- 11 files changed, 355 insertions(+), 67 deletions(-) create mode 100644 design-qa.md create mode 100644 electron/main/updateService.ts create mode 100644 tests/auto-update.test.mjs diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index abd142c..5466441 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -93,10 +93,18 @@ jobs: shell: pwsh run: | $label = "${{ steps.meta.outputs.label }}" - $installer = Get-ChildItem -Path release -Filter "NASbook Setup $label.exe" -File -ErrorAction Stop + $installer = Get-ChildItem -Path release -Filter "NASbook-Setup-$label.exe" -File -ErrorAction Stop if ($installer.Count -ne 1) { throw "Expected exactly one NASbook $label installer." } "path=$($installer.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + $metadata = Get-ChildItem -Path release -Filter "latest.yml" -File -ErrorAction Stop + if ($metadata.Count -ne 1) { throw "Expected exactly one latest.yml update manifest." } + "metadata=$($metadata.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + $blockmap = Get-ChildItem -Path release -Filter "NASbook-Setup-$label.exe.blockmap" -File -ErrorAction Stop + if ($blockmap.Count -ne 1) { throw "Expected exactly one NSIS blockmap." } + "blockmap=$($blockmap.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + - name: Write release notes if: steps.release-check.outputs.publish == 'true' shell: pwsh @@ -104,12 +112,13 @@ jobs: @" ## NASbook ${{ steps.meta.outputs.label }} - - Adds fast global search across visible note titles and content, including Arabic text. - - Guarantees queued autosaves finish before note navigation or application close. - - Separates editor HTML from Markdown through a transactional SQLite migration. - - Adds visible recovery for CRUD failures and renderer crashes. - - Reduces the default toolbar to core writing controls while preserving advanced customization. - - Upgrades Electron and resolves all reported npm audit vulnerabilities. + - Fixes section collapse while a note is unlocked and prevents viewport jumping. + - Adds automatic background update checks for installed Windows copies. + - Downloads future releases automatically and installs them on a normal safe exit. + - Preserves the existing application identity and local notes database. + + V07 is the first update-enabled release. Install V07 once over V06; future + releases will then arrive through the application automatically. "@ | Set-Content release-notes.md -Encoding utf8 - name: Publish GitHub Release @@ -120,6 +129,8 @@ jobs: run: | gh release create "${{ steps.meta.outputs.tag }}" ` "${{ steps.installer.outputs.path }}" ` + "${{ steps.installer.outputs.metadata }}" ` + "${{ steps.installer.outputs.blockmap }}" ` --title "NASbook ${{ steps.meta.outputs.label }}" ` --notes-file release-notes.md ` --target "${{ github.sha }}" @@ -129,6 +140,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: NASbook-Setup-${{ steps.meta.outputs.label }}-Windows - path: release/NASbook Setup ${{ steps.meta.outputs.label }}.exe + path: | + release/NASbook-Setup-${{ steps.meta.outputs.label }}.exe + release/NASbook-Setup-${{ steps.meta.outputs.label }}.exe.blockmap + release/latest.yml if-no-files-found: error retention-days: 30 diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index a3ae399..00cb8da 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -75,17 +75,30 @@ jobs: shell: pwsh run: | $label = "${{ steps.release-label.outputs.label }}" - $installer = Get-ChildItem -Path release -Filter "NASbook Setup $label.exe" -File -ErrorAction Stop + $installer = Get-ChildItem -Path release -Filter "NASbook-Setup-$label.exe" -File -ErrorAction Stop if ($installer.Count -ne 1) { throw "Expected exactly one NASbook $label installer." } Write-Host "Installer: $($installer.FullName)" Write-Host "Size: $([math]::Round($installer.Length / 1MB, 2)) MB" + $metadata = Get-ChildItem -Path release -Filter "latest.yml" -File -ErrorAction Stop + if ($metadata.Count -ne 1) { + throw "Expected exactly one latest.yml update manifest." + } + + $blockmap = Get-ChildItem -Path release -Filter "NASbook-Setup-$label.exe.blockmap" -File -ErrorAction Stop + if ($blockmap.Count -ne 1) { + throw "Expected exactly one NSIS blockmap." + } + - name: Upload installer artifact uses: actions/upload-artifact@v4 with: name: NASbook-Setup-${{ steps.release-label.outputs.label }}-Windows - path: release/NASbook Setup ${{ steps.release-label.outputs.label }}.exe + path: | + release/NASbook-Setup-${{ steps.release-label.outputs.label }}.exe + release/NASbook-Setup-${{ steps.release-label.outputs.label }}.exe.blockmap + release/latest.yml if-no-files-found: error retention-days: 30 diff --git a/design-qa.md b/design-qa.md new file mode 100644 index 0000000..89ec32a --- /dev/null +++ b/design-qa.md @@ -0,0 +1,48 @@ +# V07 Collapse Chevron Design QA + +- Source visual truth: `C:\Users\sufia\AppData\Local\Temp\codex-clipboard-86157121-96a8-47c5-ba3d-c87ad64345ec.png` +- Open implementation: `C:\Users\sufia\AppData\Local\Temp\nasbook-v07-collapse-open.png` +- Closed implementation: `C:\Users\sufia\AppData\Local\Temp\nasbook-v07-collapse-closed.png` +- Combined comparison: `C:\Users\sufia\AppData\Local\Temp\nasbook-v07-collapse-comparison.png` +- Viewport and CSS size: 1320 x 860 +- Source pixels: 1320 x 860 +- Implementation pixels: 1320 x 860 +- Device scale factor: 1; no density normalization required +- State: light theme, RTL note, unlocked editor, same note and scroll anchor + +## Findings + +No actionable P0, P1, or P2 differences remain in the requested control. + +- Fonts and typography: unchanged from the source; heading weight, wrapping, and line height remain intact. +- Spacing and layout rhythm: the control receives a deliberate 38 px inline slot; document width and vertical rhythm remain unchanged. +- Colors and visual tokens: the chevron and its subtle surface use the existing application accent token with improved contrast. +- Image and icon quality: the existing vector-like CSS chevron remains sharp at 1x; no branding or raster assets changed. +- Copy and content: unchanged. +- Accessibility and affordance: the 10 x 10 px stroke sits in a persistent 30 x 30 px visual target, with a stronger hover state. +- Interaction: open points down; closed RTL points left toward the text; hidden content is restored on expansion. +- Stability: heading top remained exactly 319.5 px before and after collapse. + +## Full-view Comparison Evidence + +The 3960 x 860 combined image places source, open implementation, and closed implementation in one comparison. The application shell, toolbar, editor measure, and note content remain visually unchanged outside the requested chevron treatment. + +## Focused-region Evidence + +No additional crop was required because each 1320 x 860 source capture preserves the chevron at original 1:1 density and the combined comparison makes both states readable. + +## Comparison History + +1. Source issue: the small unframed chevron had weak affordance and ambiguous state direction. +2. Fix: consolidated conflicting style rules, increased chevron size and contrast, added a subtle persistent surface, and split closed direction using inherited RTL/LTR writing direction. +3. Post-fix evidence: open transform is 45 degrees with visible content; closed RTL transform is 135 degrees with the next section node hidden; the heading anchor remains fixed. + +## Implementation Checklist + +- [x] Open state points down. +- [x] Closed state points toward text in RTL and LTR. +- [x] Chevron is prominent without changing branding. +- [x] Collapse does not move the heading anchor. +- [x] Automated tests, production build, and packaged runtime pass. + +final result: passed diff --git a/electron/main/index.ts b/electron/main/index.ts index dde3ed5..2d5bf33 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -12,6 +12,7 @@ import { createGoogleAuthService } from "./googleAuthService"; import { createGoogleDriveBackupService } from "./googleDriveBackupService"; import { createGmailBackupService } from "./gmailBackupService"; import { isSafeExternalUrl } from "../../src/shared/externalUrl"; +import { disposeUpdateService, initializeUpdateService } from "./updateService"; const gotTheLock = app.requestSingleInstanceLock(); @@ -172,7 +173,7 @@ if (!gotTheLock) { registerIpcHandlers({ appName: app.getName(), - appVersion: "V06", + appVersion: "V07", database: notesbookDatabase, settingsStore, backupService, @@ -182,6 +183,7 @@ if (!gotTheLock) { }); createMainWindow(); + initializeUpdateService(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { @@ -197,6 +199,7 @@ if (!gotTheLock) { }); app.on("before-quit", () => { + disposeUpdateService(); notesbookDatabase?.close(); notesbookDatabase = null; }); diff --git a/electron/main/updateService.ts b/electron/main/updateService.ts new file mode 100644 index 0000000..23a2b69 --- /dev/null +++ b/electron/main/updateService.ts @@ -0,0 +1,75 @@ +import { app, Notification } from "electron"; +import * as electronUpdater from "electron-updater"; + +const { autoUpdater } = electronUpdater; +const INITIAL_CHECK_DELAY_MS = 10_000; +const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; + +let initialized = false; +let checkInProgress = false; +let initialCheckTimer: NodeJS.Timeout | null = null; +let periodicCheckTimer: NodeJS.Timeout | null = null; + +async function checkForUpdates(): Promise { + if (checkInProgress) return; + + checkInProgress = true; + try { + await autoUpdater.checkForUpdates(); + } catch (error) { + console.error("Automatic update check failed:", error); + } finally { + checkInProgress = false; + } +} + +export function initializeUpdateService(): void { + if (initialized || !app.isPackaged || process.platform !== "win32") return; + initialized = true; + + autoUpdater.autoDownload = true; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.allowPrerelease = false; + autoUpdater.logger = console; + + autoUpdater.on("checking-for-update", () => { + console.info("Checking for NASbook updates."); + }); + autoUpdater.on("update-available", (info) => { + console.info(`NASbook update ${info.version} is available; download started.`); + }); + autoUpdater.on("update-not-available", (info) => { + console.info(`NASbook ${info.version} is up to date.`); + }); + autoUpdater.on("update-downloaded", (info) => { + console.info(`NASbook update ${info.version} is ready and will install on exit.`); + if (Notification.isSupported()) { + new Notification({ + title: "NASbook", + body: "تم تنزيل تحديث جديد وسيتم تثبيته عند إغلاق البرنامج.", + silent: true, + }).show(); + } + }); + autoUpdater.on("error", (error) => { + console.error("NASbook updater error:", error); + }); + + initialCheckTimer = setTimeout(() => { + initialCheckTimer = null; + void checkForUpdates(); + }, INITIAL_CHECK_DELAY_MS); + initialCheckTimer.unref(); + + periodicCheckTimer = setInterval(() => { + void checkForUpdates(); + }, UPDATE_CHECK_INTERVAL_MS); + periodicCheckTimer.unref(); +} + +export function disposeUpdateService(): void { + if (initialCheckTimer) clearTimeout(initialCheckTimer); + if (periodicCheckTimer) clearInterval(periodicCheckTimer); + initialCheckTimer = null; + periodicCheckTimer = null; +} diff --git a/package-lock.json b/package-lock.json index 76b24b6..b0b2db5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nas-notesbook", - "version": "6.0.0", + "version": "7.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nas-notesbook", - "version": "6.0.0", + "version": "7.0.0", "hasInstallScript": true, "dependencies": { "@tiptap/extension-color": "^3.27.0", @@ -22,6 +22,7 @@ "@tiptap/react": "^3.27.0", "@tiptap/starter-kit": "^3.27.0", "better-sqlite3": "^12.11.1", + "electron-updater": "^6.8.9", "marked": "^18.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -3514,7 +3515,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/asn1js": { @@ -3842,7 +3842,6 @@ "version": "9.7.0", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -4217,7 +4216,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4687,6 +4685,69 @@ "dev": true, "license": "ISC" }, + "node_modules/electron-updater": { + "version": "6.8.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz", + "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.7.0", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-winstaller": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", @@ -5578,7 +5639,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -5947,7 +6007,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", - "dev": true, "funding": [ { "type": "github", @@ -6045,7 +6104,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/levn": { @@ -6111,6 +6169,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6325,7 +6396,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -7662,7 +7732,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -8184,6 +8253,12 @@ "semver": "bin/semver" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", diff --git a/package.json b/package.json index 7ab0f5d..1543539 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nas-notesbook", - "version": "6.0.0", - "releaseLabel": "V06", + "version": "7.0.0", + "releaseLabel": "V07", "private": true, "description": "RTL-first local desktop notebook for NASbook.", "main": "dist/electron/main/index.js", @@ -34,6 +34,7 @@ "@tiptap/react": "^3.27.0", "@tiptap/starter-kit": "^3.27.0", "better-sqlite3": "^12.11.1", + "electron-updater": "^6.8.9", "marked": "^18.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -63,6 +64,14 @@ "directories": { "output": "release" }, + "publish": [ + { + "provider": "github", + "owner": "sufyanaser", + "repo": "NASbook", + "releaseType": "release" + } + ], "files": [ "dist/**/*", "assets/icon.ico", @@ -86,7 +95,7 @@ "nsis" ], "icon": "assets/icon.ico", - "artifactName": "NASbook Setup V06.exe" + "artifactName": "NASbook-Setup-V07.exe" }, "nsis": { "oneClick": false, @@ -96,7 +105,7 @@ "installerIcon": "assets/icon.ico", "uninstallerIcon": "assets/icon.ico", "installerHeaderIcon": "assets/icon.ico", - "artifactName": "NASbook Setup V06.${ext}" + "artifactName": "NASbook-Setup-V07.${ext}" } }, "author": "Sufyan Nasser Ali" diff --git a/src/renderer/styles/editor-interaction-stability.css b/src/renderer/styles/editor-interaction-stability.css index 7d1bab1..ff7f606 100644 --- a/src/renderer/styles/editor-interaction-stability.css +++ b/src/renderer/styles/editor-interaction-stability.css @@ -18,21 +18,6 @@ justify-self: center; } -/* Replace the border triangle with one consistent chevron in editable and locked modes. */ -.note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"]::before { - width: 7px; - height: 7px; - border: 0; - border-inline-end: 2px solid var(--app-accent); - border-bottom: 2px solid var(--app-accent); - transform: translateY(-58%) rotate(45deg); - opacity: 0.95; -} - -.note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"][data-nas-collapsed="true"]::before { - transform: translateY(-50%) rotate(-45deg); -} - /* Dark fills: use white foreground. Manual inline text color still has priority. */ .note-editor-content-wrapper .ProseMirror :is( [style*="background-color: #71717a"], diff --git a/src/renderer/styles/editor-productivity.css b/src/renderer/styles/editor-productivity.css index 8c26ccf..88d3cc7 100644 --- a/src/renderer/styles/editor-productivity.css +++ b/src/renderer/styles/editor-productivity.css @@ -1,42 +1,57 @@ .note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"] { position: relative; - padding-inline-start: 30px; + padding-inline-start: 38px; } .note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"]::before { content: ""; position: absolute; - inset-inline-start: 8px; + z-index: 2; + inset-inline-start: 11px; top: 50%; - width: 0; - height: 0; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-inline-start: 8px solid var(--app-accent); - transform: translateY(-50%) rotate(90deg); + width: 10px; + height: 10px; + border: 0; + border-right: 2.5px solid var(--app-accent); + border-bottom: 2.5px solid var(--app-accent); + transform: translateY(-62%) rotate(45deg); transform-origin: center; - transition: transform 0.14s ease, opacity 0.14s ease; - opacity: 0.9; + transition: transform 0.14s ease, filter 0.14s ease; + opacity: 1; + filter: drop-shadow(0 0 2px color-mix(in srgb, var(--app-accent) 42%, transparent)); pointer-events: none; } -.note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"][data-nas-collapsed="true"]::before { - transform: translateY(-50%) rotate(0deg); -} - -.note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"]:hover::after { +.note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"]::after { content: ""; position: absolute; - inset-inline-start: 2px; + z-index: 1; + inset-inline-start: 1px; top: 50%; - width: 26px; - height: 26px; - border-radius: 7px; - background: color-mix(in srgb, var(--app-accent) 12%, transparent); + width: 30px; + height: 30px; + border: 1px solid color-mix(in srgb, var(--app-accent) 18%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--app-accent) 7%, transparent); transform: translateY(-50%); + transition: border-color 0.14s ease, background 0.14s ease; pointer-events: none; } +.note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"]:hover::after { + border-color: color-mix(in srgb, var(--app-accent) 34%, transparent); + background: color-mix(in srgb, var(--app-accent) 15%, transparent); +} + +/* Open sections point down. Closed sections point from the control toward text. */ +.note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"]:dir(ltr)[data-nas-collapsed="true"]::before { + transform: translateY(-50%) rotate(-45deg); +} + +.note-editor-content-wrapper .ProseMirror [data-nas-collapsible="true"]:dir(rtl)[data-nas-collapsed="true"]::before { + transform: translateY(-50%) rotate(135deg); +} + .note-editor-content-wrapper .ProseMirror [data-nas-collapsed-hidden="true"] { display: none !important; } diff --git a/tests/auto-update.test.mjs b/tests/auto-update.test.mjs new file mode 100644 index 0000000..e1abe1a --- /dev/null +++ b/tests/auto-update.test.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import test from "node:test"; + +const root = process.cwd(); + +async function source(path) { + return readFile(join(root, path), "utf8"); +} + +test("packaged Windows builds check, download, and install updates on safe exit", async () => { + const updaterSource = await source("electron/main/updateService.ts"); + const mainSource = await source("electron/main/index.ts"); + + assert.match(updaterSource, /app\.isPackaged/); + assert.match(updaterSource, /import \* as electronUpdater from "electron-updater"/); + assert.doesNotMatch(updaterSource, /import electronUpdater from "electron-updater"/); + assert.match(updaterSource, /process\.platform !== "win32"/); + assert.match(updaterSource, /autoUpdater\.autoDownload = true/); + assert.match(updaterSource, /autoUpdater\.autoInstallOnAppQuit = true/); + assert.match(updaterSource, /autoUpdater\.checkForUpdates\(\)/); + assert.doesNotMatch(updaterSource, /quitAndInstall/); + assert.match(mainSource, /initializeUpdateService\(\)/); + assert.match(mainSource, /disposeUpdateService\(\)/); +}); + +test("release configuration publishes GitHub updater metadata with V07", async () => { + const packageJson = JSON.parse(await source("package.json")); + const workflowSource = await source(".github/workflows/github-release.yml"); + + assert.equal(packageJson.version, "7.0.0"); + assert.equal(packageJson.releaseLabel, "V07"); + assert.equal(packageJson.build.appId, "com.nasfm.notesbook"); + assert.deepEqual(packageJson.build.publish, [ + { + provider: "github", + owner: "sufyanaser", + repo: "NASbook", + releaseType: "release", + }, + ]); + assert.match(workflowSource, /latest\.yml/); + assert.match(workflowSource, /\.blockmap/); + assert.match(workflowSource, /NASbook-Setup-\$label\.exe/); +}); diff --git a/tests/editor-productivity-features.test.mjs b/tests/editor-productivity-features.test.mjs index e80ef98..ea1ffe4 100644 --- a/tests/editor-productivity-features.test.mjs +++ b/tests/editor-productivity-features.test.mjs @@ -37,6 +37,7 @@ test("collapsible headings respect divider and heading boundaries", async () => test("collapsible headings remain stable while the editor is unlocked", async () => { const editor = await source("src/renderer/components/NoteEditorArea.tsx"); const collapse = await source("src/renderer/extensions/CollapsibleSections.ts"); + const styles = await source("src/renderer/styles/editor-productivity.css"); assert.match(editor, /CollapsibleSections/); assert.doesNotMatch(editor, /dataset\.nasCollapsible\s*=/); @@ -45,6 +46,10 @@ test("collapsible headings remain stable while the editor is unlocked", async () assert.match(collapse, /event\.stopPropagation\(\)/); assert.match(collapse, /handleDOMEvents/); assert.match(collapse, /window\.requestAnimationFrame\(\(\) => restoreHeadingAnchor/); + assert.match(styles, /width:\s*10px;[\s\S]*height:\s*10px;/); + assert.match(styles, /rotate\(45deg\)/); + assert.match(styles, /:dir\(ltr\)\[data-nas-collapsed="true"\][\s\S]*rotate\(-45deg\)/); + assert.match(styles, /:dir\(rtl\)\[data-nas-collapsed="true"\][\s\S]*rotate\(135deg\)/); }); test("visually formatted paragraphs can become collapsible sections", async () => { @@ -99,15 +104,15 @@ test("editor note actions cannot inherit the hidden note-card action styles", as assert.match(styles, /\.editor-note-actions\s*\{\s*gap: 10px;/); }); -test("release V06 is consistent across app metadata and Windows installer naming", async () => { +test("release V07 is consistent across app metadata and Windows installer naming", async () => { const packageJson = JSON.parse(await source("package.json")); const main = await source("electron/main/index.ts"); const workflow = await source(".github/workflows/windows-release.yml"); - assert.equal(packageJson.version, "6.0.0"); - assert.equal(packageJson.releaseLabel, "V06"); - assert.equal(packageJson.build.win.artifactName, "NASbook Setup V06.exe"); - assert.equal(packageJson.build.nsis.artifactName, "NASbook Setup V06.${ext}"); - assert.match(main, /appVersion: "V06"/); - assert.match(workflow, /NASbook Setup \$label\.exe/); + assert.equal(packageJson.version, "7.0.0"); + assert.equal(packageJson.releaseLabel, "V07"); + assert.equal(packageJson.build.win.artifactName, "NASbook-Setup-V07.exe"); + assert.equal(packageJson.build.nsis.artifactName, "NASbook-Setup-V07.${ext}"); + assert.match(main, /appVersion: "V07"/); + assert.match(workflow, /NASbook-Setup-\$label\.exe/); });