UI/allow logo selection (#4982)

# Description of Changes

- Allow switching between logos in-app using the same section in
settings

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
EthanHealy01 2025-11-25 15:22:14 +00:00 committed by GitHub
parent ae5b1a4b02
commit a6614e1bfb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
110 changed files with 651 additions and 189 deletions

View File

@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8" />
<base href="%BASE_URL%" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/modern-logo/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="The Free Adobe Acrobat alternative (10M+ Downloads)"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="apple-touch-icon" href="/modern-logo/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Stirling PDF</title>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 211 KiB

View File

@ -1,19 +0,0 @@
<svg width="110" height="110" viewBox="0 0 110 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3814_84404)">
<rect x="3" y="3" width="100" height="100" rx="22.6562" fill="#8E3131"/>
</g>
<path d="M18.3374 52.9353L63.6932 15.5L63.6932 50.4604L18.3374 87.8957L18.3374 52.9353Z" fill="white"/>
<path d="M42.247 55.4981L87.6566 18.0184L87.6566 53.0203L42.247 90.5L42.247 55.4981Z" fill="white" fill-opacity="0.6"/>
<defs>
<filter id="filter0_d_3814_84404" x="4.76837e-07" y="4.76837e-07" width="110" height="110" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="2" dy="2"/>
<feGaussianBlur stdDeviation="2.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3814_84404"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3814_84404" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,28 +0,0 @@
<svg width="99" height="108" viewBox="0 0 99 108" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_3814_84410)">
<path d="M70.0628 0.142909L13.7675 11.4215C8.05881 12.5613 4 17.1507 4 22.46V87.4616C4 92.081 7.79482 95.9505 12.8106 96.4905L42.3442 100L68.3543 76.4604L94.7457 51.8862L94.2837 14.691C94.2177 10.1616 89.5979 6.80205 84.7471 7.76192L78.2134 9.05176L78.2464 6.23212C78.3124 2.27263 74.2866 -0.696983 70.0628 0.142909Z" fill="#C02223"/>
<path d="M78.2812 9.05175V68.0241L94.8135 51.8562L94.3515 13.4312C94.3185 9.53168 90.3257 6.65206 86.1349 7.49195L78.2812 9.05175Z" fill="#8B2229"/>
<path d="M42.3516 99.9997L86.5559 89.5244L94.8153 51.8553L42.3516 99.9997Z" fill="url(#paint0_linear_3814_84410)" fill-opacity="0.5"/>
<path d="M55.5058 41.5075C55.5058 41.5075 50.4571 35.5383 46.7282 33.9185C44.3523 32.8986 40.8215 32.5087 38.0496 33.7685C33.9578 35.6283 33.5289 40.8476 37.2907 42.9173C38.1816 43.4272 39.3696 43.8472 40.9205 44.1771C49.2361 45.9169 62.9635 49.5165 61.2805 64.0046C61.2805 64.0046 60.3236 77.5928 44.5833 79.1526C41.8115 79.4226 39.0066 79.2426 36.3007 78.6727C32.5719 77.9228 25.9722 76.393 22.7384 74.5632L22.1444 59.0552H23.7283C23.7283 59.0552 26.8632 65.9543 35.7728 69.2839C37.4227 69.8838 39.2376 70.1238 40.9865 69.8838C43.1974 69.5839 45.8043 68.504 46.8602 65.2344C46.8602 65.2344 48.6421 60.465 39.4026 57.8554C32.0109 55.7557 25.0483 53.5959 22.1774 46.5168C20.8245 43.1573 20.5935 39.4378 21.5174 35.8682C22.7384 31.2188 26.2362 24.6797 36.1357 21.86C36.1357 21.86 49.1371 18.6504 58.0467 22.9999L57.6507 41.5075H55.5058Z" fill="url(#paint1_linear_3814_84410)"/>
</g>
<defs>
<filter id="filter0_d_3814_84410" x="0" y="0" width="98.8125" height="108" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3814_84410"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3814_84410" result="shape"/>
</filter>
<linearGradient id="paint0_linear_3814_84410" x1="65.4105" y1="100.093" x2="68.5651" y2="71.0126" gradientUnits="userSpaceOnUse">
<stop stop-color="#DCF1F3"/>
<stop offset="1" stop-color="#C2C2C9"/>
</linearGradient>
<linearGradient id="paint1_linear_3814_84410" x1="38.5393" y1="85.0187" x2="43.5426" y2="7.50144" gradientUnits="userSpaceOnUse">
<stop stop-color="#DCF1F3"/>
<stop offset="1" stop-color="#C2C2C9"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,4 +0,0 @@
<svg width="57" height="52" viewBox="0 0 57 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.07814 26.5002L34.9865 -1.74377e-05L34.9865 23.5005L4.07809 50.0008L4.07814 26.5002Z" fill="#ACACAC" fill-opacity="0.8"/>
<path d="M20.8792 27.6687L52.0781 1.50155L52.0781 25.8343L20.8793 52.0015L20.8792 27.6687Z" fill="#FC9999" fill-opacity="0.9"/>
</svg>

