2024.5.0 (#39)

This commit is contained in:
Gregory Schier
2024-06-03 14:08:24 -07:00
committed by GitHub
parent 60e469a1c9
commit 4f9a7e9c88
197 changed files with 12283 additions and 3505 deletions

View File

@@ -0,0 +1,31 @@
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import type { Appearance } from './window';
export function getCSSAppearance(): Appearance {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export async function getWindowAppearance(): Promise<Appearance> {
const a = await getCurrent().theme();
return a ?? getCSSAppearance();
}
/**
* Subscribe to appearance (dark/light) changes. Note, we use Tauri Window appearance instead of
* CSS appearance because CSS won't fire the way we handle window theme management.
*/
export function subscribeToWindowAppearanceChange(
cb: (appearance: Appearance) => void,
): () => void {
const container = { unsubscribe: () => {} };
getCurrent()
.onThemeChanged((t) => {
cb(t.payload);
})
.then((l) => {
container.unsubscribe = l;
});
return () => container.unsubscribe();
}

View 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('rgb(0,0,0)', 'light').translucify(1);
}
static white(): Color {
return new Color('rgb(0,0,0)', 'light').lower(1);
}
static black(): Color {
return new Color('rgb(0,0,0)', 'light').lift(1);
}
private clone(): Color {
return new Color(this.css(), this.theme);
}
lower(mod: number): Color {
return this.theme === 'dark' ? this._darken(mod) : this._lighten(mod);
}
lift(mod: number): Color {
return this.theme === 'dark' ? this._lighten(mod) : this._darken(mod);
}
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})`;
}
hex(): string {
return parseColor(this.css()).hex;
}
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;
}
}

View File

@@ -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%;');
});
});

View File

@@ -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]}%;`;
}

View File

@@ -0,0 +1,22 @@
import { catppuccin } from './themes/catppuccin';
import { dracula } from './themes/dracula';
import { github } from './themes/github';
import { hotdogStand } from './themes/hotdog-stand';
import { monokaiPro } from './themes/monokai-pro';
import { moonlight } from './themes/moonlight';
import { rosePine } from './themes/rose-pine';
import { yaak, yaakDark, yaakLight } from './themes/yaak';
export const defaultDarkTheme = yaakDark;
export const defaultLightTheme = yaakLight;
export const yaakThemes = [
...yaak,
...catppuccin,
...rosePine,
...monokaiPro,
...github,
...moonlight,
...dracula,
...hotdogStand,
];

View File

