2017-11-20 20:45:09 +00:00
|
|
|
import { times } from 'lodash'
|
2018-11-14 16:39:17 +00:00
|
|
|
import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
|
2019-12-28 16:49:35 +00:00
|
|
|
import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend, alphaBlendLayers } from '../color_convert/color_convert.js'
|
2017-02-22 20:14:55 +00:00
|
|
|
|
2020-01-02 18:36:10 +00:00
|
|
|
export const CURRENT_VERSION = 3
|
2020-01-06 20:55:14 +00:00
|
|
|
/* This is a definition of all layer combinations
|
|
|
|
* each key is a topmost layer, each value represents layer underneath
|
|
|
|
* this is essentially a simplified tree
|
|
|
|
*/
|
|
|
|
export const LAYERS = {
|
|
|
|
undelay: null, // root
|
|
|
|
topBar: null, // no transparency support
|
|
|
|
badge: null, // no transparency support
|
|
|
|
fg: null,
|
|
|
|
bg: 'underlay',
|
|
|
|
panel: 'bg',
|
|
|
|
btn: 'bg',
|
|
|
|
btnPanel: 'panel',
|
|
|
|
btnTopBar: 'topBar',
|
|
|
|
input: 'bg',
|
|
|
|
inputPanel: 'panel',
|
|
|
|
inputTopBar: 'topBar',
|
|
|
|
alert: 'bg',
|
|
|
|
alertPanel: 'panel'
|
|
|
|
}
|
|
|
|
|
|
|
|
export const SLOT_INHERITANCE = {
|
|
|
|
bg: null,
|
|
|
|
fg: null,
|
|
|
|
text: null,
|
|
|
|
underlay: '#000000',
|
|
|
|
link: '--accent',
|
|
|
|
accent: '--link',
|
|
|
|
faint: '--text',
|
|
|
|
faintLink: '--link',
|
|
|
|
|
|
|
|
cBlue: '#0000ff',
|
|
|
|
cRed: '#FF0000',
|
|
|
|
cGreen: '#00FF00',
|
|
|
|
cOrange: '#E3FF00',
|
|
|
|
|
|
|
|
lightBg: {
|
|
|
|
depends: ['bg'],
|
|
|
|
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
|
|
|
},
|
|
|
|
lightText: {
|
|
|
|
depends: ['text'],
|
|
|
|
color: (mod, text) => brightness(20 * mod, text).rgb
|
|
|
|
},
|
|
|
|
|
|
|
|
border: {
|
|
|
|
depends: 'fg',
|
|
|
|
color: (mod, fg) => brightness(2 * mod, fg).rgb
|
|
|
|
},
|
|
|
|
|
|
|
|
linkBg: {
|
|
|
|
depends: ['accent', 'bg'],
|
|
|
|
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg).rgb
|
|
|
|
},
|
|
|
|
|
|
|
|
icon: {
|
|
|
|
depends: ['bg', 'text'],
|
|
|
|
color: (mod, bg, text) => mixrgb(bg, text)
|
|
|
|
},
|
|
|
|
|
|
|
|
// Foreground
|
|
|
|
fgText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['text'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'fg',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
fgLink: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['link'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'fg',
|
|
|
|
textColor: 'preserve'
|
|
|
|
},
|
|
|
|
|
|
|
|
// Panel header
|
|
|
|
panel: '--fg',
|
|
|
|
panelText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['fgText'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'panel',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
panelFaint: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['fgText'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'panel',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
panelLink: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['fgLink'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'panel',
|
|
|
|
textColor: 'preserve'
|
|
|
|
},
|
|
|
|
|
|
|
|
// Top bar
|
|
|
|
topBar: '--fg',
|
|
|
|
topBarText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['fgText'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'topBar',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
topBarLink: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['fgLink'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'topBar',
|
|
|
|
textColor: 'preserve'
|
|
|
|
},
|
|
|
|
|
|
|
|
// Buttons
|
|
|
|
btn: '--fg',
|
|
|
|
btnText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['fgText'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'btn'
|
|
|
|
},
|
|
|
|
btnPanelText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['panelText'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'btnPanel',
|
|
|
|
variant: 'btn',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
btnTopBarText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['topBarText'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'btnTopBar',
|
|
|
|
variant: 'btn',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
|
|
|
|
// Input fields
|
|
|
|
input: '--fg',
|
|
|
|
inputText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['text'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'input',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
inputPanelText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['panelText'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'inputPanel',
|
|
|
|
variant: 'input',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
inputTopbarText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['topBarText'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'inputTopBar',
|
|
|
|
variant: 'input',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
|
|
|
|
alertError: '--cRed',
|
|
|
|
alertErrorText: {
|
|
|
|
depends: ['text', 'alertError'],
|
|
|
|
layer: 'alert',
|
|
|
|
variant: 'alertError',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
alertErrorPanelText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['panelText', 'alertError'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'alertPanel',
|
|
|
|
variant: 'alertError',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
|
|
|
|
alertWarning: '--cOrange',
|
|
|
|
alertWarningText: {
|
|
|
|
depends: ['text', 'alertWarning'],
|
|
|
|
layer: 'alert',
|
|
|
|
variant: 'alertWarning',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
alertWarningPanelText: {
|
2020-01-11 21:07:41 +00:00
|
|
|
depends: ['panelText', 'alertWarning'],
|
2020-01-06 20:55:14 +00:00
|
|
|
layer: 'alertPanel',
|
|
|
|
variant: 'alertWarning',
|
|
|
|
textColor: true
|
|
|
|
},
|
|
|
|
|
|
|
|
badgeNotification: '--cRed',
|
|
|
|
badgeNotificationText: {
|
|
|
|
depends: ['text', 'badgeNotification'],
|
|
|
|
layer: 'badge',
|
|
|
|
variant: 'badgeNotification',
|
2020-01-11 21:07:41 +00:00
|
|
|
textColor: 'bw'
|
2020-01-06 20:55:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-11 21:07:41 +00:00
|
|
|
export const getLayersArray = (layer, data = LAYERS) => {
|
|
|
|
let array = [layer]
|
|
|
|
let parent = data[layer]
|
|
|
|
while (parent) {
|
|
|
|
array.unshift(parent)
|
|
|
|
parent = data[parent]
|
|
|
|
}
|
|
|
|
return array
|
|
|
|
}
|
|
|
|
|
|
|
|
export const getLayers = (layer, variant = layer, colors, opacity) => {
|
|
|
|
return getLayersArray(layer).map((currentLayer) => ([
|
|
|
|
currentLayer === layer
|
|
|
|
? colors[variant]
|
|
|
|
: colors[currentLayer],
|
|
|
|
opacity[currentLayer]
|
|
|
|
]))
|
|
|
|
}
|
|
|
|
|
2020-01-06 20:55:14 +00:00
|
|
|
const getDependencies = (key, inheritance) => {
|
|
|
|
const data = inheritance[key]
|
|
|
|
if (typeof data === 'string' && data.startsWith('--')) {
|
|
|
|
return [data.substring(2)]
|
|
|
|
} else {
|
|
|
|
if (data === null) return []
|
2020-01-11 21:07:41 +00:00
|
|
|
const { depends, layer, variant } = data
|
|
|
|
const layerDeps = layer
|
|
|
|
? getLayersArray(layer).map(currentLayer => {
|
|
|
|
return currentLayer === layer
|
|
|
|
? variant || layer
|
|
|
|
: currentLayer
|
|
|
|
})
|
|
|
|
: []
|
2020-01-06 20:55:14 +00:00
|
|
|
if (Array.isArray(depends)) {
|
2020-01-11 21:07:41 +00:00
|
|
|
return [...depends, ...layerDeps]
|
2020-01-06 20:55:14 +00:00
|
|
|
} else {
|
2020-01-11 21:07:41 +00:00
|
|
|
return [...layerDeps]
|
2020-01-06 20:55:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const topoSort = (
|
|
|
|
inheritance = SLOT_INHERITANCE,
|
|
|
|
getDeps = getDependencies
|
|
|
|
) => {
|
|
|
|
// This is an implementation of https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
|
|
|
|
|
|
|
|
const allKeys = Object.keys(inheritance)
|
|
|
|
const whites = new Set(allKeys)
|
|
|
|
const grays = new Set()
|
|
|
|
const blacks = new Set()
|
|
|
|
const unprocessed = [...allKeys]
|
|
|
|
const output = []
|
|
|
|
|
|
|
|
const step = (node) => {
|
|
|
|
if (whites.has(node)) {
|
|
|
|
// Make node "gray"
|
|
|
|
whites.delete(node)
|
|
|
|
grays.add(node)
|
|
|
|
// Do step for each node connected to it (one way)
|
|
|
|
getDeps(node, inheritance).forEach(step)
|
|
|
|
// Make node "black"
|
|
|
|
grays.delete(node)
|
|
|
|
blacks.add(node)
|
|
|
|
// Put it into the output list
|
|
|
|
output.push(node)
|
|
|
|
} else if (grays.has(node)) {
|
|
|
|
console.debug('Cyclic depenency in topoSort, ignoring')
|
|
|
|
output.push(node)
|
|
|
|
} else if (blacks.has(node)) {
|
|
|
|
// do nothing
|
|
|
|
} else {
|
|
|
|
throw new Error('Unintended condition in topoSort!')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
while (unprocessed.length > 0) {
|
|
|
|
step(unprocessed.pop())
|
|
|
|
}
|
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
|
|
|
export const SLOT_ORDERED = topoSort(SLOT_INHERITANCE)
|
|
|
|
|
2017-11-17 15:24:42 +00:00
|
|
|
// 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.
|
|
|
|
|
|
|
|
const setStyle = (href, commit) => {
|
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
|
2019-08-27 16:17:10 +00:00
|
|
|
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)
|
2017-02-22 20:14:55 +00:00
|
|
|
|
|
|
|
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)
|
2018-03-31 18:14:36 +00:00
|
|
|
// const styleSheet = styleEl.sheet
|
2017-01-16 16:44:26 +00:00
|
|
|
|
2019-08-27 16:17:10 +00:00
|
|
|
body.classList.remove('hidden')
|
2017-01-16 16:44:26 +00:00
|
|
|
}
|
2017-11-13 23:37:49 +00:00
|
|
|
|
2017-11-17 15:24:42 +00:00
|
|
|
cssEl.addEventListener('load', setDynamic)
|
|
|
|
}
|
2017-11-13 23:37:49 +00:00
|
|
|
|
2018-10-02 18:43:58 +00:00
|
|
|
const rgb2rgba = function (rgba) {
|
|
|
|
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
|
|
|
|
}
|
|
|
|
|
2018-11-14 16:39:17 +00:00
|
|
|
const getTextColor = function (bg, text, preserve) {
|
2018-10-02 18:43:58 +00:00
|
|
|
const bgIsLight = convert(bg).hsl.l > 50
|
|
|
|
const textIsLight = convert(text).hsl.l > 50
|
|
|
|
|
2020-01-11 21:07:41 +00:00
|
|
|
console.log(bgIsLight, textIsLight)
|
|
|
|
|
2018-10-02 18:43:58 +00:00
|
|
|
if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
|
|
|
|
const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
|
2018-11-14 16:39:17 +00:00
|
|
|
const result = Object.assign(base, invertLightness(text).rgb)
|
|
|
|
if (!preserve && getContrastRatio(bg, result) < 4.5) {
|
2019-12-28 16:49:35 +00:00
|
|
|
// B&W
|
2018-11-14 16:39:17 +00:00
|
|
|
return contrastRatio(bg, text).rgb
|
|
|
|
}
|
2019-12-28 16:49:35 +00:00
|
|
|
// Inverted color
|
2018-11-14 16:39:17 +00:00
|
|
|
return result
|
2018-10-02 18:43:58 +00:00
|
|
|
}
|
|
|
|
return text
|
|
|
|
}
|
|
|
|
|
2018-12-10 22:39:18 +00:00
|
|
|
const applyTheme = (input, commit) => {
|
2018-11-19 17:22:46 +00:00
|
|
|
const { rules, theme } = generatePreset(input)
|
2017-11-17 15:24:42 +00:00
|
|
|
const head = document.head
|
|
|
|
const body = document.body
|
2019-08-27 16:17:10 +00:00
|
|
|
body.classList.add('hidden')
|
2017-11-13 23:37:49 +00:00
|
|
|
|
2017-11-17 15:24:42 +00:00
|
|
|
const styleEl = document.createElement('style')
|
|
|
|
head.appendChild(styleEl)
|
|
|
|
const styleSheet = styleEl.sheet
|
2017-11-17 00:17:36 +00:00
|
|
|
|
2018-04-01 02:28:20 +00:00
|
|
|
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')
|
2019-08-27 16:17:10 +00:00
|
|
|
body.classList.remove('hidden')
|
2017-11-18 11:13:51 +00:00
|
|
|
|
2018-10-02 18:43:58 +00:00
|
|
|
// commit('setOption', { name: 'colors', value: htmlColors })
|
|
|
|
// commit('setOption', { name: 'radii', value: radii })
|
2018-10-03 18:21:48 +00:00
|
|
|
commit('setOption', { name: 'customTheme', value: input })
|
2018-11-14 18:53:51 +00:00
|
|
|
commit('setOption', { name: 'colors', value: theme.colors })
|
2017-11-17 15:24:42 +00:00
|
|
|
}
|
|
|
|
|
2018-12-02 05:47:55 +00:00
|
|
|
const getCssShadow = (input, usesDropShadow) => {
|
2018-11-23 07:17:01 +00:00
|
|
|
if (input.length === 0) {
|
|
|
|
return 'none'
|
|
|
|
}
|
|
|
|
|
2018-12-02 05:47:55 +00:00
|
|
|
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(', ')
|
2018-11-19 01:40:25 +00:00
|
|
|
}
|
|
|
|
|
2018-11-30 13:39:07 +00:00
|
|
|
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)
|
2018-11-30 13:39:07 +00:00
|
|
|
.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)
|
2018-12-02 05:47:55 +00:00
|
|
|
]).join(' '))
|
|
|
|
.map(_ => `drop-shadow(${_})`)
|
|
|
|
.join(' ')
|
2018-11-30 13:39:07 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 01:26:45 +00:00
|
|
|
const getCssColor = (input, a) => {
|
|
|
|
let rgb = {}
|
|
|
|
if (typeof input === 'object') {
|
|
|
|
rgb = input
|
|
|
|
} else if (typeof input === 'string') {
|
|
|
|
if (input.startsWith('#')) {
|
|
|
|
rgb = hex2rgb(input)
|
|
|
|
} else if (input.startsWith('--')) {
|
|
|
|
return `var(${input})`
|
|
|
|
} else {
|
|
|
|
return input
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return rgb2rgba({ ...rgb, a })
|
|
|
|
}
|
|
|
|
|
2019-12-29 22:45:48 +00:00
|
|
|
const generateColors = (themeData) => {
|
|
|
|
const rawOpacity = Object.assign({
|
2020-01-02 18:36:10 +00:00
|
|
|
panel: 1,
|
|
|
|
btn: 1,
|
|
|
|
border: 1,
|
|
|
|
bg: 1,
|
2020-01-06 20:55:14 +00:00
|
|
|
badge: 1,
|
|
|
|
text: 1,
|
2018-10-07 19:03:34 +00:00
|
|
|
alert: 0.5,
|
|
|
|
input: 0.5,
|
2019-12-28 16:49:35 +00:00
|
|
|
faint: 0.5,
|
|
|
|
underlay: 0.15
|
2019-12-29 22:45:48 +00:00
|
|
|
}, Object.entries(themeData.opacity || {}).reduce((acc, [k, v]) => {
|
2018-11-14 16:39:17 +00:00
|
|
|
if (typeof v !== 'undefined') {
|
|
|
|
acc[k] = v
|
|
|
|
}
|
|
|
|
return acc
|
|
|
|
}, {}))
|
2019-12-28 13:55:42 +00:00
|
|
|
|
2019-12-29 22:45:48 +00:00
|
|
|
const inputColors = themeData.colors || themeData
|
|
|
|
const transparentsOpacity = Object.entries(inputColors).reduce((acc, [k, v]) => {
|
|
|
|
if (v === 'transparent') {
|
|
|
|
acc[k] = 0
|
|
|
|
}
|
|
|
|
return acc
|
|
|
|
}, {})
|
|
|
|
|
|
|
|
const opacity = { ...rawOpacity, ...transparentsOpacity }
|
|
|
|
|
2020-01-06 20:55:14 +00:00
|
|
|
// Cycle one: just whatever we have
|
|
|
|
const sourceColors = Object.entries(inputColors).reduce((acc, [k, v]) => {
|
2018-10-02 18:43:58 +00:00
|
|
|
if (typeof v === 'object') {
|
|
|
|
acc[k] = v
|
|
|
|
} else {
|
2019-12-29 22:45:48 +00:00
|
|
|
let value = v
|
|
|
|
if (v === 'transparent') {
|
2020-01-02 18:36:10 +00:00
|
|
|
// TODO: hack to keep rest of the code from complaining
|
2019-12-29 22:45:48 +00:00
|
|
|
value = '#FF00FF'
|
|
|
|
}
|
|
|
|
acc[k] = hex2rgb(value)
|
2018-10-02 18:43:58 +00:00
|
|
|
}
|
|
|
|
return acc
|
|
|
|
}, {})
|
|
|
|
|
2020-01-06 20:55:14 +00:00
|
|
|
const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l
|
2018-10-07 19:03:34 +00:00
|
|
|
const mod = isLightOnDark ? 1 : -1
|
|
|
|
|
2020-01-06 20:55:14 +00:00
|
|
|
const colors = SLOT_ORDERED.reduce((acc, key) => {
|
|
|
|
const value = SLOT_INHERITANCE[key]
|
|
|
|
if (sourceColors[key]) {
|
|
|
|
return { ...acc, [key]: { ...sourceColors[key] } }
|
|
|
|
} else if (typeof value === 'string' && value.startsWith('#')) {
|
|
|
|
return { ...acc, [key]: convert(value).rgb }
|
|
|
|
} else {
|
|
|
|
const isObject = typeof value === 'object'
|
|
|
|
const defaultColorFunc = (mod, dep) => ({ ...dep })
|
|
|
|
const deps = getDependencies(key, SLOT_INHERITANCE)
|
|
|
|
const colorFunc = (isObject && value.color) || defaultColorFunc
|
|
|
|
|
|
|
|
if (value.textColor) {
|
2020-01-11 21:07:41 +00:00
|
|
|
const bg = alphaBlendLayers(
|
|
|
|
{ ...acc[deps[0]] },
|
|
|
|
getLayers(
|
|
|
|
value.layer,
|
|
|
|
value.variant || value.layer,
|
|
|
|
acc,
|
|
|
|
opacity
|
2020-01-06 20:55:14 +00:00
|
|
|
)
|
2020-01-11 21:07:41 +00:00
|
|
|
)
|
|
|
|
if (value.textColor === 'bw') {
|
|
|
|
return {
|
|
|
|
...acc,
|
|
|
|
[key]: contrastRatio(bg)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
...acc,
|
|
|
|
[key]: getTextColor(
|
|
|
|
bg,
|
|
|
|
{ ...acc[deps[0]] },
|
|
|
|
value.textColor === 'preserve'
|
|
|
|
)
|
|
|
|
}
|
2020-01-06 20:55:14 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log('BENIS', key, deps, deps.map((dep) => ({ ...acc[dep] })))
|
|
|
|
return {
|
|
|
|
...acc,
|
|
|
|
[key]: colorFunc(
|
|
|
|
mod,
|
|
|
|
...deps.map((dep) => ({ ...acc[dep] }))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, {})
|
2018-10-07 19:03:34 +00:00
|
|
|
|
2020-01-06 20:55:14 +00:00
|
|
|
// Inheriting opacities
|
2018-10-07 19:03:34 +00:00
|
|
|
Object.entries(opacity).forEach(([ k, v ]) => {
|
|
|
|
if (typeof v === 'undefined') return
|
|
|
|
if (k === 'alert') {
|
2018-11-13 13:30:01 +00:00
|
|
|
colors.alertError.a = v
|
2019-10-07 20:46:40 +00:00
|
|
|
colors.alertWarning.a = v
|
2018-10-07 19:03:34 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if (k === 'faint') {
|
|
|
|
colors[k + 'Link'].a = v
|
|
|
|
colors['panelFaint'].a = v
|
|
|
|
}
|
2018-11-21 15:22:05 +00:00
|
|
|
if (k === 'bg') {
|
|
|
|
colors['lightBg'].a = v
|
|
|
|
}
|
2020-01-06 20:55:14 +00:00
|
|
|
if (k === 'badge') {
|
|
|
|
colors['badgeNotification'].a = v
|
|
|
|
}
|
2018-11-19 17:22:46 +00:00
|
|
|
if (colors[k]) {
|
|
|
|
colors[k].a = v
|
|
|
|
} else {
|
|
|
|
console.error('Wrong key ' + k)
|
|
|
|
}
|
2018-10-07 19:03:34 +00:00
|
|
|
})
|
2018-10-02 18:43:58 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v)
|
|
|
|
return acc
|
|
|
|
}, { complete: {}, solid: {} })
|
2018-10-02 18:43:58 +00:00
|
|
|
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: {
|
2018-10-07 19:03:34 +00:00
|
|
|
colors: htmlColors.solid,
|
2018-11-19 17:22:46 +00:00
|
|
|
opacity
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2018-11-23 08:36:36 +00:00
|
|
|
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
|
|
|
|
}
|
2018-10-02 18:43:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-25 18:48:16 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-19 17:22:46 +00:00
|
|
|
const generateShadows = (input) => {
|
2018-11-22 21:24:16 +00:00
|
|
|
const border = (top, shadow) => ({
|
2018-11-21 18:23:07 +00:00
|
|
|
x: 0,
|
2018-11-22 21:24:16 +00:00
|
|
|
y: top ? 1 : -1,
|
2018-11-21 18:23:07 +00:00
|
|
|
blur: 0,
|
2018-11-21 18:32:51 +00:00
|
|
|
spread: 0,
|
2018-11-22 21:24:16 +00:00
|
|
|
color: shadow ? '#000000' : '#FFFFFF',
|
2018-11-21 18:23:07 +00:00
|
|
|
alpha: 0.2,
|
|
|
|
inset: true
|
2018-11-22 21:24:16 +00:00
|
|
|
})
|
|
|
|
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
|
|
|
|
const inputInsetFakeBorders = [border(true, true), border(false, false)]
|
|
|
|
const hoverGlow = {
|
2018-11-21 18:23:07 +00:00
|
|
|
x: 0,
|
2018-11-22 21:24:16 +00:00
|
|
|
y: 0,
|
|
|
|
blur: 4,
|
2018-11-21 18:32:51 +00:00
|
|
|
spread: 0,
|
2018-11-22 21:24:16 +00:00
|
|
|
color: '--faint',
|
|
|
|
alpha: 1
|
|
|
|
}
|
|
|
|
|
2018-11-19 17:22:46 +00:00
|
|
|
const shadows = {
|
|
|
|
panel: [{
|
|
|
|
x: 1,
|
|
|
|
y: 1,
|
|
|
|
blur: 4,
|
|
|
|
spread: 0,
|
|
|
|
color: '#000000',
|
|
|
|
alpha: 0.6
|
|
|
|
}],
|
2018-11-21 18:40:45 +00:00
|
|
|
topBar: [{
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
blur: 4,
|
|
|
|
spread: 0,
|
|
|
|
color: '#000000',
|
|
|
|
alpha: 0.6
|
|
|
|
}],
|
2018-11-21 18:23:07 +00:00
|
|
|
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
|
|
|
|
}],
|
2018-11-25 14:21:53 +00:00
|
|
|
avatarStatus: [],
|
2018-11-21 18:23:07 +00:00
|
|
|
panelHeader: [],
|
|
|
|
button: [{
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
blur: 2,
|
|
|
|
spread: 0,
|
|
|
|
color: '#000000',
|
|
|
|
alpha: 1
|
|
|
|
}, ...buttonInsetFakeBorders],
|
2018-11-22 21:24:16 +00:00
|
|
|
buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
|
|
|
|
buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
|
2018-11-21 18:32:51 +00:00
|
|
|
input: [...inputInsetFakeBorders, {
|
2018-11-21 18:23:07 +00:00
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
blur: 2,
|
|
|
|
inset: true,
|
|
|
|
spread: 0,
|
|
|
|
color: '#000000',
|
|
|
|
alpha: 1
|
2018-11-21 18:32:51 +00:00
|
|
|
}],
|
2018-11-19 17:22:46 +00:00
|
|
|
...(input.shadows || {})
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
rules: {
|
2018-11-30 13:39:07 +00:00
|
|
|
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
|
2018-12-02 05:47:55 +00:00
|
|
|
.map(([k, v]) => [
|
|
|
|
`--${k}Shadow: ${getCssShadow(v)}`,
|
|
|
|
`--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
|
|
|
|
`--${k}ShadowInset: ${getCssShadow(v, true)}`
|
|
|
|
].join(';'))
|
2018-11-30 13:39:07 +00:00
|
|
|
.join(';')
|
2018-11-19 17:22:46 +00:00
|
|
|
},
|
|
|
|
theme: {
|
|
|
|
shadows
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-25 18:48:16 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const generatePreset = (input) => {
|
|
|
|
const shadows = generateShadows(input)
|
|
|
|
const colors = generateColors(input)
|
|
|
|
const radii = generateRadii(input)
|
2018-11-25 18:48:16 +00:00
|
|
|
const fonts = generateFonts(input)
|
2018-11-19 17:22:46 +00:00
|
|
|
|
2018-11-25 18:48:16 +00:00
|
|
|
return composePreset(colors, radii, shadows, fonts)
|
2018-11-19 17:22:46 +00:00
|
|
|
}
|
|
|
|
|
2018-12-10 22:38:20 +00:00
|
|
|
const getThemes = () => {
|
|
|
|
return window.fetch('/static/styles.json')
|
2017-11-17 15:24:42 +00:00
|
|
|
.then((data) => data.json())
|
|
|
|
.then((themes) => {
|
2018-12-10 22:38:20 +00:00
|
|
|
return Promise.all(Object.entries(themes).map(([k, v]) => {
|
|
|
|
if (typeof v === 'object') {
|
|
|
|
return Promise.resolve([k, v])
|
|
|
|
} else if (typeof v === 'string') {
|
|
|
|
return window.fetch(v)
|
|
|
|
.then((data) => data.json())
|
|
|
|
.then((theme) => {
|
|
|
|
return [k, theme]
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.error(e)
|
|
|
|
return []
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
})
|
|
|
|
.then((promises) => {
|
|
|
|
return promises
|
|
|
|
.filter(([k, v]) => v)
|
|
|
|
.reduce((acc, [k, v]) => {
|
|
|
|
acc[k] = v
|
|
|
|
return acc
|
|
|
|
}, {})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const setPreset = (val, commit) => {
|
2019-02-09 09:26:35 +00:00
|
|
|
return getThemes().then((themes) => {
|
2018-12-10 22:38:20 +00:00
|
|
|
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
|
|
|
const isV1 = Array.isArray(theme)
|
|
|
|
const data = isV1 ? {} : theme.theme
|
|
|
|
|
|
|
|
if (isV1) {
|
2017-11-17 15:24:42 +00:00
|
|
|
const bgRgb = hex2rgb(theme[1])
|
2018-10-07 16:59:22 +00:00
|
|
|
const fgRgb = hex2rgb(theme[2])
|
2017-11-17 15:24:42 +00:00
|
|
|
const textRgb = hex2rgb(theme[3])
|
|
|
|
const linkRgb = hex2rgb(theme[4])
|
2018-04-01 19:07:25 +00:00
|
|
|
|
|
|
|
const cRedRgb = hex2rgb(theme[5] || '#FF0000')
|
2018-04-07 18:58:29 +00:00
|
|
|
const cGreenRgb = hex2rgb(theme[6] || '#00FF00')
|
|
|
|
const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
|
2018-04-01 19:07:25 +00:00
|
|
|
const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
|
|
|
|
|
2018-12-10 22:38:20 +00:00
|
|
|
data.colors = {
|
2017-11-17 15:24:42 +00:00
|
|
|
bg: bgRgb,
|
2018-10-07 16:59:22 +00:00
|
|
|
fg: fgRgb,
|
2017-11-17 15:24:42 +00:00
|
|
|
text: textRgb,
|
2018-04-01 19:07:25 +00:00
|
|
|
link: linkRgb,
|
|
|
|
cRed: cRedRgb,
|
|
|
|
cBlue: cBlueRgb,
|
|
|
|
cGreen: cGreenRgb,
|
|
|
|
cOrange: cOrangeRgb
|
2017-11-17 15:24:42 +00:00
|
|
|
}
|
2018-12-10 22:38:20 +00:00
|
|
|
}
|
2018-04-01 19:07:25 +00:00
|
|
|
|
2018-12-10 22:38:20 +00:00
|
|
|
// This is a hack, this function is only called during initial load.
|
|
|
|
// We want to cancel loading the theme from config.json if we're already
|
|
|
|
// loading a theme from the persisted state.
|
|
|
|
// Needed some way of dealing with the async way of things.
|
|
|
|
// load config -> set preset -> wait for styles.json to load ->
|
|
|
|
// load persisted state -> set colors -> styles.json loaded -> set colors
|
|
|
|
if (!window.themeLoaded) {
|
2018-12-10 22:39:18 +00:00
|
|
|
applyTheme(data, commit)
|
2018-12-10 22:38:20 +00:00
|
|
|
}
|
|
|
|
})
|
2017-01-16 16:44:26 +00:00
|
|
|
}
|
|
|
|
|
2018-11-19 17:22:46 +00:00
|
|
|
export {
|
2017-11-17 15:24:42 +00:00
|
|
|
setStyle,
|
|
|
|
setPreset,
|
2018-12-10 23:05:22 +00:00
|
|
|
applyTheme,
|
2018-10-04 15:16:14 +00:00
|
|
|
getTextColor,
|
2018-11-19 17:22:46 +00:00
|
|
|
generateColors,
|
|
|
|
generateRadii,
|
|
|
|
generateShadows,
|
2018-11-25 18:48:16 +00:00
|
|
|
generateFonts,
|
2018-11-19 01:40:25 +00:00
|
|
|
generatePreset,
|
2018-12-10 22:38:20 +00:00
|
|
|
getThemes,
|
2018-11-19 17:22:46 +00:00
|
|
|
composePreset,
|
2018-11-30 13:39:07 +00:00
|
|
|
getCssShadow,
|
|
|
|
getCssShadowFilter
|
2017-01-16 16:44:26 +00:00
|
|
|
}
|