Before

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><defs id="defs173"><linearGradient id="XMLID_5_" x1="304.496" x2="316.036" y1="422.91" y2="326.263" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#dcf1f3" id="stop156"/><stop offset="1" style="stop-color:#c2c2c9" id="stop158"/></linearGradient></defs><style id="style150" type="text/css">.st1{fill:#c02223}.st2{fill:#882425}.st3{fill:url(#XMLID_5_)}.st4{fill:url(#XMLID_7_)}</style><g id="XMLID_4_"><path id="XMLID_131_" d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z" class="st1" style="stroke-width:1.45391"/><path id="XMLID_117_" d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z" class="st2" style="stroke-width:1.45391"/><polygon id="XMLID_18_" points="234.7 422.6 368.5 387.7 393.5 262.2" class="st3" style="fill:url(#XMLID_5_)" transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)"/><linearGradient id="XMLID_7_" x1="223.084" x2="241.417" y1="372.756" y2="114.557" gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#dcf1f3" id="stop163"/><stop offset="1" style="stop-color:#c2c2c9" id="stop165"/></linearGradient><path id="XMLID_6_" d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z" class="st4" style="fill:url(#XMLID_7_);stroke-width:1.45391"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -1,12 +1,11 @@
<svg width="82" height="92" viewBox="0 0 82 92" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dddd_3933_98714)">
<path d="M56.3678 0.557543L9.07973 10.0315C4.2844 10.989 0.875 14.8441 0.875 19.3039V73.9053C0.875 77.7856 4.06265 81.0359 8.27589 81.4895L33.0841 84.4375L54.9327 64.6642L77.1014 44.0219L76.7133 12.778C76.6579 8.97326 72.7773 6.15122 68.7027 6.95752L63.2143 8.04098L63.2421 5.67248C63.2975 2.34651 59.9158 -0.147965 56.3678 0.557543Z" fill="white"/>
<path d="M63.269 8.04043V57.5772L77.1561 43.9962L76.7681 11.7192C76.7403 8.44358 73.3864 6.02469 69.8661 6.7302L63.269 8.04043Z" fill="#D3D3D3"/>
<path d="M33.0874 84.4375L70.2191 75.6383L77.157 43.9963L33.0874 84.4375Z" fill="#D0DBDC"/>
<path d="M44.1432 35.3049C44.1432 35.3049 39.9022 30.2907 36.77 28.9301C34.7743 28.0734 31.8084 27.7459 29.48 28.8041C26.0429 30.3663 25.6826 34.7506 28.8425 36.4891C29.5909 36.9175 30.5888 37.2702 31.8915 37.5474C38.8767 39.0088 50.4076 42.0324 48.994 54.2024C48.994 54.2024 48.1901 65.6166 34.9683 66.9268C32.6399 67.1536 30.2839 67.0024 28.0109 66.5236C24.8787 65.8937 19.335 64.6087 16.6185 63.0717L16.1196 50.045H17.4501C17.4501 50.045 20.0834 55.8402 27.5674 58.6371C28.9534 59.141 30.4779 59.3426 31.947 59.141C33.8041 58.889 35.9939 57.982 36.8809 55.2355C36.8809 55.2355 38.3777 51.2292 30.6165 49.0371C24.4075 47.2733 18.5589 45.4592 16.1473 39.5127C15.0109 36.6907 14.8168 33.5663 15.593 30.5679C16.6185 26.6624 19.5567 21.1695 27.8723 18.801C27.8723 18.801 38.7935 16.105 46.2776 19.7585L45.9449 35.3049H44.1432Z" fill="#171A1F"/>
<path d="M56.3682 0.557473C59.916 -0.147829 63.2976 2.34683 63.2422 5.67271L63.2139 8.04087L68.7031 6.95689C72.7776 6.15095 76.6575 8.97364 76.7129 12.7782L77.1016 44.0213L54.9326 64.6639L33.084 84.4374L8.27637 81.4891C4.06313 81.0356 0.875008 77.7854 0.875 73.9051V19.3036C0.875108 14.8438 4.28482 10.9886 9.08008 10.0311L56.3682 0.557473ZM46.2773 19.7586C38.7933 16.1052 27.8721 18.8006 27.8721 18.8006C19.5566 21.1692 16.6184 26.6627 15.5928 30.5682C14.8168 33.5665 15.0111 36.6907 16.1475 39.5126C18.559 45.4589 24.4073 47.2732 30.6162 49.037C38.3528 51.2221 36.8902 55.2099 36.8809 55.2352C35.9939 57.9815 33.8044 58.8885 31.9473 59.1405C30.4782 59.3421 28.9533 59.1405 27.5674 58.6366C20.0835 55.8397 17.4502 50.0448 17.4502 50.0448H16.1191L16.6182 63.0711C19.3345 64.6081 24.8785 65.8934 28.0107 66.5233C30.2837 67.002 32.6404 67.1534 34.9688 66.9266C48.1902 65.6162 48.9941 54.202 48.9941 54.202C50.4076 42.0323 38.8767 39.0091 31.8916 37.5477C30.589 37.2706 29.5911 36.9174 28.8428 36.4891C25.6828 34.7505 26.0434 30.3658 29.4805 28.8036C31.8088 27.7456 34.7749 28.0729 36.7705 28.9295C39.9027 30.2903 44.1436 35.3045 44.1436 35.3045H45.9453L46.2773 19.7586Z" fill="white"/>
<path d="M63.2779 8.03641L63.2781 57.7846L77.1651 44.2035L76.7771 11.9265C76.7494 8.65091 73.3953 6.02067 69.875 6.72618L63.2779 8.03641Z" fill="#D3D3D3"/>
</g>
<defs>
<filter id="filter0_dddd_3933_98714" x="-8.34465e-07" y="-4.17233e-07" width="81.0945" height="91.875" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter0_dddd_3933_98714" x="-8.34465e-07" y="-4.17233e-07" width="81.1025" height="91.875" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.4375"/>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,5 +1,5 @@
<svg width="77" height="84" viewBox="0 0 77 84" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.2124 83.9998L69.3441 75.2006L76.282 43.5586L32.2124 83.9998Z" fill="#D0DBDC"/>
<path d="M55.4932 0.119997C59.0409 -0.585291 62.4223 1.90861 62.3672 5.23426L62.3389 7.6034L67.8281 6.51941C71.9024 5.71349 75.7821 8.53542 75.8379 12.3397L76.2266 43.5839L54.0576 64.2264L32.209 83.9999L7.40137 81.0516C3.18818 80.5981 8.70906e-05 77.3479 0 73.4677V18.8661C3.4631e-05 14.4063 3.40977 10.5511 8.20508 9.59363L55.4932 0.119997ZM45.4023 19.3212C37.9183 15.6676 26.9971 18.3632 26.9971 18.3632C18.6818 20.7316 15.7434 26.2243 14.7178 30.1298C13.9417 33.1282 14.136 36.253 15.2725 39.0751C17.684 45.0213 23.5324 46.8357 29.7412 48.5995C37.5024 50.7916 36.0059 54.7977 36.0059 54.7977C35.1189 57.544 32.9294 58.451 31.0723 58.703C29.6032 58.9046 28.0783 58.703 26.6924 58.1991C19.2266 55.4091 16.588 49.6354 16.5752 49.6073H15.2441L15.7432 62.6337C18.4595 64.1706 24.0035 65.4559 27.1357 66.0858C29.4087 66.5646 31.7654 66.7159 34.0938 66.4891C47.3152 65.1787 48.1191 53.7645 48.1191 53.7645C49.5327 41.5946 38.0017 38.5707 31.0166 37.1093C29.714 36.8321 28.7161 36.4799 27.9678 36.0516C24.8078 34.3131 25.1683 29.9283 28.6055 28.3661C30.9338 27.3081 33.8999 27.6354 35.8955 28.4921C39.0277 29.8529 43.2686 34.8671 43.2686 34.8671H45.0703L45.4023 19.3212Z" fill="white"/>
<path d="M62.394 7.60275V57.1395L76.2811 43.5585L75.8931 11.2815C75.8653 8.0059 72.5114 5.58701 68.9911 6.29252L62.394 7.60275Z" fill="#EEEEEE"/>
<path d="M32.2124 83.9997L69.3441 75.2005L76.282 43.5585L32.2124 83.9997Z" fill="#D0DBDC"/>
<path d="M55.4932 0.119995C59.0409 -0.585293 62.4223 1.9086 62.3672 5.23425L62.3389 7.60339L67.8281 6.51941C71.9024 5.71349 75.7821 8.53542 75.8379 12.3397L76.2266 43.5839L54.0576 64.2264L32.209 83.9999L7.40137 81.0516C3.18818 80.5981 8.70906e-05 77.3479 0 73.4677V18.8661C3.4631e-05 14.4063 3.40977 10.5511 8.20508 9.59363L55.4932 0.119995ZM45.4023 19.3212C37.9183 15.6676 26.9971 18.3632 26.9971 18.3632C18.6818 20.7316 15.7434 26.2243 14.7178 30.1298C13.9417 33.1282 14.136 36.253 15.2725 39.0751C17.684 45.0213 23.5324 46.8357 29.7412 48.5995C37.5024 50.7916 36.0059 54.7977 36.0059 54.7977C35.1189 57.544 32.9294 58.451 31.0723 58.703C29.6032 58.9046 28.0783 58.703 26.6924 58.1991C19.2266 55.4091 16.588 49.6354 16.5752 49.6073H15.2441L15.7432 62.6337C18.4595 64.1706 24.0035 65.4559 27.1357 66.0858C29.4087 66.5646 31.7654 66.7159 34.0938 66.4891C47.3152 65.1787 48.1191 53.7645 48.1191 53.7645C49.5327 41.5946 38.0017 38.5707 31.0166 37.1093C29.714 36.8321 28.7161 36.4799 27.9678 36.0516C24.8078 34.3131 25.1684 29.9283 28.6055 28.3661C30.9338 27.3081 33.8999 27.6354 35.8955 28.4921C39.0277 29.8529 43.2686 34.8671 43.2686 34.8671H45.0703L45.4023 19.3212Z" fill="white"/>
<path d="M62.394 7.60263V57.1394L76.2811 43.5584L75.8931 11.2814C75.8653 8.00577 72.5114 5.58689 68.9911 6.2924L62.394 7.60263Z" fill="#EEEEEE"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -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"
},

