mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-27 03:41:11 +01:00
Theme system refactor (#31)
This commit is contained in:
94
src-web/lib/theme/color.ts
Normal file
94
src-web/lib/theme/color.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import parseColor from 'parse-color';
|
||||
|
||||
export class Color {
|
||||
private theme: 'dark' | 'light' = 'light';
|
||||
|
||||
private hue: number = 0;
|
||||
private saturation: number = 0;
|
||||
private lightness: number = 0;
|
||||
private alpha: number = 1;
|
||||
|
||||
constructor(cssColor: string, theme: 'dark' | 'light') {
|
||||
try {
|
||||
const { hsla } = parseColor(cssColor || '');
|
||||
this.hue = hsla[0];
|
||||
this.saturation = hsla[1];
|
||||
this.lightness = hsla[2];
|
||||
this.alpha = hsla[3] ?? 1;
|
||||
this.theme = theme;
|
||||
} catch (err) {
|
||||
console.log('Failed to parse CSS color', cssColor, err);
|
||||
}
|
||||
}
|
||||
|
||||
static transparent(): Color {
|
||||
return new Color('rgba(0, 0, 0, 0.1)', 'light');
|
||||
}
|
||||
|
||||
private clone(): Color {
|
||||
return new Color(this.css(), this.theme);
|
||||
}
|
||||
|
||||
lower(mod: number): Color {
|
||||
return this.theme === 'dark' ? this._darken(mod) : this._lighten(mod);
|
||||
}
|
||||
|
||||
lowerTo(value: number): Color {
|
||||
return this.theme === 'dark'
|
||||
? this._darken(1)._lighten(value)
|
||||
: this._lighten(1)._darken(1 - value);
|
||||
}
|
||||
|
||||
lift(mod: number): Color {
|
||||
return this.theme === 'dark' ? this._lighten(mod) : this._darken(mod);
|
||||
}
|
||||
|
||||
liftTo(value: number): Color {
|
||||
return this.theme === 'dark'
|
||||
? this._lighten(1)._darken(1 - value)
|
||||
: this._darken(1)._lighten(value);
|
||||
}
|
||||
|
||||
translucify(mod: number): Color {
|
||||
const c = this.clone();
|
||||
c.alpha = c.alpha - c.alpha * mod;
|
||||
return c;
|
||||
}
|
||||
|
||||
desaturate(mod: number): Color {
|
||||
const c = this.clone();
|
||||
c.saturation = c.saturation - c.saturation * mod;
|
||||
return c;
|
||||
}
|
||||
|
||||
saturate(mod: number): Color {
|
||||
const c = this.clone();
|
||||
c.saturation = this.saturation + (100 - this.saturation) * mod;
|
||||
return c;
|
||||
}
|
||||
|
||||
lighterThan(c: Color): boolean {
|
||||
return this.lightness > c.lightness;
|
||||
}
|
||||
|
||||
css(): string {
|
||||
// If opacity is 1, allow for Tailwind modification
|
||||
const h = Math.round(this.hue);
|
||||
const s = Math.round(this.saturation);
|
||||
const l = Math.round(this.lightness);
|
||||
const a = Math.round(this.alpha * 100) / 100;
|
||||
return `hsla(${h}, ${s}%, ${l}%, ${a})`;
|
||||
}
|
||||
|
||||
private _lighten(mod: number): Color {
|
||||
const c = this.clone();
|
||||
c.lightness = this.lightness + (100 - this.lightness) * mod;
|
||||
return c;
|
||||
}
|
||||
|
||||
private _darken(mod: number): Color {
|
||||
const c = this.clone();
|
||||
c.lightness = this.lightness - this.lightness * mod;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generateColorVariant, toTailwindVariable } from './theme';
|
||||
|
||||
describe('Generate colors', () => {
|
||||
it('Generates dark colors', () => {
|
||||
expect(generateColorVariant('hsl(0,0%,50%)', 50, 'dark', 0.2, 0.8)).toBe('hsl(0,0%,14.0%)');
|
||||
expect(generateColorVariant('hsl(0,0%,50%)', 950, 'dark', 0.2, 0.8)).toBe('hsl(0,0%,77.0%)');
|
||||
expect(generateColorVariant('hsl(0,0%,50%)', 50, 'dark', 0.4, 0.6)).toBe('hsl(0,0%,23.0%)');
|
||||
expect(generateColorVariant('hsl(0,0%,50%)', 950, 'dark', 0.4, 0.6)).toBe('hsl(0,0%,59.0%)');
|
||||
});
|
||||
it('Generates light colors', () => {
|
||||
expect(generateColorVariant('hsl(0,0%,50%)', 50, 'light', 0.2, 0.8)).toBe('hsl(0,0%,80.0%)');
|
||||
expect(generateColorVariant('hsl(0,0%,50%)', 950, 'light', 0.2, 0.8)).toBe('hsl(0,0%,14.0%)');
|
||||
expect(generateColorVariant('hsl(0,0%,50%)', 50, 'light', 0.4, 0.6)).toBe('hsl(0,0%,60.0%)');
|
||||
expect(generateColorVariant('hsl(0,0%,50%)', 950, 'light', 0.4, 0.6)).toBe('hsl(0,0%,23.0%)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generates Tailwind color', () => {
|
||||
it('Does it', () => {
|
||||
expect(
|
||||
toTailwindVariable({ name: 'blue', cssColor: 'hsl(10, 20%, 30%)', variant: 100 }),
|
||||
).toEqual('--color-blue-100: 10 20% 30%;');
|
||||
});
|
||||
});
|
||||
@@ -1,172 +0,0 @@
|
||||
import parseColor from 'parse-color';
|
||||
import type { Appearance } from './window';
|
||||
|
||||
export type AppThemeColor =
|
||||
| 'gray'
|
||||
| 'red'
|
||||
| 'orange'
|
||||
| 'yellow'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'pink'
|
||||
| 'violet';
|
||||
const colorNames: AppThemeColor[] = [
|
||||
'gray',
|
||||
'red',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'pink',
|
||||
'violet',
|
||||
];
|
||||
export type AppThemeColorVariant =
|
||||
| 0
|
||||
| 50
|
||||
| 100
|
||||
| 200
|
||||
| 300
|
||||
| 400
|
||||
| 500
|
||||
| 600
|
||||
| 700
|
||||
| 800
|
||||
| 900
|
||||
| 950
|
||||
| 1000;
|
||||
|
||||
export const appThemeVariants: AppThemeColorVariant[] = [
|
||||
0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 1000,
|
||||
];
|
||||
|
||||
export type AppThemeLayer = 'root' | 'sidebar' | 'titlebar' | 'content' | 'above';
|
||||
export type AppThemeColors = Record<AppThemeColor, string>;
|
||||
|
||||
export interface AppThemeLayerStyle {
|
||||
colors: AppThemeColors;
|
||||
blackPoint?: number;
|
||||
whitePoint?: number;
|
||||
}
|
||||
|
||||
interface ThemeColorObj {
|
||||
name: AppThemeColor;
|
||||
variant: AppThemeColorVariant;
|
||||
cssColor: string;
|
||||
}
|
||||
|
||||
export interface AppTheme {
|
||||
name: string;
|
||||
appearance: Appearance;
|
||||
layers: Partial<Record<AppThemeLayer, AppThemeLayerStyle>>;
|
||||
}
|
||||
|
||||
export function generateCSS(t: AppTheme): ThemeColorObj[] {
|
||||
const rootColors = t.layers.root?.colors;
|
||||
if (rootColors === undefined) return [];
|
||||
|
||||
const colors: ThemeColorObj[] = [];
|
||||
for (const color of colorNames) {
|
||||
const rawValue = rootColors[color];
|
||||
if (!rawValue) continue;
|
||||
colors.push(
|
||||
...generateColors(
|
||||
color,
|
||||
rawValue,
|
||||
t.appearance,
|
||||
t.layers.root?.blackPoint,
|
||||
t.layers.root?.whitePoint,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
export function generateColors(
|
||||
name: AppThemeColor,
|
||||
color: string,
|
||||
appearance: Appearance,
|
||||
blackPoint = 0,
|
||||
whitePoint = 1,
|
||||
): ThemeColorObj[] {
|
||||
const colors = [];
|
||||
for (const variant of appThemeVariants) {
|
||||
colors.push({
|
||||
name,
|
||||
variant,
|
||||
cssColor: generateColorVariant(color, variant, appearance, blackPoint, whitePoint),
|
||||
});
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
const lightnessMap: Record<Appearance, Record<AppThemeColorVariant, number>> = {
|
||||
system: {
|
||||
// Not actually used
|
||||
0: 1,
|
||||
50: 1,
|
||||
100: 0.9,
|
||||
200: 0.7,
|
||||
300: 0.4,
|
||||
400: 0.2,
|
||||
500: 0,
|
||||
600: -0.2,
|
||||
700: -0.4,
|
||||
800: -0.6,
|
||||
900: -0.8,
|
||||
950: -0.9,
|
||||
1000: -1,
|
||||
},
|
||||
light: {
|
||||
0: 1,
|
||||
50: 1,
|
||||
100: 0.9,
|
||||
200: 0.7,
|
||||
300: 0.4,
|
||||
400: 0.2,
|
||||
500: 0,
|
||||
600: -0.2,
|
||||
700: -0.4,
|
||||
800: -0.6,
|
||||
900: -0.8,
|
||||
950: -0.9,
|
||||
1000: -1,
|
||||
},
|
||||
dark: {
|
||||
0: -1,
|
||||
50: -0.9,
|
||||
100: -0.8,
|
||||
200: -0.6,
|
||||
300: -0.4,
|
||||
400: -0.2,
|
||||
500: 0,
|
||||
600: 0.2,
|
||||
700: 0.4,
|
||||
800: 0.6,
|
||||
900: 0.8,
|
||||
950: 0.9,
|
||||
1000: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export function generateColorVariant(
|
||||
color: string,
|
||||
variant: AppThemeColorVariant,
|
||||
appearance: Appearance,
|
||||
blackPoint = 0,
|
||||
whitePoint = 1,
|
||||
): string {
|
||||
const { hsl } = parseColor(color || '');
|
||||
const lightnessMod = lightnessMap[appearance][variant];
|
||||
// const lightnessMod = (appearance === 'dark' ? 1 : -1) * ((variant / 1000) * 2 - 1);
|
||||
const newL =
|
||||
lightnessMod > 0
|
||||
? hsl[2] + (100 * whitePoint - hsl[2]) * lightnessMod
|
||||
: hsl[2] + hsl[2] * (1 - blackPoint) * lightnessMod;
|
||||
return `hsl(${hsl[0]},${hsl[1]}%,${newL.toFixed(1)}%)`;
|
||||
}
|
||||
|
||||
export function toTailwindVariable({ name, variant, cssColor }: ThemeColorObj): string {
|
||||
const { hsl } = parseColor(cssColor || '');
|
||||
return `--color-${name}-${variant}: ${hsl[0]} ${hsl[1]}% ${hsl[2]}%;`;
|
||||
}
|
||||
@@ -1,76 +1,313 @@
|
||||
import type { AppTheme, AppThemeColors } from './theme';
|
||||
import { generateCSS, toTailwindVariable } from './theme';
|
||||
import { indent } from '../indent';
|
||||
import { Color } from './color';
|
||||
|
||||
export type Appearance = 'dark' | 'light' | 'system';
|
||||
|
||||
const DEFAULT_APPEARANCE: Appearance = 'system';
|
||||
|
||||
enum Theme {
|
||||
yaak = 'yaak',
|
||||
catppuccin = 'catppuccin',
|
||||
interface ThemeComponent {
|
||||
background?: Color;
|
||||
backgroundHighlight?: Color;
|
||||
backgroundHighlightSecondary?: Color;
|
||||
backgroundActive?: Color;
|
||||
foreground?: Color;
|
||||
foregroundSubtle?: Color;
|
||||
foregroundSubtler?: Color;
|
||||
colors?: Partial<RootColors>;
|
||||
}
|
||||
|
||||
const themes: Record<Theme, AppThemeColors> = {
|
||||
yaak: {
|
||||
gray: 'hsl(245, 23%, 45%)',
|
||||
red: 'hsl(342,100%, 63%)',
|
||||
orange: 'hsl(32, 98%, 54%)',
|
||||
yellow: 'hsl(52, 79%, 58%)',
|
||||
green: 'hsl(136, 62%, 54%)',
|
||||
blue: 'hsl(206, 100%, 56%)',
|
||||
pink: 'hsl(300, 100%, 71%)',
|
||||
violet: 'hsl(266, 100%, 73%)',
|
||||
},
|
||||
catppuccin: {
|
||||
gray: 'hsl(240, 23%, 47%)',
|
||||
red: 'hsl(343, 91%, 74%)',
|
||||
orange: 'hsl(23, 92%, 74%)',
|
||||
yellow: 'hsl(41, 86%, 72%)',
|
||||
green: 'hsl(115, 54%, 65%)',
|
||||
blue: 'hsl(217, 92%, 65%)',
|
||||
pink: 'hsl(316, 72%, 75%)',
|
||||
violet: 'hsl(267, 84%, 70%)',
|
||||
},
|
||||
};
|
||||
interface YaakTheme extends ThemeComponent {
|
||||
name: string;
|
||||
components?: {
|
||||
dialog?: ThemeComponent;
|
||||
sidebar?: ThemeComponent;
|
||||
responsePane?: ThemeComponent;
|
||||
appHeader?: ThemeComponent;
|
||||
button?: ThemeComponent;
|
||||
banner?: ThemeComponent;
|
||||
placeholder?: ThemeComponent;
|
||||
};
|
||||
}
|
||||
|
||||
const darkTheme: AppTheme = {
|
||||
name: 'Default Dark',
|
||||
appearance: 'dark',
|
||||
layers: {
|
||||
root: {
|
||||
blackPoint: 0.2,
|
||||
colors: themes.yaak,
|
||||
interface RootColors {
|
||||
primary: Color;
|
||||
secondary: Color;
|
||||
info: Color;
|
||||
success: Color;
|
||||
notice: Color;
|
||||
warning: Color;
|
||||
danger: Color;
|
||||
}
|
||||
|
||||
type ColorName = keyof RootColors;
|
||||
type ComponentName = keyof NonNullable<YaakTheme['components']>;
|
||||
|
||||
const yaakThemes: Record<string, YaakTheme> = {
|
||||
yaakLight: {
|
||||
name: 'Yaak (Light)',
|
||||
background: new Color('#f2f4f7', 'light').lower(1),
|
||||
foreground: new Color('hsl(219,23%,15%)', 'light'),
|
||||
colors: {
|
||||
primary: new Color('hsl(266,100%,70%)', 'light'),
|
||||
secondary: new Color('hsl(220,24%,59%)', 'light'),
|
||||
info: new Color('hsl(206,100%,48%)', 'light'),
|
||||
success: new Color('hsl(155,95%,33%)', 'light'),
|
||||
notice: new Color('hsl(45,100%,41%)', 'light'),
|
||||
warning: new Color('hsl(30,100%,43%)', 'light'),
|
||||
danger: new Color('hsl(335,75%,57%)', 'light'),
|
||||
},
|
||||
components: {
|
||||
sidebar: {
|
||||
background: new Color('#f2f4f7', 'light'),
|
||||
},
|
||||
},
|
||||
} as YaakTheme,
|
||||
|
||||
yaakDark: {
|
||||
name: 'Yaak Dark',
|
||||
background: new Color('hsl(244,23%,12%)', 'dark'),
|
||||
foreground: new Color('#bcbad4', 'dark'),
|
||||
|
||||
colors: {
|
||||
primary: new Color('hsl(266,100%,79%)', 'dark'),
|
||||
secondary: new Color('hsl(245,23%,60%)', 'dark'),
|
||||
info: new Color('hsl(206,100%,63%)', 'dark'),
|
||||
success: new Color('hsl(150,100%,37%)', 'dark'),
|
||||
notice: new Color('hsl(48,80%,63%)', 'dark'),
|
||||
warning: new Color('hsl(28,100%,61%)', 'dark'),
|
||||
danger: new Color('hsl(342,90%,68%)', 'dark'),
|
||||
},
|
||||
|
||||
components: {
|
||||
sidebar: {
|
||||
background: new Color('hsl(243,23%,15%)', 'dark'),
|
||||
},
|
||||
responsePane: {
|
||||
background: new Color('hsl(243,23%,15%)', 'dark'),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const lightTheme: AppTheme = {
|
||||
name: 'Default Light',
|
||||
appearance: 'light',
|
||||
layers: {
|
||||
root: {
|
||||
colors: {
|
||||
gray: '#7f8fb0',
|
||||
red: '#ec3f87',
|
||||
orange: '#ff8000',
|
||||
yellow: '#e7cf24',
|
||||
green: '#00d365',
|
||||
blue: '#0090ff',
|
||||
pink: '#ea6cea',
|
||||
violet: '#ac6cff',
|
||||
catppuccin: {
|
||||
name: 'Catppuccin',
|
||||
background: new Color('#181825', 'dark'),
|
||||
foreground: new Color('#cdd6f4', 'dark'),
|
||||
foregroundSubtle: new Color('#cdd6f4', 'dark').lower(0.1).translucify(0.3),
|
||||
foregroundSubtler: new Color('#cdd6f4', 'dark').lower(0.1).translucify(0.55),
|
||||
colors: {
|
||||
primary: new Color('#cba6f7', 'dark'),
|
||||
secondary: new Color('#bac2de', 'dark'),
|
||||
info: new Color('#89b4fa', 'dark'),
|
||||
success: new Color('#a6e3a1', 'dark'),
|
||||
notice: new Color('#f9e2af', 'dark'),
|
||||
warning: new Color('#fab387', 'dark'),
|
||||
danger: new Color('#f38ba8', 'dark'),
|
||||
},
|
||||
components: {
|
||||
dialog: {
|
||||
background: new Color('#181825', 'dark'),
|
||||
},
|
||||
sidebar: {
|
||||
background: new Color('#1e1e2e', 'dark'),
|
||||
},
|
||||
appHeader: {
|
||||
background: new Color('#11111b', 'dark'),
|
||||
},
|
||||
responsePane: {
|
||||
background: new Color('#1e1e2e', 'dark'),
|
||||
},
|
||||
button: {
|
||||
colors: {
|
||||
primary: new Color('#cba6f7', 'dark').lower(0.2),
|
||||
secondary: new Color('#bac2de', 'dark').lower(0.2),
|
||||
info: new Color('#89b4fa', 'dark').lower(0.2),
|
||||
success: new Color('#a6e3a1', 'dark').lower(0.2),
|
||||
notice: new Color('#f9e2af', 'dark').lower(0.2),
|
||||
warning: new Color('#fab387', 'dark').lower(0.2),
|
||||
danger: new Color('#f38ba8', 'dark').lower(0.2),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type CSSVariables = Record<string, string | undefined>;
|
||||
|
||||
function themeVariables(theme?: ThemeComponent, base?: CSSVariables): CSSVariables | null {
|
||||
const vars: CSSVariables = {
|
||||
'--background': theme?.background?.css(),
|
||||
'--background-highlight':
|
||||
theme?.backgroundHighlight?.css() ?? theme?.background?.lift(0.11).css(),
|
||||
'--background-highlight-secondary':
|
||||
theme?.backgroundHighlightSecondary?.css() ?? theme?.background?.lift(0.06).css(),
|
||||
'--background-active':
|
||||
theme?.backgroundActive?.css() ?? theme?.colors?.primary?.lower(0.2).translucify(0.8).css(),
|
||||
'--background-backdrop': theme?.background?.lower(0.2).translucify(0.2).css(),
|
||||
'--background-selection': theme?.colors?.primary?.lower(0.1).translucify(0.7).css(),
|
||||
'--fg': theme?.foreground?.css(),
|
||||
'--fg-subtle': theme?.foregroundSubtle?.css() ?? theme?.foreground?.lower(0.2).css(),
|
||||
'--fg-subtler': theme?.foregroundSubtler?.css() ?? theme?.foreground?.lower(0.3).css(),
|
||||
'--border-focus': theme?.colors?.info?.css(),
|
||||
};
|
||||
|
||||
for (const [color, value] of Object.entries(theme?.colors ?? {})) {
|
||||
vars[`--fg-${color}`] = (value as Color).css();
|
||||
}
|
||||
|
||||
// Extend with base
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
if (!v && base?.[k]) {
|
||||
vars[k] = base[k];
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
function placeholderColorVariables(color: Color): CSSVariables {
|
||||
return {
|
||||
'--fg': color.lift(0.6).css(),
|
||||
'--fg-subtle': color.lift(0.4).css(),
|
||||
'--fg-subtler': color.css(),
|
||||
'--background': color.lower(0.2).translucify(0.8).css(),
|
||||
'--background-highlight': color.lower(0.2).translucify(0.2).css(),
|
||||
'--background-highlight-secondary': color.lower(0.1).translucify(0.7).css(),
|
||||
};
|
||||
}
|
||||
|
||||
function bannerColorVariables(color: Color): CSSVariables {
|
||||
return {
|
||||
'--fg': color.lift(0.8).css(),
|
||||
'--fg-subtle': color.translucify(0.3).css(),
|
||||
'--fg-subtler': color.css(),
|
||||
'--background': color.css(),
|
||||
'--background-highlight': color.lift(0.3).translucify(0.4).css(),
|
||||
'--background-highlight-secondary': color.translucify(0.9).css(),
|
||||
};
|
||||
}
|
||||
|
||||
function buttonSolidColorVariables(color: Color): CSSVariables {
|
||||
return {
|
||||
'--fg': new Color('white', 'dark').css(),
|
||||
'--background': color.lower(0.15).css(),
|
||||
'--background-highlight': color.css(),
|
||||
'--background-highlight-secondary': color.lower(0.3).css(),
|
||||
};
|
||||
}
|
||||
|
||||
function buttonBorderColorVariables(color: Color): CSSVariables {
|
||||
return {
|
||||
'--fg': color.lift(0.6).css(),
|
||||
'--fg-subtle': color.lift(0.4).css(),
|
||||
'--fg-subtler': color.lift(0.4).translucify(0.6).css(),
|
||||
'--background': Color.transparent().css(),
|
||||
'--background-highlight': color.translucify(0.8).css(),
|
||||
};
|
||||
}
|
||||
|
||||
function variablesToCSS(selector: string | null, vars: CSSVariables | null): string | null {
|
||||
if (vars == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const css = Object.entries(vars ?? {})
|
||||
.filter(([, value]) => value)
|
||||
.map(([name, value]) => `${name}: ${value};`)
|
||||
.join('\n');
|
||||
|
||||
return selector == null ? css : `${selector} {\n${indent(css)}\n}`;
|
||||
}
|
||||
|
||||
function componentCSS(
|
||||
component: ComponentName,
|
||||
components?: YaakTheme['components'],
|
||||
): string | null {
|
||||
if (components == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const themeVars = themeVariables(components[component]);
|
||||
return variablesToCSS(`.x-theme-${component}`, themeVars);
|
||||
}
|
||||
|
||||
function buttonCSS(color: ColorName, colors?: Partial<RootColors>): string | null {
|
||||
const cssColor = colors?.[color];
|
||||
if (cssColor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
variablesToCSS(`.x-theme-button--solid--${color}`, buttonSolidColorVariables(cssColor)),
|
||||
variablesToCSS(`.x-theme-button--border--${color}`, buttonBorderColorVariables(cssColor)),
|
||||
].join('\n\n');
|
||||
}
|
||||
|
||||
function bannerCSS(color: ColorName, colors?: Partial<RootColors>): string | null {
|
||||
const cssColor = colors?.[color];
|
||||
if (cssColor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [variablesToCSS(`.x-theme-banner--${color}`, bannerColorVariables(cssColor))].join('\n\n');
|
||||
}
|
||||
|
||||
function placeholderCSS(color: ColorName, colors?: Partial<RootColors>): string | null {
|
||||
const cssColor = colors?.[color];
|
||||
if (cssColor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
variablesToCSS(`.x-theme-placeholder-widget--${color}`, placeholderColorVariables(cssColor)),
|
||||
].join('\n\n');
|
||||
}
|
||||
|
||||
function isThemeDark(theme: YaakTheme): boolean {
|
||||
if (theme.background && theme.foreground) {
|
||||
return theme.foreground.lighterThan(theme.background);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setThemeOnDocument(yaakThemes.yaakLight!);
|
||||
setThemeOnDocument(yaakThemes.yaakDark!);
|
||||
|
||||
export function getThemeCSS(theme: YaakTheme): string {
|
||||
let themeCSS = '';
|
||||
try {
|
||||
const baseCss = variablesToCSS(null, themeVariables(theme));
|
||||
const { components, colors } = theme;
|
||||
themeCSS = [
|
||||
baseCss,
|
||||
...Object.keys(components ?? {}).map((key) =>
|
||||
componentCSS(key as ComponentName, theme.components),
|
||||
),
|
||||
...Object.keys(colors ?? {}).map((key) =>
|
||||
buttonCSS(key as ColorName, theme.components?.button?.colors ?? colors),
|
||||
),
|
||||
...Object.keys(colors ?? {}).map((key) =>
|
||||
bannerCSS(key as ColorName, theme.components?.banner?.colors ?? colors),
|
||||
),
|
||||
...Object.keys(colors ?? {}).map((key) =>
|
||||
placeholderCSS(key as ColorName, theme.components?.placeholder?.colors ?? colors),
|
||||
),
|
||||
].join('\n\n');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return themeCSS;
|
||||
}
|
||||
|
||||
export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) {
|
||||
const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance;
|
||||
const theme = resolvedAppearance === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance);
|
||||
}
|
||||
|
||||
export function setThemeOnDocument(theme: YaakTheme) {
|
||||
document.documentElement.setAttribute('data-theme', theme.name);
|
||||
|
||||
let existingStyleEl = document.head.querySelector(`style[data-theme-definition]`);
|
||||
const darkOrLight = isThemeDark(theme) ? 'dark' : 'light';
|
||||
let existingStyleEl = document.head.querySelector(`style[data-theme-definition=${darkOrLight}]`);
|
||||
if (!existingStyleEl) {
|
||||
const styleEl = document.createElement('style');
|
||||
document.head.appendChild(styleEl);
|
||||
@@ -78,16 +315,12 @@ export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARA
|
||||
}
|
||||
|
||||
existingStyleEl.textContent = [
|
||||
`/* ${darkTheme.name} */`,
|
||||
`[data-resolved-appearance="dark"] {`,
|
||||
...generateCSS(darkTheme).map(toTailwindVariable),
|
||||
'}',
|
||||
`/* ${lightTheme.name} */`,
|
||||
`[data-resolved-appearance="light"] {`,
|
||||
...generateCSS(lightTheme).map(toTailwindVariable),
|
||||
`/* ${theme.name} */`,
|
||||
`[data-resolved-appearance="${isThemeDark(theme) ? 'dark' : 'light'}"] {`,
|
||||
getThemeCSS(theme),
|
||||
'}',
|
||||
].join('\n');
|
||||
existingStyleEl.setAttribute('data-theme-definition', '');
|
||||
existingStyleEl.setAttribute('data-theme-definition', darkOrLight);
|
||||
}
|
||||
|
||||
export function getPreferredAppearance(): Appearance {
|
||||
|
||||
Reference in New Issue
Block a user