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 {