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.
No files matched your search
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 1.8 MiB |
@@ -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 |
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 |
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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 |
@@ -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.
|
||||||
|
|
||||||
[](images/screenshots/ferese-main-hero.png)
|
[](images/screenshots/ferese-main-hero.png)
|
||||||
|
|
||||||
## Rosé Pine
|
## Rosé Pine
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
@@ -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}')
|
||||||
@@ -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'}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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, ¤t);
|
let family_sections = theme_controls::FamilySections::new(&resolved.families, ¤t);
|
||||||
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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
After Width: | Height: | Size: 784 B |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -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.
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||