import { Scale, Color } from "chroma-js" import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax" export { Syntax, ThemeSyntax, SyntaxHighlightStyle } import { ThemeConfig, ThemeAppearance, ThemeConfigInputColors, } from "./theme_config" import { get_ramps } from "./ramps" export interface Theme { name: string is_light: boolean /** * App background, other elements that should sit directly on top of the background. */ lowest: Layer /** * Panels, tabs, other UI surfaces that sit on top of the background. */ middle: Layer /** * Editors like code buffers, conversation editors, etc. */ highest: Layer ramps: RampSet popover_shadow: Shadow modal_shadow: Shadow players: Players syntax?: Partial color_family: ColorFamily } export interface Meta { name: string author: string url: string license: License } export interface License { SPDX: SPDXExpression } // License name -> License text export interface Licenses { [key: string]: string } // FIXME: Add support for the SPDX expression syntax export type SPDXExpression = "MIT" export interface Player { cursor: string selection: string } export interface Players { "0": Player "1": Player "2": Player "3": Player "4": Player "5": Player "6": Player "7": Player } export type ColorFamily = Partial<{ [K in keyof RampSet]: ColorFamilyRange }> export interface ColorFamilyRange { low: number high: number range: number scaling_value: number } export interface Shadow { blur: number color: string offset: number[] } export type StyleSets = keyof Layer export interface Layer { base: StyleSet variant: StyleSet on: StyleSet accent: StyleSet positive: StyleSet warning: StyleSet negative: StyleSet } export interface RampSet { neutral: Scale red: Scale orange: Scale yellow: Scale green: Scale cyan: Scale blue: Scale violet: Scale magenta: Scale } export type Styles = keyof StyleSet export interface StyleSet { default: Style active: Style disabled: Style hovered: Style pressed: Style inverted: Style } export interface Style { background: string border: string foreground: string } export function create_theme(theme: ThemeConfig): Theme { const { name, appearance, input_color, override: { syntax }, } = theme const is_light = appearance === ThemeAppearance.Light const color_ramps: ThemeConfigInputColors = input_color // Chromajs scales from 0 to 1 flipped if is_light is true const ramps = get_ramps(is_light, color_ramps) const lowest = lowest_layer(ramps) const middle = middle_layer(ramps) const highest = highest_layer(ramps) const popover_shadow = { blur: 4, color: ramps .neutral(is_light ? 7 : 0) .darken() .alpha(0.2) .hex(), // TODO used blend previously. Replace with something else offset: [1, 2], } const modal_shadow = { blur: 16, color: ramps .neutral(is_light ? 7 : 0) .darken() .alpha(0.2) .hex(), // TODO used blend previously. Replace with something else offset: [0, 2], } const players = { "0": player(ramps.blue), "1": player(ramps.green), "2": player(ramps.magenta), "3": player(ramps.orange), "4": player(ramps.violet), "5": player(ramps.cyan), "6": player(ramps.red), "7": player(ramps.yellow), } const color_family = build_color_family(ramps) return { name, is_light, ramps, lowest, middle, highest, popover_shadow, modal_shadow, players, syntax, color_family, } } function player(ramp: Scale): Player { return { selection: ramp(0.5).alpha(0.24).hex(), cursor: ramp(0.5).hex(), } } function build_color_family(ramps: RampSet): ColorFamily { const color_family: ColorFamily = {} for (const ramp in ramps) { const ramp_value = ramps[ramp as keyof RampSet] const lightnessValues = [ramp_value(0).get('hsl.l') * 100, ramp_value(1).get('hsl.l') * 100] const low = Math.min(...lightnessValues) const high = Math.max(...lightnessValues) const range = high - low color_family[ramp as keyof RampSet] = { low, high, range, scaling_value: 100 / range, } } return color_family } function lowest_layer(ramps: RampSet): Layer { return { base: build_style_set(ramps.neutral, 0.2, 1), variant: build_style_set(ramps.neutral, 0.2, 0.7), on: build_style_set(ramps.neutral, 0.1, 1), accent: build_style_set(ramps.blue, 0.1, 0.5), positive: build_style_set(ramps.green, 0.1, 0.5), warning: build_style_set(ramps.yellow, 0.1, 0.5), negative: build_style_set(ramps.red, 0.1, 0.5), } } function middle_layer(ramps: RampSet): Layer { return { base: build_style_set(ramps.neutral, 0.1, 1), variant: build_style_set(ramps.neutral, 0.1, 0.7), on: build_style_set(ramps.neutral, 0, 1), accent: build_style_set(ramps.blue, 0.1, 0.5), positive: build_style_set(ramps.green, 0.1, 0.5), warning: build_style_set(ramps.yellow, 0.1, 0.5), negative: build_style_set(ramps.red, 0.1, 0.5), } } function highest_layer(ramps: RampSet): Layer { return { base: build_style_set(ramps.neutral, 0, 1), variant: build_style_set(ramps.neutral, 0, 0.7), on: build_style_set(ramps.neutral, 0.1, 1), accent: build_style_set(ramps.blue, 0.1, 0.5), positive: build_style_set(ramps.green, 0.1, 0.5), warning: build_style_set(ramps.yellow, 0.1, 0.5), negative: build_style_set(ramps.red, 0.1, 0.5), } } function build_style_set( ramp: Scale, background_base: number, foreground_base: number, step = 0.08 ): StyleSet { const style_definitions = build_style_definition( background_base, foreground_base, step ) function color_string(index_or_color: number | Color): string { if (typeof index_or_color === "number") { return ramp(index_or_color).hex() } else { return index_or_color.hex() } } function build_style(style: Styles): Style { return { background: color_string(style_definitions.background[style]), border: color_string(style_definitions.border[style]), foreground: color_string(style_definitions.foreground[style]), } } return { default: build_style("default"), hovered: build_style("hovered"), pressed: build_style("pressed"), active: build_style("active"), disabled: build_style("disabled"), inverted: build_style("inverted"), } } function build_style_definition(bg_base: number, fg_base: number, step = 0.08) { return { background: { default: bg_base, hovered: bg_base + step, pressed: bg_base + step * 1.5, active: bg_base + step * 2.2, disabled: bg_base, inverted: fg_base + step * 6, }, border: { default: bg_base + step * 1, hovered: bg_base + step, pressed: bg_base + step, active: bg_base + step * 3, disabled: bg_base + step * 0.5, inverted: bg_base - step * 3, }, foreground: { default: fg_base, hovered: fg_base, pressed: fg_base, active: fg_base + step * 6, disabled: bg_base + step * 4, inverted: bg_base + step * 2, }, } }