Organize misc files into theme, themes and styleTrees
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import chroma from "chroma-js"
|
||||
|
||||
export function withOpacity(color: string, opacity: number): string {
|
||||
return chroma(color).alpha(opacity).hex()
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import { Scale, Color } from "chroma-js"
|
||||
import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
|
||||
export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
|
||||
import {
|
||||
ThemeConfig,
|
||||
ThemeAppearance,
|
||||
ThemeConfigInputColors,
|
||||
} from "./themeConfig"
|
||||
import { getRamps } from "./ramps"
|
||||
|
||||
export interface ColorScheme {
|
||||
name: string
|
||||
isLight: boolean
|
||||
|
||||
lowest: Layer
|
||||
middle: Layer
|
||||
highest: Layer
|
||||
|
||||
ramps: RampSet
|
||||
|
||||
popoverShadow: Shadow
|
||||
modalShadow: Shadow
|
||||
|
||||
players: Players
|
||||
syntax?: Partial<ThemeSyntax>
|
||||
}
|
||||
|
||||
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 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 createColorScheme(theme: ThemeConfig): ColorScheme {
|
||||
const {
|
||||
name,
|
||||
appearance,
|
||||
inputColor,
|
||||
override: { syntax },
|
||||
} = theme
|
||||
|
||||
const isLight = appearance === ThemeAppearance.Light
|
||||
const colorRamps: ThemeConfigInputColors = inputColor
|
||||
|
||||
// Chromajs scales from 0 to 1 flipped if isLight is true
|
||||
const ramps = getRamps(isLight, colorRamps)
|
||||
const lowest = lowestLayer(ramps)
|
||||
const middle = middleLayer(ramps)
|
||||
const highest = highestLayer(ramps)
|
||||
|
||||
const popoverShadow = {
|
||||
blur: 4,
|
||||
color: ramps
|
||||
.neutral(isLight ? 7 : 0)
|
||||
.darken()
|
||||
.alpha(0.2)
|
||||
.hex(), // TODO used blend previously. Replace with something else
|
||||
offset: [1, 2],
|
||||
}
|
||||
|
||||
const modalShadow = {
|
||||
blur: 16,
|
||||
color: ramps
|
||||
.neutral(isLight ? 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),
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
isLight,
|
||||
|
||||
ramps,
|
||||
|
||||
lowest,
|
||||
middle,
|
||||
highest,
|
||||
|
||||
popoverShadow,
|
||||
modalShadow,
|
||||
|
||||
players,
|
||||
syntax,
|
||||
}
|
||||
}
|
||||
|
||||
function player(ramp: Scale): Player {
|
||||
return {
|
||||
selection: ramp(0.5).alpha(0.24).hex(),
|
||||
cursor: ramp(0.5).hex(),
|
||||
}
|
||||
}
|
||||
|
||||
function lowestLayer(ramps: RampSet): Layer {
|
||||
return {
|
||||
base: buildStyleSet(ramps.neutral, 0.2, 1),
|
||||
variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
|
||||
on: buildStyleSet(ramps.neutral, 0.1, 1),
|
||||
accent: buildStyleSet(ramps.blue, 0.1, 0.5),
|
||||
positive: buildStyleSet(ramps.green, 0.1, 0.5),
|
||||
warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
|
||||
negative: buildStyleSet(ramps.red, 0.1, 0.5),
|
||||
}
|
||||
}
|
||||
|
||||
function middleLayer(ramps: RampSet): Layer {
|
||||
return {
|
||||
base: buildStyleSet(ramps.neutral, 0.1, 1),
|
||||
variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
|
||||
on: buildStyleSet(ramps.neutral, 0, 1),
|
||||
accent: buildStyleSet(ramps.blue, 0.1, 0.5),
|
||||
positive: buildStyleSet(ramps.green, 0.1, 0.5),
|
||||
warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
|
||||
negative: buildStyleSet(ramps.red, 0.1, 0.5),
|
||||
}
|
||||
}
|
||||
|
||||
function highestLayer(ramps: RampSet): Layer {
|
||||
return {
|
||||
base: buildStyleSet(ramps.neutral, 0, 1),
|
||||
variant: buildStyleSet(ramps.neutral, 0, 0.7),
|
||||
on: buildStyleSet(ramps.neutral, 0.1, 1),
|
||||
accent: buildStyleSet(ramps.blue, 0.1, 0.5),
|
||||
positive: buildStyleSet(ramps.green, 0.1, 0.5),
|
||||
warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
|
||||
negative: buildStyleSet(ramps.red, 0.1, 0.5),
|
||||
}
|
||||
}
|
||||
|
||||
function buildStyleSet(
|
||||
ramp: Scale,
|
||||
backgroundBase: number,
|
||||
foregroundBase: number,
|
||||
step: number = 0.08
|
||||
): StyleSet {
|
||||
let styleDefinitions = buildStyleDefinition(
|
||||
backgroundBase,
|
||||
foregroundBase,
|
||||
step
|
||||
)
|
||||
|
||||
function colorString(indexOrColor: number | Color): string {
|
||||
if (typeof indexOrColor === "number") {
|
||||
return ramp(indexOrColor).hex()
|
||||
} else {
|
||||
return indexOrColor.hex()
|
||||
}
|
||||
}
|
||||
|
||||
function buildStyle(style: Styles): Style {
|
||||
return {
|
||||
background: colorString(styleDefinitions.background[style]),
|
||||
border: colorString(styleDefinitions.border[style]),
|
||||
foreground: colorString(styleDefinitions.foreground[style]),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
default: buildStyle("default"),
|
||||
hovered: buildStyle("hovered"),
|
||||
pressed: buildStyle("pressed"),
|
||||
active: buildStyle("active"),
|
||||
disabled: buildStyle("disabled"),
|
||||
inverted: buildStyle("inverted"),
|
||||
}
|
||||
}
|
||||
|
||||
function buildStyleDefinition(
|
||||
bgBase: number,
|
||||
fgBase: number,
|
||||
step: number = 0.08
|
||||
) {
|
||||
return {
|
||||
background: {
|
||||
default: bgBase,
|
||||
hovered: bgBase + step,
|
||||
pressed: bgBase + step * 1.5,
|
||||
active: bgBase + step * 2.2,
|
||||
disabled: bgBase,
|
||||
inverted: fgBase + step * 6,
|
||||
},
|
||||
border: {
|
||||
default: bgBase + step * 1,
|
||||
hovered: bgBase + step,
|
||||
pressed: bgBase + step,
|
||||
active: bgBase + step * 3,
|
||||
disabled: bgBase + step * 0.5,
|
||||
inverted: bgBase - step * 3,
|
||||
},
|
||||
foreground: {
|
||||
default: fgBase,
|
||||
hovered: fgBase,
|
||||
pressed: fgBase,
|
||||
active: fgBase + step * 6,
|
||||
disabled: bgBase + step * 4,
|
||||
inverted: bgBase + step * 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./colorScheme"
|
||||
export * from "./ramps"
|
||||
export * from "./syntax"
|
||||
export * from "./themeConfig"
|
||||
@@ -0,0 +1,47 @@
|
||||
import chroma, { Color, Scale } from "chroma-js"
|
||||
import { RampSet } from "./colorScheme"
|
||||
import {
|
||||
ThemeConfigInputColors,
|
||||
ThemeConfigInputColorsKeys,
|
||||
} from "./themeConfig"
|
||||
|
||||
export function colorRamp(color: Color): Scale {
|
||||
let endColor = color.desaturate(1).brighten(5)
|
||||
let startColor = color.desaturate(1).darken(4)
|
||||
return chroma.scale([startColor, color, endColor]).mode("lab")
|
||||
}
|
||||
|
||||
/**
|
||||
* Chromajs mutates the underlying ramp when you call domain. This causes problems because
|
||||
we now store the ramps object in the theme so that we can pull colors out of them.
|
||||
So instead of calling domain and storing the result, we have to construct new ramps for each
|
||||
theme so that we don't modify the passed in ramps.
|
||||
This combined with an error in the type definitions for chroma js means we have to cast the colors
|
||||
function to any in order to get the colors back out from the original ramps.
|
||||
* @param isLight
|
||||
* @param colorRamps
|
||||
* @returns
|
||||
*/
|
||||
export function getRamps(
|
||||
isLight: boolean,
|
||||
colorRamps: ThemeConfigInputColors
|
||||
): RampSet {
|
||||
const ramps: RampSet = {} as any
|
||||
const colorsKeys = Object.keys(colorRamps) as ThemeConfigInputColorsKeys[]
|
||||
|
||||
if (isLight) {
|
||||
for (const rampName of colorsKeys) {
|
||||
ramps[rampName] = chroma.scale(
|
||||
colorRamps[rampName].colors(100).reverse()
|
||||
)
|
||||
}
|
||||
ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse())
|
||||
} else {
|
||||
for (const rampName of colorsKeys) {
|
||||
ramps[rampName] = chroma.scale(colorRamps[rampName].colors(100))
|
||||
}
|
||||
ramps.neutral = chroma.scale(colorRamps.neutral.colors(100))
|
||||
}
|
||||
|
||||
return ramps
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import deepmerge from "deepmerge"
|
||||
import { FontWeight, fontWeights } from "../common"
|
||||
import { ColorScheme } from "./colorScheme"
|
||||
import chroma from "chroma-js"
|
||||
|
||||
export interface SyntaxHighlightStyle {
|
||||
color?: string
|
||||
weight?: FontWeight
|
||||
underline?: boolean
|
||||
italic?: boolean
|
||||
}
|
||||
|
||||
export interface Syntax {
|
||||
// == Text Styles ====== /
|
||||
comment: SyntaxHighlightStyle
|
||||
// elixir: doc comment
|
||||
"comment.doc": SyntaxHighlightStyle
|
||||
primary: SyntaxHighlightStyle
|
||||
predictive: SyntaxHighlightStyle
|
||||
|
||||
// === Formatted Text ====== /
|
||||
emphasis: SyntaxHighlightStyle
|
||||
"emphasis.strong": SyntaxHighlightStyle
|
||||
title: SyntaxHighlightStyle
|
||||
linkUri: SyntaxHighlightStyle
|
||||
linkText: SyntaxHighlightStyle
|
||||
/** md: indented_code_block, fenced_code_block, code_span */
|
||||
"text.literal": SyntaxHighlightStyle
|
||||
|
||||
// == Punctuation ====== /
|
||||
punctuation: SyntaxHighlightStyle
|
||||
/** Example: `(`, `[`, `{`...*/
|
||||
"punctuation.bracket": SyntaxHighlightStyle
|
||||
/**., ;*/
|
||||
"punctuation.delimiter": SyntaxHighlightStyle
|
||||
// js, ts: ${, } in a template literal
|
||||
// yaml: *, &, ---, ...
|
||||
"punctuation.special": SyntaxHighlightStyle
|
||||
// md: list_marker_plus, list_marker_dot, etc
|
||||
"punctuation.list_marker": SyntaxHighlightStyle
|
||||
|
||||
// == Strings ====== /
|
||||
|
||||
string: SyntaxHighlightStyle
|
||||
// css: color_value
|
||||
// js: this, super
|
||||
// toml: offset_date_time, local_date_time...
|
||||
"string.special": SyntaxHighlightStyle
|
||||
// elixir: atom, quoted_atom, keyword, quoted_keyword
|
||||
// ruby: simple_symbol, delimited_symbol...
|
||||
"string.special.symbol"?: SyntaxHighlightStyle
|
||||
// elixir, python, yaml...: escape_sequence
|
||||
"string.escape"?: SyntaxHighlightStyle
|
||||
// Regular expressions
|
||||
"string.regex"?: SyntaxHighlightStyle
|
||||
|
||||
// == Types ====== /
|
||||
// We allow Function here because all JS objects literals have this property
|
||||
constructor: SyntaxHighlightStyle | Function
|
||||
variant: SyntaxHighlightStyle
|
||||
type: SyntaxHighlightStyle
|
||||
// js: predefined_type
|
||||
"type.builtin"?: SyntaxHighlightStyle
|
||||
|
||||
// == Values
|
||||
variable: SyntaxHighlightStyle
|
||||
// this, ...
|
||||
// css: -- (var(--foo))
|
||||
// lua: self
|
||||
"variable.special"?: SyntaxHighlightStyle
|
||||
// c: statement_identifier,
|
||||
label: SyntaxHighlightStyle
|
||||
// css: tag_name, nesting_selector, universal_selector...
|
||||
tag: SyntaxHighlightStyle
|
||||
// css: attribute, pseudo_element_selector (tag_name),
|
||||
attribute: SyntaxHighlightStyle
|
||||
// css: class_name, property_name, namespace_name...
|
||||
property: SyntaxHighlightStyle
|
||||
// true, false, null, nullptr
|
||||
constant: SyntaxHighlightStyle
|
||||
// css: @media, @import, @supports...
|
||||
// js: declare, implements, interface, keyof, public...
|
||||
keyword: SyntaxHighlightStyle
|
||||
// note: js enum is currently defined as a keyword
|
||||
enum: SyntaxHighlightStyle
|
||||
// -, --, ->, !=, &&, ||, <=...
|
||||
operator: SyntaxHighlightStyle
|
||||
number: SyntaxHighlightStyle
|
||||
boolean: SyntaxHighlightStyle
|
||||
// elixir: __MODULE__, __DIR__, __ENV__, etc
|
||||
// go: nil, iota
|
||||
"constant.builtin"?: SyntaxHighlightStyle
|
||||
|
||||
// == Functions ====== /
|
||||
|
||||
function: SyntaxHighlightStyle
|
||||
// lua: assert, error, loadfile, tostring, unpack...
|
||||
"function.builtin"?: SyntaxHighlightStyle
|
||||
// go: call_expression, method_declaration
|
||||
// js: call_expression, method_definition, pair (key, arrow function)
|
||||
// rust: function_item name: (identifier)
|
||||
"function.definition"?: SyntaxHighlightStyle
|
||||
// rust: macro_definition name: (identifier)
|
||||
"function.special.definition"?: SyntaxHighlightStyle
|
||||
"function.method"?: SyntaxHighlightStyle
|
||||
// ruby: identifier/"defined?" // Nate note: I don't fully understand this one.
|
||||
"function.method.builtin"?: SyntaxHighlightStyle
|
||||
|
||||
// == Unsorted ====== /
|
||||
// lua: hash_bang_line
|
||||
preproc: SyntaxHighlightStyle
|
||||
// elixir, python: interpolation (ex: foo in ${foo})
|
||||
// js: template_substitution
|
||||
embedded: SyntaxHighlightStyle
|
||||
}
|
||||
|
||||
export type ThemeSyntax = Partial<Syntax>
|
||||
|
||||
const defaultSyntaxHighlightStyle: Omit<SyntaxHighlightStyle, "color"> = {
|
||||
weight: "normal",
|
||||
underline: false,
|
||||
italic: false,
|
||||
}
|
||||
|
||||
function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
|
||||
// Make a temporary object that is allowed to be missing
|
||||
// the "color" property for each style
|
||||
const syntax: {
|
||||
[key: string]: Omit<SyntaxHighlightStyle, "color">
|
||||
} = {}
|
||||
|
||||
const light = colorScheme.isLight
|
||||
|
||||
// then spread the default to each style
|
||||
for (const key of Object.keys({} as Syntax)) {
|
||||
syntax[key as keyof Syntax] = {
|
||||
...defaultSyntaxHighlightStyle,
|
||||
}
|
||||
}
|
||||
|
||||
// Mix the neutral and blue colors to get a
|
||||
// predictive color distinct from any other color in the theme
|
||||
const predictive = chroma
|
||||
.mix(
|
||||
colorScheme.ramps.neutral(0.4).hex(),
|
||||
colorScheme.ramps.blue(0.4).hex(),
|
||||
0.45,
|
||||
"lch"
|
||||
)
|
||||
.hex()
|
||||
|
||||
const color = {
|
||||
primary: colorScheme.ramps.neutral(1).hex(),
|
||||
comment: colorScheme.ramps.neutral(0.71).hex(),
|
||||
punctuation: colorScheme.ramps.neutral(0.86).hex(),
|
||||
predictive: predictive,
|
||||
emphasis: colorScheme.ramps.blue(0.5).hex(),
|
||||
string: colorScheme.ramps.orange(0.5).hex(),
|
||||
function: colorScheme.ramps.yellow(0.5).hex(),
|
||||
type: colorScheme.ramps.cyan(0.5).hex(),
|
||||
constructor: colorScheme.ramps.blue(0.5).hex(),
|
||||
variant: colorScheme.ramps.blue(0.5).hex(),
|
||||
property: colorScheme.ramps.blue(0.5).hex(),
|
||||
enum: colorScheme.ramps.orange(0.5).hex(),
|
||||
operator: colorScheme.ramps.orange(0.5).hex(),
|
||||
number: colorScheme.ramps.green(0.5).hex(),
|
||||
boolean: colorScheme.ramps.green(0.5).hex(),
|
||||
constant: colorScheme.ramps.green(0.5).hex(),
|
||||
keyword: colorScheme.ramps.blue(0.5).hex(),
|
||||
}
|
||||
|
||||
// Then assign colors and use Syntax to enforce each style getting it's own color
|
||||
const defaultSyntax: Syntax = {
|
||||
...syntax,
|
||||
comment: {
|
||||
color: color.comment,
|
||||
},
|
||||
"comment.doc": {
|
||||
color: color.comment,
|
||||
},
|
||||
primary: {
|
||||
color: color.primary,
|
||||
},
|
||||
predictive: {
|
||||
color: color.predictive,
|
||||
italic: true,
|
||||
},
|
||||
emphasis: {
|
||||
color: color.emphasis,
|
||||
},
|
||||
"emphasis.strong": {
|
||||
color: color.emphasis,
|
||||
weight: fontWeights.bold,
|
||||
},
|
||||
title: {
|
||||
color: color.primary,
|
||||
weight: fontWeights.bold,
|
||||
},
|
||||
linkUri: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
underline: true,
|
||||
},
|
||||
linkText: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
italic: true,
|
||||
},
|
||||
"text.literal": {
|
||||
color: color.string,
|
||||
},
|
||||
punctuation: {
|
||||
color: color.punctuation,
|
||||
},
|
||||
"punctuation.bracket": {
|
||||
color: color.punctuation,
|
||||
},
|
||||
"punctuation.delimiter": {
|
||||
color: color.punctuation,
|
||||
},
|
||||
"punctuation.special": {
|
||||
color: colorScheme.ramps.neutral(0.86).hex(),
|
||||
},
|
||||
"punctuation.list_marker": {
|
||||
color: color.punctuation,
|
||||
},
|
||||
string: {
|
||||
color: color.string,
|
||||
},
|
||||
"string.special": {
|
||||
color: color.string,
|
||||
},
|
||||
"string.special.symbol": {
|
||||
color: color.string,
|
||||
},
|
||||
"string.escape": {
|
||||
color: color.comment,
|
||||
},
|
||||
"string.regex": {
|
||||
color: color.string,
|
||||
},
|
||||
constructor: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
},
|
||||
variant: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
},
|
||||
type: {
|
||||
color: color.type,
|
||||
},
|
||||
variable: {
|
||||
color: color.primary,
|
||||
},
|
||||
label: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
},
|
||||
tag: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
},
|
||||
attribute: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
},
|
||||
property: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
},
|
||||
constant: {
|
||||
color: color.constant,
|
||||
},
|
||||
keyword: {
|
||||
color: color.keyword,
|
||||
},
|
||||
enum: {
|
||||
color: color.enum,
|
||||
},
|
||||
operator: {
|
||||
color: color.operator,
|
||||
},
|
||||
number: {
|
||||
color: color.number,
|
||||
},
|
||||
boolean: {
|
||||
color: color.boolean,
|
||||
},
|
||||
function: {
|
||||
color: color.function,
|
||||
},
|
||||
preproc: {
|
||||
color: color.primary,
|
||||
},
|
||||
embedded: {
|
||||
color: color.primary,
|
||||
},
|
||||
}
|
||||
|
||||
return defaultSyntax
|
||||
}
|
||||
|
||||
function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
|
||||
if (!colorScheme.syntax) {
|
||||
return defaultSyntax
|
||||
}
|
||||
|
||||
return deepmerge<Syntax, Partial<ThemeSyntax>>(
|
||||
defaultSyntax,
|
||||
colorScheme.syntax,
|
||||
{
|
||||
arrayMerge: (destinationArray, sourceArray) => [
|
||||
...destinationArray,
|
||||
...sourceArray,
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function buildSyntax(colorScheme: ColorScheme): Syntax {
|
||||
const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme)
|
||||
|
||||
const syntax = mergeSyntax(defaultSyntax, colorScheme)
|
||||
|
||||
return syntax
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Scale, Color } from "chroma-js"
|
||||
import { Syntax } from "./syntax"
|
||||
|
||||
interface ThemeMeta {
|
||||
/** The name of the theme */
|
||||
name: string
|
||||
/** The theme's appearance. Either `light` or `dark`. */
|
||||
appearance: ThemeAppearance
|
||||
/** The author of the theme
|
||||
*
|
||||
* Ideally formatted as `Full Name <email>`
|
||||
*
|
||||
* Example: `John Doe <john@doe.com>`
|
||||
*/
|
||||
author: string
|
||||
/** SPDX License string
|
||||
*
|
||||
* Example: `MIT`
|
||||
*/
|
||||
licenseType?: string | ThemeLicenseType
|
||||
licenseUrl?: string
|
||||
licenseFile: string
|
||||
themeUrl?: string
|
||||
}
|
||||
|
||||
export interface ThemeConfigInputColors {
|
||||
neutral: Scale<Color>
|
||||
red: Scale<Color>
|
||||
orange: Scale<Color>
|
||||
yellow: Scale<Color>
|
||||
green: Scale<Color>
|
||||
cyan: Scale<Color>
|
||||
blue: Scale<Color>
|
||||
violet: Scale<Color>
|
||||
magenta: Scale<Color>
|
||||
}
|
||||
|
||||
export type ThemeConfigInputColorsKeys = keyof ThemeConfigInputColors
|
||||
|
||||
/** Allow any part of a syntax highlight style to be overriden by the theme
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* override: {
|
||||
* syntax: {
|
||||
* boolean: {
|
||||
* underline: true,
|
||||
* },
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type ThemeConfigInputSyntax = Partial<Syntax>
|
||||
|
||||
interface ThemeConfigOverrides {
|
||||
syntax: ThemeConfigInputSyntax
|
||||
}
|
||||
|
||||
type ThemeConfigProperties = ThemeMeta & {
|
||||
inputColor: ThemeConfigInputColors
|
||||
override: ThemeConfigOverrides
|
||||
}
|
||||
|
||||
// This should be the format a theme is defined as
|
||||
export type ThemeConfig = {
|
||||
[K in keyof ThemeConfigProperties]: ThemeConfigProperties[K]
|
||||
}
|
||||
|
||||
interface ThemeColors {
|
||||
neutral: string[]
|
||||
red: string[]
|
||||
orange: string[]
|
||||
yellow: string[]
|
||||
green: string[]
|
||||
cyan: string[]
|
||||
blue: string[]
|
||||
violet: string[]
|
||||
magenta: string[]
|
||||
}
|
||||
|
||||
type ThemeSyntax = Required<Syntax>
|
||||
|
||||
export type ThemeProperties = ThemeMeta & {
|
||||
color: ThemeColors
|
||||
syntax: ThemeSyntax
|
||||
}
|
||||
|
||||
// This should be a theme after all its properties have been resolved
|
||||
export type Theme = {
|
||||
[K in keyof ThemeProperties]: ThemeProperties[K]
|
||||
}
|
||||
|
||||
export enum ThemeAppearance {
|
||||
Light = "light",
|
||||
Dark = "dark",
|
||||
}
|
||||
|
||||
export enum ThemeLicenseType {
|
||||
MIT = "MIT",
|
||||
Apache2 = "Apache License 2.0",
|
||||
}
|
||||
|
||||
export type ThemeFamilyItem =
|
||||
| ThemeConfig
|
||||
| { light: ThemeConfig; dark: ThemeConfig }
|
||||
|
||||
type ThemeFamilyProperties = Partial<Omit<ThemeMeta, "name" | "appearance">> & {
|
||||
name: string
|
||||
default: ThemeFamilyItem
|
||||
variants: {
|
||||
[key: string]: ThemeFamilyItem
|
||||
}
|
||||
}
|
||||
|
||||
// Idea: A theme family is a collection of themes that share the same name
|
||||
// For example, a theme family could be `One Dark` and have a `light` and `dark` variant
|
||||
// The Ayu family could have `light`, `mirage`, and `dark` variants
|
||||
|
||||
type ThemeFamily = {
|
||||
[K in keyof ThemeFamilyProperties]: ThemeFamilyProperties[K]
|
||||
}
|
||||
|
||||
/** The collection of all themes
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* {
|
||||
* one_dark,
|
||||
* one_light,
|
||||
* ayu: {
|
||||
* name: 'Ayu',
|
||||
* default: 'ayu_mirage',
|
||||
* variants: {
|
||||
* light: 'ayu_light',
|
||||
* mirage: 'ayu_mirage',
|
||||
* dark: 'ayu_dark',
|
||||
* },
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type ThemeIndex = Record<string, ThemeFamily | ThemeConfig>
|
||||
Reference in New Issue
Block a user