diff --git a/src/App.scss b/src/App.scss index 9225075b6..1145ded46 100644 --- a/src/App.scss +++ b/src/App.scss @@ -211,7 +211,7 @@ nav { .app-layout { --miniColumn: 25rem; --maxiColumn: 45rem; - --columnGap: 1em; + --columnGap: 1rem; --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); @@ -671,7 +671,8 @@ option { display: inline-flex; vertical-align: middle; - > * { + > *, + > * .button-default { --_roundness-left: 0; --_roundness-right: 0; @@ -679,11 +680,13 @@ option { flex: 1 1 auto; } - > *:first-child { + > *:first-child, + > *:first-child .button-default { --_roundness-left: var(--roundness); } - > *:last-child { + > *:last-child, + > *:last-child .button-default { --_roundness-right: var(--roundness); } } diff --git a/src/components/panel.style.js b/src/components/panel.style.js index ad16c18ff..1bba4766d 100644 --- a/src/components/panel.style.js +++ b/src/components/panel.style.js @@ -20,6 +20,16 @@ export default { 'Tab', 'ListItem' ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'Input', + 'PanelHeader', + 'Alert' + ], defaultRules: [ { directives: { diff --git a/src/components/root.style.js b/src/components/root.style.js index f9bdf16e8..4bd735aa5 100644 --- a/src/components/root.style.js +++ b/src/components/root.style.js @@ -12,6 +12,11 @@ export default { 'Alert', 'Button' // mobile post button ], + validInnerComponentsLite: [ + 'Underlay', + 'Scrollbar', + 'ScrollbarElement' + ], defaultRules: [ { directives: { diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js index d340fd00e..7ec22641f 100644 --- a/src/components/settings_modal/tabs/appearance_tab.js +++ b/src/components/settings_modal/tabs/appearance_tab.js @@ -6,6 +6,18 @@ import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue import FontControl from 'src/components/font_control/font_control.vue' +import { normalizeThemeData } from 'src/modules/interface' + +import { + getThemes +} from 'src/services/style_setter/style_setter.js' +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' +import { init } from 'src/services/theme_data/theme_data_3.service.js' +import { + getCssRules, + getScopedVersion +} from 'src/services/theme_data/css_utils.js' + import SharedComputedObject from '../helpers/shared_computed_object.js' import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' import { library } from '@fortawesome/fontawesome-svg-core' @@ -13,6 +25,8 @@ import { faGlobe } from '@fortawesome/free-solid-svg-icons' +import Preview from './theme_tab/preview.vue' + library.add( faGlobe ) @@ -20,6 +34,8 @@ library.add( const AppearanceTab = { data () { return { + availableStyles: [], + intersectionObserver: null, thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({ key: mode, value: mode, @@ -28,12 +44,12 @@ const AppearanceTab = { forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({ key: mode, value: i - 1, - label: this.$t(`settings.forced_roundness_mode_${mode}`) + label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`) })), underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({ key: mode, value: mode, - label: this.$t(`settings.underlay_override_mode_${mode}`) + label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`) })) } }, @@ -44,9 +60,61 @@ const AppearanceTab = { FloatSetting, UnitSetting, ProfileSettingIndicator, - FontControl + FontControl, + Preview + }, + mounted () { + getThemes() + .then((promises) => { + return Promise.all( + Object.entries(promises) + .map(([k, v]) => v.then(res => [k, res])) + ) + }) + .then(themes => themes.reduce((acc, [k, v]) => { + if (v) { + return [ + ...acc, + { + name: v.name || v[0], + key: k, + data: v + } + ] + } else { + return acc + } + }, [])) + .then((themesComplete) => { + this.availableStyles = themesComplete + }) + + if (window.IntersectionObserver) { + this.intersectionObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(({ target, isIntersecting }) => { + if (!isIntersecting) return + const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey) + this.$nextTick(() => { + if (theme) theme.ready = true + }) + observer.unobserve(target) + }) + }, { + root: this.$refs.themeList + }) + } + }, + updated () { + this.$nextTick(() => { + this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => { + this.intersectionObserver.observe(node) + }) + }) }, computed: { + noIntersectionObserver () { + return !window.IntersectionObserver + }, horizontalUnits () { return defaultHorizontalUnits }, @@ -76,7 +144,39 @@ const AppearanceTab = { this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) } }, + isCustomThemeUsed () { + const { theme } = this.mergedConfig + return theme === 'custom' || theme === null + }, ...SharedComputedObject() + }, + methods: { + isThemeActive (key) { + const { theme } = this.mergedConfig + console.log(key, theme) + return key === theme + }, + setTheme (name) { + this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true }) + }, + previewTheme (key, input) { + const style = normalizeThemeData(input) + const x = 2 + if (x === 1) return + const theme2 = convertTheme2To3(style) + const theme3 = init({ + inputRuleset: theme2, + ultimateBackgroundColor: '#000000', + liteMode: true, + debug: true, + onlyNormalState: true + }) + + return getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview-' + key + ).join('\n') + } } } diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue index d3df76a11..748a69ce7 100644 --- a/src/components/settings_modal/tabs/appearance_tab.vue +++ b/src/components/settings_modal/tabs/appearance_tab.vue @@ -1,5 +1,39 @@ + diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 7ca3b066e..39dc372ef 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -30,7 +30,10 @@ import { import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' import { init } from 'src/services/theme_data/theme_data_3.service.js' -import { getCssRules } from 'src/services/theme_data/css_utils.js' +import { + getCssRules, + getScopedVersion +} from 'src/services/theme_data/css_utils.js' import ColorInput from 'src/components/color_input/color_input.vue' import RangeInput from 'src/components/range_input/range_input.vue' @@ -499,18 +502,14 @@ export default { } }, setCustomTheme () { - this.$store.dispatch('setOption', { - name: 'customTheme', - value: { + this.$store.dispatch('setThemeV2', { + customTheme: { ignore: true, themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, ...this.previewTheme - } - }) - this.$store.dispatch('setOption', { - name: 'customThemeSource', - value: { + }, + customThemeSource: { themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, shadows: this.shadowsLocal, @@ -607,7 +606,7 @@ export default { normalizeLocalState (theme, version = 0, source, forceSource = false) { let input if (typeof source !== 'undefined') { - if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { + if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) { input = source version = source.themeEngineVersion } else { @@ -707,17 +706,10 @@ export default { liteMode: true }) - this.themeV3Preview = getCssRules(theme3.eager) - .map(x => { - if (x.startsWith('html')) { - return x.replace('html', '#theme-preview') - } else if (x.startsWith('#content')) { - return x.replace('#content', '#theme-preview') - } else { - return '#theme-preview > ' + x - } - }) - .join('\n') + this.themeV3Preview = getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview' + ).join('\n') } }, watch: { diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss index 4cb37c1e8..3ae8864cf 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -161,107 +161,6 @@ } } - .preview-container { - border-top: 1px dashed; - border-bottom: 1px dashed; - border-color: var(--border); - margin: 1em 0; - padding: 1em; - background-color: var(--wallpaper); - background-image: var(--body-background-image); - background-size: cover; - background-position: 50% 50%; - - .dummy { - .post { - font-family: var(--postFont); - display: flex; - - .content { - flex: 1; - - h4 { - margin-bottom: 0.25em; - } - - .icons { - margin-top: 0.5em; - display: flex; - - i { - margin-right: 1em; - } - } - } - } - - .after-post { - margin-top: 1em; - display: flex; - align-items: center; - } - - .avatar, - .avatar-alt { - background: - linear-gradient( - 135deg, - #b8e1fc 0%, - #a9d2f3 10%, - #90bae4 25%, - #90bcea 37%, - #90bff0 50%, - #6ba8e5 51%, - #a2daf5 83%, - #bdf3fd 100% - ); - color: black; - font-family: sans-serif; - text-align: center; - margin-right: 1em; - } - - .avatar-alt { - flex: 0 auto; - margin-left: 28px; - font-size: 12px; - min-width: 20px; - min-height: 20px; - line-height: 20px; - } - - .avatar { - flex: 0 auto; - width: 48px; - height: 48px; - font-size: 14px; - line-height: 48px; - } - - .actions { - display: flex; - align-items: baseline; - - .checkbox { - display: inline-flex; - align-items: baseline; - margin-right: 1em; - flex: 1; - } - } - - .separator { - margin: 1em; - border-bottom: 1px solid; - border-color: var(--border); - } - - .btn { - min-width: 3em; - } - } - } - .radius-item { flex-basis: auto; } @@ -314,10 +213,6 @@ max-width: 50em; } - .theme-preview-content { - padding: 20px; - } - .theme-warning { display: flex; align-items: baseline; diff --git a/src/components/status/post.style.js b/src/components/status/post.style.js index 8dce527ec..d0038424e 100644 --- a/src/components/status/post.style.js +++ b/src/components/status/post.style.js @@ -17,6 +17,15 @@ export default { 'Attachment', 'PollGraph' ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'ButtonUnstyled', + 'RichContent', + 'Avatar' + ], defaultRules: [ { directives: { diff --git a/src/i18n/en.json b/src/i18n/en.json index 770a4a658..c3357ef5a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -386,11 +386,6 @@ "navbar_size": "Top bar size", "panel_header_size": "Panel header size", "visual_tweaks": "Minor visual tweaks", - "force_interface_roundness": "Override interface roundness/sharpness", - "forced_roundness_mode_disabled": "Use theme defaults", - "forced_roundness_mode_sharp": "Force sharp edges", - "forced_roundness_mode_nonsharp": "Force not-so-sharp (1px roundness) edges", - "forced_roundness_mode_round": "Force round edges", "theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)", "scale_and_layout": "Interface scale and layout", "mfa": { @@ -745,10 +740,22 @@ "enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.", "more_settings": "More settings", "style": { + "custom_theme_used": "(Custom theme)", "themes2_outdated": "Editor for Themes V2 is no longer supported and presented here for sake of legacy.", "update_preview": "Update preview", "themes3": { "define": "Override", + "hacks": { + "underlay_overrides": "Change underlay", + "underlay_override_mode_none": "Theme default", + "underlay_override_mode_opaque": "Replace with solid color", + "underlay_override_mode_transparent": "Remove entirely (might break some themes)", + "force_interface_roundness": "Override interface roundness/sharpness", + "forced_roundness_mode_disabled": "Use theme defaults", + "forced_roundness_mode_sharp": "Force sharp edges", + "forced_roundness_mode_nonsharp": "Force not-so-sharp (1px roundness) edges", + "forced_roundness_mode_round": "Force round edges" + }, "font": { "group-builtin": "Browser default fonts", "builtin" : { diff --git a/src/modules/config.js b/src/modules/config.js index 56151d2a0..e54a883cb 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -35,11 +35,26 @@ export const multiChoiceProperties = [ export const defaultState = { expertLevel: 0, // used to track which settings to show and hide - colors: {}, - theme: undefined, - customTheme: undefined, - customThemeSource: undefined, - forceThemeRecompilation: false, + + // Theme stuff + theme: undefined, // Very old theme store, stores preset name, still in use + + // V1 + colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore + + // V2 + customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event. + customThemeSource: undefined, // "source", stores original theme data + + // V3 + themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions + forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists + palette: null, // not used yet, will be used for V3 + theme3hacks: { // Hacks, user overrides that are independent of theme used + underlay: 'none', + badgeColor: null + }, + hideISP: false, hideInstanceWallpaper: false, hideShoutbox: false, @@ -92,11 +107,6 @@ export const defaultState = { chatMention: true, polls: true }, - palette: null, - theme3hacks: { - underlay: 'none', - badgeColor: null - }, webPushNotifications: false, webPushAlwaysShowNotifications: false, muteWords: [], @@ -164,7 +174,6 @@ export const defaultState = { maxDepthInThread: undefined, // instance default autocompleteSelect: undefined, // instance default closingDrawerMarksAsSeen: undefined, // instance default - themeDebug: false, unseenAtTop: undefined, // instance default ignoreInactionableSeen: undefined // instance default } @@ -255,6 +264,12 @@ const config = { revert }) }, + setThemeV2 ({ commit, dispatch }, { customTheme, customThemeSource }) { + commit('setOption', { name: 'theme', value: 'custom' }) + commit('setOption', { name: 'customTheme', value: customTheme }) + commit('setOption', { name: 'customThemeSource', value: customThemeSource }) + dispatch('setTheme', { themeData: customThemeSource, recompile: true }) + }, setOption ({ commit, dispatch, state }, { name, value }) { const exceptions = new Set([ 'useStreamingApi' @@ -272,25 +287,22 @@ const config = { dispatch('disableMastoSockets') dispatch('setOption', { name: 'useStreamingApi', value: false }) }) + break } } } else { commit('setOption', { name, value }) - if ( - name.startsWith('theme3hacks') || - APPEARANCE_SETTINGS_KEYS.has(name) - ) { + if (APPEARANCE_SETTINGS_KEYS.has(name)) { applyConfig(state) } + if (name.startsWith('theme3hacks')) { + dispatch('setTheme', { recompile: true }) + } switch (name) { case 'theme': - dispatch('setTheme', { themeName: value, recompile: true }) + if (value === 'custom') break + dispatch('setTheme', { themeName: value, recompile: true, saveData: true }) break - case 'customTheme': - case 'customThemeSource': { - if (!value.ignore) dispatch('setTheme', { themeData: value }) - break - } case 'themeDebug': { dispatch('setTheme', { recompile: true }) break diff --git a/src/modules/instance.js b/src/modules/instance.js index 85a966b89..99b8b5d52 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -1,6 +1,3 @@ -import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js' -import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js' -import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js' @@ -45,7 +42,7 @@ const defaultState = { registrationOpen: true, server: 'http://localhost:4040/', textlimit: 5000, - themeData: undefined, + themeData: undefined, // used for theme editor v2 vapidPublicKey: undefined, // Stuff from static/config.json @@ -375,88 +372,6 @@ const instance = { console.warn(e) } }, - - setTheme ({ commit, state, rootState }, { themeName, themeData, recompile } = {}) { - // const { - // themeApplied - // } = rootState.interface - const { - theme: instanceThemeName - } = state - - const { - customTheme: userThemeSnapshot, - customThemeSource: userThemeSource, - forceThemeRecompilation, - themeDebug - } = rootState.config - - const forceRecompile = forceThemeRecompilation || recompile - - // If we're not not forced to recompile try using - // cache (tryLoadCache return true if load successful) - if (!forceRecompile && !themeDebug && tryLoadCache()) { - commit('setThemeApplied') - } - - const normalizeThemeData = (themeData) => { - console.log('NORMAL', themeData) - if (themeData.themeFileVerison === 1) { - return generatePreset(themeData).theme - } - // New theme presets don't have 'theme' property, they use 'source' - const themeSource = themeData.source - - let out // shout, shout let it all out - if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) { - out = themeSource || themeData - } else { - out = themeData.theme - } - - // generatePreset here basically creates/updates "snapshot", - // while also fixing the 2.2 -> 2.3 colors/shadows/etc - return generatePreset(out).theme - } - - let promise = null - - if (themeName) { - // commit('setInstanceOption', { name: 'theme', value: themeName }) - promise = getPreset(themeName) - .then(themeData => { - // commit('setInstanceOption', { name: 'themeData', value: themeData }) - return normalizeThemeData(themeData) - }) - } else if (themeData) { - promise = Promise.resolve(normalizeThemeData(themeData)) - } else { - if (userThemeSource || userThemeSnapshot) { - if (userThemeSource && userThemeSource.themeEngineVersion === CURRENT_VERSION) { - promise = Promise.resolve(normalizeThemeData(userThemeSource)) - } else { - promise = Promise.resolve(normalizeThemeData(userThemeSnapshot)) - } - } else if (instanceThemeName) { - promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData)) - } - } - - promise - .then(realThemeData => { - console.log('FR FR 1', realThemeData) - const ruleset = convertTheme2To3(realThemeData) - console.log('FR FR 2', ruleset) - - applyTheme( - ruleset, - () => commit('setThemeApplied'), - themeDebug - ) - }) - - return promise - }, fetchEmoji ({ dispatch, state }) { if (!state.customEmojiFetched) { state.customEmojiFetched = true diff --git a/src/modules/interface.js b/src/modules/interface.js index 2ccfeef35..92f3e9ac1 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -1,3 +1,7 @@ +import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js' +import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js' +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' + const defaultState = { localFonts: null, themeApplied: false, @@ -207,8 +211,141 @@ const interfaceMod = { }, setLastTimeline ({ commit }, value) { commit('setLastTimeline', value) + }, + setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) { + const { + theme: instanceThemeName + } = rootState.instance + + const { + theme: userThemeName, + customTheme: userThemeSnapshot, + customThemeSource: userThemeSource, + forceThemeRecompilation, + themeDebug, + theme3hacks + } = rootState.config + + const actualThemeName = userThemeName || instanceThemeName + + const forceRecompile = forceThemeRecompilation || recompile + + let promise = null + + if (themeData) { + promise = Promise.resolve(normalizeThemeData(themeData)) + } else if (themeName) { + promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData)) + } else if (userThemeSource || userThemeSnapshot) { + if (userThemeSource && userThemeSource.themeEngineVersion === CURRENT_VERSION) { + promise = Promise.resolve(normalizeThemeData(userThemeSource)) + } else { + promise = Promise.resolve(normalizeThemeData(userThemeSnapshot)) + } + } else if (actualThemeName && actualThemeName !== 'custom') { + promise = getPreset(actualThemeName).then(themeData => { + const realThemeData = normalizeThemeData(themeData) + if (actualThemeName === instanceThemeName) { + // This sole line is the reason why this whole block is above the recompilation check + commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } }) + } + return realThemeData + }) + } else { + throw new Error('Cannot load any theme!') + } + + // If we're not not forced to recompile try using + // cache (tryLoadCache return true if load successful) + if (!forceRecompile && !themeDebug && tryLoadCache()) { + commit('setThemeApplied') + return + } + + promise + .then(realThemeData => { + const theme2ruleset = convertTheme2To3(realThemeData) + + if (saveData) { + commit('setOption', { name: 'theme', value: themeName || actualThemeName }) + commit('setOption', { name: 'customTheme', value: realThemeData }) + commit('setOption', { name: 'customThemeSource', value: realThemeData }) + } + const hacks = [] + + Object.entries(theme3hacks).forEach(([key, value]) => { + switch (key) { + case 'underlay': { + if (value !== 'none') { + const newRule = { + component: 'Underlay', + directives: {} + } + if (value === 'opaque') { + newRule.directives.opacity = 1 + newRule.directives.background = '--wallpaper' + } + if (value === 'transparent') { + newRule.directives.opacity = 0 + } + console.log('NEW RULE', newRule) + hacks.push(newRule) + } + break + } + } + }) + + const ruleset = [ + ...theme2ruleset, + ...hacks + ] + + applyTheme( + ruleset, + () => commit('setThemeApplied'), + themeDebug + ) + }) + + return promise } } } export default interfaceMod + +export const normalizeThemeData = (input) => { + let themeData = input + + if (Array.isArray(themeData)) { + themeData = { colors: {} } + themeData.colors.bg = input[1] + themeData.colors.fg = input[2] + themeData.colors.text = input[3] + themeData.colors.link = input[4] + themeData.colors.cRed = input[5] + themeData.colors.cGreen = input[6] + themeData.colors.cBlue = input[7] + themeData.colors.cOrange = input[8] + return generatePreset(themeData).theme + } + + if (themeData.themeFileVerison === 1) { + return generatePreset(themeData).theme + } + + // New theme presets don't have 'theme' property, they use 'source' + const themeSource = themeData.source + + let out // shout, shout let it all out + if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) { + out = themeSource || themeData + } else { + out = themeData.theme + } + + // generatePreset here basically creates/updates "snapshot", + // while also fixing the 2.2 -> 2.3 colors/shadows/etc + return generatePreset(out).theme +} diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js index 8423e8ace..9bce48340 100644 --- a/src/services/theme_data/css_utils.js +++ b/src/services/theme_data/css_utils.js @@ -159,3 +159,15 @@ export const getCssRules = (rules, debug) => rules.map(rule => { footer ].join('\n') }).filter(x => x) + +export const getScopedVersion = (rules, newScope) => { + return rules.map(x => { + if (x.startsWith('html')) { + return x.replace('html', newScope) + } else if (x.startsWith('#content')) { + return x.replace('#content', newScope) + } else { + return newScope + ' > ' + x + } + }) +} diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js index 83ca82423..75f8dd93c 100644 --- a/src/services/theme_data/iss_utils.js +++ b/src/services/theme_data/iss_utils.js @@ -39,7 +39,23 @@ export const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector +/** + * Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) + * selector. + * + * "path" here refers to "fake" selector that cannot be actually used in UI but is used for internal + * purposes + * + * @param {Object} components - object containing all components definitions + * + * @returns {Function} + * @param {Object} rule - rule in question to convert to CSS selector + * @param {boolean} ignoreOutOfTreeSelector - wthether to ignore aformentioned field in + * component definition and use selector + * @param {boolean} isParent - (mostly) internal argument used when recursing + * + * @returns {String} CSS selector (or path) + */ export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { if (!rule && !isParent) return null const component = components[rule.component] @@ -79,6 +95,17 @@ export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelecto return selectors.trim() } +/** + * Check if combination matches + * + * @param {Object} criteria - criteria to match against + * @param {Object} subject - rule/combination to check match + * @param {boolean} strict - strict checking: + * By default every variant and state inherits from "normal" state/variant + * so when checking if combination matches, it WILL match against "normal" + * state/variant. In strict mode inheritance is ignored an "normal" does + * not match + */ export const combinationsMatch = (criteria, subject, strict) => { if (criteria.component !== subject.component) return false @@ -101,6 +128,15 @@ export const combinationsMatch = (criteria, subject, strict) => { return true } +/** + * Search for rule that matches `criteria` in set of rules + * meant to be used in a ruleset.filter() function + * + * @param {Object} criteria - criteria to search for + * @param {boolean} strict - whether search strictly or not (see combinationsMatch) + * + * @return function that returns true/false if subject matches + */ export const findRules = (criteria, strict) => subject => { // If we searching for "general" rules - ignore "specific" ones if (criteria.parent === null && !!subject.parent) return false @@ -125,6 +161,7 @@ export const findRules = (criteria, strict) => subject => { return true } +// Pre-fills 'normal' state/variant if missing export const normalizeCombination = rule => { rule.variant = rule.variant ?? 'normal' rule.state = [...new Set(['normal', ...(rule.state || [])])] diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js index b54366bd8..95eb03c19 100644 --- a/src/services/theme_data/theme2_to_theme3.js +++ b/src/services/theme_data/theme2_to_theme3.js @@ -12,7 +12,9 @@ export const basePaletteKeys = new Set([ 'cBlue', 'cRed', 'cGreen', - 'cOrange' + 'cOrange', + + 'wallpaper' ]) export const fontsKeys = new Set([ diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index e802a893d..cf58da119 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -149,11 +149,30 @@ const ruleToSelector = genericRuleToSelector(components) export const getEngineChecksum = () => engineChecksum +/** + * Initializes and compiles the theme according to the ruleset + * + * @param {Object[]} inputRuleset - set of rules to compile theme against. Acts as an override to + * component default rulesets + * @param {string} ultimateBackgroundColor - Color that will be the "final" background for + * calculating contrast ratios and making text automatically accessible. Really used for cases when + * stuff is transparent. + * @param {boolean} debug - print out debug information in console, mostly just performance stuff + * @param {boolean} liteMode - use validInnerComponentsLite instead of validInnerComponents, meant to + * generatate theme previews and such that need to be compiled faster and don't require a lot of other + * components present in "normal" mode + * @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme + * previews since states are the biggest factor for compilation time and are completely unnecessary + * when previewing multiple themes at same time + * @param {string} rootComponentName - [UNTESTED] which component to start from, meant for previewing a + * part of the theme (i.e. just the button) for themes 3 editor. + */ export const init = ({ inputRuleset, ultimateBackgroundColor, debug = false, liteMode = false, + onlyNormalState = false, rootComponentName = 'Root' }) => { if (!inputRuleset) throw new Error('Ruleset is null or undefined!') @@ -402,11 +421,16 @@ export const init = ({ const processInnerComponent = (component, parent) => { const combinations = [] const { - validInnerComponents = [], states: originalStates = {}, variants: originalVariants = {} } = component + const validInnerComponents = ( + liteMode + ? (component.validInnerComponentsLite || component.validInnerComponents) + : component.validInnerComponents + ) || [] + // Normalizing states and variants to always include "normal" const states = { normal: '', ...originalStates } const variants = { normal: '', ...originalVariants } @@ -418,22 +442,26 @@ export const init = ({ // Optimization: we only really need combinations without "normal" because all states implicitly have it const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal') - const stateCombinations = [ - ['normal'], - ...getAllPossibleCombinations(permutationStateKeys) - .map(combination => ['normal', ...combination]) - .filter(combo => { - // Optimization: filter out some hard-coded combinations that don't make sense - if (combo.indexOf('disabled') >= 0) { - return !( - combo.indexOf('hover') >= 0 || - combo.indexOf('focused') >= 0 || - combo.indexOf('pressed') >= 0 - ) - } - return true - }) - ] + const stateCombinations = onlyNormalState + ? [ + ['normal'] + ] + : [ + ['normal'], + ...getAllPossibleCombinations(permutationStateKeys) + .map(combination => ['normal', ...combination]) + .filter(combo => { + // Optimization: filter out some hard-coded combinations that don't make sense + if (combo.indexOf('disabled') >= 0) { + return !( + combo.indexOf('hover') >= 0 || + combo.indexOf('focused') >= 0 || + combo.indexOf('pressed') >= 0 + ) + } + return true + }) + ] const stateVariantCombination = Object.keys(variants).map(variant => { return stateCombinations.map(state => ({ variant, state })) @@ -460,7 +488,9 @@ export const init = ({ const t0 = performance.now() const combinations = processInnerComponent(components[rootComponentName] ?? components.Root) const t1 = performance.now() - console.debug('Tree traveral took ' + (t1 - t0) + ' ms') + if (debug) { + console.debug('Tree traveral took ' + (t1 - t0) + ' ms') + } const result = combinations.map((combination) => { if (combination.lazy) { @@ -470,7 +500,9 @@ export const init = ({ } }).filter(x => x) const t2 = performance.now() - console.debug('Eager processing took ' + (t2 - t1) + ' ms') + if (debug) { + console.debug('Eager processing took ' + (t2 - t1) + ' ms') + } return { lazy: result.filter(x => typeof x === 'function'),