diff --git a/common/ExifIO.ts b/common/ExifIO.ts index 7c0bed0cc..9d6b58fd1 100644 --- a/common/ExifIO.ts +++ b/common/ExifIO.ts @@ -46,15 +46,13 @@ import { Awaited } from './core'; import fse from 'fs-extra'; import { action, makeObservable, observable, runInAction } from 'mobx'; import exiftool from 'node-exiftool'; -import path from 'path'; -import { IS_DEV, IS_WIN } from './process'; +import { IS_WIN } from './process'; +import { getExtraResourcePath } from './fs'; -// The exif binary is placed using ElectronBuilder's extraResources: https://www.electron.build/configuration/contents#extraresources -// there also is process.resourcesPath but that doesn't work in dev mode -const resourcesPath = (IS_DEV ? '../' : '../../') + 'resources' + '/exiftool'; +// The exif binary is placed using ElectronBuilder's extraResources: const exiftoolRunnable = IS_WIN ? 'exiftool.exe' : 'exiftool.pl'; -const EXIF_TOOL_PATH = path.resolve(__dirname, resourcesPath, exiftoolRunnable); +const EXIF_TOOL_PATH = getExtraResourcePath(`exiftool/${exiftoolRunnable}`); console.log('Exif tool path: ', EXIF_TOOL_PATH); const ep = new exiftool.ExiftoolProcess(EXIF_TOOL_PATH); diff --git a/common/fs.ts b/common/fs.ts index 1eaea975d..128f90d4d 100644 --- a/common/fs.ts +++ b/common/fs.ts @@ -2,7 +2,8 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require('path'); import fse from 'fs-extra'; -import { thumbnailFormat } from 'common/config'; +import { thumbnailFormat } from '../common/config'; +import { IS_DEV } from './process'; export function getThumbnailPath(filePath: string, thumbnailDirectory: string): string { const baseFilename = path.basename(filePath, path.extname(filePath)); @@ -57,3 +58,15 @@ function hashString(s: string) { } return hash; } + +/** + * Gets the path to a resource set up in `"extraResources`" in the package.json. + * See https://www.electron.build/configuration/contents#extraresources + * Could look into process.resourcesPath, but that doesn't seem to work in dev mode + * @param resourcePath The from from the resources directory, e.g. `"themes/myTheme.css"` + */ +export function getExtraResourcePath(resourcePath: string): string { + const relativeResourcesPath = (IS_DEV ? '../' : '../../') + 'resources'; + console.log({ relativeResourcesPath, __dirname, resourcePath }); + return path.resolve(__dirname, relativeResourcesPath, resourcePath); +} diff --git a/package.json b/package.json index b35e5072c..d48d7492c 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,9 @@ "resources/exiftool/.Exiftool_config" ] }, + "extraResources": [ + "resources/themes" + ], "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true diff --git a/resources/exiftool/README.md b/resources/exiftool/README.md new file mode 100644 index 000000000..2b87dd192 --- /dev/null +++ b/resources/exiftool/README.md @@ -0,0 +1,7 @@ +# Exiftool + +These files were downloaded from https://exiftool.org/index.html +- The `win` version was renamed from `exiftool(-k).exe` to `exiftool.exe`. This one does not need the `lib` folder. +- The `nix` version was extracted from the Perl archive (first download link), renamed from `exiftool` to `exiftool.pl`. This one requires the `lib` folder. + +Current version: 12.23 (April 1, 2021) diff --git a/resources/style/content.scss b/resources/style/content.scss index 8b397d19d..7041146f7 100644 --- a/resources/style/content.scss +++ b/resources/style/content.scss @@ -80,7 +80,7 @@ [aria-selected='true'] > .thumbnail { // If selected, show a blue border - background-color: var(--accent-color-blue); + background-color: var(--accent-color); > img, > .image-error { clip-path: inset(0.25rem round 0.125rem); // old design @@ -159,7 +159,7 @@ margin: auto; border-radius: 50%; border: 0.25rem solid var(--background-color); - border-top-color: var(--accent-color-blue); + border-top-color: var(--accent-color); animation: 1.5s spin infinite linear; @keyframes spin { @@ -187,7 +187,7 @@ contain: layout size; &[aria-selected='true'] { - background-color: var(--accent-color-blue); + background-color: var(--accent-color); color: white; } @@ -266,7 +266,7 @@ &:active, &:hover { - background-color: var(--accent-color-blue); + background-color: var(--accent-color); } } diff --git a/resources/style/inspector.scss b/resources/style/inspector.scss index e6ca6db1b..a0c8baa4d 100644 --- a/resources/style/inspector.scss +++ b/resources/style/inspector.scss @@ -44,7 +44,7 @@ &[aria-pressed='true'], &[aria-checked='true'] { - color: var(--accent-color-blue); + color: var(--accent-color); } } } diff --git a/resources/style/remake/global.scss b/resources/style/remake/global.scss index 122cb36c9..2bfedde83 100644 --- a/resources/style/remake/global.scss +++ b/resources/style/remake/global.scss @@ -96,7 +96,7 @@ select { &:focus-within { background: var(--input-color); - box-shadow: 0 0 0 0.125rem var(--accent-color-blue); + box-shadow: 0 0 0 0.125rem var(--accent-color); } } diff --git a/resources/style/remake/outliner.scss b/resources/style/remake/outliner.scss index cbe5d8b61..d46d306c8 100644 --- a/resources/style/remake/outliner.scss +++ b/resources/style/remake/outliner.scss @@ -88,7 +88,7 @@ &[aria-pressed='true'], &[aria-checked='true'] { - color: var(--accent-color-blue); + color: var(--accent-color); } } } @@ -113,19 +113,19 @@ margin-left: -2px; &[data-dnd-target='true'] { - color: var(--accent-color-blue); + color: var(--accent-color); border: 2px solid transparent; &.center { - border: 2px solid var(--accent-color-blue); + border: 2px solid var(--accent-color); } &.top { - border-top: 2px solid var(--accent-color-blue); + border-top: 2px solid var(--accent-color); } &.bottom { - border-bottom: 2px solid var(--accent-color-blue); + border-bottom: 2px solid var(--accent-color); } } @@ -186,7 +186,7 @@ &:hover, &[aria-pressed='true'], &[aria-checked='true'] { - color: var(--accent-color-blue); + color: var(--accent-color); } } diff --git a/resources/style/remake/themes.scss b/resources/style/remake/themes.scss index 69b09e3bf..80d8eee1e 100644 --- a/resources/style/remake/themes.scss +++ b/resources/style/remake/themes.scss @@ -12,18 +12,6 @@ --shade1: rgb(21, 23, 26); --shade2: rgb(18, 18, 24); - --input-color: var(--shade1); - --input-color-hover: var(--shade2); - - --hover-color: var(--shade1); - --hover-color-alt: var(--shade2); - - --scrollbar-track-color: var(--shade1); - --scrollbar-thumb-color: var(--text-color-muted); - --scrollbar-thumb-hover-color: var(--text-color); - - --border-color: var(--shade2); - --titlebar-background-color: #14181a; --titlebar-background-color-alt: #1f2224; --titlebar-color: #888; @@ -43,6 +31,21 @@ --shade1: rgb(232, 232, 232); --shade2: rgb(221, 221, 221); + --titlebar-background-color: rgb(221, 223, 226); + --titlebar-background-color-alt: rgb(201, 203, 206); + --titlebar-color: #888; +} + +// All themes +.dark, +.light { + --accent-color-blue: #147df1; + --accent-color-red: rgb(250, 52, 37); + --accent-color-yellow: rgb(255, 193, 47); + --accent-color-green: #7af2a6; + --box-shadow: 0.125rem 0.25rem 1rem rgba(0, 0, 0, 0.125); + --accent-color: var(--accent-color-blue); + --input-color: var(--shade1); --input-color-hover: var(--shade2); @@ -54,21 +57,18 @@ --scrollbar-thumb-hover-color: var(--text-color); --border-color: var(--shade2); + --input-color: var(--shade1); + --input-color-hover: var(--shade2); - --titlebar-background-color: rgb(221, 223, 226); - --titlebar-background-color-alt: rgb(201, 203, 206); - --titlebar-color: #888; -} + --hover-color: var(--shade1); + --hover-color-alt: var(--shade2); + + --scrollbar-track-color: var(--shade1); + --scrollbar-thumb-color: var(--text-color-muted); + --scrollbar-thumb-hover-color: var(--text-color); + + --border-color: var(--shade2); -// All themes -.dark, -.light { - --accent-color-blue: #147df1; /* rgb(51, 153, 255); */ - --accent-color-red: rgb(250, 52, 37); - --accent-color-yellow: rgb(255, 193, 47); - --accent-color-green: #7af2a6; /*rgb(104, 232, 188);*/ - --box-shadow: 0.125rem 0.25rem 1rem rgba(0, 0, 0, 0.125); - --accent-color: var(--accent-color-blue); color: var(--text-color); background: var(--background-color); } diff --git a/resources/test_images/README.md b/resources/test_images/README.md new file mode 100644 index 000000000..a64d1df77 --- /dev/null +++ b/resources/test_images/README.md @@ -0,0 +1,5 @@ +# Test images +It would be nice to have automated tests for ensuring we support all image types and filenames we intend to support, but it's quite difficult to set up. + +For now, just add this directory as a Location in Allusion to confirm all files we want to support show up properly. +Feel free to add more test images. diff --git a/resources/themes/README.md b/resources/themes/README.md new file mode 100644 index 000000000..abca42fdd --- /dev/null +++ b/resources/themes/README.md @@ -0,0 +1,21 @@ +# Custom themes + +This folder contains a couple of theme presets, that will be copied to the user's Allusion custom themes directory when installing Allusion. + +## Creating a custom theme +The default theme of Allusion can be used as a reference, which can be found in the `/resources/style/remake/themes.scss` file of Allusion's source code, which can be found [here](https://github.com/allusion-app/Allusion/blob/master/resources/style/remake/themes.scss). + +Feel free to share yours and try those of others through the [Github Discussions](https://github.com/allusion-app/Allusion/discussions/categories/show-and-tell)! + +## Loading a custom theme +1. Open the settings panel +2. In Appearance > Theme Customization, click the Folder button to open the "themes" folder +3. Copy one of the preset files or create a new `.css` file with the name of your theme and customize it to your liking +4. Click the Refresh button in the settings panel +5. Pick the theme from the dropdown menu +6. Enjoy! + +## DevTools +The built-in Chrome Dev Tools can be used to preview colors intuitively before noting them down in a CSS file. It can be accessed through the settings panel at Advanced > Toggle DevTools or by pressing `Ctrl + Shift + I` in the main Allusion window. + +The theme colors are applied through the `.light` and `.dark` classes, which are set on the first HTML child element of `body > #app`. diff --git a/resources/themes/dimmed.css b/resources/themes/dimmed.css new file mode 100644 index 000000000..d06691ece --- /dev/null +++ b/resources/themes/dimmed.css @@ -0,0 +1,35 @@ +.dark { + --text-color: rgb(200, 200, 200); + --text-color-alt: rgb(0, 0, 0); + --text-color-muted: rgb(128, 128, 128); + --text-color-strong: rgb(255, 255, 255); + + --background-color: hsl(223, 11%, 20%); + --background-color-alt: hsl(229, 14%, 25%); + --background-color-selected: hsl(209, 48%, 40%); + + --shade1: hsl(216, 11%, 15%); + --shade2: hsl(240, 14%, 13%); + + --titlebar-background-color: hsl(200, 13%, 14%); + --titlebar-background-color-alt: hsl(204, 7%, 18%); + --titlebar-color: #777; +} + +.light { + --text-color: hsl(0, 0%, 20%); + --text-color-alt: hsl(0, 0%, 90%); + --text-color-muted: rgb(128, 128, 128); + --text-color-strong: hsl(0, 0%, 10%); + + --background-color: hsl(0, 0%, 85%); + --background-color-alt: hsl(0, 0%, 92%); + --background-color-selected: hsl(209, 66%, 72%); + + --shade1: hsl(0, 0%, 86%); + --shade2: hsl(0, 0%, 78%); + + --titlebar-background-color: hsl(216, 8%, 83%); + --titlebar-background-color-alt: hsl(216, 5%, 75%); + --titlebar-color: #777; +} diff --git a/resources/themes/monochrome.css b/resources/themes/monochrome.css new file mode 100644 index 000000000..d2ce61a8a --- /dev/null +++ b/resources/themes/monochrome.css @@ -0,0 +1,39 @@ +.dark { + --text-color: rgb(200, 200, 200); + --text-color-alt: rgb(0, 0, 0); + --text-color-muted: rgb(128, 128, 128); + --text-color-strong: rgb(255, 255, 255); + + --background-color: hsl(0, 0%, 12%); + --background-color-alt: hsl(0, 0%, 16%); + --background-color-selected: hsl(209, 0%, 35%); + + --shade1: hsl(216, 0%, 9%); + --shade2: hsl(240, 0%, 8%); + + --titlebar-background-color: hsl(200, 0%, 9%); + --titlebar-background-color-alt: hsl(204, 0%, 13%); + --titlebar-color: #888; +} + +.light { + --text-color: rgb(64, 64, 64); + --text-color-alt: rgb(255, 255, 255); + --text-color-muted: rgb(128, 128, 128); + --text-color-strong: rgb(0, 0, 0); + + --background-color: rgb(240, 240, 240); + --background-color-alt: rgb(248, 248, 248); + --background-color-selected: hsl(209, 0%, 77%); + + --shade1: hsl(0, 0%, 91%); + --shade2: hsl(0, 0%, 87%); + + --titlebar-background-color: hsl(0, 0%, 88%); + --titlebar-background-color-alt: hsl(216, 0%, 80%); + --titlebar-color: #888; +} + +.dark, .light { + --accent-color: #888; +} diff --git a/resources/themes/readme.md b/resources/themes/readme.md new file mode 100644 index 000000000..abca42fdd --- /dev/null +++ b/resources/themes/readme.md @@ -0,0 +1,21 @@ +# Custom themes + +This folder contains a couple of theme presets, that will be copied to the user's Allusion custom themes directory when installing Allusion. + +## Creating a custom theme +The default theme of Allusion can be used as a reference, which can be found in the `/resources/style/remake/themes.scss` file of Allusion's source code, which can be found [here](https://github.com/allusion-app/Allusion/blob/master/resources/style/remake/themes.scss). + +Feel free to share yours and try those of others through the [Github Discussions](https://github.com/allusion-app/Allusion/discussions/categories/show-and-tell)! + +## Loading a custom theme +1. Open the settings panel +2. In Appearance > Theme Customization, click the Folder button to open the "themes" folder +3. Copy one of the preset files or create a new `.css` file with the name of your theme and customize it to your liking +4. Click the Refresh button in the settings panel +5. Pick the theme from the dropdown menu +6. Enjoy! + +## DevTools +The built-in Chrome Dev Tools can be used to preview colors intuitively before noting them down in a CSS file. It can be accessed through the settings panel at Advanced > Toggle DevTools or by pressing `Ctrl + Shift + I` in the main Allusion window. + +The theme colors are applied through the `.light` and `.dark` classes, which are set on the first HTML child element of `body > #app`. diff --git a/src/Messaging.ts b/src/Messaging.ts index a532938f3..8ce53c5d9 100644 --- a/src/Messaging.ts +++ b/src/Messaging.ts @@ -238,6 +238,11 @@ export class RendererMessenger { const userDataPath = await RendererMessenger.getPath('userData'); return path.join(userDataPath, 'backups'); }; + + static getThemesDirectory = async () => { + const userDataPath = await RendererMessenger.getPath('userData'); + return path.join(userDataPath, 'themes'); + }; } export class MainMessenger { diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 57d6a2d20..ef30e8810 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -16,6 +16,7 @@ import WindowTitlebar from './containers/WindowTitlebar'; import { DropContextProvider } from './contexts/DropContext'; import Main from './containers/Main'; import About from './containers/About'; +import { CustomThemeProvider } from './hooks/useCustomTheme'; const SPLASH_SCREEN_TIME = 1400; const PLATFORM = process.platform; @@ -52,30 +53,32 @@ const App = observer(() => { } return ( - -
- {!uiStore.isFullScreen && } + + +
+ {!uiStore.isFullScreen && } - -
+ +
- + - + - + - + - - -
-
+ + +
+
+ ); }); diff --git a/src/frontend/components/PopupWindow.tsx b/src/frontend/components/PopupWindow.tsx index 8dd98ae7f..5382d0ea5 100644 --- a/src/frontend/components/PopupWindow.tsx +++ b/src/frontend/components/PopupWindow.tsx @@ -30,6 +30,11 @@ const PopupWindow: React.FC = (props) => { copyStyles(document, externalWindow.document); containerEl.setAttribute('data-os', PLATFORM); + // Hacky func for re-applying CSS to settings when changing that of the main window + (window as any).reapplyPopupStyles = () => { + copyStyles(document, externalWindow.document); + }; + externalWindow.addEventListener('beforeunload', props.onClose); if (props.closeOnEscape) { @@ -62,6 +67,11 @@ const PopupWindow: React.FC = (props) => { export default PopupWindow; function copyStyles(sourceDoc: Document, targetDoc: Document) { + // First clear any existing styles + ['style', 'link'].forEach((t) => + Array.from(targetDoc.getElementsByTagName(t)).forEach((i) => i.parentElement?.removeChild(i)), + ); + for (let i = 0; i < sourceDoc.styleSheets.length; i++) { const styleSheet = sourceDoc.styleSheets[i]; // production mode bundles CSS in one file diff --git a/src/frontend/containers/Settings/index.tsx b/src/frontend/containers/Settings/index.tsx index 27b40f927..42cf19eab 100644 --- a/src/frontend/containers/Settings/index.tsx +++ b/src/frontend/containers/Settings/index.tsx @@ -1,13 +1,16 @@ +import { chromeExtensionUrl } from 'common/config'; +import { getFilenameFriendlyFormattedDateTime } from 'common/fmt'; +import { getThumbnailPath, isDirEmpty } from 'common/fs'; +import { WINDOW_STORAGE_KEY } from 'common/window'; import { shell } from 'electron'; import { runInAction } from 'mobx'; import { observer } from 'mobx-react-lite'; import SysPath from 'path'; import React, { ReactNode, useCallback, useEffect, useState } from 'react'; -import { chromeExtensionUrl } from 'common/config'; import { IMG_EXTENSIONS, IMG_EXTENSIONS_TYPE } from 'src/entities/File'; import { AppToaster } from 'src/frontend/components/Toaster'; +import useCustomTheme from 'src/frontend/hooks/useCustomTheme'; import { RendererMessenger } from 'src/Messaging'; -import { WINDOW_STORAGE_KEY } from 'common/window'; import { Button, ButtonGroup, @@ -23,8 +26,6 @@ import { Alert, DialogButton } from 'widgets/popovers'; import PopupWindow from '../../components/PopupWindow'; import { useStore } from '../../contexts/StoreContext'; import { moveThumbnailDir } from '../../image/ThumbnailGeneration'; -import { getFilenameFriendlyFormattedDateTime } from 'common/fmt'; -import { getThumbnailPath, isDirEmpty } from 'common/fs'; import { ClearDbButton } from '../ErrorBoundary'; import HotkeyMapper from './HotkeyMapper'; import Tabs, { TabItem } from './Tabs'; @@ -73,14 +74,18 @@ const Appearance = observer(() => { + + + +
+ +
Full screen
- -

Thumbnail

@@ -154,6 +159,41 @@ const Zoom = () => { ); }; +const CustomThemePicker = () => { + const { theme, setTheme, refresh, options, themeDir } = useCustomTheme(); + + useEffect(() => { + refresh(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ Theme customization + {' '} + + shell.showItemInFolder(themeDir)} + data-tooltip="Open the directory containing the theme files" + /> +
+ ); +}; + const ImportExport = observer(() => { const rootStore = useStore(); const { fileStore, tagStore, exifTool } = rootStore; diff --git a/src/frontend/hooks/useCustomTheme.tsx b/src/frontend/hooks/useCustomTheme.tsx new file mode 100644 index 000000000..bf8a5c2dc --- /dev/null +++ b/src/frontend/hooks/useCustomTheme.tsx @@ -0,0 +1,127 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import fse from 'fs-extra'; +import path from 'path'; + +import useLocalStorage from './useLocalStorage'; +import { RendererMessenger } from 'src/Messaging'; +import { AppToaster } from '../components/Toaster'; +import { getExtraResourcePath } from '../../../common/fs'; + +type CustomThemeContextType = { + theme: string; + setTheme: (filename: string) => void; + themeDir: string; + options: string[]; + refresh: () => void; +}; + +const CustomThemeContext = React.createContext({ + theme: '', + // eslint-disable-next-line @typescript-eslint/no-empty-function + setTheme: () => {}, + themeDir: '', + options: [], + // eslint-disable-next-line @typescript-eslint/no-empty-function + refresh: () => {}, +}); + +/** Copies preset themes from /resources/themes into the user's custom theme directory */ +async function copyPresets(themeDir: string) { + try { + const presetDir = getExtraResourcePath('themes'); + const files = await fse.readdir(presetDir); + for (const file of files) { + await fse.copy(`${presetDir}/${file}`, `${themeDir}/${file}`); + } + } catch (e) { + console.error(e); + } +} + +async function loadThemeFiles(themeDir: string): Promise { + try { + await fse.ensureDir(themeDir); + + // Place default custom themes from resources/themes in the theme directory + await copyPresets(themeDir); + + const files = await fse.readdir(themeDir); + return files.filter((f) => f.endsWith('.css')); + } catch (e) { + console.error(e); + } + return []; +} + +function applyTheme(themeDir: string, filename: string) { + // First clear the previously applied custom theme + const customThemeLinkId = 'custom-theme-link'; + document.getElementById(customThemeLinkId)?.remove(); + + // Then apply the new one + if (filename) { + const link = document.createElement('link'); + link.id = customThemeLinkId; + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = `file:///${path.join(themeDir, filename)}`; + + link.onerror = () => + AppToaster.show({ + message: 'Could not load theme', + timeout: 5000, + clickAction: { onClick: RendererMessenger.toggleDevTools, label: 'Toggle DevTools' }, + }); + + // The style of the settings panel doesn't automatically update, since it's a separate window + // This function is exposed in the PopupWindow component as a workaround + link.onload = () => (window as any).reapplyPopupStyles?.(); + + document.head.appendChild(link); + } else { + (window as any).reapplyPopupStyles?.(); + } +} + +export const CustomThemeProvider = ({ children }: { children: React.ReactNode }) => { + const [themeDir, setThemeDir] = useState(''); + const [theme, setTheme] = useLocalStorage('custom-theme', ''); + const [options, setOptions] = useState([]); + + useEffect(() => { + // Load options and previously selected theme on startup + RendererMessenger.getThemesDirectory().then((dir) => { + setThemeDir(dir); + loadThemeFiles(dir).then(setOptions); + if (theme) { + applyTheme(dir, theme); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const refresh = useCallback(() => { + loadThemeFiles(themeDir).then(setOptions); + applyTheme(themeDir, theme); + }, [theme, themeDir]); + + const setThemeAndApply = useCallback( + (filename: string) => { + setTheme(filename); + applyTheme(themeDir, filename); + }, + [setTheme, themeDir], + ); + + return ( + + {children} + + ); +}; + +export default function useCustomTheme() { + return useContext(CustomThemeContext); +} diff --git a/widgets/Tree/tree.scss b/widgets/Tree/tree.scss index 46f8eb0f3..488fa0460 100644 --- a/widgets/Tree/tree.scss +++ b/widgets/Tree/tree.scss @@ -71,7 +71,7 @@ // If focused, show an outline // If selected as well: do nothing special: keep the selected background color // hacky solution so outline shows both horizontal and vertical, and layout doesn't shift. I don't know what I"m doing - border: 2px solid var(--accent-color-blue); + border: 2px solid var(--accent-color); // TODO: proper background color change for hover } diff --git a/widgets/menus/menu.scss b/widgets/menus/menu.scss index 373b62fb8..e29432b1c 100644 --- a/widgets/menus/menu.scss +++ b/widgets/menus/menu.scss @@ -119,7 +119,7 @@ a[role='menuitem'] { } &::-webkit-slider-thumb { - background: var(--accent-color-blue); + background: var(--accent-color); cursor: pointer; } }