pleroma-fe/src/services/theme_data/theme_data_3.service.js

359 lines
12 KiB
JavaScript
Raw Normal View History

import { convert } from 'chromatism'
2024-01-31 15:39:51 +00:00
import { alphaBlend, getTextColor, rgba2css, mixrgb } from '../color_convert/color_convert.js'
2024-01-18 12:35:25 +00:00
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'
2024-01-18 12:35:25 +00:00
import Button from 'src/components/button.style.js'
import Text from 'src/components/text.style.js'
2024-01-31 15:39:51 +00:00
import Link from 'src/components/link.style.js'
2024-01-18 12:35:25 +00:00
import Icon from 'src/components/icon.style.js'
const root = Underlay
const components = {
Underlay,
Panel,
PanelHeader,
2024-01-18 12:35:25 +00:00
Button,
Text,
2024-01-31 15:39:51 +00:00
Link,
2024-01-18 12:35:25 +00:00
Icon
}
// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations
export const getAllPossibleCombinations = (array) => {
2024-01-18 12:35:25 +00:00
const combos = [array.map(x => [x])]
for (let comboSize = 2; comboSize <= array.length; comboSize++) {
const previous = combos[combos.length - 1]
const selfSet = new Set()
const newCombos = previous.map(self => {
self.forEach(x => selfSet.add(x))
const nonSelf = array.filter(x => !selfSet.has(x))
return nonSelf.map(x => [...self, x])
})
const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], [])
combos.push(flatCombos)
}
return combos.reduce((acc, x) => [...acc, ...x], [])
}
2024-01-31 15:39:51 +00:00
export const ruleToSelector = (rule, isParent) => {
const component = components[rule.component]
2024-01-31 15:39:51 +00:00
const { states, variants, selector, outOfTreeSelector } = 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]
}
2024-01-31 15:39:51 +00:00
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
2024-01-31 15:39:51 +00:00
if (!a.startsWith('.')) return -1
else return 0
})
.join('')
if (rule.parent) {
2024-01-31 15:39:51 +00:00
return ruleToSelector(rule.parent, true) + ' ' + selectors
}
return selectors
}
2024-01-31 15:39:51 +00:00
export const init = (extraRuleset, palette) => {
2024-01-18 12:35:25 +00:00
const rootName = root.name
const rules = []
const rulesByComponent = {}
2024-01-31 15:39:51 +00:00
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)
}
2024-01-31 15:39:51 +00:00
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
}
}
2024-01-31 15:39:51 +00:00
const combinationMatches = doesCombinationMatch()
if (!parent || !combinationMatches) return combinationMatches
2024-01-31 15:39:51 +00:00
// 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
}
2024-01-31 15:39:51 +00:00
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) {
2024-01-31 15:39:51 +00:00
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
}
return filter(lowerLevelComponent) ? lowerLevelComponent : null
}
2024-01-18 12:35:25 +00:00
2024-01-31 15:39:51 +00:00
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 })
}
}
2024-01-18 12:35:25 +00:00
const processInnerComponent = (component, parent) => {
const {
validInnerComponents = [],
2024-01-18 12:35:25 +00:00
states: originalStates = {},
variants: originalVariants = {},
name
2024-01-18 12:35:25 +00:00
} = component
const states = { normal: '', ...originalStates }
const variants = { normal: '', ...originalVariants }
const innerComponents = validInnerComponents.map(name => components[name])
const stateCombinations = getAllPossibleCombinations(Object.keys(states))
const stateVariantCombination = Object.keys(variants).map(variant => {
return stateCombinations.map(state => ({ variant, state }))
}).reduce((acc, x) => [...acc, ...x], [])
const VIRTUAL_COMPONENTS = new Set(['Text', 'Link', 'Icon'])
2024-01-18 12:35:25 +00:00
stateVariantCombination.forEach(combination => {
2024-01-31 15:39:51 +00:00
let needRuleAdd = false
2024-01-31 15:39:51 +00:00
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('')
2024-01-31 15:39:51 +00:00
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
})
2024-01-31 15:39:51 +00:00
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]
}
2024-01-31 15:39:51 +00:00
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'
}
2024-01-31 15:39:51 +00:00
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 {
2024-01-31 15:39:51 +00:00
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
}
2024-01-31 15:39:51 +00:00
}
if (needRuleAdd) {
addRule(lastRule)
}
}
2024-01-31 15:39:51 +00:00
if (existingRules.length !== 0) {
console.warn('MORE EXISTING RULES', existingRules)
}
}
innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, { parent, component: name, ...combination }))
2024-01-18 12:35:25 +00:00
})
}
processInnerComponent(components[rootName])
return {
raw: rules,
css: rules.map(rule => {
2024-01-31 15:39:51 +00:00
if (rule.virtual) return ''
let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '')
if (!selector) {
selector = 'body'
}
const header = selector + ' {'
const footer = '}'
2024-01-31 15:39:51 +00:00
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) {
2024-01-31 15:39:51 +00:00
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')
2024-01-31 15:39:51 +00:00
return [
header,
directives + ';',
' color: var(--text);',
'',
virtualDirectives,
footer
].join('\n')
}).filter(x => x)
}
2024-01-18 12:35:25 +00:00
}