Update branding

Update the Ferese symbols, appearance-specific wallpapers, and rounded wallpaper and theme previews. Bundle Figtree and add the Ferese name to the website header.

Verified configuration, theme, Settings, wallpaper and installer tests, both UI renderers, the static site build, and browser layouts.
This commit is contained in:
Samuel Onoja committed 2026-10-04 11:11:14 +01:00
1 parent f81d8d50ea
commit 3533fac521
35 files changed
+1212 -309

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ on:
paths: paths:
- "site/**" - "site/**"
- "docs/**" - "docs/**"
- "packaging/icons/ferese.svg" - "assets/branding/**"
- "scripts/build-site.py" - "scripts/build-site.py"
- ".github/workflows/pages.yml" - ".github/workflows/pages.yml"
workflow_dispatch: workflow_dispatch:
+5 -1
View File
@@ -1,5 +1,9 @@
<p align="center"> <p align="center">
<img src="docs/images/ferese-lockup.svg" width="300" alt="Ferese"> <picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/branding/ferese-blue-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="assets/branding/ferese-blue-light.svg">
<img src="assets/branding/ferese-blue-flat.svg" width="160" height="160" alt="Ferese">
</picture>
</p> </p>
<p align="center"><strong>A configurable Wayland desktop</strong></p> <p align="center"><strong>A configurable Wayland desktop</strong></p>
+19
View File
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-labelledby="t" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToyMDBiNDZhNS00MzBjLTQ0YzYtOWEyNi00MTJjOTY0ZDZlNTUAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaAU3ROHwJLvccKfifOtisiMAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDphYzgzM2JmYS0xNWUxLTQyYjUtYWE0Yy03ZTA4MjIyOTkzN2RscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoKnwqCDusoXBZopMLg7kk1QAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggWrQV4c0Z2wXDwLizvmxmPGGcoXngs0I7I9FDRtxHWrCkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaKsn7yKWhg+JwUsLE4l1HHMAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCBdct3O/g97e2fKBP+gUsy4YgkpnwHV1CeZWnYPU69p0mRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi5Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjIwMGI0NmE1LTQzMGMtNDRjNi05YTI2LTQxMmM5NjRkNmU1NS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjlkY2FjOTYwLWEzMWUtNDNlMS04YTMxLWJlNTYwN2UxYTI4NHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBatBXhzRnbBcPAuLO+bGY8YZyheeCzQjsj0UNG3EdasKJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggxheLqIxsdl4BSsN5iCOfu7njZksjyK9LTM3YVyy3h0uiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggzKu+bf43egViKIdS5Po/09V4cwbp08Z7caz0fE+Woc10Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
<title id="t">Ferese Blue glow (dark)</title>
<defs>
<linearGradient id="gl" gradientUnits="userSpaceOnUse" x1="174" y1="741" x2="728" y2="389"><stop offset="0" stop-color="#8FC2FF"/><stop offset="0.5" stop-color="#4D95FF"/><stop offset="1" stop-color="#2F7BFF"/></linearGradient>
<linearGradient id="gr" gradientUnits="userSpaceOnUse" x1="356" y1="857" x2="1073" y2="410"><stop offset="0" stop-color="#2F7BFF"/><stop offset="0.55" stop-color="#2A64F5"/><stop offset="1" stop-color="#2149E0"/></linearGradient>
<linearGradient id="edge" gradientUnits="userSpaceOnUse" x1="200" y1="900" x2="1100" y2="380">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.75"/><stop offset="1" stop-color="#CFE4FF" stop-opacity="0.3375"/></linearGradient>
<filter id="drop" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="16"/><feOffset dy="22"/></filter>
<filter id="blur0" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="46"/></filter><filter id="blur1" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="20"/></filter><filter id="blur2" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="8"/></filter>
</defs>
<g transform="translate(-54 -58) scale(0.89225)">
<g filter="url(#drop)" opacity="0.45"><path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="#020612"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="#020612"/></g>
<g filter="url(#blur0)" opacity="0.55"><path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="#2F7BFF"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="#2F7BFF"/></g>
<g filter="url(#blur1)" opacity="0.55"><path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="#4D95FF"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="#4D95FF"/></g>
<g filter="url(#blur2)" opacity="0.45"><path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="#9CCBFF"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="#9CCBFF"/></g>
<path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="url(#gl)"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="url(#gr)"/>
<path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="none" stroke="url(#edge)" stroke-width="2.4" stroke-linejoin="round"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="none" stroke="url(#edge)" stroke-width="2.4" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-labelledby="t" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo5MjZiMGZjMC03NzgzLTQyMjEtYjQ4ZC1lNmNiYzNjMDViZDIAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaIwcQOifgdYBkQq+CZ9YANQAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDo4ODExMjQ0Yy04YjA3LTQ2NzUtODQzMi0zY2NhYzQ0ODViZDFscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo3XpLe6BUgGn6u/EXqBpahwAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggTfOC90ZuSUsMb3m0SS6AHpToOt8ZIwG7pY6E8uPho66kZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaL6emUOsKdDfhUli6KFBL3wAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCCHORF8I/qCVx2OCPdNJg7IEQl+IocVqCMSuKCixYOpVmRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi5Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjkyNmIwZmMwLTc3ODMtNDIyMS1iNDhkLWU2Y2JjM2MwNWJkMi9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjM2MWY1YThkLTFmNjAtNDhkNC1iMjI5LWI4OWJlYzc3OThhOHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBN84L3Rm5JSwxvebRJLoAelOg63xkjAbuljoTy4+GjrqJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggQ1jsnE3M6NjwOEibVzQLOGVizW8lSiAh24YjmMF/IACiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFgg8+STvA5M2LMsUCIt+QLX2MjHWADRyr/REUb26DN6GaV0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
<title id="t">Ferese — flat</title>
<defs>
<linearGradient id="gl" gradientUnits="userSpaceOnUse" x1="174" y1="741" x2="728" y2="389"></linearGradient>
<linearGradient id="gr" gradientUnits="userSpaceOnUse" x1="356" y1="857" x2="1073" y2="410"></linearGradient>
<linearGradient id="edge" gradientUnits="userSpaceOnUse" x1="200" y1="900" x2="1100" y2="380">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0"/><stop offset="1" stop-color="#CFE4FF" stop-opacity="0.0"/></linearGradient>
<filter id="drop" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="16"/><feOffset dy="22"/></filter>
</defs>
<g transform="translate(-54 -58) scale(0.89225)">
<path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="#2F7BFF"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="#2F7BFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-labelledby="t" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo0ZGEyNWIzZi00MWY2LTQyZmUtODYxOS1hMzYxZmExZGY5ZjUAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaMWT+erUN/0ltovTisUDl3AAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDoxMjlkNWVhYi00ODA1LTRiMGMtYjZlYy0wOGM3NWIyMWE3MDVscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNozOYTMPzLnzA8QdTfbX73ngAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggjlLsRndCO72mRss4tAAhv3x2Pp+nXIUGYhCLkBlNBjWkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaPoH+Alu2rF6Xq43POgXKXwAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCCOp0GlGbehYxHafv+VWEY+znuLp7ed4j44iRqLXVOemWRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi5Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjRkYTI1YjNmLTQxZjYtNDJmZS04NjE5LWEzNjFmYTFkZjlmNS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmM0YTIwM2VkLWNmOTMtNDE5NS05NjZlLTMxNDg2NWNlMzlmZXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCOUuxGd0I7vaZGyzi0ACG/fHY+n6dchQZiEIuQGU0GNaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFgg16LtIL9sWqVVOUn/rVs9b2vnQJUZvEVx3ijZ9N9AxqOiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggLRwRahdQ3ZbIdkViwGS993ZlrIo3AVfqeDHDQefdmvl0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
<title id="t">Ferese Blue (light)</title>
<defs>
<linearGradient id="gl" gradientUnits="userSpaceOnUse" x1="174" y1="741" x2="728" y2="389"><stop offset="0" stop-color="#4D95FF"/><stop offset="0.5" stop-color="#2F7BFF"/><stop offset="1" stop-color="#2A64F5"/></linearGradient>
<linearGradient id="gr" gradientUnits="userSpaceOnUse" x1="356" y1="857" x2="1073" y2="410"><stop offset="0" stop-color="#2A64F5"/><stop offset="0.55" stop-color="#2151E8"/><stop offset="1" stop-color="#1A3BC4"/></linearGradient>
<linearGradient id="edge" gradientUnits="userSpaceOnUse" x1="200" y1="900" x2="1100" y2="380">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.35"/><stop offset="1" stop-color="#CFE4FF" stop-opacity="0.1575"/></linearGradient>
<filter id="drop" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="16"/><feOffset dy="22"/></filter>
<filter id="blur0" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="34"/></filter><filter id="blur1" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="14"/></filter>
</defs>
<g transform="translate(-54 -58) scale(0.89225)">
<g filter="url(#blur0)" opacity="0.28"><path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="#2F7BFF"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="#2F7BFF"/></g>
<g filter="url(#blur1)" opacity="0.25"><path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="#4D95FF"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="#4D95FF"/></g>
<path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="url(#gl)"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="url(#gr)"/>
<path d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z" fill="none" stroke="url(#edge)" stroke-width="2.4" stroke-linejoin="round"/><path d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z" fill="none" stroke="url(#edge)" stroke-width="2.4" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

-62
View File
@@ -1,62 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo4MzkwZjY3Yi1lMWMzLTRkZjItYjc5Ni1kMThmOTcwMzJiOTgAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaHMKM0mGirl6EAsa915f3d8AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDplYTUzOTY3Ni03MTE4LTQzMGItYTU3Ni02MDk2Zjk0ZmY0NTdscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNomXih8e+70OF/YLHlVgL9LgAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggnypnxLNZxd05KEbslyHWdZn0du7pvfZdyFM3FXgUiimkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaEYNHg3OE8TdlNzzrWNuGkkAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCBmyCV0xk4L/Lqf9X1S9qWNTl5TCz9r70H5YR7X/R91+mRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBiaZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjgzOTBmNjdiLWUxYzMtNGRmMi1iNzk2LWQxOGY5NzAzMmI5OC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOmY4MGM0NTA5LTE1NmEtNDQxYi04ZDU4LTNjMzE5YTRiOWU4ZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCfKmfEs1nF3TkoRuyXIdZ1mfR27um99l3IUzcVeBSKKaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggBkna+FVWA7ttg4e5fN3SGCgCzCQlDA9sXSLaXIILGWiiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggaFremDqsMi+9hC8EXdAqcvUmalJPQpuJro7fci7a9hF0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
<defs>
<radialGradient id="bg" cx="50%" cy="42%" r="75%">
<stop offset="0%" stop-color="#12151c"/>
<stop offset="55%" stop-color="#0a0c11"/>
<stop offset="100%" stop-color="#050609"/>
</radialGradient>
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6ea3ff"/>
<stop offset="100%" stop-color="#3d7be6"/>
</linearGradient>
<linearGradient id="glass" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.06"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.015"/>
</linearGradient>
<filter id="soft" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="18"/>
</filter>
<filter id="softer" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="45"/>
</filter>
<filter id="markGlow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="14" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- base -->
<rect width="1920" height="1080" fill="url(#bg)"/>
<!-- distant blurred glow behind mark -->
<circle cx="960" cy="540" r="420" fill="url(#glow)" opacity="0.10" filter="url(#softer)"/>
<!-- faint background tiling grid, evoking Ferese's window layout -->
<g opacity="0.35">
<rect x="90" y="90" width="430" height="300" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.05" filter="url(#soft)"/>
<rect x="90" y="420" width="430" height="220" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.05" filter="url(#soft)"/>
<rect x="1400" y="120" width="420" height="230" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.05" filter="url(#soft)"/>
<rect x="1400" y="380" width="420" height="330" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.05" filter="url(#soft)"/>
<rect x="120" y="760" width="360" height="220" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.04" filter="url(#soft)"/>
<rect x="1440" y="800" width="360" height="180" rx="18" fill="url(#glass)" stroke="#ffffff" stroke-opacity="0.04" filter="url(#soft)"/>
</g>
<!-- centered Ferese mark, scaled from the 200x200 source -->
<g transform="translate(960,540) scale(1.9) translate(-100,-100)" filter="url(#markGlow)">
<rect x="20" y="20" width="50" height="160" rx="8" fill="#3d7be6"/>
<rect x="80" y="20" width="100" height="55" rx="8" fill="#3d7be6"/>
<rect x="80" y="95" width="70" height="45" rx="8" fill="#3d7be6"/>
</g>
<!-- subtle vignette -->
<rect width="1920" height="1080" fill="url(#bg)" opacity="0" />
<rect x="0" y="0" width="1920" height="1080" fill="black" opacity="0.12" style="mix-blend-mode:multiply"/>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

+1 -1
View File
@@ -508,7 +508,7 @@ mod tests {
fn default_wallpaper_is_bundled_and_decodable() { fn default_wallpaper_is_bundled_and_decodable() {
let config: WallpaperConfig = ferese_config::from_str("").unwrap(); let config: WallpaperConfig = ferese_config::from_str("").unwrap();
let path = config.path.unwrap(); let path = config.path.unwrap();
assert_eq!(image::image_dimensions(&path).unwrap(), (1920, 1080)); assert_eq!(image::image_dimensions(&path).unwrap(), (3840, 2160));
assert!(WallpaperState::new(WallpaperConfig::default()).owns_background()); assert!(WallpaperState::new(WallpaperConfig::default()).owns_background());
} }
} }
+17 -4
View File
@@ -38,13 +38,26 @@ pub fn config_path() -> Option<PathBuf> {
} }
pub fn default_wallpaper() -> &'static str { pub fn default_wallpaper() -> &'static str {
static PATH: std::sync::OnceLock<String> = std::sync::OnceLock::new(); default_wallpaper_for(theme::Appearance::Dark)
PATH.get_or_init(|| { }
pub fn default_wallpaper_for(appearance: theme::Appearance) -> &'static str {
static LIGHT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static DARK: std::sync::OnceLock<String> = std::sync::OnceLock::new();
let (slot, name) = match appearance {
theme::Appearance::Light => (&LIGHT, "ferese-wallpaper-light.png"),
theme::Appearance::Dark => (&DARK, "ferese-wallpaper-dark.jpg"),
};
slot.get_or_init(|| {
let path = std::env::current_exe() let path = std::env::current_exe()
.ok() .ok()
.and_then(|path| path.parent().map(|dir| dir.join("wallpapers/ferese.png"))) .and_then(|path| path.parent().map(|dir| dir.join("wallpapers").join(name)))
.filter(|path| path.is_file()) .filter(|path| path.is_file())
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png")); .unwrap_or_else(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../assets/wallpapers")
.join(name)
});
path.canonicalize().unwrap_or(path).to_string_lossy().into_owned() path.canonicalize().unwrap_or(path).to_string_lossy().into_owned()
}) })
+58
View File
@@ -575,6 +575,7 @@ pub fn preset(name: &str, appearance: Appearance) -> Result<Tokens, String> {
return Err(format!("Preset {name} does not support {appearance:?}")); return Err(format!("Preset {name} does not support {appearance:?}"));
} }
let mut tokens = default_tokens(); let mut tokens = default_tokens();
tokens.background.path = Some(crate::default_wallpaper_for(appearance).into());
tokens.colors.surface_base = preset.base.into(); tokens.colors.surface_base = preset.base.into();
tokens.colors.surface_raised = preset.raised.into(); tokens.colors.surface_raised = preset.raised.into();
tokens.colors.application_background = preset.application_background.into(); tokens.colors.application_background = preset.application_background.into();
@@ -601,6 +602,7 @@ pub fn preset(name: &str, appearance: Appearance) -> Result<Tokens, String> {
return Ok(tokens); return Ok(tokens);
} }
let mut t = default_tokens(); let mut t = default_tokens();
t.background.path = Some(crate::default_wallpaper_for(appearance).into());
let (base, text, muted, accent, border) = match name { let (base, text, muted, accent, border) = match name {
"monochrome" => ("#101012", "#EDEDF0", "#97979F", "#E5E5E5", "#FFFFFF18"), "monochrome" => ("#101012", "#EDEDF0", "#97979F", "#E5E5E5", "#FFFFFF18"),
"dracula" => ("#282A36", "#F8F8F2", "#A4ADCD", "#BD93F9", "#44475A"), "dracula" => ("#282A36", "#F8F8F2", "#A4ADCD", "#BD93F9", "#44475A"),
@@ -962,6 +964,62 @@ mod tests {
) )
} }
#[test]
fn wallpapers_follow_appearance_unless_explicitly_selected() {
for (mode, appearance) in [("light", Appearance::Light), ("dark", Appearance::Dark)] {
let default = candidate(&format!("theme {{ mode \"{mode}\"; }}")).unwrap();
assert_eq!(
default.theme.tokens.background.path.as_deref(),
Some(Path::new(crate::default_wallpaper_for(appearance)))
);
let shared = candidate(&format!(
"theme {{ mode \"{mode}\"; background {{ path \"shared.png\"; }}; }}"
))
.unwrap();
assert_eq!(
shared.theme.tokens.background.path.as_deref(),
Some(Path::new("/config/ferese/shared.png"))
);
let separate = candidate(&format!("theme {{ mode \"{mode}\"; background {{ path \"shared.png\"; }}; light {{ background {{ path \"light.png\"; }}; }}; dark {{ background {{ path \"dark.jpg\"; }}; }}; }}")).unwrap();
assert_eq!(
separate.theme.tokens.background.path.as_deref(),
Some(Path::new(&format!(
"/config/ferese/{}",
if appearance == Appearance::Light {
"light.png"
} else {
"dark.jpg"
}
)))
);
}
let auto = candidate("theme { mode \"auto\"; schedule { timezone \"UTC\"; }; }").unwrap();
assert_eq!(auto.theme.appearance, Appearance::Light);
assert_eq!(
auto.theme.tokens.background.path.as_deref(),
Some(Path::new(crate::default_wallpaper_for(Appearance::Light)))
);
}
#[test]
fn imported_theme_wallpaper_is_not_overwritten_by_bundled_defaults() {
let source = "theme { mode \"light\"; family \"custom\"; custom-themes { custom { file \"custom.kdl\"; }; }; }";
let result = resolve(
&Document::parse(source).unwrap(),
Path::new("/config/ferese"),
"2026-09-30T12:00:00Z".parse().unwrap(),
|_| Ok("light { background { path \"authored.png\"; }; }".into()),
)
.unwrap();
assert_eq!(
result.theme.tokens.background.path.as_deref(),
Some(Path::new("/config/ferese/authored.png"))
);
}
#[test] #[test]
fn automatic_gradients_follow_presets_and_preserve_border_alpha() { fn automatic_gradients_follow_presets_and_preserve_border_alpha() {
for preset in crate::presets::PRESETS { for preset in crate::presets::PRESETS {
+2 -2
View File
@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1058" height="650" viewBox="105 314 1058 650" role="img" aria-labelledby="title desc"> <svg xmlns="http://www.w3.org/2000/svg" width="1058" height="650" viewBox="105 314 1058 650" role="img" aria-labelledby="title desc">
<title id="title">Ferese orbit</title> <title id="title">Ferese</title>
<desc id="desc">Open, tilted orbit symbol. Smooth cubic Bezier vector paths; transparent background.</desc> <desc id="desc">Ferese symbol with smooth cubic Bezier paths and a transparent background.</desc>
<path id="sweep-left" fill="currentColor" d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z"/> <path id="sweep-left" fill="currentColor" d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z"/>
<path id="sweep-right" fill="currentColor" d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z"/> <path id="sweep-right" fill="currentColor" d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 890 B

After

Width:  |  Height:  |  Size: 874 B

+75 -19
View File
@@ -32,11 +32,17 @@ pub fn tile<M: Clone + 'static>(
font, font,
id, id,
} = options; } = options;
use cosmic::widget::{button, column, row}; use cosmic::widget::{button, container, row};
let mut caption = row([]) let label_color = family
.spacing(6) .palette(variant.unwrap_or(Appearance::Light))
.align_y(cosmic::iced::Alignment::Center) .or(family.dark.as_ref())
.push(crate::text(family.name.clone(), font).size(12)); .map(|palette| crate::parse_color(&palette.text).unwrap())
.unwrap_or(palette.text);
let mut caption = row([]).spacing(6).align_y(cosmic::iced::Alignment::Center).push(
crate::text(family.name.clone(), font)
.size(12)
.class(cosmic::theme::Text::Color(label_color)),
);
if family.light.is_none() || family.dark.is_none() { if family.light.is_none() || family.dark.is_none() {
caption = caption.push( caption = caption.push(
crate::text( crate::text(
@@ -48,19 +54,31 @@ pub fn tile<M: Clone + 'static>(
font, font,
) )
.size(10) .size(10)
.class(cosmic::theme::Text::Color(palette.muted)), .class(cosmic::theme::Text::Color(label_color)),
); );
} }
let image = preview(family, variant, active, selected); let family_preview = family.clone();
let image = let caption: Element<'static, M> = container(caption)
cosmic::widget::responsive(move |size| image.clone().height(Length::Fixed(size.width * 126. / 200.)).into()) .padding(8)
.height(Length::Shrink); .width(Length::Fill)
let content = column([]).spacing(2).push(image).push(caption); .height(Length::Fill)
.align_y(cosmic::iced::alignment::Vertical::Bottom)
.into();
let image = cosmic::widget::responsive(move |size| {
let height = size.width * 126. / 200.;
// Match the button's logical radius when scaling the cached SVG artwork.
let radius = 10. * 200. / size.width.max(1.);
preview_with_radius(&family_preview, variant, active, selected, radius)
.height(Length::Fixed(height))
.into()
})
.height(Length::Shrink);
let content = cosmic::iced::widget::stack([image.into(), caption]);
let tile = button::custom(content) let tile = button::custom(content)
.id(id.clone()) .id(id.clone())
.name(format!("{}, {}", family.name, family.availability())) .name(format!("{}, {}", family.name, family.availability()))
.width(Length::Fill) .width(Length::Fill)
.padding(8) .padding(0)
.class(tile_style(palette, selected)) .class(tile_style(palette, selected))
.on_press(on_select); .on_press(on_select);
radio_with_tab_stop(tile, id, selected, tab_stop, navigate) radio_with_tab_stop(tile, id, selected, tab_stop, navigate)
@@ -70,11 +88,17 @@ fn tile_style(palette: crate::Palette, selected: bool) -> cosmic::theme::Button
let style = move |focused: bool, hovered: bool| cosmic::widget::button::Style { let style = move |focused: bool, hovered: bool| cosmic::widget::button::Style {
shape: Some(BorderShape::Continuous), shape: Some(BorderShape::Continuous),
outline: None, outline: None,
background: Some(if hovered { palette.sidebar } else { palette.card }.into()), background: None,
border_width: if selected { 2. } else { 0. }, border_width: if selected || focused {
2.
} else if hovered {
1.
} else {
0.
},
border_color: palette.accent, border_color: palette.accent,
border_radius: 10.into(), border_radius: 10.into(),
outline_width: if focused { 2. } else { 0. }, outline_width: 0.,
outline_color: palette.text, outline_color: palette.text,
text_color: Some(palette.text), text_color: Some(palette.text),
icon_color: Some(palette.text), icon_color: Some(palette.text),
@@ -93,15 +117,26 @@ pub fn preview(
variant: Option<Appearance>, variant: Option<Appearance>,
active: Option<Appearance>, active: Option<Appearance>,
selected: bool, selected: bool,
) -> cosmic::widget::icon::Icon {
preview_with_radius(family, variant, active, selected, 10.)
}
fn preview_with_radius(
family: &Family,
variant: Option<Appearance>,
active: Option<Appearance>,
selected: bool,
radius: f32,
) -> cosmic::widget::icon::Icon { ) -> cosmic::widget::icon::Icon {
use cosmic::widget::icon; use cosmic::widget::icon;
let key = format!("{}/{variant:?}/{active:?}/{selected}", family.id); let key = format!("{}/{variant:?}/{active:?}/{selected}/{}", family.id, radius.to_bits());
static CACHE: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, (Family, icon::Handle)>>> = static CACHE: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, (Family, icon::Handle)>>> =
std::sync::OnceLock::new(); std::sync::OnceLock::new();
let cache = CACHE.get_or_init(Default::default); let cache = CACHE.get_or_init(Default::default);
let mut cache = cache.lock().unwrap(); let mut cache = cache.lock().unwrap();
if cache.get(&key).is_none_or(|(cached, _)| cached != family) { if cache.get(&key).is_none_or(|(cached, _)| cached != family) {
let handle = icon::from_svg_bytes(svg(family, variant, active, selected).into_bytes()).symbolic(false); let handle = icon::from_svg_bytes(svg_with_radius(family, variant, active, selected, radius).into_bytes())
.symbolic(false);
cache.insert(key.clone(), (family.clone(), handle)); cache.insert(key.clone(), (family.clone(), handle));
} }
let handle = cache[&key].1.clone(); let handle = cache[&key].1.clone();
@@ -130,11 +165,32 @@ fn desktop(p: &Palette) -> String {
} }
pub fn svg(family: &Family, variant: Option<Appearance>, active: Option<Appearance>, selected: bool) -> String { pub fn svg(family: &Family, variant: Option<Appearance>, active: Option<Appearance>, selected: bool) -> String {
svg_with_radius(family, variant, active, selected, 10.)
}
fn svg_with_radius(
family: &Family,
variant: Option<Appearance>,
active: Option<Appearance>,
selected: bool,
radius: f32,
) -> String {
let mut result = String::from( let mut result = String::from(
r##"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="126" viewBox="0 0 200 126"><defs> r##"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="126" viewBox="0 0 200 126"><defs>
<clipPath id="light"><rect width="100" height="126"/></clipPath><clipPath id="dark"><rect x="100" width="100" height="126"/></clipPath> <clipPath id="light"><rect width="100" height="126"/></clipPath><clipPath id="dark"><rect x="100" width="100" height="126"/></clipPath>
<pattern id="missing" width="8" height="8" patternUnits="userSpaceOnUse"><rect width="8" height="8" fill="#D9DDE2"/><path d="M-2 2l4-4M0 8l8-8M6 10l4-4" stroke="#C4C9D1"/></pattern></defs>"##, <pattern id="missing" width="8" height="8" patternUnits="userSpaceOnUse"><rect width="8" height="8" fill="#D9DDE2"/><path d="M-2 2l4-4M0 8l8-8M6 10l4-4" stroke="#C4C9D1"/></pattern>"##,
); );
let outline =
cosmic::iced::border::Outline::new([0., 0., 200., 126.], [f64::from(radius); 4], BorderShape::Continuous)
.expect("finite preview dimensions");
// Rasterized once per cached preview; no shaped rendering layer per tile.
let points = outline.polygon(0.02).expect("positive contour tolerance");
result.push_str("<clipPath id=\"preview\"><polygon points=\"");
for [x, y] in points {
use std::fmt::Write as _;
write!(result, "{x:.4},{y:.4} ").unwrap();
}
result.push_str("\"/></clipPath></defs><g clip-path=\"url(#preview)\">");
for appearance in [Appearance::Light, Appearance::Dark] { for appearance in [Appearance::Light, Appearance::Dark] {
if variant.is_some_and(|variant| variant != appearance) { if variant.is_some_and(|variant| variant != appearance) {
continue; continue;
@@ -196,7 +252,7 @@ pub fn svg(family: &Family, variant: Option<Appearance>, active: Option<Appearan
)); ));
result.push_str(&format!(r#"<circle cx="183" cy="16" r="10" fill="{}"/><path d="M178 16l3 3 6-6" fill="none" stroke="{foreground}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>"#, p.accent)); result.push_str(&format!(r#"<circle cx="183" cy="16" r="10" fill="{}"/><path d="M178 16l3 3 6-6" fill="none" stroke="{foreground}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>"#, p.accent));
} }
result.push_str("</svg>"); result.push_str("</g></svg>");
result result
} }
+23 -2
View File
@@ -249,7 +249,7 @@ prevent popup cleanup.
Settings → Appearance offers six paired themes: **Ferese Blue** (default), Settings → Appearance offers six paired themes: **Ferese Blue** (default),
**Catppuccin**, **Gruvbox**, **Rosé Pine**, **Tokyo Night**, and **Everforest**. **Catppuccin**, **Gruvbox**, **Rosé Pine**, **Tokyo Night**, and **Everforest**.
Ferese Blue uses the logo accent, `#3D7BE6`, and has light and dark variants. Ferese Blue uses the default accent, `#3D7BE6`, and has light and dark variants.
Choose Light, Dark, or Auto in Settings or Control Center. Auto follows local Choose Light, Dark, or Auto in Settings or Control Center. Auto follows local
07:00 and 19:00 boundaries unless you change its schedule. 07:00 and 19:00 boundaries unless you change its schedule.
@@ -278,13 +278,34 @@ one appearance.
| `appearance.inactive-dim.amount` | number 0–1 | `0.15` | Darkening strength | | `appearance.inactive-dim.amount` | number 0–1 | `0.15` | Darkening strength |
| `appearance.inactive-dim.duration-ms` | number ≥ 0 | `150` | Dimming transition; 0 snaps | | `appearance.inactive-dim.duration-ms` | number ≥ 0 | `150` | Dimming transition; 0 snaps |
| `theme.typography.font-family` | string | `"Inter"` | Shell, Settings and overview font | | `theme.typography.font-family` | string | `"Inter"` | Shell, Settings and overview font |
| `theme.background.path` | string | bundled Ferese wallpaper | Wallpaper image path; an existing selection overrides the default | | `theme.background.path` | string | matching light/dark wallpaper | Shared wallpaper; an explicit path overrides both bundled defaults |
| `theme.light.background.path`, `theme.dark.background.path` | string | shared path, or matching default | Wallpaper for one appearance; overrides the shared path |
| `theme.background.mode` | `"fill"`, `"fit"` | `"fill"` | Crop or letterbox | | `theme.background.mode` | `"fill"`, `"fit"` | `"fill"` | Crop or letterbox |
| `theme.light.background.mode`, `theme.dark.background.mode` | `"fill"`, `"fit"` | shared placement | Image placement for one appearance |
| `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material | | `theme.material.style` | `"solid"`, `"translucent"` | `"solid"` | Shell background material |
| `theme.material.opacity` | number 0–1 | `0.78` | Shared shell background opacity for bars, menus, popovers, notifications and themed dialogs. Text/icons stay opaque; 0 hides the material. Solid mode is always opaque | | `theme.material.opacity` | number 0–1 | `0.78` | Shared shell background opacity for bars, menus, popovers, notifications and themed dialogs. Text/icons stay opaque; 0 hides the material. Solid mode is always opaque |
| `theme.material.tint-strength` | number 0–1 | Dark `0.5`, light `1` | Color strength over the blurred backdrop | | `theme.material.tint-strength` | number 0–1 | Dark `0.5`, light `1` | Color strength over the blurred backdrop |
| `theme.material.blur-radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables | | `theme.material.blur-radius` | number ≥ 0 | `12` | Translucent backdrop blur, capped at 32; 0 disables |
Settings → Wallpaper shows the current image and both bundled defaults. Use
**Apply to** to change both modes or just one. **Match appearance** restores the
bundled light and dark pair. Custom images can also be selected separately in KDL:
```kdl
theme {
light {
background { path "~/Pictures/day.png"; }
}
dark {
background { path "~/Pictures/night.jpg"; }
}
}
```
The wallpaper follows manual light/dark changes and automatic appearance changes.
A shared `theme.background.path` applies to both modes unless a per-mode path
is set.
| `theme.colors` key | Default | Meaning | | `theme.colors` key | Default | Meaning |
| --- | --- | --- | | --- | --- | --- |
| `surface-base` | `"#111821"` | Material/card base color | | `surface-base` | `"#111821"` | Material/card base color |
-24
View File
@@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 80" role="img" aria-labelledby="title desc">
<title id="title">Ferese</title>
<desc id="desc">The blue Ferese symbol beside the Ferese wordmark</desc>
<defs>
<linearGradient id="mark" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6ea3ff"/>
<stop offset="100%" stop-color="#3d7be6"/>
</linearGradient>
<style>
.wordmark { fill: #1c1d21; }
@media (prefers-color-scheme: dark) {
.wordmark { fill: #f2f2f7; }
}
</style>
</defs>
<g transform="translate(0 8) scale(.32)" fill="url(#mark)">
<rect x="20" y="20" width="50" height="160" rx="8"/>
<rect x="80" y="20" width="100" height="55" rx="8"/>
<rect x="80" y="95" width="70" height="45" rx="8"/>
</g>
<text class="wordmark" x="78" y="56" font-family="-apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" font-size="48" font-weight="700">Ferese</text>
</svg>

Before

Width:  |  Height:  |  Size: 977 B

+2 -2
View File
@@ -4,9 +4,9 @@ These screenshots were taken on September 30, 2026. Click an image to open it at
## Ferese Blue ## Ferese Blue
Settings and Control Center with the default wallpaper. Settings and Control Center with the previous default wallpaper.
[![Settings and Control Center with the default wallpaper.](images/screenshots/ferese-main-hero.png)](images/screenshots/ferese-main-hero.png) [![Settings and Control Center with the previous default wallpaper.](images/screenshots/ferese-main-hero.png)](images/screenshots/ferese-main-hero.png)
## Rosé Pine ## Rosé Pine
+6 -6
View File
@@ -1,18 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1058" height="650" viewBox="105 314 1058 650" role="img" aria-labelledby="title desc"> <svg xmlns="http://www.w3.org/2000/svg" width="1058" height="650" viewBox="105 314 1058 650" role="img" aria-labelledby="title desc">
<title id="title">Ferese orbit — gradient</title> <title id="title">Ferese — gradient</title>
<desc id="desc">Open, tilted orbit symbol. Smooth cubic Bezier vector paths; transparent background.</desc> <desc id="desc">Ferese symbol with smooth cubic Bezier paths and a transparent background.</desc>
<defs> <defs>
<linearGradient id="ferese-orbit-left" gradientUnits="userSpaceOnUse" x1="174" y1="741" x2="728" y2="389"> <linearGradient id="ferese-left" gradientUnits="userSpaceOnUse" x1="174" y1="741" x2="728" y2="389">
<stop offset="0" stop-color="#70DEF1"/> <stop offset="0" stop-color="#70DEF1"/>
<stop offset="0.53" stop-color="#30ADFF"/> <stop offset="0.53" stop-color="#30ADFF"/>
<stop offset="1" stop-color="#087CFF"/> <stop offset="1" stop-color="#087CFF"/>
</linearGradient> </linearGradient>
<linearGradient id="ferese-orbit-right" gradientUnits="userSpaceOnUse" x1="356" y1="857" x2="1073" y2="410"> <linearGradient id="ferese-right" gradientUnits="userSpaceOnUse" x1="356" y1="857" x2="1073" y2="410">
<stop offset="0" stop-color="#22BCF5"/> <stop offset="0" stop-color="#22BCF5"/>
<stop offset="0.48" stop-color="#087FFF"/> <stop offset="0.48" stop-color="#087FFF"/>
<stop offset="1" stop-color="#093CE2"/> <stop offset="1" stop-color="#093CE2"/>
</linearGradient> </linearGradient>
</defs> </defs>
<path id="sweep-left" fill="url(#ferese-orbit-left)" d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z"/> <path id="sweep-left" fill="url(#ferese-left)" d="M 698 380 C 585 378 397 451 280 530 C 197 586 145 655 145 724 C 145 811 227 867 355 857 C 299 803 295 755 325 689 C 367 598 462 520 567 467 C 620 440 670 423 712 407 C 732 399 736 391 721 385 C 715 382 707 380 698 380 Z"/>
<path id="sweep-right" fill="url(#ferese-orbit-right)" d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z"/> <path id="sweep-right" fill="url(#ferese-right)" d="M 795 373 C 849 354 922 351 982 358 C 1071 368 1123 404 1123 461 C 1123 552 1027 681 906 780 C 782 881 643 927 524 921 C 453 918 397 898 355 857 C 496 845 671 797 807 720 C 932 649 992 573 986 516 C 980 456 928 423 844 408 C 821 404 799 401 787 395 C 774 389 780 379 795 373 Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+4 -4
View File
@@ -33,7 +33,7 @@ for name in ('index.html', 'styles.css', 'app.js'):
(OUTPUT / 'assets').mkdir(exist_ok=True) (OUTPUT / 'assets').mkdir(exist_ok=True)
shutil.copytree(ROOT / 'site/assets', OUTPUT / 'assets', dirs_exist_ok=True) shutil.copytree(ROOT / 'site/assets', OUTPUT / 'assets', dirs_exist_ok=True)
shutil.copytree(ROOT / 'docs/images', OUTPUT / 'assets', dirs_exist_ok=True) shutil.copytree(ROOT / 'docs/images', OUTPUT / 'assets', dirs_exist_ok=True)
shutil.copy2(ROOT / 'packaging/icons/ferese.svg', OUTPUT / 'assets/ferese.svg') shutil.copytree(ROOT / 'assets/branding', OUTPUT / 'assets/branding')
(OUTPUT / 'docs').mkdir(exist_ok=True) (OUTPUT / 'docs').mkdir(exist_ok=True)
(OUTPUT / '.nojekyll').touch() (OUTPUT / '.nojekyll').touch()
search_index = [] search_index = []
@@ -103,13 +103,13 @@ for page_number, (slug, title, source) in enumerate(PAGES):
doc = f'''<!doctype html> doc = f'''<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#10151c"><title>{escape(title)} — Ferese handbook</title><meta name="description" content="{escape(title)} in the Ferese handbook: setup, configuration, and guides for your Wayland desktop."> <meta name="theme-color" content="#10151c"><title>{escape(title)} — Ferese handbook</title><meta name="description" content="{escape(title)} in the Ferese handbook: setup, configuration, and guides for your Wayland desktop.">
<link rel="icon" href="../assets/ferese.svg" type="image/svg+xml"><link rel="stylesheet" href="../styles.css"><script src="../app.js" defer></script></head> <link rel="icon" href="../assets/branding/ferese-blue-flat.svg" type="image/svg+xml"><link rel="icon" href="../assets/favicon.ico" sizes="16x16 32x32 48x48"><link rel="icon" href="../assets/favicon-32.png" type="image/png" sizes="32x32"><link rel="stylesheet" href="../styles.css"><script src="../app.js" defer></script></head>
<body class="docs-page"><a class="skip" href="#main">Skip to content</a> <body class="docs-page"><a class="skip" href="#main">Skip to content</a>
<header class="site-header"><div class="header-inner"><a class="wordmark" href="../" aria-label="Ferese home"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 3h6v18H3zM11 3h10v6H11zM11 11h7v6h-7z"/></svg>ferese</a><nav aria-label="Main navigation"><a href="../">Overview</a><a href="./" aria-current="page">Documentation</a><a href="{REPO}">GitHub <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></a></nav></div></header> <header class="site-header"><div class="header-inner"><a class="brand" href="../" aria-label="Ferese home"><img class="brand-icon" src="../assets/branding/ferese-blue-dark.svg" width="1024" height="1024" alt=""><span class="brand-name">Ferese</span></a><nav aria-label="Main navigation"><a href="../">Overview</a><a href="./" aria-current="page">Documentation</a><a href="{REPO}">GitHub <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></a></nav></div></header>
<div class="docs-layout"><aside class="docs-sidebar"><div class="docs-search" hidden><label class="sr-only" for="docs-search">Search the handbook</label><input id="docs-search" type="search" placeholder="Search the handbook…" autocomplete="off" aria-controls="search-results"><div class="search-results" id="search-results" hidden></div><p class="sr-only" id="search-status" role="status"></p></div><details open><summary>FERESE HANDBOOK</summary><nav aria-label="Documentation">{navigation}</nav></details><div class="sidebar-links"><a href="{REPO}/issues">Report an issue <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></a><a href="../">Back to Ferese ←</a></div></aside> <div class="docs-layout"><aside class="docs-sidebar"><div class="docs-search" hidden><label class="sr-only" for="docs-search">Search the handbook</label><input id="docs-search" type="search" placeholder="Search the handbook…" autocomplete="off" aria-controls="search-results"><div class="search-results" id="search-results" hidden></div><p class="sr-only" id="search-status" role="status"></p></div><details open><summary>FERESE HANDBOOK</summary><nav aria-label="Documentation">{navigation}</nav></details><div class="sidebar-links"><a href="{REPO}/issues">Report an issue <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></a><a href="../">Back to Ferese ←</a></div></aside>
<main class="docs-article" id="main"><div class="breadcrumb"><a href="./">Handbook</a><span aria-hidden="true">/</span><span>{escape(title)}</span></div><article class="prose">{body}</article><a class="edit-link" href="{REPO}/blob/main/{source.relative_to(ROOT)}">View this guide on GitHub <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></a><nav class="docs-pager" aria-label="Previous and next guide">{pager}</nav></main> <main class="docs-article" id="main"><div class="breadcrumb"><a href="./">Handbook</a><span aria-hidden="true">/</span><span>{escape(title)}</span></div><article class="prose">{body}</article><a class="edit-link" href="{REPO}/blob/main/{source.relative_to(ROOT)}">View this guide on GitHub <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></a><nav class="docs-pager" aria-label="Previous and next guide">{pager}</nav></main>
<aside class="docs-toc" aria-label="On this page">{'<p>ON THIS PAGE</p><nav>' + contents + '</nav>' if contents else ''}</aside></div> <aside class="docs-toc" aria-label="On this page">{'<p>ON THIS PAGE</p><nav>' + contents + '</nav>' if contents else ''}</aside></div>
<footer class="site-footer page-width"><a class="wordmark" href="../">ferese</a><p>A Wayland desktop with scrolling layouts.</p><a href="{REPO}">Built in the open <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></a></footer></body></html>''' <footer class="site-footer page-width"><a class="brand" href="../" aria-label="Ferese home"><img class="brand-icon" src="../assets/branding/ferese-blue-dark.svg" width="1024" height="1024" alt=""></a><p>A Wayland desktop with scrolling layouts.</p><a href="{REPO}">Built in the open <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></a></footer></body></html>'''
(OUTPUT / 'docs' / f'{slug}.html').write_text(doc) (OUTPUT / 'docs' / f'{slug}.html').write_text(doc)
(OUTPUT / 'docs/search-index.json').write_text(json.dumps(search_index, ensure_ascii=False)) (OUTPUT / 'docs/search-index.json').write_text(json.dumps(search_index, ensure_ascii=False))
print(f'Built website and {len(PAGES)} handbook pages at {OUTPUT}') print(f'Built website and {len(PAGES)} handbook pages at {OUTPUT}')
+4
View File
@@ -143,6 +143,10 @@ def validate_bundle(bundle):
for item in manifest['files']: for item in manifest['files']:
target = item['target'] target = item['target']
allowed = expected.get(target) allowed = expected.get(target)
# Older releases bundled this retired source asset. Keep their manifests
# verifiable for upgrades and rollback without adding it to new bundles.
if allowed is None and target == 'wallpapers/ferese.svg':
allowed = {'target': target, 'mode': '0644'}
if allowed is None and re.fullmatch(r'licenses/theme-[A-Za-z0-9._-]+\.txt', target): if allowed is None and re.fullmatch(r'licenses/theme-[A-Za-z0-9._-]+\.txt', target):
allowed = {'target': target, 'mode': '0644'} allowed = {'target': target, 'mode': '0644'}
+7 -2
View File
@@ -101,8 +101,13 @@
"mode": "0644" "mode": "0644"
}, },
{ {
"source": "assets/wallpapers/ferese.svg", "source": "assets/wallpapers/ferese-wallpaper-dark.jpg",
"target": "wallpapers/ferese.svg", "target": "wallpapers/ferese-wallpaper-dark.jpg",
"mode": "0644"
},
{
"source": "assets/wallpapers/ferese-wallpaper-light.png",
"target": "wallpapers/ferese-wallpaper-light.png",
"mode": "0644" "mode": "0644"
}, },
{ {
+25
View File
@@ -127,6 +127,31 @@ m.main()
self.assertEqual(os.readlink(self.base / 'previous'), 'releases/second') self.assertEqual(os.readlink(self.base / 'previous'), 'releases/second')
self.assertEqual([(self.root / path).read_bytes() for path in (PORTAL, UNIT, DESKTOP)], first) self.assertEqual([(self.root / path).read_bytes() for path in (PORTAL, UNIT, DESKTOP)], first)
def test_upgrade_and_rollback_accept_retired_wallpaper_svg_with_verified_checksum(self):
legacy = Path(self.temporary.name) / 'legacy'
shutil.copytree(self.bundles[0], legacy)
svg = legacy / 'wallpapers/ferese.svg'
svg.write_text('<svg xmlns="http://www.w3.org/2000/svg"/>\n')
svg.chmod(0o644)
manifest_path = legacy / 'manifest.json'
manifest = json.loads(manifest_path.read_text())
manifest['files'].append({'target': 'wallpapers/ferese.svg', 'mode': '0644',
'sha256': installer.digest(svg)})
manifest_path.write_text(json.dumps(manifest))
self.run_command('install', legacy)
self.assertTrue((self.base / 'current/wallpapers/ferese.svg').is_file())
self.install(1)
self.assertEqual(os.readlink(self.base / 'current'), 'releases/second')
self.assertFalse((self.base / 'current/wallpapers/ferese.svg').exists())
self.run_command('rollback')
self.assertEqual(os.readlink(self.base / 'current'), 'releases/first')
self.assertEqual((self.base / 'current/wallpapers/ferese.svg').read_bytes(), svg.read_bytes())
svg.write_text('corrupt legacy wallpaper')
with self.assertRaisesRegex(ValueError, 'Checksum mismatch: wallpapers/ferese.svg'):
installer.validate_bundle(legacy)
def test_upgrade_uses_previous_installer_ownership_receipts(self): def test_upgrade_uses_previous_installer_ownership_receipts(self):
self.install() self.install()
release = self.base / 'releases/first' release = self.base / 'releases/first'
@@ -312,7 +312,7 @@ mod tests {
( (
"/unused/connections-test.kdl".into(), "/unused/connections-test.kdl".into(),
crate::store::Snapshot::parse(String::new()), crate::store::Snapshot::parse(String::new()),
Some(super::super::Tab::Bluetooth), Some(crate::InitialPage::Connections(super::super::Tab::Bluetooth)),
), ),
); );
assert_eq!(app.page, crate::schema::Page::Connections); assert_eq!(app.page, crate::schema::Page::Connections);
+2 -2
View File
@@ -176,7 +176,7 @@ impl App {
.into() .into()
} }
Kind::Choice { default, choices } => { Kind::Choice { default, choices } => {
let value = self.draft.string(&path, default); let value = self.wallpaper_field_value(&path, default);
let mut options = row([]).spacing(2); let mut options = row([]).spacing(2);
for (key, label) in choices { for (key, label) in choices {
options = options.push( options = options.push(
@@ -196,7 +196,7 @@ impl App {
if argv { if argv {
self.draft.argv(&path) self.draft.argv(&path)
} else { } else {
self.draft.string(&path, default) self.wallpaper_field_value(&path, default)
} }
}); });
let commit = field.clone(); let commit = field.clone();
+133 -99
View File
@@ -12,6 +12,7 @@ mod fonts;
mod schema; mod schema;
mod store; mod store;
mod visuals; mod visuals;
mod wallpaper_controls;
mod watch; mod watch;
use std::collections::HashMap; use std::collections::HashMap;
@@ -28,7 +29,7 @@ use store::{Edit, Snapshot, set};
fn main() -> cosmic::iced::Result { fn main() -> cosmic::iced::Result {
let mut args = std::env::args_os().skip(1); let mut args = std::env::args_os().skip(1);
let mut path = store::config_path(); let mut path = store::config_path();
let mut connection_tab = None; let mut initial_page = None;
while let Some(arg) = args.next() { while let Some(arg) = args.next() {
match arg.to_str() { match arg.to_str() {
Some("--config") => { Some("--config") => {
@@ -39,17 +40,18 @@ fn main() -> cosmic::iced::Result {
path = PathBuf::from(value); path = PathBuf::from(value);
} }
Some("--page") => { Some("--page") => {
connection_tab = match args.next().as_deref().and_then(|s| s.to_str()) { initial_page = match args.next().as_deref().and_then(|s| s.to_str()) {
Some("connections" | "wifi") => Some(connections::Tab::Wifi), Some("connections" | "wifi") => Some(InitialPage::Connections(connections::Tab::Wifi)),
Some("bluetooth") => Some(connections::Tab::Bluetooth), Some("bluetooth") => Some(InitialPage::Connections(connections::Tab::Bluetooth)),
Some("wallpaper") => Some(InitialPage::Wallpaper),
_ => { _ => {
eprintln!("--page accepts connections, wifi, or bluetooth"); eprintln!("--page accepts connections, wifi, bluetooth, or wallpaper");
return Ok(()); return Ok(());
} }
}; };
} }
_ => { _ => {
eprintln!("Usage: ferese-settings [--config PATH] [--page connections|wifi|bluetooth]"); eprintln!("Usage: ferese-settings [--config PATH] [--page connections|wifi|bluetooth|wallpaper]");
return Ok(()); return Ok(());
} }
} }
@@ -65,10 +67,16 @@ fn main() -> cosmic::iced::Result {
.is_daemon(false) .is_daemon(false)
.antialiasing(true) .antialiasing(true)
.default_text_size(14.), .default_text_size(14.),
(path, initial, connection_tab), (path, initial, initial_page),
) )
} }
#[derive(Clone, Copy)]
enum InitialPage {
Connections(connections::Tab),
Wallpaper,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum Message { enum Message {
Connections(connections::Input), Connections(connections::Input),
@@ -109,12 +117,16 @@ enum Message {
ThemeImported(Result<Option<(String, String)>, String>), ThemeImported(Result<Option<(String, String)>, String>),
ExpireUndo(u64), ExpireUndo(u64),
PickWallpaper, PickWallpaper,
WallpaperTarget(wallpaper_controls::Target),
UseDefaultWallpaper(ferese_config::theme::Appearance),
RestoreDefaultWallpapers,
DefaultWallpaperThumbnail(ferese_config::theme::Appearance, Result<widget::image::Handle, String>),
PreviewLock, PreviewLock,
LockPreviewStarted(Result<(), String>), LockPreviewStarted(Result<(), String>),
AddNote, AddNote,
NoteAction(String, widget::text_editor::Action), NoteAction(String, widget::text_editor::Action),
SaveNote(String, u64), SaveNote(String, u64),
WallpaperPicked(Result<Option<String>, String>), WallpaperPicked(wallpaper_controls::Target, Result<Option<String>, String>),
NewCommand(String), NewCommand(String),
AddCommand, AddCommand,
AddSwipe(&'static str), AddSwipe(&'static str),
@@ -157,10 +169,7 @@ struct App {
auto_details: bool, auto_details: bool,
advanced_theme: bool, advanced_theme: bool,
theme_file_target: usize, theme_file_target: usize,
thumbnail: Option<widget::image::Handle>, wallpaper: wallpaper_controls::State,
thumbnail_path: String,
thumbnail_loading: bool,
thumbnail_error: Option<String>,
} }
fn initial_visible_rows() -> std::collections::HashSet<(&'static str, usize)> { fn initial_visible_rows() -> std::collections::HashSet<(&'static str, usize)> {
@@ -197,7 +206,7 @@ struct NoteEditor {
impl cosmic::Application for App { impl cosmic::Application for App {
type Executor = cosmic::executor::Default; type Executor = cosmic::executor::Default;
type Flags = (PathBuf, Result<Snapshot, String>, Option<connections::Tab>); type Flags = (PathBuf, Result<Snapshot, String>, Option<InitialPage>);
type Message = Message; type Message = Message;
const APP_ID: &'static str = "dev.ferese.Settings"; const APP_ID: &'static str = "dev.ferese.Settings";
@@ -209,7 +218,7 @@ impl cosmic::Application for App {
&mut self.core &mut self.core
} }
fn init(mut core: Core, (path, initial, connection_tab): Self::Flags) -> (Self, Task<Message>) { fn init(mut core: Core, (path, initial, initial_page): Self::Flags) -> (Self, Task<Message>) {
core.window.show_headerbar = false; core.window.show_headerbar = false;
let status = if path == store::config_path() { let status = if path == store::config_path() {
@@ -226,7 +235,10 @@ impl cosmic::Application for App {
let family_sections = theme_controls::FamilySections::new(&resolved.families, &current); let family_sections = theme_controls::FamilySections::new(&resolved.families, &current);
let mut app = Self { let mut app = Self {
connections: connections::State { connections: connections::State {
tab: connection_tab.unwrap_or_default(), tab: match initial_page {
Some(InitialPage::Connections(tab)) => tab,
_ => connections::Tab::default(),
},
..connections::State::default() ..connections::State::default()
}, },
displays: vec![], displays: vec![],
@@ -243,10 +255,10 @@ impl cosmic::Application for App {
saving_previous: String::new(), saving_previous: String::new(),
error, error,
status, status,
page: if connection_tab.is_some() { page: match initial_page {
Page::Connections Some(InitialPage::Connections(_)) => Page::Connections,
} else { Some(InitialPage::Wallpaper) => Page::Wallpaper,
Page::Appearance None => Page::Appearance,
}, },
profile_pages: std::env::var_os("FERESE_PROFILE_SETTINGS").is_some(), profile_pages: std::env::var_os("FERESE_PROFILE_SETTINGS").is_some(),
page_transition: None, page_transition: None,
@@ -266,10 +278,7 @@ impl cosmic::Application for App {
auto_details: false, auto_details: false,
advanced_theme: false, advanced_theme: false,
theme_file_target: 0, theme_file_target: 0,
thumbnail: None, wallpaper: Default::default(),
thumbnail_path: String::new(),
thumbnail_loading: false,
thumbnail_error: None,
}; };
let task = app let task = app
.core .core
@@ -283,7 +292,8 @@ impl cosmic::Application for App {
} else { } else {
Task::none() Task::none()
}; };
(app, Task::batch([task, connections])) let thumbnails = app.load_thumbnail();
(app, Task::batch([task, connections, thumbnails]))
} }
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
@@ -321,6 +331,8 @@ impl cosmic::Application for App {
return self.edit_many(displays::edits(&prefix, mode, automatic)); return self.edit_many(displays::edits(&prefix, mode, automatic));
} }
Message::ThemeChanged(snapshot) => { Message::ThemeChanged(snapshot) => {
let wallpaper_changed =
self.resolved.presented.tokens.background.path != snapshot.presented.tokens.background.path;
let font_changed = self.resolved.presented.tokens.typography.font_family let font_changed = self.resolved.presented.tokens.typography.font_family
!= snapshot.presented.tokens.typography.font_family; != snapshot.presented.tokens.typography.font_family;
if self.resolved.families != snapshot.families { if self.resolved.families != snapshot.families {
@@ -333,7 +345,12 @@ impl cosmic::Application for App {
self.font = ferese_theme::font(Some(&self.resolved.presented.tokens.typography.font_family)); self.font = ferese_theme::font(Some(&self.resolved.presented.tokens.typography.font_family));
} }
return self.update_theme(); let theme = self.update_theme();
return if wallpaper_changed {
Task::batch([theme, self.load_thumbnail()])
} else {
theme
};
} }
Message::NoteAction(id, action) => { Message::NoteAction(id, action) => {
if let Some(editor) = self.note_editors.get_mut(&id) { if let Some(editor) = self.note_editors.get_mut(&id) {
@@ -739,7 +756,8 @@ impl cosmic::Application for App {
Message::ThemeImported(Err(error)) => self.error = Some(error), Message::ThemeImported(Err(error)) => self.error = Some(error),
Message::ExpireUndo(revision) if revision == self.undo_revision => self.undo = None, Message::ExpireUndo(revision) if revision == self.undo_revision => self.undo = None,
Message::PickWallpaper => { Message::PickWallpaper => {
return cosmic::task::future(async { let target = self.wallpaper.target;
return cosmic::task::future(async move {
let result = tokio::task::spawn_blocking(|| { let result = tokio::task::spawn_blocking(|| {
store::pick_file( store::pick_file(
"Choose a wallpaper", "Choose a wallpaper",
@@ -748,34 +766,31 @@ impl cosmic::Application for App {
}) })
.await .await
.unwrap_or_else(|e| Err(e.to_string())); .unwrap_or_else(|e| Err(e.to_string()));
Message::WallpaperPicked(result) Message::WallpaperPicked(target, result)
}); });
} }
Message::WallpaperPicked(Ok(Some(path))) => { Message::WallpaperPicked(target, Ok(Some(path))) => {
return self.change(set("theme.background.path", path)); return self.apply_wallpaper_edits(wallpaper_controls::edits(target, "path", path));
} }
Message::WallpaperPicked(Err(error)) => self.error = Some(error), Message::WallpaperPicked(_, Err(error)) => self.error = Some(error),
Message::Thumbnail(path, result) => { Message::Thumbnail(path, result) => {
self.thumbnail_loading = false; self.wallpaper_thumbnail(path, result);
if path
== self
.current
.string("theme.background.path", ferese_config::default_wallpaper())
{
self.thumbnail_path = path;
match result {
Ok(handle) => {
self.thumbnail = Some(handle);
self.thumbnail_error = None;
}
Err(error) => {
self.thumbnail = None;
self.thumbnail_error = Some(error);
}
}
}
return self.load_thumbnail(); return self.load_thumbnail();
} }
Message::DefaultWallpaperThumbnail(appearance, result) => {
self.default_wallpaper_thumbnail(appearance, result);
}
Message::WallpaperTarget(target) => self.wallpaper.target = target,
Message::UseDefaultWallpaper(appearance) => {
return self.apply_wallpaper_edits(wallpaper_controls::edits(
self.wallpaper.target,
"path",
ferese_config::default_wallpaper_for(appearance),
));
}
Message::RestoreDefaultWallpapers => {
return self.apply_wallpaper_edits(wallpaper_controls::restore_defaults());
}
Message::NewCommand(value) => self.new_command = value, Message::NewCommand(value) => self.new_command = value,
Message::AddCommand if !self.saving => match shlex::split(&self.new_command).filter(|a| !a.is_empty()) { Message::AddCommand if !self.saving => match shlex::split(&self.new_command).filter(|a| !a.is_empty()) {
Some(args) => { Some(args) => {
@@ -887,57 +902,6 @@ impl App {
self.note_editors.retain(|id, _| ids.contains(id)); self.note_editors.retain(|id, _| ids.contains(id));
} }
fn load_thumbnail(&mut self) -> Task<Message> {
let path = self
.current
.string("theme.background.path", ferese_config::default_wallpaper());
if self.page != Page::Wallpaper || self.thumbnail_loading || path == self.thumbnail_path {
return Task::none();
}
if path.is_empty() {
self.thumbnail = None;
self.thumbnail_path.clear();
self.thumbnail_error = None;
return Task::none();
}
self.thumbnail_loading = true;
cosmic::task::future(async move {
let (send, receive) = cosmic::iced::futures::channel::oneshot::channel();
let file = path.clone();
std::thread::spawn(move || {
let result = (|| -> Result<_, String> {
let mut reader = image::ImageReader::open(&file)
.map_err(|e| e.to_string())?
.with_guessed_format()
.map_err(|e| e.to_string())?;
let mut limits = image::Limits::default();
limits.max_alloc = Some(256 * 1024 * 1024);
limits.max_image_width = Some(16384);
limits.max_image_height = Some(16384);
reader.limits(limits);
let pixels = reader
.decode()
.map_err(|e| e.to_string())?
.thumbnail(1000, 500)
.into_rgba8();
Ok(widget::image::Handle::from_rgba(
pixels.width(),
pixels.height(),
pixels.into_raw(),
))
})();
let _ = send.send(result);
});
Message::Thumbnail(
path,
receive
.await
.unwrap_or_else(|_| Err("Could not load wallpaper preview.".into())),
)
})
}
fn edit_many(&mut self, edits: Vec<Edit>) -> Task<Message> { fn edit_many(&mut self, edits: Vec<Edit>) -> Task<Message> {
for edit in edits { for edit in edits {
if let Err(error) = self.draft.edit(&edit) { if let Err(error) = self.draft.edit(&edit) {
@@ -960,6 +924,16 @@ impl App {
} }
fn change(&mut self, edit: Edit) -> Task<Message> { fn change(&mut self, edit: Edit) -> Task<Message> {
if let Edit::Set(path, value) = &edit
&& matches!(path.as_str(), "theme.background.path" | "theme.background.mode")
{
return self.apply_wallpaper_edits(wallpaper_controls::edits(
wallpaper_controls::Target::Both,
path.rsplit('.').next().unwrap(),
value.clone(),
));
}
match self.draft.edit(&edit) { match self.draft.edit(&edit) {
Ok(()) => { Ok(()) => {
// A shared style selection applies to both appearances while // A shared style selection applies to both appearances while
@@ -1061,6 +1035,66 @@ mod tests {
1 + tree.children.iter().map(tree_nodes).sum::<usize>() 1 + tree.children.iter().map(tree_nodes).sum::<usize>()
} }
#[test]
fn wallpaper_preview_uses_presented_state_and_tracks_mode_changes() {
let mut app = app();
app.path = store::config_path();
app.current = Snapshot::parse("theme { background { path \"/saved.png\"; }; }".into()).unwrap();
app.resolved.presented.tokens.background.path = Some("/active.png".into());
assert_eq!(app.active_wallpaper_path(), "/active.png");
app.page = Page::Wallpaper;
let mut snapshot = app.resolved.clone();
snapshot.presented.tokens.background.path =
Some(ferese_config::default_wallpaper_for(ferese_config::theme::Appearance::Light).into());
let task = app.update(Message::ThemeChanged(Box::new(snapshot)));
assert!(task.units() > 0);
assert_eq!(
app.active_wallpaper_path(),
ferese_config::default_wallpaper_for(ferese_config::theme::Appearance::Light)
);
}
#[test]
fn wallpaper_preview_config_preserves_its_mode_and_relative_paths() {
let mut app = app();
app.path = PathBuf::from("/preview/config.kdl");
app.current = Snapshot::parse("theme { mode \"light\"; background { path \"shared.png\"; }; light { background { path \"light.png\"; }; }; }".into()).unwrap();
assert_eq!(app.active_wallpaper_path(), "/preview/light.png");
app.wallpaper.target = wallpaper_controls::Target::Dark;
assert_eq!(app.active_wallpaper_path(), "/preview/light.png");
}
#[test]
fn choosing_a_default_clears_old_input_and_keeps_the_other_mode() {
let mut app = app();
app.saving = true;
app.wallpaper.target = wallpaper_controls::Target::Light;
app.draft = Snapshot::parse("theme { dark { background { path \"/custom-dark.png\"; }; }; }".into()).unwrap();
app.inputs
.insert("theme.light.background.path".into(), "/unfinished.png".into());
let _ = app.update(Message::UseDefaultWallpaper(ferese_config::theme::Appearance::Light));
assert!(!app.inputs.contains_key("theme.light.background.path"));
assert_eq!(
app.draft.string("theme.light.background.path", ""),
ferese_config::default_wallpaper_for(ferese_config::theme::Appearance::Light)
);
assert_eq!(app.draft.string("theme.dark.background.path", ""), "/custom-dark.png");
}
#[test]
fn wallpaper_picker_keeps_its_target_when_selection_changes() {
let mut app = app();
app.saving = true;
app.wallpaper.target = wallpaper_controls::Target::Dark;
let _ = app.update(Message::WallpaperPicked(
wallpaper_controls::Target::Light,
Ok(Some("/chosen.png".into())),
));
assert_eq!(app.draft.string("theme.light.background.path", ""), "/chosen.png");
assert!(app.draft.item("theme.dark.background.path").is_none());
}
#[test] #[test]
fn shortcuts_build_controls_only_for_mounted_rows() { fn shortcuts_build_controls_only_for_mounted_rows() {
let mut app = shortcuts(100); let mut app = shortcuts(100);
+6 -23
View File
@@ -179,30 +179,13 @@ impl App {
} }
if self.page == Page::Wallpaper { if self.page == Page::Wallpaper {
if let Some(handle) = &self.thumbnail { body = body.push(self.wallpaper_controls());
body = body.push(
widget::image(handle.clone())
.width(Length::Fill)
.height(160)
.content_fit(cosmic::iced::ContentFit::Contain),
);
} else {
body = body.push(self.note(if self.thumbnail_loading {
"Loading wallpaper preview…"
} else {
self.thumbnail_error
.as_deref()
.unwrap_or("Choose an image to preview your wallpaper.")
}));
}
body = body.push(self.settings_button(
"Choose image…",
"M3 4h18v16H3z M3 15l5-5 5 5 3-3 5 5 M15 8h.01",
Some(Message::PickWallpaper),
false,
));
} }
let fields = schema::fields(self.page); let fields = if self.page == Page::Wallpaper {
self.wallpaper_fields()
} else {
schema::fields(self.page)
};
if !fields.is_empty() { if !fields.is_empty() {
let mut group = column([]).spacing(1); let mut group = column([]).spacing(1);
+2 -2
View File
@@ -249,8 +249,8 @@ impl App {
let list = crate::navigation::gallery_list(appearance, custom); let list = crate::navigation::gallery_list(appearance, custom);
for chunk in 0..self.family_sections.indices(custom).len().div_ceil(3) { for chunk in 0..self.family_sections.indices(custom).len().div_ceil(3) {
let content = widget::responsive(move |size| { let content = widget::responsive(move |size| {
// Three previews: 200:126 image, caption, and tile padding. // Three borderless previews, with names overlaid inside each image.
let height = ((size.width - 12.) / 3. - 16.).max(0.) * 126. / 200. + 34.; let height = ((size.width - 12.) / 3.).max(0.) * 126. / 200.;
let content: Element<'_, Message> = if self.visible_rows.contains(&(list, chunk)) { let content: Element<'_, Message> = if self.visible_rows.contains(&(list, chunk)) {
self.theme_gallery_row(chunk, appearance, custom) self.theme_gallery_row(chunk, appearance, custom)
} else { } else {
@@ -0,0 +1,591 @@
use cosmic::app::Task;
use cosmic::widget::{button, column, row};
use ferese_config::theme::Appearance;
use super::{Alignment, App, Element, Field, Kind, Length, Message, Page, schema, visuals, widget};
use crate::store::{Edit, Snapshot, set};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum Target {
#[default]
Both,
Light,
Dark,
}
impl Target {
const ALL: [Self; 3] = [Self::Both, Self::Light, Self::Dark];
fn prefix(self) -> &'static str {
match self {
Self::Both => "theme.background",
Self::Light => "theme.light.background",
Self::Dark => "theme.dark.background",
}
}
fn appearance(self, active: Appearance) -> Appearance {
match self {
Self::Both => active,
Self::Light => Appearance::Light,
Self::Dark => Appearance::Dark,
}
}
}
impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Both => "Both modes",
Self::Light => "Light mode",
Self::Dark => "Dark mode",
})
}
}
#[derive(Default)]
pub(super) struct State {
pub target: Target,
active: Preview,
defaults: [Preview; 2],
}
#[derive(Default)]
struct Preview {
path: String,
requested: Option<String>,
handle: Option<widget::image::Handle>,
error: Option<String>,
}
impl Preview {
fn request(&mut self, path: &str) -> bool {
if self.requested.is_some() || self.path == path {
return false;
}
self.handle = None;
self.error = None;
if path.is_empty() {
self.path.clear();
return false;
}
self.path.clear();
self.requested = Some(path.to_owned());
true
}
fn finish(&mut self, path: String, desired: &str, result: Result<widget::image::Handle, String>) {
if self.requested.as_deref() != Some(&path) {
return;
}
self.requested = None;
if path != desired {
return;
}
self.path = path;
match result {
Ok(handle) => {
self.handle = Some(handle);
self.error = None;
}
Err(error) => {
self.handle = None;
self.error = Some(error);
}
}
}
}
fn index(appearance: Appearance) -> usize {
usize::from(appearance == Appearance::Dark)
}
pub(super) fn edits(target: Target, token: &str, value: impl Into<serde_json::Value>) -> Vec<Edit> {
let mut edits = vec![set(&format!("{}.{token}", target.prefix()), value)];
if target == Target::Both {
for kind in ["light", "dark"] {
edits.push(Edit::Unset(format!("theme.{kind}.background.{token}")));
}
}
edits
}
pub(super) fn restore_defaults() -> Vec<Edit> {
[Target::Both, Target::Light, Target::Dark]
.map(|target| Edit::Unset(format!("{}.path", target.prefix())))
.into()
}
fn setting(snapshot: &Snapshot, target: Target, token: &str, fallback: &str) -> String {
snapshot
.item(&format!("{}.{token}", target.prefix()))
.or_else(|| snapshot.item(&format!("theme.background.{token}")))
.and_then(serde_json::Value::as_str)
.unwrap_or(fallback)
.to_owned()
}
fn image_button_style(palette: visuals::Palette, selected: bool) -> cosmic::theme::Button {
let paint = move |hovered: bool, focused: bool| button::Style {
shape: Some(cosmic::iced::border::Shape::Continuous),
background: None,
border_radius: palette.radius.min(10.).into(),
border_width: if selected || focused {
2.
} else if hovered {
1.
} else {
0.
},
border_color: palette.accent,
..Default::default()
};
cosmic::theme::Button::Custom {
active: Box::new(move |focused, _| paint(false, focused)),
hovered: Box::new(move |focused, _| paint(true, focused)),
pressed: Box::new(move |focused, _| paint(true, focused)),
disabled: Box::new(move |_| paint(false, false)),
}
}
fn thumbnail(path: String, builtin: Option<Appearance>) -> Task<Message> {
cosmic::task::future(async move {
let (send, receive) = cosmic::iced::futures::channel::oneshot::channel();
let file = path.clone();
std::thread::spawn(move || {
let result = (|| -> Result<_, String> {
let mut reader = image::ImageReader::open(&file)
.map_err(|e| e.to_string())?
.with_guessed_format()
.map_err(|e| e.to_string())?;
let mut limits = image::Limits::default();
limits.max_alloc = Some(256 * 1024 * 1024);
limits.max_image_width = Some(16384);
limits.max_image_height = Some(16384);
reader.limits(limits);
let (width, height) = if builtin.is_some() { (400, 225) } else { (1000, 500) };
let pixels = reader
.decode()
.map_err(|e| e.to_string())?
.thumbnail(width, height)
.into_rgba8();
Ok(widget::image::Handle::from_rgba(
pixels.width(),
pixels.height(),
pixels.into_raw(),
))
})();
let _ = send.send(result);
});
let result = receive
.await
.unwrap_or_else(|_| Err("Could not load wallpaper preview.".into()));
match builtin {
Some(appearance) => Message::DefaultWallpaperThumbnail(appearance, result),
None => Message::Thumbnail(path, result),
}
})
}
impl App {
pub(super) fn apply_wallpaper_edits(&mut self, edits: Vec<Edit>) -> Task<Message> {
for edit in &edits {
if let Edit::Set(path, _) | Edit::Unset(path) = edit {
self.inputs.remove(path);
}
}
self.edit_many(edits)
}
pub(super) fn active_wallpaper_path(&self) -> String {
if self.path == crate::store::config_path() {
return self
.resolved
.presented
.tokens
.background
.path
.as_ref()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_default();
}
// A separate config preview must not borrow the live desktop's path.
let appearance = match self.current.string("theme.mode", "auto").as_str() {
"light" => Appearance::Light,
"dark" => Appearance::Dark,
_ => self.resolved.theme.appearance,
};
let target = if appearance == Appearance::Light {
Target::Light
} else {
Target::Dark
};
let path = setting(
&self.current,
target,
"path",
ferese_config::default_wallpaper_for(appearance),
);
if path.is_empty() {
return path;
}
ferese_config::theme::theme_path(self.path.parent().unwrap_or(std::path::Path::new(".")), path.as_ref())
.to_string_lossy()
.into_owned()
}
pub(super) fn load_thumbnail(&mut self) -> Task<Message> {
if self.page != Page::Wallpaper {
return Task::none();
}
let mut tasks = Vec::with_capacity(3);
let path = self.active_wallpaper_path();
if self.wallpaper.active.request(&path) {
tasks.push(thumbnail(path, None));
}
for appearance in [Appearance::Light, Appearance::Dark] {
let path = ferese_config::default_wallpaper_for(appearance);
if self.wallpaper.defaults[index(appearance)].request(path) {
tasks.push(thumbnail(path.into(), Some(appearance)));
}
}
Task::batch(tasks)
}
pub(super) fn wallpaper_thumbnail(&mut self, path: String, result: Result<widget::image::Handle, String>) {
let desired = self.active_wallpaper_path();
self.wallpaper.active.finish(path, &desired, result);
}
pub(super) fn default_wallpaper_thumbnail(
&mut self,
appearance: Appearance,
result: Result<widget::image::Handle, String>,
) {
let path = ferese_config::default_wallpaper_for(appearance);
self.wallpaper.defaults[index(appearance)].finish(path.into(), path, result);
}
pub(super) fn wallpaper_fields(&self) -> Vec<Field> {
let active = if self.path == crate::store::config_path() {
self.resolved.presented.appearance
} else {
match self.current.string("theme.mode", "auto").as_str() {
"light" => Appearance::Light,
"dark" => Appearance::Dark,
_ => self.resolved.theme.appearance,
}
};
schema::fields(Page::Wallpaper)
.into_iter()
.map(|mut field| {
field.path = field.path.replace("theme.background", self.wallpaper.target.prefix());
if let Kind::Text { default, .. } = &mut field.kind {
*default = ferese_config::default_wallpaper_for(self.wallpaper.target.appearance(active));
}
field
})
.collect()
}
pub(super) fn wallpaper_field_value(&self, path: &str, fallback: &str) -> String {
if path.starts_with(self.wallpaper.target.prefix()) {
return setting(
&self.draft,
self.wallpaper.target,
path.rsplit('.').next().unwrap(),
fallback,
);
}
self.draft.string(path, fallback)
}
pub(super) fn wallpaper_controls(&self) -> Element<'_, Message> {
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
let preview = &self.wallpaper.active;
let mut body = column([]).spacing(8).push(self.label("Current wallpaper", 13.));
if let Some(handle) = &preview.handle {
body = body.push(
widget::container(
widget::image(handle.clone())
.border_radius(palette.radius.min(10.))
.shape(cosmic::iced::border::Shape::Continuous)
.width(Length::Shrink)
.height(180)
.content_fit(cosmic::iced::ContentFit::Contain),
)
.center_x(Length::Fill),
);
} else {
body = body.push(self.note(if preview.requested.is_some() {
"Loading wallpaper preview…"
} else {
preview.error.as_deref().unwrap_or("No wallpaper image selected.")
}));
}
body = body.push(
row([])
.spacing(8)
.align_y(Alignment::Center)
.push(self.label("Apply to", 12.))
.push(ferese_theme::controls::select(
cosmic::iced::widget::pick_list(Target::ALL, Some(self.wallpaper.target), Message::WallpaperTarget)
.font(self.font)
.text_size(12),
palette,
))
.push(widget::Space::new().width(Length::Fill))
.push(self.settings_button(
"Choose image…",
"M3 4h18v16H3z M3 15l5-5 5 5 3-3 5 5 M15 8h.01",
Some(Message::PickWallpaper),
false,
)),
);
let mut defaults = row([]).spacing(8);
let active = self.active_wallpaper_path();
for appearance in [Appearance::Light, Appearance::Dark] {
let name = if appearance == Appearance::Light {
"Light"
} else {
"Dark"
};
let preview = &self.wallpaper.defaults[index(appearance)];
let image: Element<'_, Message> = if let Some(handle) = &preview.handle {
widget::image(handle.clone())
.border_radius(palette.radius.min(10.))
.shape(cosmic::iced::border::Shape::Continuous)
.width(Length::Fill)
.height(110)
.content_fit(cosmic::iced::ContentFit::Cover)
.into()
} else {
widget::container(self.note(preview.error.as_deref().unwrap_or("Loading…")))
.width(Length::Fill)
.height(110)
.into()
};
let label_color = if preview.handle.is_none() {
palette.text
} else if appearance == Appearance::Light {
cosmic::iced::Color::from_rgb8(24, 33, 44)
} else {
cosmic::iced::Color::WHITE
};
let label = widget::container(self.label(name, 12.).class(cosmic::theme::Text::Color(label_color)))
.padding(8)
.width(Length::Fill)
.height(Length::Fill);
let tile = cosmic::iced::widget::stack([image, label.into()]);
defaults = defaults.push(
button::custom(tile)
.name(format!("Use default {name} wallpaper"))
.padding(0)
.width(Length::Fill)
.class(image_button_style(
palette,
active == ferese_config::default_wallpaper_for(appearance),
))
.on_press(Message::UseDefaultWallpaper(appearance)),
);
}
body.push(
row([])
.spacing(8)
.align_y(Alignment::Center)
.push(self.label("Default wallpapers", 13.))
.push(widget::Space::new().width(Length::Fill))
.push(self.settings_button(
"Match appearance",
"M20 7v5h-5 M4 17v-5h5 M6 7a7 7 0 0 1 12-2l2 2 M18 17a7 7 0 0 1-12 2l-2-2",
Some(Message::RestoreDefaultWallpapers),
false,
)),
)
.push(defaults)
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "release UI preview; requires headless renderer backends"]
fn render_preview_pages_on_both_backends() {
use cosmic::Application;
use cosmic::iced::advanced::renderer::{Headless, Renderer as _};
use cosmic::iced::advanced::{Layout, layout, mouse, renderer, widget::Tree};
use cosmic::iced::{Color, Font, Pixels, Rectangle, Size};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut app = App::init(
cosmic::app::Core::default(),
("/preview/config.kdl".into(), Snapshot::parse(String::new()), None),
)
.0;
for appearance in [Appearance::Light, Appearance::Dark] {
let path = ferese_config::default_wallpaper_for(appearance);
let pixels = image::open(path).unwrap().thumbnail(400, 225).into_rgba8();
let handle = widget::image::Handle::from_rgba(pixels.width(), pixels.height(), pixels.into_raw());
assert!(app.wallpaper.defaults[index(appearance)].request(path));
app.default_wallpaper_thumbnail(appearance, Ok(handle));
}
for backend in ["wgpu", "tiny-skia"] {
let mut renderer = runtime
.block_on(<cosmic::Renderer as Headless>::new(
Font::default(),
Pixels(14.),
Some(backend),
))
.expect("requested renderer");
for appearance in [Appearance::Light, Appearance::Dark] {
let name = if appearance == Appearance::Light {
"light"
} else {
"dark"
};
app.current = Snapshot::parse(format!("theme {{ mode \"{name}\"; }}")).unwrap();
app.draft = app.current.clone();
app.visible_rows = (0..app.resolved.families.len().div_ceil(3))
.map(|row| ("theme", row))
.collect();
let pixels = image::open(ferese_config::default_wallpaper_for(appearance))
.unwrap()
.thumbnail(1000, 500)
.into_rgba8();
app.wallpaper.active.handle = Some(widget::image::Handle::from_rgba(
pixels.width(),
pixels.height(),
pixels.into_raw(),
));
for page in [Page::Wallpaper, Page::Appearance] {
app.page = page;
let bounds = Rectangle::with_size(Size::new(1000., 780.));
let theme = app.native_palette.native_theme();
let mut view = app.page_view();
let mut tree = Tree::new(view.as_widget());
let node = view.as_widget_mut().layout(
&mut tree,
&renderer,
&layout::Limits::new(Size::ZERO, bounds.size()),
);
renderer.reset(bounds);
view.as_widget().draw(
&tree,
&mut renderer,
&theme,
&renderer::Style {
text_color: Color::WHITE,
icon_color: Color::WHITE,
scale_factor: 1.25,
},
Layout::new(&node),
mouse::Cursor::Unavailable,
&bounds,
);
let pixels = Headless::screenshot(
&mut renderer,
Size::new(1250, 975),
1.25,
theme.cosmic().bg_color().into(),
);
let preview = if page == Page::Wallpaper { "wallpaper" } else { "theme" };
let path = format!("/tmp/ferese-{preview}-{backend}-{name}.png");
image::save_buffer(&path, &pixels, 1250, 975, image::ColorType::Rgba8).unwrap();
eprintln!("Preview page: {path}");
}
}
}
}
#[test]
fn selecting_both_removes_mode_overrides_without_changing_lock_wallpaper() {
let mut snapshot = Snapshot::parse(
"theme { background { lock-path \"/lock.png\"; }; light { background { path \"/light.png\"; }; }; dark { background { path \"/dark.png\"; }; }; }".into(),
).unwrap();
for edit in edits(Target::Both, "path", "/shared.png") {
snapshot.edit(&edit).unwrap();
}
assert_eq!(snapshot.string("theme.background.path", ""), "/shared.png");
assert!(snapshot.item("theme.light.background.path").is_none());
assert!(snapshot.item("theme.dark.background.path").is_none());
assert_eq!(snapshot.string("theme.background.lock_path", ""), "/lock.png");
}
#[test]
fn selecting_one_mode_preserves_the_other_and_inherits_shared_settings() {
let mut snapshot =
Snapshot::parse("theme { background { path \"/shared.png\"; mode \"fit\"; }; }".into()).unwrap();
for edit in edits(Target::Light, "path", "/light.png") {
snapshot.edit(&edit).unwrap();
}
assert_eq!(setting(&snapshot, Target::Light, "path", ""), "/light.png");
assert_eq!(setting(&snapshot, Target::Dark, "path", ""), "/shared.png");
assert_eq!(setting(&snapshot, Target::Light, "mode", "fill"), "fit");
}
#[test]
fn restoring_defaults_keeps_placement_and_lock_image() {
let mut snapshot = Snapshot::parse("theme { background { path \"/custom.png\"; lock-path \"/lock.png\"; mode \"fit\"; }; light { background { path \"/light.png\"; }; }; }".into()).unwrap();
for edit in restore_defaults() {
snapshot.edit(&edit).unwrap();
}
for target in Target::ALL {
assert!(snapshot.item(&format!("{}.path", target.prefix())).is_none());
}
assert_eq!(snapshot.string("theme.background.mode", ""), "fit");
assert_eq!(snapshot.string("theme.background.lock_path", ""), "/lock.png");
}
#[test]
fn returning_to_a_previous_path_during_a_load_restarts_its_preview() {
let mut preview = Preview::default();
let image = widget::image::Handle::from_rgba(1, 1, vec![0, 0, 0, 255]);
assert!(preview.request("/first.png"));
preview.finish("/first.png".into(), "/first.png", Ok(image.clone()));
assert!(preview.request("/second.png"));
preview.finish("/second.png".into(), "/first.png", Ok(image));
assert!(preview.request("/first.png"));
}
#[test]
fn stale_preview_completion_does_not_replace_the_active_wallpaper() {
let mut preview = Preview::default();
assert!(preview.request("/old.png"));
let image = widget::image::Handle::from_rgba(1, 1, vec![0, 0, 0, 255]);
preview.finish("/old.png".into(), "/new.png", Ok(image.clone()));
assert!(preview.handle.is_none());
assert!(preview.request("/new.png"));
preview.finish("/old.png".into(), "/new.png", Ok(image.clone()));
assert_eq!(preview.requested.as_deref(), Some("/new.png"));
preview.finish("/new.png".into(), "/new.png", Ok(image));
assert!(preview.handle.is_some());
assert!(!preview.request("/new.png"));
}
}
+17
View File
@@ -35,3 +35,20 @@ The site uses relative links, including for handbook search, so it also works un
For deployment setup, follow [GitHub’s custom workflow For deployment setup, follow [GitHub’s custom workflow
documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages). documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages).
## Brand assets
The Ferese symbols live in `assets/branding`. The website uses
`ferese-blue-dark.svg`. The README chooses the light or dark SVG to match
the reader's appearance and uses the flat blue SVG as its fallback. Keep their
1024×1024 canvas square so the symbol and glow are not stretched or cropped.
The favicon uses the flat blue symbol. `site/assets/favicon-32.png` is 32×32;
`site/assets/favicon.ico` contains 16×16, 32×32 and 48×48 images. These files are
checked in, so building the site needs no icon renderer. Their links work at
both the site root and the GitHub Pages project path.
The light and dark wallpapers live in `assets/wallpapers`. Both are
3840×2160 and are bundled by the installer. Ferese selects the matching image
when no custom wallpaper is set. Settings keeps both defaults available below
the current wallpaper preview. Existing screenshots show the previous artwork.
Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+93
View File
@@ -0,0 +1,93 @@
Copyright 2022 The Figtree Project Authors (https://github.com/erikdkennedy/figtree)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+14 -8
View File
@@ -12,17 +12,20 @@
<meta property="og:image" content="https://ferese-wm.github.io/ferese/assets/screenshots/ferese-main-hero.png" /> <meta property="og:image" content="https://ferese-wm.github.io/ferese/assets/screenshots/ferese-main-hero.png" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<title>Ferese — Wayland desktop</title> <title>Ferese — Wayland desktop</title>
<link rel="icon" href="assets/ferese.svg" type="image/svg+xml" /> <link rel="icon" href="assets/branding/ferese-blue-flat.svg" type="image/svg+xml" />
<link rel="stylesheet" href="styles.css?v=jetbrains" /> <link rel="icon" href="assets/favicon.ico" sizes="16x16 32x32 48x48" />
<link rel="icon" href="assets/favicon-32.png" type="image/png" sizes="32x32" />
<link rel="stylesheet" href="styles.css?v=figtree" />
</head> </head>
<body> <body>
<a class="skip" href="#main">Skip to content</a> <a class="skip" href="#main">Skip to content</a>
<header class="site-header"> <header class="site-header">
<div class="header-inner"> <div class="header-inner">
<a class="wordmark" href="./" aria-label="Ferese home"><svg viewBox="0 0 24 24" aria-hidden="true"> <a class="brand" href="./" aria-label="Ferese home">
<path d="M3 3h6v18H3zM11 3h10v6H11zM11 11h7v6h-7z" /> <img class="brand-icon" src="assets/branding/ferese-blue-dark.svg" width="1024" height="1024" alt="" />
</svg>ferese</a> <span class="brand-name">Ferese</span>
</a>
<nav aria-label="Main navigation"> <nav aria-label="Main navigation">
<a href="./" aria-current="page">Overview</a><a href="docs/screenshots.html">Screenshots</a><a <a href="./" aria-current="page">Overview</a><a href="docs/screenshots.html">Screenshots</a><a
href="docs/">Documentation</a><a href="https://github.com/ferese-wm/ferese">GitHub <svg class="link-arrow" href="docs/">Documentation</a><a href="https://github.com/ferese-wm/ferese">GitHub <svg class="link-arrow"
@@ -35,6 +38,7 @@
</header> </header>
<main id="main" class="simple-home"> <main id="main" class="simple-home">
<section class="product-intro page-width"> <section class="product-intro page-width">
<img class="intro-icon" src="assets/branding/ferese-blue-dark.svg" width="1024" height="1024" alt="" />
<h1 class="product-tagline"> <h1 class="product-tagline">
A configurable Wayland desktop. A configurable Wayland desktop.
</h1> </h1>
@@ -55,10 +59,10 @@
<figure class="product-screenshot page-width"> <figure class="product-screenshot page-width">
<a href="assets/screenshots/ferese-main-hero.png" aria-label="View the full-resolution Ferese desktop screenshot"> <a href="assets/screenshots/ferese-main-hero.png" aria-label="View the full-resolution Ferese desktop screenshot">
<img src="assets/screenshots/ferese-main-hero.png" width="2880" height="1800" <img src="assets/screenshots/ferese-main-hero.png" width="2880" height="1800"
alt="Ferese Blue desktop with Settings, Control Center and the default wallpaper" fetchpriority="high" /> alt="Ferese Blue desktop with Settings, Control Center and the previous default wallpaper" fetchpriority="high" />
</a> </a>
<figcaption> <figcaption>
Ferese Blue with Settings, Control Center, and the default wallpaper. Ferese Blue with Settings, Control Center, and the previous default wallpaper.
</figcaption> </figcaption>
</figure> </figure>
<section class="product-features page-width" aria-label="Features"> <section class="product-features page-width" aria-label="Features">
@@ -111,7 +115,9 @@
</section> </section>
</main> </main>
<footer class="site-footer page-width"> <footer class="site-footer page-width">
<a class="wordmark" href="./">ferese</a> <a class="brand" href="./" aria-label="Ferese home">
<img class="brand-icon" src="assets/branding/ferese-blue-dark.svg" width="1024" height="1024" alt="" />
</a>
<p>A Wayland desktop with scrolling layouts.</p> <p>A Wayland desktop with scrolling layouts.</p>
<a href="docs/">Read the handbook <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" <a href="docs/">Read the handbook <svg class="link-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
+72 -44
View File
@@ -1,3 +1,11 @@
@font-face {
font-family: "Figtree";
src: url("assets/fonts/Figtree-Variable.ttf") format("truetype");
font-style: normal;
font-weight: 300 900;
font-display: swap;
}
@font-face { @font-face {
font-family: "JetBrains Mono"; font-family: "JetBrains Mono";
src: url("assets/fonts/JetBrainsMono-Regular.ttf") format("truetype"); src: url("assets/fonts/JetBrainsMono-Regular.ttf") format("truetype");
@@ -16,7 +24,7 @@
--soft: #1c2838; --soft: #1c2838;
--mono: --mono:
"JetBrains Mono", ui-monospace, "SFMono-Regular", Consolas, monospace; "JetBrains Mono", ui-monospace, "SFMono-Regular", Consolas, monospace;
--sans: var(--mono); --sans: "Figtree", system-ui, -apple-system, "Segoe UI", sans-serif;
color-scheme: dark; color-scheme: dark;
} }
* { * {
@@ -30,7 +38,7 @@ body {
margin: 0; margin: 0;
background: var(--paper); background: var(--paper);
color: var(--ink); color: var(--ink);
font: 16px/1.65 var(--sans); font: 17px/1.65 var(--sans);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
a { a {
@@ -80,28 +88,33 @@ dl {
height: 88px; height: 88px;
gap: 30px; gap: 30px;
} }
.wordmark { .brand {
font-size: 29px;
font-weight: 750;
letter-spacing: -1.4px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px;
} }
.wordmark svg { .site-header .brand {
width: 26px; gap: 8px;
height: 26px; flex-shrink: 0;
fill: var(--blue); }
.brand-name {
font-size: 20px;
font-weight: 600;
letter-spacing: -0.02em;
}
.brand-icon {
width: 56px;
height: auto;
} }
.site-header nav { .site-header nav {
display: flex; display: flex;
gap: 36px; gap: 36px;
align-items: center; align-items: center;
font-size: 13px; font-size: 14px;
} }
.site-header nav a { .site-header nav a {
padding: 10px 0; padding: 10px 0;
position: relative; position: relative;
white-space: nowrap;
} }
.site-header nav a[aria-current="page"]:after { .site-header nav a[aria-current="page"]:after {
content: ""; content: "";
@@ -169,7 +182,7 @@ h1 span {
background: var(--ink); background: var(--ink);
color: #fff; color: #fff;
padding: 13px 19px; padding: 13px 19px;
font-size: 13px; font-size: 14px;
transition: background 0.15s; transition: background 0.15s;
} }
.button:hover { .button:hover {
@@ -407,12 +420,12 @@ kbd {
padding-block: 35px; padding-block: 35px;
} }
.site-footer p { .site-footer p {
font-size: 11px; font-size: 12px;
color: var(--muted); color: var(--muted);
} }
.site-footer > a:last-child { .site-footer > a:last-child {
margin-left: auto; margin-left: auto;
font-size: 11px; font-size: 12px;
} }
.skip { .skip {
position: fixed; position: fixed;
@@ -471,7 +484,7 @@ kbd {
gap: 3px; gap: 3px;
} }
.docs-sidebar nav a { .docs-sidebar nav a {
font-size: 13px; font-size: 14px;
padding: 8px 12px; padding: 8px 12px;
border-left: 2px solid transparent; border-left: 2px solid transparent;
} }
@@ -510,7 +523,7 @@ kbd {
line-height: 1.15; line-height: 1.15;
} }
.prose { .prose {
font-size: 14px; font-size: 16px;
line-height: 1.9; line-height: 1.9;
} }
.prose > p { .prose > p {
@@ -518,7 +531,7 @@ kbd {
color: #c2ccd9; color: #c2ccd9;
} }
.prose > p:first-of-type { .prose > p:first-of-type {
font-size: 16px; font-size: 17px;
line-height: 1.85; line-height: 1.85;
color: var(--muted); color: var(--muted);
} }
@@ -633,7 +646,7 @@ kbd {
gap: 12px; gap: 12px;
} }
.docs-toc a { .docs-toc a {
font-size: 11px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
color: var(--muted); color: var(--muted);
border-left: 1px solid var(--line); border-left: 1px solid var(--line);
@@ -756,18 +769,15 @@ kbd {
} }
.header-inner { .header-inner {
height: 74px; height: 74px;
gap: 18px; gap: 12px;
} }
.wordmark { .brand-icon {
font-size: 25px; width: 48px;
gap: 6px; height: auto;
}
.wordmark svg {
width: 23px;
} }
.site-header nav { .site-header nav {
gap: 18px; gap: 12px;
font-size: 11px; font-size: 12px;
} }
.site-header nav a:first-child { .site-header nav a:first-child {
display: none; display: none;
@@ -915,10 +925,10 @@ kbd {
padding-block: 26px; padding-block: 26px;
} }
.site-footer p { .site-footer p {
font-size: 10px; font-size: 12px;
} }
.site-footer > a:last-child { .site-footer > a:last-child {
font-size: 10px; font-size: 12px;
} }
.docs-layout { .docs-layout {
width: calc(100% - 36px); width: calc(100% - 36px);
@@ -946,7 +956,7 @@ kbd {
gap: 0; gap: 0;
} }
.docs-sidebar nav a { .docs-sidebar nav a {
font-size: 12px; font-size: 13px;
padding: 6px 10px; padding: 6px 10px;
} }
.sidebar-links { .sidebar-links {
@@ -963,7 +973,7 @@ kbd {
letter-spacing: -1px; letter-spacing: -1px;
} }
.prose { .prose {
font-size: 14px; font-size: 16px;
} }
.prose h2 { .prose h2 {
font-size: 25px; font-size: 25px;
@@ -979,6 +989,19 @@ kbd {
scroll-margin-top: 20px; scroll-margin-top: 20px;
} }
} }
@media (max-width: 480px) {
.header-inner {
height: auto;
min-height: 74px;
flex-wrap: wrap;
gap: 4px;
padding-block: 10px;
}
.site-header nav {
width: 100%;
justify-content: space-between;
}
}
@media (max-width: 380px) { @media (max-width: 380px) {
h1 { h1 {
font-size: 47px; font-size: 47px;
@@ -1018,10 +1041,15 @@ kbd {
} }
.product-intro { .product-intro {
text-align: center; text-align: center;
padding-block: 48px 34px; padding-block: 24px 34px;
}
.intro-icon {
width: 128px;
height: auto;
margin: 0 auto;
} }
.product-tagline { .product-tagline {
font-size: 18px; font-size: 20px;
font-weight: 400; font-weight: 400;
letter-spacing: normal; letter-spacing: normal;
line-height: 1.5; line-height: 1.5;
@@ -1030,7 +1058,7 @@ kbd {
color: var(--muted); color: var(--muted);
max-width: 600px; max-width: 600px;
margin: 13px auto 0; margin: 13px auto 0;
font-size: 15px; font-size: 17px;
} }
.product-links { .product-links {
display: flex; display: flex;
@@ -1057,7 +1085,7 @@ kbd {
background: var(--soft); background: var(--soft);
} }
.source-link { .source-link {
font-size: 13px; font-size: 14px;
color: var(--muted); color: var(--muted);
margin-left: 5px; margin-left: 5px;
} }
@@ -1069,7 +1097,7 @@ kbd {
.product-screenshot figcaption { .product-screenshot figcaption {
justify-content: center; justify-content: center;
padding-top: 10px; padding-top: 10px;
font-size: 11px; font-size: 12px;
} }
.product-features { .product-features {
display: grid; display: grid;
@@ -1080,7 +1108,7 @@ kbd {
.product-features h2, .product-features h2,
.product-motion h2, .product-motion h2,
.product-getting-started h2 { .product-getting-started h2 {
font-size: 20px; font-size: 22px;
font-weight: 600; font-weight: 600;
letter-spacing: -0.4px; letter-spacing: -0.4px;
line-height: 1.4; line-height: 1.4;
@@ -1088,7 +1116,7 @@ kbd {
.product-features p, .product-features p,
.product-motion p, .product-motion p,
.product-getting-started p { .product-getting-started p {
font-size: 14px; font-size: 16px;
color: var(--muted); color: var(--muted);
margin-top: 12px; margin-top: 12px;
line-height: 1.8; line-height: 1.8;
@@ -1096,7 +1124,7 @@ kbd {
.product-features a, .product-features a,
.product-motion a { .product-motion a {
display: inline-block; display: inline-block;
font-size: 12px; font-size: 14px;
color: var(--blue); color: var(--blue);
margin-top: 12px; margin-top: 12px;
} }
@@ -1119,20 +1147,20 @@ kbd {
padding-block: 35px 27px; padding-block: 35px 27px;
} }
.product-tagline { .product-tagline {
font-size: 17px; font-size: 19px;
} }
.product-description { .product-description {
font-size: 14px; font-size: 16px;
} }
.product-links { .product-links {
gap: 10px; gap: 10px;
} }
.product-links .button { .product-links .button {
padding: 10px 15px; padding: 10px 15px;
font-size: 12px; font-size: 13px;
} }
.source-link { .source-link {
font-size: 12px; font-size: 13px;
} }
.product-features { .product-features {
grid-template-columns: 1fr; grid-template-columns: 1fr;