View File

@ -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"
}

View File

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,4 @@
<svg width="92" height="100" viewBox="0 0 92 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 50L60 0V46.5L0 96.5V50Z" fill="#E6E6E6" fill-opacity="0.4"/>
<path d="M32 53L92 3V49.5L32 99.5V53Z" fill="#E6E6E6" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@ -0,0 +1,4 @@
<svg width="92" height="100" viewBox="0 0 92 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 50L60 0V46.5L0 96.5V50Z" fill="#ACACAC" fill-opacity="0.3"/>
<path d="M32 53L92 3V49.5L32 99.5V53Z" fill="#FC9999" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192" fill="none">
<path d="M7.26375 95.8344L123.374 4.32822e-05L123.375 89.4987L7.26375 185.333L7.26375 95.8344Z" fill="white"/>
<path d="M68.4794 102.395L184.728 6.44717L184.728 96.052L68.4794 192L68.4794 102.395Z" fill="white" fill-opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -10,15 +10,15 @@
<!-- Page 1: Hero / Cover Page -->
<div class="page page-1">
<div class="decorative-shapes">
<img src="../../public/branding/StirlingPDFLogoNoTextLight.svg" class="shape shape-1" alt="">
<img src="../../public/branding/StirlingPDFLogoNoTextDark.svg" class="shape shape-2" alt="">
<img src="../../public/branding/StirlingPDFLogoNoTextLight.svg" class="shape shape-3" alt="">
<img src="../../public/branding/StirlingPDFLogoNoTextDark.svg" class="shape shape-4" alt="">
<img src="../../public/branding/StirlingPDFLogoNoTextLight.svg" class="shape shape-5" alt="">
<img src="../../public/modern-logo/StirlingPDFLogoNoTextLight.svg" class="shape shape-1" alt="">
<img src="../../public/modern-logo/StirlingPDFLogoNoTextDark.svg" class="shape shape-2" alt="">
<img src="../../public/modern-logo/StirlingPDFLogoNoTextLight.svg" class="shape shape-3" alt="">
<img src="../../public/modern-logo/StirlingPDFLogoNoTextDark.svg" class="shape shape-4" alt="">
<img src="../../public/modern-logo/StirlingPDFLogoNoTextLight.svg" class="shape shape-5" alt="">
</div>
<div class="hero-content">
<div class="logo-container">
<img src="../../public/branding/StirlingPDFLogoWhiteText.svg" alt="Stirling PDF" class="hero-logo">
<img src="../../public/modern-logo/StirlingPDFLogoWhiteText.svg" alt="Stirling PDF" class="hero-logo">
</div>
<h1 class="hero-tagline">The Free Adobe Acrobat Alternative</h1>
<div class="hero-stats">

