diff --git a/index.html b/index.html
index 2eefeee2cd..bf9e914ba5 100644
--- a/index.html
+++ b/index.html
@@ -85,12 +85,13 @@
href="./public/res/apple/apple-touch-icon-180x180.png"
/>
-
+
+
diff --git a/package-lock.json b/package-lock.json
index b7281a0d8a..4f9f845a9f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6041,7 +6041,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
- "devOptional": true
+ "optional": true
},
"node_modules/acorn": {
"version": "8.14.0",
@@ -6885,7 +6885,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
- "devOptional": true,
+ "optional": true,
"engines": {
"node": ">=10"
}
@@ -9498,7 +9498,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
- "devOptional": true,
+ "optional": true,
"dependencies": {
"minipass": "^3.0.0"
},
@@ -9510,7 +9510,7 @@
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
- "devOptional": true,
+ "optional": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -9522,7 +9522,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "devOptional": true
+ "optional": true
},
"node_modules/fs.realpath": {
"version": "1.0.0",
@@ -10448,6 +10448,7 @@
"integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
"dev": true,
"license": "ISC",
+ "optional": true,
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
@@ -12284,7 +12285,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
- "devOptional": true,
+ "optional": true,
"engines": {
"node": ">=8"
}
@@ -12293,7 +12294,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
- "devOptional": true,
+ "optional": true,
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
@@ -12306,7 +12307,7 @@
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
- "devOptional": true,
+ "optional": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -12318,13 +12319,13 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "devOptional": true
+ "optional": true
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
- "devOptional": true,
+ "optional": true,
"bin": {
"mkdirp": "bin/cmd.js"
},
@@ -12465,7 +12466,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
- "devOptional": true,
+ "optional": true,
"dependencies": {
"abbrev": "1"
},
@@ -17414,7 +17415,7 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
- "devOptional": true,
+ "optional": true,
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@@ -17431,7 +17432,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "devOptional": true
+ "optional": true
},
"node_modules/temp-dir": {
"version": "2.0.0",
diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx
index b861a060b7..506cfa3f05 100644
--- a/src/app/features/settings/general/General.tsx
+++ b/src/app/features/settings/general/General.tsx
@@ -29,8 +29,11 @@ import {
} from 'folds';
import { isKeyHotkey } from 'is-hotkey';
import FocusTrap from 'focus-trap-react';
+import { HexColorPicker } from 'react-colorful';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
+import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
+import { PowerColorBadge } from '../../../components/power/PowerColorBadge';
import { useSetting } from '../../../state/hooks/settings';
import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
import { SettingTile } from '../../../components/setting-tile';
@@ -259,55 +262,116 @@ function SystemThemePreferences() {
);
}
-function PageZoomInput() {
- const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom');
- const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`);
+function NumberSettingInput({ settingKey, min = 0, max = 360 , percent = false}: NumberSettingInputProps) {
+
+ const [value, setValue] = useSetting(settingsAtom, settingKey);
+ const [current, setCurrent] = useState(`${value}`);
- const handleZoomChange: ChangeEventHandler = (evt) => {
- setCurrentZoom(evt.target.value);
+ const handleChange: ChangeEventHandler = (evt) => {
+ setCurrent(evt.target.value);
};
- const handleZoomEnter: KeyboardEventHandler = (evt) => {
+ const handleKeyDown: KeyboardEventHandler = (evt) => {
if (isKeyHotkey('escape', evt)) {
evt.stopPropagation();
- setCurrentZoom(pageZoom.toString());
+ setCurrent(`${value}`);
}
if (
isKeyHotkey('enter', evt) &&
'value' in evt.target &&
typeof evt.target.value === 'string'
) {
- const newZoom = parseInt(evt.target.value, 10);
- if (Number.isNaN(newZoom)) return;
- const safeZoom = Math.max(Math.min(newZoom, 150), 75);
- setPageZoom(safeZoom);
- setCurrentZoom(safeZoom.toString());
+ const newValue = parseInt(evt.target.value, 10);
+ if (Number.isNaN(newValue)) return;
+
+ const safeValue = Math.max(Math.min(newValue, max), min);
+ setValue(safeValue);
+ setCurrent(safeValue.toString());
}
};
return (
%}
+ min={min}
+ max={max}
+ value={current}
+ onChange={handleChange}
+ onKeyDown={handleKeyDown}
+ after={{percent ? '%' : ''}}
outlined
/>
);
}
+type NumberSettingKey = 'transparency' | 'pageZoom' | 'angle' | 'blur';
+interface NumberSettingInputProps {
+ settingKey: NumberSettingKey;
+ min?: number;
+ max?: number;
+ percent?: boolean;
+}
+
+function TransparencyInput() {
+ return ;
+}
+function AngleInput() {
+ return ;
+}
+function BlurInput() {
+ return ;
+}
+function PageZoomInput() {
+ return ;
+}
+
+type ColorPickerButtonProps = {
+ color: string;
+ onChange: (color: string) => void;
+ onRemove?: () => void;
+}
+function ColorPickerButton({ color, onChange, onRemove, }: ColorPickerButtonProps) {
+ return (
+ }
+ onRemove={onRemove}
+ >
+ {(openPicker, opened) => (
+
+ )}
+
+ );
+}
+
function Appearance() {
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
+ const [customBackgroundEnabled, setCustomBackgroundEnabled] = useSetting(settingsAtom, 'customBackgroundEnabled');
+ const [customBackgroundOnly, setCustomBackgroundOnly] = useSetting(settingsAtom, 'customBackgroundOnly');
+ const [transparency, setTransparency] = useSetting(settingsAtom, 'transparency');
+ const [customBgColor1, setCustomBgColor1] = useSetting(settingsAtom, 'customBgColor1');
+ const [customBgColor2, setCustomBgColor2] = useSetting(settingsAtom, 'customBgColor2');
+ const [customBgColor3, setCustomBgColor3] = useSetting(settingsAtom, 'customBgColor3');
+ const [customBgColor4, setCustomBgColor4] = useSetting(settingsAtom, 'customBgColor4');
+ const [customBgColor5, setCustomBgColor5] = useSetting(settingsAtom, 'customBgColor5');
+
return (
Appearance
@@ -333,6 +397,83 @@ function Appearance() {
/>
+
+ {
+ setCustomBackgroundEnabled(value);
+ if(value && transparency == 0) setTransparency(15)
+ }}
+ />
+ }
+ />
+
+
+ {customBackgroundEnabled && (
+ <>
+
+
+ setCustomBgColor1('')}
+ />
+ setCustomBgColor2('')}
+ />
+ setCustomBgColor3('')}
+ />
+ setCustomBgColor4('')}
+ />
+ setCustomBgColor5('')}
+ />
+
+
+
+
+
+ }
+ />
+
+
+
+ }/>
+
+
+
+ }/>
+
+
+ {/* The blur setting is currently not applied as it does not offer a visible effect at the moment */}
+ {/*
+ }/>
+ */}
+ >
+ )}
+
MemberPowerTag;
diff --git a/src/app/pages/ThemeManager.tsx b/src/app/pages/ThemeManager.tsx
index 69d50cdb9c..3bba67ec10 100644
--- a/src/app/pages/ThemeManager.tsx
+++ b/src/app/pages/ThemeManager.tsx
@@ -1,5 +1,5 @@
import React, { ReactNode, useEffect } from 'react';
-import { configClass, varsClass } from 'folds';
+import { config, configClass, varsClass } from 'folds';
import {
DarkTheme,
LightTheme,
@@ -10,6 +10,7 @@ import {
} from '../hooks/useTheme';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
+import { hexToGrayscale } from '../plugins/colorUtils';
export function UnAuthRouteThemeManager() {
const systemThemeKind = useSystemThemeKind();
@@ -35,7 +36,6 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
useEffect(() => {
document.body.className = '';
document.body.classList.add(configClass, varsClass);
-
document.body.classList.add(...activeTheme.classNames);
if (monochromeMode) {
@@ -45,5 +45,61 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
}
}, [activeTheme, monochromeMode]);
- return {children};
+ return (
+
+
+ {children}
+
+ );
+}
+
+export function CustomThemeManager() {
+ const [customBackgroundEnabled] = useSetting(settingsAtom, 'customBackgroundEnabled');
+ const [customBackgroundOnly] = useSetting(settingsAtom, 'customBackgroundOnly');
+ const [customBgColor1] = useSetting(settingsAtom, 'customBgColor1');
+ const [customBgColor2] = useSetting(settingsAtom, 'customBgColor2');
+ const [customBgColor3] = useSetting(settingsAtom, 'customBgColor3');
+ const [customBgColor4] = useSetting(settingsAtom, 'customBgColor4');
+ const [customBgColor5] = useSetting(settingsAtom, 'customBgColor5');
+ const [transparency] = useSetting(settingsAtom, 'transparency');
+ const [angle] = useSetting(settingsAtom, 'angle');
+ const [blur] = useSetting(settingsAtom, 'blur');
+ const [monochromeMode] = useSetting(settingsAtom, 'monochromeMode');
+
+ useEffect(() => {
+ const gradientLayer = document.getElementById('gradientLayer');
+ if (!gradientLayer) return;
+
+ const colors = [customBgColor1, customBgColor2, customBgColor3, customBgColor4, customBgColor5]
+ .filter(Boolean)
+ .map((c) => (monochromeMode ? hexToGrayscale(c) : c))
+ .join(', ');
+
+ if (!customBackgroundEnabled || !colors) {
+ gradientLayer.style.background = 'none';
+ gradientLayer.style.opacity = '0';
+ gradientLayer.style.backdropFilter = 'none';
+ return;
+ }
+
+ gradientLayer.style.setProperty('z-index', customBackgroundOnly ? config.zIndex.Z100 : config.zIndex.Max);
+ gradientLayer.style.background = `linear-gradient(${angle}deg, ${colors})`;
+ gradientLayer.style.backdropFilter = blur > 0 ? `blur(${blur}px)` : 'none';
+ gradientLayer.style.opacity = `${(transparency ?? 0) / 100}`;
+
+ }, [
+ customBackgroundEnabled,
+ customBackgroundOnly,
+ customBgColor1,
+ customBgColor2,
+ customBgColor3,
+ customBgColor4,
+ customBgColor5,
+ transparency,
+ angle,
+ blur,
+ monochromeMode,
+ ]);
+
+ return null;
}
diff --git a/src/app/plugins/color.ts b/src/app/plugins/colorUtils.ts
similarity index 53%
rename from src/app/plugins/color.ts
rename to src/app/plugins/colorUtils.ts
index 47c73170f8..5f2e200912 100644
--- a/src/app/plugins/color.ts
+++ b/src/app/plugins/colorUtils.ts
@@ -14,3 +14,16 @@ export const accessibleColor = (themeKind: ThemeKind, color: string): string =>
return chroma(color).set('lab.l', lightness).hex();
};
+
+export const hexToGrayscale = (hex: string): string => {
+ const clean = hex.replace('#', '');
+
+ const r = parseInt(clean.substring(0, 2), 16);
+ const g = parseInt(clean.substring(2, 4), 16);
+ const b = parseInt(clean.substring(4, 6), 16);
+
+ const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
+ const grayHex = gray.toString(16).padStart(2, '0');
+
+ return `#${grayHex}${grayHex}${grayHex}`;
+}
\ No newline at end of file
diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts
index 9fbad4ff0f..cdeabf883d 100644
--- a/src/app/state/settings.ts
+++ b/src/app/state/settings.ts
@@ -21,6 +21,16 @@ export interface Settings {
lightThemeId?: string;
darkThemeId?: string;
monochromeMode?: boolean;
+ customBackgroundEnabled: boolean;
+ customBackgroundOnly: boolean;
+ transparency: number;
+ blur: number;
+ angle: number;
+ customBgColor1: string,
+ customBgColor2: string,
+ customBgColor3: string,
+ customBgColor4: string,
+ customBgColor5: string,
isMarkdown: boolean;
editorToolbar: boolean;
twitterEmoji: boolean;
@@ -55,6 +65,16 @@ const defaultSettings: Settings = {
lightThemeId: undefined,
darkThemeId: undefined,
monochromeMode: false,
+ customBackgroundEnabled: false,
+ customBackgroundOnly: false,
+ transparency: 15,
+ blur: 0,
+ angle: 45,
+ customBgColor1: '#6600ff',
+ customBgColor2: '#ff0000',
+ customBgColor3: '#00ff11',
+ customBgColor4: '#00eaff',
+ customBgColor5: '#ddff00',
isMarkdown: true,
editorToolbar: false,
twitterEmoji: false,
diff --git a/src/index.css b/src/index.css
index ca28536d22..26d3e04b41 100644
--- a/src/index.css
+++ b/src/index.css
@@ -54,6 +54,7 @@ body {
/*Why font-variant-ligatures => https://github.com/rsms/inter/issues/222 */
font-variant-ligatures: no-contextual;
}
+
#root {
width: 100%;
height: 100%;
@@ -61,6 +62,15 @@ body {
flex-direction: column;
}
+#gradientLayer {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ opacity: 0;
+ background: none;
+ backdrop-filter: none;
+}
+
*,
*::before,
*::after {