[ADD] runbot_merge: color theme switcher

Uses localStorage rather than the odoo backend cookie because
Odoo *requires* the cookie, it can't fallback on the system theme, and
we might want a different color theme on the frontend and backend
since the frontend dark theme is pretty half-assed.

Also load the file by hand, as adding it to `assets_frontend` causes a
flash of system color theme. It would probably be possible to create
an asset which is not deferred or lazy (but wouldn't be async either
which is a shame), but I can't be arsed.

Part of #1088
This commit is contained in:
Xavier Morel 2025-03-12 15:10:26 +01:00
parent 9138aae3d8
commit 6f65836a22
3 changed files with 70 additions and 3 deletions

View File

@ -1,6 +1,6 @@
// resets a bunch of CSS rules to either `inherit` color or use CSS variables, // resets a bunch of CSS rules to either `inherit` color or use CSS variables,
// such that they properly follow CSS variables // such that they properly follow CSS variables
:root { :root, :root.light {
color-scheme: light dark; color-scheme: light dark;
// dunno why it's not global // dunno why it's not global
@ -17,11 +17,11 @@
// adjusting the conversion just set the value we want // adjusting the conversion just set the value we want
--danger-bg-rgb: 242, 222, 222; --danger-bg-rgb: 242, 222, 222;
} }
@function invlight($value) { @function invlight($value) {
@return hsl(hue($value), saturation($value), 100% - lightness($value)); @return hsl(hue($value), saturation($value), 100% - lightness($value));
} }
@media (prefers-color-scheme: dark) { @mixin darkrules {
:root {
$ibc: invlight($body-color); $ibc: invlight($body-color);
--body-color: #{$ibc}; --body-color: #{$ibc};
--body-color-rgb: #{to-rgb($ibc)}; --body-color-rgb: #{to-rgb($ibc)};
@ -41,8 +41,17 @@
))}; ))};
} }
--danger-bg-rgb: 41, 10, 10; --danger-bg-rgb: 41, 10, 10;
}
:root.dark {
@include darkrules;
}
@media (prefers-color-scheme: dark) {
:root:not(.light) {
@include darkrules;
} }
} }
body { body {
font-family: inherit; font-family: inherit;
} }

View File

@ -0,0 +1,42 @@
function setColorScheme(t) {
const classes = document.documentElement.classList;
classes.remove('light', 'dark');
const buttons = document.querySelectorAll('.theme-toggle button');
for(const button of buttons) {
button.classList.toggle(
'active',
(t === 'light' && button.classList.contains('fa-sun-o'))
|| (t === 'dark' && button.classList.contains('fa-moon-o'))
|| (t !== 'light' && t !== 'dark' && button.classList.contains('fa-ban'))
);
}
switch (t) {
case 'light': case 'dark':
classes.add(t);
window.localStorage.setItem('color-scheme', t);
break;
default:
window.localStorage.removeItem('color-scheme');
}
}
window.addEventListener("click", (e) => {
const target = e.target;
if (target.matches(".btn-group.theme-toggle button")) {
setColorScheme(
target.classList.contains('fa-sun-o') ? 'light' :
target.classList.contains('fa-moon-o') ? 'dark' :
null
);
}
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", (e) => {
setColorScheme(window.localStorage.getItem('color-scheme'));
});
} else {
setColorScheme(window.localStorage.getItem('color-scheme'));
}

View File

@ -640,4 +640,20 @@ action = batches
</tbody> </tbody>
</table> </table>
</template> </template>
<template id="theme-script" inherit_id="website.layout">
<xpath expr="//head" position="inside">
<script type="text/javascript"
src="/runbot_merge/static/src/js/runbot_merge.js"
async="async"/>
</xpath>
</template>
<template id="theme-toggle" inherit_id="website.placeholder_header_call_to_action">
<xpath expr="." position="inside">
<div class="btn-group btn-group-toggle theme-toggle">
<button class="btn btn-outline-secondary fa fa-sun-o"/>
<button class="btn btn-outline-secondary fa fa-moon-o"/>
<button class="btn btn-outline-secondary fa fa-ban active"/>
</div>
</xpath>
</template>
</odoo> </odoo>