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} + ); +}