diff --git a/.vscode/settings.json b/.vscode/settings.json index 126df5835..cd6c404d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -115,5 +115,15 @@ "stylelint.validate": [ "css", "scss" - ] + ], + "[scss]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint", + "editor.formatOnSave": true, + }, + "[typescript]": { + "editor.formatOnSave": true, + }, + "[javascript]": { + "editor.formatOnSave": true + } } diff --git a/src/common/settings/migration.ts b/src/common/settings/migration.ts index f856009b6..5d3ef69eb 100644 --- a/src/common/settings/migration.ts +++ b/src/common/settings/migration.ts @@ -55,3 +55,26 @@ export const migrateOldStorageSettings = (settings: ReadonlyStorage): Partial { + const themeMap: Record = { + dark: 'default-dark', + light: 'default-light', + system: 'default-system', + }; + if (settings.theme && Object.hasOwn(themeMap, settings.theme)) { + // eslint-disable-next-line no-param-reassign + settings.theme = themeMap[settings.theme]; + } + return settings; +}; + +type SettingsMigration = (settings: Partial) => Partial; +const migrations: SettingsMigration[] = [newThemeSystem]; +export const migrateSettings = (settings: Partial): ChainnerSettings => { + for (const migration of migrations) { + // eslint-disable-next-line no-param-reassign + settings = migration(settings); + } + return { ...defaultSettings, ...settings }; +}; diff --git a/src/common/settings/settings.ts b/src/common/settings/settings.ts index 5861a2dcb..3f26a9035 100644 --- a/src/common/settings/settings.ts +++ b/src/common/settings/settings.ts @@ -5,7 +5,7 @@ export interface ChainnerSettings { systemPythonLocation: string; // renderer - theme: 'light' | 'dark' | 'system'; + theme: string; checkForUpdatesOnStartup: boolean; startupTemplate: string; animateChain: boolean; @@ -32,7 +32,7 @@ export const defaultSettings: Readonly = { systemPythonLocation: '', // renderer - theme: 'dark', + theme: 'default-dark', checkForUpdatesOnStartup: true, startupTemplate: '', animateChain: true, diff --git a/src/main/setting-storage.ts b/src/main/setting-storage.ts index 34821242c..b25c5b4c5 100644 --- a/src/main/setting-storage.ts +++ b/src/main/setting-storage.ts @@ -1,7 +1,8 @@ +import deepEqual from 'fast-deep-equal'; import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs'; import { LocalStorage } from 'node-localstorage'; import path from 'path'; -import { migrateOldStorageSettings } from '../common/settings/migration'; +import { migrateOldStorageSettings, migrateSettings } from '../common/settings/migration'; import { ChainnerSettings, defaultSettings } from '../common/settings/settings'; import { getRootDir } from './platform'; @@ -14,7 +15,17 @@ export const writeSettings = (settings: ChainnerSettings) => { export const readSettings = (): ChainnerSettings => { if (existsSync(settingsJson)) { // settings.json - return JSON.parse(readFileSync(settingsJson, 'utf-8')) as ChainnerSettings; + const fileContent = readFileSync(settingsJson, 'utf-8'); + const partialSettings = JSON.parse(fileContent) as Partial; + const originalPartialSettings = { ...partialSettings }; + const settings = migrateSettings(partialSettings); + + if (!deepEqual(originalPartialSettings, settings)) { + // write settings if they aren't up to date + writeSettings(settings); + } + + return settings; } // legacy settings @@ -29,10 +40,7 @@ export const readSettings = (): ChainnerSettings => { keys: Array.from({ length: storage.length }, (_, i) => storage.key(i)), getItem: (key: string) => storage.getItem(key), }); - const settings: ChainnerSettings = { - ...defaultSettings, - ...partialSettings, - }; + const settings = migrateSettings(partialSettings); // write a new settings.json we'll use form now on writeSettings(settings); diff --git a/src/renderer/colors.scss b/src/renderer/colors.scss index beada085d..a7d4d3701 100644 --- a/src/renderer/colors.scss +++ b/src/renderer/colors.scss @@ -1,85 +1,72 @@ @use 'sass:color'; +@use 'sass:map'; -:root { - $gray-100: #edf2f7; - $gray-200: #e2e8f0; - $gray-300: #cbd5e0; - $gray-400: #a0aec0; - $gray-500: #718096; - $gray-600: #4a5568; - $gray-700: #2d3748; - $gray-800: #1a202c; - $gray-900: #171923; - - /* color palette */ - --gray-50: #f7fafc; - --gray-100: #{$gray-100}; - --gray-125: #{color.mix($gray-100, $gray-200, 25%)}; - --gray-150: #{color.mix($gray-100, $gray-200, 50%)}; - --gray-175: #{color.mix($gray-100, $gray-200, 75%)}; - --gray-200: #{$gray-200}; - --gray-250: #{color.mix($gray-200, $gray-300, 50%)}; - --gray-275: #{color.mix($gray-200, $gray-300, 75%)}; - --gray-300: #{$gray-300}; - --gray-325: #{color.mix($gray-300, $gray-400, 25%)}; - --gray-400: #{$gray-400}; - --gray-500: #{$gray-500}; - --gray-600: #{$gray-600}; - --gray-650: #{color.mix($gray-600, $gray-700, 50%)}; - --gray-700: #{$gray-700}; - --gray-750: #{color.mix($gray-700, $gray-800, 50%)}; - --gray-800: #{$gray-800}; - --gray-850: #{color.mix($gray-800, $gray-900, 50%)}; - --gray-900: #{$gray-900}; +@mixin theme-variables($theme) { + @each $shade, $color in $theme { + $next-shade: map.get($theme, $shade + 100); + --theme-#{$shade}: #{$color}; + @if $next-shade { + $twenty-five-percent: color.mix($color, $next-shade, 75%); + $fifty-percent: color.mix($color, $next-shade, 50%); + $seventy-five-percent: color.mix($color, $next-shade, 25%); + + --theme-#{$shade + 25}: #{$twenty-five-percent}; + --theme-#{$shade + 50}: #{$fifty-percent}; + --theme-#{$shade + 75}: #{$seventy-five-percent}; + } + } +} + +:root[data-theme='dark'] { /* backgrounds */ - --bg-800: var(--gray-800); - --bg-700: var(--gray-700); - --bg-600: var(--gray-600); + --bg-800: var(--theme-800); + --bg-700: var(--theme-700); + --bg-600: var(--theme-600); /* font colors */ --fg-000: white; - --fg-300: var(--gray-300); + --fg-300: var(--theme-300); --link-color: var(--chakra-colors-blue-300); /* node colors */ - --node-border-color: var(--gray-800); - --node-bg-color: var(--gray-750); - --node-icon-color: var(--gray-200); + --node-border-color: var(--theme-800); + --node-bg-color: var(--theme-750); + --node-icon-color: var(--theme-200); - --node-valid-bg: var(--gray-800); - --node-valid-fg: var(--gray-100); + --node-valid-bg: var(--theme-800); + --node-valid-fg: var(--theme-100); --node-invalid-bg: var(--chakra-colors-red-600); - --node-invalid-fg: var(--gray-800); + --node-invalid-fg: var(--theme-800); - --node-disable-bg: var(--gray-800); - --node-disable-fg: var(--gray-400); + --node-disable-bg: var(--theme-800); + --node-disable-fg: var(--theme-400); - --node-timer-bg: var(--gray-800); - --node-timer-fg: var(--gray-600); + --node-timer-bg: var(--theme-800); + --node-timer-fg: var(--theme-600); --node-image-preview-bg: var(--node-bg-color); - --node-image-preview-color: var(--gray-400); + --node-image-preview-color: var(--theme-400); - --node-image-preview-button-bg: var(--gray-750); - --node-image-preview-button-bg-hover: var(--gray-600); - --node-image-preview-button-fg: var(--gray-400); + --node-image-preview-button-bg: var(--theme-750); + --node-image-preview-button-bg-hover: var(--theme-600); + --node-image-preview-button-fg: var(--theme-400); - --tag-bg: var(--gray-750); - --tag-fg: var(--gray-300); + --custom-tag-bg: var(--theme-750); + --custom-tag-fg: var(--theme-300); - --connection-color: #171923; - --chain-hole-color: #1a202c; + --connection-color: var(--theme-900); // #171923; + --chain-hole-color: var(--theme-800); // #1a202c; /* node selector colors */ - --selector-node-bg: var(--gray-700); - --selector-icon: var(--gray-400); - --selector-text-bg: var(--gray-700); - --selector-text-bg-hover: var(--gray-600); + --selector-node-bg: var(--theme-700); + --selector-icon: var(--theme-400); + --selector-text-bg: var(--theme-700); + --selector-text-bg-hover: var(--theme-600); /* specific colors */ - --window-bg: var(--gray-900); + --window-bg: var(--theme-900); --header-bg: var(--bg-800); --chain-editor-bg: var(--bg-800); --node-selector-bg: var(--bg-800); @@ -87,51 +74,246 @@ :root[data-theme='light'] { /* backgrounds */ - --bg-800: var(--gray-50); - --bg-700: var(--gray-175); - --bg-600: var(--gray-200); + --bg-800: var(--theme-50); + --bg-700: var(--theme-175); + --bg-600: var(--theme-200); /* font colors */ --fg-000: black; - --fg-300: var(--gray-500); + --fg-300: var(--theme-500); --link-color: var(--chakra-colors-blue-500); /* node colors */ - --node-border-color: var(--gray-500); - --node-bg-color: var(--gray-250); - --node-icon-color: var(--gray-800); + --node-border-color: var(--theme-500); + --node-bg-color: var(--theme-250); + --node-icon-color: var(--theme-800); - --node-valid-bg: var(--gray-150); - --node-valid-fg: var(--gray-900); + --node-valid-bg: var(--theme-150); + --node-valid-fg: var(--theme-900); --node-invalid-bg: var(--chakra-colors-red-500); - --node-invalid-fg: var(--gray-200); + --node-invalid-fg: var(--theme-200); - --node-disable-bg: var(--gray-150); - --node-disable-fg: var(--gray-500); + --node-disable-bg: var(--theme-150); + --node-disable-fg: var(--theme-500); - --node-timer-bg: var(--gray-150); - --node-timer-fg: var(--gray-500); + --node-timer-bg: var(--theme-150); + --node-timer-fg: var(--theme-500); --node-image-preview-bg: var(--node-bg-color); - --node-image-preview-color: var(--gray-700); + --node-image-preview-color: var(--theme-700); - --node-image-preview-button-bg: var(--gray-300); - --node-image-preview-button-bg-hover: var(--gray-400); - --node-image-preview-button-fg: var(--gray-700); + --node-image-preview-button-bg: var(--theme-300); + --node-image-preview-button-bg-hover: var(--theme-400); + --node-image-preview-button-fg: var(--theme-700); - --tag-bg: var(--gray-300); - --tag-fg: var(--gray-700); + --custom-tag-bg: var(--theme-200) !important; + --custom-tag-fg: var(--theme-700); - --connection-color: var(--gray-50); - --chain-hole-color: var(--gray-50); + --connection-color: var(--theme-50); + --chain-hole-color: var(--theme-50); /* node selector colors */ - --selector-node-bg: var(--gray-150); - --selector-icon: var(--gray-600); - --selector-text-bg: var(--gray-150); - --selector-text-bg-hover: var(--gray-250); + --selector-node-bg: var(--theme-150); + --selector-icon: var(--theme-600); + --selector-text-bg: var(--theme-150); + --selector-text-bg-hover: var(--theme-250); /* specific colors */ - --window-bg: var(--gray-150); + --window-bg: var(--theme-150); + --header-bg: var(--bg-800); + --chain-editor-bg: var(--bg-800); + --node-selector-bg: var(--bg-800); +} + +// Default theme (copied from Chakra UI) +:root[data-custom-theme='default-dark'], +:root[data-custom-theme='default-light'], +:root[data-custom-theme='default-system'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #edf2f7, + 200: #e2e8f0, + 300: #cbd5e0, + 400: #a0aec0, + 500: #718096, + 600: #4a5568, + 700: #2d3748, + 800: #1a202c, + 900: #171923, + ) + ); +} + +// Charcoal theme +:root[data-custom-theme='charcoal-dark'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #d3d3d9, + 200: #a7a7b4, + 300: #7b7b8e, + 400: #545463, + 500: #2e2e36, + 600: #26262c, + 700: #1c1c21, + 800: #0e0e10, + 900: #09090b, + ) + ); +} + +// Coffee theme +:root[data-custom-theme='coffee-dark'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #ddd2ce, + 200: #bba69f, + 300: #9a7d72, + 500: #795548, + 600: #5a4037, + 700: #3d2c26, + 800: #161210, + 900: #0c0b0b, + ) + ); +} + +// Blueberry Theme +:root[data-custom-theme='blueberry-dark'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #cdd2ec, + 200: #9ba6d8, + 300: #667dc4, + 500: #1e56b0, + 600: #204181, + 700: #1c2c55, + 800: #0f1422, + 900: #0a0e18, + ) + ); +} + +// Dusk Theme +:root[data-custom-theme='dusk-dark'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #c6c3da, + 200: #908ab6, + 300: #595593, + 500: #1d2570, + 600: #1b1e54, + 700: #171639, + 800: #0e0a1a, + 900: #08050f, + ) + ); +} + +// OLED Theme +:root[data-custom-theme='oled-dark'] { + @include theme-variables( + ( + 000: #d4d4d4, + 100: #949494, + 200: #646464, + 300: #414141, + 500: #363636, + 600: #292929, + 700: #181818, + 800: #000000, + 900: #000000, + ) + ); +} + +// Cyberpunk Theme +:root[data-custom-theme='cyberpunk-dark'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #f7edf5, + 200: #f0e2ec, + 300: #e0cbda, + 400: #c0a0b8, + 500: #96717f, + 600: #684a63, + 700: #002d30, + 800: #3a0323, + 900: #231721, + ) + ); + --chain-editor-bg: #000; + --header-bg: #000; + --window-bg: #140206; + + --selector-node-bg: #16939c; + --node-selector-bg: #3b0b1a; + --selector-icon: #ef0; + + --node-border-color: #ef0; + --node-bg-color: #000d0e; + --node-icon-color: var(--theme-200); +} + +// "Mixer" Theme +:root[data-custom-theme='mixer-dark'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #ebebeb, + 200: #e0e0e0, + 300: #d6d6d6, + 400: #b1b1b1, + 500: #585858, + 600: #5a5a5a, + 700: #3f3f3f, + 800: #232323, + 900: #151515, + ) + ); +} + +// "NotReal" Theme +:root[data-custom-theme='notreal-dark'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #ebebeb, + 200: #e0e0e0, + 300: #d6d6d6, + 400: #b1b1b1, + 500: #585858, + 600: #171916, + 700: #10120f, + 800: #171916, + 900: #151515, + ) + ); + --chain-editor-bg: #272727; + --node-selector-bg: #202020; + --header-bg: #202020; +} + +// "ComfortUI" Theme +:root[data-custom-theme='comfort-dark'] { + @include theme-variables( + ( + 000: #ffffff, + 100: #ebebeb, + 200: #e0e0e0, + 300: #d6d6d6, + 400: #b1b1b1, + 500: #585858, + 600: #5a5a5a, + 700: #363636, + 800: #212121, + 900: #151515, + ) + ); } diff --git a/src/renderer/components/SettingsModal.tsx b/src/renderer/components/SettingsModal.tsx index 12470fb56..1bb293e36 100644 --- a/src/renderer/components/SettingsModal.tsx +++ b/src/renderer/components/SettingsModal.tsx @@ -54,14 +54,23 @@ const AppearanceSettings = memo(() => { w="full" > setTheme(value as never)} + setValue={(value) => setTheme(value)} setting={{ - label: 'Select Theme', + label: 'Color Theme', description: "Choose the Theme for chaiNNer's appearance.", options: [ - { label: 'Dark Mode', value: 'dark' }, - { label: 'Light Mode', value: 'light' }, - { label: 'System', value: 'system' }, + { label: 'Dark', value: 'default-dark' }, + { label: 'Light', value: 'default-light' }, + { label: 'System', value: 'default-system' }, + { label: 'Charcoal', value: 'charcoal-dark' }, + { label: 'Coffee', value: 'coffee-dark' }, + { label: 'Blueberry', value: 'blueberry-dark' }, + { label: 'Dusk', value: 'dusk-dark' }, + { label: 'OLED', value: 'oled-dark' }, + { label: 'Cyberpunk', value: 'cyberpunk-dark' }, + { label: 'Mixer3D', value: 'mixer-dark' }, + { label: 'NotRealEngine', value: 'notreal-dark' }, + { label: 'ComfortUI', value: 'comfort-dark' }, ], small: true, }} diff --git a/src/renderer/components/TypeTag.tsx b/src/renderer/components/TypeTag.tsx index 586f098c6..8da35b07e 100644 --- a/src/renderer/components/TypeTag.tsx +++ b/src/renderer/components/TypeTag.tsx @@ -263,8 +263,8 @@ export const TypeTag = memo( const { isOptional, ...rest } = props; return ( { - setColorMode(settings.theme); + const [, darkOrLight] = settings.theme.split('-'); + setColorMode(darkOrLight); + document.documentElement.setAttribute('data-custom-theme', settings.theme); }, [setColorMode, settings.theme]); const contextValue = useMemoObject({ diff --git a/src/renderer/global.scss b/src/renderer/global.scss index ecf610764..803162258 100644 --- a/src/renderer/global.scss +++ b/src/renderer/global.scss @@ -98,7 +98,7 @@ body { } &:disabled > svg { - fill: var(--gray-600); + fill: var(--theme-600); } } @@ -120,7 +120,7 @@ body { [data-theme='light'] { .react-flow__controls:hover { - background: var(--gray-300) !important; + background: var(--theme-300) !important; } .react-flow__minimap { @@ -145,19 +145,19 @@ body { background: rgb(203 213 224 / 66%) !important; &:not(:disabled):hover { - background: var(--gray-400) !important; - border: 0 solid var(--gray-500) !important; + background: var(--theme-400) !important; + border: 0 solid var(--theme-500) !important; } &:disabled > svg { - fill: var(--gray-400); + fill: var(--theme-400); } } } [data-theme='dark'] { .react-flow__controls:hover { - background: var(--gray-700) !important; + background: var(--theme-700) !important; } .react-flow__controls-button { @@ -165,8 +165,8 @@ body { background: rgb(45 55 72 / 66%) !important; &:not(:disabled):hover { - background: var(--gray-600) !important; - border: 0 solid var(--gray-600) !important; + background: var(--theme-600) !important; + border: 0 solid var(--theme-600) !important; } &:not(:disabled) > svg { @@ -205,12 +205,12 @@ body { [data-theme='dark'] ::-webkit-scrollbar-thumb { border-radius: 8px; - background-color: var(--gray-700); + background-color: var(--theme-700); } [data-theme='light'] ::-webkit-scrollbar-thumb { border-radius: 8px; - background-color: var(--gray-300); + background-color: var(--theme-300); } #menu-button-global-context-menu { diff --git a/src/renderer/hooks/useThemeColor.ts b/src/renderer/hooks/useThemeColor.ts index 5a9dda992..1a2dc8754 100644 --- a/src/renderer/hooks/useThemeColor.ts +++ b/src/renderer/hooks/useThemeColor.ts @@ -1,14 +1,17 @@ import { useColorMode } from '@chakra-ui/react'; import { useMemo } from 'react'; import { lazy } from '../../common/util'; +import { useSettings } from '../contexts/SettingsContext'; const light = lazy(() => getComputedStyle(document.documentElement)); const dark = lazy(() => getComputedStyle(document.documentElement)); export const useThemeColor = (name: `--${string}`): string => { const { colorMode } = useColorMode(); + const { theme } = useSettings(); return useMemo(() => { const styles = colorMode === 'dark' ? dark() : light(); return styles.getPropertyValue(name).trim(); - }, [colorMode, name]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [colorMode, name, theme]); }; diff --git a/src/renderer/theme.ts b/src/renderer/theme.ts index 7760185f3..0d881c4f2 100644 --- a/src/renderer/theme.ts +++ b/src/renderer/theme.ts @@ -27,7 +27,7 @@ const system = { const grays = [50, 100, 200, 300, 400, 500, 600, 650, 700, 750, 800, 850, 900]; const colors = { - gray: Object.fromEntries(grays.map((v) => [v, `var(--gray-${v})`])), + gray: Object.fromEntries(grays.map((v) => [v, `var(--theme-${v})`])), }; const fonts = { diff --git a/tests/common/__snapshots__/settings.test.ts.snap b/tests/common/__snapshots__/settings.test.ts.snap index 2ffdf4dc9..e85dc7f0d 100644 --- a/tests/common/__snapshots__/settings.test.ts.snap +++ b/tests/common/__snapshots__/settings.test.ts.snap @@ -43,6 +43,7 @@ exports[`Migrate settings 1`] = ` "use_fp16": false, }, }, + "showMinimap": false, "snapToGrid": true, "snapToGridAmount": 16, "startupTemplate": "", @@ -91,7 +92,7 @@ exports[`Migrate settings 1`] = ` ], }, "systemPythonLocation": "", - "theme": "dark", + "theme": "default-dark", "useSystemPython": false, "viewportExportPadding": 20, } diff --git a/tests/common/settings.test.ts b/tests/common/settings.test.ts index 5b638cd2a..7b2025587 100644 --- a/tests/common/settings.test.ts +++ b/tests/common/settings.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { migrateOldStorageSettings } from '../../src/common/settings/migration'; +import { migrateOldStorageSettings, migrateSettings } from '../../src/common/settings/migration'; const oldSettingData: Partial> = { 'allow-multiple-instances': 'false', @@ -78,13 +78,14 @@ const oldSettingData: Partial> = { test(`Migrate settings`, () => { const unusedKeys = new Set(Object.keys(oldSettingData)); - const settings = migrateOldStorageSettings({ + let settings = migrateOldStorageSettings({ keys: Object.keys(oldSettingData), getItem: (key: string) => { unusedKeys.delete(key); return oldSettingData[key] ?? null; }, }); + settings = migrateSettings(settings); expect(settings).toMatchSnapshot(); expect(unusedKeys).toMatchSnapshot();