Merge branch 'themes3' into shigusegubu-themes3
This commit is contained in:
commit
ebeeec6391
143 changed files with 3616 additions and 1417 deletions
|
|
@ -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})`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,27 @@
|
|||
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 { hex2rgb } from '../color_convert/color_convert.js'
|
||||
import { generatePreset } from '../theme_data/theme_data.service.js'
|
||||
import { init } from '../theme_data/theme_data_3.service.js'
|
||||
import { convertTheme2To3 } from '../theme_data/theme2_to_theme3.js'
|
||||
import { getCssRules } from '../theme_data/css_utils.js'
|
||||
import { defaultState } from '../../modules/config.js'
|
||||
|
||||
export const applyTheme = (input) => {
|
||||
const { rules } = generatePreset(input)
|
||||
let extraRules
|
||||
if (input.themeType !== 1) {
|
||||
const t0 = performance.now()
|
||||
const { theme } = generatePreset(input)
|
||||
const t1 = performance.now()
|
||||
console.debug('Themes 2 initialization took ' + (t1 - t0) + 'ms')
|
||||
extraRules = convertTheme2To3(theme)
|
||||
} else {
|
||||
console.debug(input)
|
||||
extraRules = convertTheme2To3(input)
|
||||
}
|
||||
|
||||
const t1 = performance.now()
|
||||
const themes3 = init(extraRules, '#FFFFFF')
|
||||
const t2 = performance.now()
|
||||
console.debug('Themes 3 (eager) initialization took ' + (t2 - t1) + 'ms')
|
||||
const head = document.head
|
||||
const body = document.body
|
||||
body.classList.add('hidden')
|
||||
|
|
@ -13,12 +30,31 @@ export const applyTheme = (input) => {
|
|||
head.appendChild(styleEl)
|
||||
const styleSheet = styleEl.sheet
|
||||
|
||||
styleSheet.toString()
|
||||
styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max')
|
||||
styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max')
|
||||
styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max')
|
||||
styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max')
|
||||
getCssRules(themes3.eager, themes3.staticVars).forEach(rule => {
|
||||
// Hack to support multiple selectors on same component
|
||||
if (rule.match(/::-webkit-scrollbar-button/)) {
|
||||
const parts = rule.split(/[{}]/g)
|
||||
const newRule = [
|
||||
parts[0],
|
||||
', ',
|
||||
parts[0].replace(/button/, 'thumb'),
|
||||
' {',
|
||||
parts[1],
|
||||
'}'
|
||||
].join('')
|
||||
styleSheet.insertRule(newRule, 'index-max')
|
||||
} else {
|
||||
styleSheet.insertRule(rule, 'index-max')
|
||||
}
|
||||
})
|
||||
body.classList.remove('hidden')
|
||||
themes3.lazy.then(lazyRules => {
|
||||
getCssRules(lazyRules, themes3.staticVars).forEach(rule => {
|
||||
styleSheet.insertRule(rule, 'index-max')
|
||||
})
|
||||
const t3 = performance.now()
|
||||
console.debug('Themes 3 finalization (lazy) took ' + (t3 - t2) + 'ms')
|
||||
})
|
||||
}
|
||||
|
||||
const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) =>
|
||||
|
|
@ -51,308 +87,6 @@ export const applyConfig = (config) => {
|
|||
body.classList.remove('hidden')
|
||||
}
|
||||
|
||||
export const getCssShadow = (input, usesDropShadow) => {
|
||||
if (input.length === 0) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
return input
|
||||
.filter(_ => usesDropShadow ? _.inset : _)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
shad.blur,
|
||||
shad.spread
|
||||
].map(_ => _ + 'px').concat([
|
||||
getCssColor(shad.color, shad.alpha),
|
||||
shad.inset ? 'inset' : ''
|
||||
]).join(' ')).join(', ')
|
||||
}
|
||||
|
||||
const getCssShadowFilter = (input) => {
|
||||
if (input.length === 0) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
return input
|
||||
// drop-shadow doesn't support inset or spread
|
||||
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
// drop-shadow's blur is twice as strong compared to box-shadow
|
||||
shad.blur / 2
|
||||
].map(_ => _ + 'px').concat([
|
||||
getCssColor(shad.color, shad.alpha)
|
||||
]).join(' '))
|
||||
.map(_ => `drop-shadow(${_})`)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export const generateColors = (themeData) => {
|
||||
const sourceColors = !themeData.themeEngineVersion
|
||||
? colors2to3(themeData.colors || themeData)
|
||||
: themeData.colors || themeData
|
||||
|
||||
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
|
||||
|
||||
const htmlColors = Object.entries(colors)
|
||||
.reduce((acc, [k, v]) => {
|
||||
if (!v) return acc
|
||||
acc.solid[k] = rgb2hex(v)
|
||||
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
|
||||
return acc
|
||||
}, { complete: {}, solid: {} })
|
||||
return {
|
||||
rules: {
|
||||
colors: Object.entries(htmlColors.complete)
|
||||
.filter(([k, v]) => v)
|
||||
.map(([k, v]) => `--${k}: ${v}`)
|
||||
.join(';')
|
||||
},
|
||||
theme: {
|
||||
colors: htmlColors.solid,
|
||||
opacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateRadii = (input) => {
|
||||
let inputRadii = input.radii || {}
|
||||
// v1 -> v2
|
||||
if (typeof input.btnRadius !== 'undefined') {
|
||||
inputRadii = Object
|
||||
.entries(input)
|
||||
.filter(([k, v]) => k.endsWith('Radius'))
|
||||
.reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {})
|
||||
}
|
||||
const radii = Object.entries(inputRadii).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, {
|
||||
btn: 4,
|
||||
input: 4,
|
||||
checkbox: 2,
|
||||
panel: 10,
|
||||
avatar: 5,
|
||||
avatarAlt: 50,
|
||||
tooltip: 2,
|
||||
attachment: 5,
|
||||
chatMessage: inputRadii.panel
|
||||
})
|
||||
|
||||
return {
|
||||
rules: {
|
||||
radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
|
||||
},
|
||||
theme: {
|
||||
radii
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateFonts = (input) => {
|
||||
const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, acc[k])
|
||||
return acc
|
||||
}, {
|
||||
interface: {
|
||||
family: 'sans-serif'
|
||||
},
|
||||
input: {
|
||||
family: 'inherit'
|
||||
},
|
||||
post: {
|
||||
family: 'inherit'
|
||||
},
|
||||
postCode: {
|
||||
family: 'monospace'
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
rules: {
|
||||
fonts: Object
|
||||
.entries(fonts)
|
||||
.filter(([k, v]) => v)
|
||||
.map(([k, v]) => `--${k}Font: ${v.family}`).join(';')
|
||||
},
|
||||
theme: {
|
||||
fonts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const border = (top, shadow) => ({
|
||||
x: 0,
|
||||
y: top ? 1 : -1,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
color: shadow ? '#000000' : '#FFFFFF',
|
||||
alpha: 0.2,
|
||||
inset: true
|
||||
})
|
||||
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
||||
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
||||
const hoverGlow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '--faint',
|
||||
alpha: 1
|
||||
}
|
||||
|
||||
export const DEFAULT_SHADOWS = {
|
||||
panel: [{
|
||||
x: 1,
|
||||
y: 1,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
topBar: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
popup: [{
|
||||
x: 2,
|
||||
y: 2,
|
||||
blur: 3,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.5
|
||||
}],
|
||||
avatar: [{
|
||||
x: 0,
|
||||
y: 1,
|
||||
blur: 8,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.7
|
||||
}],
|
||||
avatarStatus: [],
|
||||
panelHeader: [],
|
||||
button: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}, ...buttonInsetFakeBorders],
|
||||
buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
|
||||
buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
|
||||
input: [...inputInsetFakeBorders, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
inset: true,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}]
|
||||
}
|
||||
export const generateShadows = (input, colors) => {
|
||||
// TODO this is a small hack for `mod` to work with shadows
|
||||
// this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element
|
||||
const hackContextDict = {
|
||||
button: 'btn',
|
||||
panel: 'bg',
|
||||
top: 'topBar',
|
||||
popup: 'popover',
|
||||
avatar: 'bg',
|
||||
panelHeader: 'panel',
|
||||
input: 'input'
|
||||
}
|
||||
|
||||
const cleanInputShadows = Object.fromEntries(
|
||||
Object.entries(input.shadows || {})
|
||||
.map(([name, shadowSlot]) => [
|
||||
name,
|
||||
// defaulting color to black to avoid potential problems
|
||||
shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef }))
|
||||
])
|
||||
)
|
||||
const inputShadows = cleanInputShadows && !input.themeEngineVersion
|
||||
? shadows2to3(cleanInputShadows, input.opacity)
|
||||
: cleanInputShadows || {}
|
||||
const shadows = Object.entries({
|
||||
...DEFAULT_SHADOWS,
|
||||
...inputShadows
|
||||
}).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const slotFirstWord = slotName.replace(/[A-Z].*$/, '')
|
||||
const colorSlotName = hackContextDict[slotFirstWord]
|
||||
const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
color: rgb2hex(computeDynamicColor(
|
||||
def.color,
|
||||
(variableSlot) => convert(colors[variableSlot]).rgb,
|
||||
mod
|
||||
))
|
||||
}
|
||||
], [])
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
|
||||
return {
|
||||
rules: {
|
||||
shadows: Object
|
||||
.entries(shadows)
|
||||
// TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
||||
// convert all non-inset shadows into filter: drop-shadow() to boost performance
|
||||
.map(([k, v]) => [
|
||||
`--${k}Shadow: ${getCssShadow(v)}`,
|
||||
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
|
||||
`--${k}ShadowInset: ${getCssShadow(v, true)}`
|
||||
].join(';'))
|
||||
.join(';')
|
||||
},
|
||||
theme: {
|
||||
shadows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const composePreset = (colors, radii, shadows, fonts) => {
|
||||
return {
|
||||
rules: {
|
||||
...shadows.rules,
|
||||
...colors.rules,
|
||||
...radii.rules,
|
||||
...fonts.rules
|
||||
},
|
||||
theme: {
|
||||
...shadows.theme,
|
||||
...colors.theme,
|
||||
...radii.theme,
|
||||
...fonts.theme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generatePreset = (input) => {
|
||||
const colors = generateColors(input)
|
||||
return composePreset(
|
||||
colors,
|
||||
generateRadii(input),
|
||||
generateShadows(input, colors.theme.colors, colors.mod),
|
||||
generateFonts(input)
|
||||
)
|
||||
}
|
||||
|
||||
export const getThemes = () => {
|
||||
const cache = 'no-store'
|
||||
|
||||
|
|
@ -382,54 +116,13 @@ export const getThemes = () => {
|
|||
}, {})
|
||||
})
|
||||
}
|
||||
export const colors2to3 = (colors) => {
|
||||
return Object.entries(colors).reduce((acc, [slotName, color]) => {
|
||||
const btnPositions = ['', 'Panel', 'TopBar']
|
||||
switch (slotName) {
|
||||
case 'lightBg':
|
||||
return { ...acc, highlight: color }
|
||||
case 'btnText':
|
||||
return {
|
||||
...acc,
|
||||
...btnPositions
|
||||
.reduce(
|
||||
(statePositionAcc, position) =>
|
||||
({ ...statePositionAcc, ['btn' + position + 'Text']: color })
|
||||
, {}
|
||||
)
|
||||
}
|
||||
default:
|
||||
return { ...acc, [slotName]: color }
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles compatibility issues when importing v2 theme's shadows to current format
|
||||
*
|
||||
* Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables
|
||||
*/
|
||||
export const shadows2to3 = (shadows, opacity) => {
|
||||
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const isDynamic = ({ color = '#000000' }) => color.startsWith('--')
|
||||
const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha
|
||||
}
|
||||
], [])
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const getPreset = (val) => {
|
||||
return getThemes()
|
||||
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
|
||||
.then((theme) => {
|
||||
const isV1 = Array.isArray(theme)
|
||||
const data = isV1 ? {} : theme.theme
|
||||
const data = isV1 ? { themeType: 1 } : theme.theme
|
||||
|
||||
if (isV1) {
|
||||
const bg = hex2rgb(theme[1])
|
||||
|
|
@ -449,4 +142,4 @@ export const getPreset = (val) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
|
||||
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data))
|
||||
|
|
|
|||
163
src/services/theme_data/css_utils.js
Normal file
163
src/services/theme_data/css_utils.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { convert } from 'chromatism'
|
||||
|
||||
import { hex2rgb, rgba2css } from '../color_convert/color_convert.js'
|
||||
|
||||
// This changes what backgrounds are used to "stacked" solid colors so you can see
|
||||
// what theme engine "thinks" is actual background color is for purposes of text color
|
||||
// generation and for when --stacked variable is used
|
||||
const DEBUG = false
|
||||
|
||||
export const parseCssShadow = (text) => {
|
||||
const dimensions = /(\d[a-z]*\s?){2,4}/.exec(text)?.[0]
|
||||
const inset = /inset/.exec(text)?.[0]
|
||||
const color = text.replace(dimensions, '').replace(inset, '')
|
||||
|
||||
const [x, y, blur = 0, spread = 0] = dimensions.split(/ /).filter(x => x).map(x => x.trim())
|
||||
const isInset = inset?.trim() === 'inset'
|
||||
const colorString = color.split(/ /).filter(x => x).map(x => x.trim())[0]
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
blur,
|
||||
spread,
|
||||
inset: isInset,
|
||||
color: colorString
|
||||
}
|
||||
}
|
||||
|
||||
export const getCssColorString = (color, alpha = 1) => rgba2css({ ...convert(color).rgb, a: alpha })
|
||||
|
||||
export const getCssShadow = (input, usesDropShadow) => {
|
||||
if (input.length === 0) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
return input
|
||||
.filter(_ => usesDropShadow ? _.inset : _)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
shad.blur,
|
||||
shad.spread
|
||||
].map(_ => _ + 'px ').concat([
|
||||
getCssColorString(shad.color, shad.alpha),
|
||||
shad.inset ? 'inset' : ''
|
||||
]).join(' ')).join(', ')
|
||||
}
|
||||
|
||||
export const getCssShadowFilter = (input) => {
|
||||
if (input.length === 0) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
return input
|
||||
// drop-shadow doesn't support inset or spread
|
||||
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
// drop-shadow's blur is twice as strong compared to box-shadow
|
||||
shad.blur / 2
|
||||
].map(_ => _ + 'px').concat([
|
||||
getCssColorString(shad.color, shad.alpha)
|
||||
]).join(' '))
|
||||
.map(_ => `drop-shadow(${_})`)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export const getCssRules = (rules) => rules.map(rule => {
|
||||
let selector = rule.selector
|
||||
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 'roundness': {
|
||||
return ' ' + [
|
||||
'--roundness: ' + v + 'px'
|
||||
].join(';\n ')
|
||||
}
|
||||
case 'shadow': {
|
||||
return ' ' + [
|
||||
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
|
||||
'--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow),
|
||||
'--shadowInset: ' + getCssShadow(rule.dynamicVars.shadow, true)
|
||||
].join(';\n ')
|
||||
}
|
||||
case 'background': {
|
||||
if (DEBUG) {
|
||||
return `
|
||||
--background: ${getCssColorString(rule.dynamicVars.stacked)};
|
||||
background-color: ${getCssColorString(rule.dynamicVars.stacked)};
|
||||
`
|
||||
}
|
||||
if (v === 'transparent') {
|
||||
if (rule.component === 'Root') return []
|
||||
return [
|
||||
rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + v) : '',
|
||||
' --background: ' + v
|
||||
].filter(x => x).join(';\n')
|
||||
}
|
||||
const color = getCssColorString(rule.dynamicVars.background, rule.directives.opacity)
|
||||
const cssDirectives = ['--background: ' + color]
|
||||
if (rule.directives.backgroundNoCssColor !== 'yes') {
|
||||
cssDirectives.push('background-color: ' + color)
|
||||
}
|
||||
return cssDirectives.filter(x => x).join(';\n')
|
||||
}
|
||||
case 'blur': {
|
||||
const cssDirectives = []
|
||||
if (rule.directives.opacity < 1) {
|
||||
cssDirectives.push(`--backdrop-filter: blur(${v}) `)
|
||||
if (rule.directives.backgroundNoCssColor !== 'yes') {
|
||||
cssDirectives.push(`backdrop-filter: blur(${v}) `)
|
||||
}
|
||||
}
|
||||
return cssDirectives.join(';\n')
|
||||
}
|
||||
case 'font': {
|
||||
return 'font-family: ' + v
|
||||
}
|
||||
case 'textColor': {
|
||||
if (rule.directives.textNoCssColor === 'yes') { return '' }
|
||||
return 'color: ' + v
|
||||
}
|
||||
default:
|
||||
if (k.startsWith('--')) {
|
||||
const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme!
|
||||
switch (type) {
|
||||
case 'color': {
|
||||
const color = rule.dynamicVars[k]
|
||||
if (typeof color === 'string') {
|
||||
return k + ': ' + rgba2css(hex2rgb(color))
|
||||
} else {
|
||||
return k + ': ' + rgba2css(color)
|
||||
}
|
||||
}
|
||||
case 'generic':
|
||||
return k + ': ' + value
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}).filter(x => x).map(x => ' ' + x).join(';\n')
|
||||
|
||||
return [
|
||||
header,
|
||||
directives + ';',
|
||||
(!rule.virtual && rule.directives.textNoCssColor !== 'yes') ? ' color: var(--text);' : '',
|
||||
'',
|
||||
virtualDirectives,
|
||||
footer
|
||||
].join('\n')
|
||||
}).filter(x => x)
|
||||
119
src/services/theme_data/iss_utils.js
Normal file
119
src/services/theme_data/iss_utils.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }}
|
||||
// into an array [item2, item3] for iterating
|
||||
export const unroll = (item) => {
|
||||
const out = []
|
||||
let currentParent = item
|
||||
while (currentParent) {
|
||||
out.push(currentParent)
|
||||
currentParent = currentParent.parent
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations
|
||||
export const getAllPossibleCombinations = (array) => {
|
||||
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], [])
|
||||
}
|
||||
|
||||
// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector
|
||||
export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => {
|
||||
if (!rule && !isParent) return null
|
||||
const component = components[rule.component]
|
||||
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]
|
||||
} else {
|
||||
applicableVariant = variants?.normal ?? ''
|
||||
}
|
||||
|
||||
let realSelector
|
||||
if (selector === ':root') {
|
||||
realSelector = ''
|
||||
} else if (isParent) {
|
||||
realSelector = selector
|
||||
} else {
|
||||
if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector
|
||||
else realSelector = selector
|
||||
}
|
||||
|
||||
const selectors = [realSelector, applicableVariant, ...applicableStates]
|
||||
.toSorted((a, b) => {
|
||||
if (a.startsWith(':')) return 1
|
||||
if (/^[a-z]/.exec(a)) return -1
|
||||
else return 0
|
||||
})
|
||||
.join('')
|
||||
|
||||
if (rule.parent) {
|
||||
return (genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, true) + ' ' + selectors).trim()
|
||||
}
|
||||
return selectors.trim()
|
||||
}
|
||||
|
||||
export const combinationsMatch = (criteria, subject, strict) => {
|
||||
if (criteria.component !== subject.component) return false
|
||||
|
||||
// All variants inherit from normal
|
||||
if (subject.variant !== 'normal' || strict) {
|
||||
if (criteria.variant !== subject.variant) return false
|
||||
}
|
||||
|
||||
// Subject states > 1 essentially means state is "normal" and therefore matches
|
||||
if (subject.state.length > 1 || strict) {
|
||||
const subjectStatesSet = new Set(subject.state)
|
||||
const criteriaStatesSet = new Set(criteria.state)
|
||||
|
||||
const setsAreEqual =
|
||||
[...criteriaStatesSet].every(state => subjectStatesSet.has(state)) &&
|
||||
[...subjectStatesSet].every(state => criteriaStatesSet.has(state))
|
||||
|
||||
if (!setsAreEqual) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export 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, strict)) return false
|
||||
|
||||
if (criteria.parent !== undefined && criteria.parent !== null) {
|
||||
if (!subject.parent && !strict) return true
|
||||
const pathCriteria = unroll(criteria)
|
||||
const pathSubject = unroll(subject)
|
||||
if (pathCriteria.length < pathSubject.length) return false
|
||||
|
||||
// Search: .a .b .c
|
||||
// Matches: .a .b .c; .b .c; .c; .z .a .b .c
|
||||
// Does not match .a .b .c .d, .a .b .e
|
||||
for (let i = 0; i < pathCriteria.length; i++) {
|
||||
const criteriaParent = pathCriteria[i]
|
||||
const subjectParent = pathSubject[i]
|
||||
if (!subjectParent) return true
|
||||
if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const normalizeCombination = rule => {
|
||||
rule.variant = rule.variant ?? 'normal'
|
||||
rule.state = [...new Set(['normal', ...(rule.state || [])])]
|
||||
}
|
||||
2
src/services/theme_data/pleromafe.t3.js
Normal file
2
src/services/theme_data/pleromafe.t3.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const sampleRules = [
|
||||
]
|
||||
176
src/services/theme_data/theme2_keys.js
Normal file
176
src/services/theme_data/theme2_keys.js
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
export default [
|
||||
'bg',
|
||||
'wallpaper',
|
||||
'fg',
|
||||
'text',
|
||||
'underlay',
|
||||
'link',
|
||||
'accent',
|
||||
'faint',
|
||||
'faintLink',
|
||||
'postFaintLink',
|
||||
|
||||
'cBlue',
|
||||
'cRed',
|
||||
'cGreen',
|
||||
'cOrange',
|
||||
|
||||
'profileBg',
|
||||
'profileTint',
|
||||
|
||||
'highlight',
|
||||
'highlightLightText',
|
||||
'highlightPostLink',
|
||||
'highlightFaintText',
|
||||
'highlightFaintLink',
|
||||
'highlightPostFaintLink',
|
||||
'highlightText',
|
||||
'highlightLink',
|
||||
'highlightIcon',
|
||||
|
||||
'popover',
|
||||
'popoverLightText',
|
||||
'popoverPostLink',
|
||||
'popoverFaintText',
|
||||
'popoverFaintLink',
|
||||
'popoverPostFaintLink',
|
||||
'popoverText',
|
||||
'popoverLink',
|
||||
'popoverIcon',
|
||||
|
||||
'selectedPost',
|
||||
'selectedPostFaintText',
|
||||
'selectedPostLightText',
|
||||
'selectedPostPostLink',
|
||||
'selectedPostFaintLink',
|
||||
'selectedPostText',
|
||||
'selectedPostLink',
|
||||
'selectedPostIcon',
|
||||
|
||||
'selectedMenu',
|
||||
'selectedMenuLightText',
|
||||
'selectedMenuFaintText',
|
||||
'selectedMenuFaintLink',
|
||||
'selectedMenuText',
|
||||
'selectedMenuLink',
|
||||
'selectedMenuIcon',
|
||||
|
||||
'selectedMenuPopover',
|
||||
'selectedMenuPopoverLightText',
|
||||
'selectedMenuPopoverFaintText',
|
||||
'selectedMenuPopoverFaintLink',
|
||||
'selectedMenuPopoverText',
|
||||
'selectedMenuPopoverLink',
|
||||
'selectedMenuPopoverIcon',
|
||||
|
||||
'lightText',
|
||||
|
||||
'postLink',
|
||||
|
||||
'postGreentext',
|
||||
|
||||
'postCyantext',
|
||||
|
||||
'border',
|
||||
|
||||
'poll',
|
||||
'pollText',
|
||||
|
||||
'icon',
|
||||
|
||||
// Foreground,
|
||||
'fgText',
|
||||
'fgLink',
|
||||
|
||||
// Panel header,
|
||||
'panel',
|
||||
'panelText',
|
||||
'panelFaint',
|
||||
'panelLink',
|
||||
|
||||
// Top bar,
|
||||
'topBar',
|
||||
'topBarLink',
|
||||
|
||||
// Tabs,
|
||||
'tab',
|
||||
'tabText',
|
||||
'tabActiveText',
|
||||
|
||||
// Buttons,
|
||||
'btn',
|
||||
'btnText',
|
||||
'btnPanelText',
|
||||
'btnTopBarText',
|
||||
|
||||
// Buttons: pressed,
|
||||
'btnPressed',
|
||||
'btnPressedText',
|
||||
'btnPressedPanel',
|
||||
'btnPressedPanelText',
|
||||
'btnPressedTopBar',
|
||||
'btnPressedTopBarText',
|
||||
|
||||
// Buttons: toggled,
|
||||
'btnToggled',
|
||||
'btnToggledText',
|
||||
'btnToggledPanelText',
|
||||
'btnToggledTopBarText',
|
||||
|
||||
// Buttons: disabled,
|
||||
'btnDisabled',
|
||||
'btnDisabledText',
|
||||
'btnDisabledPanelText',
|
||||
'btnDisabledTopBarText',
|
||||
|
||||
// Input fields,
|
||||
'input',
|
||||
'inputText',
|
||||
'inputPanelText',
|
||||
'inputTopbarText',
|
||||
|
||||
'alertError',
|
||||
'alertErrorText',
|
||||
'alertErrorPanelText',
|
||||
|
||||
'alertWarning',
|
||||
'alertWarningText',
|
||||
'alertWarningPanelText',
|
||||
|
||||
'alertSuccess',
|
||||
'alertSuccessText',
|
||||
'alertSuccessPanelText',
|
||||
|
||||
'alertNeutral',
|
||||
'alertNeutralText',
|
||||
'alertNeutralPanelText',
|
||||
|
||||
'alertPopupError',
|
||||
'alertPopupErrorText',
|
||||
|
||||
'alertPopupWarning',
|
||||
'alertPopupWarningText',
|
||||
|
||||
'alertPopupSuccess',
|
||||
'alertPopupSuccessText',
|
||||
|
||||
'alertPopupNeutral',
|
||||
'alertPopupNeutralText',
|
||||
|
||||
'badgeNotification',
|
||||
'badgeNotificationText',
|
||||
|
||||
'badgeNeutral',
|
||||
'badgeNeutralText',
|
||||
|
||||
'chatBg',
|
||||
|
||||
'chatMessageIncomingBg',
|
||||
'chatMessageIncomingText',
|
||||
'chatMessageIncomingLink',
|
||||
'chatMessageIncomingBorder',
|
||||
'chatMessageOutgoingBg',
|
||||
'chatMessageOutgoingText',
|
||||
'chatMessageOutgoingLink',
|
||||
'chatMessageOutgoingBorder'
|
||||
]
|
||||
502
src/services/theme_data/theme2_to_theme3.js
Normal file
502
src/services/theme_data/theme2_to_theme3.js
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
import allKeys from './theme2_keys'
|
||||
|
||||
// keys that are meant to be used globally, i.e. what's the rest of the theme is based upon.
|
||||
export const basePaletteKeys = new Set([
|
||||
'bg',
|
||||
'fg',
|
||||
'text',
|
||||
'link',
|
||||
'accent',
|
||||
|
||||
'cBlue',
|
||||
'cRed',
|
||||
'cGreen',
|
||||
'cOrange'
|
||||
])
|
||||
|
||||
export const fontsKeys = new Set([
|
||||
'interface',
|
||||
'input',
|
||||
'post',
|
||||
'postCode'
|
||||
])
|
||||
|
||||
export const opacityKeys = new Set([
|
||||
'alert',
|
||||
'alertPopup',
|
||||
'bg',
|
||||
'border',
|
||||
'btn',
|
||||
'faint',
|
||||
'input',
|
||||
'panel',
|
||||
'popover',
|
||||
'profileTint',
|
||||
'underlay'
|
||||
])
|
||||
|
||||
export const shadowsKeys = new Set([
|
||||
'panel',
|
||||
'topBar',
|
||||
'popup',
|
||||
'avatar',
|
||||
'avatarStatus',
|
||||
'panelHeader',
|
||||
'button',
|
||||
'buttonHover',
|
||||
'buttonPressed',
|
||||
'input'
|
||||
])
|
||||
|
||||
export const radiiKeys = new Set([
|
||||
'btn',
|
||||
'input',
|
||||
'checkbox',
|
||||
'panel',
|
||||
'avatar',
|
||||
'avatarAlt',
|
||||
'tooltip',
|
||||
'attachment',
|
||||
'chatMessage'
|
||||
])
|
||||
|
||||
// Keys that are not available in editor and never meant to be edited
|
||||
export const hiddenKeys = new Set([
|
||||
'profileBg',
|
||||
'profileTint'
|
||||
])
|
||||
|
||||
export const extendedBasePrefixes = [
|
||||
'border',
|
||||
'icon',
|
||||
'highlight',
|
||||
'lightText',
|
||||
|
||||
'popover',
|
||||
|
||||
'panel',
|
||||
'topBar',
|
||||
'tab',
|
||||
'btn',
|
||||
'input',
|
||||
'selectedMenu',
|
||||
|
||||
'alert',
|
||||
'alertPopup',
|
||||
'badge',
|
||||
|
||||
'post',
|
||||
'selectedPost', // wrong nomenclature
|
||||
'poll',
|
||||
|
||||
'chatBg',
|
||||
'chatMessage'
|
||||
]
|
||||
export const nonComponentPrefixes = new Set([
|
||||
'border',
|
||||
'icon',
|
||||
'highlight',
|
||||
'lightText',
|
||||
'chatBg'
|
||||
])
|
||||
|
||||
export const extendedBaseKeys = Object.fromEntries(
|
||||
extendedBasePrefixes.map(prefix => [
|
||||
prefix,
|
||||
allKeys.filter(k => {
|
||||
if (prefix === 'alert') {
|
||||
return k.startsWith(prefix) && !k.startsWith('alertPopup')
|
||||
}
|
||||
return k.startsWith(prefix)
|
||||
})
|
||||
])
|
||||
)
|
||||
|
||||
// Keysets that are only really used intermideately, i.e. to generate other colors
|
||||
export const temporary = new Set([
|
||||
'',
|
||||
'highlight'
|
||||
])
|
||||
|
||||
export const temporaryColors = {}
|
||||
|
||||
export const convertTheme2To3 = (data) => {
|
||||
data.colors.accent = data.colors.accent || data.colors.link
|
||||
data.colors.link = data.colors.link || data.colors.accent
|
||||
const generateRoot = () => {
|
||||
const directives = {}
|
||||
basePaletteKeys.forEach(key => { directives['--' + key] = 'color | ' + data.colors[key] })
|
||||
return {
|
||||
component: 'Root',
|
||||
directives
|
||||
}
|
||||
}
|
||||
|
||||
const convertOpacity = () => {
|
||||
const newRules = []
|
||||
Object.keys(data.opacity).forEach(key => {
|
||||
if (!opacityKeys.has(key) || data.opacity[key] === undefined) return null
|
||||
const originalOpacity = data.opacity[key]
|
||||
const rule = {}
|
||||
|
||||
switch (key) {
|
||||
case 'alert':
|
||||
rule.component = 'Alert'
|
||||
break
|
||||
case 'alertPopup':
|
||||
rule.component = 'Alert'
|
||||
rule.parent = { component: 'Popover' }
|
||||
break
|
||||
case 'bg':
|
||||
rule.component = 'Panel'
|
||||
break
|
||||
case 'border':
|
||||
rule.component = 'Border'
|
||||
break
|
||||
case 'btn':
|
||||
rule.component = 'Button'
|
||||
break
|
||||
case 'faint':
|
||||
rule.component = 'Text'
|
||||
rule.state = ['faint']
|
||||
break
|
||||
case 'input':
|
||||
rule.component = 'Input'
|
||||
break
|
||||
case 'panel':
|
||||
rule.component = 'PanelHeader'
|
||||
break
|
||||
case 'popover':
|
||||
rule.component = 'Popover'
|
||||
break
|
||||
case 'profileTint':
|
||||
return null
|
||||
case 'underlay':
|
||||
rule.component = 'Underlay'
|
||||
break
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'alert':
|
||||
case 'alertPopup':
|
||||
case 'bg':
|
||||
case 'btn':
|
||||
case 'input':
|
||||
case 'panel':
|
||||
case 'popover':
|
||||
case 'underlay':
|
||||
rule.directives = { opacity: originalOpacity }
|
||||
break
|
||||
case 'faint':
|
||||
case 'border':
|
||||
rule.directives = { textOpacity: originalOpacity }
|
||||
break
|
||||
}
|
||||
|
||||
newRules.push(rule)
|
||||
|
||||
if (rule.component === 'Button') {
|
||||
newRules.push({ ...rule, component: 'ScrollbarElement' })
|
||||
newRules.push({ ...rule, component: 'Tab' })
|
||||
}
|
||||
})
|
||||
return newRules
|
||||
}
|
||||
|
||||
const convertRadii = () => {
|
||||
const newRules = []
|
||||
Object.keys(data.radii).forEach(key => {
|
||||
if (!radiiKeys.has(key) || data.radii[key] === undefined) return null
|
||||
const originalRadius = data.radii[key]
|
||||
const rule = {}
|
||||
|
||||
switch (key) {
|
||||
case 'btn':
|
||||
rule.component = 'Button'
|
||||
break
|
||||
case 'tab':
|
||||
rule.component = 'Tab'
|
||||
break
|
||||
case 'input':
|
||||
rule.component = 'Input'
|
||||
break
|
||||
case 'checkbox':
|
||||
rule.component = 'Input'
|
||||
rule.variant = 'checkbox'
|
||||
break
|
||||
case 'panel':
|
||||
rule.component = 'Panel'
|
||||
break
|
||||
case 'avatar':
|
||||
rule.component = 'Avatar'
|
||||
break
|
||||
case 'avatarAlt':
|
||||
rule.component = 'Avatar'
|
||||
rule.variant = 'compact'
|
||||
break
|
||||
case 'tooltip':
|
||||
rule.component = 'Popover'
|
||||
break
|
||||
case 'attachment':
|
||||
rule.component = 'Attachment'
|
||||
break
|
||||
case 'ChatMessage':
|
||||
rule.component = 'Button'
|
||||
break
|
||||
}
|
||||
rule.directives = {
|
||||
roundness: originalRadius
|
||||
}
|
||||
newRules.push(rule)
|
||||
if (rule.component === 'Button') {
|
||||
newRules.push({ ...rule, component: 'ScrollbarElement' })
|
||||
newRules.push({ ...rule, component: 'Tab' })
|
||||
}
|
||||
})
|
||||
return newRules
|
||||
}
|
||||
|
||||
const convertFonts = () => {
|
||||
const newRules = []
|
||||
Object.keys(data.fonts).forEach(key => {
|
||||
if (!fontsKeys.has(key)) return
|
||||
const originalFont = data.fonts[key].family
|
||||
const rule = {}
|
||||
|
||||
switch (key) {
|
||||
case 'interface':
|
||||
case 'postCode':
|
||||
rule.component = 'Root'
|
||||
break
|
||||
case 'input':
|
||||
rule.component = 'Input'
|
||||
break
|
||||
case 'post':
|
||||
rule.component = 'RichContent'
|
||||
break
|
||||
}
|
||||
switch (key) {
|
||||
case 'interface':
|
||||
case 'input':
|
||||
case 'post':
|
||||
rule.directives = { '--font': 'generic | ' + originalFont }
|
||||
break
|
||||
case 'postCode':
|
||||
rule.directives = { '--monoFont': 'generic | ' + originalFont }
|
||||
newRules.push({ ...rule, component: 'RichContent' })
|
||||
break
|
||||
}
|
||||
newRules.push(rule)
|
||||
})
|
||||
return newRules
|
||||
}
|
||||
const convertShadows = () => {
|
||||
const newRules = []
|
||||
Object.keys(data.shadows).forEach(key => {
|
||||
if (!shadowsKeys.has(key)) return
|
||||
const originalShadow = data.shadows[key]
|
||||
const rule = {}
|
||||
|
||||
switch (key) {
|
||||
case 'panel':
|
||||
rule.component = 'Panel'
|
||||
break
|
||||
case 'topBar':
|
||||
rule.component = 'TopBar'
|
||||
break
|
||||
case 'popup':
|
||||
rule.component = 'Popover'
|
||||
break
|
||||
case 'avatar':
|
||||
rule.component = 'Avatar'
|
||||
break
|
||||
case 'avatarStatus':
|
||||
rule.component = 'Avatar'
|
||||
rule.parent = { component: 'Post' }
|
||||
break
|
||||
case 'panelHeader':
|
||||
rule.component = 'PanelHeader'
|
||||
break
|
||||
case 'button':
|
||||
rule.component = 'Button'
|
||||
break
|
||||
case 'buttonHover':
|
||||
rule.component = 'Button'
|
||||
rule.state = ['hover']
|
||||
break
|
||||
case 'buttonPressed':
|
||||
rule.component = 'Button'
|
||||
rule.state = ['pressed']
|
||||
break
|
||||
case 'input':
|
||||
rule.component = 'Input'
|
||||
break
|
||||
}
|
||||
rule.directives = {
|
||||
shadow: originalShadow
|
||||
}
|
||||
newRules.push(rule)
|
||||
if (key === 'topBar') {
|
||||
newRules.push({ ...rule, component: 'PanelHeader', parent: { component: 'MobileDrawer' } })
|
||||
}
|
||||
if (key === 'avatarStatus') {
|
||||
newRules.push({ ...rule, parent: { component: 'Notification' } })
|
||||
}
|
||||
if (key === 'buttonPressed') {
|
||||
newRules.push({ ...rule, state: ['toggled'] })
|
||||
}
|
||||
|
||||
if (rule.component === 'Button') {
|
||||
newRules.push({ ...rule, component: 'ScrollbarElement' })
|
||||
newRules.push({ ...rule, component: 'Tab' })
|
||||
}
|
||||
})
|
||||
return newRules
|
||||
}
|
||||
|
||||
const extendedRules = Object.entries(extendedBaseKeys).map(([prefix, keys]) => {
|
||||
if (nonComponentPrefixes.has(prefix)) return null
|
||||
const rule = {}
|
||||
if (prefix === 'alertPopup') {
|
||||
rule.component = 'Alert'
|
||||
rule.parent = { component: 'Popover' }
|
||||
} else if (prefix === 'selectedPost') {
|
||||
rule.component = 'Post'
|
||||
rule.state = ['selected']
|
||||
} else if (prefix === 'selectedMenu') {
|
||||
rule.component = 'MenuItem'
|
||||
rule.state = ['hover']
|
||||
} else if (prefix === 'chatMessageIncoming') {
|
||||
rule.component = 'ChatMessage'
|
||||
} else if (prefix === 'chatMessageOutgoing') {
|
||||
rule.component = 'ChatMessage'
|
||||
rule.variant = 'outgoing'
|
||||
} else if (prefix === 'panel') {
|
||||
rule.component = 'PanelHeader'
|
||||
} else if (prefix === 'topBar') {
|
||||
rule.component = 'TopBar'
|
||||
} else if (prefix === 'chatMessage') {
|
||||
rule.component = 'ChatMessage'
|
||||
} else if (prefix === 'poll') {
|
||||
rule.component = 'PollGraph'
|
||||
} else if (prefix === 'btn') {
|
||||
rule.component = 'Button'
|
||||
} else {
|
||||
rule.component = prefix[0].toUpperCase() + prefix.slice(1).toLowerCase()
|
||||
}
|
||||
return keys.map((key) => {
|
||||
if (!data.colors[key]) return null
|
||||
const leftoverKey = key.replace(prefix, '')
|
||||
const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g)
|
||||
const last = parts.slice(-1)[0]
|
||||
let newRule = { directives: {} }
|
||||
let variantArray = []
|
||||
|
||||
switch (last) {
|
||||
case 'Text':
|
||||
case 'Faint': // typo
|
||||
case 'Link':
|
||||
case 'Icon':
|
||||
case 'Greentext':
|
||||
case 'Cyantext':
|
||||
case 'Border':
|
||||
newRule.parent = rule
|
||||
newRule.directives.textColor = data.colors[key]
|
||||
newRule.directives.textAuto = 'no-auto'
|
||||
variantArray = parts.slice(0, -1)
|
||||
break
|
||||
default:
|
||||
newRule = { ...rule, directives: {} }
|
||||
newRule.directives.background = data.colors[key]
|
||||
variantArray = parts
|
||||
break
|
||||
}
|
||||
|
||||
if (last === 'Text' || last === 'Link') {
|
||||
const secondLast = parts.slice(-2)[0]
|
||||
if (secondLast === 'Light') {
|
||||
return null // unsupported
|
||||
} else if (secondLast === 'Faint') {
|
||||
newRule.state = ['faint']
|
||||
variantArray = parts.slice(0, -2)
|
||||
}
|
||||
}
|
||||
|
||||
switch (last) {
|
||||
case 'Text':
|
||||
case 'Link':
|
||||
case 'Icon':
|
||||
case 'Border':
|
||||
newRule.component = last
|
||||
break
|
||||
case 'Greentext':
|
||||
case 'Cyantext':
|
||||
newRule.component = 'FunText'
|
||||
newRule.variant = last.toLowerCase()
|
||||
break
|
||||
case 'Faint':
|
||||
newRule.component = 'Text'
|
||||
newRule.state = ['faint']
|
||||
break
|
||||
}
|
||||
|
||||
variantArray = variantArray.filter(x => x !== 'Bg')
|
||||
|
||||
if (last === 'Link' && prefix === 'selectedPost') {
|
||||
// selectedPost has typo - duplicate 'Post'
|
||||
variantArray = variantArray.filter(x => x !== 'Post')
|
||||
}
|
||||
|
||||
if (prefix === 'popover' && variantArray[0] === 'Post') {
|
||||
newRule.component = 'Post'
|
||||
newRule.parent = { component: 'Popover' }
|
||||
variantArray = variantArray.filter(x => x !== 'Post')
|
||||
}
|
||||
|
||||
if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') {
|
||||
newRule.parent = { component: 'Popover' }
|
||||
variantArray = variantArray.filter(x => x !== 'Popover')
|
||||
}
|
||||
|
||||
switch (prefix) {
|
||||
case 'btn':
|
||||
case 'input':
|
||||
case 'alert': {
|
||||
const hasPanel = variantArray.find(x => x === 'Panel')
|
||||
if (hasPanel) {
|
||||
newRule.parent = { component: 'PanelHeader' }
|
||||
variantArray = variantArray.filter(x => x !== 'Panel')
|
||||
}
|
||||
const hasTop = variantArray.find(x => x === 'Top') // TopBar
|
||||
if (hasTop) {
|
||||
newRule.parent = { component: 'TopBar' }
|
||||
variantArray = variantArray.filter(x => x !== 'Top' && x !== 'Bar')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (variantArray.length > 0) {
|
||||
if (prefix === 'btn') {
|
||||
newRule.state = variantArray.map(x => x.toLowerCase())
|
||||
} else {
|
||||
newRule.variant = variantArray[0].toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
if (newRule.component === 'Panel') {
|
||||
return [newRule, { ...newRule, component: 'MobileDrawer' }]
|
||||
} else if (newRule.component === 'Button') {
|
||||
return [newRule, { ...newRule, component: 'Tab' }, { ...newRule, component: 'ScrollbarElement' }]
|
||||
} else if (newRule.component === 'TopBar') {
|
||||
return [newRule, { ...newRule, parent: { component: 'MobileDrawer' }, component: 'PanelHeader' }]
|
||||
} else {
|
||||
return [newRule]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const flatExtRules = extendedRules.filter(x => x).reduce((acc, x) => [...acc, ...x], []).filter(x => x).reduce((acc, x) => [...acc, ...x], [])
|
||||
|
||||
return [generateRoot(), ...convertShadows(), ...convertRadii(), ...convertOpacity(), ...convertFonts(), ...flatExtRules]
|
||||
}
|
||||
92
src/services/theme_data/theme3_slot_functions.js
Normal file
92
src/services/theme_data/theme3_slot_functions.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { convert, brightness } from 'chromatism'
|
||||
import { alphaBlend, relativeLuminance } from '../color_convert/color_convert.js'
|
||||
|
||||
export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => {
|
||||
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-,.'"\s]*)\)/.exec(text).groups
|
||||
const args = argsString.split(/,/g).map(a => a.trim())
|
||||
|
||||
const func = functions[funcName]
|
||||
if (args.length < func.argsNeeded) {
|
||||
throw new Error(`$${funcName} requires at least ${func.argsNeeded} arguments, but ${args.length} were provided`)
|
||||
}
|
||||
return func.exec(args, { findColor, findShadow }, { dynamicVars, staticVars })
|
||||
}
|
||||
|
||||
export const colorFunctions = {
|
||||
alpha: {
|
||||
argsNeeded: 2,
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [color, amountArg] = args
|
||||
|
||||
const colorArg = convert(findColor(color, { dynamicVars, staticVars })).rgb
|
||||
const amount = Number(amountArg)
|
||||
return { ...colorArg, a: amount }
|
||||
}
|
||||
},
|
||||
blend: {
|
||||
argsNeeded: 3,
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [backgroundArg, amountArg, foregroundArg] = args
|
||||
|
||||
const background = convert(findColor(backgroundArg, { dynamicVars, staticVars })).rgb
|
||||
const foreground = convert(findColor(foregroundArg, { dynamicVars, staticVars })).rgb
|
||||
const amount = Number(amountArg)
|
||||
|
||||
return alphaBlend(background, amount, foreground)
|
||||
}
|
||||
},
|
||||
mod: {
|
||||
argsNeeded: 2,
|
||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||
const [colorArg, amountArg] = args
|
||||
|
||||
const color = convert(findColor(colorArg, { dynamicVars, staticVars })).rgb
|
||||
const amount = Number(amountArg)
|
||||
|
||||
const effectiveBackground = dynamicVars.lowerLevelBackground
|
||||
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
return brightness(amount * mod, color).rgb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const shadowFunctions = {
|
||||
borderSide: {
|
||||
argsNeeded: 3,
|
||||
exec: (args, { findColor }) => {
|
||||
const [color, side, alpha = '1', widthArg = '1', inset = 'inset'] = args
|
||||
|
||||
const width = Number(widthArg)
|
||||
const isInset = inset === 'inset'
|
||||
|
||||
const targetShadow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
color,
|
||||
alpha: Number(alpha),
|
||||
inset: isInset
|
||||
}
|
||||
|
||||
side.split('-').forEach((position) => {
|
||||
switch (position) {
|
||||
case 'left':
|
||||
targetShadow.x = width * (inset ? 1 : -1)
|
||||
break
|
||||
case 'right':
|
||||
targetShadow.x = -1 * width * (inset ? 1 : -1)
|
||||
break
|
||||
case 'top':
|
||||
targetShadow.y = width * (inset ? 1 : -1)
|
||||
break
|
||||
case 'bottom':
|
||||
targetShadow.y = -1 * width * (inset ? 1 : -1)
|
||||
break
|
||||
}
|
||||
})
|
||||
return [targetShadow]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { convert, brightness, contrastRatio } from 'chromatism'
|
||||
import { alphaBlendLayers, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
|
||||
import { rgb2hex, rgba2css, alphaBlendLayers, getTextColor, relativeLuminance, getCssColor } from '../color_convert/color_convert.js'
|
||||
import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
|
||||
|
||||
/*
|
||||
|
|
@ -407,3 +407,347 @@ export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({
|
|||
}
|
||||
}
|
||||
}, { colors: {}, opacity: {} })
|
||||
|
||||
export const composePreset = (colors, radii, shadows, fonts) => {
|
||||
return {
|
||||
rules: {
|
||||
...shadows.rules,
|
||||
...colors.rules,
|
||||
...radii.rules,
|
||||
...fonts.rules
|
||||
},
|
||||
theme: {
|
||||
...shadows.theme,
|
||||
...colors.theme,
|
||||
...radii.theme,
|
||||
...fonts.theme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generatePreset = (input) => {
|
||||
const colors = generateColors(input)
|
||||
return composePreset(
|
||||
colors,
|
||||
generateRadii(input),
|
||||
generateShadows(input, colors.theme.colors, colors.mod),
|
||||
generateFonts(input)
|
||||
)
|
||||
}
|
||||
|
||||
export const getCssShadow = (input, usesDropShadow) => {
|
||||
if (input.length === 0) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
return input
|
||||
.filter(_ => usesDropShadow ? _.inset : _)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
shad.blur,
|
||||
shad.spread
|
||||
].map(_ => _ + 'px').concat([
|
||||
getCssColor(shad.color, shad.alpha),
|
||||
shad.inset ? 'inset' : ''
|
||||
]).join(' ')).join(', ')
|
||||
}
|
||||
|
||||
const getCssShadowFilter = (input) => {
|
||||
if (input.length === 0) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
return input
|
||||
// drop-shadow doesn't support inset or spread
|
||||
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
|
||||
.map((shad) => [
|
||||
shad.x,
|
||||
shad.y,
|
||||
// drop-shadow's blur is twice as strong compared to box-shadow
|
||||
shad.blur / 2
|
||||
].map(_ => _ + 'px').concat([
|
||||
getCssColor(shad.color, shad.alpha)
|
||||
]).join(' '))
|
||||
.map(_ => `drop-shadow(${_})`)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export const generateColors = (themeData) => {
|
||||
const sourceColors = !themeData.themeEngineVersion
|
||||
? colors2to3(themeData.colors || themeData)
|
||||
: themeData.colors || themeData
|
||||
|
||||
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
|
||||
|
||||
const htmlColors = Object.entries(colors)
|
||||
.reduce((acc, [k, v]) => {
|
||||
if (!v) return acc
|
||||
acc.solid[k] = rgb2hex(v)
|
||||
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
|
||||
return acc
|
||||
}, { complete: {}, solid: {} })
|
||||
return {
|
||||
rules: {
|
||||
colors: Object.entries(htmlColors.complete)
|
||||
.filter(([k, v]) => v)
|
||||
.map(([k, v]) => `--${k}: ${v}`)
|
||||
.join(';')
|
||||
},
|
||||
theme: {
|
||||
colors: htmlColors.solid,
|
||||
opacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateRadii = (input) => {
|
||||
let inputRadii = input.radii || {}
|
||||
// v1 -> v2
|
||||
if (typeof input.btnRadius !== 'undefined') {
|
||||
inputRadii = Object
|
||||
.entries(input)
|
||||
.filter(([k, v]) => k.endsWith('Radius'))
|
||||
.reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {})
|
||||
}
|
||||
const radii = Object.entries(inputRadii).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, {
|
||||
btn: 4,
|
||||
input: 4,
|
||||
checkbox: 2,
|
||||
panel: 10,
|
||||
avatar: 5,
|
||||
avatarAlt: 50,
|
||||
tooltip: 2,
|
||||
attachment: 5,
|
||||
chatMessage: inputRadii.panel
|
||||
})
|
||||
|
||||
return {
|
||||
rules: {
|
||||
radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
|
||||
},
|
||||
theme: {
|
||||
radii
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateFonts = (input) => {
|
||||
const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
|
||||
acc[k] = v
|
||||
return acc
|
||||
}, acc[k])
|
||||
return acc
|
||||
}, {
|
||||
interface: {
|
||||
family: 'sans-serif'
|
||||
},
|
||||
input: {
|
||||
family: 'inherit'
|
||||
},
|
||||
post: {
|
||||
family: 'inherit'
|
||||
},
|
||||
postCode: {
|
||||
family: 'monospace'
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
rules: {
|
||||
fonts: Object
|
||||
.entries(fonts)
|
||||
.filter(([k, v]) => v)
|
||||
.map(([k, v]) => `--${k}Font: ${v.family}`).join(';')
|
||||
},
|
||||
theme: {
|
||||
fonts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const border = (top, shadow) => ({
|
||||
x: 0,
|
||||
y: top ? 1 : -1,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
color: shadow ? '#000000' : '#FFFFFF',
|
||||
alpha: 0.2,
|
||||
inset: true
|
||||
})
|
||||
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
||||
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
||||
const hoverGlow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '--faint',
|
||||
alpha: 1
|
||||
}
|
||||
|
||||
export const DEFAULT_SHADOWS = {
|
||||
panel: [{
|
||||
x: 1,
|
||||
y: 1,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
topBar: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 4,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.6
|
||||
}],
|
||||
popup: [{
|
||||
x: 2,
|
||||
y: 2,
|
||||
blur: 3,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.5
|
||||
}],
|
||||
avatar: [{
|
||||
x: 0,
|
||||
y: 1,
|
||||
blur: 8,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 0.7
|
||||
}],
|
||||
avatarStatus: [],
|
||||
panelHeader: [],
|
||||
button: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}, ...buttonInsetFakeBorders],
|
||||
buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
|
||||
buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
|
||||
input: [...inputInsetFakeBorders, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
inset: true,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}]
|
||||
}
|
||||
export const generateShadows = (input, colors) => {
|
||||
// TODO this is a small hack for `mod` to work with shadows
|
||||
// this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element
|
||||
const hackContextDict = {
|
||||
button: 'btn',
|
||||
panel: 'bg',
|
||||
top: 'topBar',
|
||||
popup: 'popover',
|
||||
avatar: 'bg',
|
||||
panelHeader: 'panel',
|
||||
input: 'input'
|
||||
}
|
||||
|
||||
const cleanInputShadows = Object.fromEntries(
|
||||
Object.entries(input.shadows || {})
|
||||
.map(([name, shadowSlot]) => [
|
||||
name,
|
||||
// defaulting color to black to avoid potential problems
|
||||
shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef }))
|
||||
])
|
||||
)
|
||||
const inputShadows = cleanInputShadows && !input.themeEngineVersion
|
||||
? shadows2to3(cleanInputShadows, input.opacity)
|
||||
: cleanInputShadows || {}
|
||||
const shadows = Object.entries({
|
||||
...DEFAULT_SHADOWS,
|
||||
...inputShadows
|
||||
}).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const slotFirstWord = slotName.replace(/[A-Z].*$/, '')
|
||||
const colorSlotName = hackContextDict[slotFirstWord]
|
||||
const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
color: rgb2hex(computeDynamicColor(
|
||||
def.color,
|
||||
(variableSlot) => convert(colors[variableSlot]).rgb,
|
||||
mod
|
||||
))
|
||||
}
|
||||
], [])
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
|
||||
return {
|
||||
rules: {
|
||||
shadows: Object
|
||||
.entries(shadows)
|
||||
// TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
|
||||
// convert all non-inset shadows into filter: drop-shadow() to boost performance
|
||||
.map(([k, v]) => [
|
||||
`--${k}Shadow: ${getCssShadow(v)}`,
|
||||
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
|
||||
`--${k}ShadowInset: ${getCssShadow(v, true)}`
|
||||
].join(';'))
|
||||
.join(';')
|
||||
},
|
||||
theme: {
|
||||
shadows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles compatibility issues when importing v2 theme's shadows to current format
|
||||
*
|
||||
* Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables
|
||||
*/
|
||||
export const shadows2to3 = (shadows, opacity) => {
|
||||
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
|
||||
const isDynamic = ({ color = '#000000' }) => color.startsWith('--')
|
||||
const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
|
||||
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
|
||||
...shadowAcc,
|
||||
{
|
||||
...def,
|
||||
alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha
|
||||
}
|
||||
], [])
|
||||
return { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const colors2to3 = (colors) => {
|
||||
return Object.entries(colors).reduce((acc, [slotName, color]) => {
|
||||
const btnPositions = ['', 'Panel', 'TopBar']
|
||||
switch (slotName) {
|
||||
case 'lightBg':
|
||||
return { ...acc, highlight: color }
|
||||
case 'btnText':
|
||||
return {
|
||||
...acc,
|
||||
...btnPositions
|
||||
.reduce(
|
||||
(statePositionAcc, position) =>
|
||||
({ ...statePositionAcc, ['btn' + position + 'Text']: color })
|
||||
, {}
|
||||
)
|
||||
}
|
||||
default:
|
||||
return { ...acc, [slotName]: color }
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
|
|
|||
474
src/services/theme_data/theme_data_3.service.js
Normal file
474
src/services/theme_data/theme_data_3.service.js
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
import { convert, brightness } from 'chromatism'
|
||||
import { flattenDeep } from 'lodash'
|
||||
import {
|
||||
alphaBlend,
|
||||
getTextColor,
|
||||
rgba2css,
|
||||
mixrgb,
|
||||
relativeLuminance
|
||||
} from '../color_convert/color_convert.js'
|
||||
|
||||
import {
|
||||
colorFunctions,
|
||||
shadowFunctions,
|
||||
process
|
||||
} from './theme3_slot_functions.js'
|
||||
|
||||
import {
|
||||
unroll,
|
||||
getAllPossibleCombinations,
|
||||
genericRuleToSelector,
|
||||
normalizeCombination,
|
||||
findRules
|
||||
} from './iss_utils.js'
|
||||
import { parseCssShadow } from './css_utils.js'
|
||||
|
||||
// Ensuring the order of components
|
||||
const components = {
|
||||
Root: null,
|
||||
Text: null,
|
||||
FunText: null,
|
||||
Link: null,
|
||||
Icon: null,
|
||||
Border: null,
|
||||
Panel: null,
|
||||
Chat: null,
|
||||
ChatMessage: null
|
||||
}
|
||||
|
||||
const findShadow = (shadows, { dynamicVars, staticVars }) => {
|
||||
return (shadows || []).map(shadow => {
|
||||
let targetShadow
|
||||
if (typeof shadow === 'string') {
|
||||
if (shadow.startsWith('$')) {
|
||||
targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars })
|
||||
} else if (shadow.startsWith('--')) {
|
||||
const [variable] = shadow.split(/,/g).map(str => str.trim()) // discarding modifier since it's not supported
|
||||
const variableSlot = variable.substring(2)
|
||||
return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
|
||||
} else {
|
||||
targetShadow = parseCssShadow(shadow)
|
||||
}
|
||||
} else {
|
||||
targetShadow = shadow
|
||||
}
|
||||
|
||||
const shadowArray = Array.isArray(targetShadow) ? targetShadow : [targetShadow]
|
||||
return shadowArray.map(s => ({
|
||||
...s,
|
||||
color: findColor(s.color, { dynamicVars, staticVars })
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
const findColor = (color, { dynamicVars, staticVars }) => {
|
||||
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 === 'stack') {
|
||||
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 }
|
||||
} else {
|
||||
const virtualSlot = variableSlot.replace(/^parent/, '')
|
||||
targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb
|
||||
}
|
||||
} else {
|
||||
switch (variableSlot) {
|
||||
case 'inheritedBackground':
|
||||
targetColor = convert(dynamicVars.inheritedBackground).rgb
|
||||
break
|
||||
case 'background':
|
||||
targetColor = convert(dynamicVars.background).rgb
|
||||
break
|
||||
default:
|
||||
targetColor = convert(staticVars[variableSlot]).rgb
|
||||
}
|
||||
}
|
||||
|
||||
if (modifier) {
|
||||
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 {
|
||||
targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars })
|
||||
} catch (e) {
|
||||
console.error('Failure executing color function', e)
|
||||
targetColor = '#FF00FF'
|
||||
}
|
||||
}
|
||||
// Color references other color
|
||||
return targetColor
|
||||
}
|
||||
|
||||
const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => {
|
||||
const opacity = directives.textOpacity
|
||||
const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb
|
||||
const textColor = convert(findColor(intendedTextColor, { dynamicVars, staticVars })).rgb
|
||||
if (opacity === null || opacity === undefined || opacity >= 1) {
|
||||
return convert(textColor).hex
|
||||
}
|
||||
if (opacity === 0) {
|
||||
return convert(backgroundColor).hex
|
||||
}
|
||||
const opacityMode = 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 })
|
||||
}
|
||||
}
|
||||
|
||||
// Loading all style.js[on] files dynamically
|
||||
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
|
||||
componentsContext.keys().forEach(key => {
|
||||
const component = componentsContext(key).default
|
||||
if (components[component.name] != null) {
|
||||
console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`)
|
||||
}
|
||||
components[component.name] = component
|
||||
})
|
||||
|
||||
const ruleToSelector = genericRuleToSelector(components)
|
||||
|
||||
export const init = (extraRuleset, ultimateBackgroundColor) => {
|
||||
const staticVars = {}
|
||||
const stacked = {}
|
||||
const computed = {}
|
||||
|
||||
const eagerRules = []
|
||||
const lazyRules = []
|
||||
|
||||
const rulesetUnsorted = [
|
||||
...Object.values(components)
|
||||
.map(c => (c.defaultRules || []).map(r => ({ component: c.name, ...r })))
|
||||
.reduce((acc, arr) => [...acc, ...arr], []),
|
||||
...extraRuleset
|
||||
].map(rule => {
|
||||
normalizeCombination(rule)
|
||||
let currentParent = rule.parent
|
||||
while (currentParent) {
|
||||
normalizeCombination(currentParent)
|
||||
currentParent = currentParent.parent
|
||||
}
|
||||
|
||||
return rule
|
||||
})
|
||||
|
||||
const ruleset = rulesetUnsorted
|
||||
.map((data, index) => ({ data, index }))
|
||||
.sort(({ data: a, index: ai }, { data: b, index: bi }) => {
|
||||
const parentsA = unroll(a).length
|
||||
const parentsB = unroll(b).length
|
||||
|
||||
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 parentsA - parentsB
|
||||
})
|
||||
.map(({ data }) => data)
|
||||
|
||||
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
|
||||
|
||||
let counter = 0
|
||||
const promises = []
|
||||
const processInnerComponent = (component, rules, parent) => {
|
||||
const addRule = (rule) => {
|
||||
rules.push(rule)
|
||||
}
|
||||
|
||||
const parentSelector = ruleToSelector(parent, true)
|
||||
// const parentList = parent ? unroll(parent).reverse().map(c => c.component) : []
|
||||
// if (!component.virtual) {
|
||||
// const path = [...parentList, component.name].join(' > ')
|
||||
// console.log('Component ' + path + ' process starting')
|
||||
// }
|
||||
// const t0 = performance.now()
|
||||
const {
|
||||
validInnerComponents = [],
|
||||
states: originalStates = {},
|
||||
variants: originalVariants = {},
|
||||
name
|
||||
} = component
|
||||
|
||||
// Normalizing states and variants to always include "normal"
|
||||
const states = { normal: '', ...originalStates }
|
||||
const variants = { normal: '', ...originalVariants }
|
||||
const innerComponents = (validInnerComponents).map(name => {
|
||||
const result = components[name]
|
||||
if (result === undefined) console.error(`Component ${component.name} references a component ${name} which does not exist!`)
|
||||
return result
|
||||
})
|
||||
|
||||
// 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 stateVariantCombination = Object.keys(variants).map(variant => {
|
||||
return stateCombinations.map(state => ({ variant, state }))
|
||||
}).reduce((acc, x) => [...acc, ...x], [])
|
||||
|
||||
stateVariantCombination.forEach(combination => {
|
||||
counter++
|
||||
// const tt0 = performance.now()
|
||||
|
||||
combination.component = component.name
|
||||
const soloSelector = ruleToSelector(combination, true)
|
||||
const soloCssSelector = ruleToSelector(combination)
|
||||
const selector = [parentSelector, soloSelector].filter(x => x).join(' ')
|
||||
const cssSelector = [parentSelector, soloCssSelector].filter(x => x).join(' ')
|
||||
|
||||
const lowerLevelSelector = parentSelector
|
||||
const lowerLevelBackground = computed[lowerLevelSelector]?.background
|
||||
const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
|
||||
const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
|
||||
|
||||
const dynamicVars = computed[selector] || {
|
||||
lowerLevelBackground,
|
||||
lowerLevelVirtualDirectives,
|
||||
lowerLevelVirtualDirectivesRaw
|
||||
}
|
||||
|
||||
// Inheriting all of the applicable rules
|
||||
const existingRules = ruleset.filter(findRules({ component: component.name, ...combination, parent }))
|
||||
const computedDirectives = existingRules.map(r => r.directives).reduce((acc, directives) => ({ ...acc, ...directives }), {})
|
||||
const computedRule = {
|
||||
component: component.name,
|
||||
...combination,
|
||||
parent,
|
||||
directives: computedDirectives
|
||||
}
|
||||
|
||||
computed[selector] = computed[selector] || {}
|
||||
computed[selector].computedRule = computedRule
|
||||
computed[selector].dynamicVars = dynamicVars
|
||||
|
||||
if (virtualComponents.has(component.name)) {
|
||||
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('')
|
||||
|
||||
let inheritedTextColor = computedDirectives.textColor
|
||||
let inheritedTextAuto = computedDirectives.textAuto
|
||||
let inheritedTextOpacity = computedDirectives.textOpacity
|
||||
let inheritedTextOpacityMode = computedDirectives.textOpacityMode
|
||||
const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ')
|
||||
const lowerLevelTextRule = computed[lowerLevelTextSelector]
|
||||
|
||||
if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) {
|
||||
inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor
|
||||
inheritedTextAuto = computedDirectives.textAuto ?? lowerLevelTextRule.textAuto
|
||||
inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
|
||||
inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode
|
||||
}
|
||||
|
||||
const newTextRule = {
|
||||
...computedRule,
|
||||
directives: {
|
||||
...computedRule.directives,
|
||||
textColor: inheritedTextColor,
|
||||
textAuto: inheritedTextAuto ?? 'preserve',
|
||||
textOpacity: inheritedTextOpacity,
|
||||
textOpacityMode: inheritedTextOpacityMode
|
||||
}
|
||||
}
|
||||
|
||||
dynamicVars.inheritedBackground = lowerLevelBackground
|
||||
dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb
|
||||
|
||||
const intendedTextColor = convert(findColor(inheritedTextColor, { dynamicVars, staticVars })).rgb
|
||||
const textColor = newTextRule.directives.textAuto === 'no-auto'
|
||||
? intendedTextColor
|
||||
: getTextColor(
|
||||
convert(stacked[lowerLevelSelector]).rgb,
|
||||
intendedTextColor,
|
||||
newTextRule.directives.textAuto === 'preserve'
|
||||
)
|
||||
|
||||
// Updating previously added rule
|
||||
const earlyLowerLevelRules = rules.filter(findRules(parent, true))
|
||||
const earlyLowerLevelRule = earlyLowerLevelRules.slice(-1)[0]
|
||||
|
||||
const virtualDirectives = earlyLowerLevelRule.virtualDirectives || {}
|
||||
const virtualDirectivesRaw = earlyLowerLevelRule.virtualDirectivesRaw || {}
|
||||
|
||||
// Storing color data in lower layer to use as custom css properties
|
||||
virtualDirectives[virtualName] = getTextColorAlpha(newTextRule.directives, textColor, dynamicVars)
|
||||
virtualDirectivesRaw[virtualName] = textColor
|
||||
earlyLowerLevelRule.virtualDirectives = virtualDirectives
|
||||
earlyLowerLevelRule.virtualDirectivesRaw = virtualDirectivesRaw
|
||||
computed[lowerLevelSelector].virtualDirectives = virtualDirectives
|
||||
computed[lowerLevelSelector].virtualDirectivesRaw = virtualDirectivesRaw
|
||||
} else {
|
||||
computed[selector] = computed[selector] || {}
|
||||
|
||||
// TODO: DEFAULT TEXT COLOR
|
||||
const lowerLevelStackedBackground = stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb
|
||||
|
||||
if (computedDirectives.background) {
|
||||
let inheritRule = null
|
||||
const variantRules = ruleset.filter(findRules({ component: component.name, variant: combination.variant, parent }))
|
||||
const lastVariantRule = variantRules[variantRules.length - 1]
|
||||
if (lastVariantRule) {
|
||||
inheritRule = lastVariantRule
|
||||
} else {
|
||||
const normalRules = ruleset.filter(findRules({ component: component.name, parent }))
|
||||
const lastNormalRule = normalRules[normalRules.length - 1]
|
||||
inheritRule = lastNormalRule
|
||||
}
|
||||
|
||||
const inheritSelector = ruleToSelector({ ...inheritRule, parent }, true)
|
||||
const inheritedBackground = computed[inheritSelector].background
|
||||
|
||||
dynamicVars.inheritedBackground = inheritedBackground
|
||||
|
||||
const rgb = convert(findColor(computedDirectives.background, { dynamicVars, staticVars })).rgb
|
||||
|
||||
if (!stacked[selector]) {
|
||||
let blend
|
||||
const alpha = computedDirectives.opacity ?? 1
|
||||
if (alpha >= 1) {
|
||||
blend = rgb
|
||||
} else if (alpha <= 0) {
|
||||
blend = lowerLevelStackedBackground
|
||||
} else {
|
||||
blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground)
|
||||
}
|
||||
stacked[selector] = blend
|
||||
computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 }
|
||||
}
|
||||
}
|
||||
|
||||
if (computedDirectives.shadow) {
|
||||
dynamicVars.shadow = flattenDeep(findShadow(flattenDeep(computedDirectives.shadow), { dynamicVars, staticVars }))
|
||||
}
|
||||
|
||||
if (!stacked[selector]) {
|
||||
computedDirectives.background = 'transparent'
|
||||
computedDirectives.opacity = 0
|
||||
stacked[selector] = lowerLevelStackedBackground
|
||||
computed[selector].background = { ...lowerLevelStackedBackground, a: 0 }
|
||||
}
|
||||
|
||||
dynamicVars.stacked = stacked[selector]
|
||||
dynamicVars.background = computed[selector].background
|
||||
|
||||
const dynamicSlots = Object.entries(computedDirectives).filter(([k, v]) => k.startsWith('--'))
|
||||
|
||||
dynamicSlots.forEach(([k, v]) => {
|
||||
const [type, ...value] = v.split('|').map(x => x.trim()) // woah, Extreme!
|
||||
switch (type) {
|
||||
case 'color': {
|
||||
const color = findColor(value[0], { dynamicVars, staticVars })
|
||||
dynamicVars[k] = color
|
||||
if (component.name === 'Root') {
|
||||
staticVars[k.substring(2)] = color
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'shadow': {
|
||||
const shadow = value
|
||||
dynamicVars[k] = shadow
|
||||
if (component.name === 'Root') {
|
||||
staticVars[k.substring(2)] = shadow
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'generic': {
|
||||
dynamicVars[k] = value
|
||||
if (component.name === 'Root') {
|
||||
staticVars[k.substring(2)] = value
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addRule({
|
||||
dynamicVars,
|
||||
selector: cssSelector,
|
||||
component: component.name,
|
||||
...combination,
|
||||
parent,
|
||||
directives: computedDirectives
|
||||
})
|
||||
}
|
||||
|
||||
innerComponents.forEach(innerComponent => {
|
||||
if (innerComponent.lazy) {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
processInnerComponent(innerComponent, lazyRules, { parent, component: name, ...combination })
|
||||
resolve()
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
}, 0)
|
||||
}))
|
||||
} else {
|
||||
processInnerComponent(innerComponent, rules, { parent, component: name, ...combination })
|
||||
}
|
||||
})
|
||||
// const tt1 = performance.now()
|
||||
// if (!component.virtual) {
|
||||
// console.log('State-variant ' + combination.variant + ' : ' + combination.state.join('+') + ' procession time: ' + (tt1 - tt0) + 'ms')
|
||||
// }
|
||||
})
|
||||
|
||||
// const t1 = performance.now()
|
||||
// if (!component.virtual) {
|
||||
// const path = [...parentList, component.name].join(' > ')
|
||||
// console.log('Component ' + path + ' procession time: ' + (t1 - t0) + 'ms')
|
||||
// }
|
||||
}
|
||||
|
||||
processInnerComponent(components.Root, eagerRules)
|
||||
console.debug('Eager combinations processed:' + counter)
|
||||
const lazyExec = Promise.all(promises).then(() => {
|
||||
console.debug('Total combinations processed: ' + counter)
|
||||
}).then(() => lazyRules)
|
||||
|
||||
return {
|
||||
lazy: lazyExec,
|
||||
eager: eagerRules,
|
||||
staticVars
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue