From a741639085d70c22a9f49890542a142a223bf981 Mon Sep 17 00:00:00 2001 From: alistair3149 Date: Wed, 24 Apr 2024 20:23:45 -0400 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E2=9C=A8=20move=20theme=20prefer?= =?UTF-8?q?ences=20to=20clientPrefs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since clientPrefs and other related features are not avaliable until MW 1.42, many of them are backported as polyfill. Instead of using cookies, the polyfill are using localStorage only like Citizen in the past. There are many changes behind the scene, but the most important one being that `skin-citizen-*` theme classes are now soft-deprecated, and replaced by the standardized `skin-theme-clientpref-*` classes. There will be sufficient time before the hard deprecation. Related: #780 --- i18n/en.json | 13 +- i18n/qqq.json | 10 +- includes/Hooks/SkinHooks.php | 1 + includes/Partials/Theme.php | 13 +- .../addPortlet.polyfill.js | 88 +++++ .../clientPreferences.js | 348 ++++++++++++++++++ .../clientPreferences.json | 6 + ...ocalStorage.js => clientPrefs.polyfill.js} | 3 +- .../skins.citizen.preferences.js | 53 ++- .../skins.citizen.preferences.less | 121 +++--- .../templates/preferences.mustache | 15 +- resources/skins.citizen.scripts/inline.js | 94 +++-- skin.json | 13 +- 13 files changed, 616 insertions(+), 162 deletions(-) create mode 100644 resources/skins.citizen.preferences/addPortlet.polyfill.js create mode 100644 resources/skins.citizen.preferences/clientPreferences.js create mode 100644 resources/skins.citizen.preferences/clientPreferences.json rename resources/skins.citizen.preferences/{clientPrefs.localStorage.js => clientPrefs.polyfill.js} (97%) diff --git a/i18n/en.json b/i18n/en.json index 40129365c..c415fb154 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -43,12 +43,15 @@ "citizen-tagline-ns-help": "Help page", "citizen-tagline-ns-category": "Category page", "citizen-tagline-user-regdate": "Joined $1", - "prefs-citizen-theme-label": "Theme", - "prefs-citizen-theme-option-auto": "Auto", - "prefs-citizen-theme-option-light": "Light", - "prefs-citizen-theme-option-dark": "Dark", "prefs-citizen-fontsize-label": "Font size", "prefs-citizen-pagewidth-label": "Page width", "prefs-citizen-lineheight-label": "Line height", - "prefs-citizen-resetbutton-label": "Reset to default" + "prefs-citizen-resetbutton-label": "Reset to default", + + "skin-theme-name": "Color", + "skin-theme-description": "Reduces the light emitted by device screens.", + "skin-theme-day-label": "Day", + "skin-theme-night-label": "Night", + "skin-theme-os-label": "Automatic", + "skin-theme-exclusion-notice": "This page is always in day mode." } diff --git a/i18n/qqq.json b/i18n/qqq.json index 29a660c76..91fe88715 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -46,12 +46,12 @@ "citizen-tagline-ns-help": "Tagline for pages in help namespace", "citizen-tagline-ns-category": "Tagline for pages in category namespace", "citizen-tagline-user-regdate": "Label for registration date in taglines on userpages", - "prefs-citizen-theme-label": "Tooltip for the theme dropdown in Special:Preferences", - "prefs-citizen-theme-option-auto": "Label for the auto theme option", - "prefs-citizen-theme-option-light": "Label for the light theme option", - "prefs-citizen-theme-option-dark": "Label for the dark theme option", "prefs-citizen-fontsize-label": "Label for the font size settings", "prefs-citizen-pagewidth-label": "Label for the page width settings", "prefs-citizen-lineheight-label": "Label for the line height settings", - "prefs-citizen-resetbutton-label": "Label for the reset button that restore default settings" + "prefs-citizen-resetbutton-label": "Label for the reset button that restore default settings", + "skin-theme-name": "Label for setting that allows you to change theme.", + "skin-theme-description": "Description for theme.", + "skin-theme-day-label": "Label for light theme (standard mode).", + "skin-theme-night-label": "Label for night theme (dark mode)." } diff --git a/includes/Hooks/SkinHooks.php b/includes/Hooks/SkinHooks.php index 105366b62..73a150fcc 100644 --- a/includes/Hooks/SkinHooks.php +++ b/includes/Hooks/SkinHooks.php @@ -67,6 +67,7 @@ public function onBeforePageDisplay( $out, $skin ): void { $script = file_get_contents( MW_INSTALL_PATH . '/skins/Citizen/resources/skins.citizen.scripts/inline.js' ); $script = Html::inlineScript( $script ); + // TODO: Consider turning on cache after this is more stable $script = RL\ResourceLoader::filter( 'minify-js', $script, [ 'cache' => false ] ); $out->addHeadItem( 'skin.citizen.inline', $script ); } diff --git a/includes/Partials/Theme.php b/includes/Partials/Theme.php index 2d66b5507..a9ff2a10a 100644 --- a/includes/Partials/Theme.php +++ b/includes/Partials/Theme.php @@ -25,6 +25,12 @@ namespace MediaWiki\Skins\Citizen\Partials; +const CLIENTPREFS_THEME_MAP = [ + 'auto' => 'os', + 'light' => 'day', + 'dark' => 'night' +]; + /** * Theme switcher partial of Skin Citizen */ @@ -42,7 +48,12 @@ public function setSkinTheme( array &$options ) { // Set theme to site theme $theme = $this->getConfigValue( 'CitizenThemeDefault' ) ?? 'auto'; - // Add HTML class based on theme set + // Legacy class to be deprecated $out->addHtmlClasses( 'skin-citizen-' . $theme ); + + // Add HTML class based on theme set + if ( CLIENTPREFS_THEME_MAP[ $theme ] ) { + $out->addHtmlClasses( 'skin-theme-clientpref-' . $theme ); + } } } diff --git a/resources/skins.citizen.preferences/addPortlet.polyfill.js b/resources/skins.citizen.preferences/addPortlet.polyfill.js new file mode 100644 index 000000000..b972d343b --- /dev/null +++ b/resources/skins.citizen.preferences/addPortlet.polyfill.js @@ -0,0 +1,88 @@ +/** + * TODO: Revisit when we move to MW 1.43 and the interface is more stable + */ + +/** + * Creates default portlet. + * Based on Vector + * + * @param {Element} portlet + * @return {Element} + */ +function addDefaultPortlet( portlet ) { + const ul = portlet.querySelector( 'ul' ); + if ( !ul ) { + return portlet; + } + ul.classList.add( 'citizen-menu__content-list' ); + const label = portlet.querySelector( 'label' ); + if ( label ) { + const labelDiv = document.createElement( 'div' ); + labelDiv.classList.add( 'citizen-menu__heading' ); + labelDiv.innerHTML = label.textContent || ''; + portlet.insertBefore( labelDiv, label ); + label.remove(); + } + let wrapper = portlet.querySelector( 'div:last-child' ); + if ( wrapper ) { + ul.remove(); + wrapper.appendChild( ul ); + wrapper.classList.add( 'citizen-menu__content' ); + } else { + wrapper = document.createElement( 'div' ); + wrapper.classList.add( 'citizen-menu__content' ); + ul.remove(); + wrapper.appendChild( ul ); + portlet.appendChild( wrapper ); + } + portlet.classList.add( 'citizen-menu' ); + return portlet; +} + +/** + * Polyfill for mw.util.addPortlet for < MW 1.42 + * + * @return {Element} + */ +function addPortlet() { + if ( mw.util.addPortlet ) { + return addDefaultPortlet( mw.util.addPortlet ); + } + + return function ( id, label, before ) { + const portlet = document.createElement( 'div' ); + portlet.classList.add( 'mw-portlet', 'mw-portlet-' + id, 'emptyPortlet', + // Additional class is added to allow skins to track portlets added via this mechanism. + 'mw-portlet-js' + ); + portlet.id = id; + if ( label ) { + const labelNode = document.createElement( 'label' ); + labelNode.textContent = label; + portlet.appendChild( labelNode ); + } + const listWrapper = document.createElement( 'div' ); + const list = document.createElement( 'ul' ); + listWrapper.appendChild( list ); + portlet.appendChild( listWrapper ); + if ( before ) { + let referenceNode; + try { + referenceNode = document.querySelector( before ); + } catch ( e ) { + // CSS selector not supported by browser. + } + if ( referenceNode ) { + const parentNode = referenceNode.parentNode; + parentNode.insertBefore( portlet, referenceNode ); + } else { + return null; + } + } + mw.hook( 'util.addPortlet' ).fire( portlet, before ); + return addDefaultPortlet( portlet ); + }; +} + +/** @module addPortlet */ +module.exports = addPortlet; diff --git a/resources/skins.citizen.preferences/clientPreferences.js b/resources/skins.citizen.preferences/clientPreferences.js new file mode 100644 index 000000000..327963b17 --- /dev/null +++ b/resources/skins.citizen.preferences/clientPreferences.js @@ -0,0 +1,348 @@ +/** + * @typedef {Object} ClientPreference + * @property {string[]} options that are valid for this client preference + * @property {string} preferenceKey for registered users. + * @property {string} [type] defaults to radio. Supported: radio, switch + * @property {Function} [callback] callback executed after a client preference has been modified. + */ + +/** + * @typedef {Object} PreferenceOption + * @property {string} label + * @property {string} value + */ + +const addPortlet = require( './addPortlet.polyfill.js' )(); +const clientPrefs = require( './clientPrefs.polyfill.js' )(); + +/** + * Get the list of client preferences that are active on the page, including hidden. + * + * @return {string[]} of active client preferences + */ +function getClientPreferences() { + return Array.from( document.documentElement.classList ).filter( + ( className ) => className.match( /-clientpref-/ ) + ).map( ( className ) => className.split( '-clientpref-' )[ 0 ] ); +} + +/** + * Check if the feature is excluded from the current page. + * + * @param {string} featureName + * @return {boolean} + */ +function isFeatureExcluded( featureName ) { + return document.documentElement.classList.contains( featureName + '-clientpref-excluded' ); +} + +/** + * Get the list of client preferences that are active on the page and not hidden. + * + * @param {Record} config + * @return {string[]} of user facing client preferences + */ +function getVisibleClientPreferences( config ) { + const active = getClientPreferences(); + // Order should be based on key in config.json + return Object.keys( config ).filter( ( key ) => active.indexOf( key ) > -1 ); +} + +/** + * @param {string} featureName + * @param {string} value + * @param {Record} config + */ +function toggleDocClassAndSave( featureName, value, config ) { + const pref = config[ featureName ]; + const callback = pref.callback || ( () => {} ); + clientPrefs.set( featureName, value ); + callback(); +} + +/** + * @param {string} featureName + * @param {string} value + * @return {string} + */ +const getInputId = ( featureName, value ) => `skin-client-pref-${ featureName }-value-${ value }`; + +/** + * @param {string} type + * @param {string} featureName + * @param {string} value + * @return {HTMLInputElement} + */ +function makeInputElement( type, featureName, value ) { + const input = document.createElement( 'input' ); + const name = `skin-client-pref-${ featureName }-group`; + const id = getInputId( featureName, value ); + input.name = name; + input.id = id; + input.type = type; + if ( type === 'checkbox' ) { + input.checked = value === '1'; + } else { + input.value = value; + } + input.setAttribute( 'data-event-name', id ); + return input; +} + +/** + * @param {string} featureName + * @param {string} value + * @return {HTMLLabelElement} + */ +function makeLabelElement( featureName, value ) { + const label = document.createElement( 'label' ); + // eslint-disable-next-line mediawiki/msg-doc + label.textContent = mw.msg( `${ featureName }-${ value }-label` ); + label.setAttribute( 'for', getInputId( featureName, value ) ); + return label; +} + +/** + * Create an element that informs users that a feature is not functional + * on a given page. This message is hidden by default and made visible in + * CSS if a specific exclusion class exists. + * + * @param {string} featureName + * @return {HTMLElement} + */ +function makeExclusionNotice( featureName ) { + const p = document.createElement( 'p' ); + // eslint-disable-next-line mediawiki/msg-doc + const noticeMessage = mw.message( `${ featureName }-exclusion-notice` ); + p.classList.add( 'exclusion-notice', `${ featureName }-exclusion-notice` ); + p.textContent = noticeMessage.text(); + return p; +} + +/** + * @param {Element} parent + * @param {string} featureName + * @param {string} value + * @param {string} currentValue + * @param {Record} config + */ +function appendRadioToggle( parent, featureName, value, currentValue, config ) { + const input = makeInputElement( 'radio', featureName, value ); + // input.classList.add( 'cdx-radio__input' ); + input.classList.add( 'citizen-client-prefs-radio__input' ); + if ( currentValue === value ) { + input.checked = true; + } + + if ( isFeatureExcluded( featureName ) ) { + input.disabled = true; + } + + const icon = document.createElement( 'span' ); + // icon.classList.add( 'cdx-radio__icon' ); + icon.classList.add( 'citizen-client-prefs-radio__icon' ); + const label = makeLabelElement( featureName, value ); + // label.classList.add( 'cdx-radio__label' ); + label.classList.add( 'citizen-client-prefs-radio__label' ); + const container = document.createElement( 'div' ); + // container.classList.add( 'cdx-radio' ); + container.classList.add( 'citizen-client-prefs-radio' ); + container.appendChild( input ); + container.appendChild( icon ); + container.appendChild( label ); + parent.appendChild( container ); + input.addEventListener( 'change', () => { + toggleDocClassAndSave( featureName, value, config ); + } ); +} + +/** + * @param {Element} form + * @param {string} featureName + * @param {HTMLElement} labelElement + * @param {string} currentValue + * @param {Record} config + */ +function appendToggleSwitch( form, featureName, labelElement, currentValue, config ) { + const input = makeInputElement( 'checkbox', featureName, currentValue ); + // input.classList.add( 'cdx-toggle-switch__input' ); + input.classList.add( 'citizen-client-prefs-toggle-switch__input' ); + const switcher = document.createElement( 'span' ); + // switcher.classList.add( 'cdx-toggle-switch__switch' ); + switcher.classList.add( 'citizen-client-prefs-toggle-switch__switch' ); + const grip = document.createElement( 'span' ); + // grip.classList.add( 'cdx-toggle-switch__switch__grip' ); + grip.classList.add( 'citizen-client-prefs-toggle-switch__switch__grip' ); + switcher.appendChild( grip ); + const label = labelElement || makeLabelElement( featureName, currentValue ); + // label.classList.add( 'cdx-toggle-switch__label' ); + label.classList.add( 'citizen-client-prefs-toggle-switch__label' ); + const toggleSwitch = document.createElement( 'span' ); + // toggleSwitch.classList.add( 'cdx-toggle-switch' ); + toggleSwitch.classList.add( 'citizen-client-prefs-toggle-switch' ); + toggleSwitch.appendChild( input ); + toggleSwitch.appendChild( switcher ); + toggleSwitch.appendChild( label ); + input.addEventListener( 'change', () => { + toggleDocClassAndSave( featureName, input.checked ? '1' : '0', config ); + } ); + form.appendChild( toggleSwitch ); +} + +/** + * @param {string} className + * @return {Element} + */ +function createRow( className ) { + const row = document.createElement( 'div' ); + row.setAttribute( 'class', className ); + return row; +} + +/** + * Get the label for the feature. + * + * @param {string} featureName + * @return {MwMessage} + */ +const getFeatureLabelMsg = ( featureName ) => + // eslint-disable-next-line mediawiki/msg-doc + mw.message( `${ featureName }-name` ); + +/** + * adds a toggle button + * + * @param {string} featureName + * @param {Record} config + * @return {Element|null} + */ +function makeControl( featureName, config ) { + const pref = config[ featureName ]; + if ( !pref ) { + return null; + } + const currentValue = clientPrefs.get( featureName ); + // The client preference was invalid. This shouldn't happen unless a gadget + // or script has modified the documentElement. + if ( typeof currentValue === 'boolean' ) { + return null; + } + const row = createRow( '' ); + const form = document.createElement( 'form' ); + const type = pref.type || 'radio'; + switch ( type ) { + case 'radio': + pref.options.forEach( ( value ) => { + appendRadioToggle( form, featureName, value, currentValue, config ); + } ); + break; + case 'switch': { + const labelElement = document.createElement( 'label' ); + labelElement.textContent = getFeatureLabelMsg( featureName ).text(); + appendToggleSwitch( form, featureName, labelElement, currentValue, config ); + break; + } default: + throw new Error( 'Unknown client preference! Only switch or radio are supported.' ); + } + row.appendChild( form ); + + if ( isFeatureExcluded( featureName ) ) { + const exclusionNotice = makeExclusionNotice( featureName ); + row.appendChild( exclusionNotice ); + } + return row; +} + +/** + * @param {Element} parent + * @param {string} featureName + * @param {Record} config + */ +function makeClientPreference( parent, featureName, config ) { + const labelMsg = getFeatureLabelMsg( featureName ); + // If the user is not debugging messages and no language exists, + // exit as its a hidden client preference. + if ( !labelMsg.exists() && mw.config.get( 'wgUserLanguage' ) !== 'qqx' ) { + return; + } else { + const id = `skin-client-prefs-${ featureName }`; + const portlet = addPortlet( id, labelMsg.text() ); + const labelElement = portlet.querySelector( 'label' ); + // eslint-disable-next-line mediawiki/msg-doc + const descriptionMsg = mw.message( `${ featureName }-description` ); + if ( descriptionMsg.exists() ) { + const desc = document.createElement( 'span' ); + desc.classList.add( 'skin-client-pref-description' ); + desc.textContent = descriptionMsg.text(); + if ( labelElement && labelElement.parentNode ) { + labelElement.appendChild( desc ); + } + } + const row = makeControl( featureName, config ); + parent.appendChild( portlet ); + if ( row ) { + const tmp = mw.util.addPortletLink( id, '', '' ); + // create a dummy link + if ( tmp ) { + const link = tmp.querySelector( 'a' ); + if ( link ) { + link.replaceWith( row ); + } + } + } + } +} + +/** + * Fills the client side preference dropdown with controls. + * + * @param {string} selector of element to fill with client preferences + * @param {Record} config + * @return {Promise} + */ +function render( selector, config ) { + const node = document.querySelector( selector ); + if ( !node ) { + return Promise.reject(); + } + return new Promise( ( resolve ) => { + getVisibleClientPreferences( config ).forEach( ( pref ) => { + makeClientPreference( node, pref, config ); + } ); + mw.requestIdleCallback( () => { + resolve( node ); + } ); + } ); +} + +/** + * @param {string} clickSelector what to click + * @param {string} renderSelector where to render + * @param {Record} config + */ +function bind( clickSelector, renderSelector, config ) { + let enhanced = false; + const chk = /** @type {HTMLInputElement} */ ( + document.querySelector( clickSelector ) + ); + if ( !chk ) { + return; + } + if ( chk.checked ) { + render( renderSelector, config ); + enhanced = true; + } else { + chk.addEventListener( 'input', () => { + if ( enhanced ) { + return; + } + render( renderSelector, config ); + enhanced = true; + } ); + } +} +module.exports = { + bind, + toggleDocClassAndSave, + render +}; diff --git a/resources/skins.citizen.preferences/clientPreferences.json b/resources/skins.citizen.preferences/clientPreferences.json new file mode 100644 index 000000000..e0586f2ac --- /dev/null +++ b/resources/skins.citizen.preferences/clientPreferences.json @@ -0,0 +1,6 @@ +{ + "skin-theme": { + "options": [ "os", "day", "night" ], + "preferenceKey": "citizen-theme" + } +} diff --git a/resources/skins.citizen.preferences/clientPrefs.localStorage.js b/resources/skins.citizen.preferences/clientPrefs.polyfill.js similarity index 97% rename from resources/skins.citizen.preferences/clientPrefs.localStorage.js rename to resources/skins.citizen.preferences/clientPrefs.polyfill.js index ef9c1fc23..fb4a0c0a6 100644 --- a/resources/skins.citizen.preferences/clientPrefs.localStorage.js +++ b/resources/skins.citizen.preferences/clientPrefs.polyfill.js @@ -1,5 +1,6 @@ /** - * mw.user.clientPrefs modified to only use localStorage + * Polyfill for mw.user.clientPrefs for < MW 1.42 + * Modified to use localStorage for all users * TODO: Revisit when we move to MW 1.43 and the interface is more stable */ diff --git a/resources/skins.citizen.preferences/skins.citizen.preferences.js b/resources/skins.citizen.preferences/skins.citizen.preferences.js index cc2bc9ddd..2dd71fb91 100644 --- a/resources/skins.citizen.preferences/skins.citizen.preferences.js +++ b/resources/skins.citizen.preferences/skins.citizen.preferences.js @@ -18,7 +18,7 @@ const CLIENTPREFS_THEME_MAP = { dark: 'night' }; -const clientPrefs = require( './clientPrefs.localStorage.js' )(); +const clientPrefs = require( './clientPrefs.polyfill.js' )(); /** * Set the value of the input element @@ -56,7 +56,6 @@ function setIndicator( key, value ) { */ function convertForForm( pref ) { return { - theme: pref.theme, fontsize: Number( pref.fontsize.slice( 0, -1 ) ) / 10 - 8, pagewidth: Number( pref.pagewidth.slice( 0, -2 ) ) / 120 - 6, lineheight: ( pref.lineheight - 1 ) * 10 @@ -105,7 +104,6 @@ function getPref() { }; const pref = { - theme: mw.storage.get( PREFIX_KEY + 'theme' ), fontsize: mw.storage.get( PREFIX_KEY + 'fontsize' ) || initFontSize(), pagewidth: mw.storage.get( PREFIX_KEY + 'pagewidth' ) || rootStyle.getPropertyValue( '--width-layout' ), lineheight: mw.storage.get( PREFIX_KEY + 'lineheight' ) || rootStyle.getPropertyValue( '--line-height' ) @@ -125,17 +123,12 @@ function setPref() { formData = Object.fromEntries( new FormData( document.getElementById( CLASS + '-form' ) ) ), currentPref = convertForForm( getPref() ), newPref = { - theme: formData[ CLASS + '-theme' ], fontsize: Number( formData[ CLASS + '-fontsize' ] ), pagewidth: Number( formData[ CLASS + '-pagewidth' ] ), lineheight: Number( formData[ CLASS + '-lineheight' ] ) }; - if ( currentPref.theme !== newPref.theme ) { - mw.storage.set( PREFIX_KEY + 'theme', newPref.theme ); - clientPrefs.set( 'skin-theme', CLIENTPREFS_THEME_MAP[ newPref.theme ] ); - - } else if ( currentPref.fontsize !== newPref.fontsize ) { + if ( currentPref.fontsize !== newPref.fontsize ) { const formattedFontSize = ( newPref.fontsize + 8 ) * 10 + '%'; mw.storage.set( PREFIX_KEY + 'fontsize', formattedFontSize ); setIndicator( 'fontsize', formattedFontSize ); @@ -166,7 +159,6 @@ function setPref() { * @return {void} */ function resetPref() { - // Do not reset theme as its default value is defined somewhere else const keys = [ 'fontsize', 'pagewidth', 'lineheight' ]; // Remove style @@ -239,15 +231,12 @@ function togglePanel() { toggle = document.getElementById( CLASS + '-toggle' ), panel = document.getElementById( CLASS + '-panel' ), form = document.getElementById( CLASS + '-form' ), - themeOption = document.getElementById( CLASS + '-theme' ), resetButton = document.getElementById( CLASS + '-resetbutton' ); if ( !panel.classList.contains( CLASS_PANEL_ACTIVE ) ) { panel.classList.add( CLASS_PANEL_ACTIVE ); toggle.setAttribute( 'aria-expanded', true ); form.addEventListener( 'input', setPref ); - // Some browser doesn't fire input events when checking radio buttons - themeOption.addEventListener( 'click', setPref ); resetButton.addEventListener( 'click', resetPref ); window.addEventListener( 'click', dismissOnClickOutside ); window.addEventListener( 'keydown', dismissOnEscape ); @@ -255,7 +244,6 @@ function togglePanel() { panel.classList.remove( CLASS_PANEL_ACTIVE ); toggle.setAttribute( 'aria-expanded', false ); form.removeEventListener( 'input', setPref ); - themeOption.removeEventListener( 'click', setPref ); resetButton.removeEventListener( 'click', resetPref ); window.removeEventListener( 'click', dismissOnClickOutside ); window.removeEventListener( 'keydown', dismissOnEscape ); @@ -270,10 +258,6 @@ function togglePanel() { function getMessages() { const keys = [ 'preferences', - 'prefs-citizen-theme-label', - 'prefs-citizen-theme-option-auto', - 'prefs-citizen-theme-option-light', - 'prefs-citizen-theme-option-dark', 'prefs-citizen-fontsize-label', 'prefs-citizen-pagewidth-label', 'prefs-citizen-lineheight-label', @@ -313,19 +297,10 @@ function initPanel( event ) { // TODO: Use ES6 template literals when RL does not screw up multiline const panel = template.render( data ).get()[ 1 ]; - // The priorities is as follow: - // 1. User-set theme (localStorage) - // 2. Site default theme (wgCitizenThemeDefault) - // 3. Fallback to auto - const currentTheme = prefValue.theme || - require( './config.json' ).wgCitizenThemeDefault || - 'auto'; - // Attach panel after button event.currentTarget.parentNode.insertBefore( panel, event.currentTarget.nextSibling ); // Set up initial state - document.getElementById( CLASS + '-theme__input__' + currentTheme ).checked = true; keys.forEach( ( key ) => { setIndicator( key, pref[ key ] ); setInputValue( key, prefValue[ key ] ); @@ -334,6 +309,30 @@ function initPanel( event ) { togglePanel(); event.currentTarget.addEventListener( 'click', togglePanel ); event.currentTarget.removeEventListener( 'click', initPanel ); + + const clientPreferenceSelector = '#citizen-client-prefs'; + const clientPreferenceExists = document.querySelectorAll( clientPreferenceSelector ).length > 0; + if ( clientPreferenceExists ) { + const clientPreferences = require( /** @type {string} */ ( './clientPreferences.js' ) ); + const clientPreferenceConfig = ( require( './clientPreferences.json' ) ); + + // Support legacy skin-citizen-* class + // TODO: Remove it in the future version after sufficient time + clientPreferenceConfig[ 'skin-theme' ].callback = () => { + const LEGACY_THEME_CLASSES = [ + 'skin-citizen-auto', + 'skin-citizen-light', + 'skin-citizen-dark' + ]; + const legacyThemeKey = Object.keys( CLIENTPREFS_THEME_MAP ).find( ( key ) => { + return CLIENTPREFS_THEME_MAP[ key ] === clientPrefs.get( 'skin-theme' ); + } ); + document.documentElement.classList.remove( ...LEGACY_THEME_CLASSES ); + document.documentElement.classList.add( `skin-citizen-${ legacyThemeKey }` ); + }; + + clientPreferences.render( clientPreferenceSelector, clientPreferenceConfig ); + } } /** diff --git a/resources/skins.citizen.preferences/skins.citizen.preferences.less b/resources/skins.citizen.preferences/skins.citizen.preferences.less index 5ef77c06a..ccc37a968 100644 --- a/resources/skins.citizen.preferences/skins.citizen.preferences.less +++ b/resources/skins.citizen.preferences/skins.citizen.preferences.less @@ -17,31 +17,15 @@ width: 100%; } - &__value { - font-weight: var( --font-weight-medium ); - color: var( --color-base--emphasized ); + &__title { + font-size: var( --font-size-x-small ); + color: var( --color-base--subtle ); + letter-spacing: 0.05em; } - } - &-theme { - &-option { - flex-grow: 1; - padding: 0.5rem 1rem; + &__value { font-weight: var( --font-weight-medium ); - text-align: center; - white-space: nowrap; - cursor: pointer; - border: 2px solid var( --border-color-base ); - border-radius: var( --border-radius--medium ); - - &:hover { - border-color: var( --color-primary--hover ); - box-shadow: var( --box-shadow-card ); - } - - &:active { - border-color: var( --color-primary--active ); - } + color: var( --color-base--emphasized ); } } @@ -89,39 +73,6 @@ margin: var( --space-md ) 0; } - &-theme { - &-option { - &-light { - color: ~'hsl( var( --color-primary__h ), 30%, 20% )'; - background: ~'hsl( var( --color-primary__h ), 25%, 94% )'; - } - - &-dark { - color: ~'hsl( var( --color-primary__h ), 10%, 75% )'; - background: ~'hsl( var( --color-primary__h ), 20%, 10% )'; - } - } - - fieldset { - display: flex; - gap: 0.5rem; - width: 100%; - padding: 0; - margin-top: 0.25rem; - } - - // Let label be the radio button - input { - display: none; - - &:checked { - + label { - border-color: var( --color-primary ); - } - } - } - } - &-resetbutton { width: 100%; padding: var( --space-sm ) var( --space-md ); @@ -145,6 +96,66 @@ } } +// New clientPrefs styles +.citizen-client-prefs { + &-radio { + &__input { + // Hide radio button because we use label as button + display: none; + + &:checked { + ~ .citizen-client-prefs-radio__label { + border-color: var( --color-primary ); + } + } + } + + &__label { + display: block; + padding: var( --space-xs ) var( --space-md ); + font-weight: var( --font-weight-medium ); + border: 2px solid var( --border-color-base ); + border-radius: var( --border-radius--medium ); + } + } +} + +#citizen-client-prefs { + .citizen-menu { + &__content { + padding: 0 var( --space-md ); + } + } +} + +#skin-client-prefs-skin-theme { + form { + display: grid; + // This is not the best because it does not adapt to the text length but will revisit later + grid-template-columns: 1fr 1fr; + gap: var( --space-xxs ); + text-align: center; + } + + label { + background: var( --color-surface-0 ); + + &[ for='skin-client-pref-skin-theme-value-day' ] { + // color-base of day theme + color: ~'hsl( var( --color-primary__h ), 30%, 20% )'; + // color-surface-0 of day theme + background: ~'hsl( var( --color-primary__h ), 30%, 96% )'; + } + + &[ for='skin-client-pref-skin-theme-value-night' ] { + // color-base of night theme + color: ~'hsl( var( --color-primary__h ), 25%, 80% )'; + // color-surface-0 of night theme + background: ~'hsl( var( --color-primary__h ), 20%, 10% )'; + } + } +} + @media ( hover: hover ) { .citizen-pref:hover .citizen-pref__button .citizen-ui-icon::before { transform: rotate3d( 0, 0, 1, 90deg ); diff --git a/resources/skins.citizen.preferences/templates/preferences.mustache b/resources/skins.citizen.preferences/templates/preferences.mustache index ed5dc10aa..98774e8bf 100644 --- a/resources/skins.citizen.preferences/templates/preferences.mustache +++ b/resources/skins.citizen.preferences/templates/preferences.mustache @@ -11,21 +11,8 @@