feat: Added translucency effect to menus, dialogs and alerts. Can be turned off from Preferences > Appeareance (#2387) [skip e2e]
This commit is contained in:
64
packages/ui-services/src/Theme/Color.spec.ts
Normal file
64
packages/ui-services/src/Theme/Color.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Color } from './Color'
|
||||
|
||||
describe('Color', () => {
|
||||
it('should throw an error if the color is invalid', () => {
|
||||
expect(() => new Color('#ff')).toThrowError('Invalid color')
|
||||
})
|
||||
|
||||
it('should parse a rgb string', () => {
|
||||
const color = new Color('rgb(255, 0, 0)')
|
||||
expect(color.r).toBe(255)
|
||||
expect(color.g).toBe(0)
|
||||
expect(color.b).toBe(0)
|
||||
expect(color.a).toBe(1)
|
||||
})
|
||||
|
||||
it('should throw error if rgb string is invalid', () => {
|
||||
expect(() => new Color('rgb(255, 0)')).toThrowError('Invalid color')
|
||||
expect(() => new Color('rgb(266, -1, 0)')).toThrowError('Invalid color')
|
||||
})
|
||||
|
||||
it('should parse a hex string', () => {
|
||||
const color = new Color('#ff0000')
|
||||
expect(color.r).toBe(255)
|
||||
expect(color.g).toBe(0)
|
||||
expect(color.b).toBe(0)
|
||||
expect(color.a).toBe(1)
|
||||
})
|
||||
|
||||
it('should throw error if hex string is invalid', () => {
|
||||
expect(() => new Color('#ff')).toThrowError('Invalid color')
|
||||
expect(() => new Color('#ff000')).toThrowError('Invalid color')
|
||||
expect(() => new Color('#ff00000')).toThrowError('Invalid color')
|
||||
})
|
||||
|
||||
it('should set the alpha value', () => {
|
||||
const color = new Color('rgb(255, 0, 0)')
|
||||
color.setAlpha(0.5)
|
||||
expect(color.a).toBe(0.5)
|
||||
})
|
||||
|
||||
it('should throw error if alpha value is invalid', () => {
|
||||
const color = new Color('rgb(255, 0, 0)')
|
||||
expect(() => color.setAlpha(-1)).toThrowError('Invalid alpha value')
|
||||
expect(() => color.setAlpha(1.1)).toThrowError('Invalid alpha value')
|
||||
})
|
||||
|
||||
it('should convert to string', () => {
|
||||
const color = new Color('rgb(255, 0, 0)')
|
||||
color.setAlpha(0.5)
|
||||
expect(color.toString()).toBe('rgba(255, 0, 0, 0.5)')
|
||||
})
|
||||
|
||||
it('should return correct isDark value', () => {
|
||||
const autobiographyThemeBG = 'rgb(237, 228, 218)'
|
||||
const darkThemeBG = 'rgb(15, 16, 17)'
|
||||
const solarizedThemeBG = 'rgb(0, 43, 54)'
|
||||
const titaniumThemeBG = 'rgb(238, 239, 241)'
|
||||
|
||||
expect(new Color(autobiographyThemeBG).isDark()).toBe(false)
|
||||
expect(new Color(darkThemeBG).isDark()).toBe(true)
|
||||
expect(new Color(solarizedThemeBG).isDark()).toBe(true)
|
||||
expect(new Color(titaniumThemeBG).isDark()).toBe(false)
|
||||
})
|
||||
})
|
||||
84
packages/ui-services/src/Theme/Color.ts
Normal file
84
packages/ui-services/src/Theme/Color.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
const RGBRegex = /-?\b[0-9]{1,3}\b/g
|
||||
|
||||
export class Color {
|
||||
r: number = 0
|
||||
g: number = 0
|
||||
b: number = 0
|
||||
a: number = 1
|
||||
|
||||
constructor(color: string) {
|
||||
if (color.startsWith('rgb')) {
|
||||
this.setFromRGB(color)
|
||||
} else if (color.startsWith('#')) {
|
||||
this.setFromHex(color)
|
||||
} else {
|
||||
throw new Error('Invalid color')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color from a hex string
|
||||
* @param hex - The hex string to set
|
||||
*/
|
||||
setFromHex(hex: string) {
|
||||
if (!hex.startsWith('#')) {
|
||||
throw new Error('Invalid color')
|
||||
}
|
||||
const hexValue = hex.substring(1)
|
||||
if (hexValue.length !== 3 && hexValue.length !== 6) {
|
||||
throw new Error('Invalid color')
|
||||
}
|
||||
const r = parseInt(hexValue.substring(0, 2), 16)
|
||||
const g = parseInt(hexValue.substring(2, 4), 16)
|
||||
const b = parseInt(hexValue.substring(4, 6), 16)
|
||||
this.r = r
|
||||
this.g = g
|
||||
this.b = b
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color from an RGB string
|
||||
* @param color - The RGB string to set
|
||||
*/
|
||||
setFromRGB(color: string) {
|
||||
if (!color.startsWith('rgb')) {
|
||||
throw new Error('Invalid color')
|
||||
}
|
||||
const regexMatches = color.match(RGBRegex)
|
||||
if (!regexMatches || regexMatches.length !== 3) {
|
||||
throw new Error('Invalid color')
|
||||
}
|
||||
const [r, g, b] = regexMatches.map((value) => parseInt(value, 10))
|
||||
if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
|
||||
throw new Error('Invalid color')
|
||||
}
|
||||
this.r = r
|
||||
this.g = g
|
||||
this.b = b
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the alpha value of the color
|
||||
* @param alpha - The alpha value to set (0-1)
|
||||
*/
|
||||
setAlpha(alpha: number) {
|
||||
if (alpha < 0 || alpha > 1) {
|
||||
throw new Error('Invalid alpha value')
|
||||
}
|
||||
this.a = alpha
|
||||
return this
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the color is dark
|
||||
* Based on RGB->YIQ equation https://24ways.org/2010/calculating-color-contrast
|
||||
*/
|
||||
isDark() {
|
||||
const yiq = (this.r * 299 + this.g * 587 + this.b * 114) / 1000
|
||||
return yiq <= 128
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
LocalStorageDecryptedContextualPayload,
|
||||
PrefKey,
|
||||
ThemeInterface,
|
||||
PrefDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import {
|
||||
InternalEventBusInterface,
|
||||
@@ -21,6 +22,7 @@ import { AbstractUIServicee } from '../Abstract/AbstractUIService'
|
||||
import { GetAllThemesUseCase } from './GetAllThemesUseCase'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { ActiveThemeList } from './ActiveThemeList'
|
||||
import { Color } from './Color'
|
||||
|
||||
const CachedThemesKey = 'cachedThemes'
|
||||
const TimeBeforeApplyingColorScheme = 5
|
||||
@@ -82,6 +84,8 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
return
|
||||
}
|
||||
|
||||
this.toggleTranslucentUIColors()
|
||||
|
||||
let hasChange = false
|
||||
|
||||
const { features, uuids } = this.components.getActiveThemesIdentifiers()
|
||||
@@ -169,6 +173,8 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
}
|
||||
|
||||
private async handlePreferencesChangeEvent() {
|
||||
this.toggleTranslucentUIColors()
|
||||
|
||||
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
|
||||
|
||||
const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings
|
||||
@@ -339,6 +345,8 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
.handleThemeSchemeChange(packageInfo.isDark ?? false, this.getBackgroundColor())
|
||||
})
|
||||
}
|
||||
|
||||
this.toggleTranslucentUIColors()
|
||||
}
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
}
|
||||
@@ -356,8 +364,11 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
|
||||
this.themesActiveInTheUI.remove(id)
|
||||
|
||||
if (this.themesActiveInTheUI.isEmpty() && this.application.isNativeMobileWeb()) {
|
||||
this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff')
|
||||
if (this.themesActiveInTheUI.isEmpty()) {
|
||||
if (this.application.isNativeMobileWeb()) {
|
||||
this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff')
|
||||
}
|
||||
this.toggleTranslucentUIColors()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +377,31 @@ export class ThemeManager extends AbstractUIServicee {
|
||||
return bgColor.length ? bgColor : '#ffffff'
|
||||
}
|
||||
|
||||
private shouldUseTranslucentUI() {
|
||||
return this.application.getPreference(PrefKey.UseTranslucentUI, PrefDefaults[PrefKey.UseTranslucentUI])
|
||||
}
|
||||
|
||||
private toggleTranslucentUIColors() {
|
||||
if (!this.shouldUseTranslucentUI()) {
|
||||
document.documentElement.style.removeProperty('--popover-background-color')
|
||||
document.documentElement.style.removeProperty('--popover-backdrop-filter')
|
||||
document.body.classList.remove('translucent-ui')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const backgroundColor = new Color(this.getBackgroundColor())
|
||||
const backdropFilter = backgroundColor.isDark()
|
||||
? 'blur(12px) saturate(190%) contrast(70%) brightness(80%)'
|
||||
: 'blur(12px) saturate(190%) contrast(50%) brightness(130%)'
|
||||
const translucentBackgroundColor = backgroundColor.setAlpha(0.65).toString()
|
||||
document.documentElement.style.setProperty('--popover-background-color', translucentBackgroundColor)
|
||||
document.documentElement.style.setProperty('--popover-backdrop-filter', backdropFilter)
|
||||
document.body.classList.add('translucent-ui')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs the active theme's background color to the 'theme-color' meta tag
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color
|
||||
|
||||
Reference in New Issue
Block a user