aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/color_input/color_input.scss65
-rw-r--r--src/services/color_convert/color_convert.js99
-rw-r--r--src/services/style_setter/style_setter.js442
-rw-r--r--src/services/theme_data/theme_data.service.js315
4 files changed, 499 insertions, 422 deletions
diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss
new file mode 100644
index 00000000..92bf87c5
--- /dev/null
+++ b/src/components/color_input/color_input.scss
@@ -0,0 +1,65 @@
+@import '../../_variables.scss';
+
+.color-input {
+ display: inline-flex;
+
+ &-field.input {
+ display: inline-flex;
+ flex: 0 0 0;
+ max-width: 9em;
+ align-items: stretch;
+ padding: .2em 8px;
+
+ input {
+ background: none;
+ color: $fallback--lightText;
+ color: var(--inputText, $fallback--lightText);
+ border: none;
+ padding: 0;
+ margin: 0;
+
+ &.textColor {
+ flex: 1 0 3em;
+ min-width: 3em;
+ padding: 0;
+ }
+
+ &.nativeColor {
+ flex: 0 0 2em;
+ min-width: 2em;
+ align-self: center;
+ height: 100%;
+ }
+ }
+ .transparentIndicator {
+ flex: 0 0 2em;
+ min-width: 2em;
+ align-self: center;
+ height: 100%;
+ // forgot to install counter-strike source, ooops
+ background-color: #FF00FF;
+ position: relative;
+ &::before, &::after {
+ display: block;
+ content: '';
+ background-color: #000000;
+ position: absolute;
+ height: 50%;
+ width: 50%;
+ }
+ &::after {
+ top: 0;
+ left: 0;
+ }
+ &::before {
+ bottom: 0;
+ right: 0;
+ }
+ }
+ }
+
+ .label {
+ flex: 1 1 auto;
+ }
+
+}
diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js
index 32b4d50e..464f6495 100644
--- a/src/services/color_convert/color_convert.js
+++ b/src/services/color_convert/color_convert.js
@@ -1,9 +1,16 @@
-import { map } from 'lodash'
+import { invertLightness, convert, contrastRatio } from 'chromatism'
// useful for visualizing color when debugging
export const consoleColor = (color) => console.log('%c##########', 'background: ' + color + '; color: ' + color)
-const rgb2hex = (r, g, b) => {
+/**
+ * Convert r, g, b values into hex notation. All components are [0-255]
+ *
+ * @param {Number|String|Object} r - Either red component, {r,g,b} object, or hex string
+ * @param {Number} [g] - Green component
+ * @param {Number} [b] - Blue component
+ */
+export const rgb2hex = (r, g, b) => {
if (r === null || typeof r === 'undefined') {
return undefined
}
@@ -14,7 +21,7 @@ const rgb2hex = (r, g, b) => {
if (typeof r === 'object') {
({ r, g, b } = r)
}
- [r, g, b] = map([r, g, b], (val) => {
+ [r, g, b] = [r, g, b].map(val => {
val = Math.ceil(val)
val = val < 0 ? 0 : val
val = val > 255 ? 255 : val
@@ -82,6 +89,7 @@ const getContrastRatio = (a, b) => {
return (l1 + 0.05) / (l2 + 0.05)
}
+
/**
* Same as `getContrastRatio` but for multiple layers in-between
*
@@ -101,7 +109,7 @@ export const getContrastRatioLayers = (text, layers, bedrock) => {
* @param {Object} bg - bottom layer color
* @returns {Object} sRGB of resulting color
*/
-const alphaBlend = (fg, fga, bg) => {
+export const alphaBlend = (fg, fga, bg) => {
if (fga === 1 || typeof fga === 'undefined') return fg
return 'rgb'.split('').reduce((acc, c) => {
// Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
@@ -121,14 +129,20 @@ export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color,
return alphaBlend(color, opacity, acc)
}, bedrock)
-const invert = (rgb) => {
+export const invert = (rgb) => {
return 'rgb'.split('').reduce((acc, c) => {
acc[c] = 255 - rgb[c]
return acc
}, {})
}
-const hex2rgb = (hex) => {
+/**
+ * Converts #rrggbb hex notation into an {r, g, b} object
+ *
+ * @param {String} hex - #rrggbb string
+ * @returns {Object} rgb representation of the color, values are 0-255
+ */
+export const hex2rgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
@@ -137,18 +151,75 @@ const hex2rgb = (hex) => {
} : null
}
-const mixrgb = (a, b) => {
+/**
+ * Old somewhat weird function for mixing two colors together
+ *
+ * @param {Object} a - one color (rgb)
+ * @param {Object} b - other color (rgb)
+ * @returns {Object} result
+ */
+export const mixrgb = (a, b) => {
return Object.keys(a).reduce((acc, k) => {
acc[k] = (a[k] + b[k]) / 2
return acc
}, {})
}
+/**
+ * Converts rgb object into a CSS rgba() color
+ *
+ * @param {Object} color - rgb
+ * @returns {String} CSS rgba() color
+ */
+export const rgba2css = function (rgba) {
+ return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
+}
+
+/**
+ * Get text color for given background color and intended text color
+ * This checks if text and background don't have enough color and inverts
+ * text color's lightness if needed. If text color is still not enough it
+ * will fall back to black or white
+ *
+ * @param {Object} bg - background color
+ * @param {Object} text - intended text color
+ * @param {Boolean} preserve - try to preserve intended text color's hue/saturation (i.e. no BW)
+ */
+export const getTextColor = function (bg, text, preserve) {
+ const bgIsLight = convert(bg).hsl.l > 50
+ const textIsLight = convert(text).hsl.l > 50
+
+ if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
+ const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
+ const result = Object.assign(base, invertLightness(text).rgb)
+ if (!preserve && getContrastRatio(bg, result) < 4.5) {
+ // B&W
+ return contrastRatio(bg, text).rgb
+ }
+ // Inverted color
+ return result
+ }
+ return text
+}
-export {
- rgb2hex,
- hex2rgb,
- mixrgb,
- invert,
- getContrastRatio,
- alphaBlend
+/**
+ * Converts color to CSS Color value
+ *
+ * @param {Object|String} input - color
+ * @param {Number} [a] - alpha value
+ * @returns {String} a CSS Color value
+ */
+export 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 rgba2css({ ...rgb, a })
}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 992b3194..46b08628 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,275 +1,13 @@
import { times } from 'lodash'
-import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
-import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend, alphaBlendLayers } from '../color_convert/color_convert.js'
-
-export const CURRENT_VERSION = 3
-/* 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: {
- depends: ['text'],
- layer: 'fg',
- textColor: true
- },
- fgLink: {
- depends: ['link'],
- layer: 'fg',
- textColor: 'preserve'
- },
-
- // Panel header
- panel: '--fg',
- panelText: {
- depends: ['fgText'],
- layer: 'panel',
- textColor: true
- },
- panelFaint: {
- depends: ['fgText'],
- layer: 'panel',
- textColor: true
- },
- panelLink: {
- depends: ['fgLink'],
- layer: 'panel',
- textColor: 'preserve'
- },
-
- // Top bar
- topBar: '--fg',
- topBarText: {
- depends: ['fgText'],
- layer: 'topBar',
- textColor: true
- },
- topBarLink: {
- depends: ['fgLink'],
- layer: 'topBar',
- textColor: 'preserve'
- },
-
- // Buttons
- btn: '--fg',
- btnText: {
- depends: ['fgText'],
- layer: 'btn'
- },
- btnPanelText: {
- depends: ['panelText'],
- layer: 'btnPanel',
- variant: 'btn',
- textColor: true
- },
- btnTopBarText: {
- depends: ['topBarText'],
- layer: 'btnTopBar',
- variant: 'btn',
- textColor: true
- },
-
- // Input fields
- input: '--fg',
- inputText: {
- depends: ['text'],
- layer: 'input',
- textColor: true
- },
- inputPanelText: {
- depends: ['panelText'],
- layer: 'inputPanel',
- variant: 'input',
- textColor: true
- },
- inputTopbarText: {
- depends: ['topBarText'],
- layer: 'inputTopBar',
- variant: 'input',
- textColor: true
- },
-
- alertError: '--cRed',
- alertErrorText: {
- depends: ['text', 'alertError'],
- layer: 'alert',
- variant: 'alertError',
- textColor: true
- },
- alertErrorPanelText: {
- depends: ['panelText', 'alertError'],
- layer: 'alertPanel',
- variant: 'alertError',
- textColor: true
- },
-
- alertWarning: '--cOrange',
- alertWarningText: {
- depends: ['text', 'alertWarning'],
- layer: 'alert',
- variant: 'alertWarning',
- textColor: true
- },
- alertWarningPanelText: {
- depends: ['panelText', 'alertWarning'],
- layer: 'alertPanel',
- variant: 'alertWarning',
- textColor: true
- },
-
- badgeNotification: '--cRed',
- badgeNotificationText: {
- depends: ['text', 'badgeNotification'],
- layer: 'badge',
- variant: 'badgeNotification',
- textColor: 'bw'
- }
-}
-
-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]
- ]))
-}
-
-const getDependencies = (key, inheritance) => {
- const data = inheritance[key]
- if (typeof data === 'string' && data.startsWith('--')) {
- return [data.substring(2)]
- } else {
- if (data === null) return []
- const { depends, layer, variant } = data
- const layerDeps = layer
- ? getLayersArray(layer).map(currentLayer => {
- return currentLayer === layer
- ? variant || layer
- : currentLayer
- })
- : []
- if (Array.isArray(depends)) {
- return [...depends, ...layerDeps]
- } else {
- return [...layerDeps]
- }
- }
-}
-
-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)
+import { convert } from 'chromatism'
+import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js'
+import { getColors } 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.
-const setStyle = (href, commit) => {
+export const setStyle = (href, commit) => {
/***
What's going on here?
I want to make it easy for admins to style this application. To have
@@ -315,30 +53,7 @@ const setStyle = (href, commit) => {
cssEl.addEventListener('load', setDynamic)
}
-const rgb2rgba = function (rgba) {
- return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
-}
-
-const getTextColor = function (bg, text, preserve) {
- const bgIsLight = convert(bg).hsl.l > 50
- const textIsLight = convert(text).hsl.l > 50
-
- console.log(bgIsLight, textIsLight)
-
- if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
- const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
- const result = Object.assign(base, invertLightness(text).rgb)
- if (!preserve && getContrastRatio(bg, result) < 4.5) {
- // B&W
- return contrastRatio(bg, text).rgb
- }
- // Inverted color
- return result
- }
- return text
-}
-
-const applyTheme = (input, commit) => {
+export const applyTheme = (input, commit) => {
const { rules, theme } = generatePreset(input)
const head = document.head
const body = document.body
@@ -399,22 +114,6 @@ const getCssShadowFilter = (input) => {
.join(' ')
}
-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 })
-}
-
const generateColors = (themeData) => {
const rawOpacity = Object.assign({
panel: 1,
@@ -435,14 +134,16 @@ const generateColors = (themeData) => {
}, {}))
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 }
+ const opacity = {
+ ...rawOpacity,
+ ...Object.entries(inputColors).reduce((acc, [k, v]) => {
+ if (v === 'transparent') {
+ acc[k] = 0
+ }
+ return acc
+ }, {})
+ }
// Cycle one: just whatever we have
const sourceColors = Object.entries(inputColors).reduce((acc, [k, v]) => {
@@ -462,55 +163,7 @@ const generateColors = (themeData) => {
const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l
const mod = isLightOnDark ? 1 : -1
- 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) {
- const bg = alphaBlendLayers(
- { ...acc[deps[0]] },
- getLayers(
- value.layer,
- value.variant || value.layer,
- acc,
- opacity
- )
- )
- if (value.textColor === 'bw') {
- return {
- ...acc,
- [key]: contrastRatio(bg)
- }
- } else {
- return {
- ...acc,
- [key]: getTextColor(
- bg,
- { ...acc[deps[0]] },
- value.textColor === 'preserve'
- )
- }
- }
- } else {
- console.log('BENIS', key, deps, deps.map((dep) => ({ ...acc[dep] })))
- return {
- ...acc,
- [key]: colorFunc(
- mod,
- ...deps.map((dep) => ({ ...acc[dep] }))
- )
- }
- }
- }
- }, {})
+ const colors = getColors(sourceColors, opacity, mod)
// Inheriting opacities
Object.entries(opacity).forEach(([ k, v ]) => {
@@ -541,7 +194,7 @@ const generateColors = (themeData) => {
.reduce((acc, [k, v]) => {
if (!v) return acc
acc.solid[k] = rgb2hex(v)
- acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v)
+ acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
return acc
}, { complete: {}, solid: {} })
return {
@@ -740,14 +393,12 @@ const composePreset = (colors, radii, shadows, fonts) => {
}
}
-const generatePreset = (input) => {
- const shadows = generateShadows(input)
- const colors = generateColors(input)
- const radii = generateRadii(input)
- const fonts = generateFonts(input)
-
- return composePreset(colors, radii, shadows, fonts)
-}
+const generatePreset = (input) => composePreset(
+ generateColors(input),
+ generateRadii(input),
+ generateShadows(input),
+ generateFonts(input)
+)
const getThemes = () => {
return window.fetch('/static/styles.json')
@@ -779,33 +430,24 @@ const getThemes = () => {
})
}
-const setPreset = (val, commit) => {
+export const setPreset = (val, commit) => {
return getThemes().then((themes) => {
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
const isV1 = Array.isArray(theme)
const data = isV1 ? {} : theme.theme
if (isV1) {
- const bgRgb = hex2rgb(theme[1])
- const fgRgb = hex2rgb(theme[2])
- const textRgb = hex2rgb(theme[3])
- const linkRgb = hex2rgb(theme[4])
-
- const cRedRgb = hex2rgb(theme[5] || '#FF0000')
- const cGreenRgb = hex2rgb(theme[6] || '#00FF00')
- const cBlueRgb = hex2rgb(theme[7] || '#0000FF')
- const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00')
-
- data.colors = {
- bg: bgRgb,
- fg: fgRgb,
- text: textRgb,
- link: linkRgb,
- cRed: cRedRgb,
- cBlue: cBlueRgb,
- cGreen: cGreenRgb,
- cOrange: cOrangeRgb
- }
+ 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 }
}
// This is a hack, this function is only called during initial load.
@@ -819,19 +461,3 @@ const setPreset = (val, commit) => {
}
})
}
-
-export {
- setStyle,
- setPreset,
- applyTheme,
- getTextColor,
- generateColors,
- generateRadii,
- generateShadows,
- generateFonts,
- generatePreset,
- getThemes,
- composePreset,
- getCssShadow,
- getCssShadowFilter
-}
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
new file mode 100644
index 00000000..c9c80727
--- /dev/null
+++ b/src/services/theme_data/theme_data.service.js
@@ -0,0 +1,315 @@
+import { convert, brightness, contrastRatio } from 'chromatism'
+import { alphaBlend, alphaBlendLayers, getTextColor, mixrgb } from '../color_convert/color_convert.js'
+
+export const CURRENT_VERSION = 3
+/* 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: {
+ depends: ['text'],
+ layer: 'fg',
+ textColor: true
+ },
+ fgLink: {
+ depends: ['link'],
+ layer: 'fg',
+ textColor: 'preserve'
+ },
+
+ // Panel header
+ panel: '--fg',
+ panelText: {
+ depends: ['fgText'],
+ layer: 'panel',
+ textColor: true
+ },
+ panelFaint: {
+ depends: ['fgText'],
+ layer: 'panel',
+ textColor: true
+ },
+ panelLink: {
+ depends: ['fgLink'],
+ layer: 'panel',
+ textColor: 'preserve'
+ },
+
+ // Top bar
+ topBar: '--fg',
+ topBarText: {
+ depends: ['fgText'],
+ layer: 'topBar',
+ textColor: true
+ },
+ topBarLink: {
+ depends: ['fgLink'],
+ layer: 'topBar',
+ textColor: 'preserve'
+ },
+
+ // Buttons
+ btn: '--fg',
+ btnText: {
+ depends: ['fgText'],
+ layer: 'btn'
+ },
+ btnPanelText: {
+ depends: ['panelText'],
+ layer: 'btnPanel',
+ variant: 'btn',
+ textColor: true
+ },
+ btnTopBarText: {
+ depends: ['topBarText'],
+ layer: 'btnTopBar',
+ variant: 'btn',
+ textColor: true
+ },
+
+ // Input fields
+ input: '--fg',
+ inputText: {
+ depends: ['text'],
+ layer: 'input',
+ textColor: true
+ },
+ inputPanelText: {
+ depends: ['panelText'],
+ layer: 'inputPanel',
+ variant: 'input',
+ textColor: true
+ },
+ inputTopbarText: {
+ depends: ['topBarText'],
+ layer: 'inputTopBar',
+ variant: 'input',
+ textColor: true
+ },
+
+ alertError: '--cRed',
+ alertErrorText: {
+ depends: ['text', 'alertError'],
+ layer: 'alert',
+ variant: 'alertError',
+ textColor: true
+ },
+ alertErrorPanelText: {
+ depends: ['panelText', 'alertError'],
+ layer: 'alertPanel',
+ variant: 'alertError',
+ textColor: true
+ },
+
+ alertWarning: '--cOrange',
+ alertWarningText: {
+ depends: ['text', 'alertWarning'],
+ layer: 'alert',
+ variant: 'alertWarning',
+ textColor: true
+ },
+ alertWarningPanelText: {
+ depends: ['panelText', 'alertWarning'],
+ layer: 'alertPanel',
+ variant: 'alertWarning',
+ textColor: true
+ },
+
+ badgeNotification: '--cRed',
+ badgeNotificationText: {
+ depends: ['text', 'badgeNotification'],
+ layer: 'badge',
+ variant: 'badgeNotification',
+ textColor: 'bw'
+ }
+}
+
+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]
+ ]))
+}
+
+const getDependencies = (key, inheritance) => {
+ const data = inheritance[key]
+ if (typeof data === 'string' && data.startsWith('--')) {
+ return [data.substring(2)]
+ } else {
+ if (data === null) return []
+ const { depends, layer, variant } = data
+ const layerDeps = layer
+ ? getLayersArray(layer).map(currentLayer => {
+ return currentLayer === layer
+ ? variant || layer
+ : currentLayer
+ })
+ : []
+ if (Array.isArray(depends)) {
+ return [...depends, ...layerDeps]
+ } else {
+ return [...layerDeps]
+ }
+ }
+}
+
+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)
+
+export const getColors = (sourceColors, sourceOpacity, mod) => 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) {
+ const bg = alphaBlendLayers(
+ { ...acc[deps[0]] },
+ getLayers(
+ value.layer,
+ value.variant || value.layer,
+ acc,
+ sourceOpacity
+ )
+ )
+ if (value.textColor === 'bw') {
+ return {
+ ...acc,
+ [key]: contrastRatio(bg)
+ }
+ } else {
+ return {
+ ...acc,
+ [key]: getTextColor(
+ bg,
+ { ...acc[deps[0]] },
+ value.textColor === 'preserve'
+ )
+ }
+ }
+ } else {
+ console.log('BENIS', key, deps, deps.map((dep) => ({ ...acc[dep] })))
+ return {
+ ...acc,
+ [key]: colorFunc(
+ mod,
+ ...deps.map((dep) => ({ ...acc[dep] }))
+ )
+ }
+ }
+ }
+}, {})