Project

General

Profile

Feature #33056 » 0002-Add-a-dark-color-scheme-for-the-default-theme.patch

Florian Walchshofer, 2026-10-18 21:17

View differences:

app/assets/stylesheets/dark.css
1
/**
2
 * Redmine - project management software
3
 * Copyright (C) 2006-  Jean-Philippe Lang
4
 * This code is released under the GNU General Public License.
5
 */
6

  
7
/*
8
 * Dark color scheme for the default theme.
9
 *
10
 * This stylesheet is loaded after application.css with
11
 * media="screen and (prefers-color-scheme: dark)". It redefines the
12
 * semantic color tokens and the Open Color palette, so most rules in
13
 * application.css adapt without changes:
14
 *
15
 * - gray-0..9 are inverted (based on the GitHub dark palette)
16
 * - hue-0..3 become dark, tinted background colors
17
 * - hue-4..6 are unchanged mid tones
18
 * - hue-7..9 become light colors for text on dark backgrounds
19
 */
20

  
21
:root {
22
  color-scheme: dark;
23

  
24
  --color-current-marker: #4c8ed9;
25
  --color-top-menu-background: #12273a;
26
  --color-header-background: #1c3d57;
27
  --color-text-muted: #8b949e;
28
  --color-top-menu-text: #c9d1d9;
29
  --color-top-menu-link: #e6edf3;
30
  --color-header-text: #ffffff;
31
  --color-header-link: #f0f6fc;
32
  --color-background: #0d1117;
33
  --color-background-overlay: #161b22;
34
  --color-selected-background: #2c6aa0;
35
  --color-selected-text: #ffffff;
36
  --color-shadow-rgb: 1, 4, 9;
37

  
38
  --oc-gray-0: #161b22;
39
  --oc-gray-0-rgb: 22, 27, 34;
40
  --oc-gray-1: #1c2128;
41
  --oc-gray-1-rgb: 28, 33, 40;
42
  --oc-gray-2: #21262d;
43
  --oc-gray-2-rgb: 33, 38, 45;
44
  --oc-gray-3: #2d333b;
45
  --oc-gray-3-rgb: 45, 51, 59;
46
  --oc-gray-4: #30363d;
47
  --oc-gray-4-rgb: 48, 54, 61;
48
  --oc-gray-5: #6e7681;
49
  --oc-gray-5-rgb: 110, 118, 129;
50
  --oc-gray-6: #8b949e;
51
  --oc-gray-6-rgb: 139, 148, 158;
52
  --oc-gray-7: #adbac7;
53
  --oc-gray-7-rgb: 173, 186, 199;
54
  --oc-gray-8: #cdd9e5;
55
  --oc-gray-8-rgb: 205, 217, 229;
56
  --oc-gray-9: #e6edf3;
57
  --oc-gray-9-rgb: 230, 237, 243;
58

  
59
  --oc-red-0: #20161c;
60
  --oc-red-0-rgb: 32, 22, 28;
61
  --oc-red-1: #2e1a1f;
62
  --oc-red-1-rgb: 46, 26, 31;
63
  --oc-red-2: #411f24;
64
  --oc-red-2-rgb: 65, 31, 36;
65
  --oc-red-3: #5e272b;
66
  --oc-red-3-rgb: 94, 39, 43;
67
  --oc-red-4: #ff8787;
68
  --oc-red-4-rgb: 255, 135, 135;
69
  --oc-red-5: #ff6b6b;
70
  --oc-red-5-rgb: 255, 107, 107;
71
  --oc-red-6: #fa5252;
72
  --oc-red-6-rgb: 250, 82, 82;
73
  --oc-red-7: #ff6b6b;
74
  --oc-red-7-rgb: 255, 107, 107;
75
  --oc-red-8: #ff8787;
76
  --oc-red-8-rgb: 255, 135, 135;
77
  --oc-red-9: #ffa8a8;
78
  --oc-red-9-rgb: 255, 168, 168;
79

  
80
  --oc-pink-0: #1e151f;
81
  --oc-pink-0-rgb: 30, 21, 31;
82
  --oc-pink-1: #2b1926;
83
  --oc-pink-1-rgb: 43, 25, 38;
84
  --oc-pink-2: #3d1d2e;
85
  --oc-pink-2-rgb: 61, 29, 46;
86
  --oc-pink-3: #57243b;
87
  --oc-pink-3-rgb: 87, 36, 59;
88
  --oc-pink-4: #f783ac;
89
  --oc-pink-4-rgb: 247, 131, 172;
90
  --oc-pink-5: #f06595;
91
  --oc-pink-5-rgb: 240, 101, 149;
92
  --oc-pink-6: #e64980;
93
  --oc-pink-6-rgb: 230, 73, 128;
94
  --oc-pink-7: #f06595;
95
  --oc-pink-7-rgb: 240, 101, 149;
96
  --oc-pink-8: #f783ac;
97
  --oc-pink-8-rgb: 247, 131, 172;
98
  --oc-pink-9: #faa2c1;
99
  --oc-pink-9-rgb: 250, 162, 193;
100

  
101
  --oc-grape-0: #1b1627;
102
  --oc-grape-0-rgb: 27, 22, 39;
103
  --oc-grape-1: #261932;
104
  --oc-grape-1-rgb: 38, 25, 50;
105
  --oc-grape-2: #341e42;
106
  --oc-grape-2-rgb: 52, 30, 66;
107
  --oc-grape-3: #49255a;
108
  --oc-grape-3-rgb: 73, 37, 90;
109
  --oc-grape-4: #da77f2;
110
  --oc-grape-4-rgb: 218, 119, 242;
111
  --oc-grape-5: #cc5de8;
112
  --oc-grape-5-rgb: 204, 93, 232;
113
  --oc-grape-6: #be4bdb;
114
  --oc-grape-6-rgb: 190, 75, 219;
115
  --oc-grape-7: #cc5de8;
116
  --oc-grape-7-rgb: 204, 93, 232;
117
  --oc-grape-8: #da77f2;
118
  --oc-grape-8-rgb: 218, 119, 242;
119
  --oc-grape-9: #e599f7;
120
  --oc-grape-9-rgb: 229, 153, 247;
121

  
122
  --oc-violet-0: #161629;
123
  --oc-violet-0-rgb: 22, 22, 41;
124
  --oc-violet-1: #1c1a36;
125
  --oc-violet-1-rgb: 28, 26, 54;
126
  --oc-violet-2: #251f47;
127
  --oc-violet-2-rgb: 37, 31, 71;
128
  --oc-violet-3: #322661;
129
  --oc-violet-3-rgb: 50, 38, 97;
130
  --oc-violet-4: #9775fa;
131
  --oc-violet-4-rgb: 151, 117, 250;
132
  --oc-violet-5: #845ef7;
133
  --oc-violet-5-rgb: 132, 94, 247;
134
  --oc-violet-6: #7950f2;
135
  --oc-violet-6-rgb: 121, 80, 242;
136
  --oc-violet-7: #845ef7;
137
  --oc-violet-7-rgb: 132, 94, 247;
138
  --oc-violet-8: #9775fa;
139
  --oc-violet-8-rgb: 151, 117, 250;
140
  --oc-violet-9: #b197fc;
141
  --oc-violet-9-rgb: 177, 151, 252;
142

  
143
  --oc-indigo-0: #121829;
144
  --oc-indigo-0-rgb: 18, 24, 41;
145
  --oc-indigo-1: #161e36;
146
  --oc-indigo-1-rgb: 22, 30, 54;
147
  --oc-indigo-2: #1b2548;
148
  --oc-indigo-2-rgb: 27, 37, 72;
149
  --oc-indigo-3: #223162;
150
  --oc-indigo-3-rgb: 34, 49, 98;
151
  --oc-indigo-4: #748ffc;
152
  --oc-indigo-4-rgb: 116, 143, 252;
153
  --oc-indigo-5: #5c7cfa;
154
  --oc-indigo-5-rgb: 92, 124, 250;
155
  --oc-indigo-6: #4c6ef5;
156
  --oc-indigo-6-rgb: 76, 110, 245;
157
  --oc-indigo-7: #5c7cfa;
158
  --oc-indigo-7-rgb: 92, 124, 250;
159
  --oc-indigo-8: #748ffc;
160
  --oc-indigo-8-rgb: 116, 143, 252;
161
  --oc-indigo-9: #91a7ff;
162
  --oc-indigo-9-rgb: 145, 167, 255;
163

  
164
  --oc-blue-0: #0f1b28;
165
  --oc-blue-0-rgb: 15, 27, 40;
166
  --oc-blue-1: #102234;
167
  --oc-blue-1-rgb: 16, 34, 52;
168
  --oc-blue-2: #122c45;
169
  --oc-blue-2-rgb: 18, 44, 69;
170
  --oc-blue-3: #143a5d;
171
  --oc-blue-3-rgb: 20, 58, 93;
172
  --oc-blue-4: #4dabf7;
173
  --oc-blue-4-rgb: 77, 171, 247;
174
  --oc-blue-5: #339af0;
175
  --oc-blue-5-rgb: 51, 154, 240;
176
  --oc-blue-6: #228be6;
177
  --oc-blue-6-rgb: 34, 139, 230;
178
  --oc-blue-7: #339af0;
179
  --oc-blue-7-rgb: 51, 154, 240;
180
  --oc-blue-8: #4dabf7;
181
  --oc-blue-8-rgb: 77, 171, 247;
182
  --oc-blue-9: #74c0fc;
183
  --oc-blue-9-rgb: 116, 192, 252;
184

  
185
  --oc-cyan-0: #0e1d24;
186
  --oc-cyan-0-rgb: 14, 29, 36;
187
  --oc-cyan-1: #0e262f;
188
  --oc-cyan-1-rgb: 14, 38, 47;
189
  --oc-cyan-2: #0f333c;
190
  --oc-cyan-2-rgb: 15, 51, 60;
191
  --oc-cyan-3: #104550;
192
  --oc-cyan-3-rgb: 16, 69, 80;
193
  --oc-cyan-4: #3bc9db;
194
  --oc-cyan-4-rgb: 59, 201, 219;
195
  --oc-cyan-5: #22b8cf;
196
  --oc-cyan-5-rgb: 34, 184, 207;
197
  --oc-cyan-6: #15aabf;
198
  --oc-cyan-6-rgb: 21, 170, 191;
199
  --oc-cyan-7: #22b8cf;
200
  --oc-cyan-7-rgb: 34, 184, 207;
201
  --oc-cyan-8: #3bc9db;
202
  --oc-cyan-8-rgb: 59, 201, 219;
203
  --oc-cyan-9: #66d9e8;
204
  --oc-cyan-9-rgb: 102, 217, 232;
205

  
206
  --oc-teal-0: #0d1e20;
207
  --oc-teal-0-rgb: 13, 30, 32;
208
  --oc-teal-1: #0e2827;
209
  --oc-teal-1-rgb: 14, 40, 39;
210
  --oc-teal-2: #0e362f;
211
  --oc-teal-2-rgb: 14, 54, 47;
212
  --oc-teal-3: #0f4a3d;
213
  --oc-teal-3-rgb: 15, 74, 61;
214
  --oc-teal-4: #38d9a9;
215
  --oc-teal-4-rgb: 56, 217, 169;
216
  --oc-teal-5: #20c997;
217
  --oc-teal-5-rgb: 32, 201, 151;
218
  --oc-teal-6: #12b886;
219
  --oc-teal-6-rgb: 18, 184, 134;
220
  --oc-teal-7: #20c997;
221
  --oc-teal-7-rgb: 32, 201, 151;
222
  --oc-teal-8: #38d9a9;
223
  --oc-teal-8-rgb: 56, 217, 169;
224
  --oc-teal-9: #63e6be;
225
  --oc-teal-9-rgb: 99, 230, 190;
226

  
227
  --oc-green-0: #111f1c;
228
  --oc-green-0-rgb: 17, 31, 28;
229
  --oc-green-1: #142a20;
230
  --oc-green-1-rgb: 20, 42, 32;
231
  --oc-green-2: #183825;
232
  --oc-green-2-rgb: 24, 56, 37;
233
  --oc-green-3: #1e4c2d;
234
  --oc-green-3-rgb: 30, 76, 45;
235
  --oc-green-4: #69db7c;
236
  --oc-green-4-rgb: 105, 219, 124;
237
  --oc-green-5: #51cf66;
238
  --oc-green-5-rgb: 81, 207, 102;
239
  --oc-green-6: #40c057;
240
  --oc-green-6-rgb: 64, 192, 87;
241
  --oc-green-7: #51cf66;
242
  --oc-green-7-rgb: 81, 207, 102;
243
  --oc-green-8: #69db7c;
244
  --oc-green-8-rgb: 105, 219, 124;
245
  --oc-green-9: #8ce99a;
246
  --oc-green-9-rgb: 140, 233, 154;
247

  
248
  --oc-lime-0: #162018;
249
  --oc-lime-0-rgb: 22, 32, 24;
250
  --oc-lime-1: #1d2b18;
251
  --oc-lime-1-rgb: 29, 43, 24;
252
  --oc-lime-2: #273919;
253
  --oc-lime-2-rgb: 39, 57, 25;
254
  --oc-lime-3: #355019;
255
  --oc-lime-3-rgb: 53, 80, 25;
256
  --oc-lime-4: #a9e34b;
257
  --oc-lime-4-rgb: 169, 227, 75;
258
  --oc-lime-5: #94d82d;
259
  --oc-lime-5-rgb: 148, 216, 45;
260
  --oc-lime-6: #82c91e;
261
  --oc-lime-6-rgb: 130, 201, 30;
262
  --oc-lime-7: #94d82d;
263
  --oc-lime-7-rgb: 148, 216, 45;
264
  --oc-lime-8: #a9e34b;
265
  --oc-lime-8-rgb: 169, 227, 75;
266
  --oc-lime-9: #c0eb75;
267
  --oc-lime-9-rgb: 192, 235, 117;
268

  
269
  --oc-yellow-0: #201e16;
270
  --oc-yellow-0-rgb: 32, 30, 22;
271
  --oc-yellow-1: #2e2714;
272
  --oc-yellow-1-rgb: 46, 39, 20;
273
  --oc-yellow-2: #413413;
274
  --oc-yellow-2-rgb: 65, 52, 19;
275
  --oc-yellow-3: #5e4711;
276
  --oc-yellow-3-rgb: 94, 71, 17;
277
  --oc-yellow-4: #ffd43b;
278
  --oc-yellow-4-rgb: 255, 212, 59;
279
  --oc-yellow-5: #fcc419;
280
  --oc-yellow-5-rgb: 252, 196, 25;
281
  --oc-yellow-6: #fab005;
282
  --oc-yellow-6-rgb: 250, 176, 5;
283
  --oc-yellow-7: #fcc419;
284
  --oc-yellow-7-rgb: 252, 196, 25;
285
  --oc-yellow-8: #ffd43b;
286
  --oc-yellow-8-rgb: 255, 212, 59;
287
  --oc-yellow-9: #ffe066;
288
  --oc-yellow-9-rgb: 255, 224, 102;
289

  
290
  --oc-orange-0: #201a17;
291
  --oc-orange-0-rgb: 32, 26, 23;
292
  --oc-orange-1: #2f2017;
293
  --oc-orange-1-rgb: 47, 32, 23;
294
  --oc-orange-2: #422916;
295
  --oc-orange-2-rgb: 66, 41, 22;
296
  --oc-orange-3: #5f3616;
297
  --oc-orange-3-rgb: 95, 54, 22;
298
  --oc-orange-4: #ffa94d;
299
  --oc-orange-4-rgb: 255, 169, 77;
300
  --oc-orange-5: #ff922b;
301
  --oc-orange-5-rgb: 255, 146, 43;
302
  --oc-orange-6: #fd7e14;
303
  --oc-orange-6-rgb: 253, 126, 20;
304
  --oc-orange-7: #ff922b;
305
  --oc-orange-7-rgb: 255, 146, 43;
306
  --oc-orange-8: #ffa94d;
307
  --oc-orange-8-rgb: 255, 169, 77;
308
  --oc-orange-9: #ffc078;
309
  --oc-orange-9-rgb: 255, 192, 120;
310

  
311
}
312

  
313
/* Colored chips keep the colors of the light scheme */
314
.avatar-color-0 { background-color: #f03e3e; }
315
.avatar-color-1 { background-color: #fd7e14; }
316
.avatar-color-2 { background-color: #ffd43b; }
317
.avatar-color-3 { background-color: #37b24d; }
318
.avatar-color-4 { background-color: #15aabf; }
319
.avatar-color-5 { background-color: #1c7ed6; }
320
.avatar-color-6 { background-color: #ae3ec9; }
321
.avatar-color-7 { background-color: #f06595; }
322
div.avatar-with-child > .avatar:nth-child(2) { border-color: var(--color-background); }
323
.badge-private { background: #e03131; border-color: #e03131; }
324
li.change.change-A svg.icon-svg { fill: #37b24d; }
325
li.change.change-M svg.icon-svg { fill: #f59f00; }
326
li.change.change-C svg.icon-svg { fill: #1098ad; }
327
li.change.change-R svg.icon-svg { fill: #ae3ec9; }
328
li.change.change-D svg.icon-svg { fill: #f03e3e; }
329

  
330
.tooltip:hover { color: var(--oc-gray-9); }
331

  
332
/* Neutral backgrounds instead of the tinted yellow and orange */
333
div.issue, #login-form, div.wiki ul.toc {
334
  background: var(--oc-gray-0);
335
  border: 1px solid var(--oc-gray-4);
336
}
337

  
338
div.wiki table, div.wiki td, div.wiki th { border-color: var(--oc-gray-4); }
339

  
340
/*
341
 * The wiki toolbar icons are SVG background images with a fixed dark stroke.
342
 * Render the buttons with light colors and invert them. jstoolbar.css is
343
 * loaded after this stylesheet, hence the more specific selectors.
344
 */
345
.jstBlock .jstElements button {
346
  filter: invert(1) hue-rotate(180deg);
347
  background-color: #f8f9fa;
348
  border-color: #ced4da;
349
  color: #212529;
350
}
351
.jstBlock .jstElements .jstb_pre::before { color: #212529; }
352
.jstBlock .jstElements button:hover {
353
  background-color: #dee2e6;
354
  border-color: #adb5bd;
355
}
356

  
357
/* Distinguish buttons from text fields */
358
input[type="submit"], button[type="submit"], input[type="button"] { background-color: var(--oc-gray-1); }
359

  
360
/* jQuery UI */
361
.ui-widget-content {
362
  background: var(--color-background-overlay);
363
  border-color: var(--oc-gray-4);
364
  color: var(--oc-gray-9);
365
}
366
.ui-widget.ui-widget-content { border-color: var(--oc-gray-4); }
367
.ui-widget-header {
368
  background: var(--oc-gray-2);
369
  border-color: var(--oc-gray-4);
370
  color: var(--oc-gray-9);
371
}
372
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default, .ui-button {
373
  background: var(--oc-gray-1);
374
  border-color: var(--oc-gray-4);
375
  color: var(--oc-gray-8);
376
}
377
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: var(--oc-gray-8); }
378
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover,
379
.ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus,
380
.ui-button:hover, .ui-button:focus {
381
  background: var(--oc-gray-3);
382
  border-color: var(--oc-gray-5);
383
  color: var(--oc-gray-9);
384
}
385
.ui-state-hover a, .ui-state-hover a:hover, .ui-state-focus a, .ui-state-focus a:hover { color: var(--oc-gray-9); }
386
.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {
387
  background: var(--oc-yellow-1);
388
  border-color: var(--oc-yellow-3);
389
  color: var(--oc-yellow-9);
390
}
391
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a { color: var(--oc-yellow-9); }
392
.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {
393
  background: var(--oc-red-1);
394
  border-color: var(--oc-red-3);
395
  color: var(--oc-red-9);
396
}
397
.ui-icon, .ui-widget-content .ui-icon, .ui-widget-header .ui-icon,
398
.ui-state-hover .ui-icon, .ui-state-focus .ui-icon, .ui-button .ui-icon { filter: invert(1); }
399
.ui-state-active .ui-icon { filter: none; }
app/javascript/controllers/mermaid_controller.js
85 85
    if (!pre) return;
86 86

  
87 87
    if (!mermaidInitialized) {
88
      mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true });
88
      const darkMode = getComputedStyle(document.documentElement).colorScheme === 'dark';
89
      mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true,
90
                           theme: darkMode ? 'dark' : 'default' });
89 91
      mermaidInitialized = true;
90 92
    }
91 93

  
app/views/layouts/base.html.erb
9 9
<%= csrf_meta_tag %>
10 10
<%= favicon %>
11 11
<%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'dropdown', 'responsive', :media => 'all' %>
12
<% unless current_theme %>
13
<meta name="color-scheme" content="light dark" />
14
<%= stylesheet_link_tag 'dark', :media => 'screen and (prefers-color-scheme: dark)' %>
15
<% end %>
12 16
<%= javascript_importmap_tags %>
13 17
<%= javascript_heads %>
14 18
<%= heads_for_theme %>
test/integration/lib/redmine/themes_test.rb
37 37
    assert_select "link[rel=stylesheet]:match('href', ?)", %r{/assets/themes/#{@theme.dir}/application-\w+\.css}
38 38
  end
39 39

  
40
  def test_dark_css_with_default_theme
41
    Setting.ui_theme = ''
42
    get '/'
43

  
44
    assert_response :success
45
    assert_select 'meta[name=color-scheme][content=?]', 'light dark'
46
    assert_select "link[rel=stylesheet][media=?]:match('href', ?)",
47
                  'screen and (prefers-color-scheme: dark)', %r{/assets/dark-\w+\.css}
48
  end
49

  
50
  def test_no_dark_css_with_theme
51
    get '/'
52

  
53
    assert_response :success
54
    assert_select 'meta[name=color-scheme]', 0
55
    assert_select "link[rel=stylesheet]:match('href', ?)", %r{/dark-\w+\.css}, 0
56
  end
57

  
40 58
  def test_without_theme_js
41 59
    # simulate a state theme.js does not exists
42 60
    @theme.javascripts.clear
(11-11/11)