mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Enable theming via new HTML element attributes (#37477)
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
<html class="no-reduce-motion theme-light">
|
<html class="no-reduce-motion" data-color-scheme="light">
|
||||||
</html>
|
</html>
|
||||||
@@ -89,6 +89,12 @@ module ApplicationHelper
|
|||||||
Rails.env.production? ? site_title : "#{site_title} (Dev)"
|
Rails.env.production? ? site_title : "#{site_title} (Dev)"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def page_color_scheme
|
||||||
|
return content_for(:force_color_scheme) if content_for(:force_color_scheme)
|
||||||
|
|
||||||
|
color_scheme
|
||||||
|
end
|
||||||
|
|
||||||
def label_for_scope(scope)
|
def label_for_scope(scope)
|
||||||
safe_join [
|
safe_join [
|
||||||
tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }),
|
tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }),
|
||||||
@@ -153,6 +159,19 @@ module ApplicationHelper
|
|||||||
tag.meta(content: content, property: property)
|
tag.meta(content: content, property: property)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def html_attributes
|
||||||
|
base = {
|
||||||
|
lang: I18n.locale,
|
||||||
|
class: html_classes,
|
||||||
|
'data-contrast': contrast.parameterize,
|
||||||
|
'data-color-scheme': page_color_scheme.parameterize,
|
||||||
|
}
|
||||||
|
|
||||||
|
base[:'data-system-theme'] = 'true' if page_color_scheme == 'auto'
|
||||||
|
|
||||||
|
base
|
||||||
|
end
|
||||||
|
|
||||||
def html_classes
|
def html_classes
|
||||||
output = []
|
output = []
|
||||||
output << content_for(:html_classes)
|
output << content_for(:html_classes)
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
(function (element) {
|
(function (element) {
|
||||||
const {userTheme} = element.dataset;
|
const {colorScheme, contrast} = element.dataset;
|
||||||
|
|
||||||
const colorSchemeMediaWatcher = window.matchMedia('(prefers-color-scheme: dark)');
|
const colorSchemeMediaWatcher = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
const contrastMediaWatcher = window.matchMedia('(prefers-contrast: more)');
|
const contrastMediaWatcher = window.matchMedia('(prefers-contrast: more)');
|
||||||
|
|
||||||
const updateColorScheme = () => {
|
const updateColorScheme = () => {
|
||||||
const useDarkMode = userTheme === 'system' ? colorSchemeMediaWatcher.matches : userTheme !== 'mastodon-light';
|
const useDarkMode = colorScheme === 'auto' ? colorSchemeMediaWatcher.matches : colorScheme === 'dark';
|
||||||
element.dataset.mode = useDarkMode ? 'dark' : 'light';
|
|
||||||
|
element.dataset.colorScheme = useDarkMode ? 'dark' : 'light';
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateContrast = () => {
|
const updateContrast = () => {
|
||||||
const useHighContrast = userTheme === 'contrast' || contrastMediaWatcher.matches;
|
const useHighContrast = contrast === 'high' || contrastMediaWatcher.matches;
|
||||||
|
|
||||||
element.dataset.contrast = useHighContrast ? 'high' : 'default';
|
element.dataset.contrast = useHighContrast ? 'high' : 'default';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import type { ApiAnnualReportState } from '@/mastodon/api/annual_report';
|
import type { ApiAnnualReportState } from '@/mastodon/api/annual_report';
|
||||||
import { Button } from '@/mastodon/components/button';
|
import { Button } from '@/mastodon/components/button';
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@ export const AnnualReportAnnouncement: React.FC<
|
|||||||
AnnualReportAnnouncementProps
|
AnnualReportAnnouncementProps
|
||||||
> = ({ year, state, onRequestBuild, onOpen, onDismiss }) => {
|
> = ({ year, state, onRequestBuild, onOpen, onDismiss }) => {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('theme-dark', styles.wrapper)}>
|
<div className={styles.wrapper} data-color-scheme='dark'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='annual_report.announcement.title'
|
id='annual_report.announcement.title'
|
||||||
defaultMessage='Wrapstodon {year} has arrived'
|
defaultMessage='Wrapstodon {year} has arrived'
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
|||||||
const topHashtag = report.data.top_hashtags[0];
|
const topHashtag = report.data.top_hashtags[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={moduleClassNames(styles.wrapper, 'theme-dark')}>
|
<div className={styles.wrapper} data-color-scheme='dark'>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h1>Wrapstodon {report.year}</h1>
|
<h1>Wrapstodon {report.year}</h1>
|
||||||
{account && <p>@{account.acct}</p>}
|
{account && <p>@{account.acct}</p>}
|
||||||
|
|||||||
@@ -60,11 +60,8 @@ const AnnualReportModal: React.FC<{
|
|||||||
// default modal backdrop, preventing clicks to pass through.
|
// default modal backdrop, preventing clicks to pass through.
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames('modal-root__modal', styles.modalWrapper)}
|
||||||
'modal-root__modal',
|
data-color-scheme='dark'
|
||||||
styles.modalWrapper,
|
|
||||||
'theme-dark',
|
|
||||||
)}
|
|
||||||
onClick={handleCloseModal}
|
onClick={handleCloseModal}
|
||||||
>
|
>
|
||||||
{!showAnnouncement ? (
|
{!showAnnouncement ? (
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ describe('emoji', () => {
|
|||||||
|
|
||||||
it('does an emoji containing ZWJ properly', () => {
|
it('does an emoji containing ZWJ properly', () => {
|
||||||
expect(emojify('💂♀️💂♂️'))
|
expect(emojify('💂♀️💂♂️'))
|
||||||
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
|
.toEqual('<img draggable="false" class="emojione" alt="💂♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f.svg"><img draggable="false" class="emojione" alt="💂♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
|
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Trie from 'substring-trie';
|
import Trie from 'substring-trie';
|
||||||
|
|
||||||
import { getUserTheme, isDarkMode } from '@/mastodon/utils/theme';
|
import { getIsSystemTheme, isDarkMode } from '@/mastodon/utils/theme';
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { autoPlayGif } from '../../initial_state';
|
import { autoPlayGif } from '../../initial_state';
|
||||||
@@ -98,7 +98,7 @@ const emojifyTextNode = (node, customEmojis) => {
|
|||||||
const { filename, shortCode } = unicodeMapping[unicode_emoji];
|
const { filename, shortCode } = unicodeMapping[unicode_emoji];
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
|
|
||||||
const isSystemTheme = getUserTheme() === 'system';
|
const isSystemTheme = getIsSystemTheme();
|
||||||
|
|
||||||
const theme = (isSystemTheme || !isDarkMode()) ? 'light' : 'dark';
|
const theme = (isSystemTheme || !isDarkMode()) ? 'light' : 'dark';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
export function getUserTheme() {
|
export function getIsSystemTheme() {
|
||||||
const { userTheme } = document.documentElement.dataset;
|
const { systemTheme } = document.documentElement.dataset;
|
||||||
return userTheme;
|
return systemTheme === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDarkMode() {
|
export function isDarkMode() {
|
||||||
const { userTheme } = document.documentElement.dataset;
|
const { colorScheme } = document.documentElement.dataset;
|
||||||
return userTheme === 'system'
|
return colorScheme === 'dark';
|
||||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
: userTheme !== 'mastodon-light';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,49 +5,29 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
@include base.palette;
|
@include base.palette;
|
||||||
|
|
||||||
&:where([data-user-theme='system']) {
|
|
||||||
color-scheme: dark light;
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
@include dark.tokens;
|
|
||||||
@include utils.invert-on-dark;
|
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
|
||||||
@include dark.contrast-overrides;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
@include light.tokens;
|
|
||||||
@include utils.invert-on-light;
|
|
||||||
|
|
||||||
@media (prefers-contrast: more) {
|
|
||||||
@include light.contrast-overrides;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-dark,
|
[data-color-scheme='dark'],
|
||||||
html:where(
|
html:not([data-color-scheme]) {
|
||||||
:not([data-user-theme='mastodon-light'], [data-user-theme='system'])
|
|
||||||
) {
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
@include dark.tokens;
|
@include dark.tokens;
|
||||||
@include utils.invert-on-dark;
|
@include utils.invert-on-dark;
|
||||||
|
|
||||||
|
&[data-contrast='high'],
|
||||||
|
[data-contrast='high'] & {
|
||||||
|
@include dark.contrast-overrides;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-user-theme='contrast'],
|
[data-color-scheme='light'] {
|
||||||
html[data-user-theme='contrast'] .theme-dark {
|
|
||||||
@include dark.contrast-overrides;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light,
|
|
||||||
html:where([data-user-theme='mastodon-light']) {
|
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
|
||||||
@include light.tokens;
|
@include light.tokens;
|
||||||
@include utils.invert-on-light;
|
@include utils.invert-on-light;
|
||||||
|
|
||||||
|
&[data-contrast='high'],
|
||||||
|
[data-contrast='high'] & {
|
||||||
|
@include light.contrast-overrides;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
!!! 5
|
!!! 5
|
||||||
%html{ lang: I18n.locale, class: html_classes, 'data-user-theme': current_theme.parameterize, 'data-contrast': contrast.parameterize, 'data-mode': color_scheme.parameterize }
|
%html{ html_attributes }
|
||||||
%head
|
%head
|
||||||
%meta{ charset: 'utf-8' }/
|
%meta{ charset: 'utf-8' }/
|
||||||
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' }/
|
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' }/
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
= vite_typescript_tag 'wrapstodon.tsx', crossorigin: 'anonymous'
|
= vite_typescript_tag 'wrapstodon.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :html_classes, 'theme-dark'
|
- content_for :force_color_scheme, 'dark'
|
||||||
|
|
||||||
#wrapstodon
|
#wrapstodon
|
||||||
= render_wrapstodon_share_data @generated_annual_report
|
= render_wrapstodon_share_data @generated_annual_report
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ RSpec.describe 'Content-Security-Policy' do
|
|||||||
img-src 'self' data: blob: #{local_domain}
|
img-src 'self' data: blob: #{local_domain}
|
||||||
manifest-src 'self' #{local_domain}
|
manifest-src 'self' #{local_domain}
|
||||||
media-src 'self' data: #{local_domain}
|
media-src 'self' data: #{local_domain}
|
||||||
script-src 'self' #{local_domain} 'wasm-unsafe-eval' 'sha256-Q/2Cjx8v06hAdOF8/DeBUpsmBcSj7sLN3I/WpTF8T8c='
|
script-src 'self' #{local_domain} 'wasm-unsafe-eval' 'sha256-Z5KW83D+6/pygIQS3h9XDpF52xW3l3BHc7JL9tj3uMs='
|
||||||
style-src 'self' #{local_domain} 'nonce-ZbA+JmE7+bK8F5qvADZHuQ=='
|
style-src 'self' #{local_domain} 'nonce-ZbA+JmE7+bK8F5qvADZHuQ=='
|
||||||
worker-src 'self' blob: #{local_domain}
|
worker-src 'self' blob: #{local_domain}
|
||||||
CSP
|
CSP
|
||||||
|
|||||||
Reference in New Issue
Block a user