From 521d308a6c6777a45c94183751f3305ce23bdad3 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 18 Jan 2024 14:35:25 +0200 Subject: themes 3 initial work --- src/components/icon.style.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/components/icon.style.js (limited to 'src/components/icon.style.js') diff --git a/src/components/icon.style.js b/src/components/icon.style.js new file mode 100644 index 00000000..1e2781d6 --- /dev/null +++ b/src/components/icon.style.js @@ -0,0 +1,3 @@ +export default { + name: 'Icon' +} -- cgit v1.2.3-70-g09d2 From 0729b529d7da2002f25039e1dad2732302009cf3 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jan 2024 00:43:46 +0200 Subject: some more stuff, generating CSS selectors from rules --- src/components/button.style.js | 1 + src/components/icon.style.js | 3 +- src/components/panel.style.js | 1 + src/components/text.style.js | 4 +- src/components/underlay.style.js | 3 +- src/services/theme_data/pleromafe.t3.js | 26 +++++++++ src/services/theme_data/theme_data_3.service.js | 62 +++++++++++++++++----- .../specs/services/theme_data/theme_data3.spec.js | 26 +++++++++ 8 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 src/services/theme_data/pleromafe.t3.js create mode 100644 test/unit/specs/services/theme_data/theme_data3.spec.js (limited to 'src/components/icon.style.js') diff --git a/src/components/button.style.js b/src/components/button.style.js index 8f2e8f82..1c229f43 100644 --- a/src/components/button.style.js +++ b/src/components/button.style.js @@ -1,5 +1,6 @@ export default { name: 'Button', + selector: '.btn', states: { hover: ':hover', disabled: ':disabled', diff --git a/src/components/icon.style.js b/src/components/icon.style.js index 1e2781d6..732cf16f 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -1,3 +1,4 @@ export default { - name: 'Icon' + name: 'Icon', + selector: '.icon' } diff --git a/src/components/panel.style.js b/src/components/panel.style.js index 1666d923..d34d5434 100644 --- a/src/components/panel.style.js +++ b/src/components/panel.style.js @@ -1,5 +1,6 @@ export default { name: 'Panel', + selector: '.panel', validInnerComponents: [ 'Text', 'Icon', diff --git a/src/components/text.style.js b/src/components/text.style.js index f87268bb..2aa5e745 100644 --- a/src/components/text.style.js +++ b/src/components/text.style.js @@ -1,9 +1,7 @@ export default { name: 'Text', + selector: '', states: { faint: '.faint' - }, - variants: { - green: '/.greentext' } } diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js index bae9fc0b..426df1d7 100644 --- a/src/components/underlay.style.js +++ b/src/components/underlay.style.js @@ -1,5 +1,6 @@ export default { - name: 'Panel', + name: 'Underlay', + selector: '.underlay', validInnerComponents: [ 'Panel' ] diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js new file mode 100644 index 00000000..b44a395e --- /dev/null +++ b/src/services/theme_data/pleromafe.t3.js @@ -0,0 +1,26 @@ +export const sampleRules = [ + { + component: 'Underlay', + // variant: 'normal', + // state: 'normal' + directives: { + background: '#000', + opacity: 0.2 + } + }, + { + component: 'Panel', + directives: { + background: '#FFFFFF', + opacity: 0.9 + } + }, + { + component: 'Button', + directives: { + background: '#808080', + text: '#FFFFFF', + opacity: 0.5 + } + } +] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 3a6fd552..39a9998d 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -14,7 +14,7 @@ const components = { } // This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations -const getAllPossibleCombinations = (array) => { +export const getAllPossibleCombinations = (array) => { const combos = [array.map(x => [x])] for (let comboSize = 2; comboSize <= array.length; comboSize++) { const previous = combos[combos.length - 1] @@ -30,15 +30,52 @@ const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -export const init = () => { +export const ruleToSelector = (rule) => { + const component = components[rule.component] + const { states, variants, selector } = component + + const applicableStates = (rule.state.filter(x => x !== 'normal') || []).map(state => states[state]) + + const applicableVariantName = (rule.variant || 'normal') + let applicableVariant = '' + if (applicableVariantName !== 'normal') { + applicableVariant = variants[applicableVariantName] + } + + const selectors = [selector, applicableVariant, ...applicableStates] + .toSorted((a, b) => { + if (a.startsWith(':')) return 1 + else return -1 + }) + .join('') + + if (rule.parent) { + return ruleToSelector(rule.parent) + ' ' + selectors + } + return selectors +} + +export const init = (ruleset) => { const rootName = root.name const rules = [] + const rulesByComponent = {} + + const addRule = (rule) => { + rules.push(rule) + rulesByComponent[rule.component] = rulesByComponent[rule.component] || [] + rulesByComponent.push(rule) + } + + ruleset.forEach(rule => { + + }) const processInnerComponent = (component, parent) => { const { - validInnerComponents, + validInnerComponents = [], states: originalStates = {}, - variants: originalVariants = {} + variants: originalVariants = {}, + name } = component const states = { normal: '', ...originalStates } @@ -51,16 +88,17 @@ export const init = () => { }).reduce((acc, x) => [...acc, ...x], []) stateVariantCombination.forEach(combination => { - rules.push(({ - parent, - component: component.name, - state: combination.state, - variant: combination.variant - })) - - innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, combination)) + // addRule(({ + // parent, + // component: component.name, + // state: combination.state, + // variant: combination.variant + // })) + + innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, { parent, component: name, ...combination })) }) } processInnerComponent(components[rootName]) + return rules } diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js new file mode 100644 index 00000000..e76200c0 --- /dev/null +++ b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -0,0 +1,26 @@ +// import { topoSort } from 'src/services/theme_data/theme_data.service.js' +import { + getAllPossibleCombinations, + init, + ruleToSelector +} from 'src/services/theme_data/theme_data_3.service.js' +import { + sampleRules +} from 'src/services/theme_data/pleromafe.t3.js' + +describe.only('Theme Data 3', () => { + describe('getAllPossibleCombinations', () => { + it('test simple case', () => { + const out = getAllPossibleCombinations([1, 2, 3]).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([[1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]]) + }) + }) + + describe('init', () => { + it('test simple case', () => { + const out = init(sampleRules) + console.log(JSON.stringify(out, null, 2)) + out.forEach(r => console.log(ruleToSelector(r))) + }) + }) +}) -- cgit v1.2.3-70-g09d2 From 53a4b1f9a6a9aa6bc044609c3accb074d924daf9 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 31 Jan 2024 17:39:51 +0200 Subject: better virtual components and stuff --- src/App.scss | 28 +- src/components/icon.style.js | 13 +- src/components/link.style.js | 25 ++ src/components/notification/notification.vue | 2 +- src/components/notifications/notifications.scss | 16 +- src/components/panel.style.js | 1 + src/components/panel_header.style.js | 1 + src/components/status/status.vue | 2 +- src/components/status_body/status_body.scss | 5 +- src/components/text.style.js | 38 ++- src/components/underlay.style.js | 3 +- src/panel.scss | 11 - src/services/color_convert/color_convert.js | 3 +- src/services/style_setter/style_setter.js | 19 +- src/services/theme_data/pleromafe.t3.js | 16 +- src/services/theme_data/theme_data_3.service.js | 304 ++++++++++++++++----- .../specs/services/theme_data/theme_data3.spec.js | 2 +- 17 files changed, 353 insertions(+), 136 deletions(-) create mode 100644 src/components/link.style.js (limited to 'src/components/icon.style.js') diff --git a/src/App.scss b/src/App.scss index ef68ac50..8e9f3171 100644 --- a/src/App.scss +++ b/src/App.scss @@ -24,8 +24,7 @@ body { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); margin: 0; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overscroll-behavior-y: none; @@ -111,8 +110,7 @@ body { a { text-decoration: none; - color: $fallback--link; - color: var(--link, $fallback--link); + color: var(--link); } h4 { @@ -128,8 +126,7 @@ h4 { i[class*="icon-"], .svg-inline--fa, .iconLetter { - color: $fallback--icon; - color: var(--icon, $fallback--icon); + color: var(--icon); } .button-unstyled:hover, @@ -763,17 +760,11 @@ option { } .faint { - color: $fallback--faint; - color: var(--faint, $fallback--faint); -} - -.faint-link { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + --text: var(--textFaint); + --textGreentext: var(--textGreentextFaint); + --link: var(--linkFaint); - &:hover { - text-decoration: underline; - } + color: var(--text); } .visibility-notice { @@ -816,6 +807,11 @@ option { opacity: 0.25; } +.timeago { + --link: var(--text); + --linkFaint: var(--textFaint); +} + .login-hint { text-align: center; diff --git a/src/components/icon.style.js b/src/components/icon.style.js index 732cf16f..adc72fc5 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -1,4 +1,15 @@ export default { name: 'Icon', - selector: '.icon' + virtual: true, + selector: '.svg-inline--fa', + defaultRules: [ + { + component: 'Icon', + directives: { + textColor: '--text', + textOpacity: 0.5, + textOpacityMode: 'mixrgb' + } + } + ] } diff --git a/src/components/link.style.js b/src/components/link.style.js new file mode 100644 index 00000000..0128fd91 --- /dev/null +++ b/src/components/link.style.js @@ -0,0 +1,25 @@ +export default { + name: 'Link', + selector: 'a', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + component: 'Link', + directives: { + textColor: '--link' + } + }, + { + component: 'Link', + state: ['faint'], + directives: { + textColor: '--link', + textOpacity: 0.5, + textOpacityMode: 'fake' + } + } + ] +} diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index a8eceab0..5c425200 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -155,7 +155,7 @@ .button-default { flex-shrink: 0; diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index 23e4ca61..d92bbbe0 100644 --- a/src/services/color_convert/color_convert.js +++ b/src/services/color_convert/color_convert.js @@ -173,7 +173,7 @@ export const mixrgb = (a, b) => { * @returns {String} CSS rgba() color */ export const rgba2css = function (rgba) { - return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})` + return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a ?? 1})` } /** @@ -188,7 +188,6 @@ export const rgba2css = function (rgba) { */ export const getTextColor = function (bg, text, preserve) { const contrast = getContrastRatio(bg, text) - console.log(contrast) if (contrast < 4.5) { const base = typeof text.a !== 'undefined' ? { a: text.a } : {} diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 43fe3c73..ba98c4d9 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,10 +1,15 @@ import { convert } from 'chromatism' import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js' import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js' +import { init } from '../theme_data/theme_data_3.service.js' +import { + sampleRules +} from 'src/services/theme_data/pleromafe.t3.js' import { defaultState } from '../../modules/config.js' export const applyTheme = (input) => { - const { rules } = generatePreset(input) + const { rules, t3b } = generatePreset(input) + const themes3 = init(sampleRules, t3b) const head = document.head const body = document.body body.classList.add('hidden') @@ -18,6 +23,10 @@ export const applyTheme = (input) => { styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') + themes3.css.forEach(rule => { + console.log(rule) + styleSheet.insertRule(rule, 'index-max') + }) body.classList.remove('hidden') } @@ -326,7 +335,7 @@ export const generateShadows = (input, colors) => { } } -export const composePreset = (colors, radii, shadows, fonts) => { +export const composePreset = (colors, radii, shadows, fonts, t3b) => { return { rules: { ...shadows.rules, @@ -339,7 +348,8 @@ export const composePreset = (colors, radii, shadows, fonts) => { ...colors.theme, ...radii.theme, ...fonts.theme - } + }, + t3b } } @@ -349,7 +359,8 @@ export const generatePreset = (input) => { colors, generateRadii(input), generateShadows(input, colors.theme.colors, colors.mod), - generateFonts(input) + generateFonts(input), + colors.theme.colors ) } diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js index 8d8e19cd..9fadf0ee 100644 --- a/src/services/theme_data/pleromafe.t3.js +++ b/src/services/theme_data/pleromafe.t3.js @@ -11,30 +11,30 @@ export const sampleRules = [ { component: 'Panel', directives: { - background: '#FFFFFF', - opacity: 0.9 + background: '--fg' + // opacity: 0.9 } }, { component: 'PanelHeader', directives: { - background: '#000000', - opacity: 0.9 + background: '--fg' + // opacity: 0.9 } }, { component: 'Button', directives: { - background: '#000000', - opacity: 0.8 + background: '--fg' + // opacity: 0.8 } }, { component: 'Button', state: ['hover'], directives: { - background: '#FF00FF', - opacity: 0.9 + background: '#FFFFFF' + // opacity: 0.9 } } ] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 2ef5e17f..c9468d07 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -1,11 +1,12 @@ import { convert } from 'chromatism' -import { alphaBlend, getTextColor, rgba2css } from '../color_convert/color_convert.js' +import { alphaBlend, getTextColor, rgba2css, mixrgb } from '../color_convert/color_convert.js' import Underlay from 'src/components/underlay.style.js' import Panel from 'src/components/panel.style.js' import PanelHeader from 'src/components/panel_header.style.js' import Button from 'src/components/button.style.js' import Text from 'src/components/text.style.js' +import Link from 'src/components/link.style.js' import Icon from 'src/components/icon.style.js' const root = Underlay @@ -15,6 +16,7 @@ const components = { PanelHeader, Button, Text, + Link, Icon } @@ -35,9 +37,9 @@ export const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -export const ruleToSelector = (rule) => { +export const ruleToSelector = (rule, isParent) => { const component = components[rule.component] - const { states, variants, selector } = component + const { states, variants, selector, outOfTreeSelector } = component const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) @@ -47,56 +49,104 @@ export const ruleToSelector = (rule) => { applicableVariant = variants[applicableVariantName] } - const selectors = [selector, applicableVariant, ...applicableStates] + let realSelector + if (isParent) { + realSelector = selector + } else { + if (outOfTreeSelector) realSelector = outOfTreeSelector + else realSelector = selector + } + + const selectors = [realSelector, applicableVariant, ...applicableStates] .toSorted((a, b) => { if (a.startsWith(':')) return 1 - else return -1 + if (!a.startsWith('.')) return -1 + else return 0 }) .join('') if (rule.parent) { - return ruleToSelector(rule.parent) + ' ' + selectors + return ruleToSelector(rule.parent, true) + ' ' + selectors } return selectors } -export const init = (ruleset) => { +export const init = (extraRuleset, palette) => { const rootName = root.name const rules = [] const rulesByComponent = {} + const ruleset = [ + ...Object.values(components).map(c => c.defaultRules || []).reduce((acc, arr) => [...acc, ...arr], []), + ...extraRuleset + ] + const addRule = (rule) => { rules.push(rule) rulesByComponent[rule.component] = rulesByComponent[rule.component] || [] rulesByComponent[rule.component].push(rule) } - const findRules = (combination) => rule => { - if (combination.component !== rule.component) return false - if (Object.prototype.hasOwnProperty.call(rule, 'variant')) { - if (combination.variant !== rule.variant) return false - } else { - if (combination.variant !== 'normal') return false + const findRules = (combination, parent) => rule => { + // inexact search + const doesCombinationMatch = () => { + if (combination.component !== rule.component) return false + if (Object.prototype.hasOwnProperty.call(rule, 'variant')) { + if (combination.variant !== rule.variant) return false + } else { + if (combination.variant !== 'normal') return false + } + + if (Object.prototype.hasOwnProperty.call(rule, 'state')) { + const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) + const combinationSet = new Set(['normal', ...combination.state]) + const setsAreEqual = combination.state.every(state => ruleStatesSet.has(state)) && + [...ruleStatesSet].every(state => combinationSet.has(state)) + return setsAreEqual + } else { + if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false + return true + } } + const combinationMatches = doesCombinationMatch() + if (!parent || !combinationMatches) return combinationMatches - if (Object.prototype.hasOwnProperty.call(rule, 'state')) { - const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) - const combinationSet = new Set(['normal', ...combination.state]) - const setsAreEqual = combination.state.every(state => ruleStatesSet.has(state)) && - [...ruleStatesSet].every(state => combinationSet.has(state)) - return setsAreEqual - } else { - if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false - return true + // exact search + + // unroll parents into array + const unroll = (item) => { + const out = [] + let currentParent = item.parent + while (currentParent) { + const { parent: newParent, ...rest } = currentParent + out.push(rest) + currentParent = newParent + } + return out } + const { parent: _, ...rest } = parent + const pathSearch = [rest, ...unroll(parent)] + const pathRule = unroll(rule) + if (pathSearch.length !== pathRule.length) return false + const pathsMatch = pathSearch.every((searchRule, i) => { + const existingRule = pathRule[i] + if (existingRule.component !== searchRule.component) return false + if (existingRule.variant !== searchRule.variant) return false + const existingRuleStatesSet = new Set(['normal', ...(existingRule.state || [])]) + const searchStatesSet = new Set(['normal', ...(searchRule.state || [])]) + const setsAreEqual = existingRule.state.every(state => searchStatesSet.has(state)) && + [...searchStatesSet].every(state => existingRuleStatesSet.has(state)) + return setsAreEqual + }) + return pathsMatch } const findLowerLevelRule = (parent, filter = () => true) => { let lowerLevelComponent = null let currentParent = parent while (currentParent) { - const rulesParent = ruleset.filter(findRules(currentParent, true)) - rulesParent > 1 && console.log('OOPS') + const rulesParent = ruleset.filter(findRules(currentParent)) + rulesParent > 1 && console.warn('OOPS') lowerLevelComponent = rulesParent[rulesParent.length - 1] currentParent = currentParent.parent if (lowerLevelComponent && filter(lowerLevelComponent)) currentParent = null @@ -104,6 +154,35 @@ export const init = (ruleset) => { return filter(lowerLevelComponent) ? lowerLevelComponent : null } + const findColor = (color) => { + if (typeof color === 'string' && color.startsWith('--')) { + const name = color.substring(2) + return palette[name] + } + return color + } + + const getTextColorAlpha = (rule, lowerRule, value) => { + const opacity = rule.directives.textOpacity + const textColor = convert(findColor(value)).rgb + if (opacity === null || opacity === undefined || opacity >= 1) { + return convert(textColor).hex + } + const backgroundColor = convert(lowerRule.cache.background).rgb + if (opacity === 0) { + return convert(backgroundColor).hex + } + const opacityMode = rule.directives.textOpacityMode + switch (opacityMode) { + case 'fake': + return convert(alphaBlend(textColor, opacity, backgroundColor)).hex + case 'mixrgb': + return convert(mixrgb(backgroundColor, textColor)).hex + default: + return rgba2css({ a: opacity, ...textColor }) + } + } + const processInnerComponent = (component, parent) => { const { validInnerComponents = [], @@ -124,79 +203,156 @@ export const init = (ruleset) => { const VIRTUAL_COMPONENTS = new Set(['Text', 'Link', 'Icon']) stateVariantCombination.forEach(combination => { - const existingRules = ruleset.filter(findRules({ component: component.name, ...combination })) - const lastRule = existingRules[existingRules.length - 1] + let needRuleAdd = false - if (existingRules.length !== 0) { - const { directives } = lastRule - const rgb = convert(directives.background).rgb + if (VIRTUAL_COMPONENTS.has(component.name)) { + const selector = component.name + ruleToSelector({ component: component.name, ...combination }) + const virtualName = [ + '--', + component.name.toLowerCase(), + combination.variant === 'normal' + ? '' + : combination.variant[0].toUpperCase() + combination.variant.slice(1).toLowerCase(), + ...combination.state.filter(x => x !== 'normal').toSorted().map(state => state[0].toUpperCase() + state.slice(1).toLowerCase()) + ].join('') - // TODO: DEFAULT TEXT COLOR - const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb + const lowerLevel = findLowerLevelRule(parent, (r) => { + if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false + if (r.cache.background === undefined) return false + if (r.cache.textDefined) { + return !r.cache.textDefined[selector] + } + return true + }) - if (!lastRule.cache?.background) { - const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb - lastRule.cache = lastRule.cache || {} - lastRule.cache.background = blend + if (!lowerLevel) return + + let inheritedTextColorRule + const inheritedTextColorRules = findLowerLevelRule(parent, (r) => { + return r.cache?.textDefined?.[selector] + }) + + if (!inheritedTextColorRule) { + const generalTextColorRules = ruleset.filter(findRules({ component: component.name, ...combination }, null, true)) + inheritedTextColorRule = generalTextColorRules[generalTextColorRules.length - 1] + } else { + inheritedTextColorRule = inheritedTextColorRules[inheritedTextColorRules.length - 1] + } - addRule(lastRule) + let inheritedTextColor + let inheritedTextOpacity = {} + if (inheritedTextColorRule) { + inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor) + // also inherit opacity settings + const { textOpacity, textOpacityMode } = inheritedTextColorRule.directives + inheritedTextOpacity = { textOpacity, textOpacityMode } + } else { + // Emergency fallback + inheritedTextColor = '#000000' } + + const textColor = getTextColor( + convert(lowerLevel.cache.background).rgb, + convert(inheritedTextColor).rgb, + component.name === 'Link' // make it configurable? + ) + + lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} + lowerLevel.cache.textDefined[selector] = textColor + lowerLevel.virtualDirectives = lowerLevel.virtualDirectives || {} + lowerLevel.virtualDirectives[virtualName] = getTextColorAlpha(inheritedTextColorRule, lowerLevel, textColor) + + const directives = { + textColor, + ...inheritedTextOpacity + } + + // Debug: lets you see what it think background color should be + directives.background = convert(lowerLevel.cache.background).hex + + addRule({ + parent, + virtual: true, + component: component.name, + ...combination, + cache: { background: lowerLevel.cache.background }, + directives + }) } else { - if (VIRTUAL_COMPONENTS.has(component.name)) { - const selector = component.name + ruleToSelector({ component: component.name, ...combination }) - - const lowerLevel = findLowerLevelRule(parent, (r) => { - if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false - if (r.cache?.background === undefined) return false - if (r.cache.textDefined) { - return !r.cache.textDefined[selector] - } - return true - }) - if (!lowerLevel) return - lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} - lowerLevel.cache.textDefined[selector] = true - addRule({ - parent, - component: component.name, - ...combination, - directives: { - // TODO: DEFAULT TEXT COLOR - textColor: getTextColor(convert(lowerLevel.cache.background).rgb, convert('#FFFFFF').rgb, component.name === 'Link'), - // Debug: lets you see what it think background color should be - background: convert(lowerLevel.cache.background).hex + const existingGlobalRules = ruleset.filter(findRules({ component: component.name, ...combination }, null)) + const existingRules = ruleset.filter(findRules({ component: component.name, ...combination }, parent)) + + // Global (general) rules + if (existingGlobalRules.length !== 0) { + const lastRule = existingGlobalRules[existingGlobalRules.length - 1] + const { directives } = lastRule + lastRule.cache = lastRule.cache || {} + + if (directives.background) { + const rgb = convert(findColor(directives.background)).rgb + + // TODO: DEFAULT TEXT COLOR + const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb + + if (!lastRule.cache.background) { + const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb + lastRule.cache.background = blend + + needRuleAdd = true } - }) + } + + if (needRuleAdd) { + addRule(lastRule) + } } - } + if (existingRules.length !== 0) { + console.warn('MORE EXISTING RULES', existingRules) + } + } innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, { parent, component: name, ...combination })) }) } processInnerComponent(components[rootName]) - // console.info(rules.map(x => [ - // (parent?.component || 'root') + ' -> ' + x.component, - // // 'Cached background:' + convert(bg).hex, - // // 'Color: ' + convert(x.directives.background).hex + ' A:' + x.directives.opacity, - // JSON.stringify(x.directives) - // // '=> Blend: ' + convert(x.cache.background).hex - // ].join(' '))) - return { raw: rules, css: rules.map(rule => { - const header = ruleToSelector(rule) + ' {' + if (rule.virtual) return '' + + let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '') + if (!selector) { + selector = 'body' + } + const header = selector + ' {' const footer = '}' + + const virtualDirectives = Object.entries(rule.virtualDirectives || {}).map(([k, v]) => { + return ' ' + k + ': ' + v + }).join(';\n') + const directives = Object.entries(rule.directives).map(([k, v]) => { switch (k) { - case 'background': return 'background-color: ' + rgba2css({ ...convert(v).rgb, a: rule.directives.opacity ?? 1 }) - case 'textColor': return 'color: ' + rgba2css({ ...convert(v).rgb, a: rule.directives.opacity ?? 1 }) + case 'background': { + return 'background-color: ' + rgba2css({ ...convert(findColor(v)).rgb, a: rule.directives.opacity ?? 1 }) + } + case 'textColor': { + return 'color: ' + v + } default: return '' } }).filter(x => x).map(x => ' ' + x).join(';\n') - return [header, directives, footer].join('\n') - }) + + return [ + header, + directives + ';', + ' color: var(--text);', + '', + virtualDirectives, + footer + ].join('\n') + }).filter(x => x) } } diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js index 915bb5ce..0d5870cc 100644 --- a/test/unit/specs/services/theme_data/theme_data3.spec.js +++ b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -17,7 +17,7 @@ describe.only('Theme Data 3', () => { describe('init', () => { it('test simple case', () => { - const out = init(sampleRules) + const out = init(sampleRules, palette) // console.log(JSON.stringify(out, null, 2)) console.log('\n' + out.css.join('\n') + '\n') }) -- cgit v1.2.3-70-g09d2 From 85d55c55e76edfd7661b259d12618e71669ee2d2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 7 Feb 2024 16:15:25 +0200 Subject: add directive to completely disable automatic text color creation --- src/components/icon.style.js | 3 ++- src/services/theme_data/theme_data_3.service.js | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) (limited to 'src/components/icon.style.js') diff --git a/src/components/icon.style.js b/src/components/icon.style.js index adc72fc5..67b8abb5 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -8,7 +8,8 @@ export default { directives: { textColor: '--text', textOpacity: 0.5, - textOpacityMode: 'mixrgb' + textOpacityMode: 'mixrgb', + textAuto: 'no-auto' } } ] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index c1f8c730..e9bee453 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -392,12 +392,15 @@ export const init = (extraRuleset, palette) => { const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ') const lowerLevelBackground = cache[lowerLevelSelector].background - const textColor = getTextColor( - convert(lowerLevelBackground).rgb, - // TODO properly provide "parent" text color? - convert(findColor(inheritedTextColor, null, lowerLevelBackground)).rgb, - newTextRule.directives.textAuto === 'preserve' - ) + // TODO properly provide "parent" text color? + const intendedTextColor = convert(findColor(inheritedTextColor, null, lowerLevelBackground)).rgb + const textColor = newTextRule.directives.textAuto === 'no-auto' + ? intendedTextColor + : getTextColor( + convert(lowerLevelBackground).rgb, + intendedTextColor, + newTextRule.directives.textAuto === 'preserve' + ) // Storing color data in lower layer to use as custom css properties cache[lowerLevelSelector].textDefined = cache[lowerLevelSelector].textDefined || {} -- cgit v1.2.3-70-g09d2 From 1229bbd855108c03df060474a3bc50100782bf4f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 7 Feb 2024 16:17:53 +0200 Subject: don't use no-auto for icons for now --- src/components/icon.style.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/components/icon.style.js') diff --git a/src/components/icon.style.js b/src/components/icon.style.js index 67b8abb5..66c80882 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -7,9 +7,9 @@ export default { component: 'Icon', directives: { textColor: '--text', + // textAuto: 'no-auto', // doesn't work well with mixrgb? textOpacity: 0.5, - textOpacityMode: 'mixrgb', - textAuto: 'no-auto' + textOpacityMode: 'mixrgb' } } ] -- cgit v1.2.3-70-g09d2 From a7d771e8c819b7f20bd0048b50e2d14816d28668 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 7 Feb 2024 18:54:00 +0200 Subject: more dynamic vars, PROPER ICON COLORS --- src/components/icon.style.js | 6 +- src/services/theme_data/theme_data_3.service.js | 89 +++++++++++++++---------- 2 files changed, 55 insertions(+), 40 deletions(-) (limited to 'src/components/icon.style.js') diff --git a/src/components/icon.style.js b/src/components/icon.style.js index 66c80882..81bbbce0 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -6,10 +6,8 @@ export default { { component: 'Icon', directives: { - textColor: '--text', - // textAuto: 'no-auto', // doesn't work well with mixrgb? - textOpacity: 0.5, - textOpacityMode: 'mixrgb' + textColor: '$blend(--parent, 0.5, --parent--text)', + textAuto: 'no-auto' } } ] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index e9bee453..c5695d67 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -141,12 +141,12 @@ export const ruleToSelector = (rule, ignoreOutOfTreeSelector, isParent) => { return selectors.trim() } -const combinationsMatch = (criteria, subject) => { +const combinationsMatch = (criteria, subject, strict) => { if (criteria.component !== subject.component) return false // All variants inherit from normal const subjectVariant = Object.prototype.hasOwnProperty.call(subject, 'variant') ? subject.variant : 'normal' - if (subjectVariant !== 'normal') { + if (subjectVariant !== 'normal' || strict) { if (criteria.variant !== subject.variant) return false } @@ -154,7 +154,7 @@ const combinationsMatch = (criteria, subject) => { const criteriaStatesSet = new Set(['normal', ...(criteria.state || [])]) // Subject states > 1 essentially means state is "normal" and therefore matches - if (subjectStatesSet.size > 1) { + if (subjectStatesSet.size > 1 || strict) { const setsAreEqual = [...criteriaStatesSet].every(state => subjectStatesSet.has(state)) && [...subjectStatesSet].every(state => criteriaStatesSet.has(state)) @@ -164,13 +164,13 @@ const combinationsMatch = (criteria, subject) => { return true } -const findRules = criteria => subject => { +const findRules = (criteria, strict) => subject => { // If we searching for "general" rules - ignore "specific" ones if (criteria.parent === null && !!subject.parent) return false - if (!combinationsMatch(criteria, subject)) return false + if (!combinationsMatch(criteria, subject, strict)) return false if (criteria.parent !== undefined && criteria.parent !== null) { - if (!subject.parent) return true + if (!subject.parent && !strict) return true const pathCriteria = unroll(criteria) const pathSubject = unroll(subject) if (pathCriteria.length < pathSubject.length) return false @@ -182,7 +182,7 @@ const findRules = criteria => subject => { const criteriaParent = pathCriteria[i] const subjectParent = pathSubject[i] if (!subjectParent) return true - if (!combinationsMatch(criteriaParent, subjectParent)) return false + if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false } } return true @@ -205,21 +205,24 @@ export const init = (extraRuleset, palette) => { rules.push(rule) } - const findColor = (color, inheritedBackground, lowerLevelBackground) => { + const findColor = (color, dynamicVars) => { if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color let targetColor = null if (color.startsWith('--')) { const [variable, modifier] = color.split(/,/g).map(str => str.trim()) const variableSlot = variable.substring(2) if (variableSlot.startsWith('parent')) { - // TODO support more than just background? if (variableSlot === 'parent') { - targetColor = lowerLevelBackground + targetColor = dynamicVars.lowerLevelBackground + } else { + const virtualSlot = variableSlot.replace(/^parent/, '') + targetColor = dynamicVars.lowerLevelVirtualDirectives[virtualSlot] } } else { + // TODO add support for --current prefix switch (variableSlot) { case 'background': - targetColor = inheritedBackground + targetColor = dynamicVars.inheritedBackground break default: targetColor = palette[variableSlot] @@ -227,12 +230,13 @@ export const init = (extraRuleset, palette) => { } if (modifier) { - const effectiveBackground = lowerLevelBackground ?? targetColor + const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 const mod = isLightOnDark ? 1 : -1 targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb } } + if (color.startsWith('$')) { try { const { funcName, argsString } = /\$(?\w+)\((?[a-zA-Z0-9-,.'"\s]*)\)/.exec(color).groups @@ -242,9 +246,10 @@ export const init = (extraRuleset, palette) => { if (args.length !== 3) { throw new Error(`$blend requires 3 arguments, ${args.length} were provided`) } - const backgroundArg = findColor(args[2], inheritedBackground, lowerLevelBackground) - const foregroundArg = findColor(args[0], inheritedBackground, lowerLevelBackground) + const backgroundArg = convert(findColor(args[2], dynamicVars)).rgb + const foregroundArg = convert(findColor(args[0], dynamicVars)).rgb const amount = Number(args[1]) + console.log('ASS', backgroundArg, foregroundArg, amount) targetColor = alphaBlend(backgroundArg, amount, foregroundArg) break } @@ -260,17 +265,17 @@ export const init = (extraRuleset, palette) => { const cssColorString = (color, alpha) => rgba2css({ ...convert(color).rgb, a: alpha }) - const getTextColorAlpha = (rule, lowerColor, value) => { - const opacity = rule.directives.textOpacity - const backgroundColor = convert(lowerColor).rgb - const textColor = convert(findColor(value, backgroundColor)).rgb + const getTextColorAlpha = (directives, intendedTextColor, dynamicVars) => { + const opacity = directives.textOpacity + const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb + const textColor = convert(findColor(intendedTextColor, dynamicVars)).rgb if (opacity === null || opacity === undefined || opacity >= 1) { return convert(textColor).hex } if (opacity === 0) { return convert(backgroundColor).hex } - const opacityMode = rule.directives.textOpacityMode + const opacityMode = directives.textOpacityMode switch (opacityMode) { case 'fake': return convert(alphaBlend(textColor, opacity, backgroundColor)).hex @@ -341,6 +346,16 @@ export const init = (extraRuleset, palette) => { const soloSelector = ruleToSelector({ component: component.name, ...combination }, true) const selector = ruleToSelector({ component: component.name, ...combination, parent }, true) + const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ') + const lowerLevelBackground = cache[lowerLevelSelector]?.background + const lowerLevelVirtualDirectives = cache[lowerLevelSelector]?.virtualDirectives + // console.log('ASS', lowerLevelVirtualDirectives) + + const dynamicVars = { + lowerLevelBackground, + lowerLevelVirtualDirectives + } + // Inheriting all of the applicable rules const existingRules = ruleset.filter(findRules({ component: component.name, ...combination, parent })) const { directives: computedDirectives } = existingRules.reduce((acc, rule) => merge(acc, rule), {}) @@ -389,11 +404,10 @@ export const init = (extraRuleset, palette) => { } } - const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ') - const lowerLevelBackground = cache[lowerLevelSelector].background + dynamicVars.inheritedBackground = lowerLevelBackground // TODO properly provide "parent" text color? - const intendedTextColor = convert(findColor(inheritedTextColor, null, lowerLevelBackground)).rgb + const intendedTextColor = convert(findColor(inheritedTextColor, dynamicVars)).rgb const textColor = newTextRule.directives.textAuto === 'no-auto' ? intendedTextColor : getTextColor( @@ -402,23 +416,25 @@ export const init = (extraRuleset, palette) => { newTextRule.directives.textAuto === 'preserve' ) - // Storing color data in lower layer to use as custom css properties - cache[lowerLevelSelector].textDefined = cache[lowerLevelSelector].textDefined || {} - cache[lowerLevelSelector].textDefined[selector] = textColor + // Updating previously added rule + const earlyLowerLevelRules = rules.filter(findRules(parent, true)) + const earlyLowerLevelRule = earlyLowerLevelRules.slice(-1)[0] - const virtualDirectives = {} - virtualDirectives[virtualName] = getTextColorAlpha(newTextRule, lowerLevelBackground, textColor) + const virtualDirectives = earlyLowerLevelRule.virtualDirectives || {} - // lastRule.computed = lastRule.computed || {} + // Storing color data in lower layer to use as custom css properties + virtualDirectives[virtualName] = getTextColorAlpha(newTextRule.directives, textColor, dynamicVars) + earlyLowerLevelRule.virtualDirectives = virtualDirectives + cache[lowerLevelSelector].virtualDirectives = virtualDirectives + + // Debug: lets you see what it think background color should be const directives = { textColor, + background: convert(cache[lowerLevelSelector].background).hex, ...inheritedTextOpacity } - // Debug: lets you see what it think background color should be - // directives.background = convert(cache[lowerLevelSelector].background).hex - addRule({ parent, virtual: true, @@ -445,15 +461,16 @@ export const init = (extraRuleset, palette) => { const inheritSelector = ruleToSelector({ ...inheritRule, parent }, true) const inheritedBackground = cache[inheritSelector].background - const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ') // TODO: DEFAULT TEXT COLOR - const bg = cache[lowerLevelSelector]?.background || convert('#FFFFFF').rgb + const lowerLevelComputedBackground = computed[lowerLevelSelector]?.background || convert('#FFFFFF').rgb + + dynamicVars.inheritedBackground = inheritedBackground - const rgb = convert(findColor(computedDirectives.background, inheritedBackground, cache[lowerLevelSelector].background)).rgb + const rgb = convert(findColor(computedDirectives.background, dynamicVars)).rgb if (!cache[selector].background) { - const blend = computedDirectives.opacity < 1 ? alphaBlend(rgb, computedDirectives.opacity, bg) : rgb + const blend = computedDirectives.opacity < 1 ? alphaBlend(rgb, computedDirectives.opacity, lowerLevelComputedBackground) : rgb cache[selector].background = blend computed[selector].background = rgb @@ -479,7 +496,7 @@ export const init = (extraRuleset, palette) => { return { raw: rules, css: rules.map(rule => { - // if (rule.virtual) return '' + if (rule.virtual) return '' let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '') if (!selector) { -- cgit v1.2.3-70-g09d2 From f0bbb75df50eb7df91cbb172f2b1ff4a5a4d5310 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 19 Feb 2024 00:00:43 +0200 Subject: properly sort, properly do icon colors, some initial attachment buttons work --- src/components/attachment.style.js | 23 +++++++++++++++++ src/components/button_unstyled.style.js | 2 ++ src/components/icon.style.js | 2 +- src/services/theme_data/theme_data_3.service.js | 33 +++++++++++++++++-------- 4 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 src/components/attachment.style.js (limited to 'src/components/icon.style.js') diff --git a/src/components/attachment.style.js b/src/components/attachment.style.js new file mode 100644 index 00000000..b6c5da6e --- /dev/null +++ b/src/components/attachment.style.js @@ -0,0 +1,23 @@ +export default { + name: 'Attachment', + selector: '.Attachment', + validInnerComponents: [ + 'Border', + 'ButtonUnstyled' + ], + defaultRules: [ + { + directives: { + roundness: 3 + } + }, + { + component: 'ButtonUnstyled', + parent: { component: 'Attachment' }, + directives: { + background: '#FFFFFF', + opacity: 0.9 + } + } + ] +} diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js index 89e8499c..60dd0d47 100644 --- a/src/components/button_unstyled.style.js +++ b/src/components/button_unstyled.style.js @@ -16,6 +16,8 @@ export default { defaultRules: [ { directives: { + background: '#ffffff', + opacity: 0, shadow: [] } }, diff --git a/src/components/icon.style.js b/src/components/icon.style.js index 81bbbce0..6cb9e4e3 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -6,7 +6,7 @@ export default { { component: 'Icon', directives: { - textColor: '$blend(--parent, 0.5, --parent--text)', + textColor: '$blend(--stack, 0.5, --parent--text)', textAuto: 'no-auto' } } diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index c301d148..84ff20b2 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -16,7 +16,8 @@ const components = { FunText: null, Link: null, Icon: null, - Border: null + Border: null, + Panel: null } // Loading all style.js[on] files dynamically @@ -175,10 +176,14 @@ export const init = (extraRuleset, palette) => { const parentsA = unroll(a).length const parentsB = unroll(b).length - if (parentsA === parentsB || (parentsB !== 0 && parentsA !== 0)) return ai - bi + if (parentsA === parentsB) { + if (a.component === 'Text') return -1 + if (b.component === 'Text') return 1 + return ai - bi + } if (parentsA === 0 && parentsB !== 0) return -1 if (parentsB === 0 && parentsA !== 0) return 1 - return 0 // failsafe, shouldn't happen? + return parentsA - parentsB }) .map(({ data }) => data) @@ -190,7 +195,12 @@ export const init = (extraRuleset, palette) => { if (color.startsWith('--')) { const [variable, modifier] = color.split(/,/g).map(str => str.trim()) const variableSlot = variable.substring(2) - if (variableSlot.startsWith('parent')) { + if (variableSlot === 'stack') { + console.log(dynamicVars) + console.log(stacked) + const { r, g, b } = dynamicVars.stacked + targetColor = { r, g, b } + } else if (variableSlot.startsWith('parent')) { if (variableSlot === 'parent') { const { r, g, b } = dynamicVars.lowerLevelBackground targetColor = { r, g, b } @@ -451,6 +461,7 @@ export const init = (extraRuleset, palette) => { } dynamicVars.inheritedBackground = lowerLevelBackground + dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb const intendedTextColor = convert(findColor(inheritedTextColor, dynamicVars)).rgb const textColor = newTextRule.directives.textAuto === 'no-auto' @@ -500,7 +511,7 @@ export const init = (extraRuleset, palette) => { let addRuleNeeded = false // TODO: DEFAULT TEXT COLOR - const lowerLevelComputedBackground = computed[lowerLevelSelector]?.background || convert('#FFFFFF').rgb + const lowerLevelStackedBackground = stacked[lowerLevelSelector] || convert('#FF00FF').rgb if (computedDirectives.shadow != null || computedDirectives.roundness != null) { addRuleNeeded = true @@ -532,11 +543,12 @@ export const init = (extraRuleset, palette) => { if (alpha >= 1) { blend = rgb } else if (alpha <= 0) { - blend = lowerLevelComputedBackground + blend = lowerLevelStackedBackground } else { - blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelComputedBackground) + blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground) } stacked[selector] = blend + dynamicVars.stacked = blend computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 } } } @@ -544,11 +556,12 @@ export const init = (extraRuleset, palette) => { if (!stacked[selector]) { computedDirectives.background = 'transparent' computedDirectives.opacity = 0 - stacked[selector] = lowerLevelComputedBackground - computed[selector].background = { ...lowerLevelComputedBackground, a: 0 } + stacked[selector] = lowerLevelStackedBackground + computed[selector].background = { ...lowerLevelStackedBackground, a: 0 } } - computed[selector].dynamicVars.background = computed[selector].background + dynamicVars.stacked = lowerLevelStackedBackground + dynamicVars.background = computed[selector].background if (addRuleNeeded) { addRule({ -- cgit v1.2.3-70-g09d2