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.scale((color_ramps[rampName].colors as any)()).domain([1, 0]);
28 }
29 ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([7, 0]);
30 } else {
31 for (var rampName in color_ramps) {
32 ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([0, 1]);
33 }
34 ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([0, 7]);
35 }
36
37 let blend = isLight ? 0.12 : 0.24;
38
39 function sample(ramp: Scale, index: number): string {
40 return ramp(index).hex();
41 }
42 const darkest = ramps.neutral(isLight ? 7 : 0).hex();
43
44 const backgroundColor = {
45 // Title bar
46 100: {
47 base: sample(ramps.neutral, 1.25),
48 hovered: sample(ramps.neutral, 1.5),
49 active: sample(ramps.neutral, 1.75),
50 },
51 // Midground (panels, etc)
52 300: {
53 base: sample(ramps.neutral, 1),
54 hovered: sample(ramps.neutral, 1.25),
55 active: sample(ramps.neutral, 1.5),
56 },
57 // Editor
58 500: {
59 base: sample(ramps.neutral, 0),
60 hovered: sample(ramps.neutral, 0.25),
61 active: sample(ramps.neutral, 0.5),
62 },
63 on300: {
64 base: sample(ramps.neutral, 0),
65 hovered: sample(ramps.neutral, 0.25),
66 active: sample(ramps.neutral, 0.5),
67 },
68 on500: {
69 base: sample(ramps.neutral, 1.25),
70 hovered: sample(ramps.neutral, 1.5),
71 active: sample(ramps.neutral, 1.75),
72 },
73 ok: {
74 base: withOpacity(sample(ramps.green, 0.5), 0.15),
75 hovered: withOpacity(sample(ramps.green, 0.5), 0.2),
76 active: withOpacity(sample(ramps.green, 0.5), 0.25),
77 },
78 error: {
79 base: withOpacity(sample(ramps.red, 0.5), 0.15),
80 hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
81 active: withOpacity(sample(ramps.red, 0.5), 0.25),
82 },
83 warning: {
84 base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
85 hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
86 active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
87 },
88 info: {
89 base: withOpacity(sample(ramps.blue, 0.5), 0.15),
90 hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
91 active: withOpacity(sample(ramps.blue, 0.5), 0.25),
92 },
93 };
94
95 const borderColor = {
96 primary: sample(ramps.neutral, isLight ? 1.5 : 0),
97 secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
98 muted: sample(ramps.neutral, isLight ? 1 : 3),
99 active: sample(ramps.neutral, isLight ? 4 : 3),
100 onMedia: withOpacity(darkest, 0.1),
101 ok: withOpacity(sample(ramps.green, 0.5), 0.15),
102 error: withOpacity(sample(ramps.red, 0.5), 0.15),
103 warning: withOpacity(sample(ramps.yellow, 0.5), 0.15),
104 info: withOpacity(sample(ramps.blue, 0.5), 0.15),
105 };
106
107 const textColor = {
108 primary: sample(ramps.neutral, 6),
109 secondary: sample(ramps.neutral, 5),
110 muted: sample(ramps.neutral, 5),
111 placeholder: sample(ramps.neutral, 4),
112 active: sample(ramps.neutral, 7),
113 feature: sample(ramps.blue, 0.5),
114 ok: sample(ramps.green, 0.5),
115 error: sample(ramps.red, 0.5),
116 warning: sample(ramps.yellow, 0.5),
117 info: sample(ramps.blue, 0.5),
118 onMedia: darkest,
119 };
120
121 const player = {
122 1: buildPlayer(sample(ramps.blue, 0.5)),
123 2: buildPlayer(sample(ramps.green, 0.5)),
124 3: buildPlayer(sample(ramps.magenta, 0.5)),
125 4: buildPlayer(sample(ramps.orange, 0.5)),
126 5: buildPlayer(sample(ramps.violet, 0.5)),
127 6: buildPlayer(sample(ramps.cyan, 0.5)),
128 7: buildPlayer(sample(ramps.red, 0.5)),
129 8: buildPlayer(sample(ramps.yellow, 0.5)),
130 };
131
132 const editor = {
133 background: backgroundColor[500].base,
134 indent_guide: borderColor.muted,
135 indent_guide_active: borderColor.secondary,
136 line: {
137 active: sample(ramps.neutral, 1),
138 highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
139 },
140 highlight: {
141 selection: player[1].selectionColor,
142 occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
143 activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
144 matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
145 match: sample(ramps.violet, 0.15),
146 activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
147 related: backgroundColor[500].hovered,
148 },
149 gutter: {
150 primary: textColor.placeholder,
151 active: textColor.active,
152 },
153 };
154
155 const syntax: Syntax = {
156 primary: {
157 color: sample(ramps.neutral, 7),
158 weight: fontWeights.normal,
159 },
160 comment: {
161 color: sample(ramps.neutral, 5),
162 weight: fontWeights.normal,
163 },
164 punctuation: {
165 color: sample(ramps.neutral, 6),
166 weight: fontWeights.normal,
167 },
168 constant: {
169 color: sample(ramps.neutral, 4),
170 weight: fontWeights.normal,
171 },
172 keyword: {
173 color: sample(ramps.blue, 0.5),
174 weight: fontWeights.normal,
175 },
176 function: {
177 color: sample(ramps.yellow, 0.5),
178 weight: fontWeights.normal,
179 },
180 type: {
181 color: sample(ramps.cyan, 0.5),
182 weight: fontWeights.normal,
183 },
184 constructor: {
185 color: sample(ramps.blue, 0.5),
186 weight: fontWeights.normal,
187 },
188 variant: {
189 color: sample(ramps.blue, 0.5),
190 weight: fontWeights.normal,
191 },
192 property: {
193 color: sample(ramps.blue, 0.5),
194 weight: fontWeights.normal,
195 },
196 enum: {
197 color: sample(ramps.orange, 0.5),
198 weight: fontWeights.normal,
199 },
200 operator: {
201 color: sample(ramps.orange, 0.5),
202 weight: fontWeights.normal,
203 },
204 string: {
205 color: sample(ramps.orange, 0.5),
206 weight: fontWeights.normal,
207 },
208 number: {
209 color: sample(ramps.green, 0.5),
210 weight: fontWeights.normal,
211 },
212 boolean: {
213 color: sample(ramps.green, 0.5),
214 weight: fontWeights.normal,
215 },
216 predictive: {
217 color: textColor.muted,
218 weight: fontWeights.normal,
219 },
220 title: {
221 color: sample(ramps.yellow, 0.5),
222 weight: fontWeights.bold,
223 },
224 emphasis: {
225 color: textColor.feature,
226 weight: fontWeights.normal,
227 },
228 "emphasis.strong": {
229 color: textColor.feature,
230 weight: fontWeights.bold,
231 },
232 linkUri: {
233 color: sample(ramps.green, 0.5),
234 weight: fontWeights.normal,
235 underline: true,
236 },
237 linkText: {
238 color: sample(ramps.orange, 0.5),
239 weight: fontWeights.normal,
240 italic: true,
241 },
242 };
243
244 const shadow = withOpacity(
245 ramps.neutral(isLight ? 7 : 0).darken().hex(),
246 blend);
247
248 return {
249 name,
250 isLight,
251 backgroundColor,
252 borderColor,
253 textColor,
254 iconColor: textColor,
255 editor,
256 syntax,
257 player,
258 shadow,
259 ramps,
260 };
261}