1import chroma, { Color, Scale } from "chroma-js";
2import { fontWeights } from "../../common";
3import { withOpacity } from "../../utils/color";
4import Theme, { buildPlayer, Syntax } from "./theme";
5
6export function colorRamp(color: Color): Scale {
7 let hue = color.hsl()[0];
8 let endColor = chroma.hsl(hue, 0.88, 0.96);
9 let startColor = chroma.hsl(hue, 0.68, 0.12);
10 return chroma.scale([startColor, color, endColor]).mode("hsl");
11}
12
13export function createTheme(
14 name: string,
15 isLight: boolean,
16 color_ramps: { [rampName: string]: Scale }
17): Theme {
18 let ramps: typeof color_ramps = {};
19 // Chromajs mutates the underlying ramp when you call domain. This causes problems because
20 // we now store the ramps object in the theme so that we can pull colors out of them.
21 // So instead of calling domain and storing the result, we have to construct new ramps for each
22 // theme so that we don't modify the passed in ramps.
23 // This combined with an error in the type definitions for chroma js means we have to cast the colors
24 // function to any in order to get the colors back out from the original ramps.
25 if (isLight) {
26 for (var rampName in color_ramps) {
27 ramps[rampName] = chroma
28 .scale((color_ramps[rampName].colors as any)())
29 .domain([1, 0]);
30 }
31 ramps.neutral = chroma
32 .scale((color_ramps.neutral.colors as any)())
33 .domain([7, 0]);
34 } else {
35 for (var rampName in color_ramps) {
36 ramps[rampName] = chroma
37 .scale((color_ramps[rampName].colors as any)())
38 .domain([0, 1]);
39 }
40 ramps.neutral = chroma
41 .scale((color_ramps.neutral.colors as any)())
42 .domain([0, 7]);
43 }
44
45 let blend = isLight ? 0.12 : 0.24;
46
47 function sample(ramp: Scale, index: number): string {
48 return ramp(index).hex();
49 }
50 const darkest = ramps.neutral(isLight ? 7 : 0).hex();
51
52 const backgroundColor = {
53 // Title bar
54 100: {
55 base: sample(ramps.neutral, 1.25),
56 hovered: sample(ramps.neutral, 1.5),
57 active: sample(ramps.neutral, 1.75),
58 },
59 // Midground (panels, etc)
60 300: {
61 base: sample(ramps.neutral, 1),
62 hovered: sample(ramps.neutral, 1.25),
63 active: sample(ramps.neutral, 1.5),
64 },
65 // Editor
66 500: {
67 base: sample(ramps.neutral, 0),
68 hovered: sample(ramps.neutral, 0.25),
69 active: sample(ramps.neutral, 0.5),
70 },
71 on300: {
72 base: sample(ramps.neutral, 0),
73 hovered: sample(ramps.neutral, 0.5),
74 active: sample(ramps.neutral, 1),
75 },
76 on500: {
77 base: sample(ramps.neutral, 1),
78 hovered: sample(ramps.neutral, 1.5),
79 active: sample(ramps.neutral, 2),
80 },
81 ok: {
82 base: withOpacity(sample(ramps.green, 0.5), 0.15),
83 hovered: withOpacity(sample(ramps.green, 0.5), 0.2),
84 active: withOpacity(sample(ramps.green, 0.5), 0.25),
85 },
86 error: {
87 base: withOpacity(sample(ramps.red, 0.5), 0.15),
88 hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
89 active: withOpacity(sample(ramps.red, 0.5), 0.25),
90 },
91 on500Error: {
92 base: sample(ramps.red, 0.05),
93 hovered: sample(ramps.red, 0.1),
94 active: sample(ramps.red, 0.15),
95 },
96 warning: {
97 base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
98 hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
99 active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
100 },
101 on500Warning: {
102 base: sample(ramps.yellow, 0.05),
103 hovered: sample(ramps.yellow, 0.1),
104 active: sample(ramps.yellow, 0.15),
105 },
106 info: {
107 base: withOpacity(sample(ramps.blue, 0.5), 0.15),
108 hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
109 active: withOpacity(sample(ramps.blue, 0.5), 0.25),
110 },
111 on500Info: {
112 base: sample(ramps.blue, 0.05),
113 hovered: sample(ramps.blue, 0.1),
114 active: sample(ramps.blue, 0.15),
115 },
116 };
117
118 const borderColor = {
119 primary: sample(ramps.neutral, isLight ? 1.5 : 0),
120 secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
121 muted: sample(ramps.neutral, isLight ? 1 : 3),
122 active: sample(ramps.neutral, isLight ? 4 : 3),
123 onMedia: withOpacity(darkest, 0.1),
124 ok: sample(ramps.green, 0.3),
125 error: sample(ramps.red, 0.3),
126 warning: sample(ramps.yellow, 0.3),
127 info: sample(ramps.blue, 0.3),
128 };
129
130 const textColor = {
131 primary: sample(ramps.neutral, 6),
132 secondary: sample(ramps.neutral, 5),
133 muted: sample(ramps.neutral, 4),
134 placeholder: sample(ramps.neutral, 3),
135 active: sample(ramps.neutral, 7),
136 feature: sample(ramps.blue, 0.5),
137 ok: sample(ramps.green, 0.5),
138 error: sample(ramps.red, 0.5),
139 warning: sample(ramps.yellow, 0.5),
140 info: sample(ramps.blue, 0.5),
141 onMedia: darkest,
142 };
143
144 const player = {
145 1: buildPlayer(sample(ramps.blue, 0.5)),
146 2: buildPlayer(sample(ramps.green, 0.5)),
147 3: buildPlayer(sample(ramps.magenta, 0.5)),
148 4: buildPlayer(sample(ramps.orange, 0.5)),
149 5: buildPlayer(sample(ramps.violet, 0.5)),
150 6: buildPlayer(sample(ramps.cyan, 0.5)),
151 7: buildPlayer(sample(ramps.red, 0.5)),
152 8: buildPlayer(sample(ramps.yellow, 0.5)),
153 };
154
155 const editor = {
156 background: backgroundColor[500].base,
157 indent_guide: borderColor.muted,
158 indent_guide_active: borderColor.secondary,
159 line: {
160 active: sample(ramps.neutral, 1),
161 highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
162 },
163 highlight: {
164 selection: player[1].selectionColor,
165 occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
166 activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
167 matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
168 match: sample(ramps.violet, 0.15),
169 activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
170 related: backgroundColor[500].hovered,
171 },
172 gutter: {
173 primary: textColor.placeholder,
174 active: textColor.active,
175 },
176 };
177
178 const syntax: Syntax = {
179 primary: {
180 color: sample(ramps.neutral, 7),
181 weight: fontWeights.normal,
182 },
183 comment: {
184 color: sample(ramps.neutral, 5),
185 weight: fontWeights.normal,
186 },
187 punctuation: {
188 color: sample(ramps.neutral, 6),
189 weight: fontWeights.normal,
190 },
191 constant: {
192 color: sample(ramps.neutral, 4),
193 weight: fontWeights.normal,
194 },
195 keyword: {
196 color: sample(ramps.blue, 0.5),
197 weight: fontWeights.normal,
198 },
199 function: {
200 color: sample(ramps.yellow, 0.5),
201 weight: fontWeights.normal,
202 },
203 type: {
204 color: sample(ramps.cyan, 0.5),
205 weight: fontWeights.normal,
206 },
207 constructor: {
208 color: sample(ramps.blue, 0.5),
209 weight: fontWeights.normal,
210 },
211 variant: {
212 color: sample(ramps.blue, 0.5),
213 weight: fontWeights.normal,
214 },
215 property: {
216 color: sample(ramps.blue, 0.5),
217 weight: fontWeights.normal,
218 },
219 enum: {
220 color: sample(ramps.orange, 0.5),
221 weight: fontWeights.normal,
222 },
223 operator: {
224 color: sample(ramps.orange, 0.5),
225 weight: fontWeights.normal,
226 },
227 string: {
228 color: sample(ramps.orange, 0.5),
229 weight: fontWeights.normal,
230 },
231 number: {
232 color: sample(ramps.green, 0.5),
233 weight: fontWeights.normal,
234 },
235 boolean: {
236 color: sample(ramps.green, 0.5),
237 weight: fontWeights.normal,
238 },
239 predictive: {
240 color: textColor.muted,
241 weight: fontWeights.normal,
242 },
243 title: {
244 color: sample(ramps.yellow, 0.5),
245 weight: fontWeights.bold,
246 },
247 emphasis: {
248 color: textColor.feature,
249 weight: fontWeights.normal,
250 },
251 "emphasis.strong": {
252 color: textColor.feature,
253 weight: fontWeights.bold,
254 },
255 linkUri: {
256 color: sample(ramps.green, 0.5),
257 weight: fontWeights.normal,
258 underline: true,
259 },
260 linkText: {
261 color: sample(ramps.orange, 0.5),
262 weight: fontWeights.normal,
263 italic: true,
264 },
265 };
266
267 const shadow = withOpacity(
268 ramps
269 .neutral(isLight ? 7 : 0)
270 .darken()
271 .hex(),
272 blend
273 );
274
275 return {
276 name,
277 isLight,
278 backgroundColor,
279 borderColor,
280 textColor,
281 iconColor: textColor,
282 editor,
283 syntax,
284 player,
285 shadow,
286 ramps,
287 };
288}