1import { Scale, Color } from "chroma-js"
2import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
3export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
4import {
5 ThemeConfig,
6 ThemeAppearance,
7 ThemeConfigInputColors,
8} from "./themeConfig"
9import { getRamps } from "./ramps"
10
11export interface ColorScheme {
12 name: string
13 isLight: boolean
14
15 lowest: Layer
16 middle: Layer
17 highest: Layer
18
19 ramps: RampSet
20
21 popoverShadow: Shadow
22 modalShadow: Shadow
23
24 players: Players
25 syntax?: Partial<ThemeSyntax>
26}
27
28export interface Meta {
29 name: string
30 author: string
31 url: string
32 license: License
33}
34
35export interface License {
36 SPDX: SPDXExpression
37}
38
39// License name -> License text
40export interface Licenses {
41 [key: string]: string
42}
43
44// FIXME: Add support for the SPDX expression syntax
45export type SPDXExpression = "MIT"
46
47export interface Player {
48 cursor: string
49 selection: string
50}
51
52export interface Players {
53 "0": Player
54 "1": Player
55 "2": Player
56 "3": Player
57 "4": Player
58 "5": Player
59 "6": Player
60 "7": Player
61}
62
63export interface Shadow {
64 blur: number
65 color: string
66 offset: number[]
67}
68
69export type StyleSets = keyof Layer
70export interface Layer {
71 base: StyleSet
72 variant: StyleSet
73 on: StyleSet
74 accent: StyleSet
75 positive: StyleSet
76 warning: StyleSet
77 negative: StyleSet
78}
79
80export interface RampSet {
81 neutral: Scale
82 red: Scale
83 orange: Scale
84 yellow: Scale
85 green: Scale
86 cyan: Scale
87 blue: Scale
88 violet: Scale
89 magenta: Scale
90}
91
92export type Styles = keyof StyleSet
93export interface StyleSet {
94 default: Style
95 active: Style
96 disabled: Style
97 hovered: Style
98 pressed: Style
99 inverted: Style
100}
101
102export interface Style {
103 background: string
104 border: string
105 foreground: string
106}
107
108export function createColorScheme(theme: ThemeConfig): ColorScheme {
109 const {
110 name,
111 appearance,
112 inputColor,
113 override: { syntax },
114 } = theme
115
116 const isLight = appearance === ThemeAppearance.Light
117 const colorRamps: ThemeConfigInputColors = inputColor
118
119 // Chromajs scales from 0 to 1 flipped if isLight is true
120 const ramps = getRamps(isLight, colorRamps)
121 const lowest = lowestLayer(ramps)
122 const middle = middleLayer(ramps)
123 const highest = highestLayer(ramps)
124
125 const popoverShadow = {
126 blur: 4,
127 color: ramps
128 .neutral(isLight ? 7 : 0)
129 .darken()
130 .alpha(0.2)
131 .hex(), // TODO used blend previously. Replace with something else
132 offset: [1, 2],
133 }
134
135 const modalShadow = {
136 blur: 16,
137 color: ramps
138 .neutral(isLight ? 7 : 0)
139 .darken()
140 .alpha(0.2)
141 .hex(), // TODO used blend previously. Replace with something else
142 offset: [0, 2],
143 }
144
145 const players = {
146 "0": player(ramps.blue),
147 "1": player(ramps.green),
148 "2": player(ramps.magenta),
149 "3": player(ramps.orange),
150 "4": player(ramps.violet),
151 "5": player(ramps.cyan),
152 "6": player(ramps.red),
153 "7": player(ramps.yellow),
154 }
155
156 return {
157 name,
158 isLight,
159
160 ramps,
161
162 lowest,
163 middle,
164 highest,
165
166 popoverShadow,
167 modalShadow,
168
169 players,
170 syntax,
171 }
172}
173
174function player(ramp: Scale): Player {
175 return {
176 selection: ramp(0.5).alpha(0.24).hex(),
177 cursor: ramp(0.5).hex(),
178 }
179}
180
181function lowestLayer(ramps: RampSet): Layer {
182 return {
183 base: buildStyleSet(ramps.neutral, 0.2, 1),
184 variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
185 on: buildStyleSet(ramps.neutral, 0.1, 1),
186 accent: buildStyleSet(ramps.blue, 0.1, 0.5),
187 positive: buildStyleSet(ramps.green, 0.1, 0.5),
188 warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
189 negative: buildStyleSet(ramps.red, 0.1, 0.5),
190 }
191}
192
193function middleLayer(ramps: RampSet): Layer {
194 return {
195 base: buildStyleSet(ramps.neutral, 0.1, 1),
196 variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
197 on: buildStyleSet(ramps.neutral, 0, 1),
198 accent: buildStyleSet(ramps.blue, 0.1, 0.5),
199 positive: buildStyleSet(ramps.green, 0.1, 0.5),
200 warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
201 negative: buildStyleSet(ramps.red, 0.1, 0.5),
202 }
203}
204
205function highestLayer(ramps: RampSet): Layer {
206 return {
207 base: buildStyleSet(ramps.neutral, 0, 1),
208 variant: buildStyleSet(ramps.neutral, 0, 0.7),
209 on: buildStyleSet(ramps.neutral, 0.1, 1),
210 accent: buildStyleSet(ramps.blue, 0.1, 0.5),
211 positive: buildStyleSet(ramps.green, 0.1, 0.5),
212 warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
213 negative: buildStyleSet(ramps.red, 0.1, 0.5),
214 }
215}
216
217function buildStyleSet(
218 ramp: Scale,
219 backgroundBase: number,
220 foregroundBase: number,
221 step: number = 0.08
222): StyleSet {
223 let styleDefinitions = buildStyleDefinition(
224 backgroundBase,
225 foregroundBase,
226 step
227 )
228
229 function colorString(indexOrColor: number | Color): string {
230 if (typeof indexOrColor === "number") {
231 return ramp(indexOrColor).hex()
232 } else {
233 return indexOrColor.hex()
234 }
235 }
236
237 function buildStyle(style: Styles): Style {
238 return {
239 background: colorString(styleDefinitions.background[style]),
240 border: colorString(styleDefinitions.border[style]),
241 foreground: colorString(styleDefinitions.foreground[style]),
242 }
243 }
244
245 return {
246 default: buildStyle("default"),
247 hovered: buildStyle("hovered"),
248 pressed: buildStyle("pressed"),
249 active: buildStyle("active"),
250 disabled: buildStyle("disabled"),
251 inverted: buildStyle("inverted"),
252 }
253}
254
255function buildStyleDefinition(
256 bgBase: number,
257 fgBase: number,
258 step: number = 0.08
259) {
260 return {
261 background: {
262 default: bgBase,
263 hovered: bgBase + step,
264 pressed: bgBase + step * 1.5,
265 active: bgBase + step * 2.2,
266 disabled: bgBase,
267 inverted: fgBase + step * 6,
268 },
269 border: {
270 default: bgBase + step * 1,
271 hovered: bgBase + step,
272 pressed: bgBase + step,
273 active: bgBase + step * 3,
274 disabled: bgBase + step * 0.5,
275 inverted: bgBase - step * 3,
276 },
277 foreground: {
278 default: fgBase,
279 hovered: fgBase,
280 pressed: fgBase,
281 active: fgBase + step * 6,
282 disabled: bgBase + step * 4,
283 inverted: bgBase + step * 2,
284 },
285 }
286}