diff options
| author | lain <lain@soykaf.club> | 2020-06-12 14:17:56 +0000 |
|---|---|---|
| committer | lain <lain@soykaf.club> | 2020-06-12 14:17:56 +0000 |
| commit | fd109fa355ac491b86d1e10fcbfcdd577f1ac4d7 (patch) | |
| tree | c27c2f3b684540b10ef9d4477349b4a8681eb7b2 /src/services/style_setter/style_setter.js | |
| parent | 1946661911c97651ba5356db22a0ddd00ba04864 (diff) | |
| parent | 48365819d14d80d2aeb7174bca05bf76eee2e8e0 (diff) | |
Merge branch 'develop' into 'chore/improve-default-tos'
# Conflicts:
# static/terms-of-service.html
Diffstat (limited to 'src/services/style_setter/style_setter.js')
| -rw-r--r-- | src/services/style_setter/style_setter.js | 532 |
1 files changed, 200 insertions, 332 deletions
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index eaa495c4..fbdcf562 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,78 +1,9 @@ -import { times } from 'lodash' -import { brightness, invertLightness, convert, contrastRatio } from 'chromatism' -import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } from '../color_convert/color_convert.js' +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' -// 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) => { - /*** - 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') - const cssEl = document.createElement('link') - cssEl.setAttribute('rel', 'stylesheet') - cssEl.setAttribute('href', href) - head.appendChild(cssEl) - - const setDynamic = () => { - const baseEl = document.createElement('div') - 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) - - const styleEl = document.createElement('style') - head.appendChild(styleEl) - // const styleSheet = styleEl.sheet - - body.classList.remove('hidden') - } - - 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 - - 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) { - return contrastRatio(bg, text).rgb - } - return result - } - return text -} - -const applyTheme = (input, commit) => { - const { rules, theme } = generatePreset(input) +export const applyTheme = (input) => { + const { rules } = generatePreset(input) const head = document.head const body = document.body body.classList.add('hidden') @@ -87,14 +18,9 @@ const applyTheme = (input, commit) => { styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max') styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max') body.classList.remove('hidden') - - // commit('setOption', { name: 'colors', value: htmlColors }) - // commit('setOption', { name: 'radii', value: radii }) - commit('setOption', { name: 'customTheme', value: input }) - commit('setOption', { name: 'colors', value: theme.colors }) } -const getCssShadow = (input, usesDropShadow) => { +export const getCssShadow = (input, usesDropShadow) => { if (input.length === 0) { return 'none' } @@ -132,122 +58,18 @@ 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 = (input) => { - const colors = {} - const opacity = Object.assign({ - alert: 0.5, - input: 0.5, - faint: 0.5 - }, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => { - if (typeof v !== 'undefined') { - acc[k] = v - } - return acc - }, {})) - const col = Object.entries(input.colors || input).reduce((acc, [k, v]) => { - if (typeof v === 'object') { - acc[k] = v - } else { - acc[k] = hex2rgb(v) - } - return acc - }, {}) - - const isLightOnDark = convert(col.bg).hsl.l < convert(col.text).hsl.l - const mod = isLightOnDark ? 1 : -1 - - colors.text = col.text - colors.lightText = brightness(20 * mod, colors.text).rgb - colors.link = col.link - colors.faint = col.faint || Object.assign({}, col.text) - - colors.bg = col.bg - colors.lightBg = col.lightBg || brightness(5, colors.bg).rgb - - colors.fg = col.fg - colors.fgText = col.fgText || getTextColor(colors.fg, colors.text) - colors.fgLink = col.fgLink || getTextColor(colors.fg, colors.link, true) - - colors.border = col.border || brightness(2 * mod, colors.fg).rgb - - colors.btn = col.btn || Object.assign({}, col.fg) - colors.btnText = col.btnText || getTextColor(colors.btn, colors.fgText) - - colors.input = col.input || Object.assign({}, col.fg) - colors.inputText = col.inputText || getTextColor(colors.input, colors.lightText) - - colors.panel = col.panel || Object.assign({}, col.fg) - colors.panelText = col.panelText || getTextColor(colors.panel, colors.fgText) - colors.panelLink = col.panelLink || getTextColor(colors.panel, colors.fgLink) - colors.panelFaint = col.panelFaint || getTextColor(colors.panel, colors.faint) - - colors.topBar = col.topBar || Object.assign({}, col.fg) - colors.topBarText = col.topBarText || getTextColor(colors.topBar, colors.fgText) - colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink) - - colors.faintLink = col.faintLink || Object.assign({}, col.link) - colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg) - - colors.icon = mixrgb(colors.bg, colors.text) - - colors.cBlue = col.cBlue || hex2rgb('#0000FF') - colors.cRed = col.cRed || hex2rgb('#FF0000') - colors.cGreen = col.cGreen || hex2rgb('#00FF00') - colors.cOrange = col.cOrange || hex2rgb('#E3FF00') +export const generateColors = (themeData) => { + const sourceColors = !themeData.themeEngineVersion + ? colors2to3(themeData.colors || themeData) + : themeData.colors || themeData - colors.alertError = col.alertError || Object.assign({}, colors.cRed) - colors.alertErrorText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.bg), colors.text) - colors.alertErrorPanelText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.panel), colors.panelText) - - colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange) - colors.alertWarningText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.bg), colors.text) - colors.alertWarningPanelText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.panel), colors.panelText) - - colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed) - colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb - - Object.entries(opacity).forEach(([ k, v ]) => { - if (typeof v === 'undefined') return - if (k === 'alert') { - colors.alertError.a = v - colors.alertWarning.a = v - return - } - if (k === 'faint') { - colors[k + 'Link'].a = v - colors['panelFaint'].a = v - } - if (k === 'bg') { - colors['lightBg'].a = v - } - if (colors[k]) { - colors[k].a = v - } else { - console.error('Wrong key ' + k) - } - }) + 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) : rgb2rgba(v) + acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v) return acc }, { complete: {}, solid: {} }) return { @@ -264,7 +86,7 @@ const generateColors = (input) => { } } -const generateRadii = (input) => { +export const generateRadii = (input) => { let inputRadii = input.radii || {} // v1 -> v2 if (typeof input.btnRadius !== 'undefined') { @@ -297,7 +119,7 @@ const generateRadii = (input) => { } } -const generateFonts = (input) => { +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 @@ -332,89 +154,123 @@ const generateFonts = (input) => { } } -const generateShadows = (input) => { - const border = (top, shadow) => ({ - x: 0, - y: top ? 1 : -1, - blur: 0, +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: 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 = { + color: '#000000', + alpha: 0.6 + }], + topBar: [{ x: 0, y: 0, blur: 4, spread: 0, - color: '--faint', + 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 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 - }], - ...(input.shadows || {}) - } + const inputShadows = input.shadows && !input.themeEngineVersion + ? shadows2to3(input.shadows, input.opacity) + : input.shadows || {} + 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.1: if shadow doesn't have non-inset shadows with spread > 0 - optionally + // 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)}`, @@ -429,7 +285,7 @@ const generateShadows = (input) => { } } -const composePreset = (colors, radii, shadows, fonts) => { +export const composePreset = (colors, radii, shadows, fonts) => { return { rules: { ...shadows.rules, @@ -446,98 +302,110 @@ const composePreset = (colors, radii, shadows, fonts) => { } } -const generatePreset = (input) => { - const shadows = generateShadows(input) +export const generatePreset = (input) => { const colors = generateColors(input) - const radii = generateRadii(input) - const fonts = generateFonts(input) - - return composePreset(colors, radii, shadows, fonts) + return composePreset( + colors, + generateRadii(input), + generateShadows(input, colors.theme.colors, colors.mod), + generateFonts(input) + ) } -const getThemes = () => { - return window.fetch('/static/styles.json') +export const getThemes = () => { + const cache = 'no-store' + + return window.fetch('/static/styles.json', { cache }) .then((data) => data.json()) .then((themes) => { - return Promise.all(Object.entries(themes).map(([k, v]) => { + return Object.entries(themes).map(([k, v]) => { + let promise = null if (typeof v === 'object') { - return Promise.resolve([k, v]) + promise = Promise.resolve(v) } else if (typeof v === 'string') { - return window.fetch(v) + promise = window.fetch(v, { cache }) .then((data) => data.json()) - .then((theme) => { - return [k, theme] - }) .catch((e) => { console.error(e) - return [] + return null }) } - })) + return [k, promise] + }) }) .then((promises) => { return promises - .filter(([k, v]) => v) .reduce((acc, [k, v]) => { acc[k] = v return acc }, {}) }) } +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 } + } + }, {}) +} -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') +/** + * 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 }) => 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 } + }, {}) +} - data.colors = { - bg: bgRgb, - fg: fgRgb, - text: textRgb, - link: linkRgb, - cRed: cRedRgb, - cBlue: cBlueRgb, - cGreen: cGreenRgb, - cOrange: cOrangeRgb +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 + + 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 } } - } - // 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) { - applyTheme(data, commit) - } - }) + return { theme: data, source: theme.source } + }) } -export { - setStyle, - setPreset, - applyTheme, - getTextColor, - generateColors, - generateRadii, - generateShadows, - generateFonts, - generatePreset, - getThemes, - composePreset, - getCssShadow, - getCssShadowFilter -} +export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme)) |