View File

@ -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<HTMLLinkElement>(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<AppConfigProviderProps, 'children' | 'retryOptions'>;
@ -62,6 +87,7 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
<ScarfTrackingInitializer />
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<AppInitializer />
<BrandingAssetManager />
<ToolRegistryProvider>
<NavigationProvider>
<FilesModalProvider>

View File

@ -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 */}
<Group gap="xs" align="center">
<img
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
src={colorScheme === 'dark' ? wordmark.white : wordmark.grey}
alt="Stirling PDF"
style={{ height: '2.2rem', width: 'auto' }}
/>

View File

@ -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 */}
<Group gap="xs" align="center">
<img
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
src={colorScheme === 'dark' ? wordmark.white : wordmark.grey}
alt="Stirling PDF"
style={{ height: '2.2rem', width: 'auto' }}
/>

View File

@ -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;
}
}

View File

@ -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<AppConfigModalProps> = ({ opened, onClose }) => {
const AppConfigModalInner: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
const [active, setActive] = useState<NavKey>('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<AppConfigModalProps> = ({ 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 (
<Modal
@ -148,11 +161,7 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
const navItemContent = (
<div
key={item.key}
onClick={() => {
// 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<AppConfigModalProps> = ({ opened, onClose }) => {
);
};
// Wrapper component that provides the UnsavedChangesContext
const AppConfigModal: React.FC<AppConfigModalProps> = (props) => {
return (
<UnsavedChangesProvider>
<AppConfigModalInner {...props} />
</UnsavedChangesProvider>
);
};
export default AppConfigModal;

View File

@ -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<boolean>(false);
const terminology = useFileActionTerminology();
@ -87,24 +90,25 @@ const LandingPage = () => {
},
}}
>
<div
style={{
position: 'absolute',
top: 0,
right: 0,
zIndex: 10,
}}
>
<img
src={logoPath}
alt="Stirling PDF Logo"
{logoVariant === 'modern' && (
<div
style={{
height: 'auto',
pointerEvents: 'none',
position: 'absolute',
top: 0,
right: 0,
zIndex: 10,
}}
/>
</div>
>
<img
src={logoPath}
alt="Stirling PDF Logo"
style={{
height: 'auto',
pointerEvents: 'none',
}}
/>
</div>
)}
<div
className={`min-h-[45vh] flex flex-col items-center justify-center px-8 py-8 w-full min-w-[30rem] max-w-[calc(100%-2rem)] border transition-all duration-200 dropzone-inner relative`}
style={{
@ -123,7 +127,7 @@ const LandingPage = () => {
{/* Stirling PDF Branding */}
<Group gap="xs" align="center">
<img
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
src={colorScheme === 'dark' ? wordmark.white : wordmark.grey}
alt="Stirling PDF"
style={{ height: '2.2rem', width: 'auto' }}
/>

View File

@ -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<TooltipProps> = ({
}) => {
const [internalOpen, setInternalOpen] = useState(false);
const [isPinned, setIsPinned] = useState(false);
const { tooltipLogo } = useLogoAssets();
const triggerRef = useRef<HTMLElement | null>(null);
const tooltipRef = useRef<HTMLDivElement | null>(null);
@ -352,7 +353,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
<div className={styles['tooltip-logo']}>
{header.logo || (
<img
src={`${BASE_PATH}/branding/StirlingPDFLogoNoTextDark.svg`}
src={tooltipLogo}
alt="Stirling PDF"
style={{ width: '1.4rem', height: '1.4rem', display: 'block' }}
/>

View File

@ -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;

View File

@ -0,0 +1,15 @@
import type { LogoVariant } from '@app/services/preferencesService';
export const LOGO_FOLDER_BY_VARIANT: Record<LogoVariant, string> = {
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)];
};

View File

@ -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<boolean>;
/**
* Reset dirty state (call after successful save)
*/
markClean: () => void;
}
const UnsavedChangesContext = createContext<UnsavedChangesContextType | undefined>(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<boolean> => {
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 (
<UnsavedChangesContext.Provider value={{ isDirty, setIsDirty, confirmIfDirty, markClean }}>
{children}
<Modal
opened={modalOpen}
onClose={handleCancel}
title={t('admin.settings.unsavedChanges.title', 'Unsaved Changes')}
centered
size="sm"
zIndex={1500}
>
<Stack gap="md">
<Text size="sm">
{t('admin.settings.unsavedChanges.message', 'You have unsaved changes. Do you want to discard them?')}
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={handleCancel}>
{t('admin.settings.unsavedChanges.cancel', 'Keep Editing')}
</Button>
<Button color="red" onClick={handleDiscard}>
{t('admin.settings.unsavedChanges.discard', 'Discard Changes')}
</Button>
</Group>
</Stack>
</Modal>
</UnsavedChangesContext.Provider>
);
}
export function useUnsavedChanges(): UnsavedChangesContextType {
const context = useContext(UnsavedChangesContext);
if (!context) {
throw new Error('useUnsavedChanges must be used within an UnsavedChangesProvider');
}
return context;
}

View File

@ -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);
});
});
});

View File

@ -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]);
}

Some files were not shown because too many files have changed in this diff Show More