diff --git a/frontend/index.html b/frontend/index.html index b8c8bcc57..5dbaa1893 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,14 +3,14 @@ - + - + Stirling PDF diff --git a/frontend/public/Login/Firstpage.png b/frontend/public/Login/Firstpage.png index 3cee859e7..f12133f4f 100644 Binary files a/frontend/public/Login/Firstpage.png and b/frontend/public/Login/Firstpage.png differ diff --git a/frontend/public/branding/StirlingLogo.svg b/frontend/public/branding/StirlingLogo.svg deleted file mode 100644 index db1f03e00..000000000 --- a/frontend/public/branding/StirlingLogo.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/branding/StirlingLogoLegacy.svg b/frontend/public/branding/StirlingLogoLegacy.svg deleted file mode 100644 index 29a85fbef..000000000 --- a/frontend/public/branding/StirlingLogoLegacy.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/branding/StirlingPDFLogoNoTextLightHC.svg b/frontend/public/branding/StirlingPDFLogoNoTextLightHC.svg deleted file mode 100644 index a909d6016..000000000 --- a/frontend/public/branding/StirlingPDFLogoNoTextLightHC.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/public/branding/old/favicon.ico b/frontend/public/branding/old/favicon.ico deleted file mode 100644 index 8ad57cac7..000000000 Binary files a/frontend/public/branding/old/favicon.ico and /dev/null differ diff --git a/frontend/public/branding/old/favicon.png b/frontend/public/branding/old/favicon.png deleted file mode 100644 index 5edc6eae2..000000000 Binary files a/frontend/public/branding/old/favicon.png and /dev/null differ diff --git a/frontend/public/branding/old/favicon.svg b/frontend/public/branding/old/favicon.svg deleted file mode 100644 index 0fef4393a..000000000 --- a/frontend/public/branding/old/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/classic-logo/Firstpage.png b/frontend/public/classic-logo/Firstpage.png new file mode 100644 index 000000000..3cee859e7 Binary files /dev/null and b/frontend/public/classic-logo/Firstpage.png differ diff --git a/frontend/public/branding/StirlingPDFLogoBlackText.svg b/frontend/public/classic-logo/StirlingPDFLogoBlackText.svg similarity index 100% rename from frontend/public/branding/StirlingPDFLogoBlackText.svg rename to frontend/public/classic-logo/StirlingPDFLogoBlackText.svg diff --git a/frontend/public/branding/StirlingPDFLogoGreyText.svg b/frontend/public/classic-logo/StirlingPDFLogoGreyText.svg similarity index 100% rename from frontend/public/branding/StirlingPDFLogoGreyText.svg rename to frontend/public/classic-logo/StirlingPDFLogoGreyText.svg diff --git a/frontend/public/logo-tooltip.svg b/frontend/public/classic-logo/StirlingPDFLogoNoTextDark.svg similarity index 58% rename from frontend/public/logo-tooltip.svg rename to frontend/public/classic-logo/StirlingPDFLogoNoTextDark.svg index ae4b75a21..06319c171 100644 --- a/frontend/public/logo-tooltip.svg +++ b/frontend/public/classic-logo/StirlingPDFLogoNoTextDark.svg @@ -1,12 +1,11 @@ - - - + + - + diff --git a/frontend/public/branding/StirlingPDFLogoNoTextLight.svg b/frontend/public/classic-logo/StirlingPDFLogoNoTextLight.svg similarity index 100% rename from frontend/public/branding/StirlingPDFLogoNoTextLight.svg rename to frontend/public/classic-logo/StirlingPDFLogoNoTextLight.svg diff --git a/frontend/public/branding/StirlingPDFLogoWhiteText.svg b/frontend/public/classic-logo/StirlingPDFLogoWhiteText.svg similarity index 100% rename from frontend/public/branding/StirlingPDFLogoWhiteText.svg rename to frontend/public/classic-logo/StirlingPDFLogoWhiteText.svg diff --git a/frontend/public/favicon.ico b/frontend/public/classic-logo/favicon.ico similarity index 100% rename from frontend/public/favicon.ico rename to frontend/public/classic-logo/favicon.ico diff --git a/frontend/public/branding/StirlingPDFLogoNoTextDark.svg b/frontend/public/classic-logo/logo-tooltip.svg similarity index 68% rename from frontend/public/branding/StirlingPDFLogoNoTextDark.svg rename to frontend/public/classic-logo/logo-tooltip.svg index 271fb343e..a19eaabc9 100644 --- a/frontend/public/branding/StirlingPDFLogoNoTextDark.svg +++ b/frontend/public/classic-logo/logo-tooltip.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/frontend/public/logo192.png b/frontend/public/classic-logo/logo192.png similarity index 100% rename from frontend/public/logo192.png rename to frontend/public/classic-logo/logo192.png diff --git a/frontend/public/logo512.png b/frontend/public/classic-logo/logo512.png similarity index 100% rename from frontend/public/logo512.png rename to frontend/public/classic-logo/logo512.png diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 29317af86..9c1f3c253 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -366,6 +366,12 @@ "fullscreen": "Fullscreen", "sidebar": "Sidebar" }, + "logoVariant": { + "label": "Interface logo", + "description": "Override the system default logo style for this device only. Controls whether Stirling PDF uses the modern or classic logo across the interface, login header, landing page, and favicons.", + "modern": "Modern", + "classic": "Classic" + }, "defaultPdfEditor": "Default PDF editor", "defaultPdfEditorActive": "Stirling PDF is your default PDF editor", "defaultPdfEditorInactive": "Another application is set as default", @@ -3510,7 +3516,24 @@ "unexpectedError": "Unexpected error: {{message}}", "accountCreatedSuccess": "Account created successfully! You can now sign in.", "passwordChangedSuccess": "Password changed successfully! Please sign in with your new password.", - "credentialsUpdated": "Your credentials have been updated. Please sign in again." + "credentialsUpdated": "Your credentials have been updated. Please sign in again.", + "slides": { + "overview": { + "alt": "Stirling PDF overview", + "title": "Your one-stop-shop for all your PDF needs.", + "subtitle": "A privacy-first cloud suite for PDFs that lets you convert, sign, redact, and manage documents, along with 50+ other powerful tools." + }, + "edit": { + "alt": "Edit PDFs", + "title": "Edit PDFs to display/secure the information you want", + "subtitle": "With over a dozen tools to help you redact, sign, read and manipulate PDFs, you will be sure to find what you are looking for." + }, + "secure": { + "alt": "Secure PDFs", + "title": "Protect sensitive information in your PDFs", + "subtitle": "Add passwords, redact content, and manage certificates with ease." + } + } }, "signup": { "title": "Create an account", @@ -3970,8 +3993,16 @@ "saved": "Settings saved successfully", "saveSuccess": "Settings saved successfully", "save": "Save Changes", + "discard": "Discard", "restartRequired": "Restart Required", "loginRequired": "Login mode must be enabled to modify admin settings", + "unsavedChanges": { + "title": "Unsaved Changes", + "message": "You have unsaved changes. Do you want to discard them?", + "cancel": "Keep Editing", + "discard": "Discard Changes", + "hint": "You have unsaved changes" + }, "loginDisabled": { "title": "Login Mode Required", "message": "Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server.", @@ -4049,7 +4080,7 @@ }, "logoStyle": { "label": "Logo Style", - "description": "Choose between the modern minimalist logo or the classic S icon", + "description": "Set the default logo style for all users on this server. Users can override this setting in their personal preferences.", "classic": "Classic", "modern": "Modern" }, diff --git a/frontend/public/manifest-classic.json b/frontend/public/manifest-classic.json new file mode 100644 index 000000000..9b47da7d0 --- /dev/null +++ b/frontend/public/manifest-classic.json @@ -0,0 +1,26 @@ +{ + "short_name": "Stirling PDF", + "name": "Stirling PDF", + "icons": [ + { + "src": "classic-logo/favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "classic-logo/logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "classic-logo/logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index c300e9244..039dc00dc 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -3,17 +3,17 @@ "name": "Stirling PDF", "icons": [ { - "src": "favicon.ico", + "src": "modern-logo/favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "modern-logo/logo192.png", "type": "image/png", "sizes": "192x192" }, { - "src": "logo512.png", + "src": "modern-logo/logo512.png", "type": "image/png", "sizes": "512x512" } diff --git a/frontend/public/modern-logo/Firstpage.png b/frontend/public/modern-logo/Firstpage.png new file mode 100644 index 000000000..f12133f4f Binary files /dev/null and b/frontend/public/modern-logo/Firstpage.png differ diff --git a/frontend/public/modern-logo/StirlingPDFLogoBlackText.svg b/frontend/public/modern-logo/StirlingPDFLogoBlackText.svg new file mode 100644 index 000000000..a4a1a1f87 --- /dev/null +++ b/frontend/public/modern-logo/StirlingPDFLogoBlackText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/modern-logo/StirlingPDFLogoGreyText.svg b/frontend/public/modern-logo/StirlingPDFLogoGreyText.svg new file mode 100644 index 000000000..deac1ee16 --- /dev/null +++ b/frontend/public/modern-logo/StirlingPDFLogoGreyText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/modern-logo/StirlingPDFLogoNoTextDark.svg b/frontend/public/modern-logo/StirlingPDFLogoNoTextDark.svg new file mode 100644 index 000000000..a6f82dd6f --- /dev/null +++ b/frontend/public/modern-logo/StirlingPDFLogoNoTextDark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/modern-logo/StirlingPDFLogoNoTextLight.svg b/frontend/public/modern-logo/StirlingPDFLogoNoTextLight.svg new file mode 100644 index 000000000..62a5b3838 --- /dev/null +++ b/frontend/public/modern-logo/StirlingPDFLogoNoTextLight.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/modern-logo/StirlingPDFLogoWhiteText.svg b/frontend/public/modern-logo/StirlingPDFLogoWhiteText.svg new file mode 100644 index 000000000..ade693787 --- /dev/null +++ b/frontend/public/modern-logo/StirlingPDFLogoWhiteText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/modern-logo/favicon.ico b/frontend/public/modern-logo/favicon.ico new file mode 100644 index 000000000..6d6c8521c Binary files /dev/null and b/frontend/public/modern-logo/favicon.ico differ diff --git a/frontend/public/modern-logo/logo-tooltip.svg b/frontend/public/modern-logo/logo-tooltip.svg new file mode 100644 index 000000000..2d53f287c --- /dev/null +++ b/frontend/public/modern-logo/logo-tooltip.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/public/modern-logo/logo192.png b/frontend/public/modern-logo/logo192.png new file mode 100644 index 000000000..2994ca293 Binary files /dev/null and b/frontend/public/modern-logo/logo192.png differ diff --git a/frontend/public/modern-logo/logo512.png b/frontend/public/modern-logo/logo512.png new file mode 100644 index 000000000..b48155073 Binary files /dev/null and b/frontend/public/modern-logo/logo512.png differ diff --git a/frontend/public/og_images/add-attachments.png b/frontend/public/og_images/add-attachments.png index a512c63fd..6fa6c30ad 100644 Binary files a/frontend/public/og_images/add-attachments.png and b/frontend/public/og_images/add-attachments.png differ diff --git a/frontend/public/og_images/add-image.png b/frontend/public/og_images/add-image.png index 517df62c3..a440568a8 100644 Binary files a/frontend/public/og_images/add-image.png and b/frontend/public/og_images/add-image.png differ diff --git a/frontend/public/og_images/add-page-numbers.png b/frontend/public/og_images/add-page-numbers.png index f56e6812a..13a5ff64e 100644 Binary files a/frontend/public/og_images/add-page-numbers.png and b/frontend/public/og_images/add-page-numbers.png differ diff --git a/frontend/public/og_images/add-password.png b/frontend/public/og_images/add-password.png index d62c36cf9..2da6fffaf 100644 Binary files a/frontend/public/og_images/add-password.png and b/frontend/public/og_images/add-password.png differ diff --git a/frontend/public/og_images/add-stamp.png b/frontend/public/og_images/add-stamp.png index 3ea3898d3..6427e67fa 100644 Binary files a/frontend/public/og_images/add-stamp.png and b/frontend/public/og_images/add-stamp.png differ diff --git a/frontend/public/og_images/add-watermark.png b/frontend/public/og_images/add-watermark.png index 3b0d1d2c3..eded1f352 100644 Binary files a/frontend/public/og_images/add-watermark.png and b/frontend/public/og_images/add-watermark.png differ diff --git a/frontend/public/og_images/adjust-colors-contrast.png b/frontend/public/og_images/adjust-colors-contrast.png index 3dbd697e3..9a9164c9f 100644 Binary files a/frontend/public/og_images/adjust-colors-contrast.png and b/frontend/public/og_images/adjust-colors-contrast.png differ diff --git a/frontend/public/og_images/adjust-page-size-scale.png b/frontend/public/og_images/adjust-page-size-scale.png index 6f0fb3bb9..ac17d0b54 100644 Binary files a/frontend/public/og_images/adjust-page-size-scale.png and b/frontend/public/og_images/adjust-page-size-scale.png differ diff --git a/frontend/public/og_images/auto-rename-pdf-file.png b/frontend/public/og_images/auto-rename-pdf-file.png index 884f5a8d1..540780798 100644 Binary files a/frontend/public/og_images/auto-rename-pdf-file.png and b/frontend/public/og_images/auto-rename-pdf-file.png differ diff --git a/frontend/public/og_images/auto-split-by-size-count.png b/frontend/public/og_images/auto-split-by-size-count.png index b8983e378..59c7ed77c 100644 Binary files a/frontend/public/og_images/auto-split-by-size-count.png and b/frontend/public/og_images/auto-split-by-size-count.png differ diff --git a/frontend/public/og_images/auto-split-pages.png b/frontend/public/og_images/auto-split-pages.png index 5d9f99be6..6929078e7 100644 Binary files a/frontend/public/og_images/auto-split-pages.png and b/frontend/public/og_images/auto-split-pages.png differ diff --git a/frontend/public/og_images/automate.png b/frontend/public/og_images/automate.png index 83b2d28e3..a06310701 100644 Binary files a/frontend/public/og_images/automate.png and b/frontend/public/og_images/automate.png differ diff --git a/frontend/public/og_images/certSign.png b/frontend/public/og_images/certSign.png index 83e708ddf..ce587e2ef 100644 Binary files a/frontend/public/og_images/certSign.png and b/frontend/public/og_images/certSign.png differ diff --git a/frontend/public/og_images/change-metadata.png b/frontend/public/og_images/change-metadata.png index 8951d0325..fb283aa8c 100644 Binary files a/frontend/public/og_images/change-metadata.png and b/frontend/public/og_images/change-metadata.png differ diff --git a/frontend/public/og_images/change-permissions.png b/frontend/public/og_images/change-permissions.png index 407319b13..c4e1a11e7 100644 Binary files a/frontend/public/og_images/change-permissions.png and b/frontend/public/og_images/change-permissions.png differ diff --git a/frontend/public/og_images/compare.png b/frontend/public/og_images/compare.png index 1734a9ddf..708c28837 100644 Binary files a/frontend/public/og_images/compare.png and b/frontend/public/og_images/compare.png differ diff --git a/frontend/public/og_images/compress.png b/frontend/public/og_images/compress.png index d48c34a06..1a75b905f 100644 Binary files a/frontend/public/og_images/compress.png and b/frontend/public/og_images/compress.png differ diff --git a/frontend/public/og_images/convert.png b/frontend/public/og_images/convert.png index 07332ce8d..3c2210ca0 100644 Binary files a/frontend/public/og_images/convert.png and b/frontend/public/og_images/convert.png differ diff --git a/frontend/public/og_images/cropPdf.png b/frontend/public/og_images/cropPdf.png index 509e7f62a..59590d4df 100644 Binary files a/frontend/public/og_images/cropPdf.png and b/frontend/public/og_images/cropPdf.png differ diff --git a/frontend/public/og_images/detect-split-scanned-photos.png b/frontend/public/og_images/detect-split-scanned-photos.png index fcd6beab5..2ba6cf63f 100644 Binary files a/frontend/public/og_images/detect-split-scanned-photos.png and b/frontend/public/og_images/detect-split-scanned-photos.png differ diff --git a/frontend/public/og_images/edit-table-of-contents.png b/frontend/public/og_images/edit-table-of-contents.png index 79f1a5987..64092cbbe 100644 Binary files a/frontend/public/og_images/edit-table-of-contents.png and b/frontend/public/og_images/edit-table-of-contents.png differ diff --git a/frontend/public/og_images/extract-images.png b/frontend/public/og_images/extract-images.png index 69905c6e0..5d403dbc3 100644 Binary files a/frontend/public/og_images/extract-images.png and b/frontend/public/og_images/extract-images.png differ diff --git a/frontend/public/og_images/extract-pages.png b/frontend/public/og_images/extract-pages.png index d9ff2f58c..075c8f763 100644 Binary files a/frontend/public/og_images/extract-pages.png and b/frontend/public/og_images/extract-pages.png differ diff --git a/frontend/public/og_images/flatten.png b/frontend/public/og_images/flatten.png index f5d9ba133..f9a449a20 100644 Binary files a/frontend/public/og_images/flatten.png and b/frontend/public/og_images/flatten.png differ diff --git a/frontend/public/og_images/get-all-info-on-pdf.png b/frontend/public/og_images/get-all-info-on-pdf.png index 0d19ab6e2..77fc4cf0f 100644 Binary files a/frontend/public/og_images/get-all-info-on-pdf.png and b/frontend/public/og_images/get-all-info-on-pdf.png differ diff --git a/frontend/public/og_images/home.png b/frontend/public/og_images/home.png index b8256245b..577f8f476 100644 Binary files a/frontend/public/og_images/home.png and b/frontend/public/og_images/home.png differ diff --git a/frontend/public/og_images/manage-certificates.png b/frontend/public/og_images/manage-certificates.png index 824b26a4e..da02e0847 100644 Binary files a/frontend/public/og_images/manage-certificates.png and b/frontend/public/og_images/manage-certificates.png differ diff --git a/frontend/public/og_images/mergePdfs.png b/frontend/public/og_images/mergePdfs.png index e798b7b2e..cd496d70e 100644 Binary files a/frontend/public/og_images/mergePdfs.png and b/frontend/public/og_images/mergePdfs.png differ diff --git a/frontend/public/og_images/multi-page-layout.png b/frontend/public/og_images/multi-page-layout.png index 5dd42a04b..e6eb5514b 100644 Binary files a/frontend/public/og_images/multi-page-layout.png and b/frontend/public/og_images/multi-page-layout.png differ diff --git a/frontend/public/og_images/multi-tool.png b/frontend/public/og_images/multi-tool.png index 634e8ab23..b9f0812bf 100644 Binary files a/frontend/public/og_images/multi-tool.png and b/frontend/public/og_images/multi-tool.png differ diff --git a/frontend/public/og_images/ocr.png b/frontend/public/og_images/ocr.png index cb9fade26..caf133e92 100644 Binary files a/frontend/public/og_images/ocr.png and b/frontend/public/og_images/ocr.png differ diff --git a/frontend/public/og_images/overlay-pdfs.png b/frontend/public/og_images/overlay-pdfs.png index c1b15ac39..da5484ba9 100644 Binary files a/frontend/public/og_images/overlay-pdfs.png and b/frontend/public/og_images/overlay-pdfs.png differ diff --git a/frontend/public/og_images/read.png b/frontend/public/og_images/read.png index 17292db7d..3f88441b0 100644 Binary files a/frontend/public/og_images/read.png and b/frontend/public/og_images/read.png differ diff --git a/frontend/public/og_images/redact.png b/frontend/public/og_images/redact.png index bb9941ba2..69d7d5b5d 100644 Binary files a/frontend/public/og_images/redact.png and b/frontend/public/og_images/redact.png differ diff --git a/frontend/public/og_images/remove-annotations.png b/frontend/public/og_images/remove-annotations.png index 52281c528..12b7de671 100644 Binary files a/frontend/public/og_images/remove-annotations.png and b/frontend/public/og_images/remove-annotations.png differ diff --git a/frontend/public/og_images/remove-blank-pages.png b/frontend/public/og_images/remove-blank-pages.png index 3a6892301..1675c0754 100644 Binary files a/frontend/public/og_images/remove-blank-pages.png and b/frontend/public/og_images/remove-blank-pages.png differ diff --git a/frontend/public/og_images/remove-certificate-sign.png b/frontend/public/og_images/remove-certificate-sign.png index e350f3f47..0a97e97f3 100644 Binary files a/frontend/public/og_images/remove-certificate-sign.png and b/frontend/public/og_images/remove-certificate-sign.png differ diff --git a/frontend/public/og_images/remove-image.png b/frontend/public/og_images/remove-image.png index f60c6f47f..74a7067f2 100644 Binary files a/frontend/public/og_images/remove-image.png and b/frontend/public/og_images/remove-image.png differ diff --git a/frontend/public/og_images/remove-password.png b/frontend/public/og_images/remove-password.png index c8fdc26ee..7022b7e4d 100644 Binary files a/frontend/public/og_images/remove-password.png and b/frontend/public/og_images/remove-password.png differ diff --git a/frontend/public/og_images/remove.png b/frontend/public/og_images/remove.png index 7e5472ea5..a0e66d1fd 100644 Binary files a/frontend/public/og_images/remove.png and b/frontend/public/og_images/remove.png differ diff --git a/frontend/public/og_images/reorganize-pages.png b/frontend/public/og_images/reorganize-pages.png index 8294ff8b3..7dbaf6824 100644 Binary files a/frontend/public/og_images/reorganize-pages.png and b/frontend/public/og_images/reorganize-pages.png differ diff --git a/frontend/public/og_images/repair.png b/frontend/public/og_images/repair.png index 02ed341c7..a531c1ef8 100644 Binary files a/frontend/public/og_images/repair.png and b/frontend/public/og_images/repair.png differ diff --git a/frontend/public/og_images/replace-and-invert-color.png b/frontend/public/og_images/replace-and-invert-color.png index 46f2e496f..d476a9d17 100644 Binary files a/frontend/public/og_images/replace-and-invert-color.png and b/frontend/public/og_images/replace-and-invert-color.png differ diff --git a/frontend/public/og_images/rotate.png b/frontend/public/og_images/rotate.png index 95a127449..c2eeb8170 100644 Binary files a/frontend/public/og_images/rotate.png and b/frontend/public/og_images/rotate.png differ diff --git a/frontend/public/og_images/sanitize.png b/frontend/public/og_images/sanitize.png index 884c53331..efceca8b0 100644 Binary files a/frontend/public/og_images/sanitize.png and b/frontend/public/og_images/sanitize.png differ diff --git a/frontend/public/og_images/scanner-effect.png b/frontend/public/og_images/scanner-effect.png index 182d19b37..b46275cd8 100644 Binary files a/frontend/public/og_images/scanner-effect.png and b/frontend/public/og_images/scanner-effect.png differ diff --git a/frontend/public/og_images/show-javascript.png b/frontend/public/og_images/show-javascript.png index 3e8926967..812e06553 100644 Binary files a/frontend/public/og_images/show-javascript.png and b/frontend/public/og_images/show-javascript.png differ diff --git a/frontend/public/og_images/sign.png b/frontend/public/og_images/sign.png index 13ce77067..773a5e37f 100644 Binary files a/frontend/public/og_images/sign.png and b/frontend/public/og_images/sign.png differ diff --git a/frontend/public/og_images/single-large-page.png b/frontend/public/og_images/single-large-page.png index 8b8c630c8..3bc457a99 100644 Binary files a/frontend/public/og_images/single-large-page.png and b/frontend/public/og_images/single-large-page.png differ diff --git a/frontend/public/og_images/split-by-chapters.png b/frontend/public/og_images/split-by-chapters.png index 54bc8ac44..26db04b4c 100644 Binary files a/frontend/public/og_images/split-by-chapters.png and b/frontend/public/og_images/split-by-chapters.png differ diff --git a/frontend/public/og_images/split-by-sections.png b/frontend/public/og_images/split-by-sections.png index 98615352c..e3601ddda 100644 Binary files a/frontend/public/og_images/split-by-sections.png and b/frontend/public/og_images/split-by-sections.png differ diff --git a/frontend/public/og_images/split.png b/frontend/public/og_images/split.png new file mode 100644 index 000000000..a77a065b4 Binary files /dev/null and b/frontend/public/og_images/split.png differ diff --git a/frontend/public/og_images/splitPdf.png b/frontend/public/og_images/splitPdf.png index 1e425d857..a77a065b4 100644 Binary files a/frontend/public/og_images/splitPdf.png and b/frontend/public/og_images/splitPdf.png differ diff --git a/frontend/public/og_images/unlock-pdf-forms.png b/frontend/public/og_images/unlock-pdf-forms.png index 2f6d49f11..3e637cc4e 100644 Binary files a/frontend/public/og_images/unlock-pdf-forms.png and b/frontend/public/og_images/unlock-pdf-forms.png differ diff --git a/frontend/public/og_images/validate-pdf-signature.png b/frontend/public/og_images/validate-pdf-signature.png index d113de8ae..020ccd883 100644 Binary files a/frontend/public/og_images/validate-pdf-signature.png and b/frontend/public/og_images/validate-pdf-signature.png differ diff --git a/frontend/public/og_images/view-pdf.png b/frontend/public/og_images/view-pdf.png index bde21682a..bef62ad51 100644 Binary files a/frontend/public/og_images/view-pdf.png and b/frontend/public/og_images/view-pdf.png differ diff --git a/frontend/scripts/sample-pdf/template.html b/frontend/scripts/sample-pdf/template.html index e4ae57e50..edd7f2c9f 100644 --- a/frontend/scripts/sample-pdf/template.html +++ b/frontend/scripts/sample-pdf/template.html @@ -10,15 +10,15 @@
- - - - - + + + + +
- +

The Free Adobe Acrobat Alternative

diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index 3d313cd68..88093d84b 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { ReactNode, useEffect } from "react"; import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider"; import { FileContextProvider } from "@app/contexts/FileContext"; import { NavigationProvider } from "@app/contexts/NavigationContext"; @@ -21,6 +21,7 @@ import { CookieConsentProvider } from "@app/contexts/CookieConsentContext"; import ErrorBoundary from "@app/components/shared/ErrorBoundary"; import { useScarfTracking } from "@app/hooks/useScarfTracking"; import { useAppInitialization } from "@app/hooks/useAppInitialization"; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; // Component to initialize scarf tracking (must be inside AppConfigProvider) function ScarfTrackingInitializer() { @@ -34,6 +35,30 @@ function AppInitializer() { return null; } +function BrandingAssetManager() { + const { favicon, logo192, manifestHref } = useLogoAssets(); + + useEffect(() => { + if (typeof document === 'undefined') { + return; + } + + const setLinkHref = (selector: string, href: string) => { + const link = document.querySelector(selector); + if (link && link.getAttribute('href') !== href) { + link.setAttribute('href', href); + } + }; + + setLinkHref('link[rel="icon"]', favicon); + setLinkHref('link[rel="shortcut icon"]', favicon); + setLinkHref('link[rel="apple-touch-icon"]', logo192); + setLinkHref('link[rel="manifest"]', manifestHref); + }, [favicon, logo192, manifestHref]); + + return null; +} + // Avoid requirement to have props which are required in app providers anyway type AppConfigProviderOverrides = Omit; @@ -62,6 +87,7 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide + diff --git a/frontend/src/core/components/fileEditor/AddFileCard.tsx b/frontend/src/core/components/fileEditor/AddFileCard.tsx index 4cc3741d3..a4a549d82 100644 --- a/frontend/src/core/components/fileEditor/AddFileCard.tsx +++ b/frontend/src/core/components/fileEditor/AddFileCard.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import AddIcon from '@mui/icons-material/Add'; import { useFilesModalContext } from '@app/contexts/FilesModalContext'; import LocalIcon from '@app/components/shared/LocalIcon'; -import { BASE_PATH } from '@app/constants/app'; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; import styles from '@app/components/fileEditor/FileEditor.module.css'; import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; @@ -25,6 +25,7 @@ const AddFileCard = ({ const { openFilesModal } = useFilesModalContext(); const { colorScheme } = useMantineColorScheme(); const [isUploadHover, setIsUploadHover] = useState(false); + const { wordmark } = useLogoAssets(); const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); @@ -91,7 +92,7 @@ const AddFileCard = ({ {/* Stirling PDF Branding */} Stirling PDF diff --git a/frontend/src/core/components/fileManager/EmptyFilesState.tsx b/frontend/src/core/components/fileManager/EmptyFilesState.tsx index ae46909d1..24e79fb12 100644 --- a/frontend/src/core/components/fileManager/EmptyFilesState.tsx +++ b/frontend/src/core/components/fileManager/EmptyFilesState.tsx @@ -4,7 +4,7 @@ import HistoryIcon from '@mui/icons-material/History'; import { useTranslation } from 'react-i18next'; import { useFileManagerContext } from '@app/contexts/FileManagerContext'; import LocalIcon from '@app/components/shared/LocalIcon'; -import { BASE_PATH } from '@app/constants/app'; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; @@ -13,6 +13,7 @@ const EmptyFilesState: React.FC = () => { const { colorScheme } = useMantineColorScheme(); const { onLocalFileClick } = useFileManagerContext(); const [isUploadHover, setIsUploadHover] = useState(false); + const { wordmark } = useLogoAssets(); const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); @@ -57,7 +58,7 @@ const EmptyFilesState: React.FC = () => { {/* Stirling PDF Logo */} Stirling PDF diff --git a/frontend/src/core/components/shared/AppConfigModal.css b/frontend/src/core/components/shared/AppConfigModal.css index 2ada07184..5b006e5bf 100644 --- a/frontend/src/core/components/shared/AppConfigModal.css +++ b/frontend/src/core/components/shared/AppConfigModal.css @@ -127,4 +127,45 @@ display: flex; justify-content: flex-end; gap: 0.5rem; +} + +/* Settings section container for sticky footer support */ +.settings-section-container { + display: flex; + flex-direction: column; + min-height: 100%; + position: relative; +} + +.settings-section-content { + flex: 1; + padding-bottom: 5rem; /* Space for sticky footer */ +} + +/* Sticky footer for save/discard buttons */ +.settings-sticky-footer { + position: sticky; + bottom: 0; + left: 0; + right: 0; + background: var(--modal-content-bg); + border-top: 1px solid var(--modal-header-border); + padding: 1rem 2rem; + margin: 0 -2rem; + margin-bottom: -1rem; + z-index: 10; + box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1); +} + +/* Mobile adjustments */ +@media (max-width: 1024px) { + .settings-sticky-footer { + padding: 0.75rem 1rem; + margin: 0 -1rem; + margin-bottom: -1rem; + } + + .settings-section-content { + padding-bottom: 4rem; + } } \ No newline at end of file diff --git a/frontend/src/core/components/shared/AppConfigModal.tsx b/frontend/src/core/components/shared/AppConfigModal.tsx index fe351c0ae..5a54e20e6 100644 --- a/frontend/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/src/core/components/shared/AppConfigModal.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { Modal, Text, ActionIcon, Tooltip, Group } from '@mantine/core'; import { useNavigate, useLocation } from 'react-router-dom'; import LocalIcon from '@app/components/shared/LocalIcon'; @@ -9,19 +9,21 @@ import '@app/components/shared/AppConfigModal.css'; import { useIsMobile } from '@app/hooks/useIsMobile'; import { Z_INDEX_OVER_FULLSCREEN_SURFACE, Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; import { useLicenseAlert } from '@app/hooks/useLicenseAlert'; +import { UnsavedChangesProvider, useUnsavedChanges } from '@app/contexts/UnsavedChangesContext'; interface AppConfigModalProps { opened: boolean; onClose: () => void; } -const AppConfigModal: React.FC = ({ opened, onClose }) => { +const AppConfigModalInner: React.FC = ({ opened, onClose }) => { const [active, setActive] = useState('general'); const isMobile = useIsMobile(); const navigate = useNavigate(); const location = useLocation(); const { config } = useAppConfig(); const licenseAlert = useLicenseAlert(); + const { confirmIfDirty } = useUnsavedChanges(); // Extract section from URL path (e.g., /settings/people -> people) const getSectionFromPath = (pathname: string): NavKey | null => { @@ -97,11 +99,22 @@ const AppConfigModal: React.FC = ({ opened, onClose }) => { return null; }, [configNavSections, active]); - const handleClose = () => { + const handleClose = useCallback(async () => { + const canProceed = await confirmIfDirty(); + if (!canProceed) return; + // Navigate back to home when closing modal navigate('/', { replace: true }); onClose(); - }; + }, [confirmIfDirty, navigate, onClose]); + + const handleNavigation = useCallback(async (key: NavKey) => { + const canProceed = await confirmIfDirty(); + if (!canProceed) return; + + setActive(key); + navigate(`/settings/${key}`); + }, [confirmIfDirty, navigate]); return ( = ({ opened, onClose }) => { const navItemContent = (
{ - // Allow navigation even when disabled - the content inside will be disabled - setActive(item.key); - navigate(`/settings/${item.key}`); - }} + onClick={() => handleNavigation(item.key)} className={`modal-nav-item ${isMobile ? 'mobile' : ''}`} style={{ background: isActive ? colors.navItemActiveBg : 'transparent', @@ -226,4 +235,13 @@ const AppConfigModal: React.FC = ({ opened, onClose }) => { ); }; +// Wrapper component that provides the UnsavedChangesContext +const AppConfigModal: React.FC = (props) => { + return ( + + + + ); +}; + export default AppConfigModal; diff --git a/frontend/src/core/components/shared/LandingPage.tsx b/frontend/src/core/components/shared/LandingPage.tsx index 46b75b7c5..68ee35537 100644 --- a/frontend/src/core/components/shared/LandingPage.tsx +++ b/frontend/src/core/components/shared/LandingPage.tsx @@ -5,8 +5,9 @@ import LocalIcon from '@app/components/shared/LocalIcon'; import { useTranslation } from 'react-i18next'; import { useFileHandler } from '@app/hooks/useFileHandler'; import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { BASE_PATH } from '@app/constants/app'; import { useLogoPath } from '@app/hooks/useLogoPath'; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; +import { useLogoVariant } from '@app/hooks/useLogoVariant'; import { useFileManager } from '@app/hooks/useFileManager'; import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; @@ -19,6 +20,8 @@ const LandingPage = () => { const { openFilesModal } = useFilesModalContext(); const [isUploadHover, setIsUploadHover] = React.useState(false); const logoPath = useLogoPath(); + const logoVariant = useLogoVariant(); + const { wordmark } = useLogoAssets(); const { loadRecentFiles } = useFileManager(); const [hasRecents, setHasRecents] = React.useState(false); const terminology = useFileActionTerminology(); @@ -87,24 +90,25 @@ const LandingPage = () => { }, }} > -
- Stirling PDF Logo -
+ > + Stirling PDF Logo +
+ )}
{ {/* Stirling PDF Branding */} Stirling PDF diff --git a/frontend/src/core/components/shared/Tooltip.tsx b/frontend/src/core/components/shared/Tooltip.tsx index 7e7ee5750..db6bce849 100644 --- a/frontend/src/core/components/shared/Tooltip.tsx +++ b/frontend/src/core/components/shared/Tooltip.tsx @@ -6,7 +6,7 @@ import { useTooltipPosition } from '@app/hooks/useTooltipPosition'; import { TooltipTip } from '@app/types/tips'; import { TooltipContent } from '@app/components/shared/tooltip/TooltipContent'; import { useSidebarContext } from '@app/contexts/SidebarContext'; -import { BASE_PATH } from '@app/constants/app'; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; import styles from '@app/components/shared/tooltip/Tooltip.module.css'; import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex'; @@ -58,6 +58,7 @@ export const Tooltip: React.FC = ({ }) => { const [internalOpen, setInternalOpen] = useState(false); const [isPinned, setIsPinned] = useState(false); + const { tooltipLogo } = useLogoAssets(); const triggerRef = useRef(null); const tooltipRef = useRef(null); @@ -352,7 +353,7 @@ export const Tooltip: React.FC = ({
{header.logo || ( Stirling PDF diff --git a/frontend/src/core/components/tools/FullscreenToolSurface.tsx b/frontend/src/core/components/tools/FullscreenToolSurface.tsx index beebaefe0..2eedbf5f5 100644 --- a/frontend/src/core/components/tools/FullscreenToolSurface.tsx +++ b/frontend/src/core/components/tools/FullscreenToolSurface.tsx @@ -7,8 +7,8 @@ import FullscreenToolList from '@app/components/tools/FullscreenToolList'; import { ToolRegistryEntry } from '@app/data/toolsTaxonomy'; import { ToolId } from '@app/types/toolId'; import { useFocusTrap } from '@app/hooks/useFocusTrap'; -import { BASE_PATH } from '@app/constants/app'; import { useLogoPath } from '@app/hooks/useLogoPath'; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; import { Tooltip } from '@app/components/shared/Tooltip'; import '@app/components/tools/ToolPanel.css'; import { ToolPanelGeometry } from '@app/hooks/tools/useToolPanelGeometry'; @@ -54,9 +54,8 @@ const FullscreenToolSurface = ({ const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo"); const brandIconSrc = useLogoPath(); - const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${ - colorScheme === "dark" ? "White" : "Black" - }Text.svg`; + const { wordmark } = useLogoAssets(); + const brandTextSrc = colorScheme === "dark" ? wordmark.white : wordmark.black; const handleExit = () => { const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; diff --git a/frontend/src/core/constants/logo.ts b/frontend/src/core/constants/logo.ts new file mode 100644 index 000000000..7838f396b --- /dev/null +++ b/frontend/src/core/constants/logo.ts @@ -0,0 +1,15 @@ +import type { LogoVariant } from '@app/services/preferencesService'; + +export const LOGO_FOLDER_BY_VARIANT: Record = { + modern: 'modern-logo', + classic: 'classic-logo', +}; + +export const ensureLogoVariant = (value?: string | null): LogoVariant => { + return value === 'classic' ? 'classic' : 'modern'; +}; + +export const getLogoFolder = (variant?: LogoVariant | null): string => { + return LOGO_FOLDER_BY_VARIANT[ensureLogoVariant(variant)]; +}; + diff --git a/frontend/src/core/contexts/UnsavedChangesContext.tsx b/frontend/src/core/contexts/UnsavedChangesContext.tsx new file mode 100644 index 000000000..057239400 --- /dev/null +++ b/frontend/src/core/contexts/UnsavedChangesContext.tsx @@ -0,0 +1,95 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import { Modal, Text, Button, Group, Stack } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; + +interface UnsavedChangesContextType { + isDirty: boolean; + setIsDirty: (dirty: boolean) => void; + /** + * Call this before navigating away or closing. + * Returns a promise that resolves to true if safe to proceed, false if blocked. + */ + confirmIfDirty: () => Promise; + /** + * Reset dirty state (call after successful save) + */ + markClean: () => void; +} + +const UnsavedChangesContext = createContext(undefined); + +interface UnsavedChangesProviderProps { + children: ReactNode; +} + +export function UnsavedChangesProvider({ children }: UnsavedChangesProviderProps) { + const { t } = useTranslation(); + const [isDirty, setIsDirty] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null); + + const confirmIfDirty = useCallback((): Promise => { + if (!isDirty) { + return Promise.resolve(true); + } + + return new Promise((resolve) => { + setResolvePromise(() => resolve); + setModalOpen(true); + }); + }, [isDirty]); + + const markClean = useCallback(() => { + setIsDirty(false); + }, []); + + const handleDiscard = () => { + setModalOpen(false); + setIsDirty(false); + resolvePromise?.(true); + setResolvePromise(null); + }; + + const handleCancel = () => { + setModalOpen(false); + resolvePromise?.(false); + setResolvePromise(null); + }; + + return ( + + {children} + + + + {t('admin.settings.unsavedChanges.message', 'You have unsaved changes. Do you want to discard them?')} + + + + + + + + + ); +} + +export function useUnsavedChanges(): UnsavedChangesContextType { + const context = useContext(UnsavedChangesContext); + if (!context) { + throw new Error('useUnsavedChanges must be used within an UnsavedChangesProvider'); + } + return context; +} + diff --git a/frontend/src/core/hooks/useLogoAssets.test.ts b/frontend/src/core/hooks/useLogoAssets.test.ts new file mode 100644 index 000000000..3f36d262d --- /dev/null +++ b/frontend/src/core/hooks/useLogoAssets.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { LOGO_FOLDER_BY_VARIANT } from '@app/constants/logo'; +import type { LogoVariant } from '@app/services/preferencesService'; + +/** + * Tests that all required logo assets exist for each logo variant. + * This ensures that when useLogoAssets returns paths, those files actually exist. + */ +describe('useLogoAssets - Logo Asset Files', () => { + const publicDir = path.resolve(__dirname, '../../../public'); + + // All asset files that useLogoAssets references + const requiredAssets = [ + 'logo-tooltip.svg', + 'Firstpage.png', + 'favicon.ico', + 'logo192.png', + 'logo512.png', + 'StirlingPDFLogoWhiteText.svg', + 'StirlingPDFLogoBlackText.svg', + 'StirlingPDFLogoGreyText.svg', + ]; + + const logoVariants: LogoVariant[] = ['modern', 'classic']; + + describe.each(logoVariants)('%s logo variant', (variant) => { + const folder = LOGO_FOLDER_BY_VARIANT[variant]; + const folderPath = path.join(publicDir, folder); + + test(`folder "${folder}" should exist`, () => { + expect(fs.existsSync(folderPath)).toBe(true); + }); + + test.each(requiredAssets)('should have %s', (assetName) => { + const assetPath = path.join(folderPath, assetName); + expect( + fs.existsSync(assetPath), + `Missing asset: ${folder}/${assetName}` + ).toBe(true); + }); + }); + + describe('manifest files', () => { + test('manifest.json should exist for modern variant', () => { + const manifestPath = path.join(publicDir, 'manifest.json'); + expect(fs.existsSync(manifestPath)).toBe(true); + }); + + test('manifest-classic.json should exist for classic variant', () => { + const manifestPath = path.join(publicDir, 'manifest-classic.json'); + expect(fs.existsSync(manifestPath)).toBe(true); + }); + }); +}); + diff --git a/frontend/src/core/hooks/useLogoAssets.ts b/frontend/src/core/hooks/useLogoAssets.ts new file mode 100644 index 000000000..4d80b0809 --- /dev/null +++ b/frontend/src/core/hooks/useLogoAssets.ts @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import { BASE_PATH } from '@app/constants/app'; +import { getLogoFolder } from '@app/constants/logo'; +import { useLogoVariant } from '@app/hooks/useLogoVariant'; + +export function useLogoAssets() { + const logoVariant = useLogoVariant(); + + return useMemo(() => { + const folder = getLogoFolder(logoVariant); + const folderPath = `${BASE_PATH}/${folder}`; + + return { + logoVariant, + folder, + folderPath, + getAssetPath: (name: string) => `${folderPath}/${name}`, + tooltipLogo: `${folderPath}/logo-tooltip.svg`, + firstPage: `${folderPath}/Firstpage.png`, + favicon: `${folderPath}/favicon.ico`, + logo192: `${folderPath}/logo192.png`, + logo512: `${folderPath}/logo512.png`, + wordmark: { + white: `${folderPath}/StirlingPDFLogoWhiteText.svg`, + black: `${folderPath}/StirlingPDFLogoBlackText.svg`, + grey: `${folderPath}/StirlingPDFLogoGreyText.svg`, + }, + manifestHref: logoVariant === 'classic' + ? `${BASE_PATH}/manifest-classic.json` + : `${BASE_PATH}/manifest.json`, + }; + }, [logoVariant]); +} + diff --git a/frontend/src/core/hooks/useLogoPath.ts b/frontend/src/core/hooks/useLogoPath.ts index db97f9c08..a4901334c 100644 --- a/frontend/src/core/hooks/useLogoPath.ts +++ b/frontend/src/core/hooks/useLogoPath.ts @@ -1,31 +1,22 @@ import { useMemo } from 'react'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; import { useMantineColorScheme } from '@mantine/core'; -import { BASE_PATH } from '@app/constants/app'; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; /** * Hook to get the correct logo path based on app config (logo style) and theme (light/dark) * * Logo styles: - * - classic: branding/old/favicon.svg (classic S logo - default) - * - modern: StirlingPDFLogoNoText{Light|Dark}.svg (minimalist modern design) + * - classic: classic S logo stored in /classic-logo + * - modern: minimalist logo stored in /modern-logo * * @returns The path to the appropriate logo SVG file */ export function useLogoPath(): string { - const { config } = useAppConfig(); const { colorScheme } = useMantineColorScheme(); + const { folderPath } = useLogoAssets(); return useMemo(() => { - const logoStyle = config?.logoStyle || 'classic'; - - if (logoStyle === 'classic') { - // Classic logo (old favicon) - same for both light and dark modes - return `${BASE_PATH}/branding/old/favicon.svg`; - } - - // Modern logo - different for light and dark modes const themeSuffix = colorScheme === 'dark' ? 'Dark' : 'Light'; - return `${BASE_PATH}/branding/StirlingPDFLogoNoText${themeSuffix}.svg`; - }, [config?.logoStyle, colorScheme]); + return `${folderPath}/StirlingPDFLogoNoText${themeSuffix}.svg`; + }, [colorScheme, folderPath]); } diff --git a/frontend/src/core/hooks/useLogoVariant.ts b/frontend/src/core/hooks/useLogoVariant.ts new file mode 100644 index 000000000..3df447c09 --- /dev/null +++ b/frontend/src/core/hooks/useLogoVariant.ts @@ -0,0 +1,18 @@ +import { useMemo } from 'react'; +import { usePreferences } from '@app/contexts/PreferencesContext'; +import { useAppConfig } from '@app/contexts/AppConfigContext'; +import type { LogoVariant } from '@app/services/preferencesService'; +import { ensureLogoVariant } from '@app/constants/logo'; + +export function useLogoVariant(): LogoVariant { + const { preferences } = usePreferences(); + const { config } = useAppConfig(); + + return useMemo(() => { + // Check local storage first, then fall back to server config + const preferenceVariant = preferences.logoVariant; + const configVariant = config?.logoStyle; + return ensureLogoVariant(preferenceVariant ?? configVariant); + }, [config?.logoStyle, preferences.logoVariant]); +} + diff --git a/frontend/src/core/pages/HomePage.tsx b/frontend/src/core/pages/HomePage.tsx index cc9a3e783..e03f46ee2 100644 --- a/frontend/src/core/pages/HomePage.tsx +++ b/frontend/src/core/pages/HomePage.tsx @@ -4,11 +4,11 @@ import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; import { Group, useMantineColorScheme } from "@mantine/core"; import { useSidebarContext } from "@app/contexts/SidebarContext"; import { useDocumentMeta } from "@app/hooks/useDocumentMeta"; -import { BASE_PATH } from "@app/constants/app"; import { useBaseUrl } from "@app/hooks/useBaseUrl"; import { useIsMobile } from "@app/hooks/useIsMobile"; import { useAppConfig } from "@app/contexts/AppConfigContext"; import { useLogoPath } from "@app/hooks/useLogoPath"; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; import { useCookieConsentContext } from "@app/contexts/CookieConsentContext"; import { useFileContext } from "@app/contexts/file/fileHooks"; import { useNavigationActions } from "@app/contexts/NavigationContext"; @@ -84,9 +84,8 @@ export default function HomePage() { const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo"); const brandIconSrc = useLogoPath(); - const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${ - colorScheme === "dark" ? "White" : "Black" - }Text.svg`; + const { wordmark } = useLogoAssets(); + const brandTextSrc = colorScheme === "dark" ? wordmark.white : wordmark.black; const handleSelectMobileView = useCallback((view: MobileView) => { setActiveMobileView(view); diff --git a/frontend/src/core/services/preferencesService.ts b/frontend/src/core/services/preferencesService.ts index 80c35e4b1..9fd472897 100644 --- a/frontend/src/core/services/preferencesService.ts +++ b/frontend/src/core/services/preferencesService.ts @@ -1,6 +1,8 @@ import { type ToolPanelMode, DEFAULT_TOOL_PANEL_MODE } from '@app/constants/toolPanel'; import { type ThemeMode, getSystemTheme } from '@app/constants/theme'; +export type LogoVariant = 'modern' | 'classic'; + export interface UserPreferences { autoUnzip: boolean; autoUnzipFileLimit: number; @@ -14,6 +16,7 @@ export interface UserPreferences { hasSeenCookieBanner: boolean; hideUnavailableTools: boolean; hideUnavailableConversions: boolean; + logoVariant: LogoVariant | null; } export const DEFAULT_PREFERENCES: UserPreferences = { @@ -29,6 +32,7 @@ export const DEFAULT_PREFERENCES: UserPreferences = { hasSeenCookieBanner: false, hideUnavailableTools: false, hideUnavailableConversions: false, + logoVariant: null, }; const STORAGE_KEY = 'stirlingpdf_preferences'; diff --git a/frontend/src/proprietary/components/AppProviders.tsx b/frontend/src/proprietary/components/AppProviders.tsx index a0e9a63dc..e92cd5573 100644 --- a/frontend/src/proprietary/components/AppProviders.tsx +++ b/frontend/src/proprietary/components/AppProviders.tsx @@ -2,9 +2,9 @@ import { AppProviders as CoreAppProviders, AppProvidersProps } from "@core/compo import { AuthProvider } from "@app/auth/UseSession"; import { LicenseProvider } from "@app/contexts/LicenseContext"; import { CheckoutProvider } from "@app/contexts/CheckoutContext"; +import { UpdateSeatsProvider } from "@app/contexts/UpdateSeatsContext" import { UpgradeBannerInitializer } from "@app/components/shared/UpgradeBannerInitializer"; import { ServerExperienceProvider } from "@app/contexts/ServerExperienceContext"; -import { UpdateSeatsProvider } from "@app/contexts/UpdateSeatsContext"; export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) { return ( diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx index 3e2b445a6..b3bc6c99d 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, MultiSelect, Badge, SegmentedControl } from '@mantine/core'; import { alert } from '@app/components/toast'; @@ -9,6 +9,8 @@ import PendingBadge from '@app/components/shared/config/PendingBadge'; import apiClient from '@app/services/apiClient'; import { useLoginRequired } from '@app/hooks/useLoginRequired'; import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; +import { usePreferences } from '@app/contexts/PreferencesContext'; +import { useUnsavedChanges } from '@app/contexts/UnsavedChangesContext'; interface GeneralSettingsData { ui: { @@ -45,6 +47,13 @@ export default function AdminGeneralSection() { const { t } = useTranslation(); const { loginEnabled, validateLoginEnabled } = useLoginRequired(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); + const { preferences, updatePreference } = usePreferences(); + const { setIsDirty, markClean } = useUnsavedChanges(); + + // Track original settings for dirty detection + const [originalSettingsSnapshot, setOriginalSettingsSnapshot] = useState(''); + const [isDirty, setLocalIsDirty] = useState(false); + const isInitialLoad = useRef(true); const { settings, @@ -149,9 +158,79 @@ export default function AdminGeneralSection() { } }, [loginEnabled, fetchSettings]); + // Snapshot original settings after initial load and sync local preference with server + useEffect(() => { + if (!loading && isInitialLoad.current && Object.keys(settings).length > 0) { + setOriginalSettingsSnapshot(JSON.stringify(settings)); + + // Sync local preference with server setting on initial load to ensure they're in sync + // This ensures localStorage always reflects the server's authoritative value + if (loginEnabled && settings.ui?.logoStyle) { + updatePreference('logoVariant', settings.ui.logoStyle); + } + + isInitialLoad.current = false; + } + }, [loading, settings, loginEnabled, updatePreference]); + + // Track dirty state by comparing current settings to snapshot + useEffect(() => { + if (!originalSettingsSnapshot || loading) return; + + const currentSnapshot = JSON.stringify(settings); + const dirty = currentSnapshot !== originalSettingsSnapshot; + setLocalIsDirty(dirty); + setIsDirty(dirty); + }, [settings, originalSettingsSnapshot, loading, setIsDirty]); + + // Clean up dirty state on unmount + useEffect(() => { + return () => { + setIsDirty(false); + }; + }, [setIsDirty]); + + const handleDiscard = useCallback(() => { + if (originalSettingsSnapshot) { + try { + const original = JSON.parse(originalSettingsSnapshot); + setSettings(original); + setLocalIsDirty(false); + setIsDirty(false); + } catch (e) { + console.error('Failed to parse original settings:', e); + } + } + }, [originalSettingsSnapshot, setSettings, setIsDirty]); + // Override loading state when login is disabled const actualLoading = loginEnabled ? loading : false; + // Show the server setting when loaded (for admin config), otherwise show user's preference + // Note: User's preference in localStorage is separate and takes precedence in the app via useLogoVariant hook + const logoStyleValue = loginEnabled + ? (settings.ui?.logoStyle ?? preferences.logoVariant ?? 'classic') + : (preferences.logoVariant ?? 'classic'); + + const handleLogoStyleChange = (value: string) => { + const nextValue = value === 'modern' ? 'modern' : 'classic'; + + // Only update local settings state - don't update the actual preference until save + // When login is disabled, update preference immediately since there's no server to save to + if (!loginEnabled) { + updatePreference('logoVariant', nextValue); + return; + } + + setSettings({ + ...settings, + ui: { + ...settings.ui, + logoStyle: nextValue, + } + }); + }; + const handleSave = async () => { // Block save if login is disabled if (!validateLoginEnabled()) { @@ -160,6 +239,16 @@ export default function AdminGeneralSection() { try { await saveSettings(); + + // Update local preference after successful save so the app reflects the saved logo style + if (settings.ui?.logoStyle) { + updatePreference('logoVariant', settings.ui.logoStyle); + } + + // Update snapshot to current settings after successful save + setOriginalSettingsSnapshot(JSON.stringify(settings)); + setLocalIsDirty(false); + markClean(); showRestartModal(); } catch (_error) { alert({ @@ -179,8 +268,9 @@ export default function AdminGeneralSection() { } return ( - - +
+ +
{t('admin.settings.general.title', 'System Settings')} @@ -221,15 +311,15 @@ export default function AdminGeneralSection() { {t('admin.settings.general.logoStyle.description', 'Choose between the modern minimalist logo or the classic S icon')} setSettings({ ...settings, ui: { ...settings.ui, logoStyle: value as 'modern' | 'classic' } })} + value={logoStyleValue} + onChange={handleLogoStyleChange} data={[ { value: 'classic', label: (
Classic logo @@ -242,7 +332,7 @@ export default function AdminGeneralSection() { label: (
Modern logo @@ -251,7 +341,6 @@ export default function AdminGeneralSection() { ) }, ]} - disabled={!loginEnabled} />
@@ -586,12 +675,26 @@ export default function AdminGeneralSection() { - {/* Save Button */} - - - + + + {/* Sticky Save Footer - only shows when there are changes */} + {isDirty && loginEnabled && ( +
+ + + {t('admin.settings.unsavedChanges.hint', 'You have unsaved changes')} + + + + + + +
+ )} {/* Restart Confirmation Modal */} - +
); } diff --git a/frontend/src/proprietary/components/shared/loginSlides.ts b/frontend/src/proprietary/components/shared/loginSlides.ts index 167b2e6c5..51c507237 100644 --- a/frontend/src/proprietary/components/shared/loginSlides.ts +++ b/frontend/src/proprietary/components/shared/loginSlides.ts @@ -1,43 +1,60 @@ import { BASE_PATH } from '@app/constants/app'; +import { getLogoFolder } from '@app/constants/logo'; +import type { LogoVariant } from '@app/services/preferencesService'; +import type { TFunction } from 'i18next'; export type LoginCarouselSlide = { - src: string - alt?: string - title?: string - subtitle?: string - cornerModelUrl?: string - followMouseTilt?: boolean - tiltMaxDeg?: number -} + src: string; + alt?: string; + title?: string; + subtitle?: string; + cornerModelUrl?: string; + followMouseTilt?: boolean; + tiltMaxDeg?: number; +}; -export const loginSlides: LoginCarouselSlide[] = [ - { - src: `${BASE_PATH}/Login/Firstpage.png`, - alt: 'Stirling PDF overview', - title: 'Your one-stop-shop for all your PDF needs.', - subtitle: - 'A privacy-first cloud suite for PDFs that lets you convert, sign, redact, and manage documents, along with 50+ other powerful tools.', - followMouseTilt: true, - tiltMaxDeg: 5, - }, - { - src: `${BASE_PATH}/Login/AddToPDF.png`, - alt: 'Edit PDFs', - title: 'Edit PDFs to display/secure the information you want', - subtitle: - 'With over a dozen tools to help you redact, sign, read and manipulate PDFs, you will be sure to find what you are looking for.', - followMouseTilt: true, - tiltMaxDeg: 5, - }, - { - src: `${BASE_PATH}/Login/SecurePDF.png`, - alt: 'Secure PDFs', - title: 'Protect sensitive information in your PDFs', - subtitle: - 'Add passwords, redact content, and manage certificates with ease.', - followMouseTilt: true, - tiltMaxDeg: 5, - }, -]; +export const buildLoginSlides = ( + variant: LogoVariant | null | undefined, + t: TFunction +): LoginCarouselSlide[] => { + const folder = getLogoFolder(variant); + const heroImage = `${BASE_PATH}/${folder}/Firstpage.png`; -export default loginSlides; + return [ + { + src: heroImage, + alt: t('login.slides.overview.alt', 'Stirling PDF overview'), + title: t('login.slides.overview.title', 'Your one-stop-shop for all your PDF needs.'), + subtitle: t( + 'login.slides.overview.subtitle', + 'A privacy-first cloud suite for PDFs that lets you convert, sign, redact, and manage documents, along with 50+ other powerful tools.' + ), + followMouseTilt: true, + tiltMaxDeg: 5, + }, + { + src: `${BASE_PATH}/Login/AddToPDF.png`, + alt: t('login.slides.edit.alt', 'Edit PDFs'), + title: t('login.slides.edit.title', 'Edit PDFs to display/secure the information you want'), + subtitle: t( + 'login.slides.edit.subtitle', + 'With over a dozen tools to help you redact, sign, read and manipulate PDFs, you will be sure to find what you are looking for.' + ), + followMouseTilt: true, + tiltMaxDeg: 5, + }, + { + src: `${BASE_PATH}/Login/SecurePDF.png`, + alt: t('login.slides.secure.alt', 'Secure PDFs'), + title: t('login.slides.secure.title', 'Protect sensitive information in your PDFs'), + subtitle: t( + 'login.slides.secure.subtitle', + 'Add passwords, redact content, and manage certificates with ease.' + ), + followMouseTilt: true, + tiltMaxDeg: 5, + }, + ]; +}; + +export default buildLoginSlides; diff --git a/frontend/src/proprietary/routes/Login.test.tsx b/frontend/src/proprietary/routes/Login.test.tsx index e279212e5..16a6cb5e6 100644 --- a/frontend/src/proprietary/routes/Login.test.tsx +++ b/frontend/src/proprietary/routes/Login.test.tsx @@ -6,6 +6,7 @@ import { MantineProvider } from '@mantine/core'; import Login from '@app/routes/Login'; import { useAuth } from '@app/auth/UseSession'; import { springAuth } from '@app/auth/springAuthClient'; +import { PreferencesProvider } from '@app/contexts/PreferencesContext'; // Mock i18n to return fallback text vi.mock('react-i18next', () => ({ @@ -49,7 +50,9 @@ vi.mock('react-router-dom', async () => { // Test wrapper with MantineProvider const TestWrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + + {children} + ); describe('Login', () => { diff --git a/frontend/src/proprietary/routes/authShared/AuthLayout.tsx b/frontend/src/proprietary/routes/authShared/AuthLayout.tsx index ba7df3795..20ca26d9a 100644 --- a/frontend/src/proprietary/routes/authShared/AuthLayout.tsx +++ b/frontend/src/proprietary/routes/authShared/AuthLayout.tsx @@ -1,15 +1,20 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import LoginRightCarousel from '@app/components/shared/LoginRightCarousel'; -import loginSlides from '@app/components/shared/loginSlides'; +import buildLoginSlides from '@app/components/shared/loginSlides'; import styles from '@app/routes/authShared/AuthLayout.module.css'; +import { useLogoVariant } from '@app/hooks/useLogoVariant'; interface AuthLayoutProps { children: React.ReactNode } export default function AuthLayout({ children }: AuthLayoutProps) { + const { t } = useTranslation(); const cardRef = useRef(null); const [hideRightPanel, setHideRightPanel] = useState(false); + const logoVariant = useLogoVariant(); + const imageSlides = useMemo(() => buildLoginSlides(logoVariant, t), [logoVariant, t]); // Force light mode on auth pages useEffect(() => { @@ -60,7 +65,7 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
{!hideRightPanel && ( - + )}
diff --git a/frontend/src/proprietary/routes/login/LoginHeader.tsx b/frontend/src/proprietary/routes/login/LoginHeader.tsx index a6317976d..01b1a2074 100644 --- a/frontend/src/proprietary/routes/login/LoginHeader.tsx +++ b/frontend/src/proprietary/routes/login/LoginHeader.tsx @@ -1,5 +1,5 @@ -import { BASE_PATH } from '@app/constants/app'; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; interface LoginHeaderProps { title: string @@ -7,10 +7,12 @@ interface LoginHeaderProps { } export default function LoginHeader({ title, subtitle }: LoginHeaderProps) { + const { wordmark } = useLogoAssets(); + return (
- Stirling PDF + Stirling PDF

{title}

{subtitle && (