Nate ButlerFormatd91a9613 years agoHistory
329 lines · 7.9 KB · TypeScript
RawDownload
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<ThemeSyntax>    color_family: ColorFamily} export interface Meta {    name: string    author: string    url: string    license: License} export interface License {    SPDX: SPDXExpression} // License name -> License textexport interface Licenses {    [key: string]: string} // FIXME: Add support for the SPDX expression syntaxexport 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 Layerexport 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 StyleSetexport 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,        },    }}