diff --git a/app/assets/stylesheets/dark.css b/app/assets/stylesheets/dark.css new file mode 100644 index 000000000..4ba768b9b --- /dev/null +++ b/app/assets/stylesheets/dark.css @@ -0,0 +1,399 @@ +/** + * Redmine - project management software + * Copyright (C) 2006- Jean-Philippe Lang + * This code is released under the GNU General Public License. + */ + +/* + * Dark color scheme for the default theme. + * + * This stylesheet is loaded after application.css with + * media="screen and (prefers-color-scheme: dark)". It redefines the + * semantic color tokens and the Open Color palette, so most rules in + * application.css adapt without changes: + * + * - gray-0..9 are inverted (based on the GitHub dark palette) + * - hue-0..3 become dark, tinted background colors + * - hue-4..6 are unchanged mid tones + * - hue-7..9 become light colors for text on dark backgrounds + */ + +:root { + color-scheme: dark; + + --color-current-marker: #4c8ed9; + --color-top-menu-background: #12273a; + --color-header-background: #1c3d57; + --color-text-muted: #8b949e; + --color-top-menu-text: #c9d1d9; + --color-top-menu-link: #e6edf3; + --color-header-text: #ffffff; + --color-header-link: #f0f6fc; + --color-background: #0d1117; + --color-background-overlay: #161b22; + --color-selected-background: #2c6aa0; + --color-selected-text: #ffffff; + --color-shadow-rgb: 1, 4, 9; + + --oc-gray-0: #161b22; + --oc-gray-0-rgb: 22, 27, 34; + --oc-gray-1: #1c2128; + --oc-gray-1-rgb: 28, 33, 40; + --oc-gray-2: #21262d; + --oc-gray-2-rgb: 33, 38, 45; + --oc-gray-3: #2d333b; + --oc-gray-3-rgb: 45, 51, 59; + --oc-gray-4: #30363d; + --oc-gray-4-rgb: 48, 54, 61; + --oc-gray-5: #6e7681; + --oc-gray-5-rgb: 110, 118, 129; + --oc-gray-6: #8b949e; + --oc-gray-6-rgb: 139, 148, 158; + --oc-gray-7: #adbac7; + --oc-gray-7-rgb: 173, 186, 199; + --oc-gray-8: #cdd9e5; + --oc-gray-8-rgb: 205, 217, 229; + --oc-gray-9: #e6edf3; + --oc-gray-9-rgb: 230, 237, 243; + + --oc-red-0: #20161c; + --oc-red-0-rgb: 32, 22, 28; + --oc-red-1: #2e1a1f; + --oc-red-1-rgb: 46, 26, 31; + --oc-red-2: #411f24; + --oc-red-2-rgb: 65, 31, 36; + --oc-red-3: #5e272b; + --oc-red-3-rgb: 94, 39, 43; + --oc-red-4: #ff8787; + --oc-red-4-rgb: 255, 135, 135; + --oc-red-5: #ff6b6b; + --oc-red-5-rgb: 255, 107, 107; + --oc-red-6: #fa5252; + --oc-red-6-rgb: 250, 82, 82; + --oc-red-7: #ff6b6b; + --oc-red-7-rgb: 255, 107, 107; + --oc-red-8: #ff8787; + --oc-red-8-rgb: 255, 135, 135; + --oc-red-9: #ffa8a8; + --oc-red-9-rgb: 255, 168, 168; + + --oc-pink-0: #1e151f; + --oc-pink-0-rgb: 30, 21, 31; + --oc-pink-1: #2b1926; + --oc-pink-1-rgb: 43, 25, 38; + --oc-pink-2: #3d1d2e; + --oc-pink-2-rgb: 61, 29, 46; + --oc-pink-3: #57243b; + --oc-pink-3-rgb: 87, 36, 59; + --oc-pink-4: #f783ac; + --oc-pink-4-rgb: 247, 131, 172; + --oc-pink-5: #f06595; + --oc-pink-5-rgb: 240, 101, 149; + --oc-pink-6: #e64980; + --oc-pink-6-rgb: 230, 73, 128; + --oc-pink-7: #f06595; + --oc-pink-7-rgb: 240, 101, 149; + --oc-pink-8: #f783ac; + --oc-pink-8-rgb: 247, 131, 172; + --oc-pink-9: #faa2c1; + --oc-pink-9-rgb: 250, 162, 193; + + --oc-grape-0: #1b1627; + --oc-grape-0-rgb: 27, 22, 39; + --oc-grape-1: #261932; + --oc-grape-1-rgb: 38, 25, 50; + --oc-grape-2: #341e42; + --oc-grape-2-rgb: 52, 30, 66; + --oc-grape-3: #49255a; + --oc-grape-3-rgb: 73, 37, 90; + --oc-grape-4: #da77f2; + --oc-grape-4-rgb: 218, 119, 242; + --oc-grape-5: #cc5de8; + --oc-grape-5-rgb: 204, 93, 232; + --oc-grape-6: #be4bdb; + --oc-grape-6-rgb: 190, 75, 219; + --oc-grape-7: #cc5de8; + --oc-grape-7-rgb: 204, 93, 232; + --oc-grape-8: #da77f2; + --oc-grape-8-rgb: 218, 119, 242; + --oc-grape-9: #e599f7; + --oc-grape-9-rgb: 229, 153, 247; + + --oc-violet-0: #161629; + --oc-violet-0-rgb: 22, 22, 41; + --oc-violet-1: #1c1a36; + --oc-violet-1-rgb: 28, 26, 54; + --oc-violet-2: #251f47; + --oc-violet-2-rgb: 37, 31, 71; + --oc-violet-3: #322661; + --oc-violet-3-rgb: 50, 38, 97; + --oc-violet-4: #9775fa; + --oc-violet-4-rgb: 151, 117, 250; + --oc-violet-5: #845ef7; + --oc-violet-5-rgb: 132, 94, 247; + --oc-violet-6: #7950f2; + --oc-violet-6-rgb: 121, 80, 242; + --oc-violet-7: #845ef7; + --oc-violet-7-rgb: 132, 94, 247; + --oc-violet-8: #9775fa; + --oc-violet-8-rgb: 151, 117, 250; + --oc-violet-9: #b197fc; + --oc-violet-9-rgb: 177, 151, 252; + + --oc-indigo-0: #121829; + --oc-indigo-0-rgb: 18, 24, 41; + --oc-indigo-1: #161e36; + --oc-indigo-1-rgb: 22, 30, 54; + --oc-indigo-2: #1b2548; + --oc-indigo-2-rgb: 27, 37, 72; + --oc-indigo-3: #223162; + --oc-indigo-3-rgb: 34, 49, 98; + --oc-indigo-4: #748ffc; + --oc-indigo-4-rgb: 116, 143, 252; + --oc-indigo-5: #5c7cfa; + --oc-indigo-5-rgb: 92, 124, 250; + --oc-indigo-6: #4c6ef5; + --oc-indigo-6-rgb: 76, 110, 245; + --oc-indigo-7: #5c7cfa; + --oc-indigo-7-rgb: 92, 124, 250; + --oc-indigo-8: #748ffc; + --oc-indigo-8-rgb: 116, 143, 252; + --oc-indigo-9: #91a7ff; + --oc-indigo-9-rgb: 145, 167, 255; + + --oc-blue-0: #0f1b28; + --oc-blue-0-rgb: 15, 27, 40; + --oc-blue-1: #102234; + --oc-blue-1-rgb: 16, 34, 52; + --oc-blue-2: #122c45; + --oc-blue-2-rgb: 18, 44, 69; + --oc-blue-3: #143a5d; + --oc-blue-3-rgb: 20, 58, 93; + --oc-blue-4: #4dabf7; + --oc-blue-4-rgb: 77, 171, 247; + --oc-blue-5: #339af0; + --oc-blue-5-rgb: 51, 154, 240; + --oc-blue-6: #228be6; + --oc-blue-6-rgb: 34, 139, 230; + --oc-blue-7: #339af0; + --oc-blue-7-rgb: 51, 154, 240; + --oc-blue-8: #4dabf7; + --oc-blue-8-rgb: 77, 171, 247; + --oc-blue-9: #74c0fc; + --oc-blue-9-rgb: 116, 192, 252; + + --oc-cyan-0: #0e1d24; + --oc-cyan-0-rgb: 14, 29, 36; + --oc-cyan-1: #0e262f; + --oc-cyan-1-rgb: 14, 38, 47; + --oc-cyan-2: #0f333c; + --oc-cyan-2-rgb: 15, 51, 60; + --oc-cyan-3: #104550; + --oc-cyan-3-rgb: 16, 69, 80; + --oc-cyan-4: #3bc9db; + --oc-cyan-4-rgb: 59, 201, 219; + --oc-cyan-5: #22b8cf; + --oc-cyan-5-rgb: 34, 184, 207; + --oc-cyan-6: #15aabf; + --oc-cyan-6-rgb: 21, 170, 191; + --oc-cyan-7: #22b8cf; + --oc-cyan-7-rgb: 34, 184, 207; + --oc-cyan-8: #3bc9db; + --oc-cyan-8-rgb: 59, 201, 219; + --oc-cyan-9: #66d9e8; + --oc-cyan-9-rgb: 102, 217, 232; + + --oc-teal-0: #0d1e20; + --oc-teal-0-rgb: 13, 30, 32; + --oc-teal-1: #0e2827; + --oc-teal-1-rgb: 14, 40, 39; + --oc-teal-2: #0e362f; + --oc-teal-2-rgb: 14, 54, 47; + --oc-teal-3: #0f4a3d; + --oc-teal-3-rgb: 15, 74, 61; + --oc-teal-4: #38d9a9; + --oc-teal-4-rgb: 56, 217, 169; + --oc-teal-5: #20c997; + --oc-teal-5-rgb: 32, 201, 151; + --oc-teal-6: #12b886; + --oc-teal-6-rgb: 18, 184, 134; + --oc-teal-7: #20c997; + --oc-teal-7-rgb: 32, 201, 151; + --oc-teal-8: #38d9a9; + --oc-teal-8-rgb: 56, 217, 169; + --oc-teal-9: #63e6be; + --oc-teal-9-rgb: 99, 230, 190; + + --oc-green-0: #111f1c; + --oc-green-0-rgb: 17, 31, 28; + --oc-green-1: #142a20; + --oc-green-1-rgb: 20, 42, 32; + --oc-green-2: #183825; + --oc-green-2-rgb: 24, 56, 37; + --oc-green-3: #1e4c2d; + --oc-green-3-rgb: 30, 76, 45; + --oc-green-4: #69db7c; + --oc-green-4-rgb: 105, 219, 124; + --oc-green-5: #51cf66; + --oc-green-5-rgb: 81, 207, 102; + --oc-green-6: #40c057; + --oc-green-6-rgb: 64, 192, 87; + --oc-green-7: #51cf66; + --oc-green-7-rgb: 81, 207, 102; + --oc-green-8: #69db7c; + --oc-green-8-rgb: 105, 219, 124; + --oc-green-9: #8ce99a; + --oc-green-9-rgb: 140, 233, 154; + + --oc-lime-0: #162018; + --oc-lime-0-rgb: 22, 32, 24; + --oc-lime-1: #1d2b18; + --oc-lime-1-rgb: 29, 43, 24; + --oc-lime-2: #273919; + --oc-lime-2-rgb: 39, 57, 25; + --oc-lime-3: #355019; + --oc-lime-3-rgb: 53, 80, 25; + --oc-lime-4: #a9e34b; + --oc-lime-4-rgb: 169, 227, 75; + --oc-lime-5: #94d82d; + --oc-lime-5-rgb: 148, 216, 45; + --oc-lime-6: #82c91e; + --oc-lime-6-rgb: 130, 201, 30; + --oc-lime-7: #94d82d; + --oc-lime-7-rgb: 148, 216, 45; + --oc-lime-8: #a9e34b; + --oc-lime-8-rgb: 169, 227, 75; + --oc-lime-9: #c0eb75; + --oc-lime-9-rgb: 192, 235, 117; + + --oc-yellow-0: #201e16; + --oc-yellow-0-rgb: 32, 30, 22; + --oc-yellow-1: #2e2714; + --oc-yellow-1-rgb: 46, 39, 20; + --oc-yellow-2: #413413; + --oc-yellow-2-rgb: 65, 52, 19; + --oc-yellow-3: #5e4711; + --oc-yellow-3-rgb: 94, 71, 17; + --oc-yellow-4: #ffd43b; + --oc-yellow-4-rgb: 255, 212, 59; + --oc-yellow-5: #fcc419; + --oc-yellow-5-rgb: 252, 196, 25; + --oc-yellow-6: #fab005; + --oc-yellow-6-rgb: 250, 176, 5; + --oc-yellow-7: #fcc419; + --oc-yellow-7-rgb: 252, 196, 25; + --oc-yellow-8: #ffd43b; + --oc-yellow-8-rgb: 255, 212, 59; + --oc-yellow-9: #ffe066; + --oc-yellow-9-rgb: 255, 224, 102; + + --oc-orange-0: #201a17; + --oc-orange-0-rgb: 32, 26, 23; + --oc-orange-1: #2f2017; + --oc-orange-1-rgb: 47, 32, 23; + --oc-orange-2: #422916; + --oc-orange-2-rgb: 66, 41, 22; + --oc-orange-3: #5f3616; + --oc-orange-3-rgb: 95, 54, 22; + --oc-orange-4: #ffa94d; + --oc-orange-4-rgb: 255, 169, 77; + --oc-orange-5: #ff922b; + --oc-orange-5-rgb: 255, 146, 43; + --oc-orange-6: #fd7e14; + --oc-orange-6-rgb: 253, 126, 20; + --oc-orange-7: #ff922b; + --oc-orange-7-rgb: 255, 146, 43; + --oc-orange-8: #ffa94d; + --oc-orange-8-rgb: 255, 169, 77; + --oc-orange-9: #ffc078; + --oc-orange-9-rgb: 255, 192, 120; + +} + +/* Colored chips keep the colors of the light scheme */ +.avatar-color-0 { background-color: #f03e3e; } +.avatar-color-1 { background-color: #fd7e14; } +.avatar-color-2 { background-color: #ffd43b; } +.avatar-color-3 { background-color: #37b24d; } +.avatar-color-4 { background-color: #15aabf; } +.avatar-color-5 { background-color: #1c7ed6; } +.avatar-color-6 { background-color: #ae3ec9; } +.avatar-color-7 { background-color: #f06595; } +div.avatar-with-child > .avatar:nth-child(2) { border-color: var(--color-background); } +.badge-private { background: #e03131; border-color: #e03131; } +li.change.change-A svg.icon-svg { fill: #37b24d; } +li.change.change-M svg.icon-svg { fill: #f59f00; } +li.change.change-C svg.icon-svg { fill: #1098ad; } +li.change.change-R svg.icon-svg { fill: #ae3ec9; } +li.change.change-D svg.icon-svg { fill: #f03e3e; } + +.tooltip:hover { color: var(--oc-gray-9); } + +/* Neutral backgrounds instead of the tinted yellow and orange */ +div.issue, #login-form, div.wiki ul.toc { + background: var(--oc-gray-0); + border: 1px solid var(--oc-gray-4); +} + +div.wiki table, div.wiki td, div.wiki th { border-color: var(--oc-gray-4); } + +/* + * The wiki toolbar icons are SVG background images with a fixed dark stroke. + * Render the buttons with light colors and invert them. jstoolbar.css is + * loaded after this stylesheet, hence the more specific selectors. + */ +.jstBlock .jstElements button { + filter: invert(1) hue-rotate(180deg); + background-color: #f8f9fa; + border-color: #ced4da; + color: #212529; +} +.jstBlock .jstElements .jstb_pre::before { color: #212529; } +.jstBlock .jstElements button:hover { + background-color: #dee2e6; + border-color: #adb5bd; +} + +/* Distinguish buttons from text fields */ +input[type="submit"], button[type="submit"], input[type="button"] { background-color: var(--oc-gray-1); } + +/* jQuery UI */ +.ui-widget-content { + background: var(--color-background-overlay); + border-color: var(--oc-gray-4); + color: var(--oc-gray-9); +} +.ui-widget.ui-widget-content { border-color: var(--oc-gray-4); } +.ui-widget-header { + background: var(--oc-gray-2); + border-color: var(--oc-gray-4); + color: var(--oc-gray-9); +} +.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default, .ui-button { + background: var(--oc-gray-1); + border-color: var(--oc-gray-4); + color: var(--oc-gray-8); +} +.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: var(--oc-gray-8); } +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, +.ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus, +.ui-button:hover, .ui-button:focus { + background: var(--oc-gray-3); + border-color: var(--oc-gray-5); + color: var(--oc-gray-9); +} +.ui-state-hover a, .ui-state-hover a:hover, .ui-state-focus a, .ui-state-focus a:hover { color: var(--oc-gray-9); } +.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight { + background: var(--oc-yellow-1); + border-color: var(--oc-yellow-3); + color: var(--oc-yellow-9); +} +.ui-state-highlight a, .ui-widget-content .ui-state-highlight a { color: var(--oc-yellow-9); } +.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error { + background: var(--oc-red-1); + border-color: var(--oc-red-3); + color: var(--oc-red-9); +} +.ui-icon, .ui-widget-content .ui-icon, .ui-widget-header .ui-icon, +.ui-state-hover .ui-icon, .ui-state-focus .ui-icon, .ui-button .ui-icon { filter: invert(1); } +.ui-state-active .ui-icon { filter: none; } diff --git a/app/javascript/controllers/mermaid_controller.js b/app/javascript/controllers/mermaid_controller.js index 7d29c2cc7..88eaa22bd 100644 --- a/app/javascript/controllers/mermaid_controller.js +++ b/app/javascript/controllers/mermaid_controller.js @@ -85,7 +85,9 @@ export default class extends Controller { if (!pre) return; if (!mermaidInitialized) { - mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true }); + const darkMode = getComputedStyle(document.documentElement).colorScheme === 'dark'; + mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true, + theme: darkMode ? 'dark' : 'default' }); mermaidInitialized = true; } diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index e4ffb42ec..af0cd4e08 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -9,6 +9,10 @@ <%= csrf_meta_tag %> <%= favicon %> <%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'dropdown', 'responsive', :media => 'all' %> +<% unless current_theme %> + +<%= stylesheet_link_tag 'dark', :media => 'screen and (prefers-color-scheme: dark)' %> +<% end %> <%= javascript_importmap_tags %> <%= javascript_heads %> <%= heads_for_theme %> diff --git a/test/integration/lib/redmine/themes_test.rb b/test/integration/lib/redmine/themes_test.rb index 10cfac660..e59b62a4d 100644 --- a/test/integration/lib/redmine/themes_test.rb +++ b/test/integration/lib/redmine/themes_test.rb @@ -37,6 +37,24 @@ class ThemesTest < Redmine::IntegrationTest assert_select "link[rel=stylesheet]:match('href', ?)", %r{/assets/themes/#{@theme.dir}/application-\w+\.css} end + def test_dark_css_with_default_theme + Setting.ui_theme = '' + get '/' + + assert_response :success + assert_select 'meta[name=color-scheme][content=?]', 'light dark' + assert_select "link[rel=stylesheet][media=?]:match('href', ?)", + 'screen and (prefers-color-scheme: dark)', %r{/assets/dark-\w+\.css} + end + + def test_no_dark_css_with_theme + get '/' + + assert_response :success + assert_select 'meta[name=color-scheme]', 0 + assert_select "link[rel=stylesheet]:match('href', ?)", %r{/dark-\w+\.css}, 0 + end + def test_without_theme_js # simulate a state theme.js does not exists @theme.javascripts.clear -- 2.43.0