diff --git a/pontoon/base/migrations/0048_userprofile_theme.py b/pontoon/base/migrations/0048_userprofile_theme.py
new file mode 100644
index 0000000000..5ed5fd1196
--- /dev/null
+++ b/pontoon/base/migrations/0048_userprofile_theme.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.2.15 on 2023-10-18 21:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("base", "0047_fix_lt_plural_rule"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="userprofile",
+ name="theme",
+ field=models.CharField(
+ choices=[("dark", "Dark"), ("light", "Light"), ("system", "System")],
+ default="dark",
+ max_length=20,
+ ),
+ ),
+ ]
diff --git a/pontoon/base/models.py b/pontoon/base/models.py
index 08d7de4700..1944d3db17 100644
--- a/pontoon/base/models.py
+++ b/pontoon/base/models.py
@@ -1569,12 +1569,25 @@ class UserProfile(models.Model):
User, models.CASCADE, related_name="profile", primary_key=True
)
+ # Themes
+ class Themes(models.TextChoices):
+ DARK = "dark", "Dark"
+ LIGHT = "light", "Light"
+ SYSTEM = "system", "System"
+
# Personal information
username = models.SlugField(unique=True, blank=True, null=True)
contact_email = models.EmailField("Contact email address", blank=True, null=True)
contact_email_verified = models.BooleanField(default=False)
bio = models.TextField(max_length=160, blank=True, null=True)
+ # Theme
+ theme = models.CharField(
+ choices=Themes.choices,
+ max_length=20,
+ default=Themes.DARK,
+ )
+
# External accounts
chat = models.CharField("Chat username", max_length=255, blank=True, null=True)
github = models.CharField("GitHub username", max_length=255, blank=True, null=True)
diff --git a/pontoon/base/static/css/dark-theme.css b/pontoon/base/static/css/dark-theme.css
index 33cb2fc58f..74122028b6 100644
--- a/pontoon/base/static/css/dark-theme.css
+++ b/pontoon/base/static/css/dark-theme.css
@@ -1,5 +1,6 @@
/* Dark Theme Variables */
-:root {
+.dark-theme,
+.system-theme {
--black-1: #1c1e21;
--black-2: #000000;
--black-3: #272a2f;
diff --git a/pontoon/base/static/css/light-theme.css b/pontoon/base/static/css/light-theme.css
new file mode 100644
index 0000000000..13c6cb9ed5
--- /dev/null
+++ b/pontoon/base/static/css/light-theme.css
@@ -0,0 +1,84 @@
+/* Light Theme Variables Placeholder */
+.light-theme {
+ --black-1: #1c1e21;
+ --black-2: #000000;
+ --black-3: #272a2f;
+ --black-4: #000000cc;
+ --black-5: #000000bb;
+ --black-6: #000000dd;
+ --black-brown: #2d2323;
+ --blue-1: #3e7089;
+ --blue-2: #5f7285;
+ --brown-1: #676054;
+ --brown-2: #3b3b3b;
+ --coral: #fe8f8f;
+ --dark-brown: #232323;
+ --dark-green: #3b3d3b;
+ --dark-grey-1: #333941;
+ --dark-grey-2: #3f4752;
+ --dark-grey-3: #333333;
+ --dark-grey-4: #444444;
+ --forest-green-1: #41554c;
+ --forest-green-2: #4b6259;
+ --green: #4f7256;
+ --green-2: #64906d;
+ --grey-1: #637283;
+ --grey-2: #525a65;
+ --grey-3: #4d5967;
+ --grey-4: #5c6172;
+ --grey-5: #385465;
+ --grey-6: #777777;
+ --grey-7: #333941e6;
+ --grey-8: #ffffff33;
+ --light-blue: #4fc4f6;
+ --light-green-1: #7bc876;
+ --light-green-2: #7bc176;
+ --light-green-3: #9cd699;
+ --dark-green: #7bc87633;
+ --light-grey-1: #5e6475;
+ --light-grey-2: #888888;
+ --light-grey-3: #666666;
+ --light-grey-4: #bbbbbb;
+ --light-grey-5: #7c8b9c;
+ --light-grey-6: #cccccc;
+ --light-grey-7: #aaaaaa;
+ --light-grey-8: #c0c0c0;
+ --magenta: #843650;
+ --mustard-yellow: #fed271;
+ --neon-green: #c0ff00;
+ --orange-1: #ffa10f;
+ --orange-2: #ff7b00;
+ --pink: #ff3366cc;
+ --light-pink: #ffbed1;
+ --purple-pink: #674b54;
+ --hot-pink: #ff5f9e;
+ --red: #ff0a43;
+ --red-pink: #ff3366;
+ --dark-pink: #b3005e;
+ --lilac: #c6c1f0;
+ --taupe-1: #898989;
+ --taupe-2: #999999;
+ --white-1: #ffffff;
+ --white-2: #f4f4f4;
+ --white-3: #ebebeb;
+ --white-4: #dddddd;
+ --white-5: #e2e2e2;
+ /* Insights_tab.css Variables */
+ --green-blue: #4fc4f666;
+ --grey-9: #5f7285;
+ --dark-purple: #f148fb33;
+ --pink-3: #ffacfc;
+ --dark-purple-2: #ffacfc33;
+ --dark-magenta: #b3005e66;
+ /* Insights Pretranslation Quality Chart */
+ --brown-grey: #9c9290;
+ --brown-grey-2: #c5bfbe;
+ --lilac-purple: #9b93c9;
+ --pink-2: #c46487;
+ --light-pink-2: #ddb5d5;
+ --light-pink-3: #f498b6;
+ --light-pink-4: #c799bc;
+ --dark-pink: #b173a0;
+ --purple: #8074a8;
+ --green-brown: #7c7270;
+}
diff --git a/pontoon/base/static/css/toggle.css b/pontoon/base/static/css/toggle.css
index 1ce7262df9..6b2000901c 100644
--- a/pontoon/base/static/css/toggle.css
+++ b/pontoon/base/static/css/toggle.css
@@ -21,7 +21,7 @@
border-bottom-right-radius: 0;
}
-.toggle-button button:nth-child(2) {
+.toggle-button button:last-child {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
diff --git a/pontoon/base/static/js/theme-switcher.js b/pontoon/base/static/js/theme-switcher.js
new file mode 100644
index 0000000000..cb69d20d9c
--- /dev/null
+++ b/pontoon/base/static/js/theme-switcher.js
@@ -0,0 +1,76 @@
+$(function () {
+ function getSystemTheme() {
+ if (
+ window.matchMedia &&
+ window.matchMedia('(prefers-color-scheme: dark)').matches
+ ) {
+ return 'dark';
+ } else {
+ return 'light';
+ }
+ }
+
+ function applyTheme(newTheme) {
+ if (newTheme === 'system') {
+ newTheme = getSystemTheme();
+ }
+ $('body')
+ .removeClass('dark-theme light-theme system-theme')
+ .addClass(`${newTheme}-theme`);
+ }
+
+ window
+ .matchMedia('(prefers-color-scheme: dark)')
+ .addEventListener('change', function (e) {
+ // Check the 'data-theme' attribute on the body element
+ let userThemeSetting = $('body').data('theme');
+
+ if (userThemeSetting === 'system') {
+ applyTheme(e.matches ? 'dark' : 'light');
+ }
+ });
+
+ if ($('body').hasClass('system-theme')) {
+ let systemTheme = getSystemTheme();
+ $('body').removeClass('system-theme').addClass(`${systemTheme}-theme`);
+ }
+
+ $('.appearance .toggle-button button').click(function (e) {
+ e.preventDefault();
+
+ var self = $(this);
+
+ if (self.is('.active')) {
+ return;
+ }
+
+ var theme = self.val();
+
+ $.ajax({
+ url: '/api/v1/user/' + $('#server').data('username') + '/theme/',
+ type: 'POST',
+ data: {
+ csrfmiddlewaretoken: $('body').data('csrf'),
+ theme: theme,
+ },
+ success: function () {
+ $('.appearance .toggle-button button').removeClass('active');
+ self.addClass('active');
+ applyTheme(theme);
+
+ // Set the data-theme attribute after successfully changing the theme
+ $('body').data('theme', theme);
+
+ // Notify the user about the theme change after AJAX success
+ Pontoon.endLoader(`Theme changed to ${theme}.`);
+ },
+ error: function (request) {
+ if (request.responseText === 'error') {
+ Pontoon.endLoader('Oops, something went wrong.', 'error');
+ } else {
+ Pontoon.endLoader(request.responseText, 'error');
+ }
+ },
+ });
+ });
+});
diff --git a/pontoon/base/templates/base.html b/pontoon/base/templates/base.html
index d581e961fc..117fa90058 100644
--- a/pontoon/base/templates/base.html
+++ b/pontoon/base/templates/base.html
@@ -33,8 +33,9 @@
{% block content %}
diff --git a/pontoon/base/templatetags/helpers.py b/pontoon/base/templatetags/helpers.py
index 24e7b48a7d..4507d91e5c 100644
--- a/pontoon/base/templatetags/helpers.py
+++ b/pontoon/base/templatetags/helpers.py
@@ -36,6 +36,14 @@ def return_url(request):
return url
+@library.global_function
+def theme(user):
+ """Get user's theme or return 'dark' if user is not authenticated."""
+ if user.is_authenticated:
+ return user.profile.theme
+ return "dark"
+
+
@library.global_function
def static(path):
return staticfiles_storage.url(path)
diff --git a/pontoon/contributors/static/css/settings.css b/pontoon/contributors/static/css/settings.css
index 3c2f4786cc..000d96d65d 100644
--- a/pontoon/contributors/static/css/settings.css
+++ b/pontoon/contributors/static/css/settings.css
@@ -129,6 +129,26 @@
font-style: italic;
}
+#main .appearance .field .help {
+ margin: 0 0 -12px;
+}
+
+#main .appearance .toggle-button button {
+ width: 207px;
+}
+
+#main .appearance .toggle-button button:first-child {
+ margin-right: -1px;
+}
+
+#main .appearance .toggle-button button:nth-child(2) {
+ border-radius: 0;
+}
+
+#main .appearance .toggle-button button .icon {
+ margin-right: 10px;
+}
+
#main .field .verify {
color: var(--light-green-1);
font-style: italic;
diff --git a/pontoon/contributors/static/js/profile.js b/pontoon/contributors/static/js/profile.js
index be2ba1f52c..d9e5de6a7c 100644
--- a/pontoon/contributors/static/js/profile.js
+++ b/pontoon/contributors/static/js/profile.js
@@ -12,7 +12,7 @@ const shortDateFormat = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
});
-const style = getComputedStyle(document.documentElement);
+const style = getComputedStyle(document.body);
var Pontoon = (function (my) {
return $.extend(true, my, {
diff --git a/pontoon/contributors/static/js/settings.js b/pontoon/contributors/static/js/settings.js
index 8953d88205..d7eec829a5 100644
--- a/pontoon/contributors/static/js/settings.js
+++ b/pontoon/contributors/static/js/settings.js
@@ -1,6 +1,6 @@
$(function () {
// Toggle visibility
- $('.toggle-button button').click(function (e) {
+ $('.data-visibility .toggle-button button').click(function (e) {
e.preventDefault();
var self = $(this);
diff --git a/pontoon/contributors/templates/contributors/settings.html b/pontoon/contributors/templates/contributors/settings.html
index 273010ec75..17deaf9d4b 100644
--- a/pontoon/contributors/templates/contributors/settings.html
+++ b/pontoon/contributors/templates/contributors/settings.html
@@ -64,6 +64,18 @@ Personal information
+
+ Appearance
+
+
Choose if appearance should be dark, light, or follow your system’s settings
+
+
+
+
+
+
+
+
External accounts
@@ -83,7 +95,7 @@
External accounts
-
+
Visibility of data on the Profile page
{{ user_profile_visibility_form.visibility_email.label }}
@@ -128,7 +140,7 @@
Editor settings
- Locale settings
+ Default locales
Homepage
@@ -142,7 +154,7 @@
Locale settings
- Preferred locales to get suggestions from
+ Preferred locales to get suggestions from
{{ multiple_team_selector.render(available_locales, selected_locales, form_field='locales_order', sortable=True) }}
diff --git a/pontoon/contributors/urls.py b/pontoon/contributors/urls.py
index e6a385bc55..f772f46985 100644
--- a/pontoon/contributors/urls.py
+++ b/pontoon/contributors/urls.py
@@ -56,6 +56,12 @@ class UsernameConverter(StringConverter):
views.mark_all_notifications_as_read,
name="pontoon.contributors.notifications.mark.all.as.read",
),
+ # API: Toggle user theme preference
+ path(
+ "api/v1/user/
/theme/",
+ views.toggle_theme,
+ name="pontoon.contributors.toggle_theme",
+ ),
# API: Toggle user profile attribute
path(
"api/v1/user//",
diff --git a/pontoon/contributors/views.py b/pontoon/contributors/views.py
index e540f41f72..23d904cb60 100644
--- a/pontoon/contributors/views.py
+++ b/pontoon/contributors/views.py
@@ -4,6 +4,7 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Q
from django.http import (
@@ -187,6 +188,36 @@ def toggle_user_profile_attribute(request, username):
return JsonResponse({"status": True})
+@login_required(redirect_field_name="", login_url="/403")
+@require_POST
+@transaction.atomic
+def toggle_theme(request, username):
+ user = get_object_or_404(User, username=username)
+ if user != request.user:
+ return JsonResponse(
+ {
+ "status": False,
+ "message": "Forbidden: You don't have permission to edit this user",
+ },
+ status=403,
+ )
+
+ theme = request.POST.get("theme", None)
+
+ try:
+ profile = user.profile
+ profile.theme = theme
+ profile.full_clean()
+ profile.save()
+ except ValidationError:
+ return JsonResponse(
+ {"status": False, "message": "Bad Request: Invalid theme"},
+ status=400,
+ )
+
+ return JsonResponse({"status": True})
+
+
@login_required(redirect_field_name="", login_url="/403")
@require_POST
@transaction.atomic
diff --git a/pontoon/insights/static/js/insights.js b/pontoon/insights/static/js/insights.js
index 2a25140634..0153166021 100644
--- a/pontoon/insights/static/js/insights.js
+++ b/pontoon/insights/static/js/insights.js
@@ -11,7 +11,7 @@ const longMonthFormat = new Intl.DateTimeFormat('en', {
year: 'numeric',
});
-const style = getComputedStyle(document.documentElement);
+const style = getComputedStyle(document.body);
var Pontoon = (function (my) {
return $.extend(true, my, {
@@ -77,7 +77,7 @@ var Pontoon = (function (my) {
tooltips: {
mode: 'index',
intersect: false,
- borderColor: style.getPropertyValue('--white-1').trim(),
+ borderColor: style.getPropertyValue('--white-1'),
borderWidth: 1,
caretPadding: 5,
xPadding: 10,
diff --git a/pontoon/insights/static/js/insights_charts.js b/pontoon/insights/static/js/insights_charts.js
index 85c93492ef..a5fb5a0f37 100644
--- a/pontoon/insights/static/js/insights_charts.js
+++ b/pontoon/insights/static/js/insights_charts.js
@@ -1,5 +1,5 @@
var Pontoon = (function (my) {
- const style = getComputedStyle(document.documentElement);
+ const style = getComputedStyle(document.body);
return $.extend(true, my, {
insights: {
initialize: function () {
diff --git a/pontoon/insights/static/js/insights_tab.js b/pontoon/insights/static/js/insights_tab.js
index 97a656caed..3d8af06f6d 100644
--- a/pontoon/insights/static/js/insights_tab.js
+++ b/pontoon/insights/static/js/insights_tab.js
@@ -4,7 +4,7 @@ var Pontoon = (function (my) {
style: 'percent',
maximumFractionDigits: 2,
});
- const style = getComputedStyle(document.documentElement);
+ const style = getComputedStyle(document.body);
return $.extend(true, my, {
insights: {
renderCharts: function () {
@@ -45,7 +45,7 @@ var Pontoon = (function (my) {
plot(
activeStart,
activeEnd,
- style.getPropertyValue('--light-green-1').trim(),
+ style.getPropertyValue('--light-green-1'),
);
var inactiveLength = 2;
@@ -54,11 +54,7 @@ var Pontoon = (function (my) {
}
var inactiveStart = activeEnd;
var inactiveEnd = inactiveStart + inactiveLength;
- plot(
- inactiveStart,
- inactiveEnd,
- style.getPropertyValue('--grey-9').trim(),
- );
+ plot(inactiveStart, inactiveEnd, style.getPropertyValue('--grey-9'));
// Update number
parent.find('.active').html(active);
@@ -80,7 +76,7 @@ var Pontoon = (function (my) {
var ctx = chart[0].getContext('2d');
var gradient = ctx.createLinearGradient(0, 0, 0, 160);
- let greenBlue = style.getPropertyValue('--green-blue').trim();
+ let greenBlue = style.getPropertyValue('--green-blue');
gradient.addColorStop(0, greenBlue);
gradient.addColorStop(1, 'transparent');
@@ -93,24 +89,15 @@ var Pontoon = (function (my) {
label: 'Age of unreviewed suggestions',
data: chart.data('lifespans'),
backgroundColor: gradient,
- borderColor: [style.getPropertyValue('--light-blue').trim()],
+ borderColor: [style.getPropertyValue('--light-blue')],
borderWidth: 2,
- pointBackgroundColor: style
- .getPropertyValue('--light-blue')
- .trim(),
+ pointBackgroundColor: style.getPropertyValue('--light-blue'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--light-blue')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor:
+ style.getPropertyValue('--light-blue'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
},
],
},
@@ -119,7 +106,7 @@ var Pontoon = (function (my) {
display: false,
},
tooltips: {
- borderColor: style.getPropertyValue('--light-blue').trim(),
+ borderColor: style.getPropertyValue('--light-blue'),
borderWidth: 1,
caretPadding: 5,
xPadding: 10,
@@ -175,7 +162,7 @@ var Pontoon = (function (my) {
var ctx = chart[0].getContext('2d');
var gradient = ctx.createLinearGradient(0, 0, 0, 160);
- gradient.addColorStop(0, style.getPropertyValue('--green-blue').trim());
+ gradient.addColorStop(0, style.getPropertyValue('--green-blue'));
gradient.addColorStop(1, 'transparent');
new Chart(chart, {
@@ -188,22 +175,14 @@ var Pontoon = (function (my) {
label: 'Current month',
data: chart.data('time-to-review-suggestions'),
backgroundColor: gradient,
- borderColor: [style.getPropertyValue('--blue-1').trim()],
+ borderColor: [style.getPropertyValue('--blue-1')],
borderWidth: 2,
- pointBackgroundColor: style.getPropertyValue('--blue-1').trim(),
+ pointBackgroundColor: style.getPropertyValue('--blue-1'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--blue-1')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor: style.getPropertyValue('--blue-1'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
order: 2,
spanGaps: true,
},
@@ -211,24 +190,15 @@ var Pontoon = (function (my) {
type: 'line',
label: '12-month average',
data: chart.data('time-to-review-suggestions-12-month-avg'),
- borderColor: [style.getPropertyValue('--light-blue').trim()],
+ borderColor: [style.getPropertyValue('--light-blue')],
borderWidth: 1,
- pointBackgroundColor: style
- .getPropertyValue('--light-blue')
- .trim(),
+ pointBackgroundColor: style.getPropertyValue('--light-blue'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--light-blue')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor:
+ style.getPropertyValue('--light-blue'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
order: 1,
spanGaps: true,
},
@@ -241,7 +211,7 @@ var Pontoon = (function (my) {
tooltips: {
mode: 'index',
intersect: false,
- borderColor: style.getPropertyValue('--light-blue').trim(),
+ borderColor: style.getPropertyValue('--light-blue'),
borderWidth: 1,
caretPadding: 5,
xPadding: 10,
@@ -298,10 +268,7 @@ var Pontoon = (function (my) {
var ctx = chart[0].getContext('2d');
var gradient = ctx.createLinearGradient(0, 0, 0, 160);
- gradient.addColorStop(
- 0,
- style.getPropertyValue('--dark-magenta').trim(),
- );
+ gradient.addColorStop(0, style.getPropertyValue('--dark-magenta'));
gradient.addColorStop(1, 'transparent');
new Chart(chart, {
@@ -314,24 +281,14 @@ var Pontoon = (function (my) {
label: 'Current month',
data: chart.data('time-to-review-pretranslations'),
backgroundColor: gradient,
- borderColor: [style.getPropertyValue('--hot-pink').trim()],
+ borderColor: [style.getPropertyValue('--hot-pink')],
borderWidth: 2,
- pointBackgroundColor: style
- .getPropertyValue('--hot-pink')
- .trim(),
+ pointBackgroundColor: style.getPropertyValue('--hot-pink'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--hot-pink')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor: style.getPropertyValue('--hot-pink'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
order: 2,
spanGaps: true,
},
@@ -339,24 +296,15 @@ var Pontoon = (function (my) {
type: 'line',
label: '12-month average',
data: chart.data('time-to-review-pretranslations-12-month-avg'),
- borderColor: [style.getPropertyValue('--dark-pink').trim()],
+ borderColor: [style.getPropertyValue('--dark-pink')],
borderWidth: 1,
- pointBackgroundColor: style
- .getPropertyValue('--dark-pink')
- .trim(),
+ pointBackgroundColor: style.getPropertyValue('--dark-pink'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--dark-pink')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor:
+ style.getPropertyValue('--dark-pink'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
order: 1,
spanGaps: true,
},
@@ -369,7 +317,7 @@ var Pontoon = (function (my) {
tooltips: {
mode: 'index',
intersect: false,
- borderColor: style.getPropertyValue('--dark-pink').trim(),
+ borderColor: style.getPropertyValue('--dark-pink'),
borderWidth: 1,
caretPadding: 5,
xPadding: 10,
@@ -426,7 +374,7 @@ var Pontoon = (function (my) {
var ctx = chart[0].getContext('2d');
var gradient = ctx.createLinearGradient(0, 0, 0, 400);
- gradient.addColorStop(0, style.getPropertyValue('--dark-green').trim());
+ gradient.addColorStop(0, style.getPropertyValue('--dark-green'));
gradient.addColorStop(1, 'transparent');
var humanData = chart.data('human-translations') || [];
@@ -444,32 +392,23 @@ var Pontoon = (function (my) {
data: chart.data('completion'),
yAxisID: 'completion-y-axis',
backgroundColor: gradient,
- borderColor: [style.getPropertyValue('--light-green-1').trim()],
+ borderColor: [style.getPropertyValue('--light-green-1')],
borderWidth: 2,
- pointBackgroundColor: style
- .getPropertyValue('--light-green-1')
- .trim(),
+ pointBackgroundColor: style.getPropertyValue('--light-green-1'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--light-green-1')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor:
+ style.getPropertyValue('--light-green-1'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
},
humanData.length > 0 && {
type: 'bar',
label: 'Human translations',
data: humanData,
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--green').trim(),
- hoverBackgroundColor: style.getPropertyValue('--green').trim(),
+ backgroundColor: style.getPropertyValue('--green'),
+ hoverBackgroundColor: style.getPropertyValue('--green'),
stack: 'translations',
order: 2,
},
@@ -478,12 +417,9 @@ var Pontoon = (function (my) {
label: 'Machinery translations',
data: machineryData,
yAxisID: 'strings-y-axis',
- backgroundColor: style
- .getPropertyValue('--forest-green-1')
- .trim(),
- hoverBackgroundColor: style
- .getPropertyValue('--forest-green-1')
- .trim(),
+ backgroundColor: style.getPropertyValue('--forest-green-1'),
+ hoverBackgroundColor:
+ style.getPropertyValue('--forest-green-1'),
stack: 'translations',
order: 1,
},
@@ -492,10 +428,8 @@ var Pontoon = (function (my) {
label: 'New source strings',
data: newSourcesData,
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--black-3').trim(),
- hoverBackgroundColor: style
- .getPropertyValue('--black-3')
- .trim(),
+ backgroundColor: style.getPropertyValue('--black-3'),
+ hoverBackgroundColor: style.getPropertyValue('--black-3'),
stack: 'source-strings',
order: 3,
hidden: true,
@@ -510,7 +444,7 @@ var Pontoon = (function (my) {
tooltips: {
mode: 'index',
intersect: false,
- borderColor: style.getPropertyValue('--light-green-1').trim(),
+ borderColor: style.getPropertyValue('--light-green-1'),
borderWidth: 1,
caretPadding: 5,
xPadding: 10,
@@ -576,7 +510,7 @@ var Pontoon = (function (my) {
scaleLabel: {
display: true,
labelString: 'COMPLETION',
- fontColor: style.getPropertyValue('--white-1').trim(),
+ fontColor: style.getPropertyValue('--white-1'),
fontStyle: 100,
},
gridLines: {
@@ -598,7 +532,7 @@ var Pontoon = (function (my) {
scaleLabel: {
display: true,
labelString: 'STRINGS',
- fontColor: style.getPropertyValue('--white-1').trim(),
+ fontColor: style.getPropertyValue('--white-1'),
fontStyle: 100,
},
gridLines: {
@@ -631,7 +565,7 @@ var Pontoon = (function (my) {
var ctx = chart[0].getContext('2d');
var gradient = ctx.createLinearGradient(0, 0, 0, 400);
- gradient.addColorStop(0, style.getPropertyValue('--light-blue').trim());
+ gradient.addColorStop(0, style.getPropertyValue('--light-blue'));
gradient.addColorStop(1, 'transparent');
var unreviewedData = chart.data('unreviewed') || [];
@@ -651,32 +585,23 @@ var Pontoon = (function (my) {
data: unreviewedData,
yAxisID: 'strings-y-axis',
backgroundColor: gradient,
- borderColor: [style.getPropertyValue('--light-blue').trim()],
+ borderColor: [style.getPropertyValue('--light-blue')],
borderWidth: 2,
- pointBackgroundColor: style
- .getPropertyValue('--light-blue')
- .trim(),
+ pointBackgroundColor: style.getPropertyValue('--light-blue'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--light-blue')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor:
+ style.getPropertyValue('--light-blue'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
},
peerApprovedData.length > 0 && {
type: 'bar',
label: 'Peer-approved',
data: peerApprovedData,
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--blue-1').trim(),
- hoverBackgroundColor: style.getPropertyValue('--blue-1').trim(),
+ backgroundColor: style.getPropertyValue('--blue-1'),
+ hoverBackgroundColor: style.getPropertyValue('--blue-1'),
stack: 'review-actions',
order: 3,
},
@@ -685,8 +610,8 @@ var Pontoon = (function (my) {
label: 'Self-approved',
data: selfApprovedData,
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--grey-5').trim(),
- hoverBackgroundColor: style.getPropertyValue('--grey-5').trim(),
+ backgroundColor: style.getPropertyValue('--grey-5'),
+ hoverBackgroundColor: style.getPropertyValue('--grey-5'),
stack: 'review-actions',
order: 2,
},
@@ -695,10 +620,8 @@ var Pontoon = (function (my) {
label: 'Rejected',
data: rejectedData,
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--magenta').trim(),
- hoverBackgroundColor: style
- .getPropertyValue('--magenta')
- .trim(),
+ backgroundColor: style.getPropertyValue('--magenta'),
+ hoverBackgroundColor: style.getPropertyValue('--magenta'),
stack: 'review-actions',
order: 1,
},
@@ -707,10 +630,8 @@ var Pontoon = (function (my) {
label: 'New suggestions',
data: chart.data('new-suggestions'),
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--black-3').trim(),
- hoverBackgroundColor: style
- .getPropertyValue('--black-3')
- .trim(),
+ backgroundColor: style.getPropertyValue('--black-3'),
+ hoverBackgroundColor: style.getPropertyValue('--black-3'),
stack: 'new-suggestions',
order: 4,
hidden: true,
@@ -725,7 +646,7 @@ var Pontoon = (function (my) {
tooltips: {
mode: 'index',
intersect: false,
- borderColor: style.getPropertyValue('--light-blue').trim(),
+ borderColor: style.getPropertyValue('--light-blue'),
borderWidth: 1,
caretPadding: 5,
xPadding: 10,
@@ -806,7 +727,7 @@ var Pontoon = (function (my) {
scaleLabel: {
display: true,
labelString: 'STRINGS',
- fontColor: style.getPropertyValue('--white-1').trim(),
+ fontColor: style.getPropertyValue('--white-1'),
fontStyle: 100,
},
gridLines: {
@@ -841,15 +762,12 @@ var Pontoon = (function (my) {
var gradient_approval = ctx.createLinearGradient(0, 0, 0, 400);
gradient_approval.addColorStop(
0,
- style.getPropertyValue('--dark-purple-2').trim(),
+ style.getPropertyValue('--dark-purple-2'),
);
gradient_approval.addColorStop(1, 'transparent');
var gradient_chrf = ctx.createLinearGradient(0, 0, 0, 400);
- gradient_chrf.addColorStop(
- 0,
- style.getPropertyValue('--dark-purple').trim(),
- );
+ gradient_chrf.addColorStop(0, style.getPropertyValue('--dark-purple'));
gradient_chrf.addColorStop(1, 'transparent');
var approvedData = chart.data('approved') || [];
@@ -867,22 +785,14 @@ var Pontoon = (function (my) {
data: chart.data('approval-rate'),
yAxisID: 'approval-rate-y-axis',
backgroundColor: gradient_approval,
- borderColor: [style.getPropertyValue('--lilac').trim()],
+ borderColor: [style.getPropertyValue('--lilac')],
borderWidth: 2,
- pointBackgroundColor: style.getPropertyValue('--lilac').trim(),
+ pointBackgroundColor: style.getPropertyValue('--lilac'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--lilac')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor: style.getPropertyValue('--lilac'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
spanGaps: true,
},
{
@@ -891,22 +801,14 @@ var Pontoon = (function (my) {
data: chart.data('chrf-score'),
yAxisID: 'approval-rate-y-axis',
backgroundColor: gradient_chrf,
- borderColor: [style.getPropertyValue('--purple').trim()],
+ borderColor: [style.getPropertyValue('--purple')],
borderWidth: 2,
- pointBackgroundColor: style.getPropertyValue('--purple').trim(),
+ pointBackgroundColor: style.getPropertyValue('--purple'),
pointHitRadius: 10,
pointRadius: 4,
pointHoverRadius: 6,
- pointHoverBackgroundColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--purple')
- .trim(),
- pointHoverBorderColor: getComputedStyle(
- document.documentElement,
- )
- .getPropertyValue('--white-1')
- .trim(),
+ pointHoverBackgroundColor: style.getPropertyValue('--purple'),
+ pointHoverBorderColor: style.getPropertyValue('--white-1'),
spanGaps: true,
},
approvedData.length > 0 && {
@@ -914,8 +816,8 @@ var Pontoon = (function (my) {
label: 'Approved',
data: approvedData,
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--pink-2').trim(),
- hoverBackgroundColor: style.getPropertyValue('--pink-2').trim(),
+ backgroundColor: style.getPropertyValue('--pink-2'),
+ hoverBackgroundColor: style.getPropertyValue('--pink-2'),
stack: 'reviewed-pretranslations',
order: 2,
},
@@ -924,10 +826,8 @@ var Pontoon = (function (my) {
label: 'Rejected',
data: rejectedData,
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--light-pink').trim(),
- hoverBackgroundColor: style
- .getPropertyValue('--light-pink')
- .trim(),
+ backgroundColor: style.getPropertyValue('--light-pink'),
+ hoverBackgroundColor: style.getPropertyValue('--light-pink'),
stack: 'reviewed-pretranslations',
order: 1,
},
@@ -936,10 +836,8 @@ var Pontoon = (function (my) {
label: 'New pretranslations',
data: newData,
yAxisID: 'strings-y-axis',
- backgroundColor: style.getPropertyValue('--black-3').trim(),
- hoverBackgroundColor: style
- .getPropertyValue('--black-3')
- .trim(),
+ backgroundColor: style.getPropertyValue('--black-3'),
+ hoverBackgroundColor: style.getPropertyValue('--black-3'),
stack: 'new-pretranslations',
order: 3,
hidden: true,
@@ -954,7 +852,7 @@ var Pontoon = (function (my) {
tooltips: {
mode: 'index',
intersect: false,
- borderColor: style.getPropertyValue('--pink-3').trim(),
+ borderColor: style.getPropertyValue('--pink-3'),
borderWidth: 1,
caretPadding: 5,
xPadding: 10,
@@ -1013,7 +911,7 @@ var Pontoon = (function (my) {
scaleLabel: {
display: true,
labelString: 'APPROVAL RATE',
- fontColor: style.getPropertyValue('--white-1').trim(),
+ fontColor: style.getPropertyValue('--white-1'),
fontStyle: 100,
},
gridLines: {
@@ -1035,7 +933,7 @@ var Pontoon = (function (my) {
scaleLabel: {
display: true,
labelString: 'STRINGS',
- fontColor: style.getPropertyValue('--white-1').trim(),
+ fontColor: style.getPropertyValue('--white-1'),
fontStyle: 100,
},
gridLines: {
diff --git a/pontoon/settings/base.py b/pontoon/settings/base.py
index b0b3f9e909..df9ba8a169 100644
--- a/pontoon/settings/base.py
+++ b/pontoon/settings/base.py
@@ -363,6 +363,7 @@ def _default_from_email():
"base": {
"source_filenames": (
"css/dark-theme.css",
+ "css/light-theme.css",
"css/fontawesome-all.css",
"css/nprogress.css",
"css/boilerplate.css",
@@ -372,7 +373,11 @@ def _default_from_email():
"output_filename": "css/base.min.css",
},
"translate": {
- "source_filenames": ("translate.css", "css/dark-theme.css"),
+ "source_filenames": (
+ "translate.css",
+ "css/dark-theme.css",
+ "css/light-theme.css",
+ ),
"output_filename": "css/translate.min.css",
},
"admin": {
@@ -515,6 +520,7 @@ def _default_from_email():
"js/lib/jquery.color-2.1.2.js",
"js/lib/nprogress.js",
"js/main.js",
+ "js/theme-switcher.js",
),
"output_filename": "js/base.min.js",
},
diff --git a/translate/public/translate.html b/translate/public/translate.html
index 4d36d02cc1..ff50a3d79a 100644
--- a/translate/public/translate.html
+++ b/translate/public/translate.html
@@ -24,7 +24,7 @@
{% include "tracker.html" %}
-
+
diff --git a/translate/src/App.tsx b/translate/src/App.tsx
index 10c331672f..21ca8edb24 100644
--- a/translate/src/App.tsx
+++ b/translate/src/App.tsx
@@ -8,6 +8,7 @@ import { initLocale, Locale, updateLocale } from './context/Locale';
import { Location } from './context/Location';
import { MentionUsersProvider } from './context/MentionUsers';
import { NotificationProvider } from './context/Notification';
+import { ThemeProvider } from './context/Theme';
import { WaveLoader } from './modules/loaders';
import { NotificationPanel } from './modules/notification/components/NotificationPanel';
@@ -60,34 +61,36 @@ export function App() {
return (
-
-
-
-
-
-
-
- {allProjects ? null : }
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {allProjects ? null : }
+
+
+
+
+
+
+ {batchactions.entities.length === 0 ? (
+
+ ) : (
+
+ )}
+
-
- {batchactions.entities.length === 0 ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
+
+
+
+
+
);
diff --git a/translate/src/context/Theme.tsx b/translate/src/context/Theme.tsx
new file mode 100644
index 0000000000..2472f0754d
--- /dev/null
+++ b/translate/src/context/Theme.tsx
@@ -0,0 +1,57 @@
+import { createContext, useEffect, useState } from 'react';
+
+export const ThemeContext = createContext({
+ theme: 'system',
+});
+
+function getSystemTheme() {
+ if (
+ window.matchMedia &&
+ window.matchMedia('(prefers-color-scheme: dark)').matches
+ ) {
+ return 'dark';
+ } else {
+ return 'light';
+ }
+}
+
+export function ThemeProvider({ children }: { children: React.ReactElement }) {
+ const [theme] = useState(
+ () => document.body.getAttribute('data-theme') || 'dark',
+ );
+
+ useEffect(() => {
+ function applyTheme(newTheme: string) {
+ if (newTheme === 'system') {
+ newTheme = getSystemTheme();
+ }
+ document.body.classList.remove(
+ 'dark-theme',
+ 'light-theme',
+ 'system-theme',
+ );
+ document.body.classList.add(`${newTheme}-theme`);
+ }
+
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ function handleThemeChange(e: MediaQueryListEvent) {
+ let userThemeSetting = document.body.getAttribute('data-theme') || 'dark';
+
+ if (userThemeSetting === 'system') {
+ applyTheme(e.matches ? 'dark' : 'light');
+ }
+ }
+
+ mediaQuery.addEventListener('change', handleThemeChange);
+
+ applyTheme(theme);
+
+ return () => {
+ mediaQuery.removeEventListener('change', handleThemeChange);
+ };
+ }, [theme]);
+
+ return (
+ {children}
+ );
+}