2024-01-31 23:27:30 +00:00
|
|
|
import { convert, brightness } from 'chromatism'
|
|
|
|
import merge from 'lodash.merge'
|
|
|
|
import { alphaBlend, getTextColor, rgba2css, mixrgb, relativeLuminance } from '../color_convert/color_convert.js'
|
2024-01-23 17:18:55 +00:00
|
|
|
|
2024-01-18 12:35:25 +00:00
|
|
|
import Underlay from 'src/components/underlay.style.js'
|
|
|
|
import Panel from 'src/components/panel.style.js'
|
2024-01-23 17:18:55 +00:00
|
|
|
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,
|
2024-01-23 17:18:55 +00:00
|
|
|
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
|
2024-01-22 22:43:46 +00:00
|
|
|
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) => {
|
2024-01-22 22:43:46 +00:00
|
|
|
const component = components[rule.component]
|
2024-01-31 15:39:51 +00:00
|
|
|
const { states, variants, selector, outOfTreeSelector } = component
|
2024-01-22 22:43:46 +00:00
|
|
|
|
2024-01-23 17:18:55 +00:00
|
|
|
const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state])
|
2024-01-22 22:43:46 +00:00
|
|
|
|
|
|
|
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]
|
2024-01-22 22:43:46 +00:00
|
|
|
.toSorted((a, b) => {
|
|
|
|
if (a.startsWith(':')) return 1
|
2024-01-31 15:39:51 +00:00
|
|
|
if (!a.startsWith('.')) return -1
|
|
|
|
else return 0
|
2024-01-22 22:43:46 +00:00
|
|
|
})
|
|
|
|
.join('')
|
|
|
|
|
|
|
|
if (rule.parent) {
|
2024-01-31 15:39:51 +00:00
|
|
|
return ruleToSelector(rule.parent, true) + ' ' + selectors
|
2024-01-22 22:43:46 +00:00
|
|
|
}
|
|
|
|
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 = []
|
2024-01-22 22:43:46 +00:00
|
|
|
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
|
|
|
|
]
|
|
|
|
|
2024-01-22 22:43:46 +00:00
|
|
|
const addRule = (rule) => {
|
|
|
|
rules.push(rule)
|
|
|
|
rulesByComponent[rule.component] = rulesByComponent[rule.component] || []
|
2024-01-23 17:18:55 +00:00
|
|
|
rulesByComponent[rule.component].push(rule)
|
2024-01-22 22:43:46 +00:00
|
|
|
}
|
|
|
|
|
2024-01-31 23:27:30 +00:00
|
|
|
const findRules = (searchCombination, parent) => rule => {
|
2024-01-31 15:39:51 +00:00
|
|
|
// inexact search
|
|
|
|
const doesCombinationMatch = () => {
|
2024-01-31 23:27:30 +00:00
|
|
|
if (searchCombination.component !== rule.component) return false
|
|
|
|
const ruleVariant = Object.prototype.hasOwnProperty.call(rule, 'variant') ? rule.variant : 'normal'
|
|
|
|
|
|
|
|
if (ruleVariant !== 'normal') {
|
|
|
|
if (searchCombination.variant !== rule.variant) return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const ruleHasStateDefined = Object.prototype.hasOwnProperty.call(rule, 'state')
|
|
|
|
let ruleStateSet
|
|
|
|
if (ruleHasStateDefined) {
|
|
|
|
ruleStateSet = new Set(['normal', ...rule.state])
|
2024-01-31 15:39:51 +00:00
|
|
|
} else {
|
2024-01-31 23:27:30 +00:00
|
|
|
ruleStateSet = new Set(['normal'])
|
2024-01-31 15:39:51 +00:00
|
|
|
}
|
|
|
|
|
2024-01-31 23:27:30 +00:00
|
|
|
if (ruleStateSet.size > 1) {
|
|
|
|
const ruleStatesSet = ruleStateSet
|
|
|
|
const combinationSet = new Set(['normal', ...searchCombination.state])
|
|
|
|
const setsAreEqual = searchCombination.state.every(state => ruleStatesSet.has(state)) &&
|
2024-01-31 15:39:51 +00:00
|
|
|
[...ruleStatesSet].every(state => combinationSet.has(state))
|
|
|
|
return setsAreEqual
|
|
|
|
} else {
|
|
|
|
return true
|
|
|
|
}
|
2024-01-23 17:18:55 +00:00
|
|
|
}
|
2024-01-31 23:27:30 +00:00
|
|
|
|
2024-01-31 15:39:51 +00:00
|
|
|
const combinationMatches = doesCombinationMatch()
|
|
|
|
if (!parent || !combinationMatches) return combinationMatches
|
2024-01-23 17:18:55 +00:00
|
|
|
|
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-23 17:18:55 +00:00
|
|
|
}
|
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
|
2024-01-23 17:18:55 +00:00
|
|
|
}
|
2024-01-22 22:43:46 +00:00
|
|
|
|
2024-01-23 17:18:55 +00:00
|
|
|
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')
|
2024-01-23 17:18:55 +00:00
|
|
|
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 23:27:30 +00:00
|
|
|
const findColor = (color, background) => {
|
|
|
|
if (typeof color !== 'string' || !color.startsWith('--')) return color
|
|
|
|
let targetColor = null
|
|
|
|
// Color references other color
|
|
|
|
const [variable, modifier] = color.split(/,/g).map(str => str.trim())
|
|
|
|
const variableSlot = variable.substring(2)
|
|
|
|
targetColor = palette[variableSlot]
|
|
|
|
|
|
|
|
if (modifier) {
|
|
|
|
const effectiveBackground = background ?? targetColor
|
|
|
|
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
|
|
|
const mod = isLightOnDark ? 1 : -1
|
|
|
|
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
|
2024-01-31 15:39:51 +00:00
|
|
|
}
|
2024-01-31 23:27:30 +00:00
|
|
|
|
|
|
|
return targetColor
|
2024-01-31 15:39:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const getTextColorAlpha = (rule, lowerRule, value) => {
|
|
|
|
const opacity = rule.directives.textOpacity
|
2024-01-31 23:27:30 +00:00
|
|
|
const backgroundColor = convert(lowerRule.cache.background).rgb
|
|
|
|
const textColor = convert(findColor(value, backgroundColor)).rgb
|
2024-01-31 15:39:51 +00:00
|
|
|
if (opacity === null || opacity === undefined || opacity >= 1) {
|
|
|
|
return convert(textColor).hex
|
|
|
|
}
|
|
|
|
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 {
|
2024-01-22 22:43:46 +00:00
|
|
|
validInnerComponents = [],
|
2024-01-18 12:35:25 +00:00
|
|
|
states: originalStates = {},
|
2024-01-22 22:43:46 +00:00
|
|
|
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], [])
|
|
|
|
|
2024-01-23 17:18:55 +00:00
|
|
|
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-23 17:18:55 +00:00
|
|
|
|
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-23 17:18:55 +00:00
|
|
|
|
2024-01-31 15:39:51 +00:00
|
|
|
const lowerLevel = findLowerLevelRule(parent, (r) => {
|
2024-01-31 23:27:30 +00:00
|
|
|
if (!r) return false
|
2024-01-31 15:39:51 +00:00
|
|
|
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-23 17:18:55 +00:00
|
|
|
|
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))
|
2024-01-31 23:27:30 +00:00
|
|
|
inheritedTextColorRule = generalTextColorRules.reduce((acc, rule) => merge(acc, rule), {})
|
2024-01-31 15:39:51 +00:00
|
|
|
} else {
|
2024-01-31 23:27:30 +00:00
|
|
|
inheritedTextColorRule = inheritedTextColorRules.reduce((acc, rule) => merge(acc, rule), {})
|
2024-01-31 15:39:51 +00:00
|
|
|
}
|
2024-01-23 17:18:55 +00:00
|
|
|
|
2024-01-31 15:39:51 +00:00
|
|
|
let inheritedTextColor
|
|
|
|
let inheritedTextOpacity = {}
|
|
|
|
if (inheritedTextColorRule) {
|
2024-01-31 23:27:30 +00:00
|
|
|
inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor, convert(lowerLevel.cache.background).rgb)
|
2024-01-31 15:39:51 +00:00
|
|
|
// also inherit opacity settings
|
|
|
|
const { textOpacity, textOpacityMode } = inheritedTextColorRule.directives
|
|
|
|
inheritedTextOpacity = { textOpacity, textOpacityMode }
|
|
|
|
} else {
|
|
|
|
// Emergency fallback
|
|
|
|
inheritedTextColor = '#000000'
|
2024-01-23 17:18:55 +00:00
|
|
|
}
|
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
|
|
|
|
})
|
2024-01-23 17:18:55 +00:00
|
|
|
} 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) {
|
2024-01-31 23:27:30 +00:00
|
|
|
const totalRule = existingGlobalRules.reduce((acc, rule) => merge(acc, rule), {})
|
|
|
|
const { directives } = totalRule
|
|
|
|
|
|
|
|
// last rule is used as a cache
|
2024-01-31 15:39:51 +00:00
|
|
|
const lastRule = existingGlobalRules[existingGlobalRules.length - 1]
|
|
|
|
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-23 17:18:55 +00:00
|
|
|
}
|
2024-01-31 15:39:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (needRuleAdd) {
|
|
|
|
addRule(lastRule)
|
|
|
|
}
|
2024-01-23 17:18:55 +00:00
|
|
|
}
|
2024-01-22 22:43:46 +00:00
|
|
|
|
2024-01-31 15:39:51 +00:00
|
|
|
if (existingRules.length !== 0) {
|
|
|
|
console.warn('MORE EXISTING RULES', existingRules)
|
|
|
|
}
|
|
|
|
}
|
2024-01-22 22:43:46 +00:00
|
|
|
innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, { parent, component: name, ...combination }))
|
2024-01-18 12:35:25 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
processInnerComponent(components[rootName])
|
2024-01-23 17:18:55 +00:00
|
|
|
|
|
|
|
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 + ' {'
|
2024-01-23 17:18:55 +00:00
|
|
|
const footer = '}'
|
2024-01-31 15:39:51 +00:00
|
|
|
|
|
|
|
const virtualDirectives = Object.entries(rule.virtualDirectives || {}).map(([k, v]) => {
|
|
|
|
return ' ' + k + ': ' + v
|
|
|
|
}).join(';\n')
|
|
|
|
|
2024-01-23 17:18:55 +00:00
|
|
|
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
|
|
|
|
}
|
2024-01-23 17:18:55 +00:00
|
|
|
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-23 17:18:55 +00:00
|
|
|
}
|
2024-01-18 12:35:25 +00:00
|
|
|
}
|