Add customizable theme color (#582)
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 69 KiB |
1
code/frontend/public/icons/ext/apprise-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M433.8 75.1C386.3 26.7 323.2 0 256 0S125.7 26.7 78.2 75.1C30.8 123.4 4.7 187.7 4.7 256s26.1 132.6 73.5 180.9C125.7 485.3 188.8 512 256 512s130.3-26.7 177.8-75.1c47.4-48.3 73.5-112.6 73.5-180.9s-26.1-132.6-73.5-180.9m-76.9 98.1c-7.4-13.4-21.5-19.8-37.8-17-5.6.9-10.9-2.8-11.9-8.4-.9-5.6 2.8-10.9 8.4-11.9 25-4.2 47.7 6.3 59.3 27.4 11.4 20.8 8.4 45.8-7.7 63.7-2 2.3-4.8 3.4-7.7 3.4-2.5 0-4.9-.9-6.9-2.6-4.2-3.8-4.6-10.3-.8-14.5 10.3-11.3 12.2-27 5.1-40.1M113.1 365.1c-28.2 0-51.1-22.2-51.1-49.6 0-14.7 6.6-27.9 17.1-37 16.2 27.7 33.8 56.4 50.1 84.1-5.2 1.6-10.5 2.5-16.1 2.5m121.8 54-21.2 12.2c-7.7 4.5-17.6 1.8-22.1-5.9l-30.1-52.1 49.2-28.4 30.1 52.1c4.5 7.8 1.8 17.7-5.9 22.1M150 352.4c-16.8-29-35.8-59.7-52.6-88.7 28.8-15.5 85.8-44.5 118.5-108.4 7.2-14.1 10.9-28.8 15.7-40.9 1.5-3.9 4.7-6.9 8.7-8 1.4-.4 3.1-.6 4.9-.4 4.4.4 8.6 2.8 11 7l103.3 177.7c.2.3.3.6.5.9 4.8 9.8-3.9 21-14.7 19.3-15.3-2.4-31.9-6.7-51.3-6.8-69.4-.5-116.4 34.5-144 48.3m284.4-164.2c-1.9 22.8-9.9 38-22.3 55.2-2.1 2.9-5.4 4.5-8.8 4.5-2.2 0-4.4-.7-6.3-2-4.8-3.5-6-10.2-2.5-15.1 10.4-14.6 16.8-26.5 18.3-44.4 1.8-22.3-5.8-42.6-21.4-57-17-15.7-41.8-22.8-66.2-18.8-5.9 1-11.4-3.1-12.4-8.9-1-5.9 3.1-11.4 8.9-12.4 31-5 62.5 4.1 84.4 24.3 20.7 19 30.7 45.6 28.3 74.6"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
code/frontend/public/icons/ext/discord-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M433.7 91a416.5 416.5 0 0 0-105.6-33.2c-4.6 8.2-9.9 19.3-13.5 28.1-39.4-5.9-78.4-5.9-117.1 0-3.7-8.8-9.1-19.9-13.7-28.1-37.1 6.4-72.6 17.7-105.7 33.3-66.8 101-85 199.5-75.9 296.6 44.3 33.1 87.3 53.2 129.6 66.4 10.4-14.4 19.7-29.6 27.7-45.7-15.3-5.8-29.9-13-43.7-21.3 3.7-2.7 7.2-5.6 10.7-8.5 84.2 39.4 175.8 39.4 259 0 3.5 2.9 7.1 5.8 10.7 8.5-13.9 8.3-28.5 15.5-43.8 21.3 8 16 17.3 31.3 27.7 45.7 42.3-13.2 85.3-33.3 129.6-66.4 10.8-112.5-18-210.1-76-296.7M170.9 328c-25.3 0-46-23.6-46-52.4s20.3-52.4 46-52.4 46.5 23.6 46 52.4c.1 28.8-20.2 52.4-46 52.4m170.2 0c-25.3 0-46-23.6-46-52.4s20.3-52.4 46-52.4 46.5 23.6 46 52.4c0 28.8-20.3 52.4-46 52.4"/></svg>
|
||||
|
After Width: | Height: | Size: 747 B |
1
code/frontend/public/icons/ext/gotify-dark.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
1
code/frontend/public/icons/ext/lidarr-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M291.1 190.6c-9.5-5.2-20.3-8.3-31.7-8.9V20.9L83 322.9h136.7c9.5 5.2 20.3 8.3 31.7 8.9v160.8l16.2-27.7 160.2-274.3zm-39.7-8.9c-39.7 2.1-71.2 34.9-71.2 75.1 0 23.4 10.7 44.4 27.5 58.1H97L251.7 49.2zm7.5 283.5.4-133.4c39.7-2.1 71.2-34.9 71.2-75.1 0-23.4-10.7-44.4-27.5-58.1h110.8zm34.2-282.6c-8.1-4.1-16.7-6.9-25.7-8.2v-71.6c36.7 2.8 70.8 18.4 97.2 44.8 10.5 10.5 19.3 22.3 26.3 35zm-75.4 148.3c8.1 4.1 16.7 6.9 25.7 8.2v71.6c-36.7-2.8-70.8-18.4-97.2-44.8a156 156 0 0 1-26.3-35zm97.5 68.2L408.7 239c.7 5.8 1 11.8 1 17.7 0 41.2-16.1 80-45.2 109.2-14.3 14.4-31.1 25.6-49.3 33.2M512 256c0 141.4-114.6 256-256 256-2.2 0-4.4 0-6.6-.1l23-39.3c23.2-1.8 45.9-7.3 67.3-16.4 25.8-10.9 48.9-26.5 68.8-46.4s35.5-43 46.4-68.8c11.3-26.7 17-55.1 17-84.3s-5.7-57.6-17-84.3c-10.9-25.8-26.5-48.9-46.4-68.8s-43-35.5-68.8-46.4c-23-9.7-47.3-15.3-72.3-16.7V.3C403.5 6.2 512 118.4 512 256M171.1 456.3c23 9.7 47.3 15.3 72.3 16.7v38.7C107.8 505.1 0 393.1 0 256 0 114.6 114.6 0 256 0c2.1 0 4.2 0 6.3.1l-23.8 40.8c-23.2 1.8-45.9 7.3-67.3 16.4-25.8 10.9-48.9 26.5-68.8 46.4s-35.5 43-46.4 68.8c-11.3 26.7-17 55.1-17 84.3s5.7 57.6 17 84.3c10.9 25.8 26.5 48.9 46.4 68.8 19.8 19.8 42.9 35.4 68.7 46.4m24.4-341.9L102 274.5c-.7-5.8-1-11.8-1-17.7 0-41.2 16.1-80 45.2-109.2 14.4-14.4 31.1-25.6 49.3-33.2"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
code/frontend/public/icons/ext/notifiarr-dark.svg
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
1
code/frontend/public/icons/ext/ntfy-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M443.1 32.7h-365C40.9 32.7 9 62 9 99.2l.4 311.2-9.4 69 127.1-33.8H443c37.2 0 69.1-29.3 69.1-66.5V99.2c0-37.2-31.9-66.5-69-66.5m22 346.3c0 10-9 19.8-22.1 19.6H120.2l-64.6 19.5.7-3.8-.4-315.1c0-10.1 9.1-19.6 22.2-19.6H443c13.1 0 22.1 9.5 22.1 19.6zM110.5 139.7l124.6 67.9V254l-116.4 63.3-8.2 4.5v-50.1l76.6-40.6.5-.2-.5-.2-76.6-40.6zm158.2 152.4h132.4v46H268.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 460 B |
1
code/frontend/public/icons/ext/pushover-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M428.7 67C324.3-28.4 162.4-21.1 67 83.3S-21.1 349.6 83.3 445s266.3 88.1 361.7-16.3S533.1 162.4 428.7 67m-43 115.7c-3.1 13.2-8.9 26.6-17.5 39.9-8.6 13.4-19.4 25.5-32.3 36.3-13 10.8-27.8 19.6-44.6 26.4s-34.6 10.1-53.4 10.1h-2.1L182 415.9h-60.8l119.6-268.7 64.2-8.5-62.5 141.1c11-.8 21.8-4.6 32.3-11.2 10.6-6.6 20.3-14.9 29.2-24.9s16.5-21.1 23-33.4 11.1-24.3 13.9-36.1c1.7-7.3 2.5-14.4 2.3-21.1-.1-6.8-1.9-12.7-5.3-17.7s-8.5-9.2-15.4-12.3-16.3-4.6-28.1-4.6c-13.8 0-27.4 2.3-40.8 6.8s-25.8 11.1-37.2 19.7-21.3 19.3-29.8 32.1-14.5 27.4-18.2 43.7c-1.4 5.4-2.3 9.6-2.5 12.9-.3 3.2-.4 5.9-.2 8 .1 2.1.4 3.7.8 4.9.4 1.1.8 2.3 1.1 3.4q-21.6 0-31.5-8.7c-9.9-8.7-8.2-15.8-4.9-30.2 3.4-14.9 11.1-29.2 23-42.7 12-13.5 26.2-25.4 42.7-35.7s34.5-18.4 54.1-24.5 38.7-9.1 57.3-9.1c16.3 0 30.1 2.3 41.2 7s19.8 10.8 26 18.4 10.1 16.5 11.6 26.6c1.6 10 1.1 20.6-1.4 31.6" style="fill-rule:evenodd;clip-rule:evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 992 B |
1
code/frontend/public/icons/ext/radarr-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="m80.3 80.8 3.9 372.4c-31.4 3.9-54.9-11.8-54.9-43.1l-3.9-309.7c0-98 90.2-121.5 145.1-82.3l278.3 160.7c39.2 27.4 47 78.4 27.4 113.7-3.9-27.4-15.7-43.1-39.2-58.8L123.4 57.2C99.9 41.6 80.3 45.5 80.3 80.8m-23.5 392c23.5 7.8 47 3.9 66.6-7.8l321.5-188.2c19.6 27.4 15.7 54.9-7.8 70.6L166.5 504.2c-39.2 19.6-90.1 0-109.7-31.4M150.9 363 343 253.3 154.8 147.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 450 B |
1
code/frontend/public/icons/ext/readarr-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M255.2.4C114 .4-.4 114.8-.4 256S114 511.6 255.2 511.6 510.8 397.2 510.8 256 396.4.4 255.2.4m156.2 411.8c-20.3 20.3-43.9 36.2-70.2 47.3-27.2 11.5-56.2 17.4-86 17.4s-58.8-5.8-86-17.4c-26.3-11.1-49.9-27.1-70.2-47.3-20.3-20.3-36.2-43.9-47.3-70.2-11.5-27.2-17.4-56.2-17.4-86s5.8-58.8 17.4-86c11.1-26.3 27.1-49.9 47.3-70.2 20.3-20.3 43.9-36.2 70.2-47.3 27.2-11.5 56.2-17.4 86-17.4s58.8 5.8 86 17.4c26.3 11.1 49.9 27.1 70.2 47.3 20.3 20.3 36.2 43.9 47.3 70.2 11.5 27.2 17.4 56.2 17.4 86s-5.8 58.8-17.4 86c-11 26.3-27 49.9-47.3 70.2m-95.9-266.1c-15 4.8-32.2 11.8-46.3 21.6-.2.2-.4.3-.6.4-.3.2-.7.4-2 1.4l-1.2.8c-6.7 4.1-9.8 10.2-9.4 14.2v1.1c0 1.2-.1 3-.1 5.3-.1 4.5-.2 11-.3 19-.2 9.9-.4 22-.5 35.3-.1-11.3-.2-21.6-.3-30.2-.1-7.7-.2-14.1-.2-18.5 0-2.2-.1-4-.1-5.2v-1.8c0-.2 0-.5-.1-.9-.1-.5-.2-.9-.3-1.4-1.3-5-5.6-15.9-13.5-19.4-.1-.1-.2-.2-.4-.3-14.1-9.8-31.3-16.8-46.4-21.6 18.5-10.3 39.3-15.6 60.9-15.6 21.5.2 42.3 5.5 60.8 15.8M449.2 256c0-32-7.7-62.2-21.5-88.8 0-1.9.1-3.3.1-4.1l.1-.1v-1c-.3-24.2-7.2-26.6-15.5-27.1-.7 0-1.4-.1-2-.2h-.4c-1.1.1-2.2.1-3.2.2C371.2 90.5 316.5 62 255.2 62c-61.5 0-116.4 28.7-151.9 73.4-1.3-.1-2.7-.2-4-.2h-.4c-.6.1-1.3.1-2 .2-8.3.5-15.2 2.9-15.5 27.1v1l.1.1c0 1.1.1 3.1.1 6-13 26.1-20.3 55.4-20.3 86.5 0 32.8 8.2 63.7 22.5 90.8.1 8.1.2 15.8.3 22.7 0 2.9 1.5 8.5 7.2 8.9.3.1.8.2 1.6.3 4 .7 8.9 1.7 14.5 2.9C143 423.5 196 450 255.2 450s112.2-26.5 147.8-68.3c5.1-1.1 9.6-2 13.3-2.6.8-.1 1.3-.2 1.6-.3 5.7-.4 7.2-6 7.2-8.9.1-6.3.2-13.3.3-20.6 15.2-27.7 23.8-59.5 23.8-93.3m-383.1 0c0-26.8 5.6-52.2 15.7-75.3v.5q-.45 4.2-.6 9.6v.5c.3 6.4 1.5 84 2.4 144-11.2-24.1-17.5-51-17.5-79.3m185.7 172.9v-9c0-3.6 0-8.2-.1-13.7.3-1.5.4-3.4.1-5.6v-9c0-5.6-.1-13.6-.1-23.9-.1-19.1-.3-44.7-.6-72.3-.2-27.5-.5-53.2-.7-72.3-.1-10.3-.2-18.3-.3-23.9 0-2.9-.1-5.2-.1-6.7 0-.8 0-1.4-.1-1.9v-.5c0-.1 0-.2-.1-.4 0-.2-.1-.4-.1-.5-.8-3.3-4.5-14.1-11.4-16.9-.2-.1-.5-.3-.8-.5-16.2-11.3-54.1-30.4-128.2-35.9C144 93.8 196.5 67 255.2 67c58.4 0 110.7 26.6 145.4 68.4-74.6 5.4-112.6 24.7-128.9 36.1-.3.2-.6.4-.8.5-6.9 2.8-10.6 13.6-11.4 16.9-.1.1-.1.3-.1.5s-.1.3-.1.4v.4c0 .5 0 1.1-.1 2 0 1.6-.1 3.8-.1 6.7-.1 5.6-.2 13.6-.3 23.9-.2 19.1-.5 44.9-.7 72.5s-.5 53.3-.6 72.4c-.1 10.3-.1 18.3-.1 23.9v9c-.3 2-.2 3.7 0 5.2 0 5.8-.1 10.7-.1 14.4v9c-1 6.9 1.6 9.8 4 10.9 1 .5 2.3.8 3.4.8.8 0 1.4-.1 1.8-.4 42-32.5 94.6-49.1 128.2-57-34.6 37.8-84.3 61.6-139.5 61.6s-104.9-23.7-139.5-61.5c33.6 8 85.4 24.6 126.9 56.6.4.3 1 .4 1.8.4 1.1 0 2.3-.3 3.4-.8 2.3-1.2 4.9-4 4-10.9m176.1-237.7v-.6c0-3.4-.2-6.4-.5-9 0-1.2 0-2.3.1-3.4 10.8 23.8 16.8 50.1 16.8 77.9 0 29.4-6.7 57.3-18.8 82.1.9-60.6 2.2-140.6 2.4-147M410 139.7s.1 0 0 0c.7.1 1.4.1 2.1.2 5.4.3 10.5.7 10.8 22.1 0 .5-.1 1.1-.1 2 0 1-.1 2.4-.1 4.1-2.8-3-6.2-3.2-9.2-3.4-.7 0-1.4-.1-2.1-.2h-.2c-81.7 4.4-122.7 24.7-139.8 36.4-.4.3-.8.5-.9.6-2.2 1.9-4.4 4.7-6.3 7.5.1-11.3.3-18.6.3-19.5.7-2.6 4.1-11.5 8.5-13.1l.2-.1c.3-.2.7-.4 1.4-.9 16.5-11.4 56.1-31.3 135.4-35.7m-312.8.6c.7 0 1.4-.1 2.1-.2 79.4 4.4 118.9 24.2 135.4 35.7.7.5 1.1.7 1.4.9l.2.1c4.6 1.7 8.1 11.4 8.5 13.4l.1 1.3c.1 2.5.1 8.1.2 16.1-1.7-2.6-3.9-4.9-6.4-5.9-.2-.1-.5-.4-.9-.6-17.1-11.7-58.1-31.9-139.8-36.3h-.2c-.7.1-1.4.1-2.1.2-3 .2-6.4.4-9.2 3.3 0-1.6-.1-2.9-.1-3.8s0-1.6-.1-2c.4-21.5 5.5-21.8 10.9-22.2"/></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
1
code/frontend/public/icons/ext/sonarr-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M144.2 103.3c30.7 30.7 70 38.6 112.4 38.6 43.6 0 82.8-8.4 114.7-40.4 14.7-14.7 44.4-44.3 45.5-45.4C371.1 18.7 317.6 0 256.2 0c-60.8 0-114 18.5-159.8 55.5zM373 258.4c0 42.3 6.7 81.2 38.2 112.7 22.9 22.9 44.7 44.5 45 44.8 37-45.8 55.6-99.1 55.6-159.9 0-58.9-17.4-110.8-52.3-155.9L406.6 153c-30.9 31-33.6 57.9-33.6 105.4m-271.1 113c32.7-32.7 38-70.6 38-113.1 0-41.3-6.8-79.9-36.8-110-20.1-20-47.6-47.2-49.7-49.4-31.8 40.3-49.2 86.4-52.3 138.3-.3.6-.5 1.1-.5 1.7C.3 244.3.2 250 .2 256c0 5.7.2 11.3.4 17 .5 10.2 1.7 20.3 3.4 30.2 7.3 42.1 24.8 80 52.7 113.6.1-.2 23.2-23.4 45.2-45.4m269.6 46c-36.8-36.8-66.1-40.4-114.7-40.4-46.7 0-78.4 4.3-112.6 38.5-20.2 20.3-43.4 43.6-43.8 43.9 2.2 1.7 4.4 3.3 6.6 4.9 43 31.8 92.7 47.7 149.3 47.7q84.75 0 149.4-47.7c2.5-1.7 4.9-3.5 7.3-5.4zM186 269.1c-.5-2.8-.8-5.5-.9-8.4-.1-1.6-.1-3.1-.1-4.7 0-1.7 0-3.2.1-4.7 0-.2 0-.3.1-.5 1-17.4 7.9-32.4 20.5-45.1 13.9-13.8 30.6-20.7 50.2-20.7s36.3 6.9 50.2 20.7c13.8 14 20.7 30.8 20.7 50.3s-6.9 36.2-20.7 50.2c-.5.5-1 1.1-1.5 1.5q-3.45 3.3-7.2 6-18 13.2-41.4 13.2c-23.4 0-29.4-4.4-41.3-13.2-3.1-2.2-6.1-4.7-8.9-7.6-10.8-10.6-17.3-22.9-19.8-37" style="fill-rule:evenodd;clip-rule:evenodd"/><path d="m375.2 143.5-1.6-1.6v-.1L440 77.2l-1.4-1.4-66.4 64.6.7.7-.7-.7h-.1l-1.9-1.9-40 40.6 5 5zm-238.3 2.1 40.6 40.5 5-5-40.6-40.5-1.7 1.7-66.4-66.1-1.4 1.4 66.4 66.1zm234.9 223.9-42.6-42.4-5 5 42.6 42.4 1.8-1.8 65.6 67.8 1.4-1.4-65.5-67.9zm-233.3 2.1 1.9 1.9-64.3 64.4 1.4 1.4 64.4-64.5 1.6 1.6 39.5-41.1-5-4.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
code/frontend/public/icons/ext/telegram-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0m-46.1 291.2c-.7.8-2.8 3.4-2.5 6.7l-4 42.5-21.3-59c2.1-1.3 5-3.2 8.7-5.5 51-32.1 88.1-55.3 111.1-69.4-22.2 21.6-58 54.4-91.6 84.3zm4 66.3 4.5-48.3c6.6 4.4 16 10.8 26.5 18-17.4 17.7-26.4 26.2-31 30.3m163-202.7v.3c0 .9-.1 1.9-.2 3.2-.1.5-.1 1.1-.2 1.7v.1c-1.5 23-45.1 198.8-45.5 200.6-.1.3-1.8 6.5-7 6.7-3.2.1-6.3-1.1-8.5-3.3l-.3-.3c-17.6-15.1-74.7-53.8-94.4-66.9 7.8-7 30.7-27.5 53.5-48.6 57.8-53.3 59.3-58.5 60.1-61.3l.1-.2c.5-2.2-.1-4.4-1.6-5.9-1.7-1.7-4.2-2.3-6.9-1.6l-.5.2c-4.9 1.8-47 27.7-140.7 86.8-4.3 2.7-7.6 4.8-9.7 6.1l-61.7-20.1c-1.7-.8-2.3-1.7-1.9-2.9 0-.1.4-.6 2.5-2 9.8-6.7 157.8-61.1 255-96 2.4-.8 5.9-1.3 7.2-.8l.3.1c.1 0 .2.1.2.2v.2c.1 1.1.3 2.5.2 3.7"/></svg>
|
||||
|
After Width: | Height: | Size: 846 B |
1
code/frontend/public/icons/ext/whisparr-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M256 512c-34.6 0-68.1-6.8-99.7-20.1C125.9 479 98.5 460.5 75 437s-42-50.9-54.9-81.4C6.8 324.1 0 290.6 0 256s6.8-68.1 20.1-99.7C33 125.9 51.5 98.5 75 75s50.9-42 81.4-54.9C187.9 6.8 221.4 0 256 0s68.1 6.8 99.7 20.1C386.1 33 413.5 51.5 437 75s42 50.9 54.9 81.4c13.4 31.6 20.1 65.1 20.1 99.7s-6.8 68.1-20.1 99.7C479 386.1 460.5 413.5 437 437s-50.9 42-81.4 54.9c-31.5 13.3-65 20.1-99.6 20.1m0-486.2C129.1 25.8 25.8 129.1 25.8 256S129.1 486.2 256 486.2 486.2 382.9 486.2 256 382.9 25.8 256 25.8M59.4 413.3c-4.4 0-5.9-3.2-4.4-9.5 1.4-6.1 5.5-13.8 12.1-23.1l98-114.8q45.15-52.95 69-86.7c23.85-33.75 24.9-38.6 27.1-47.9 1-4.4.4-7.7-2.1-9.9-2.4-2.2-5.5-3.7-9.2-4.5s-7.3-1.2-10.7-1.2c-20.8 0-53.2 14.3-97.2 43-12.4 8-24.2 16.9-35.4 26.9q-16.8 14.85-32.1 31.8c-20 22.6-31.9 41.6-35.5 57-2.8 11.8.4 19 9.6 21.5 3.4 1.1 7.4 1.7 12.1 1.7 8.8 0 18.2-2.5 28.2-7.4 10-5 20-11.3 30.2-19 7.8-6.1 12.2-9.1 13.2-9.1s1.1 1 .4 2.9c-2.8 5.8-7.7 11.3-14.6 16.5s-14.7 10-23.6 14.3c-8.8 4.3-17.5 7.6-26 9.9s-15.8 3.5-21.9 3.5c-9.8 0-17.7-2.8-23.7-8.5-6-5.6-7.7-14-5.1-25 4-16.8 17.6-37.5 40.7-62 22.2-23.7 47.8-44.8 76.6-63.2 11.9-7.7 25.3-15.1 40.5-22.1 15.1-7 30.1-12.8 44.8-17.3 14.8-4.5 27.6-6.8 38.3-6.8 19.3 0 27.3 7.2 23.9 21.5-4.1 17.4-22.8 47.2-56.1 89.6-23.3 29.2-53.2 63.9-89.6 104.1-30.2 33.3-45.4 50-45.6 50 .7 0 15.8-14.2 45.3-42.5 18.1-17.6 35-33.4 50.7-47.3 15.6-13.9 30-26.1 43-36.6 6.4-5.2 14.2-11.8 23.4-19.6s19.9-17 32.2-27.5c10.6-8.8 20.9-16.8 30.8-24s18.3-10.7 25.1-10.7c7.1 0 11.3 1.9 12.6 5.8-29.9 31.4-60.1 69.8-90.4 115.3-21.6 32.5-34.4 57.1-38.4 73.9-2.3 9.9-1.7 17.1 1.9 21.5q5.4 6.6 16.8 6.6c8.3 0 17.7-2.6 28.1-7.8s21.1-12 32.1-20.2c11-8.3 21.5-16.9 31.5-26 35.7-31.9 66.3-66.5 91.7-103.7 19.9-29.2 31.9-52.5 36-69.8q.6-2.55.9-4.5c.2-1.4.3-2.6.3-3.7-5.1 0-9.9-3-14.4-9.1-2.9-3.3-3.9-6.7-3.1-10.3.8-3.3 2.7-6.1 5.6-8.3 3-2.2 6-3.3 9.2-3.3 4.6 0 7.5 2.3 8.6 7 .5 2.2 1.8 5 3.9 8.3 1.4 2.2 1.9 3.9 1.4 5 .9 2.5 3 3.7 6.5 3.7 3.4 0 7.1-1.7 10.9-5s7.6-5 11.3-5c2 0 2.7.8 2.3 2.5q-1.05 4.5-9 8.7c-2.9 1.9-6.1 3.2-9.7 3.7q-3 .45-6 .6c-3 .15-3.9-.1-5.7-.6.3 11-.1 19.1-1.4 24.4-5.2 22-21.3 50.5-48.1 85.5-13.5 17.6-28.1 34.3-43.8 50s-32.6 30.4-50.8 44.2c-36.3 28.1-65.9 42.1-88.9 42.1-25.2 0-34.7-13.2-28.4-39.7 2.9-12.4 8.6-26.5 17-42.3s17.6-31.6 27.7-47.3l41.8-64.9c-13 9.4-23.5 17-31.7 22.9-8.1 5.9-14.9 10.9-20.5 15.1-5.5 4.1-10.8 8.3-15.8 12.6s-11.1 9.4-18.2 15.5c-7.7 6.9-18.3 16.7-31.6 29.5s-29.1 28.7-47.3 47.7l-56.5 58.7c-3.2 3.3-6.8 6.3-10.6 9.1-3.6 2.2-7.1 3.6-10.2 3.6"/></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -52,6 +52,7 @@ import {
|
||||
tablerHistory,
|
||||
tablerGripVertical,
|
||||
tablerFilter,
|
||||
tablerPalette,
|
||||
} from '@ng-icons/tabler-icons';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
@@ -114,6 +115,7 @@ export const appConfig: ApplicationConfig = {
|
||||
tablerHistory,
|
||||
tablerGripVertical,
|
||||
tablerFilter,
|
||||
tablerPalette,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -128,6 +128,13 @@ export const routes: Routes = [
|
||||
'@features/settings/account/account-settings.component'
|
||||
).then((m) => m.AccountSettingsComponent),
|
||||
},
|
||||
{
|
||||
path: 'appearance',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'@features/settings/appearance/appearance-settings.component'
|
||||
).then((m) => m.AppearanceSettingsComponent),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,19 +2,68 @@ import { Injectable, signal, effect } from '@angular/core';
|
||||
|
||||
export type Theme = 'dark' | 'light';
|
||||
|
||||
export const ACCENT_PRESETS = [
|
||||
'default',
|
||||
'blue',
|
||||
'green',
|
||||
'rose',
|
||||
'amber',
|
||||
'teal',
|
||||
] as const;
|
||||
export type AccentPreset = (typeof ACCENT_PRESETS)[number];
|
||||
export type Accent = AccentPreset | 'custom';
|
||||
|
||||
// Preview swatch colors for each preset. These mirror the --brand-500 stop
|
||||
// declared in styles/_accents.scss (and styles/_tokens.scss for 'default').
|
||||
// Keep the two in sync — there is no SCSS-from-TS import path.
|
||||
export const ACCENT_PRESET_HEX: Record<AccentPreset, string> = {
|
||||
default: '#8b5cf6',
|
||||
blue: '#3b82f6',
|
||||
green: '#10b981',
|
||||
rose: '#f43f5e',
|
||||
amber: '#f59e0b',
|
||||
teal: '#14b8a6',
|
||||
};
|
||||
|
||||
const THEME_KEY = 'cleanuparr-theme';
|
||||
const PERFORMANCE_MODE_KEY = 'cleanuparr-performance-mode';
|
||||
const FULL_WIDTH_KEY = 'cleanuparr-full-width';
|
||||
const ACCENT_KEY = 'cleanuparr-accent';
|
||||
const CUSTOM_ACCENT_KEY = 'cleanuparr-custom-accent';
|
||||
|
||||
const DEFAULT_CUSTOM_ACCENT = '#8b5cf6';
|
||||
const HEX_COLOR_REGEX = /^#[0-9a-f]{6}$/i;
|
||||
const BRAND_SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;
|
||||
|
||||
// Lightness stops per shade, tuned to match the visual weight of the default purple scale.
|
||||
const LIGHTNESS_STOPS: Record<(typeof BRAND_SHADES)[number], number> = {
|
||||
50: 97,
|
||||
100: 93,
|
||||
200: 86,
|
||||
300: 75,
|
||||
400: 62,
|
||||
500: 50,
|
||||
600: 42,
|
||||
700: 34,
|
||||
800: 27,
|
||||
900: 20,
|
||||
950: 12,
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ThemeService {
|
||||
private readonly root = document.documentElement;
|
||||
private readonly _theme = signal<Theme>('dark');
|
||||
private readonly _performanceMode = signal(false);
|
||||
private readonly _fullWidth = signal(false);
|
||||
private readonly _accent = signal<Accent>('default');
|
||||
private readonly _customAccent = signal<string>(DEFAULT_CUSTOM_ACCENT);
|
||||
|
||||
readonly theme = this._theme.asReadonly();
|
||||
readonly performanceMode = this._performanceMode.asReadonly();
|
||||
readonly fullWidth = this._fullWidth.asReadonly();
|
||||
readonly accent = this._accent.asReadonly();
|
||||
readonly customAccent = this._customAccent.asReadonly();
|
||||
|
||||
constructor() {
|
||||
this.restoreFromStorage();
|
||||
@@ -55,6 +104,38 @@ export class ThemeService {
|
||||
localStorage.setItem(FULL_WIDTH_KEY, String(value));
|
||||
}
|
||||
|
||||
setAccent(accent: Accent): void {
|
||||
this._accent.set(accent);
|
||||
localStorage.setItem(ACCENT_KEY, accent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the right icon variant for the active theme. Asset filenames must
|
||||
* follow the `*-light.svg` (designed for dark backgrounds) /
|
||||
* `*-dark.svg` (designed for light backgrounds) convention.
|
||||
*/
|
||||
themedIconSrc(src: string): string {
|
||||
if (this._theme() === 'dark')
|
||||
{
|
||||
return src;
|
||||
}
|
||||
return src.replace('-light.svg', '-dark.svg');
|
||||
}
|
||||
|
||||
setCustomAccent(hex: string): void {
|
||||
const normalized = hex.trim().toLowerCase();
|
||||
if (!HEX_COLOR_REGEX.test(normalized))
|
||||
{
|
||||
return;
|
||||
}
|
||||
this._customAccent.set(normalized);
|
||||
localStorage.setItem(CUSTOM_ACCENT_KEY, normalized);
|
||||
if (this._accent() !== 'custom')
|
||||
{
|
||||
this.setAccent('custom');
|
||||
}
|
||||
}
|
||||
|
||||
private restoreFromStorage(): void {
|
||||
const savedTheme = localStorage.getItem(THEME_KEY);
|
||||
if (savedTheme === 'light' || savedTheme === 'dark') {
|
||||
@@ -70,6 +151,18 @@ export class ThemeService {
|
||||
if (savedFullWidth === 'true') {
|
||||
this._fullWidth.set(true);
|
||||
}
|
||||
|
||||
const savedAccent = localStorage.getItem(ACCENT_KEY);
|
||||
const migratedAccent = savedAccent === 'purple' ? 'default' : savedAccent;
|
||||
if (migratedAccent && this.isAccent(migratedAccent)) {
|
||||
this._accent.set(migratedAccent);
|
||||
}
|
||||
|
||||
const savedCustom = localStorage.getItem(CUSTOM_ACCENT_KEY);
|
||||
if (savedCustom && HEX_COLOR_REGEX.test(savedCustom))
|
||||
{
|
||||
this._customAccent.set(savedCustom);
|
||||
}
|
||||
}
|
||||
|
||||
private detectSystemPreferences(): void {
|
||||
@@ -81,15 +174,111 @@ export class ThemeService {
|
||||
|
||||
private bindToDom(): void {
|
||||
effect(() => {
|
||||
document.documentElement.setAttribute('data-theme', this._theme());
|
||||
this.root.setAttribute('data-theme', this._theme());
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
document.documentElement.setAttribute('data-performance-mode', String(this._performanceMode()));
|
||||
this.root.setAttribute('data-performance-mode', String(this._performanceMode()));
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
document.documentElement.setAttribute('data-full-width', String(this._fullWidth()));
|
||||
this.root.setAttribute('data-full-width', String(this._fullWidth()));
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const accent = this._accent();
|
||||
this.root.setAttribute('data-accent', accent);
|
||||
|
||||
if (accent === 'custom')
|
||||
{
|
||||
this.applyCustomAccent(this._customAccent());
|
||||
}
|
||||
else
|
||||
{
|
||||
this.clearInlineAccent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isAccent(value: string): value is Accent {
|
||||
return value === 'custom' || (ACCENT_PRESETS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
private applyCustomAccent(hex: string): void {
|
||||
const rgb = hexToRgb(hex);
|
||||
if (!rgb)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
||||
// Keep some chroma even when the user picks a near-gray, otherwise the whole
|
||||
// brand scale collapses to shades of gray and active states become invisible.
|
||||
const s = Math.max(hsl.s, 15);
|
||||
|
||||
for (const shade of BRAND_SHADES)
|
||||
{
|
||||
const l = shade === 500 ? hsl.l : LIGHTNESS_STOPS[shade];
|
||||
const { r, g, b } = hslToRgb(hsl.h, s, l);
|
||||
this.root.style.setProperty(`--brand-${shade}`, rgbToHex(r, g, b));
|
||||
}
|
||||
|
||||
this.root.style.setProperty('--accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
|
||||
}
|
||||
|
||||
private clearInlineAccent(): void {
|
||||
for (const shade of BRAND_SHADES)
|
||||
{
|
||||
this.root.style.removeProperty(`--brand-${shade}`);
|
||||
}
|
||||
this.root.style.removeProperty('--accent-rgb');
|
||||
}
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||
const match = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
||||
if (!match) return null;
|
||||
const n = parseInt(match[1], 16);
|
||||
return { r: (n >> 16) & 0xff, g: (n >> 8) & 0xff, b: n & 0xff };
|
||||
}
|
||||
|
||||
function rgbToHex(r: number, g: number, b: number): string {
|
||||
const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)));
|
||||
const hex = (v: number) => clamp(v).toString(16).padStart(2, '0');
|
||||
return `#${hex(r)}${hex(g)}${hex(b)}`;
|
||||
}
|
||||
|
||||
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
|
||||
const rn = r / 255, gn = g / 255, bn = b / 255;
|
||||
const max = Math.max(rn, gn, bn);
|
||||
const min = Math.min(rn, gn, bn);
|
||||
const l = (max + min) / 2;
|
||||
let h = 0, s = 0;
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case rn: h = (gn - bn) / d + (gn < bn ? 6 : 0); break;
|
||||
case gn: h = (bn - rn) / d + 2; break;
|
||||
case bn: h = (rn - gn) / d + 4; break;
|
||||
}
|
||||
h *= 60;
|
||||
}
|
||||
return { h, s: s * 100, l: l * 100 };
|
||||
}
|
||||
|
||||
function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
|
||||
const sn = s / 100, ln = l / 100;
|
||||
const c = (1 - Math.abs(2 * ln - 1)) * sn;
|
||||
const hp = h / 60;
|
||||
const x = c * (1 - Math.abs((hp % 2) - 1));
|
||||
let r1 = 0, g1 = 0, b1 = 0;
|
||||
if (hp >= 0 && hp < 1) [r1, g1, b1] = [c, x, 0];
|
||||
else if (hp < 2) [r1, g1, b1] = [x, c, 0];
|
||||
else if (hp < 3) [r1, g1, b1] = [0, c, x];
|
||||
else if (hp < 4) [r1, g1, b1] = [0, x, c];
|
||||
else if (hp < 5) [r1, g1, b1] = [x, 0, c];
|
||||
else [r1, g1, b1] = [c, 0, x];
|
||||
const m = ln - c / 2;
|
||||
return { r: (r1 + m) * 255, g: (g1 + m) * 255, b: (b1 + m) * 255 };
|
||||
}
|
||||
|
||||
@@ -130,16 +130,16 @@
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
background: #7E57C2;
|
||||
color: var(--color-primary-text);
|
||||
background: var(--color-primary);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-fast) var(--ease-default);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #6D28D9;
|
||||
box-shadow: 0 0 20px rgba(126, 87, 194, 0.4);
|
||||
background: var(--color-primary-hover);
|
||||
box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.4);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
|
||||
@@ -47,15 +47,15 @@
|
||||
transition: all var(--duration-normal) var(--ease-default);
|
||||
|
||||
.step-group.active & {
|
||||
background: rgba(126, 87, 194, 0.15);
|
||||
background: rgba(var(--accent-rgb), 0.15);
|
||||
color: var(--color-primary);
|
||||
box-shadow: 0 0 8px rgba(126, 87, 194, 0.25);
|
||||
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.25);
|
||||
}
|
||||
|
||||
.step-group.completed & {
|
||||
background: var(--color-primary);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 0 12px rgba(126, 87, 194, 0.35);
|
||||
box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
&:hover {
|
||||
background: var(--glass-bg-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(126, 87, 194, 0.12);
|
||||
box-shadow: 0 8px 24px rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
// GitHub — monochrome grey/white
|
||||
@@ -278,8 +278,8 @@
|
||||
}
|
||||
|
||||
&--important {
|
||||
background: rgba(126, 87, 194, 0.08);
|
||||
border-color: rgba(126, 87, 194, 0.25);
|
||||
background: rgba(var(--accent-rgb), 0.08);
|
||||
border-color: rgba(var(--accent-rgb), 0.25);
|
||||
animation: slide-up var(--duration-normal) var(--ease-default), glow-pulse-important 3s ease-in-out infinite;
|
||||
animation-delay: 0s, var(--duration-normal);
|
||||
}
|
||||
@@ -533,9 +533,9 @@
|
||||
box-shadow: 0 0 8px rgba(34, 197, 94, 0.25);
|
||||
}
|
||||
&--primary {
|
||||
background: rgba(126, 87, 194, 0.15);
|
||||
background: rgba(var(--accent-rgb), 0.15);
|
||||
color: var(--color-primary);
|
||||
box-shadow: 0 0 8px rgba(126, 87, 194, 0.25);
|
||||
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.25);
|
||||
}
|
||||
&--default {
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
@@ -726,10 +726,10 @@
|
||||
|
||||
@keyframes glow-pulse-important {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 12px rgba(126, 87, 194, 0.1), 0 0 24px rgba(126, 87, 194, 0.05);
|
||||
box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.1), 0 0 24px rgba(var(--accent-rgb), 0.05);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(126, 87, 194, 0.25), 0 0 40px rgba(126, 87, 194, 0.1);
|
||||
box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.25), 0 0 40px rgba(var(--accent-rgb), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(126, 87, 194, 0.06), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.06), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--duration-normal) var(--ease-default);
|
||||
pointer-events: none;
|
||||
@@ -233,8 +233,8 @@
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
background: rgba(126, 87, 194, 0.1);
|
||||
border: 1px solid rgba(126, 87, 194, 0.2);
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-primary);
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(126, 87, 194, 0.06), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.06), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--duration-normal) var(--ease-default);
|
||||
pointer-events: none;
|
||||
@@ -202,8 +202,8 @@
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
background: rgba(126, 87, 194, 0.1);
|
||||
border: 1px solid rgba(126, 87, 194, 0.2);
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-primary);
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(126, 87, 194, 0.06), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.06), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--duration-normal) var(--ease-default);
|
||||
pointer-events: none;
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
&__progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-primary), color-mix(in srgb, var(--color-primary) 75%, #c084fc));
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--brand-300));
|
||||
border-radius: var(--radius-full);
|
||||
transition: width var(--duration-normal) var(--ease-default);
|
||||
min-width: 0;
|
||||
@@ -307,7 +307,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(126, 87, 194, 0.06), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.06), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--duration-normal) var(--ease-default);
|
||||
pointer-events: none;
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(126, 87, 194, 0.06), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.06), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--duration-normal) var(--ease-default);
|
||||
pointer-events: none;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<app-page-header
|
||||
title="Appearance"
|
||||
subtitle="Customize the look and feel of the interface"
|
||||
/>
|
||||
|
||||
<div class="settings-form">
|
||||
<app-card header="Theme">
|
||||
<p class="section-hint">Choose between dark and light color modes.</p>
|
||||
<div class="theme-options">
|
||||
<button
|
||||
type="button"
|
||||
class="theme-option"
|
||||
[class.theme-option--active]="theme() === 'dark'"
|
||||
(click)="selectTheme('dark')"
|
||||
>
|
||||
<span class="theme-option__preview theme-option__preview--dark"></span>
|
||||
<span class="theme-option__label">Dark</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-option"
|
||||
[class.theme-option--active]="theme() === 'light'"
|
||||
(click)="selectTheme('light')"
|
||||
>
|
||||
<span class="theme-option__preview theme-option__preview--light"></span>
|
||||
<span class="theme-option__label">Light</span>
|
||||
</button>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
<app-card header="Accent color">
|
||||
<p class="section-hint">Pick a preset or choose a custom color. Changes apply instantly and persist across sessions.</p>
|
||||
<div class="swatch-grid">
|
||||
@for (swatch of presetSwatches; track swatch.value) {
|
||||
<button
|
||||
type="button"
|
||||
class="swatch"
|
||||
[class.swatch--active]="accent() === swatch.value"
|
||||
[attr.aria-label]="swatch.label"
|
||||
[attr.title]="swatch.label"
|
||||
(click)="selectAccent(swatch.value)"
|
||||
>
|
||||
<span class="swatch__dot" [style.background]="swatch.color"></span>
|
||||
<span class="swatch__label">{{ swatch.label }}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<label
|
||||
class="swatch swatch--custom"
|
||||
[class.swatch--active]="accent() === 'custom'"
|
||||
title="Custom color"
|
||||
>
|
||||
<span class="swatch__dot swatch__dot--custom"></span>
|
||||
<span class="swatch__label">Custom</span>
|
||||
<input
|
||||
type="color"
|
||||
class="swatch__input"
|
||||
[value]="customAccent()"
|
||||
(input)="onCustomColorChange($event)"
|
||||
aria-label="Pick a custom accent color"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</app-card>
|
||||
</div>
|
||||
@@ -0,0 +1,132 @@
|
||||
@use 'settings-layout' as *;
|
||||
|
||||
:host { @include settings-page; }
|
||||
|
||||
.settings-form { @include settings-form; }
|
||||
|
||||
.section-hint {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
|
||||
.theme-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-2);
|
||||
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color var(--duration-fast) var(--ease-default),
|
||||
background var(--duration-fast) var(--ease-default),
|
||||
box-shadow var(--duration-fast) var(--ease-default);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--glass-border-hover);
|
||||
background: var(--glass-bg-hover);
|
||||
}
|
||||
|
||||
&--active {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-subtle);
|
||||
}
|
||||
|
||||
&__preview {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--glass-border);
|
||||
flex-shrink: 0;
|
||||
|
||||
&--dark {
|
||||
background: linear-gradient(135deg, var(--brand-950) 0%, #0c0614 100%);
|
||||
}
|
||||
|
||||
&--light {
|
||||
background: linear-gradient(135deg, #ffffff 0%, var(--brand-100) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.swatch-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.swatch {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color var(--duration-fast) var(--ease-default),
|
||||
background var(--duration-fast) var(--ease-default),
|
||||
box-shadow var(--duration-fast) var(--ease-default);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--glass-border-hover);
|
||||
background: var(--glass-bg-hover);
|
||||
}
|
||||
|
||||
&--active {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-subtle);
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
flex-shrink: 0;
|
||||
|
||||
&--custom {
|
||||
background-image: conic-gradient(from 180deg, #f43f5e, #f59e0b, #10b981, #3b82f6, #7e57c2, #f43f5e);
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
&__input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { PageHeaderComponent } from '@layout/page-header/page-header.component';
|
||||
import { CardComponent } from '@ui';
|
||||
import {
|
||||
ACCENT_PRESETS,
|
||||
ACCENT_PRESET_HEX,
|
||||
Accent,
|
||||
Theme,
|
||||
ThemeService,
|
||||
} from '@core/services/theme.service';
|
||||
|
||||
interface AccentSwatch {
|
||||
readonly value: Accent;
|
||||
readonly label: string;
|
||||
readonly color: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-appearance-settings',
|
||||
standalone: true,
|
||||
imports: [PageHeaderComponent, CardComponent],
|
||||
templateUrl: './appearance-settings.component.html',
|
||||
styleUrl: './appearance-settings.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppearanceSettingsComponent {
|
||||
private readonly themeService = inject(ThemeService);
|
||||
|
||||
readonly theme = this.themeService.theme;
|
||||
readonly accent = this.themeService.accent;
|
||||
readonly customAccent = this.themeService.customAccent;
|
||||
|
||||
readonly presetSwatches: AccentSwatch[] = ACCENT_PRESETS.map((value) => ({
|
||||
value,
|
||||
label: value.charAt(0).toUpperCase() + value.slice(1),
|
||||
color: ACCENT_PRESET_HEX[value],
|
||||
}));
|
||||
|
||||
selectTheme(theme: Theme): void {
|
||||
this.themeService.setTheme(theme);
|
||||
}
|
||||
|
||||
selectAccent(accent: Accent): void {
|
||||
this.themeService.setAccent(accent);
|
||||
}
|
||||
|
||||
onCustomColorChange(event: Event): void {
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
this.themeService.setCustomAccent(value);
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,8 @@
|
||||
@for (provider of availableProviders; track provider.type) {
|
||||
<button class="provider-card" (click)="onProviderTypeSelected(provider.type)">
|
||||
<span class="provider-card__icon-wrapper">
|
||||
<img [src]="provider.iconLightUrl" [alt]="provider.name" class="provider-card__icon provider-card__icon--light" />
|
||||
<img [src]="provider.iconUrl" [alt]="provider.name" class="provider-card__icon provider-card__icon--normal" />
|
||||
<img [src]="themeService.themedIconSrc(provider.iconLightUrl)" alt="" aria-hidden="true" class="provider-card__icon provider-card__icon--light" />
|
||||
<img [src]="provider.iconUrl" alt="" aria-hidden="true" class="provider-card__icon provider-card__icon--normal" />
|
||||
</span>
|
||||
<span class="provider-card__name">{{ provider.name }}</span>
|
||||
<span class="provider-card__description">{{ provider.description }}</span>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { NotificationApi } from '@core/api/notification.api';
|
||||
import { ToastService } from '@core/services/toast.service';
|
||||
import { ConfirmService } from '@core/services/confirm.service';
|
||||
import { ThemeService } from '@core/services/theme.service';
|
||||
import {
|
||||
NotificationProviderDto,
|
||||
CreateDiscordProviderRequest,
|
||||
@@ -114,6 +115,9 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
|
||||
private readonly api = inject(NotificationApi);
|
||||
private readonly toast = inject(ToastService);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
protected readonly themeService = inject(ThemeService);
|
||||
|
||||
readonly theme = this.themeService.theme;
|
||||
|
||||
readonly loader = new DeferredLoader();
|
||||
readonly loadError = signal(false);
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(126, 87, 194, 0.06), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.06), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--duration-normal) var(--ease-default);
|
||||
pointer-events: none;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="auth-layout">
|
||||
<div class="auth-layout__card">
|
||||
<div class="auth-layout__brand">
|
||||
<img src="icons/128.png" alt="Cleanuparr" class="auth-layout__logo" />
|
||||
<app-logo class="auth-layout__logo" />
|
||||
<h1>Cleanuparr</h1>
|
||||
</div>
|
||||
<router-outlet />
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
&__logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
filter: drop-shadow(0 0 8px rgba(126, 87, 194, 0.4));
|
||||
filter: drop-shadow(0 0 8px rgba(var(--accent-rgb), 0.4));
|
||||
margin-bottom: var(--space-3);
|
||||
animation: float-gentle 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { LogoComponent } from '@ui';
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth-layout',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, LogoComponent],
|
||||
templateUrl: './auth-layout.component.html',
|
||||
styleUrl: './auth-layout.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[class.sidebar--mobile-open]="mobileOpen()"
|
||||
>
|
||||
<div class="sidebar__brand">
|
||||
<img src="icons/128.png" alt="Cleanuparr" class="sidebar__logo-img" />
|
||||
<app-logo class="sidebar__logo-img" />
|
||||
<span class="sidebar__logo-text">Cleanuparr</span>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
(click)="onNavItemClick()"
|
||||
>
|
||||
@if (item.iconSrc) {
|
||||
<img [src]="item.iconSrc" [alt]="item.label" class="sidebar__item-img" />
|
||||
<img [src]="themeService.themedIconSrc(item.iconSrc)" alt="" aria-hidden="true" class="sidebar__item-img" />
|
||||
} @else if (item.icon) {
|
||||
<ng-icon [name]="item.icon" class="sidebar__item-icon" />
|
||||
}
|
||||
|
||||
@@ -28,21 +28,21 @@
|
||||
}
|
||||
|
||||
&__logo-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
filter: drop-shadow(0 0 8px rgba(126, 87, 194, 0.4));
|
||||
filter: drop-shadow(0 0 8px rgba(var(--accent-rgb), 0.4));
|
||||
transition: filter var(--duration-fast) var(--ease-default);
|
||||
|
||||
.sidebar__brand:hover & {
|
||||
filter: drop-shadow(0 0 12px rgba(126, 87, 194, 0.6));
|
||||
filter: drop-shadow(0 0 12px rgba(var(--accent-rgb), 0.6));
|
||||
}
|
||||
}
|
||||
|
||||
&__logo-text {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
color: var(--sidebar-item-active-text);
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -73,7 +73,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background: linear-gradient(to bottom, transparent, rgba(12, 6, 20, 0.9));
|
||||
background: linear-gradient(to bottom, transparent, var(--sidebar-fade));
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -95,14 +95,14 @@
|
||||
|
||||
&:hover {
|
||||
background: var(--sidebar-item-hover);
|
||||
color: #ffffff;
|
||||
color: var(--sidebar-item-active-text);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: linear-gradient(90deg, rgba(126, 87, 194, 0.3), rgba(126, 87, 194, 0.08));
|
||||
background: linear-gradient(90deg, rgba(var(--accent-rgb), 0.3), rgba(var(--accent-rgb), 0.08));
|
||||
color: var(--sidebar-item-active-text);
|
||||
font-weight: 500;
|
||||
box-shadow: 0 0 20px rgba(126, 87, 194, 0.15);
|
||||
box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.15);
|
||||
position: relative;
|
||||
|
||||
// Active indicator bar
|
||||
@@ -206,7 +206,7 @@
|
||||
|
||||
&:hover {
|
||||
background: var(--sidebar-item-hover);
|
||||
color: #ffffff;
|
||||
color: var(--sidebar-item-active-text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
|
||||
&:hover {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
color: #ffffff;
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
font-family: var(--font-mono);
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
color: var(--sidebar-section-label-hover);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Component, ChangeDetectionStrategy, input, output, signal, inject, computed } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { LogoComponent } from '@ui';
|
||||
import { AppHubService } from '@core/realtime/app-hub.service';
|
||||
import { AuthService } from '@core/auth/auth.service';
|
||||
import { ThemeService } from '@core/services/theme.service';
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
@@ -20,7 +22,7 @@ interface ExternalLink {
|
||||
@Component({
|
||||
selector: 'app-nav-sidebar',
|
||||
standalone: true,
|
||||
imports: [RouterLink, RouterLinkActive, NgIcon],
|
||||
imports: [RouterLink, RouterLinkActive, NgIcon, LogoComponent],
|
||||
templateUrl: './nav-sidebar.component.html',
|
||||
styleUrl: './nav-sidebar.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -28,6 +30,9 @@ interface ExternalLink {
|
||||
export class NavSidebarComponent {
|
||||
private readonly hub = inject(AppHubService);
|
||||
private readonly auth = inject(AuthService);
|
||||
protected readonly themeService = inject(ThemeService);
|
||||
|
||||
readonly theme = this.themeService.theme;
|
||||
|
||||
collapsed = input(false);
|
||||
mobileOpen = input(false);
|
||||
@@ -74,6 +79,7 @@ export class NavSidebarComponent {
|
||||
otherSettingsItems: NavItem[] = [
|
||||
{ label: 'Notifications', icon: 'tablerBellRinging', route: '/settings/notifications' },
|
||||
{ label: 'Account', icon: 'tablerUser', route: '/settings/account' },
|
||||
{ label: 'Appearance', icon: 'tablerPalette', route: '/settings/appearance' },
|
||||
];
|
||||
|
||||
onNavItemClick(): void {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(126, 87, 194, 0.3), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.3), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
&:hover {
|
||||
background: var(--glass-bg);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 0 12px rgba(126, 87, 194, 0.15);
|
||||
box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: linear-gradient(135deg, rgba(126, 87, 194, 0.2), rgba(126, 87, 194, 0.1));
|
||||
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.2), rgba(var(--accent-rgb), 0.1));
|
||||
color: var(--color-primary);
|
||||
border: 1px solid rgba(126, 87, 194, 0.2);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
}
|
||||
|
||||
&--success {
|
||||
|
||||
@@ -70,13 +70,13 @@
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 0 20px rgba(126, 87, 194, 0.3),
|
||||
0 4px 12px rgba(126, 87, 194, 0.2);
|
||||
box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.3),
|
||||
0 4px 12px rgba(var(--accent-rgb), 0.2);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--color-primary-hover);
|
||||
box-shadow: 0 0 30px rgba(126, 87, 194, 0.4),
|
||||
0 6px 16px rgba(126, 87, 194, 0.3);
|
||||
box-shadow: 0 0 30px rgba(var(--accent-rgb), 0.4),
|
||||
0 6px 16px rgba(var(--accent-rgb), 0.3);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
// Hover lift effect with inner glow
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--glass-shadow), 0 8px 24px rgba(126, 87, 194, 0.08),
|
||||
inset 0 0 30px rgba(126, 87, 194, 0.03);
|
||||
box-shadow: var(--glass-shadow), 0 8px 24px rgba(var(--accent-rgb), 0.08),
|
||||
inset 0 0 30px rgba(var(--accent-rgb), 0.03);
|
||||
|
||||
&::before {
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
min-height: 26px;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
background: var(--color-primary-subtle);
|
||||
border: 1px solid rgba(126, 87, 194, 0.2);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-primary);
|
||||
@@ -83,7 +83,7 @@
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
background: var(--color-primary-subtle);
|
||||
border: 1px solid rgba(126, 87, 194, 0.2);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: var(--radius-full);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
@@ -27,3 +27,4 @@ export { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.componen
|
||||
export { SizeInputComponent } from './size-input/size-input.component';
|
||||
export type { SizeUnit } from './size-input/size-input.component';
|
||||
export { TooltipComponent } from './tooltip/tooltip.component';
|
||||
export { LogoComponent } from './logo/logo.component';
|
||||
|
||||
12
code/frontend/src/app/ui/logo/logo.component.html
Normal file
|
After Width: | Height: | Size: 69 KiB |
11
code/frontend/src/app/ui/logo/logo.component.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
:host {
|
||||
display: inline-flex;
|
||||
color: var(--logo-fg);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
12
code/frontend/src/app/ui/logo/logo.component.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logo',
|
||||
standalone: true,
|
||||
templateUrl: './logo.component.html',
|
||||
styleUrl: './logo.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LogoComponent {
|
||||
ariaLabel = input<string>('Cleanuparr');
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(135deg, rgba(126, 87, 194, 0.3), transparent 50%, rgba(59, 130, 246, 0.2));
|
||||
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.3), transparent 50%, rgba(var(--accent-rgb), 0.2));
|
||||
z-index: -1;
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
&:focus-within {
|
||||
border-color: var(--input-border-focus);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-subtle),
|
||||
0 0 12px rgba(126, 87, 194, 0.15);
|
||||
0 0 12px rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
&:focus-within {
|
||||
border-color: var(--input-border-focus);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-subtle),
|
||||
0 0 12px rgba(126, 87, 194, 0.15);
|
||||
0 0 12px rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
@use 'styles/tokens';
|
||||
@use 'styles/themes';
|
||||
@use 'styles/accents';
|
||||
@use 'styles/reset';
|
||||
@use 'styles/typography';
|
||||
@use 'styles/scrollbar';
|
||||
@@ -17,8 +18,8 @@
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--glass-bg) 0px,
|
||||
rgba(126, 87, 194, 0.08) 30px,
|
||||
rgba(59, 130, 246, 0.06) 50px,
|
||||
rgba(var(--accent-rgb), 0.08) 30px,
|
||||
rgba(var(--accent-rgb), 0.06) 50px,
|
||||
var(--glass-bg-hover) 70px,
|
||||
var(--glass-bg) 100px
|
||||
);
|
||||
|
||||
94
code/frontend/src/styles/_accents.scss
Normal file
@@ -0,0 +1,94 @@
|
||||
// =============================================================================
|
||||
// Accent Presets
|
||||
// Each preset overrides the --brand-* scale and --accent-rgb triple.
|
||||
// The 'default' preset keeps the UI brand palette (from _tokens.scss) unchanged
|
||||
// and restores the logo's original hexes. Scales follow the same perceptual ramp
|
||||
// (Tailwind-style 50..950 lightness).
|
||||
//
|
||||
// NOTE: The --brand-500 stop of each preset is mirrored as a TS constant in
|
||||
// app/core/services/theme.service.ts (`ACCENT_PRESET_HEX`) so the appearance
|
||||
// settings page can render preview swatches without inspecting the DOM. Keep
|
||||
// the two in sync when changing a preset's mid-tone.
|
||||
// =============================================================================
|
||||
|
||||
// Default: UI stays on brand-purple defaults; logo uses the original darker
|
||||
// purple for accent parts, and the body flips with the theme (black on dark,
|
||||
// white on light) so it blends into the sidebar.
|
||||
[data-accent='default'] {
|
||||
--logo-accent: #420077;
|
||||
}
|
||||
|
||||
[data-accent='blue'] {
|
||||
--brand-50: #eff6ff;
|
||||
--brand-100: #dbeafe;
|
||||
--brand-200: #bfdbfe;
|
||||
--brand-300: #93c5fd;
|
||||
--brand-400: #60a5fa;
|
||||
--brand-500: #3b82f6;
|
||||
--brand-600: #2563eb;
|
||||
--brand-700: #1d4ed8;
|
||||
--brand-800: #1e40af;
|
||||
--brand-900: #1e3a8a;
|
||||
--brand-950: #172554;
|
||||
--accent-rgb: 59, 130, 246;
|
||||
}
|
||||
|
||||
[data-accent='green'] {
|
||||
--brand-50: #ecfdf5;
|
||||
--brand-100: #d1fae5;
|
||||
--brand-200: #a7f3d0;
|
||||
--brand-300: #6ee7b7;
|
||||
--brand-400: #34d399;
|
||||
--brand-500: #10b981;
|
||||
--brand-600: #059669;
|
||||
--brand-700: #047857;
|
||||
--brand-800: #065f46;
|
||||
--brand-900: #064e3b;
|
||||
--brand-950: #022c22;
|
||||
--accent-rgb: 16, 185, 129;
|
||||
}
|
||||
|
||||
[data-accent='rose'] {
|
||||
--brand-50: #fff1f2;
|
||||
--brand-100: #ffe4e6;
|
||||
--brand-200: #fecdd3;
|
||||
--brand-300: #fda4af;
|
||||
--brand-400: #fb7185;
|
||||
--brand-500: #f43f5e;
|
||||
--brand-600: #e11d48;
|
||||
--brand-700: #be123c;
|
||||
--brand-800: #9f1239;
|
||||
--brand-900: #881337;
|
||||
--brand-950: #4c0519;
|
||||
--accent-rgb: 244, 63, 94;
|
||||
}
|
||||
|
||||
[data-accent='amber'] {
|
||||
--brand-50: #fffbeb;
|
||||
--brand-100: #fef3c7;
|
||||
--brand-200: #fde68a;
|
||||
--brand-300: #fcd34d;
|
||||
--brand-400: #fbbf24;
|
||||
--brand-500: #f59e0b;
|
||||
--brand-600: #d97706;
|
||||
--brand-700: #b45309;
|
||||
--brand-800: #92400e;
|
||||
--brand-900: #78350f;
|
||||
--brand-950: #451a03;
|
||||
--accent-rgb: 245, 158, 11;
|
||||
}
|
||||
|
||||
[data-accent='teal'] {
|
||||
--brand-50: #f0fdfa;
|
||||
--brand-100: #ccfbf1;
|
||||
--brand-200: #99f6e4;
|
||||
--brand-300: #5eead4;
|
||||
--brand-400: #2dd4bf;
|
||||
--brand-500: #14b8a6;
|
||||
--brand-600: #0d9488;
|
||||
--brand-700: #0f766e;
|
||||
--brand-800: #115e59;
|
||||
--brand-900: #134e4a;
|
||||
--brand-950: #042f2e;
|
||||
--accent-rgb: 20, 184, 166;
|
||||
}
|
||||
@@ -73,8 +73,8 @@
|
||||
|
||||
// Gentle glow breathe for focused inputs — very slow, very subtle
|
||||
@keyframes glow-breathe {
|
||||
0%, 100% { box-shadow: 0 0 0 3px var(--color-primary-subtle), 0 0 12px rgba(126, 87, 194, 0.12); }
|
||||
50% { box-shadow: 0 0 0 3px var(--color-primary-subtle), 0 0 16px rgba(126, 87, 194, 0.22); }
|
||||
0%, 100% { box-shadow: 0 0 0 3px var(--color-primary-subtle), 0 0 12px rgba(var(--accent-rgb), 0.12); }
|
||||
50% { box-shadow: 0 0 0 3px var(--color-primary-subtle), 0 0 16px rgba(var(--accent-rgb), 0.22); }
|
||||
}
|
||||
|
||||
// Delayed content materialise — used inside expanding containers
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
// Toast entrance glow
|
||||
@keyframes toast-glow {
|
||||
from { box-shadow: 0 0 20px var(--toast-glow-color, rgba(126, 87, 194, 0.3)); }
|
||||
from { box-shadow: 0 0 20px var(--toast-glow-color, rgba(var(--accent-rgb), 0.3)); }
|
||||
to { box-shadow: none; }
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ $_glass-noise: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http:
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-lg));
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--glass-shadow), 0 0 80px rgba(126, 87, 194, 0.04);
|
||||
box-shadow: var(--glass-shadow), 0 0 80px rgba(var(--accent-rgb), 0.04);
|
||||
|
||||
// Frost noise overlay for realism
|
||||
background-image: #{$_glass-noise};
|
||||
@@ -100,7 +100,7 @@ $_glass-noise: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http:
|
||||
outline: none;
|
||||
border-color: var(--input-border-focus);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-subtle),
|
||||
0 0 12px rgba(126, 87, 194, 0.15);
|
||||
0 0 12px rgba(var(--accent-rgb), 0.15);
|
||||
animation: glow-breathe 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(126, 87, 194, 0.06), transparent);
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--accent-rgb), 0.06), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--duration-normal) var(--ease-default);
|
||||
pointer-events: none;
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
// Dark theme (default)
|
||||
:root,
|
||||
[data-theme='dark'] {
|
||||
// Surfaces
|
||||
// Surfaces with fallbacks for browsers that can't parse color-mix()
|
||||
--surface-ground: #0c0614;
|
||||
--surface-section: #140b22;
|
||||
--surface-ground: color-mix(in srgb, var(--brand-500) 8%, #000000);
|
||||
--surface-section: #14091e;
|
||||
--surface-section: color-mix(in srgb, var(--brand-500) 14%, #000000);
|
||||
--surface-card: rgba(20, 11, 34, 0.75);
|
||||
--surface-overlay: rgba(12, 6, 20, 0.92);
|
||||
--surface-elevated: rgba(30, 18, 50, 0.65);
|
||||
@@ -34,14 +36,14 @@
|
||||
--color-primary-hover: var(--brand-400);
|
||||
--color-primary-active: var(--brand-300);
|
||||
--color-primary-text: #ffffff;
|
||||
--color-primary-subtle: rgba(126, 87, 194, 0.15);
|
||||
--color-primary-subtle: rgba(var(--accent-rgb), 0.15);
|
||||
|
||||
// Sidebar (always dark in both themes)
|
||||
--sidebar-bg: linear-gradient(180deg, #1a0e2e 0%, #0c0614 100%);
|
||||
// Sidebar
|
||||
--sidebar-bg: linear-gradient(180deg, var(--brand-950) 0%, var(--surface-ground) 100%);
|
||||
--sidebar-border: rgba(255, 255, 255, 0.06);
|
||||
--sidebar-item-text: rgba(255, 255, 255, 0.65);
|
||||
--sidebar-item-hover: rgba(126, 87, 194, 0.12);
|
||||
--sidebar-item-active: rgba(126, 87, 194, 0.22);
|
||||
--sidebar-item-hover: rgba(var(--accent-rgb), 0.12);
|
||||
--sidebar-item-active: rgba(var(--accent-rgb), 0.22);
|
||||
--sidebar-item-active-text: #ffffff;
|
||||
--sidebar-section-label: rgba(255, 255, 255, 0.45);
|
||||
--sidebar-section-label-hover: rgba(255, 255, 255, 0.65);
|
||||
@@ -61,8 +63,8 @@
|
||||
|
||||
// Scrollbar
|
||||
--scrollbar-track: transparent;
|
||||
--scrollbar-thumb: rgba(126, 87, 194, 0.45);
|
||||
--scrollbar-thumb-hover: rgba(126, 87, 194, 0.70);
|
||||
--scrollbar-thumb: rgba(var(--accent-rgb), 0.45);
|
||||
--scrollbar-thumb-hover: rgba(var(--accent-rgb), 0.70);
|
||||
|
||||
// Dropdown
|
||||
--dropdown-bg: rgba(20, 11, 34, 0.97);
|
||||
@@ -74,6 +76,18 @@
|
||||
--focus-ring: var(--brand-500);
|
||||
--focus-ring-offset: var(--surface-ground);
|
||||
|
||||
// Logo body color: black on dark theme so it blends into the dark sidebar —
|
||||
// only the accent-colored parts of the logo read against the background.
|
||||
--logo-fg: #000000;
|
||||
|
||||
// Logo accent color (follows --color-primary; overridden for 'default' preset to
|
||||
// restore the original darker purple).
|
||||
--logo-accent: var(--color-primary);
|
||||
|
||||
// Sidebar fade gradient (bottom of nav scroll area)
|
||||
--sidebar-fade: rgba(12, 6, 20, 0.90);
|
||||
--sidebar-fade: color-mix(in srgb, var(--surface-ground) 90%, transparent);
|
||||
|
||||
// Ambient orbs
|
||||
--orb-opacity: 0.15;
|
||||
--orb-blur: 120px;
|
||||
@@ -84,9 +98,11 @@
|
||||
|
||||
// Light theme
|
||||
[data-theme='light'] {
|
||||
// Surfaces
|
||||
--surface-ground: #f5f0fa;
|
||||
--surface-section: #ede5f7;
|
||||
// Surfaces with fallbacks for browsers that can't parse color-mix()
|
||||
--surface-ground: #faf7fd;
|
||||
--surface-ground: color-mix(in srgb, var(--brand-500) 3%, #ffffff);
|
||||
--surface-section: #f3edf9;
|
||||
--surface-section: color-mix(in srgb, var(--brand-500) 6%, #ffffff);
|
||||
--surface-card: rgba(255, 255, 255, 0.70);
|
||||
--surface-overlay: rgba(245, 240, 250, 0.94);
|
||||
--surface-elevated: rgba(255, 255, 255, 0.80);
|
||||
@@ -95,9 +111,9 @@
|
||||
--glass-bg: rgba(255, 255, 255, 0.50);
|
||||
--glass-bg-hover: rgba(255, 255, 255, 0.65);
|
||||
--glass-bg-active: rgba(255, 255, 255, 0.75);
|
||||
--glass-border: rgba(126, 87, 194, 0.10);
|
||||
--glass-border-hover: rgba(126, 87, 194, 0.20);
|
||||
--glass-shadow: 0 8px 32px rgba(126, 87, 194, 0.06);
|
||||
--glass-border: rgba(var(--accent-rgb), 0.10);
|
||||
--glass-border-hover: rgba(var(--accent-rgb), 0.20);
|
||||
--glass-shadow: 0 8px 32px rgba(var(--accent-rgb), 0.06);
|
||||
|
||||
// Text
|
||||
--text-primary: rgba(12, 6, 20, 0.90);
|
||||
@@ -111,50 +127,62 @@
|
||||
--color-primary-hover: var(--brand-700);
|
||||
--color-primary-active: var(--brand-800);
|
||||
--color-primary-text: #ffffff;
|
||||
--color-primary-subtle: rgba(126, 87, 194, 0.08);
|
||||
--color-primary-subtle: rgba(var(--accent-rgb), 0.08);
|
||||
|
||||
// Sidebar (stays dark purple in light theme for brand identity)
|
||||
--sidebar-bg: linear-gradient(180deg, #2d1a4e 0%, #1a0e2e 100%);
|
||||
--sidebar-border: rgba(255, 255, 255, 0.08);
|
||||
--sidebar-item-text: rgba(255, 255, 255, 0.65);
|
||||
--sidebar-item-hover: rgba(255, 255, 255, 0.10);
|
||||
--sidebar-item-active: rgba(255, 255, 255, 0.18);
|
||||
--sidebar-item-active-text: #ffffff;
|
||||
--sidebar-section-label: rgba(255, 255, 255, 0.45);
|
||||
--sidebar-section-label-hover: rgba(255, 255, 255, 0.65);
|
||||
// Sidebar (light theme): light accent-tinted background with dark text
|
||||
--sidebar-bg: linear-gradient(180deg, var(--brand-50) 0%, var(--brand-100) 100%);
|
||||
--sidebar-border: var(--divider);
|
||||
--sidebar-item-text: rgba(12, 6, 20, 0.70);
|
||||
--sidebar-item-hover: rgba(var(--accent-rgb), 0.10);
|
||||
--sidebar-item-active: rgba(var(--accent-rgb), 0.18);
|
||||
--sidebar-item-active-text: rgba(12, 6, 20, 0.95);
|
||||
--sidebar-section-label: rgba(12, 6, 20, 0.50);
|
||||
--sidebar-section-label-hover: rgba(12, 6, 20, 0.80);
|
||||
|
||||
// Toolbar
|
||||
--toolbar-bg: rgba(255, 255, 255, 0.65);
|
||||
--toolbar-border: rgba(126, 87, 194, 0.10);
|
||||
--toolbar-border: rgba(var(--accent-rgb), 0.10);
|
||||
|
||||
// Input
|
||||
--input-bg: rgba(255, 255, 255, 0.60);
|
||||
--input-bg-hover: rgba(255, 255, 255, 0.75);
|
||||
--input-border: rgba(126, 87, 194, 0.15);
|
||||
--input-border-hover: rgba(126, 87, 194, 0.25);
|
||||
--input-border: rgba(var(--accent-rgb), 0.15);
|
||||
--input-border-hover: rgba(var(--accent-rgb), 0.25);
|
||||
--input-border-focus: var(--brand-600);
|
||||
--input-placeholder: rgba(12, 6, 20, 0.45);
|
||||
--input-text: var(--text-primary);
|
||||
|
||||
// Scrollbar
|
||||
--scrollbar-track: transparent;
|
||||
--scrollbar-thumb: rgba(126, 87, 194, 0.50);
|
||||
--scrollbar-thumb-hover: rgba(126, 87, 194, 0.75);
|
||||
--scrollbar-thumb: rgba(var(--accent-rgb), 0.50);
|
||||
--scrollbar-thumb-hover: rgba(var(--accent-rgb), 0.75);
|
||||
|
||||
// Dropdown
|
||||
--dropdown-bg: rgba(255, 255, 255, 0.97);
|
||||
|
||||
// Divider
|
||||
--divider: rgba(126, 87, 194, 0.10);
|
||||
--divider: rgba(var(--accent-rgb), 0.10);
|
||||
|
||||
// Focus ring
|
||||
--focus-ring: var(--brand-600);
|
||||
--focus-ring-offset: var(--surface-ground);
|
||||
|
||||
// Ambient orbs (stronger for light backgrounds)
|
||||
// Logo body color: white on light theme so it blends into the light sidebar —
|
||||
// only the accent-colored parts of the logo read against the background.
|
||||
--logo-fg: #ffffff;
|
||||
|
||||
// Logo accent color (follows --color-primary; overridden for 'default' preset to
|
||||
// restore the original darker purple).
|
||||
--logo-accent: var(--color-primary);
|
||||
|
||||
// Sidebar fade gradient (bottom of nav scroll area)
|
||||
--sidebar-fade: rgba(237, 233, 254, 0.90);
|
||||
--sidebar-fade: color-mix(in srgb, var(--brand-100) 90%, transparent);
|
||||
|
||||
// Ambient orbs (stronger for light backgrounds; retint with accent)
|
||||
--orb-opacity: 0.35;
|
||||
--orb-blur: 150px;
|
||||
--orb-primary: radial-gradient(circle, #9333ea, #6d28d9);
|
||||
--orb-secondary: radial-gradient(circle, #7c3aed, #2563eb);
|
||||
--orb-primary: radial-gradient(circle, var(--brand-500), var(--brand-700));
|
||||
--orb-secondary: radial-gradient(circle, var(--brand-400), var(--color-info));
|
||||
--orb-tertiary: radial-gradient(circle, #0891b2, #6366f1);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
--brand-900: #{$brand-900};
|
||||
--brand-950: #{$brand-950};
|
||||
|
||||
// Accent RGB triple - used by translucent surfaces (rgba(var(--accent-rgb), X)).
|
||||
// Must match $brand-500 in _variables.scss.
|
||||
// Overridden per preset in _accents.scss and at runtime by ThemeService for custom colors.
|
||||
--accent-rgb: 139, 92, 246;
|
||||
|
||||
// Semantic colors
|
||||
--color-success: #{$color-success};
|
||||
--color-success-dim: #{$color-success-dim};
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
// These feed into _tokens.scss (CSS custom properties) and _themes.scss.
|
||||
// =============================================================================
|
||||
|
||||
// Brand colors (Cleanuparr purple palette)
|
||||
$brand-50: #f3e8ff;
|
||||
$brand-100: #e9d5ff;
|
||||
$brand-200: #d8b4fe;
|
||||
$brand-300: #c084fc;
|
||||
$brand-400: #a855f7;
|
||||
$brand-500: #7E57C2;
|
||||
$brand-600: #6D28D9;
|
||||
$brand-700: #5B21B6;
|
||||
$brand-800: #4C1D95;
|
||||
$brand-900: #3B0764;
|
||||
$brand-950: #1e0038;
|
||||
// Brand colors (Cleanuparr violet palette — cooler purple, less pink-lavender)
|
||||
$brand-50: #f5f3ff;
|
||||
$brand-100: #ede9fe;
|
||||
$brand-200: #ddd6fe;
|
||||
$brand-300: #c4b5fd;
|
||||
$brand-400: #a78bfa;
|
||||
$brand-500: #8b5cf6;
|
||||
$brand-600: #7c3aed;
|
||||
$brand-700: #6d28d9;
|
||||
$brand-800: #5b21b6;
|
||||
$brand-900: #4c1d95;
|
||||
$brand-950: #2e1065;
|
||||
|
||||
// Semantic colors
|
||||
$color-success: #22c55e;
|
||||
|
||||
21
docs/static/img/cleanuparr.svg
vendored
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 69 KiB |