@@ -0,0 +1,172 @@
import { Color } from '../color';
import type { YaakTheme } from '../window';
export const catppuccinLatte: YaakTheme = {
name: 'Catppuccin Latte',
id: 'catppuccin-latte',
background: new Color('#eff1f5', 'light'),
foreground: new Color('#4c4f69', 'dark'),
foregroundSubtle: new Color('#6c6f85', 'light'),
foregroundSubtler: new Color('#8c8fa1', 'light'),
colors: {
primary: new Color('#8839ef', 'light'),
secondary: new Color('#6c6f85', 'light'),
info: new Color('#7287fd', 'light'),
success: new Color('#179299', 'light'),
notice: new Color('#df8e1d', 'light'),
warning: new Color('#fe640b', 'light'),
danger: new Color('#e64553', 'light'),
},
components: {
sidebar: {
background: new Color('#e6e9ef', 'light'),
backgroundHighlight: new Color('#e6e9ef', 'light').lift(0.05),
foregroundSubtler: new Color('#7287fd', 'light'),
},
appHeader: {
background: new Color('#dce0e8', 'light'),
backgroundHighlight: new Color('#e6e9ef', 'light').lift(0.05),
foregroundSubtler: new Color('#7287fd', 'light'),
},
},
};
export const catppuccinMacchiato: YaakTheme = {
name: 'Catppuccin Macchiato',
id: 'catppuccin-macchiato',
background: new Color('#1e2030', 'dark'),
foreground: new Color('#cad3f5', 'dark'),
foregroundSubtle: new Color('#a5adcb', 'dark'),
foregroundSubtler: new Color('#8087a2', 'dark'),
colors: {
primary: new Color('#c6a0f6', 'dark'),
secondary: new Color('#b8c0e0', 'dark'),
info: new Color('#8aadf4', 'dark'),
success: new Color('#a6da95', 'dark'),
notice: new Color('#eed49f', 'dark'),
warning: new Color('#f5a97f', 'dark'),
danger: new Color('#ed8796', 'dark'),
},
components: {
dialog: {
background: new Color('#181825', 'dark'),
},
sidebar: {
background: new Color('#24273a', 'dark'),
backgroundHighlight: new Color('#24273a', 'dark').lift(0.05),
},
appHeader: {
background: new Color('#181926', 'dark'),
backgroundHighlight: new Color('#181926', 'dark').lift(0.1),
},
responsePane: {
background: new Color('#24273a', 'dark'),
backgroundHighlight: new Color('#24273a', 'dark').lift(0.05),
},
button: {
colors: {
primary: new Color('#c6a0f6', 'dark').lower(0.1),
secondary: new Color('#b8c0e0', 'dark').lower(0.1),
info: new Color('#8aadf4', 'dark').lower(0.1),
success: new Color('#a6da95', 'dark').lower(0.1),
notice: new Color('#eed49f', 'dark').lower(0.1),
warning: new Color('#f5a97f', 'dark').lower(0.1),
danger: new Color('#ed8796', 'dark').lower(0.1),
},
},
},
};
export const catppuccinFrappe: YaakTheme = {
name: 'Catppuccin Frappé',
id: 'catppuccin-frappe',
background: new Color('#292c3c', 'dark'),
foreground: new Color('#c6d0f5', 'dark'),
foregroundSubtle: new Color('#a5adce', 'dark'),
foregroundSubtler: new Color('#838ba7', 'dark'),
colors: {
primary: new Color('#ca9ee6', 'dark'),
secondary: new Color('#b8c0e0', 'dark'),
info: new Color('#8caaee', 'dark'),
success: new Color('#a6d189', 'dark'),
notice: new Color('#e5c890', 'dark'),
warning: new Color('#ef9f76', 'dark'),
danger: new Color('#e78284', 'dark'),
},
components: {
dialog: {
background: new Color('#181825', 'dark'),
},
sidebar: {
background: new Color('#303446', 'dark'),
backgroundHighlight: new Color('#303446', 'dark').lift(0.05),
},
appHeader: {
background: new Color('#232634', 'dark'),
backgroundHighlight: new Color('#232634', 'dark').lift(0.1),
},
responsePane: {
background: new Color('#303446', 'dark'),
backgroundHighlight: new Color('#303446', 'dark').lift(0.05),
},
button: {
colors: {
primary: new Color('#ca9ee6', 'dark').lower(0.1),
secondary: new Color('#b8c0e0', 'dark').lower(0.1),
info: new Color('#8caaee', 'dark').lower(0.1),
success: new Color('#a6d189', 'dark').lower(0.1),
notice: new Color('#e5c890', 'dark').lower(0.1),
warning: new Color('#ef9f76', 'dark').lower(0.1),
danger: new Color('#e78284', 'dark').lower(0.1),
},
},
},
};
const catppuccinMocha: YaakTheme = {
name: 'Catppuccin Mocha',
id: 'catppuccin-mocha',
background: new Color('#181825', 'dark'),
foreground: new Color('#cdd6f4', 'dark'),
foregroundSubtle: new Color('#a6adc8', 'dark'),
foregroundSubtler: new Color('#7f849c', 'dark'),
colors: {
primary: new Color('#c6a0f6', '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'),
backgroundHighlight: new Color('#1e1e2e', 'dark').lift(0.05),
},
appHeader: {
background: new Color('#11111b', 'dark'),
backgroundHighlight: new Color('#11111b', 'dark').lift(0.1),
},
responsePane: {
background: new Color('#1e1e2e', 'dark'),
backgroundHighlight: new Color('#1e1e2e', 'dark').lift(0.05),
},
button: {
colors: {
primary: new Color('#cba6f7', 'dark').lower(0.2).desaturate(0.2),
secondary: new Color('#bac2de', 'dark').lower(0.2).desaturate(0.2),
info: new Color('#89b4fa', 'dark').lower(0.2).desaturate(0.2),
success: new Color('#a6e3a1', 'dark').lower(0.2).desaturate(0.2),
notice: new Color('#f9e2af', 'dark').lower(0.2).desaturate(0.2),
warning: new Color('#fab387', 'dark').lower(0.2).desaturate(0.2),
danger: new Color('#f38ba8', 'dark').lower(0.2).desaturate(0.2),
},
},
},
};
export const catppuccin = [catppuccinFrappe, catppuccinMacchiato, catppuccinMocha, catppuccinLatte];

View File

