akkoma-fe/src/services/style_setter/style_setter.js

457 lines
12 KiB
JavaScript
Raw Normal View History

import { times } from 'lodash'
2020-01-12 01:44:06 +00:00
import { convert } from 'chromatism'
import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js'
2020-01-19 22:34:49 +00:00
import { getColors, computeDynamicColor } from '../theme_data/theme_data.service.js'
// While this is not used anymore right now, I left it in if we want to do custom
// styles that aren't just colors, so user can pick from a few different distinct
// styles as well as set their own colors in the future.
export const setStyle = (href) => {
2017-01-16 16:44:26 +00:00
/***
What's going on here?
I want to make it easy for admins to style this application. To have
a good set of default themes, I chose the system from base16
(https://chriskempson.github.io/base16/) to style all elements. They
all have the base00..0F classes. So the only thing an admin needs to
do to style Pleroma is to change these colors in that one css file.
Some default things (body text color, link color) need to be set dy-
namically, so this is done here by waiting for the stylesheet to be
loaded and then creating an element with the respective classes.
It is a bit weird, but should make life for admins somewhat easier.
***/
const head = document.head
const body = document.body
body.classList.add('hidden')
2017-01-16 16:44:26 +00:00
const cssEl = document.createElement('link')
cssEl.setAttribute('rel', 'stylesheet')
cssEl.setAttribute('href', href)
head.appendChild(cssEl)
const setDynamic = () => {
const baseEl = document.createElement('div')
2017-01-20 22:39:38 +00:00
body.appendChild(baseEl)
let colors = {}
times(16, (n) => {
const name = `base0${n.toString(16).toUpperCase()}`
baseEl.setAttribute('class', name)
const color = window.getComputedStyle(baseEl).getPropertyValue('color')
colors[name] = color
})
body.removeChild(baseEl)
2017-01-16 16:44:26 +00:00
const styleEl = document.createElement('style')
head.appendChild(styleEl)
// const styleSheet = styleEl.sheet
2017-01-16 16:44:26 +00:00
body.classList.remove('hidden')
2017-01-16 16:44:26 +00:00
}
cssEl.addEventListener('load', setDynamic)
}
export const applyTheme = (input) => {
const { rules } = generatePreset(input)
const head = document.head
const body = document.body
body.classList.add('hidden')
const styleEl = document.createElement('style')
head.appendChild(styleEl)
const styleSheet = styleEl.sheet
styleSheet.toString()
2018-11-19 17:22:46 +00:00
styleSheet.insertRule(`body { ${rules.radii} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
2018-11-25 18:48:16 +00:00
styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
body.classList.remove('hidden')
}
2020-01-13 00:08:39 +00:00
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
2018-12-02 12:10:18 +00:00
.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(' ')
}
2020-01-12 02:00:41 +00:00
export const generateColors = (themeData) => {
2020-01-22 21:26:24 +00:00
const sourceColors = !themeData.themeEngineVersion
2020-01-23 20:26:49 +00:00
? colors2to3(themeData.colors || themeData)
2020-01-22 21:26:24 +00:00
: themeData.colors || themeData
const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l
const mod = isLightOnDark ? 1 : -1
2020-01-16 18:53:05 +00:00
const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}, mod)
const htmlColors = Object.entries(colors)
2019-07-05 07:02:14 +00:00
.reduce((acc, [k, v]) => {
if (!v) return acc
acc.solid[k] = rgb2hex(v)
2020-01-12 01:44:06 +00:00
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
2019-07-05 07:02:14 +00:00
return acc
}, { complete: {}, solid: {} })
return {
2018-11-19 17:22:46 +00:00
rules: {
colors: Object.entries(htmlColors.complete)
.filter(([k, v]) => v)
.map(([k, v]) => `--${k}: ${v}`)
.join(';')
},
2018-10-04 15:16:14 +00:00
theme: {
colors: htmlColors.solid,
2018-11-19 17:22:46 +00:00
opacity
2020-01-19 22:34:49 +00:00
},
mod
2018-11-19 17:22:46 +00:00
}
}
2020-01-12 02:00:41 +00:00
export const generateRadii = (input) => {
2018-11-23 06:14:52 +00:00
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
2018-11-21 00:14:59 +00:00
return acc
}, {
2018-11-19 17:22:46 +00:00
btn: 4,
input: 4,
checkbox: 2,
2018-11-19 17:22:46 +00:00
panel: 10,
avatar: 5,
avatarAlt: 50,
tooltip: 2,
2018-11-21 00:14:59 +00:00
attachment: 5
})
2018-11-19 17:22:46 +00:00
return {
rules: {
radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
},
theme: {
2018-10-04 15:16:14 +00:00
radii
}
}
}
2020-01-12 02:00:41 +00:00
export const generateFonts = (input) => {
2018-11-25 18:48:16 +00:00
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
}
}
}
2020-01-19 22:34:49 +00:00
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,
2020-01-19 22:34:49 +00:00
color: '#000000',
alpha: 0.6
}],
topBar: [{
x: 0,
y: 0,
blur: 4,
spread: 0,
2020-01-19 22:34:49 +00:00
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
2020-01-19 22:34:49 +00:00
}, ...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, mod) => {
2020-01-23 20:26:49 +00:00
const inputShadows = input.shadows && !input.themeEngineVersion
2020-01-22 21:26:24 +00:00
? shadows2to3(input.shadows)
: input.shadows || {}
2020-01-19 22:34:49 +00:00
const shadows = Object.entries({
...DEFAULT_SHADOWS,
2020-01-22 21:26:24 +00:00
...inputShadows
}).reduce((shadowsAcc, [slotName, shadowDefs]) => {
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
2020-01-19 22:34:49 +00:00
...shadowAcc,
{
...def,
color: rgb2hex(computeDynamicColor(
def.color,
(variableSlot) => convert(colors[variableSlot]).rgb,
mod
))
}
], [])
return { ...shadowsAcc, [slotName]: newShadow }
}, {})
2018-11-19 17:22:46 +00:00
return {
rules: {
shadows: Object
.entries(shadows)
// TODO for v2.1: 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(';')
2018-11-19 17:22:46 +00:00
},
theme: {
shadows
}
}
}
2020-01-12 02:00:41 +00:00
export const composePreset = (colors, radii, shadows, fonts) => {
2018-11-19 17:22:46 +00:00
return {
rules: {
...shadows.rules,
...colors.rules,
2018-11-25 18:48:16 +00:00
...radii.rules,
...fonts.rules
2018-11-19 17:22:46 +00:00
},
theme: {
...shadows.theme,
...colors.theme,
2018-11-25 18:48:16 +00:00
...radii.theme,
...fonts.theme
2018-11-19 17:22:46 +00:00
}
}
}
2020-01-19 22:34:49 +00:00
export const generatePreset = (input) => {
const colors = generateColors(input)
return composePreset(
colors,
generateRadii(input),
generateShadows(input, colors.theme.colors, colors.mod),
generateFonts(input)
)
}
2018-11-19 17:22:46 +00:00
2020-01-12 02:00:41 +00:00
export const getThemes = () => {
2018-12-10 22:38:20 +00:00
return window.fetch('/static/styles.json')
.then((data) => data.json())
.then((themes) => {
return Object.entries(themes).map(([k, v]) => {
let promise = null
2018-12-10 22:38:20 +00:00
if (typeof v === 'object') {
promise = Promise.resolve(v)
2018-12-10 22:38:20 +00:00
} else if (typeof v === 'string') {
promise = window.fetch(v)
2018-12-10 22:38:20 +00:00
.then((data) => data.json())
.catch((e) => {
console.error(e)
return null
2018-12-10 22:38:20 +00:00
})
}
return [k, promise]
})
2018-12-10 22:38:20 +00:00
})
.then((promises) => {
return promises
.reduce((acc, [k, v]) => {
acc[k] = v
return acc
}, {})
})
}
2020-01-22 21:26:24 +00:00
export const colors2to3 = (colors) => {
return Object.entries(colors).reduce((acc, [slotName, color]) => {
const btnStates = ['', 'Pressed', 'Disabled', 'Toggled']
const btnPositions = ['', 'Panel', 'TopBar']
switch (slotName) {
case 'lightBg':
return { ...acc, highlight: color }
case 'btn':
return {
...acc,
...btnStates.reduce((stateAcc, state) => ({ ...stateAcc, ['btn' + state]: color }), {})
}
case 'btnText':
return {
...acc,
...btnPositions
.map(position => btnStates.map(state => state + position))
.flat()
.reduce(
(statePositionAcc, statePosition) =>
({ ...statePositionAcc, ['btn' + statePosition + 'Text']: color })
, {}
)
}
default:
return { ...acc, [slotName]: color }
}
}, {})
}
2018-12-10 22:38:20 +00:00
/**
* 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) => {
return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
const isDynamic = ({ color }) => color.startsWith('--')
const newShadow = shadowDefs.reduce((shadowAcc, def) => [
...shadowAcc,
{
...def,
alpha: isDynamic(def) ? 1 : def.alpha
}
], [])
return { ...shadowsAcc, [slotName]: newShadow }
}, {})
}
export const getPreset = (val) => {
return getThemes()
2020-01-19 22:34:49 +00:00
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
.then((theme) => {
const isV1 = Array.isArray(theme)
const data = isV1 ? {} : theme.theme
if (isV1) {
const bg = hex2rgb(theme[1])
const fg = hex2rgb(theme[2])
const text = hex2rgb(theme[3])
const link = hex2rgb(theme[4])
const cRed = hex2rgb(theme[5] || '#FF0000')
const cGreen = hex2rgb(theme[6] || '#00FF00')
const cBlue = hex2rgb(theme[7] || '#0000FF')
const cOrange = hex2rgb(theme[8] || '#E3FF00')
data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
}
return { theme: data, source: theme.source }
})
2017-01-16 16:44:26 +00:00
}
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))