@@ -0,0 +1,32 @@
import { Color } from '../color';
import type { YaakTheme } from '../window';
const draculaDefault: YaakTheme = {
id: 'dracula',
name: 'Dracula',
background: new Color('#282A36', 'dark'),
backgroundHighlight: new Color('#343746', 'dark'),
backgroundHighlightSecondary: new Color('#424450', 'dark'),
foreground: new Color('#F8F8F2', 'dark'),
foregroundSubtle: new Color('hsl(232,14%,65%)', 'dark'),
foregroundSubtler: new Color('hsl(232,14%,50%)', 'dark'),
colors: {
primary: new Color('#BD93F9', 'dark'),
secondary: new Color('#6272A4', 'dark'),
info: new Color('#8BE9FD', 'dark'),
success: new Color('#50FA7B', 'dark'),
notice: new Color('#F1FA8C', 'dark'),
warning: new Color('#FFB86C', 'dark'),
danger: new Color('#FF5555', 'dark'),
},
components: {
sidebar: {
background: new Color('hsl(230,15%,24%)', 'dark'),
},
appHeader: {
background: new Color('#21222C', 'dark'),
},
},
};
export const dracula = [draculaDefault];

View File

@@ -0,0 +1,57 @@
import { Color } from '../color';
import type { YaakTheme } from '../window';
const githubDark: YaakTheme = {
id: 'github-dark',
name: 'GitHub Dark',
background: new Color('#0d1218', 'dark'),
backgroundHighlight: new Color('#171c23', 'dark'),
backgroundHighlightSecondary: new Color('#1c2127', 'dark'),
foreground: new Color('#dce3eb', 'dark'),
foregroundSubtle: new Color('#88919b', 'dark'),
foregroundSubtler: new Color('#6b727d', 'dark'),
colors: {
primary: new Color('#a579ef', 'dark').lift(0.1),
secondary: new Color('#6b727d', 'dark').lift(0.1),
info: new Color('#458def', 'dark').lift(0.1),
success: new Color('#3eb24f', 'dark').lift(0.1),
notice: new Color('#dca132', 'dark').lift(0.1),
warning: new Color('#ec7934', 'dark').lift(0.1),
danger: new Color('#ee5049', 'dark').lift(0.1),
},
components: {
button: {
colors: {
primary: new Color('#a579ef', 'dark'),
secondary: new Color('#6b727d', 'dark'),
info: new Color('#458def', 'dark'),
success: new Color('#3eb24f', 'dark'),
notice: new Color('#dca132', 'dark'),
warning: new Color('#ec7934', 'dark'),
danger: new Color('#ee5049', 'dark'),
},
},
},
};
export const githubLight: YaakTheme = {
id: 'github-light',
name: 'GitHub Light',
background: new Color('#ffffff', 'light'),
backgroundHighlight: new Color('hsl(210,15%,92%)', 'light'),
backgroundHighlightSecondary: new Color('hsl(210,29%,94%)', 'light'),
foreground: new Color('#1f2328', 'light'),
foregroundSubtle: new Color('#636c76', 'light'),
foregroundSubtler: new Color('#828d94', 'light'),
colors: {
primary: new Color('#8250df', 'light'),
secondary: new Color('#6e7781', 'light'),
info: new Color('hsl(212,92%,48%)', 'light'),
success: new Color('hsl(137,66%,32%)', 'light'),
notice: new Color('hsl(40,100%,40%)', 'light'),
warning: new Color('hsl(24,100%,44%)', 'light'),
danger: new Color('#d1242f', 'light'),
},
};
export const github = [githubDark, githubLight];

View File

@@ -0,0 +1,61 @@
import { Color } from '../color';
import type { YaakTheme } from '../window';
export const hotdogStandDefault: YaakTheme = {
id: 'hotdog-stand',
name: 'Hotdog Stand',
background: new Color('#ff0000', 'dark'),
backgroundHighlight: new Color('#000000', 'dark'),
backgroundHighlightSecondary: new Color('#000000', 'dark'),
foreground: new Color('#ffffff', 'dark'),
foregroundSubtle: new Color('#ffffff', 'dark'),
foregroundSubtler: new Color('#ffff00', 'dark'),
colors: {
primary: new Color('#ffff00', 'dark'),
secondary: new Color('#ffff00', 'dark'),
info: new Color('#ffff00', 'dark'),
notice: new Color('#ffff00', 'dark'),
warning: new Color('#ffff00', 'dark'),
danger: new Color('#ffff00', 'dark'),
},
components: {
appHeader: {
background: new Color('#000000', 'dark'),
foreground: new Color('#ffffff', 'dark'),
foregroundSubtle: new Color('#ffff00', 'dark'),
foregroundSubtler: new Color('#ff0000', 'dark'),
},
menu: {
background: new Color('#000000', 'dark'),
backgroundHighlight: new Color('#ff0000', 'dark'),
backgroundHighlightSecondary: new Color('#ff0000', 'dark'),
foreground: new Color('#ffffff', 'dark'),
foregroundSubtle: new Color('#ffff00', 'dark'),
foregroundSubtler: new Color('#ffff00', 'dark'),
},
button: {
background: new Color('#000000', 'dark'),
foreground: new Color('#ffffff', 'dark'),
colors: {
primary: new Color('#000000', 'dark'),
secondary: new Color('#ffffff', 'dark'),
info: new Color('#000000', 'dark'),
notice: new Color('#ffff00', 'dark'),
warning: new Color('#000000', 'dark'),
danger: new Color('#ff0000', 'dark'),
},
},
editor: {
colors: {
primary: new Color('#ffffff', 'dark'),
secondary: new Color('#ffffff', 'dark'),
info: new Color('#ffffff', 'dark'),
notice: new Color('#ffff00', 'dark'),
warning: new Color('#ffffff', 'dark'),
danger: new Color('#ffffff', 'dark'),
},
},
},
};
export const hotdogStand = [hotdogStandDefault];

View File

@@ -0,0 +1,245 @@
import { Color } from '../color';
import type { YaakTheme } from '../window';
export const monokaiProDefault: YaakTheme = {
id: 'monokai-pro',
name: 'Monokai Pro',
background: new Color('#2d2a2e', 'dark'),
foreground: new Color('#fcfcfa', 'dark'),
foregroundSubtle: new Color('#c1c0c0', 'dark'),
foregroundSubtler: new Color('#939293', 'dark'),
colors: {
primary: new Color('#ab9df2', 'dark'),
secondary: new Color('#c1c0c0', 'dark'),
info: new Color('#78dce8', 'dark'),
success: new Color('#a9dc76', 'dark'),
notice: new Color('#ffd866', 'dark'),
warning: new Color('#fc9867', 'dark'),
danger: new Color('#ff6188', 'dark'),
},
components: {
appHeader: {
background: new Color('#221f22', 'dark'),
foreground: new Color('#c1c0c0', 'dark'),
foregroundSubtle: new Color('#939293', 'dark'),
foregroundSubtler: new Color('#727072', 'dark'),
},
button: {
colors: {
primary: new Color('#ab9df2', 'dark').lower(0.1),
secondary: new Color('#c1c0c0', 'dark').lower(0.1),
info: new Color('#78dce8', 'dark').lower(0.1),
success: new Color('#a9dc76', 'dark').lower(0.1),
notice: new Color('#ffd866', 'dark').lower(0.1),
warning: new Color('#fc9867', 'dark').lower(0.1),
danger: new Color('#ff6188', 'dark').lower(0.1),
},
},
},
};
const monokaiProClassic: YaakTheme = {
id: 'monokai-pro-classic',
name: 'Monokai Pro Classic',
background: new Color('#272822', 'dark'),
foreground: new Color('#fdfff1', 'dark'),
foregroundSubtle: new Color('#c0c1b5', 'dark'),
foregroundSubtler: new Color('#919288', 'dark'),
colors: {
primary: new Color('#ae81ff', 'dark'),
secondary: new Color('#b2b9bd', 'dark'),
info: new Color('#66d9ef', 'dark'),
success: new Color('#a6e22e', 'dark'),
notice: new Color('#e6db74', 'dark'),
warning: new Color('#fd971f', 'dark'),
danger: new Color('#f92672', 'dark'),
},
components: {
appHeader: {
background: new Color('#1d1e19', 'dark'),
foreground: new Color('#b2b9bd', 'dark'),
foregroundSubtle: new Color('#767b81', 'dark'),
foregroundSubtler: new Color('#696d77', 'dark'),
},
button: {
colors: {
primary: new Color('#ae81ff', 'dark').lower(0.1),
secondary: new Color('#b2b9bd', 'dark').lower(0.1),
info: new Color('#66d9ef', 'dark').lower(0.1),
success: new Color('#a6e22e', 'dark').lower(0.1),
notice: new Color('#e6db74', 'dark').lower(0.1),
warning: new Color('#fd971f', 'dark').lower(0.1),
danger: new Color('#f92672', 'dark').lower(0.1),
},
},
},
};
const monokaiProMachine: YaakTheme = {
id: 'monokai-pro-machine',
name: 'Monokai Pro Machine',
background: new Color('#273136', 'dark'),
foreground: new Color('#eaf2f1', 'dark'),
foregroundSubtle: new Color('#8b9798', 'dark'),
foregroundSubtler: new Color('#6b7678', 'dark'),
colors: {
primary: new Color('#baa0f8', 'dark'),
secondary: new Color('#b8c4c3', 'dark'),
info: new Color('#7cd5f1', 'dark'),
success: new Color('#a2e57b', 'dark'),
notice: new Color('#ffed72', 'dark'),
warning: new Color('#ffb270', 'dark'),
danger: new Color('#ff6d7e', 'dark'),
},
components: {
appHeader: {
background: new Color('#1d2528', 'dark'),
foreground: new Color('#b2b9bd', 'dark'),
foregroundSubtle: new Color('#767b81', 'dark'),
foregroundSubtler: new Color('#696d77', 'dark'),
},
button: {
colors: {
primary: new Color('#baa0f8', 'dark').lower(0.1),
secondary: new Color('#b8c4c3', 'dark').lower(0.1),
info: new Color('#7cd5f1', 'dark').lower(0.1),
success: new Color('#a2e57b', 'dark').lower(0.1),
notice: new Color('#ffed72', 'dark').lower(0.1),
warning: new Color('#ffb270', 'dark').lower(0.1),
danger: new Color('#ff6d7e', 'dark').lower(0.1),
},
},
},
};
const monokaiProOctagon: YaakTheme = {
id: 'monokai-pro-octagon',
name: 'Monokai Pro Octagon',
background: new Color('#282a3a', 'dark'),
foreground: new Color('#eaf2f1', 'dark'),
foregroundSubtle: new Color('#b2b9bd', 'dark'),
foregroundSubtler: new Color('#767b81', 'dark'),
colors: {
primary: new Color('#c39ac9', 'dark'),
secondary: new Color('#b2b9bd', 'dark'),
info: new Color('#9cd1bb', 'dark'),
success: new Color('#bad761', 'dark'),
notice: new Color('#ffd76d', 'dark'),
warning: new Color('#ff9b5e', 'dark'),
danger: new Color('#ff657a', 'dark'),
},
components: {
appHeader: {
background: new Color('#1e1f2b', 'dark'),
foreground: new Color('#b2b9bd', 'dark'),
foregroundSubtle: new Color('#767b81', 'dark'),
foregroundSubtler: new Color('#696d77', 'dark'),
},
button: {
colors: {
primary: new Color('#c39ac9', 'dark').lower(0.1).desaturate(0.1),
secondary: new Color('#b2b9bd', 'dark').lower(0.1).desaturate(0.1),
info: new Color('#9cd1bb', 'dark').lower(0.1).desaturate(0.1),
success: new Color('#bad761', 'dark').lower(0.1).desaturate(0.1),
notice: new Color('#ffd76d', 'dark').lower(0.1).desaturate(0.1),
warning: new Color('#ff9b5e', 'dark').lower(0.1).desaturate(0.1),
danger: new Color('#ff657a', 'dark').lower(0.1).desaturate(0.1),
},
},
},
};
const monokaiProRistretto: YaakTheme = {
id: 'monokai-pro-ristretto',
name: 'Monokai Pro Ristretto',
background: new Color('#2c2525', 'dark'),
foreground: new Color('#fff1f3', 'dark'),
foregroundSubtle: new Color('#c3b7b8', 'dark'),
foregroundSubtler: new Color('#948a8b', 'dark'),
colors: {
primary: new Color('#a8a9eb', 'dark'),
secondary: new Color('#c3b7b8', 'dark'),
info: new Color('#85dacc', 'dark'),
success: new Color('#adda78', 'dark'),
notice: new Color('#f9cc6c', 'dark'),
warning: new Color('#f38d70', 'dark'),
danger: new Color('#fd6883', 'dark'),
},
components: {
appHeader: {
background: new Color('#211c1c', 'dark'),
foreground: new Color('#c3b7b8', 'dark'),
foregroundSubtle: new Color('#948a8b', 'dark'),
foregroundSubtler: new Color('#72696a', 'dark'),
},
button: {
colors: {
primary: new Color('#a8a9eb', 'dark').lower(0.1),
secondary: new Color('#c3b7b8', 'dark').lower(0.1),
info: new Color('#85dacc', 'dark').lower(0.1),
success: new Color('#adda78', 'dark').lower(0.1),
notice: new Color('#f9cc6c', 'dark').lower(0.1),
warning: new Color('#f38d70', 'dark').lower(0.1),
danger: new Color('#fd6883', 'dark').lower(0.1),
},
},
},
};
const monokaiProSpectrum: YaakTheme = {
id: 'monokai-pro-spectrum',
name: 'Monokai Pro Spectrum',
background: new Color('#222222', 'dark'),
foreground: new Color('#f7f1ff', 'dark'),
foregroundSubtle: new Color('#bab6c0', 'dark'),
foregroundSubtler: new Color('#8b888f', 'dark'),
colors: {
primary: new Color('#948ae3', 'dark'),
secondary: new Color('#bab6c0', 'dark'),
info: new Color('#5ad4e6', 'dark'),
success: new Color('#7bd88f', 'dark'),
notice: new Color('#fce566', 'dark'),
warning: new Color('#fd9353', 'dark'),
danger: new Color('#fc618d', 'dark'),
},
components: {
appHeader: {
background: new Color('#191919', 'dark'),
foreground: new Color('#bab6c0', 'dark'),
foregroundSubtle: new Color('#8b888f', 'dark'),
foregroundSubtler: new Color('#69676c', 'dark'),
},
button: {
colors: {
primary: new Color('#948ae3', 'dark').lower(0.1),
secondary: new Color('#bab6c0', 'dark').lower(0.1),
info: new Color('#5ad4e6', 'dark').lower(0.1),
success: new Color('#7bd88f', 'dark').lower(0.1),
notice: new Color('#fce566', 'dark').lower(0.1),
warning: new Color('#fd9353', 'dark').lower(0.1),
danger: new Color('#fc618d', 'dark').lower(0.1),
},
},
},
};
export const monokaiPro = [
monokaiProDefault,
monokaiProClassic,
monokaiProMachine,
monokaiProOctagon,
monokaiProRistretto,
monokaiProSpectrum,
];

View File

@@ -0,0 +1,66 @@
import { Color } from '../color';
import type { YaakTheme } from '../window';
export const colors = {
lightRed: '#ff98a4',
red: '#ff757f',
darkRed: '#ff5370',
lightOrange: '#f8b576',
orange: '#ff966c',
darkOrange: '#fc7b7b',
yellow: '#ffc777',
green: '#c3e88d',
lightTeal: '#7af8ca',
teal: '#3ad7c7',
lightCyan: '#b4f9f8',
cyan: '#78dbff',
sky: '#60bdff',
blue: '#7cafff',
darkBlue: '#3d6fe0',
darkestBlue: '#3b63cf',
indigo: '#af9fff',
purple: '#c4a2ff',
pink: '#fca7ea',
darkPink: '#fd8aca',
saturatedGray: '#7a88cf',
desaturatedGray: '#979bb6',
gray11: '#d5def8',
gray10: '#c8d3f5',
gray9: '#b4c2f0',
gray8: '#a9b8e8',
gray7: '#828bb8',
gray6: '#444a73',
gray5: '#2f334d',
gray4: '#222436',
gray3: '#1e2030',
gray2: '#191a2a',
gray1: '#131421',
} as const;
const moonlightDefault: YaakTheme = {
id: 'moonlight',
name: 'Moonlight',
background: new Color('#222436', 'dark'),
foreground: new Color('#d5def8', 'dark'),
foregroundSubtle: new Color('#828bb8', 'dark'),
foregroundSubtler: new Color('hsl(232,26%,43%)', 'dark'),
colors: {
primary: new Color(colors.purple, 'dark'),
secondary: new Color(colors.desaturatedGray, 'dark'),
info: new Color(colors.blue, 'dark'),
success: new Color(colors.teal, 'dark'),
notice: new Color(colors.yellow, 'dark'),
warning: new Color(colors.orange, 'dark'),
danger: new Color(colors.red, 'dark'),
},
components: {
appHeader: {
background: new Color(colors.gray3, 'dark'),
},
sidebar: {
background: new Color(colors.gray3, 'dark'),
},
},
};
export const moonlight = [moonlightDefault];

View File

@@ -0,0 +1,108 @@
import { Color } from '../color';
import type { YaakTheme } from '../window';
export const rosePineDefault: YaakTheme = {
id: 'rose-pine',
name: 'Rosé Pine',
background: new Color('#191724', 'dark'),
foreground: new Color('#e0def4', 'dark'),
foregroundSubtle: new Color('#908caa', 'dark'),
foregroundSubtler: new Color('#6e6a86', 'dark'),
colors: {
primary: new Color('#c4a7e7', 'dark'),
secondary: new Color('#6e6a86', 'dark'),
info: new Color('#67abcb', 'dark'),
success: new Color('#9cd8d8', 'dark'),
notice: new Color('#f6c177', 'dark'),
warning: new Color('#f1a3a1', 'dark'),
danger: new Color('#eb6f92', 'dark'),
},
components: {
responsePane: {
background: new Color('#1f1d2e', 'dark'),
},
sidebar: {
background: new Color('#1f1d2e', 'dark'),
},
menu: {
background: new Color('#393552', 'dark'),
foregroundSubtle: new Color('#908caa', 'dark').lift(0.15),
foregroundSubtler: new Color('#6e6a86', 'dark').lift(0.15),
},
},
};
const rosePineMoon: YaakTheme = {
id: 'rose-pine-moon',
name: 'Rosé Pine Moon',
background: new Color('#232136', 'dark'),
foreground: new Color('#e0def4', 'dark'),
foregroundSubtle: new Color('#908caa', 'dark'),
foregroundSubtler: new Color('#6e6a86', 'dark'),
colors: {
primary: new Color('#c4a7e7', 'dark'),
secondary: new Color('#908caa', 'dark'),
info: new Color('#68aeca', 'dark'),
success: new Color('#9ccfd8', 'dark'),
notice: new Color('#f6c177', 'dark'),
warning: new Color('#ea9a97', 'dark'),
danger: new Color('#eb6f92', 'dark'),
},
components: {
responsePane: {
background: new Color('#2a273f', 'dark'),
},
sidebar: {
background: new Color('#2a273f', 'dark'),
},
menu: {
background: new Color('#393552', 'dark'),
foregroundSubtle: new Color('#908caa', 'dark').lift(0.15),
foregroundSubtler: new Color('#6e6a86', 'dark').lift(0.15),
},
},
};
const rosePineDawn: YaakTheme = {
id: 'rose-pine-dawn',
name: 'Rosé Pine Dawn',
background: new Color('#faf4ed', 'light'),
backgroundHighlight: new Color('#dfdad9', 'light'),
backgroundHighlightSecondary: new Color('#f4ede8', 'light'),
foreground: new Color('#575279', 'light'),
foregroundSubtle: new Color('#797593', 'light'),
foregroundSubtler: new Color('#9893a5', 'light'),
colors: {
primary: new Color('#9070ad', 'light'),
secondary: new Color('#6e6a86', 'light'),
info: new Color('#2d728d', 'light'),
success: new Color('#4f8c96', 'light'),
notice: new Color('#cb862d', 'light'),
warning: new Color('#ce7b78', 'light'),
danger: new Color('#b4637a', 'light'),
},
components: {
responsePane: {
backgroundHighlight: new Color('#e8e4e2', 'light'),
},
sidebar: {
backgroundHighlight: new Color('#e8e4e2', 'light'),
},
appHeader: {
backgroundHighlight: new Color('#e8e4e2', 'light'),
},
input: {
backgroundHighlight: new Color('#dfdad9', 'light'),
},
dialog: {
backgroundHighlight: new Color('#e8e4e2', 'light'),
},
menu: {
background: new Color('#f2e9e1', 'light'),
backgroundHighlight: new Color('#dfdad9', 'light'),
backgroundHighlightSecondary: new Color('#6e6a86', 'light'),
},
},
};
export const rosePine = [rosePineDefault, rosePineDawn, rosePineMoon];

View File

@@ -0,0 +1,84 @@
import { Color } from '../color';
import type { YaakTheme } from '../window';
export const yaakLight: YaakTheme = {
id: 'yaak-light',
name: 'Yaak Light',
background: new Color('hsl(216,24%,100%)', 'light'),
backgroundHighlight: new Color('hsl(216,24%,93%)', 'light'),
backgroundHighlightSecondary: new Color('hsl(216,24%,87%)', 'light'),
foreground: new Color('hsl(219,23%,15%)', 'light'),
foregroundSubtle: new Color('hsl(219,23%,15%)', 'light').lower(0.3),
foregroundSubtler: new Color('hsl(219,23%,15%)', 'light').lower(0.5),
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('hsl(216,24%,97%)', 'light'),
backgroundHighlight: new Color('hsl(216,24%,93%)', 'light'),
backgroundHighlightSecondary: new Color('hsl(216,24%,90%)', 'light'),
},
},
};
export const yaakDark: YaakTheme = {
id: 'yaak-dark',
name: 'Yaak Dark',
background: new Color('hsl(244,23%,14%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,23%)', 'dark'),
backgroundHighlightSecondary: new Color('hsl(244,23%,20%)', 'dark'),
foreground: new Color('hsl(245,23%,83%)', 'dark'),
foregroundSubtle: new Color('hsl(245,20%,65%)', 'dark'),
foregroundSubtler: new Color('hsl(245,18%,51%)', '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,99%,44%)', '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: {
button: {
colors: {
primary: new Color('hsl(266,100%,79%)', 'dark').lower(0.1),
secondary: new Color('hsl(245,23%,60%)', 'dark').lower(0.1),
info: new Color('hsl(206,100%,63%)', 'dark').lower(0.1),
success: new Color('hsl(150,99%,44%)', 'dark').lower(0.15),
notice: new Color('hsl(48,80%,63%)', 'dark').lower(0.2),
warning: new Color('hsl(28,100%,61%)', 'dark').lower(0.1),
danger: new Color('hsl(342,90%,68%)', 'dark').lower(0.1),
},
},
input: {
backgroundHighlight: new Color('hsl(244,23%,24%)', 'dark'),
},
dialog: {
backgroundHighlight: new Color('hsl(244,23%,24%)', 'dark'),
},
sidebar: {
background: new Color('hsl(243,23%,16%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,22%)', 'dark'),
},
responsePane: {
background: new Color('hsl(243,23%,16%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,16%)', 'dark').lift(0.08),
},
appHeader: {
background: new Color('hsl(244,23%,12%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.1),
},
},
};
export const yaak = [yaakDark, yaakLight];

View File

@@ -1,104 +1,244 @@
import type { AppTheme, AppThemeColors } from './theme';
import { generateCSS, toTailwindVariable } from './theme';
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;
shadow?: 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%)',
},
};
export interface YaakTheme extends ThemeComponent {
id: string;
name: string;
components?: {
dialog?: ThemeComponent;
menu?: ThemeComponent;
toast?: ThemeComponent;
sidebar?: ThemeComponent;
responsePane?: ThemeComponent;
appHeader?: ThemeComponent;
button?: ThemeComponent;
banner?: ThemeComponent;
placeholder?: ThemeComponent;
urlBar?: ThemeComponent;
editor?: ThemeComponent;
input?: 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;
}
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',
},
},
},
};
type ColorName = keyof RootColors;
type ComponentName = keyof NonNullable<YaakTheme['components']>;
export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) {
const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance;
const theme = resolvedAppearance === 'dark' ? darkTheme : lightTheme;
type CSSVariables = Record<string, string | undefined>;
document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance);
document.documentElement.setAttribute('data-theme', theme.name);
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(),
'--shadow':
theme?.shadow?.css() ??
Color.black()
.translucify(isThemeDark(theme ?? {}) ? 0.7 : 0.93)
.css(),
};
let existingStyleEl = document.head.querySelector(`style[data-theme-definition]`);
if (!existingStyleEl) {
const styleEl = document.createElement('style');
document.head.appendChild(styleEl);
existingStyleEl = styleEl;
for (const [color, value] of Object.entries(theme?.colors ?? {})) {
vars[`--fg-${color}`] = (value as Color).css();
}
existingStyleEl.textContent = [
`/* ${darkTheme.name} */`,
`[data-resolved-appearance="dark"] {`,
...generateCSS(darkTheme).map(toTailwindVariable),
'}',
`/* ${lightTheme.name} */`,
`[data-resolved-appearance="light"] {`,
...generateCSS(lightTheme).map(toTailwindVariable),
'}',
].join('\n');
existingStyleEl.setAttribute('data-theme-definition', '');
// Extend with base
for (const [k, v] of Object.entries(vars)) {
if (!v && base?.[k]) {
vars[k] = base[k];
}
}
return vars;
}
export function getPreferredAppearance(): Appearance {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
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(),
};
}
export function subscribeToPreferredAppearanceChange(
cb: (appearance: Appearance) => void,
): () => void {
const listener = (e: MediaQueryListEvent) => cb(e.matches ? 'dark' : 'light');
const m = window.matchMedia('(prefers-color-scheme: dark)');
m.addEventListener('change', listener);
return () => m.removeEventListener('change', listener);
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--${color}`, placeholderColorVariables(cssColor)),
].join('\n\n');
}
export function isThemeDark(theme: ThemeComponent): boolean {
if (theme.background && theme.foreground) {
return theme.foreground.lighterThan(theme.background);
}
return false;
}
export function getThemeCSS(theme: YaakTheme): string {
theme.components = theme.components ?? {};
// Toast defaults to menu styles
theme.components.toast = theme.components.toast ?? theme.components.menu;
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 [`/* ${theme.name} */`, `[data-theme="${theme.id}"] {`, indent(themeCSS), '}'].join('\n');
}
export function addThemeStylesToDocument(theme: YaakTheme) {
let styleEl = document.head.querySelector(`style[data-theme]`);
if (!styleEl) {
styleEl = document.createElement('style');
document.head.appendChild(styleEl);
}
styleEl.setAttribute('data-theme', theme.id);
styleEl.textContent = getThemeCSS(theme);
}
export function setThemeOnDocument(theme: YaakTheme) {
document.documentElement.setAttribute('data-theme', theme.id);
}
export function indent(text: string, space = ' '): string {
return text
.split('\n')
.map((line) => space + line)
.join('\n');
}