Merge branch 'themes-accent' into shigusegubu
* themes-accent: (83 commits) fix and update changelog fix/remove contrast ratios removed base16-related code fix warning stylings fixed eslint, made `mod` work properly depending on context including in shadows Better Disabled buttons support. Mammal theme fixes. Implemented proper context-aware `mod` argument - now checks lightness of "variant" color. needs retesting tho updated preview to account for underlay update button toggled state, apply it to emoji reactions removed one color TODO add theme to list Kenomo (see: #624) theme. Ability to define link color for post contents. Fixes fix rgba function, whoops lint fix rgba css generation, add some tests to automatically verify that themes are generated properly fix transparent color not affecting downstream slots fix icons in menus improved selectedMenu again popover/selected menu improvements separate actual theme data from theme framework revert fgText -> text after some consideration. case was fixed already in other way ...
This commit is contained in:
commit
25f785770c
116 changed files with 2929 additions and 3047 deletions
104
src/App.scss
104
src/App.scss
|
|
@ -31,9 +31,12 @@ h4 {
|
|||
margin: auto;
|
||||
min-height: 100vh;
|
||||
max-width: 980px;
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
align-content: flex-start;
|
||||
}
|
||||
.underlay {
|
||||
background-color: rgba(0,0,0,0.15);
|
||||
background-color: var(--underlay, rgba(0,0,0,0.15));
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
|
|
@ -98,18 +101,27 @@ button {
|
|||
&:active {
|
||||
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressed, $fallback--fg)
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnDisabled, $fallback--fg)
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg)
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggled, $fallback--fg);
|
||||
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
|
@ -121,12 +133,15 @@ button {
|
|||
}
|
||||
}
|
||||
|
||||
label.select {
|
||||
padding: 0;
|
||||
input, textarea, .select, .input {
|
||||
|
||||
}
|
||||
&.unstyled {
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
input, textarea, .select {
|
||||
border: none;
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
|
|
@ -140,13 +155,17 @@ input, textarea, .select {
|
|||
font-family: var(--inputFont, sans-serif);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 8px .5em;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: 28px;
|
||||
line-height: 16px;
|
||||
hyphens: none;
|
||||
padding: 8px .5em;
|
||||
|
||||
&.select {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:disabled, &[disabled=disabled] {
|
||||
cursor: not-allowed;
|
||||
|
|
@ -160,7 +179,7 @@ input, textarea, .select {
|
|||
right: 5px;
|
||||
height: 100%;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
line-height: 28px;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
|
|
@ -198,7 +217,7 @@ input, textarea, .select {
|
|||
&:checked + label::before {
|
||||
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
|
||||
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
|
||||
background-color: var(--link, $fallback--link);
|
||||
background-color: var(--accent, $fallback--link);
|
||||
}
|
||||
&:disabled {
|
||||
&,
|
||||
|
|
@ -235,7 +254,7 @@ input, textarea, .select {
|
|||
display: none;
|
||||
&:checked + label::before {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
&:disabled {
|
||||
&,
|
||||
|
|
@ -353,6 +372,33 @@ i[class*=icon-] {
|
|||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
|
||||
button {
|
||||
&, i[class*=icon-] {
|
||||
color: $fallback--text;
|
||||
color: var(--btnTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressedTopBar, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledTopBarText, $fallback--text);
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledTopBarText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
|
@ -487,6 +533,10 @@ main-router {
|
|||
color: $fallback--faint;
|
||||
color: var(--panelFaint, $fallback--faint);
|
||||
}
|
||||
.faint-link {
|
||||
color: $fallback--faint;
|
||||
color: var(--faintLink, $fallback--faint);
|
||||
}
|
||||
|
||||
.alert {
|
||||
white-space: nowrap;
|
||||
|
|
@ -509,6 +559,30 @@ main-router {
|
|||
align-self: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
&, i[class*=icon-] {
|
||||
color: $fallback--text;
|
||||
color: var(--btnPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnPressedPanel, $fallback--fg);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnDisabledPanelText, $fallback--text);
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
color: $fallback--text;
|
||||
color: var(--btnToggledPanelText, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
color: var(--panelLink, $fallback--link)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
</nav>
|
||||
<div
|
||||
id="content"
|
||||
class="container"
|
||||
class="container underlay"
|
||||
>
|
||||
<div class="sidebar-flexer mobile-hidden">
|
||||
<div class="sidebar-bounds">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import App from '../App.vue'
|
|||
import { windowWidth } from '../services/window_utils/window_utils'
|
||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
import { applyTheme } from '../services/style_setter/style_setter.js'
|
||||
|
||||
const getStatusnetConfig = async ({ store }) => {
|
||||
try {
|
||||
|
|
@ -258,7 +260,7 @@ const checkOAuthToken = async ({ store }) => {
|
|||
try {
|
||||
await store.dispatch('loginUser', store.getters.getUserToken())
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
|
|
@ -266,23 +268,29 @@ const checkOAuthToken = async ({ store }) => {
|
|||
}
|
||||
|
||||
const afterStoreSetup = async ({ store, i18n }) => {
|
||||
if (store.state.config.customTheme) {
|
||||
// This is a hack to deal with async loading of config.json and themes
|
||||
// See: style_setter.js, setPreset()
|
||||
window.themeLoaded = true
|
||||
store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: store.state.config.customTheme
|
||||
})
|
||||
}
|
||||
|
||||
const width = windowWidth()
|
||||
store.dispatch('setMobileLayout', width <= 800)
|
||||
await setConfig({ store })
|
||||
|
||||
const { customTheme, customThemeSource } = store.state.config
|
||||
const { theme } = store.state.instance
|
||||
const customThemePresent = customThemeSource || customTheme
|
||||
|
||||
if (customThemePresent) {
|
||||
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
|
||||
applyTheme(customThemeSource)
|
||||
} else {
|
||||
applyTheme(customTheme)
|
||||
}
|
||||
} else if (theme) {
|
||||
// do nothing, it will load asynchronously
|
||||
} else {
|
||||
console.error('Failed to load any theme!')
|
||||
}
|
||||
|
||||
// Now we can try getting the server settings and logging in
|
||||
await Promise.all([
|
||||
checkOAuthToken({ store }),
|
||||
setConfig({ store }),
|
||||
getTOS({ store }),
|
||||
getInstancePanel({ store }),
|
||||
getStickers({ store }),
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@
|
|||
top: 100%;
|
||||
right: 0;
|
||||
max-height: 400px;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: $fallback--border;
|
||||
|
|
|
|||
|
|
@ -87,13 +87,13 @@ export default {
|
|||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
&:indeterminate + .checkbox-indicator::before {
|
||||
content: '–';
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
68
src/components/color_input/color_input.scss
Normal file
68
src/components/color_input/color_input.scss
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
@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%;
|
||||
}
|
||||
}
|
||||
.computedIndicator,
|
||||
.transparentIndicator {
|
||||
flex: 0 0 2em;
|
||||
min-width: 2em;
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
}
|
||||
.transparentIndicator {
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
class="color-control style-control"
|
||||
class="color-input style-control"
|
||||
:class="{ disabled: !present || disabled }"
|
||||
>
|
||||
<label
|
||||
|
|
@ -9,46 +9,100 @@
|
|||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exlcude-disabled"
|
||||
type="checkbox"
|
||||
<Checkbox
|
||||
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
||||
>
|
||||
<label
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
||||
/>
|
||||
<input
|
||||
:id="name"
|
||||
class="color-input"
|
||||
type="color"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<input
|
||||
:id="name + '-t'"
|
||||
class="text-input"
|
||||
type="text"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<div class="input color-input-field">
|
||||
<input
|
||||
:id="name + '-t'"
|
||||
class="textColor unstyled"
|
||||
type="text"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<input
|
||||
v-if="validColor"
|
||||
:id="name"
|
||||
class="nativeColor unstyled"
|
||||
type="color"
|
||||
:value="value || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
<div
|
||||
v-if="transparentColor"
|
||||
class="transparentIndicator"
|
||||
/>
|
||||
<div
|
||||
v-if="computedColor"
|
||||
class="computedIndicator"
|
||||
:style="{backgroundColor: fallback}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
export default {
|
||||
props: [
|
||||
'name', 'label', 'value', 'fallback', 'disabled'
|
||||
],
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
// Name of color, used for identifying
|
||||
name: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
// Readable label
|
||||
label: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
// Color value, should be required but vue cannot tell the difference
|
||||
// between "property missing" and "property set to undefined"
|
||||
value: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
// Color fallback to use when value is not defeind
|
||||
fallback: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
// Disable the control
|
||||
disabled: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Show "optional" tickbox, for when value might become mandatory
|
||||
showOptionalTickbox: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
present () {
|
||||
return typeof this.value !== 'undefined'
|
||||
},
|
||||
validColor () {
|
||||
return hex2rgb(this.value || this.fallback)
|
||||
},
|
||||
transparentColor () {
|
||||
return this.value === 'transparent'
|
||||
},
|
||||
computedColor () {
|
||||
return this.value && this.value.startsWith('--')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,9 +37,17 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'large', 'contrast'
|
||||
],
|
||||
props: {
|
||||
large: {
|
||||
required: false
|
||||
},
|
||||
// TODO: Make theme switcher compute theme initially so that contrast
|
||||
// component won't be called without contrast data
|
||||
contrast: {
|
||||
required: false,
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hint () {
|
||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||
|
|
|
|||
|
|
@ -75,18 +75,18 @@
|
|||
.dialog-modal-content {
|
||||
margin: 0;
|
||||
padding: 1rem 1rem;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: .5em .5em;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
border-top: 1px solid $fallback--bg;
|
||||
border-top: 1px solid var(--bg, $fallback--bg);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
border-top: 1px solid var(--border, $fallback--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
|
|
|
|||
|
|
@ -109,10 +109,16 @@
|
|||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--popupShadow);
|
||||
min-width: 75%;
|
||||
background: $fallback--bg;
|
||||
background: var(--bg, $fallback--bg);
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--postLink: var(--popoverPostLink, $fallback--link);
|
||||
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +163,12 @@
|
|||
|
||||
&.highlighted {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--lightBg, $fallback--fg);
|
||||
background-color: var(--selectedMenuPopover, $fallback--fg);
|
||||
color: var(--selectedMenuPopoverText, $fallback--text);
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@
|
|||
left: 0;
|
||||
margin: 0 !important;
|
||||
z-index: 1;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--lightText: var(--popoverLightText, $fallback--faint);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
|
||||
.keep-open,
|
||||
.too-many-emoji {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.emoji"
|
||||
class="emoji-reaction btn btn-default"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.emoji) }"
|
||||
:class="{ 'toggled': reactedWith(reaction.emoji) }"
|
||||
@click="emojiOnClick(reaction.emoji, $event)"
|
||||
>
|
||||
<span class="reaction-emoji">{{ reaction.emoji }}</span>
|
||||
|
|
@ -40,10 +40,4 @@
|
|||
}
|
||||
}
|
||||
|
||||
.picked-reaction {
|
||||
border: 1px solid var(--link, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
exportData () {
|
||||
const stringified = JSON.stringify(this.exportObject) // Pretty-print and indent with 2 spaces
|
||||
const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces
|
||||
|
||||
// Create an invisible link with a data url and simulate a click
|
||||
const e = document.createElement('a')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
class="btn btn-default follow-button"
|
||||
:class="{ pressed: isPressed }"
|
||||
:class="{ toggled: isPressed }"
|
||||
:disabled="inProgress"
|
||||
:title="title"
|
||||
@click="onClick"
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
</div>
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
:class="{ pressed: showDropDown }"
|
||||
:class="{ toggled: showDropDown }"
|
||||
>
|
||||
{{ $t('user_card.admin_menu.moderation') }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -100,13 +100,25 @@
|
|||
|
||||
&:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--link;
|
||||
color: var(--selectedMenuText, $fallback--link);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
font-weight: bolder;
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@
|
|||
a {
|
||||
color: var(--faintLink);
|
||||
}
|
||||
.status-content a {
|
||||
color: var(--postFaintLink);
|
||||
}
|
||||
}
|
||||
padding: 0;
|
||||
.media-body {
|
||||
|
|
|
|||
|
|
@ -9,18 +9,12 @@
|
|||
>
|
||||
{{ $t('settings.style.common.opacity') }}
|
||||
</label>
|
||||
<input
|
||||
<Checkbox
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exclude-disabled"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', !present ? fallback : undefined)"
|
||||
>
|
||||
<label
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
:for="name + '-o'"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@change="$emit('input', !present ? fallback : undefined)"
|
||||
/>
|
||||
<input
|
||||
:id="name"
|
||||
|
|
@ -37,7 +31,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
export default {
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: [
|
||||
'name', 'value', 'fallback', 'disabled'
|
||||
],
|
||||
|
|
|
|||
|
|
@ -104,8 +104,10 @@
|
|||
.result-fill {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
color: $fallback--text;
|
||||
color: var(--pollText, $fallback--text);
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--linkBg, $fallback--lightBg);
|
||||
background-color: var(--poll, $fallback--lightBg);
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
top: 0;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,15 @@
|
|||
border-radius: $fallback--btnRadius;
|
||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--text;
|
||||
color: var(--popoverText, $fallback--text);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--postLink: var(--popoverPostLink, $fallback--link);
|
||||
--postFaintLink: var(--popoverPostFaintLink, $fallback--link);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
.popover-arrow {
|
||||
|
|
@ -129,6 +137,8 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
--btnText: var(--popoverText, $fallback--text);
|
||||
|
||||
&-icon {
|
||||
padding-left: 0.5rem;
|
||||
|
||||
|
|
@ -137,11 +147,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
// TODO: improve the look on breeze themes
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
box-shadow: none;
|
||||
&:active, &:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||
color: $fallback--link;
|
||||
color: var(--selectedMenuPopoverText, $fallback--link);
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
i {
|
||||
color: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<input
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
:id="name + '-o'"
|
||||
class="opt exclude-disabled"
|
||||
class="opt"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@input="$emit('input', !present ? fallback : undefined)"
|
||||
|
|
|
|||
|
|
@ -68,7 +68,12 @@
|
|||
|
||||
&-item-selected-inner {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
&-header {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,21 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
currentFallback () {
|
||||
if (this.ready && this.fallback.length > 0) {
|
||||
return this.fallback[this.selectedId]
|
||||
} else {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 0,
|
||||
spread: 0,
|
||||
inset: false,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
moveUpValid () {
|
||||
return this.ready && this.selectedId > 0
|
||||
},
|
||||
|
|
@ -80,7 +95,7 @@ export default {
|
|||
},
|
||||
style () {
|
||||
return this.ready ? {
|
||||
boxShadow: getCssShadow(this.cValue)
|
||||
boxShadow: getCssShadow(this.fallback)
|
||||
} : {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,15 +191,20 @@
|
|||
v-model="selected.color"
|
||||
:disabled="!present"
|
||||
:label="$t('settings.style.common.color')"
|
||||
:fallback="currentFallback.color"
|
||||
:show-optional-tickbox="false"
|
||||
name="shadow"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="selected.alpha"
|
||||
:disabled="!present"
|
||||
/>
|
||||
<p>
|
||||
{{ $t('settings.style.shadows.hint') }}
|
||||
</p>
|
||||
<i18n
|
||||
path="settings.style.shadows.hintV3"
|
||||
tag="p"
|
||||
>
|
||||
<code>--variable,mod</code>
|
||||
</i18n>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -223,7 +223,13 @@
|
|||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
color: var(--popoverText, $fallback--link);
|
||||
--faint: var(--popoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--popoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--icon: var(--popoverIcon, $fallback--icon);
|
||||
|
||||
.button-icon:before {
|
||||
width: 1.1em;
|
||||
|
|
@ -289,7 +295,13 @@
|
|||
|
||||
&:hover {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuPopoverText, $fallback--text);
|
||||
--faint: var(--selectedMenuPopoverFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
|
||||
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
|
||||
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -453,7 +453,15 @@ $status-margin: 0.75em;
|
|||
|
||||
&_focused {
|
||||
background-color: $fallback--lightBg;
|
||||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
background-color: var(--selectedPost, $fallback--lightBg);
|
||||
color: $fallback--text;
|
||||
color: var(--selectedPostText, $fallback--text);
|
||||
--lightText: var(--selectedPostLightText, $fallback--light);
|
||||
--faint: var(--selectedPostFaintText, $fallback--faint);
|
||||
--faintLink: var(--selectedPostFaintLink, $fallback--faint);
|
||||
--postLink: var(--selectedPostPostLink, $fallback--faint);
|
||||
--postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint);
|
||||
--icon: var(--selectedPostIcon, $fallback--icon);
|
||||
}
|
||||
|
||||
.timeline & {
|
||||
|
|
@ -581,8 +589,6 @@ $status-margin: 0.75em;
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 0.4em 0 0.2em;
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
|
||||
.replies-separator {
|
||||
|
|
@ -644,6 +650,11 @@ $status-margin: 0.75em;
|
|||
line-height: 1.4em;
|
||||
white-space: pre-wrap;
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
}
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
img {
|
||||
height: 100%;
|
||||
&:hover {
|
||||
filter: drop-shadow(0 0 5px var(--link, $fallback--link));
|
||||
filter: drop-shadow(0 0 5px var(--accent, $fallback--link));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,101 +1,117 @@
|
|||
<template>
|
||||
<div class="panel dummy">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('settings.style.preview.header') }}
|
||||
<span class="badge badge-notification">
|
||||
99
|
||||
<div class="preview-container">
|
||||
<div class="underlay underlay-preview" />
|
||||
<div class="panel dummy">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('settings.style.preview.header') }}
|
||||
<span class="badge badge-notification">
|
||||
99
|
||||
</span>
|
||||
</div>
|
||||
<span class="faint">
|
||||
{{ $t('settings.style.preview.header_faint') }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="faint">
|
||||
{{ $t('settings.style.preview.header_faint') }}
|
||||
</span>
|
||||
<span class="alert error">
|
||||
{{ $t('settings.style.preview.error') }}
|
||||
</span>
|
||||
<button class="btn">
|
||||
{{ $t('settings.style.preview.button') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body theme-preview-content">
|
||||
<div class="post">
|
||||
<div class="avatar">
|
||||
( ͡° ͜ʖ ͡°)
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>
|
||||
{{ $t('settings.style.preview.content') }}
|
||||
</h4>
|
||||
|
||||
<i18n path="settings.style.preview.text">
|
||||
<code style="font-family: var(--postCodeFont)">
|
||||
{{ $t('settings.style.preview.mono') }}
|
||||
</code>
|
||||
<a style="color: var(--link)">
|
||||
{{ $t('settings.style.preview.link') }}
|
||||
</a>
|
||||
</i18n>
|
||||
|
||||
<div class="icons">
|
||||
<i
|
||||
style="color: var(--cBlue)"
|
||||
class="button-icon icon-reply"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cGreen)"
|
||||
class="button-icon icon-retweet"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cOrange)"
|
||||
class="button-icon icon-star"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cRed)"
|
||||
class="button-icon icon-cancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="after-post">
|
||||
<div class="avatar-alt">
|
||||
:^)
|
||||
</div>
|
||||
<div class="content">
|
||||
<i18n
|
||||
path="settings.style.preview.fine_print"
|
||||
tag="span"
|
||||
class="faint"
|
||||
>
|
||||
<a style="color: var(--faintLink)">
|
||||
{{ $t('settings.style.preview.faint_link') }}
|
||||
</a>
|
||||
</i18n>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
|
||||
<span class="alert error">
|
||||
{{ $t('settings.style.preview.error') }}
|
||||
</span>
|
||||
<input
|
||||
:value="$t('settings.style.preview.input')"
|
||||
type="text"
|
||||
>
|
||||
|
||||
<div class="actions">
|
||||
<span class="checkbox">
|
||||
<input
|
||||
id="preview_checkbox"
|
||||
checked="very yes"
|
||||
type="checkbox"
|
||||
>
|
||||
<label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
|
||||
<span class="alert error">
|
||||
{{ $t('settings.style.preview.error') }}
|
||||
</span>
|
||||
<button class="btn">
|
||||
{{ $t('settings.style.preview.button') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body theme-preview-content">
|
||||
<div class="post">
|
||||
<div class="avatar">
|
||||
( ͡° ͜ʖ ͡°)
|
||||
</div>
|
||||
<div class="content">
|
||||
<h4>
|
||||
{{ $t('settings.style.preview.content') }}
|
||||
</h4>
|
||||
|
||||
<i18n path="settings.style.preview.text">
|
||||
<code style="font-family: var(--postCodeFont)">
|
||||
{{ $t('settings.style.preview.mono') }}
|
||||
</code>
|
||||
<a style="color: var(--link)">
|
||||
{{ $t('settings.style.preview.link') }}
|
||||
</a>
|
||||
</i18n>
|
||||
|
||||
<div class="icons">
|
||||
<i
|
||||
style="color: var(--cBlue)"
|
||||
class="button-icon icon-reply"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cGreen)"
|
||||
class="button-icon icon-retweet"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cOrange)"
|
||||
class="button-icon icon-star"
|
||||
/>
|
||||
<i
|
||||
style="color: var(--cRed)"
|
||||
class="button-icon icon-cancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="after-post">
|
||||
<div class="avatar-alt">
|
||||
:^)
|
||||
</div>
|
||||
<div class="content">
|
||||
<i18n
|
||||
path="settings.style.preview.fine_print"
|
||||
tag="span"
|
||||
class="faint"
|
||||
>
|
||||
<a style="color: var(--faintLink)">
|
||||
{{ $t('settings.style.preview.faint_link') }}
|
||||
</a>
|
||||
</i18n>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
|
||||
<span class="alert error">
|
||||
{{ $t('settings.style.preview.error') }}
|
||||
</span>
|
||||
<input
|
||||
:value="$t('settings.style.preview.input')"
|
||||
type="text"
|
||||
>
|
||||
|
||||
<div class="actions">
|
||||
<span class="checkbox">
|
||||
<input
|
||||
id="preview_checkbox"
|
||||
checked="very yes"
|
||||
type="checkbox"
|
||||
>
|
||||
<label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
|
||||
</span>
|
||||
<button class="btn">
|
||||
{{ $t('settings.style.preview.button') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.preview-container {
|
||||
position: relative;
|
||||
}
|
||||
.underlay-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,29 @@
|
|||
import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
|
||||
import { set, delete as del } from 'vue'
|
||||
import { generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js'
|
||||
import {
|
||||
rgb2hex,
|
||||
hex2rgb,
|
||||
getContrastRatioLayers
|
||||
} from '../../services/color_convert/color_convert.js'
|
||||
import {
|
||||
DEFAULT_SHADOWS,
|
||||
generateColors,
|
||||
generateShadows,
|
||||
generateRadii,
|
||||
generateFonts,
|
||||
composePreset,
|
||||
getThemes,
|
||||
shadows2to3,
|
||||
colors2to3
|
||||
} from '../../services/style_setter/style_setter.js'
|
||||
import {
|
||||
SLOT_INHERITANCE
|
||||
} from '../../services/theme_data/pleromafe.js'
|
||||
import {
|
||||
CURRENT_VERSION,
|
||||
OPACITIES,
|
||||
getLayers,
|
||||
getOpacitySlot
|
||||
} from '../../services/theme_data/theme_data.service.js'
|
||||
import ColorInput from '../color_input/color_input.vue'
|
||||
import RangeInput from '../range_input/range_input.vue'
|
||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||
|
|
@ -24,11 +47,21 @@ const v1OnlyNames = [
|
|||
'cOrange'
|
||||
].map(_ => _ + 'ColorLocal')
|
||||
|
||||
const colorConvert = (color) => {
|
||||
if (color.startsWith('--') || color === 'transparent') {
|
||||
return color
|
||||
} else {
|
||||
return hex2rgb(color)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
availableStyles: [],
|
||||
selected: this.$store.getters.mergedConfig.theme,
|
||||
themeWarning: undefined,
|
||||
tempImportFile: undefined,
|
||||
|
||||
previewShadows: {},
|
||||
previewColors: {},
|
||||
|
|
@ -45,51 +78,13 @@ export default {
|
|||
keepRoundness: false,
|
||||
keepFonts: false,
|
||||
|
||||
textColorLocal: '',
|
||||
linkColorLocal: '',
|
||||
...Object.keys(SLOT_INHERITANCE)
|
||||
.map(key => [key, ''])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
|
||||
|
||||
bgColorLocal: '',
|
||||
bgOpacityLocal: undefined,
|
||||
|
||||
fgColorLocal: '',
|
||||
fgTextColorLocal: undefined,
|
||||
fgLinkColorLocal: undefined,
|
||||
|
||||
btnColorLocal: undefined,
|
||||
btnTextColorLocal: undefined,
|
||||
btnOpacityLocal: undefined,
|
||||
|
||||
inputColorLocal: undefined,
|
||||
inputTextColorLocal: undefined,
|
||||
inputOpacityLocal: undefined,
|
||||
|
||||
panelColorLocal: undefined,
|
||||
panelTextColorLocal: undefined,
|
||||
panelLinkColorLocal: undefined,
|
||||
panelFaintColorLocal: undefined,
|
||||
panelOpacityLocal: undefined,
|
||||
|
||||
topBarColorLocal: undefined,
|
||||
topBarTextColorLocal: undefined,
|
||||
topBarLinkColorLocal: undefined,
|
||||
|
||||
alertErrorColorLocal: undefined,
|
||||
alertWarningColorLocal: undefined,
|
||||
|
||||
badgeOpacityLocal: undefined,
|
||||
badgeNotificationColorLocal: undefined,
|
||||
|
||||
borderColorLocal: undefined,
|
||||
borderOpacityLocal: undefined,
|
||||
|
||||
faintColorLocal: undefined,
|
||||
faintOpacityLocal: undefined,
|
||||
faintLinkColorLocal: undefined,
|
||||
|
||||
cRedColorLocal: '',
|
||||
cBlueColorLocal: '',
|
||||
cGreenColorLocal: '',
|
||||
cOrangeColorLocal: '',
|
||||
...Object.keys(OPACITIES)
|
||||
.map(key => [key, ''])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
|
||||
|
||||
shadowSelected: undefined,
|
||||
shadowsLocal: {},
|
||||
|
|
@ -108,69 +103,105 @@ export default {
|
|||
created () {
|
||||
const self = this
|
||||
|
||||
getThemes().then((themesComplete) => {
|
||||
self.availableStyles = themesComplete
|
||||
})
|
||||
getThemes()
|
||||
.then((promises) => {
|
||||
return Promise.all(
|
||||
Object.entries(promises)
|
||||
.map(([k, v]) => v.then(res => [k, res]))
|
||||
)
|
||||
})
|
||||
.then(themes => themes.reduce((acc, [k, v]) => {
|
||||
if (v) {
|
||||
return {
|
||||
...acc,
|
||||
[k]: v
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, {}))
|
||||
.then((themesComplete) => {
|
||||
self.availableStyles = themesComplete
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme)
|
||||
this.loadThemeFromLocalStorage()
|
||||
if (typeof this.shadowSelected === 'undefined') {
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
themeWarningHelp () {
|
||||
if (!this.themeWarning) return
|
||||
const t = this.$t
|
||||
const pre = 'settings.style.switcher.help.'
|
||||
const {
|
||||
origin,
|
||||
themeEngineVersion,
|
||||
type,
|
||||
noActionsPossible
|
||||
} = this.themeWarning
|
||||
if (origin === 'file') {
|
||||
// Loaded v2 theme from file
|
||||
if (themeEngineVersion === 2 && type === 'wrong_version') {
|
||||
return t(pre + 'v2_imported')
|
||||
}
|
||||
if (themeEngineVersion > CURRENT_VERSION) {
|
||||
return t(pre + 'future_version_imported') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'snapshot_missing')
|
||||
: t(pre + 'snapshot_present')
|
||||
)
|
||||
}
|
||||
if (themeEngineVersion < CURRENT_VERSION) {
|
||||
return t(pre + 'future_version_imported') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'snapshot_missing')
|
||||
: t(pre + 'snapshot_present')
|
||||
)
|
||||
}
|
||||
} else if (origin === 'localStorage') {
|
||||
if (type === 'snapshot_source_mismatch') {
|
||||
return t(pre + 'snapshot_source_mismatch')
|
||||
}
|
||||
// FE upgraded from v2
|
||||
if (themeEngineVersion === 2) {
|
||||
return t(pre + 'upgraded_from_v2')
|
||||
}
|
||||
// Admin downgraded FE
|
||||
if (themeEngineVersion > CURRENT_VERSION) {
|
||||
return t(pre + 'fe_downgraded') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'migration_snapshot_ok')
|
||||
: t(pre + 'migration_snapshot_gone')
|
||||
)
|
||||
}
|
||||
// Admin upgraded FE
|
||||
if (themeEngineVersion < CURRENT_VERSION) {
|
||||
return t(pre + 'fe_upgraded') + ' ' +
|
||||
(
|
||||
noActionsPossible
|
||||
? t(pre + 'migration_snapshot_ok')
|
||||
: t(pre + 'migration_snapshot_gone')
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedVersion () {
|
||||
return Array.isArray(this.selected) ? 1 : 2
|
||||
},
|
||||
currentColors () {
|
||||
return {
|
||||
bg: this.bgColorLocal,
|
||||
text: this.textColorLocal,
|
||||
link: this.linkColorLocal,
|
||||
|
||||
fg: this.fgColorLocal,
|
||||
fgText: this.fgTextColorLocal,
|
||||
fgLink: this.fgLinkColorLocal,
|
||||
|
||||
panel: this.panelColorLocal,
|
||||
panelText: this.panelTextColorLocal,
|
||||
panelLink: this.panelLinkColorLocal,
|
||||
panelFaint: this.panelFaintColorLocal,
|
||||
|
||||
input: this.inputColorLocal,
|
||||
inputText: this.inputTextColorLocal,
|
||||
|
||||
topBar: this.topBarColorLocal,
|
||||
topBarText: this.topBarTextColorLocal,
|
||||
topBarLink: this.topBarLinkColorLocal,
|
||||
|
||||
btn: this.btnColorLocal,
|
||||
btnText: this.btnTextColorLocal,
|
||||
|
||||
alertError: this.alertErrorColorLocal,
|
||||
alertWarning: this.alertWarningColorLocal,
|
||||
badgeNotification: this.badgeNotificationColorLocal,
|
||||
|
||||
faint: this.faintColorLocal,
|
||||
faintLink: this.faintLinkColorLocal,
|
||||
border: this.borderColorLocal,
|
||||
|
||||
cRed: this.cRedColorLocal,
|
||||
cBlue: this.cBlueColorLocal,
|
||||
cGreen: this.cGreenColorLocal,
|
||||
cOrange: this.cOrangeColorLocal
|
||||
}
|
||||
return Object.keys(SLOT_INHERITANCE)
|
||||
.map(key => [key, this[key + 'ColorLocal']])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
|
||||
},
|
||||
currentOpacity () {
|
||||
return {
|
||||
bg: this.bgOpacityLocal,
|
||||
btn: this.btnOpacityLocal,
|
||||
input: this.inputOpacityLocal,
|
||||
panel: this.panelOpacityLocal,
|
||||
topBar: this.topBarOpacityLocal,
|
||||
border: this.borderOpacityLocal,
|
||||
faint: this.faintOpacityLocal
|
||||
}
|
||||
return Object.keys(OPACITIES)
|
||||
.map(key => [key, this[key + 'OpacityLocal']])
|
||||
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
|
||||
},
|
||||
currentRadii () {
|
||||
return {
|
||||
|
|
@ -193,75 +224,66 @@ export default {
|
|||
},
|
||||
// This needs optimization maybe
|
||||
previewContrast () {
|
||||
if (!this.previewTheme.colors.bg) return {}
|
||||
const colors = this.previewTheme.colors
|
||||
const opacity = this.previewTheme.opacity
|
||||
if (!colors.bg) return {}
|
||||
const hints = (ratio) => ({
|
||||
text: ratio.toPrecision(3) + ':1',
|
||||
// AA level, AAA level
|
||||
aa: ratio >= 4.5,
|
||||
aaa: ratio >= 7,
|
||||
// same but for 18pt+ texts
|
||||
laa: ratio >= 3,
|
||||
laaa: ratio >= 4.5
|
||||
})
|
||||
try {
|
||||
if (!this.previewTheme.colors.bg) return {}
|
||||
const colors = this.previewTheme.colors
|
||||
const opacity = this.previewTheme.opacity
|
||||
if (!colors.bg) return {}
|
||||
const hints = (ratio) => ({
|
||||
text: ratio.toPrecision(3) + ':1',
|
||||
// AA level, AAA level
|
||||
aa: ratio >= 4.5,
|
||||
aaa: ratio >= 7,
|
||||
// same but for 18pt+ texts
|
||||
laa: ratio >= 3,
|
||||
laaa: ratio >= 4.5
|
||||
})
|
||||
const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
|
||||
|
||||
// fgsfds :DDDD
|
||||
const fgs = {
|
||||
text: hex2rgb(colors.text),
|
||||
panelText: hex2rgb(colors.panelText),
|
||||
panelLink: hex2rgb(colors.panelLink),
|
||||
btnText: hex2rgb(colors.btnText),
|
||||
topBarText: hex2rgb(colors.topBarText),
|
||||
inputText: hex2rgb(colors.inputText),
|
||||
const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
|
||||
const slotIsBaseText = key === 'text' || key === 'link'
|
||||
const slotIsText = slotIsBaseText || (
|
||||
typeof value === 'object' && value !== null && value.textColor
|
||||
)
|
||||
if (!slotIsText) return acc
|
||||
const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
|
||||
const background = variant || layer
|
||||
const opacitySlot = getOpacitySlot(background)
|
||||
const textColors = [
|
||||
key,
|
||||
...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
|
||||
]
|
||||
|
||||
link: hex2rgb(colors.link),
|
||||
topBarLink: hex2rgb(colors.topBarLink),
|
||||
const layers = getLayers(
|
||||
layer,
|
||||
variant || layer,
|
||||
opacitySlot,
|
||||
colorsConverted,
|
||||
opacity
|
||||
)
|
||||
|
||||
red: hex2rgb(colors.cRed),
|
||||
green: hex2rgb(colors.cGreen),
|
||||
blue: hex2rgb(colors.cBlue),
|
||||
orange: hex2rgb(colors.cOrange)
|
||||
return {
|
||||
...acc,
|
||||
...textColors.reduce((acc, textColorKey) => {
|
||||
const newKey = slotIsBaseText
|
||||
? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
|
||||
: textColorKey
|
||||
return {
|
||||
...acc,
|
||||
[newKey]: getContrastRatioLayers(
|
||||
colorsConverted[textColorKey],
|
||||
layers,
|
||||
colorsConverted[textColorKey]
|
||||
)
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
}, {})
|
||||
|
||||
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
|
||||
} catch (e) {
|
||||
console.warn('Failure computing contrasts', e)
|
||||
}
|
||||
|
||||
const bgs = {
|
||||
bg: hex2rgb(colors.bg),
|
||||
btn: hex2rgb(colors.btn),
|
||||
panel: hex2rgb(colors.panel),
|
||||
topBar: hex2rgb(colors.topBar),
|
||||
input: hex2rgb(colors.input),
|
||||
alertError: hex2rgb(colors.alertError),
|
||||
alertWarning: hex2rgb(colors.alertWarning),
|
||||
badgeNotification: hex2rgb(colors.badgeNotification)
|
||||
}
|
||||
|
||||
/* This is a bit confusing because "bottom layer" used is text color
|
||||
* This is done to get worst case scenario when background below transparent
|
||||
* layer matches text color, making it harder to read the lower alpha is.
|
||||
*/
|
||||
const ratios = {
|
||||
bgText: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.text), fgs.text),
|
||||
bgLink: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.link), fgs.link),
|
||||
bgRed: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.red), fgs.red),
|
||||
bgGreen: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.green), fgs.green),
|
||||
bgBlue: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.blue), fgs.blue),
|
||||
bgOrange: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.orange), fgs.orange),
|
||||
|
||||
tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text),
|
||||
|
||||
panelText: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText),
|
||||
panelLink: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelLink), fgs.panelLink),
|
||||
|
||||
btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText),
|
||||
|
||||
inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText),
|
||||
|
||||
topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText),
|
||||
topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink)
|
||||
}
|
||||
|
||||
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
|
||||
},
|
||||
previewRules () {
|
||||
if (!this.preview.rules) return ''
|
||||
|
|
@ -272,7 +294,7 @@ export default {
|
|||
].join(';')
|
||||
},
|
||||
shadowsAvailable () {
|
||||
return Object.keys(this.previewTheme.shadows).sort()
|
||||
return Object.keys(DEFAULT_SHADOWS).sort()
|
||||
},
|
||||
currentShadowOverriden: {
|
||||
get () {
|
||||
|
|
@ -287,7 +309,7 @@ export default {
|
|||
}
|
||||
},
|
||||
currentShadowFallback () {
|
||||
return this.previewTheme.shadows[this.shadowSelected]
|
||||
return (this.previewTheme.shadows || {})[this.shadowSelected]
|
||||
},
|
||||
currentShadow: {
|
||||
get () {
|
||||
|
|
@ -309,27 +331,34 @@ export default {
|
|||
!this.keepColor
|
||||
)
|
||||
|
||||
const theme = {}
|
||||
const source = {
|
||||
themeEngineVersion: CURRENT_VERSION
|
||||
}
|
||||
|
||||
if (this.keepFonts || saveEverything) {
|
||||
theme.fonts = this.fontsLocal
|
||||
source.fonts = this.fontsLocal
|
||||
}
|
||||
if (this.keepShadows || saveEverything) {
|
||||
theme.shadows = this.shadowsLocal
|
||||
source.shadows = this.shadowsLocal
|
||||
}
|
||||
if (this.keepOpacity || saveEverything) {
|
||||
theme.opacity = this.currentOpacity
|
||||
source.opacity = this.currentOpacity
|
||||
}
|
||||
if (this.keepColor || saveEverything) {
|
||||
theme.colors = this.currentColors
|
||||
source.colors = this.currentColors
|
||||
}
|
||||
if (this.keepRoundness || saveEverything) {
|
||||
theme.radii = this.currentRadii
|
||||
source.radii = this.currentRadii
|
||||
}
|
||||
|
||||
const theme = {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
...this.previewTheme
|
||||
}
|
||||
|
||||
return {
|
||||
// To separate from other random JSON files and possible future theme formats
|
||||
_pleroma_theme_version: 2, theme
|
||||
// To separate from other random JSON files and possible future source formats
|
||||
_pleroma_theme_version: 2, theme, source
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -346,10 +375,127 @@ export default {
|
|||
Checkbox
|
||||
},
|
||||
methods: {
|
||||
loadTheme (
|
||||
{
|
||||
theme,
|
||||
source,
|
||||
_pleroma_theme_version: fileVersion
|
||||
},
|
||||
origin,
|
||||
forceUseSource = false
|
||||
) {
|
||||
if (!source && !theme) {
|
||||
throw new Error('Can\'t load theme: empty')
|
||||
}
|
||||
const version = (origin === 'localStorage' && !theme.colors)
|
||||
? 'l1'
|
||||
: fileVersion
|
||||
const snapshotEngineVersion = (theme || {}).themeEngineVersion
|
||||
const themeEngineVersion = (source || {}).themeEngineVersion || 2
|
||||
const versionsMatch = themeEngineVersion === CURRENT_VERSION
|
||||
const sourceSnapshotMismatch = (
|
||||
theme !== undefined &&
|
||||
source !== undefined &&
|
||||
themeEngineVersion !== snapshotEngineVersion
|
||||
)
|
||||
// Force loading of source if user requested it or if snapshot
|
||||
// is unavailable
|
||||
const forcedSourceLoad = (source && forceUseSource) || !theme
|
||||
if (!(versionsMatch && !sourceSnapshotMismatch) &&
|
||||
!forcedSourceLoad &&
|
||||
version !== 'l1' &&
|
||||
origin !== 'defaults'
|
||||
) {
|
||||
if (sourceSnapshotMismatch && origin === 'localStorage') {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
themeEngineVersion,
|
||||
type: 'snapshot_source_mismatch'
|
||||
}
|
||||
} else if (!theme) {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
noActionsPossible: true,
|
||||
themeEngineVersion,
|
||||
type: 'no_snapshot_old_version'
|
||||
}
|
||||
} else if (!versionsMatch) {
|
||||
this.themeWarning = {
|
||||
origin,
|
||||
noActionsPossible: !source,
|
||||
themeEngineVersion,
|
||||
type: 'wrong_version'
|
||||
}
|
||||
}
|
||||
}
|
||||
this.normalizeLocalState(theme, version, source, forcedSourceLoad)
|
||||
},
|
||||
forceLoadLocalStorage () {
|
||||
this.loadThemeFromLocalStorage(true)
|
||||
},
|
||||
dismissWarning () {
|
||||
this.themeWarning = undefined
|
||||
this.tempImportFile = undefined
|
||||
},
|
||||
forceLoad () {
|
||||
const { origin } = this.themeWarning
|
||||
switch (origin) {
|
||||
case 'localStorage':
|
||||
this.loadThemeFromLocalStorage(true)
|
||||
break
|
||||
case 'file':
|
||||
this.onImport(this.tempImportFile, true)
|
||||
break
|
||||
}
|
||||
this.dismissWarning()
|
||||
},
|
||||
forceSnapshot () {
|
||||
const { origin } = this.themeWarning
|
||||
switch (origin) {
|
||||
case 'localStorage':
|
||||
this.loadThemeFromLocalStorage(false, true)
|
||||
break
|
||||
case 'file':
|
||||
console.err('Forcing snapshout from file is not supported yet')
|
||||
break
|
||||
}
|
||||
this.dismissWarning()
|
||||
},
|
||||
loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
|
||||
const {
|
||||
customTheme: theme,
|
||||
customThemeSource: source
|
||||
} = this.$store.getters.mergedConfig
|
||||
if (!theme && !source) {
|
||||
// Anon user or never touched themes
|
||||
this.loadTheme(
|
||||
this.$store.state.instance.themeData,
|
||||
'defaults',
|
||||
confirmLoadSource
|
||||
)
|
||||
} else {
|
||||
this.loadTheme(
|
||||
{
|
||||
theme,
|
||||
source: forceSnapshot ? theme : source
|
||||
},
|
||||
'localStorage',
|
||||
confirmLoadSource
|
||||
)
|
||||
}
|
||||
},
|
||||
setCustomTheme () {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
...this.previewTheme
|
||||
}
|
||||
})
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'customThemeSource',
|
||||
value: {
|
||||
themeEngineVersion: CURRENT_VERSION,
|
||||
shadows: this.shadowsLocal,
|
||||
fonts: this.fontsLocal,
|
||||
opacity: this.currentOpacity,
|
||||
|
|
@ -358,21 +504,27 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
onImport (parsed) {
|
||||
if (parsed._pleroma_theme_version === 1) {
|
||||
this.normalizeLocalState(parsed, 1)
|
||||
} else if (parsed._pleroma_theme_version === 2) {
|
||||
this.normalizeLocalState(parsed.theme, 2)
|
||||
}
|
||||
updatePreviewColorsAndShadows () {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
this.previewShadows = generateShadows(
|
||||
{ shadows: this.shadowsLocal },
|
||||
this.previewColors.theme.colors,
|
||||
this.previewColors.mod
|
||||
)
|
||||
},
|
||||
onImport (parsed, forceSource = false) {
|
||||
this.tempImportFile = parsed
|
||||
this.loadTheme(parsed, 'file', forceSource)
|
||||
},
|
||||
importValidator (parsed) {
|
||||
const version = parsed._pleroma_theme_version
|
||||
return version >= 1 || version <= 2
|
||||
},
|
||||
clearAll () {
|
||||
const state = this.$store.getters.mergedConfig.customTheme
|
||||
const version = state.colors ? 2 : 'l1'
|
||||
this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version)
|
||||
this.loadThemeFromLocalStorage()
|
||||
},
|
||||
|
||||
// Clears all the extra stuff when loading V1 theme
|
||||
|
|
@ -411,19 +563,37 @@ export default {
|
|||
|
||||
/**
|
||||
* This applies stored theme data onto form. Supports three versions of data:
|
||||
* v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
|
||||
* v2 (version = 2) - newer version of themes.
|
||||
* v1 (version = 1) - older version of themes (import from file)
|
||||
* v1l (version = l1) - older version of theme (load from local storage)
|
||||
* v1 and v1l differ because of way themes were stored/exported.
|
||||
* @param {Object} input - input data
|
||||
* @param {Object} theme - theme data (snapshot)
|
||||
* @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
|
||||
* @param {Object} source - theme source - this will be used if compatible
|
||||
* @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
|
||||
* this allows importing source anyway
|
||||
*/
|
||||
normalizeLocalState (input, version = 0) {
|
||||
const colors = input.colors || input
|
||||
normalizeLocalState (theme, version = 0, source, forceSource = false) {
|
||||
let input
|
||||
if (typeof source !== 'undefined') {
|
||||
if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
|
||||
input = source
|
||||
version = source.themeEngineVersion
|
||||
} else {
|
||||
input = theme
|
||||
}
|
||||
} else {
|
||||
input = theme
|
||||
}
|
||||
|
||||
const radii = input.radii || input
|
||||
const opacity = input.opacity
|
||||
const shadows = input.shadows || {}
|
||||
const fonts = input.fonts || {}
|
||||
const colors = !input.themeEngineVersion
|
||||
? colors2to3(input.colors || input)
|
||||
: input.colors || input
|
||||
|
||||
if (version === 0) {
|
||||
if (input.version) version = input.version
|
||||
|
|
@ -457,7 +627,17 @@ export default {
|
|||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
this[key + 'ColorLocal'] = rgb2hex(colors[key])
|
||||
const color = colors[key]
|
||||
const hex = rgb2hex(colors[key])
|
||||
this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
|
||||
})
|
||||
}
|
||||
|
||||
if (opacity && !this.keepOpacity) {
|
||||
this.clearOpacity()
|
||||
Object.entries(opacity).forEach(([k, v]) => {
|
||||
if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
|
||||
this[k + 'OpacityLocal'] = v
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -472,7 +652,11 @@ export default {
|
|||
|
||||
if (!this.keepShadows) {
|
||||
this.clearShadows()
|
||||
this.shadowsLocal = shadows
|
||||
if (version === 2) {
|
||||
this.shadowsLocal = shadows2to3(shadows)
|
||||
} else {
|
||||
this.shadowsLocal = shadows
|
||||
}
|
||||
this.shadowSelected = this.shadowsAvailable[0]
|
||||
}
|
||||
|
||||
|
|
@ -480,14 +664,6 @@ export default {
|
|||
this.clearFonts()
|
||||
this.fontsLocal = fonts
|
||||
}
|
||||
|
||||
if (opacity && !this.keepOpacity) {
|
||||
this.clearOpacity()
|
||||
Object.entries(opacity).forEach(([k, v]) => {
|
||||
if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
|
||||
this[k + 'OpacityLocal'] = v
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -502,8 +678,9 @@ export default {
|
|||
},
|
||||
shadowsLocal: {
|
||||
handler () {
|
||||
if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
|
||||
try {
|
||||
this.previewShadows = generateShadows({ shadows: this.shadowsLocal })
|
||||
this.updatePreviewColorsAndShadows()
|
||||
this.shadowsInvalid = false
|
||||
} catch (e) {
|
||||
this.shadowsInvalid = true
|
||||
|
|
@ -526,22 +703,18 @@ export default {
|
|||
},
|
||||
currentColors () {
|
||||
try {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
this.updatePreviewColorsAndShadows()
|
||||
this.colorsInvalid = false
|
||||
this.shadowsInvalid = false
|
||||
} catch (e) {
|
||||
this.colorsInvalid = true
|
||||
this.shadowsInvalid = true
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
currentOpacity () {
|
||||
try {
|
||||
this.previewColors = generateColors({
|
||||
opacity: this.currentOpacity,
|
||||
colors: this.currentColors
|
||||
})
|
||||
this.updatePreviewColorsAndShadows()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
|
|
@ -573,7 +746,7 @@ export default {
|
|||
this.cOrangeColorLocal = this.selected[8]
|
||||
}
|
||||
} else if (this.selectedVersion >= 2) {
|
||||
this.normalizeLocalState(this.selected.theme, 2)
|
||||
this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
@import '../../_variables.scss';
|
||||
.style-switcher {
|
||||
.theme-warning {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: .5em;
|
||||
.buttons {
|
||||
.btn {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.preset-switcher {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
|
@ -15,26 +25,23 @@
|
|||
|
||||
&.disabled {
|
||||
input, select {
|
||||
&:not(.exclude-disabled) {
|
||||
opacity: .5
|
||||
}
|
||||
opacity: .5
|
||||
}
|
||||
}
|
||||
|
||||
.opt {
|
||||
margin: .5em;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
input, select {
|
||||
min-width: 3em;
|
||||
margin: 0;
|
||||
flex: 0;
|
||||
|
||||
&[type=color] {
|
||||
padding: 1px;
|
||||
cursor: pointer;
|
||||
height: 29px;
|
||||
min-width: 2em;
|
||||
border: none;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
&[type=number] {
|
||||
min-width: 5em;
|
||||
}
|
||||
|
|
@ -42,13 +49,6 @@
|
|||
&[type=range] {
|
||||
flex: 1;
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
&[type=checkbox] + label {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
&:not([type=number]):not([type=text]) {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,53 @@
|
|||
<div class="style-switcher">
|
||||
<div class="presets-container">
|
||||
<div class="save-load">
|
||||
<export-import
|
||||
<div
|
||||
v-if="themeWarning"
|
||||
class="theme-warning"
|
||||
>
|
||||
<div class="alert warning">
|
||||
{{ themeWarningHelp }}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<template v-if="themeWarning.type === 'snapshot_source_mismatch'">
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceLoad"
|
||||
>
|
||||
{{ $t('settings.style.switcher.use_source') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceSnapshot"
|
||||
>
|
||||
{{ $t('settings.style.switcher.use_snapshot') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="themeWarning.noActionsPossible">
|
||||
<button
|
||||
class="btn"
|
||||
@click="dismissWarning"
|
||||
>
|
||||
{{ $t('general.dismiss') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
class="btn"
|
||||
@click="forceLoad"
|
||||
>
|
||||
{{ $t('settings.style.switcher.load_theme') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="dismissWarning"
|
||||
>
|
||||
{{ $t('settings.style.switcher.keep_as_is') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ExportImport
|
||||
:export-object="exportedTheme"
|
||||
:export-label="$t("settings.export_theme")"
|
||||
:import-label="$t("settings.import_theme")"
|
||||
|
|
@ -27,8 +73,8 @@
|
|||
:key="style.name"
|
||||
:value="style"
|
||||
:style="{
|
||||
backgroundColor: style[1] || style.theme.colors.bg,
|
||||
color: style[3] || style.theme.colors.text
|
||||
backgroundColor: style[1] || (style.theme || style.source).colors.bg,
|
||||
color: style[3] || (style.theme || style.source).colors.text
|
||||
}"
|
||||
>
|
||||
{{ style[0] || style.name }}
|
||||
|
|
@ -38,7 +84,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</export-import>
|
||||
</ExportImport>
|
||||
</div>
|
||||
<div class="save-load-options">
|
||||
<span class="keep-option">
|
||||
|
|
@ -70,9 +116,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-container">
|
||||
<preview :style="previewRules" />
|
||||
</div>
|
||||
<preview :style="previewRules" />
|
||||
|
||||
<keep-alive>
|
||||
<tab-switcher key="style-tweak">
|
||||
|
|
@ -106,7 +150,7 @@
|
|||
<OpacityInput
|
||||
v-model="bgOpacityLocal"
|
||||
name="bgOpacity"
|
||||
:fallback="previewTheme.opacity.bg || 1"
|
||||
:fallback="previewTheme.opacity.bg"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="textColorLocal"
|
||||
|
|
@ -114,10 +158,19 @@
|
|||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgText" />
|
||||
<ColorInput
|
||||
v-model="accentColorLocal"
|
||||
name="accentColor"
|
||||
:fallback="previewTheme.colors.link"
|
||||
:label="$t('settings.accent')"
|
||||
:show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="linkColorLocal"
|
||||
name="linkColor"
|
||||
:fallback="previewTheme.colors.accent"
|
||||
:label="$t('settings.links')"
|
||||
:show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgLink" />
|
||||
</div>
|
||||
|
|
@ -148,13 +201,13 @@
|
|||
name="cRedColor"
|
||||
:label="$t('settings.cRed')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgRed" />
|
||||
<ContrastRatio :contrast="previewContrast.bgCRed" />
|
||||
<ColorInput
|
||||
v-model="cBlueColorLocal"
|
||||
name="cBlueColor"
|
||||
:label="$t('settings.cBlue')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgBlue" />
|
||||
<ContrastRatio :contrast="previewContrast.bgCBlue" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<ColorInput
|
||||
|
|
@ -162,13 +215,13 @@
|
|||
name="cGreenColor"
|
||||
:label="$t('settings.cGreen')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgGreen" />
|
||||
<ContrastRatio :contrast="previewContrast.bgCGreen" />
|
||||
<ColorInput
|
||||
v-model="cOrangeColorLocal"
|
||||
name="cOrangeColor"
|
||||
:label="$t('settings.cOrange')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.bgOrange" />
|
||||
<ContrastRatio :contrast="previewContrast.bgCOrange" />
|
||||
</div>
|
||||
<p>{{ $t('settings.theme_help_v2_2') }}</p>
|
||||
</div>
|
||||
|
|
@ -193,6 +246,14 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.post') }}</h4>
|
||||
<ColorInput
|
||||
v-model="postLinkColorLocal"
|
||||
name="postLinkColor"
|
||||
:fallback="previewTheme.colors.accent"
|
||||
:label="$t('settings.links')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.postLink" />
|
||||
<h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
|
||||
<ColorInput
|
||||
v-model="alertErrorColorLocal"
|
||||
|
|
@ -200,14 +261,53 @@
|
|||
:label="$t('settings.style.advanced_colors.alert_error')"
|
||||
:fallback="previewTheme.colors.alertError"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.alertError" />
|
||||
<ColorInput
|
||||
v-model="alertErrorTextColorLocal"
|
||||
name="alertErrorText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertErrorText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertErrorText"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertWarningColorLocal"
|
||||
name="alertWarning"
|
||||
:label="$t('settings.style.advanced_colors.alert_warning')"
|
||||
:fallback="previewTheme.colors.alertWarning"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.alertWarning" />
|
||||
<ColorInput
|
||||
v-model="alertWarningTextColorLocal"
|
||||
name="alertWarningText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertWarningText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertWarningText"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertNeutralColorLocal"
|
||||
name="alertNeutral"
|
||||
:label="$t('settings.style.advanced_colors.alert_neutral')"
|
||||
:fallback="previewTheme.colors.alertNeutral"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="alertNeutralTextColorLocal"
|
||||
name="alertNeutralText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.alertNeutralText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.alertNeutralText"
|
||||
large="true"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="alertOpacityLocal"
|
||||
name="alertOpacity"
|
||||
:fallback="previewTheme.opacity.alert"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
|
||||
|
|
@ -217,19 +317,30 @@
|
|||
:label="$t('settings.style.advanced_colors.badge_notification')"
|
||||
:fallback="previewTheme.colors.badgeNotification"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="badgeNotificationTextColorLocal"
|
||||
name="badgeNotificationText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.badgeNotificationText"
|
||||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.badgeNotificationText"
|
||||
large="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
|
||||
<ColorInput
|
||||
v-model="panelColorLocal"
|
||||
name="panelColor"
|
||||
:fallback="fgColorLocal"
|
||||
:fallback="previewTheme.colors.panel"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="panelOpacityLocal"
|
||||
name="panelOpacity"
|
||||
:fallback="previewTheme.opacity.panel || 1"
|
||||
:fallback="previewTheme.opacity.panel"
|
||||
:disabled="panelColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="panelTextColorLocal"
|
||||
|
|
@ -239,7 +350,7 @@
|
|||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.panelText"
|
||||
large="1"
|
||||
large="true"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="panelLinkColorLocal"
|
||||
|
|
@ -249,7 +360,7 @@
|
|||
/>
|
||||
<ContrastRatio
|
||||
:contrast="previewContrast.panelLink"
|
||||
large="1"
|
||||
large="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
|
|
@ -257,7 +368,7 @@
|
|||
<ColorInput
|
||||
v-model="topBarColorLocal"
|
||||
name="topBarColor"
|
||||
:fallback="fgColorLocal"
|
||||
:fallback="previewTheme.colors.topBar"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
|
|
@ -280,13 +391,14 @@
|
|||
<ColorInput
|
||||
v-model="inputColorLocal"
|
||||
name="inputColor"
|
||||
:fallback="fgColorLocal"
|
||||
:fallback="previewTheme.colors.input"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="inputOpacityLocal"
|
||||
name="inputOpacity"
|
||||
:fallback="previewTheme.opacity.input || 1"
|
||||
:fallback="previewTheme.opacity.input"
|
||||
:disabled="inputColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="inputTextColorLocal"
|
||||
|
|
@ -301,13 +413,14 @@
|
|||
<ColorInput
|
||||
v-model="btnColorLocal"
|
||||
name="btnColor"
|
||||
:fallback="fgColorLocal"
|
||||
:fallback="previewTheme.colors.btn"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="btnOpacityLocal"
|
||||
name="btnOpacity"
|
||||
:fallback="previewTheme.opacity.btn || 1"
|
||||
:fallback="previewTheme.opacity.btn"
|
||||
:disabled="btnColorLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnTextColorLocal"
|
||||
|
|
@ -316,6 +429,124 @@
|
|||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnText" />
|
||||
<ColorInput
|
||||
v-model="btnPanelTextColorLocal"
|
||||
name="btnPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnTopBarTextColorLocal"
|
||||
name="btnTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnTopBarText" />
|
||||
<h4>{{ $t('settings.style.advanced_colors.pressed') }}</h4>
|
||||
<ColorInput
|
||||
v-model="btnPressedColorLocal"
|
||||
name="btnPressedColor"
|
||||
:fallback="previewTheme.colors.btnPressed"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnPressedTextColorLocal"
|
||||
name="btnPressedTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedText" />
|
||||
<ColorInput
|
||||
v-model="btnPressedPanelTextColorLocal"
|
||||
name="btnPressedPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnPressedTopBarTextColorLocal"
|
||||
name="btnPressedTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnPressedTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
|
||||
<h4>{{ $t('settings.style.advanced_colors.disabled') }}</h4>
|
||||
<ColorInput
|
||||
v-model="btnDisabledColorLocal"
|
||||
name="btnDisabledColor"
|
||||
:fallback="previewTheme.colors.btnDisabled"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledTextColorLocal"
|
||||
name="btnDisabledTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledPanelTextColorLocal"
|
||||
name="btnDisabledPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnDisabledTopBarTextColorLocal"
|
||||
name="btnDisabledTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnDisabledTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<h4>{{ $t('settings.style.advanced_colors.toggled') }}</h4>
|
||||
<ColorInput
|
||||
v-model="btnToggledColorLocal"
|
||||
name="btnToggledColor"
|
||||
:fallback="previewTheme.colors.btnToggled"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="btnToggledTextColorLocal"
|
||||
name="btnToggledTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledText" />
|
||||
<ColorInput
|
||||
v-model="btnToggledPanelTextColorLocal"
|
||||
name="btnToggledPanelTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledPanelText"
|
||||
:label="$t('settings.style.advanced_colors.panel_header')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledPanelText" />
|
||||
<ColorInput
|
||||
v-model="btnToggledTopBarTextColorLocal"
|
||||
name="btnToggledTopBarTextColor"
|
||||
:fallback="previewTheme.colors.btnToggledTopBarText"
|
||||
:label="$t('settings.style.advanced_colors.top_bar')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.btnToggledTopBarText" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4>
|
||||
<ColorInput
|
||||
v-model="tabColorLocal"
|
||||
name="tabColor"
|
||||
:fallback="previewTheme.colors.tab"
|
||||
:label="$t('settings.background')"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="tabTextColorLocal"
|
||||
name="tabTextColor"
|
||||
:fallback="previewTheme.colors.tabText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.tabText" />
|
||||
<ColorInput
|
||||
v-model="tabActiveTextColorLocal"
|
||||
name="tabActiveTextColor"
|
||||
:fallback="previewTheme.colors.tabActiveText"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.tabActiveText" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
|
||||
|
|
@ -328,7 +559,8 @@
|
|||
<OpacityInput
|
||||
v-model="borderOpacityLocal"
|
||||
name="borderOpacity"
|
||||
:fallback="previewTheme.opacity.border || 1"
|
||||
:fallback="previewTheme.opacity.border"
|
||||
:disabled="borderColorLocal === 'transparent'"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
|
|
@ -336,7 +568,7 @@
|
|||
<ColorInput
|
||||
v-model="faintColorLocal"
|
||||
name="faintColor"
|
||||
:fallback="previewTheme.colors.faint || 1"
|
||||
:fallback="previewTheme.colors.faint"
|
||||
:label="$t('settings.text')"
|
||||
/>
|
||||
<ColorInput
|
||||
|
|
@ -354,9 +586,146 @@
|
|||
<OpacityInput
|
||||
v-model="faintOpacityLocal"
|
||||
name="faintOpacity"
|
||||
:fallback="previewTheme.opacity.faint || 0.5"
|
||||
:fallback="previewTheme.opacity.faint"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.underlay') }}</h4>
|
||||
<ColorInput
|
||||
v-model="underlayColorLocal"
|
||||
name="underlay"
|
||||
:label="$t('settings.style.advanced_colors.underlay')"
|
||||
:fallback="previewTheme.colors.underlay"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="underlayOpacityLocal"
|
||||
name="underlayOpacity"
|
||||
:fallback="previewTheme.opacity.underlay"
|
||||
:disabled="underlayOpacityLocal === 'transparent'"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.poll') }}</h4>
|
||||
<ColorInput
|
||||
v-model="pollColorLocal"
|
||||
name="poll"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.poll"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="pollTextColorLocal"
|
||||
name="pollText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.pollText"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.icons') }}</h4>
|
||||
<ColorInput
|
||||
v-model="iconColorLocal"
|
||||
name="icon"
|
||||
:label="$t('settings.style.advanced_colors.icons')"
|
||||
:fallback="previewTheme.colors.icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4>
|
||||
<ColorInput
|
||||
v-model="highlightColorLocal"
|
||||
name="highlight"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.highlight"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="highlightTextColorLocal"
|
||||
name="highlightText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.highlightText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.highlightText" />
|
||||
<ColorInput
|
||||
v-model="highlightLinkColorLocal"
|
||||
name="highlightLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.highlightLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.highlightLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.popover') }}</h4>
|
||||
<ColorInput
|
||||
v-model="popoverColorLocal"
|
||||
name="popover"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.popover"
|
||||
/>
|
||||
<OpacityInput
|
||||
v-model="popoverOpacityLocal"
|
||||
name="popoverOpacity"
|
||||
:fallback="previewTheme.opacity.popover"
|
||||
:disabled="popoverOpacityLocal === 'transparent'"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="popoverTextColorLocal"
|
||||
name="popoverText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.popoverText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.popoverText" />
|
||||
<ColorInput
|
||||
v-model="popoverLinkColorLocal"
|
||||
name="popoverLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.popoverLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.popoverLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
|
||||
<ColorInput
|
||||
v-model="selectedPostColorLocal"
|
||||
name="selectedPost"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.selectedPost"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="selectedPostTextColorLocal"
|
||||
name="selectedPostText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.selectedPostText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedPostText" />
|
||||
<ColorInput
|
||||
v-model="selectedPostLinkColorLocal"
|
||||
name="selectedPostLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.selectedPostLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedPostLink" />
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<h4>{{ $t('settings.style.advanced_colors.selectedMenu') }}</h4>
|
||||
<ColorInput
|
||||
v-model="selectedMenuColorLocal"
|
||||
name="selectedMenu"
|
||||
:label="$t('settings.background')"
|
||||
:fallback="previewTheme.colors.selectedMenu"
|
||||
/>
|
||||
<ColorInput
|
||||
v-model="selectedMenuTextColorLocal"
|
||||
name="selectedMenuText"
|
||||
:label="$t('settings.text')"
|
||||
:fallback="previewTheme.colors.selectedMenuText"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedMenuText" />
|
||||
<ColorInput
|
||||
v-model="selectedMenuLinkColorLocal"
|
||||
name="selectedMenuLink"
|
||||
:label="$t('settings.links')"
|
||||
:fallback="previewTheme.colors.selectedMenuLink"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.selectedMenuLink" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -491,7 +860,7 @@
|
|||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
<shadow-control
|
||||
<ShadowControl
|
||||
v-model="currentShadow"
|
||||
:ready="!!currentShadowFallback"
|
||||
:fallback="currentShadowFallback"
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@
|
|||
margin-bottom: 6px - 99px;
|
||||
white-space: nowrap;
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--tabText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--tab, $fallback--fg);
|
||||
|
||||
&:not(.active) {
|
||||
z-index: 4;
|
||||
|
||||
|
|
@ -63,6 +68,8 @@
|
|||
&.active {
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
color: $fallback--text;
|
||||
color: var(--tabActiveText, $fallback--text);
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-else
|
||||
class="btn btn-default pressed"
|
||||
class="btn btn-default toggled"
|
||||
:click="unsubscribeUser"
|
||||
:title="$t('user_card.unsubscribe')"
|
||||
>
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
<div>
|
||||
<button
|
||||
v-if="user.muted"
|
||||
class="btn btn-default btn-block pressed"
|
||||
class="btn btn-default btn-block toggled"
|
||||
@click="unmuteUser"
|
||||
>
|
||||
{{ $t('user_card.muted') }}
|
||||
|
|
@ -299,6 +299,11 @@
|
|||
&-bio {
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
|
|
@ -460,14 +465,13 @@
|
|||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
||||
// TODO use proper colors
|
||||
.staff {
|
||||
flex: none;
|
||||
text-transform: capitalize;
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
color: var(--alertNeutralText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
background-color: var(--alertNeutral, $fallback--fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -538,12 +542,6 @@
|
|||
|
||||
button {
|
||||
margin: 0;
|
||||
|
||||
&.pressed {
|
||||
// TODO: This should be themed.
|
||||
border-bottom-color: rgba(255, 255, 255, 0.2);
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
"optional": "optional",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
"dismiss": "Dismiss",
|
||||
"cancel": "Cancel",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
|
|
@ -281,6 +282,7 @@
|
|||
"follow_import": "Follow import",
|
||||
"follow_import_error": "Error importing followers",
|
||||
"follows_imported": "Follows imported! Processing them will take a while.",
|
||||
"accent": "Accent",
|
||||
"foreground": "Foreground",
|
||||
"general": "General",
|
||||
"hide_attachments_in_convo": "Hide attachments in conversations",
|
||||
|
|
@ -406,7 +408,24 @@
|
|||
"save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.",
|
||||
"reset": "Reset",
|
||||
"clear_all": "Clear all",
|
||||
"clear_opacity": "Clear opacity"
|
||||
"clear_opacity": "Clear opacity",
|
||||
"load_theme": "Load theme",
|
||||
"keep_as_is": "Keep as is",
|
||||
"use_snapshot": "Old version",
|
||||
"use_source": "New version",
|
||||
"help": {
|
||||
"upgraded_from_v2": "PleromaFE has been upgraded, theme could look a little bit different than you remember.",
|
||||
"v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsitencies.",
|
||||
"future_version_imported": "File you imported was made in newer version of FE.",
|
||||
"older_version_imported": "File you imported was made in older version of FE.",
|
||||
"snapshot_present": "Theme snapshot is loaded, so all values are overriden. You can load theme's actual data instead.",
|
||||
"snapshot_missing": "No theme snapshot was in the file so it could look different than originally envisioned.",
|
||||
"fe_upgraded": "PleromaFE's theme engine upgraded after version update.",
|
||||
"fe_downgraded": "PleromaFE's version rolled back.",
|
||||
"migration_snapshot_ok": "Just to be safe, theme snapshot loaded. You can try loading theme data.",
|
||||
"migration_napshot_gone": "For whatever reason snapshot was missing, some stuff could look different than you remember.",
|
||||
"snapshot_source_mismatch": "Versions conflict: most likely FE was rolled back and updated again, if you changed theme using older version of FE you most likely want to use old version, otherwise use new version."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"color": "Color",
|
||||
|
|
@ -435,14 +454,27 @@
|
|||
"alert": "Alert background",
|
||||
"alert_error": "Error",
|
||||
"alert_warning": "Warning",
|
||||
"alert_neutral": "Neutral",
|
||||
"post": "Posts/User bios",
|
||||
"badge": "Badge background",
|
||||
"popover": "Tooltips, menus, popovers",
|
||||
"badge_notification": "Notification",
|
||||
"panel_header": "Panel header",
|
||||
"top_bar": "Top bar",
|
||||
"borders": "Borders",
|
||||
"buttons": "Buttons",
|
||||
"inputs": "Input fields",
|
||||
"faint_text": "Faded text"
|
||||
"faint_text": "Faded text",
|
||||
"underlay": "Underlay",
|
||||
"poll": "Poll graph",
|
||||
"icons": "Icons",
|
||||
"highlight": "Highlighted elements",
|
||||
"pressed": "Pressed",
|
||||
"selectedPost": "Selected post",
|
||||
"selectedMenu": "Selected menu item",
|
||||
"disabled": "Disabled",
|
||||
"toggled": "Toggled",
|
||||
"tabs": "Tabs"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Roundness"
|
||||
|
|
@ -455,7 +487,7 @@
|
|||
"blur": "Blur",
|
||||
"spread": "Spread",
|
||||
"inset": "Inset",
|
||||
"hint": "For shadows you can also use --variable as a color value to use CSS3 variables. Please note that setting opacity won't work in this case.",
|
||||
"hintV3": "For shadows you can also use the {0} notation to use other color slot.",
|
||||
"filter_hint": {
|
||||
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
|
||||
"drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
|||
|
||||
export const defaultState = {
|
||||
colors: {},
|
||||
theme: undefined,
|
||||
customTheme: undefined,
|
||||
customThemeSource: undefined,
|
||||
hideISP: false,
|
||||
// bad name: actually hides posts of muted USERS
|
||||
hideMutedPosts: undefined, // instance default
|
||||
|
|
@ -95,10 +98,10 @@ const config = {
|
|||
commit('setOption', { name, value })
|
||||
switch (name) {
|
||||
case 'theme':
|
||||
setPreset(value, commit)
|
||||
setPreset(value)
|
||||
break
|
||||
case 'customTheme':
|
||||
applyTheme(value, commit)
|
||||
applyTheme(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { set } from 'vue'
|
||||
import { setPreset } from '../services/style_setter/style_setter.js'
|
||||
import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
|
||||
import { instanceDefaultProperties } from './config.js'
|
||||
|
||||
const defaultState = {
|
||||
|
|
@ -10,6 +10,7 @@ const defaultState = {
|
|||
textlimit: 5000,
|
||||
server: 'http://localhost:4040/',
|
||||
theme: 'pleroma-dark',
|
||||
themeData: undefined,
|
||||
background: '/static/aurora_borealis.jpg',
|
||||
logo: '/static/logo.png',
|
||||
logoMask: true,
|
||||
|
|
@ -97,6 +98,9 @@ const instance = {
|
|||
dispatch('initializeSocket')
|
||||
}
|
||||
break
|
||||
case 'theme':
|
||||
dispatch('setTheme', value)
|
||||
break
|
||||
}
|
||||
},
|
||||
async getStaticEmoji ({ commit }) {
|
||||
|
|
@ -148,9 +152,16 @@ const instance = {
|
|||
}
|
||||
},
|
||||
|
||||
setTheme ({ commit }, themeName) {
|
||||
setTheme ({ commit, rootState }, themeName) {
|
||||
commit('setInstanceOption', { name: 'theme', value: themeName })
|
||||
return setPreset(themeName, commit)
|
||||
getPreset(themeName)
|
||||
.then(themeData => {
|
||||
commit('setInstanceOption', { name: 'themeData', value: themeData })
|
||||
// No need to apply theme if there's user theme already
|
||||
const { customTheme } = rootState.config
|
||||
if (customTheme) return
|
||||
applyTheme(themeData.theme)
|
||||
})
|
||||
},
|
||||
fetchEmoji ({ dispatch, state }) {
|
||||
if (!state.customEmojiFetched) {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
import { map } from 'lodash'
|
||||
import { invertLightness, contrastRatio } from 'chromatism'
|
||||
|
||||
const rgb2hex = (r, g, b) => {
|
||||
// useful for visualizing color when debugging
|
||||
export const consoleColor = (color) => console.log('%c##########', 'background: ' + color + '; color: ' + color)
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
if (r[0] === '#') {
|
||||
// TODO: clean up this mess
|
||||
if (r[0] === '#' || r === 'transparent') {
|
||||
return r
|
||||
}
|
||||
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
|
||||
|
|
@ -58,7 +69,7 @@ const srgbToLinear = (srgb) => {
|
|||
* @param {Object} srgb - sRGB color
|
||||
* @returns {Number} relative luminance
|
||||
*/
|
||||
const relativeLuminance = (srgb) => {
|
||||
export const relativeLuminance = (srgb) => {
|
||||
const { r, g, b } = srgbToLinear(srgb)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
}
|
||||
|
|
@ -71,7 +82,7 @@ const relativeLuminance = (srgb) => {
|
|||
* @param {Object} b - sRGB color
|
||||
* @returns {Number} color ratio
|
||||
*/
|
||||
const getContrastRatio = (a, b) => {
|
||||
export const getContrastRatio = (a, b) => {
|
||||
const la = relativeLuminance(a)
|
||||
const lb = relativeLuminance(b)
|
||||
const [l1, l2] = la > lb ? [la, lb] : [lb, la]
|
||||
|
|
@ -79,6 +90,17 @@ const getContrastRatio = (a, b) => {
|
|||
return (l1 + 0.05) / (l2 + 0.05)
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `getContrastRatio` but for multiple layers in-between
|
||||
*
|
||||
* @param {Object} text - text color (topmost layer)
|
||||
* @param {[Object, Number]} layers[] - layers between text and bedrock
|
||||
* @param {Object} bedrock - layer at the very bottom
|
||||
*/
|
||||
export const getContrastRatioLayers = (text, layers, bedrock) => {
|
||||
return getContrastRatio(alphaBlendLayers(bedrock, layers), text)
|
||||
}
|
||||
|
||||
/**
|
||||
* This performs alpha blending between solid background and semi-transparent foreground
|
||||
*
|
||||
|
|
@ -87,7 +109,7 @@ const getContrastRatio = (a, b) => {
|
|||
* @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
|
||||
|
|
@ -97,14 +119,30 @@ const alphaBlend = (fg, fga, bg) => {
|
|||
}, {})
|
||||
}
|
||||
|
||||
const invert = (rgb) => {
|
||||
/**
|
||||
* Same as `alphaBlend` but for multiple layers in-between
|
||||
*
|
||||
* @param {Object} bedrock - layer at the very bottom
|
||||
* @param {[Object, Number]} layers[] - layers between text and bedrock
|
||||
*/
|
||||
export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color, opacity]) => {
|
||||
return alphaBlend(color, opacity, acc)
|
||||
}, bedrock)
|
||||
|
||||
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),
|
||||
|
|
@ -113,18 +151,74 @@ const hex2rgb = (hex) => {
|
|||
} : null
|
||||
}
|
||||
|
||||
const mixrgb = (a, b) => {
|
||||
return Object.keys(a).reduce((acc, k) => {
|
||||
/**
|
||||
* 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 'rgb'.split('').reduce((acc, k) => {
|
||||
acc[k] = (a[k] + b[k]) / 2
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export {
|
||||
rgb2hex,
|
||||
hex2rgb,
|
||||
mixrgb,
|
||||
invert,
|
||||
getContrastRatio,
|
||||
alphaBlend
|
||||
/**
|
||||
* Converts rgb object into a CSS rgba() color
|
||||
*
|
||||
* @param {Object} color - rgb
|
||||
* @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})`
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 contrast = getContrastRatio(bg, text)
|
||||
|
||||
if (contrast < 4.5) {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 } 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 })
|
||||
}
|
||||
export const generateColors = (themeData) => {
|
||||
const sourceColors = !themeData.themeEngineVersion
|
||||
? colors2to3(themeData.colors || themeData)
|
||||
: themeData.colors || themeData
|
||||
|
||||
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')
|
||||
|
||||
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,83 +154,117 @@ 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.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: {
|
||||
|
|
@ -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,107 @@ 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 = () => {
|
||||
export const getThemes = () => {
|
||||
return window.fetch('/static/styles.json')
|
||||
.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)
|
||||
.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')
|
||||
|
||||
data.colors = {
|
||||
bg: bgRgb,
|
||||
fg: fgRgb,
|
||||
text: textRgb,
|
||||
link: linkRgb,
|
||||
cRed: cRedRgb,
|
||||
cBlue: cBlueRgb,
|
||||
cGreen: cGreenRgb,
|
||||
cOrange: cOrangeRgb
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 { ...shadowsAcc, [slotName]: newShadow }
|
||||
}, {})
|
||||
}
|
||||
|
||||
export {
|
||||
setStyle,
|
||||
setPreset,
|
||||
applyTheme,
|
||||
getTextColor,
|
||||
generateColors,
|
||||
generateRadii,
|
||||
generateShadows,
|
||||
generateFonts,
|
||||
generatePreset,
|
||||
getThemes,
|
||||
composePreset,
|
||||
getCssShadow,
|
||||
getCssShadowFilter
|
||||
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 }
|
||||
}
|
||||
|
||||
return { theme: data, source: theme.source }
|
||||
})
|
||||
}
|
||||
|
||||
export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
|
||||
|
|
|
|||
611
src/services/theme_data/pleromafe.js
Normal file
611
src/services/theme_data/pleromafe.js
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
import { invertLightness, brightness } from 'chromatism'
|
||||
import { alphaBlend, mixrgb } from '../color_convert/color_convert.js'
|
||||
/* 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',
|
||||
highlight: 'bg',
|
||||
panel: 'bg',
|
||||
popover: 'bg',
|
||||
selectedMenu: 'popover',
|
||||
btn: 'bg',
|
||||
btnPanel: 'panel',
|
||||
btnTopBar: 'topBar',
|
||||
input: 'bg',
|
||||
inputPanel: 'panel',
|
||||
inputTopBar: 'topBar',
|
||||
alert: 'bg',
|
||||
alertPanel: 'panel',
|
||||
poll: 'bg'
|
||||
}
|
||||
|
||||
/* By default opacity slots have 1 as default opacity
|
||||
* this allows redefining it to something else
|
||||
*/
|
||||
export const DEFAULT_OPACITY = {
|
||||
alert: 0.5,
|
||||
input: 0.5,
|
||||
faint: 0.5,
|
||||
underlay: 0.15
|
||||
}
|
||||
|
||||
/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
|
||||
* Color and opacity slots definitions. Each key represents a slot.
|
||||
*
|
||||
* Short-hands:
|
||||
* String beginning with `--` - value after dashes treated as sole
|
||||
* dependency - i.e. `--value` equivalent to { depends: ['value']}
|
||||
* String beginning with `#` - value would be treated as solid color
|
||||
* defined in hexadecimal representation (i.e. #FFFFFF) and will be
|
||||
* used as default. `#FFFFFF` is equivalent to { default: '#FFFFFF'}
|
||||
*
|
||||
* Full definition:
|
||||
* @property {String[]} depends - color slot names this color depends ones.
|
||||
* cyclic dependencies are supported to some extent but not recommended.
|
||||
* @property {String} [opacity] - opacity slot used by this color slot.
|
||||
* opacity is inherited from parents. To break inheritance graph use null
|
||||
* @property {Number} [priority] - EXPERIMENTAL. used to pre-sort slots so
|
||||
* that slots with higher priority come earlier
|
||||
* @property {Function(mod, ...colors)} [color] - function that will be
|
||||
* used to determine the color. By default it just copies first color in
|
||||
* dependency list.
|
||||
* @argument {Number} mod - `1` (light-on-dark) or `-1` (dark-on-light)
|
||||
* depending on background color (for textColor)/given color.
|
||||
* @argument {...Object} deps - each argument after mod represents each
|
||||
* color from `depends` array. All colors take user customizations into
|
||||
* account and represented by { r, g, b } objects.
|
||||
* @returns {Object} resulting color, should be in { r, g, b } form
|
||||
*
|
||||
* @property {Boolean|String} [textColor] - true to mark color slot as text
|
||||
* color. This enables automatic text color generation for the slot. Use
|
||||
* 'preserve' string if you don't want text color to fall back to
|
||||
* black/white. Use 'bw' to only ever use black or white. This also makes
|
||||
* following properties required:
|
||||
* @property {String} [layer] - which layer the text sit on top on - used
|
||||
* to account for transparency in text color calculation
|
||||
* layer is inherited from parents. To break inheritance graph use null
|
||||
* @property {String} [variant] - which color slot is background (same as
|
||||
* above, used to account for transparency)
|
||||
*/
|
||||
export const SLOT_INHERITANCE = {
|
||||
bg: {
|
||||
depends: [],
|
||||
opacity: 'bg',
|
||||
priority: 1
|
||||
},
|
||||
fg: {
|
||||
depends: [],
|
||||
priority: 1
|
||||
},
|
||||
text: {
|
||||
depends: [],
|
||||
layer: 'bg',
|
||||
opacity: null,
|
||||
priority: 1
|
||||
},
|
||||
underlay: {
|
||||
default: '#000000',
|
||||
opacity: 'underlay'
|
||||
},
|
||||
link: {
|
||||
depends: ['accent'],
|
||||
priority: 1
|
||||
},
|
||||
accent: {
|
||||
depends: ['link'],
|
||||
priority: 1
|
||||
},
|
||||
faint: {
|
||||
depends: ['text'],
|
||||
opacity: 'faint'
|
||||
},
|
||||
faintLink: {
|
||||
depends: ['link'],
|
||||
opacity: 'faint'
|
||||
},
|
||||
postFaintLink: {
|
||||
depends: ['postLink'],
|
||||
opacity: 'faint'
|
||||
},
|
||||
|
||||
cBlue: '#0000ff',
|
||||
cRed: '#FF0000',
|
||||
cGreen: '#00FF00',
|
||||
cOrange: '#E3FF00',
|
||||
|
||||
highlight: {
|
||||
depends: ['bg'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
},
|
||||
highlightLightText: {
|
||||
depends: ['lightText'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
},
|
||||
highlightPostLink: {
|
||||
depends: ['postLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
highlightFaintText: {
|
||||
depends: ['faint'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
},
|
||||
highlightFaintLink: {
|
||||
depends: ['faintLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
highlightPostFaintLink: {
|
||||
depends: ['postFaintLink'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
highlightText: {
|
||||
depends: ['text'],
|
||||
layer: 'highlight',
|
||||
textColor: true
|
||||
},
|
||||
highlightLink: {
|
||||
depends: ['link'],
|
||||
layer: 'highlight',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
highlightIcon: {
|
||||
depends: ['highlight', 'highlightText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
popover: {
|
||||
depends: ['bg'],
|
||||
opacity: 'popover'
|
||||
},
|
||||
popoverLightText: {
|
||||
depends: ['lightText'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
},
|
||||
popoverPostLink: {
|
||||
depends: ['postLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
popoverFaintText: {
|
||||
depends: ['faint'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
},
|
||||
popoverFaintLink: {
|
||||
depends: ['faintLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
popoverPostFaintLink: {
|
||||
depends: ['postFaintLink'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
popoverText: {
|
||||
depends: ['text'],
|
||||
layer: 'popover',
|
||||
textColor: true
|
||||
},
|
||||
popoverLink: {
|
||||
depends: ['link'],
|
||||
layer: 'popover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
popoverIcon: {
|
||||
depends: ['popover', 'popoverText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
selectedPost: '--highlight',
|
||||
selectedPostFaintText: {
|
||||
depends: ['highlightFaintText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
},
|
||||
selectedPostLightText: {
|
||||
depends: ['highlightLightText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
},
|
||||
selectedPostPostLink: {
|
||||
depends: ['highlightPostLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedPostFaintLink: {
|
||||
depends: ['highlightFaintLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedPostText: {
|
||||
depends: ['highlightText'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: true
|
||||
},
|
||||
selectedPostLink: {
|
||||
depends: ['highlightLink'],
|
||||
layer: 'highlight',
|
||||
variant: 'selectedPost',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedPostIcon: {
|
||||
depends: ['selectedPost', 'selectedPostText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
selectedMenu: {
|
||||
depends: ['bg'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
},
|
||||
selectedMenuLightText: {
|
||||
depends: ['highlightLightText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuFaintText: {
|
||||
depends: ['highlightFaintText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuFaintLink: {
|
||||
depends: ['highlightFaintLink'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedMenuText: {
|
||||
depends: ['highlightText'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuLink: {
|
||||
depends: ['highlightLink'],
|
||||
layer: 'selectedMenu',
|
||||
variant: 'selectedMenu',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedMenuIcon: {
|
||||
depends: ['selectedMenu', 'selectedMenuText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
selectedMenuPopover: {
|
||||
depends: ['popover'],
|
||||
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
||||
},
|
||||
selectedMenuPopoverLightText: {
|
||||
depends: ['selectedMenuLightText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuPopoverFaintText: {
|
||||
depends: ['selectedMenuFaintText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuPopoverFaintLink: {
|
||||
depends: ['selectedMenuFaintLink'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedMenuPopoverText: {
|
||||
depends: ['selectedMenuText'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: true
|
||||
},
|
||||
selectedMenuPopoverLink: {
|
||||
depends: ['selectedMenuLink'],
|
||||
layer: 'selectedMenuPopover',
|
||||
variant: 'selectedMenuPopover',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
selectedMenuPopoverIcon: {
|
||||
depends: ['selectedMenuPopover', 'selectedMenuText'],
|
||||
color: (mod, bg, text) => mixrgb(bg, text)
|
||||
},
|
||||
|
||||
lightText: {
|
||||
depends: ['text'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve',
|
||||
color: (mod, text) => brightness(20 * mod, text).rgb
|
||||
},
|
||||
|
||||
postLink: {
|
||||
depends: ['link'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
border: {
|
||||
depends: ['fg'],
|
||||
opacity: 'border',
|
||||
color: (mod, fg) => brightness(2 * mod, fg).rgb
|
||||
},
|
||||
|
||||
poll: {
|
||||
depends: ['accent', 'bg'],
|
||||
copacity: 'poll',
|
||||
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg)
|
||||
},
|
||||
pollText: {
|
||||
depends: ['text'],
|
||||
layer: 'poll',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
icon: {
|
||||
depends: ['bg', 'text'],
|
||||
inheritsOpacity: false,
|
||||
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: {
|
||||
depends: ['fg'],
|
||||
opacity: 'panel'
|
||||
},
|
||||
panelText: {
|
||||
depends: ['text'],
|
||||
layer: 'panel',
|
||||
textColor: true
|
||||
},
|
||||
panelFaint: {
|
||||
depends: ['fgText'],
|
||||
layer: 'panel',
|
||||
opacity: 'faint',
|
||||
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'
|
||||
},
|
||||
|
||||
// Tabs
|
||||
tab: {
|
||||
depends: ['btn']
|
||||
},
|
||||
tabText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
textColor: true
|
||||
},
|
||||
tabActiveText: {
|
||||
depends: ['text'],
|
||||
layer: 'bg',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
// Buttons
|
||||
btn: {
|
||||
depends: ['fg'],
|
||||
variant: 'btn',
|
||||
opacity: 'btn'
|
||||
},
|
||||
btnText: {
|
||||
depends: ['fgText'],
|
||||
layer: 'btn',
|
||||
textColor: true
|
||||
},
|
||||
btnPanelText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btn',
|
||||
textColor: true
|
||||
},
|
||||
btnTopBarText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btn',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
// Buttons: pressed
|
||||
btnPressed: {
|
||||
depends: ['btn'],
|
||||
layer: 'btn'
|
||||
},
|
||||
btnPressedText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
},
|
||||
btnPressedPanel: {
|
||||
depends: ['btnPressed'],
|
||||
layer: 'btn'
|
||||
},
|
||||
btnPressedPanelText: {
|
||||
depends: ['btnPanelText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
},
|
||||
btnPressedTopBarText: {
|
||||
depends: ['btnTopBarText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnPressed',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
// Buttons: toggled
|
||||
btnToggled: {
|
||||
depends: ['btn'],
|
||||
layer: 'btn',
|
||||
color: (mod, btn) => brightness(mod * 20, btn).rgb
|
||||
},
|
||||
btnToggledText: {
|
||||
depends: ['btnText'],
|
||||
layer: 'btn',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
},
|
||||
btnToggledPanelText: {
|
||||
depends: ['btnPanelText'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
},
|
||||
btnToggledTopBarText: {
|
||||
depends: ['btnTopBarText'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnToggled',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
// Buttons: disabled
|
||||
btnDisabled: {
|
||||
depends: ['btn', 'bg'],
|
||||
color: (mod, btn, bg) => alphaBlend(btn, 0.5, bg)
|
||||
},
|
||||
btnDisabledText: {
|
||||
depends: ['btnText', 'btnDisabled'],
|
||||
layer: 'btn',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.5, btn)
|
||||
},
|
||||
btnDisabledPanelText: {
|
||||
depends: ['btnPanelText', 'btnDisabled'],
|
||||
layer: 'btnPanel',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.5, btn)
|
||||
},
|
||||
btnDisabledTopBarText: {
|
||||
depends: ['btnTopBarText', 'btnDisabled'],
|
||||
layer: 'btnTopBar',
|
||||
variant: 'btnDisabled',
|
||||
color: (mod, text, btn) => alphaBlend(text, 0.5, btn)
|
||||
},
|
||||
|
||||
// Input fields
|
||||
input: {
|
||||
depends: ['fg'],
|
||||
opacity: 'input'
|
||||
},
|
||||
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: {
|
||||
depends: ['cRed'],
|
||||
opacity: 'alert'
|
||||
},
|
||||
alertErrorText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertError',
|
||||
textColor: true
|
||||
},
|
||||
alertErrorPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertError',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
alertWarning: {
|
||||
depends: ['cOrange'],
|
||||
opacity: 'alert'
|
||||
},
|
||||
alertWarningText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertWarning',
|
||||
textColor: true
|
||||
},
|
||||
alertWarningPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertWarning',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
alertNeutral: {
|
||||
depends: ['text'],
|
||||
opacity: 'alert'
|
||||
},
|
||||
alertNeutralText: {
|
||||
depends: ['text'],
|
||||
layer: 'alert',
|
||||
variant: 'alertNeutral',
|
||||
color: (mod, text) => invertLightness(text).rgb,
|
||||
textColor: true
|
||||
},
|
||||
alertNeutralPanelText: {
|
||||
depends: ['panelText'],
|
||||
layer: 'alertPanel',
|
||||
variant: 'alertNeutral',
|
||||
textColor: true
|
||||
},
|
||||
|
||||
badgeNotification: '--cRed',
|
||||
badgeNotificationText: {
|
||||
depends: ['text', 'badgeNotification'],
|
||||
layer: 'badge',
|
||||
variant: 'badgeNotification',
|
||||
textColor: 'bw'
|
||||
}
|
||||
}
|
||||
374
src/services/theme_data/theme_data.service.js
Normal file
374
src/services/theme_data/theme_data.service.js
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
import { convert, brightness, contrastRatio } from 'chromatism'
|
||||
import { alphaBlendLayers, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
|
||||
import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
|
||||
|
||||
/*
|
||||
* # What's all this?
|
||||
* Here be theme engine for pleromafe. All of this supposed to ease look
|
||||
* and feel customization, making widget styles and make developer's life
|
||||
* easier when it comes to supporting themes. Like many other theme systems
|
||||
* it operates on color definitions, or "slots" - for example you define
|
||||
* "button" color slot and then in UI component Button's CSS you refer to
|
||||
* it as a CSS3 Variable.
|
||||
*
|
||||
* Some applications allow you to customize colors for certain things.
|
||||
* Some UI toolkits allow you to define colors for each type of widget.
|
||||
* Most of them are pretty barebones and have no assistance for common
|
||||
* problems and cases, and in general themes themselves are very hard to
|
||||
* maintain in all aspects. This theme engine tries to solve all of the
|
||||
* common problems with themes.
|
||||
*
|
||||
* You don't have redefine several similar colors if you just want to
|
||||
* change one color - all color slots are derived from other ones, so you
|
||||
* can have at least one or two "basic" colors defined and have all other
|
||||
* components inherit and modify basic ones.
|
||||
*
|
||||
* You don't have to test contrast ratio for colors or pick text color for
|
||||
* each element even if you have light-on-dark elements in dark-on-light
|
||||
* theme.
|
||||
*
|
||||
* You don't have to maintain order of code for inheriting slots from othet
|
||||
* slots - dependency graph resolving does it for you.
|
||||
*/
|
||||
|
||||
/* This indicates that this version of code outputs similar theme data and
|
||||
* should be incremented if output changes - for instance if getTextColor
|
||||
* function changes and older themes no longer render text colors as
|
||||
* author intended previously.
|
||||
*/
|
||||
export const CURRENT_VERSION = 3
|
||||
|
||||
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, opacitySlot, colors, opacity) => {
|
||||
return getLayersArray(layer).map((currentLayer) => ([
|
||||
currentLayer === layer
|
||||
? colors[variant]
|
||||
: colors[currentLayer],
|
||||
currentLayer === layer
|
||||
? opacity[opacitySlot] || 1
|
||||
: 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts inheritance object topologically - dependant slots come after
|
||||
* dependencies
|
||||
*
|
||||
* @property {Object} inheritance - object defining the nodes
|
||||
* @property {Function} getDeps - function that returns dependencies for
|
||||
* given value and inheritance object.
|
||||
* @returns {String[]} keys of inheritance object, sorted in topological
|
||||
* order. Additionally, dependency-less nodes will always be first in line
|
||||
*/
|
||||
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.sort((a, b) => {
|
||||
const depsA = getDeps(a, inheritance).length
|
||||
const depsB = getDeps(b, inheritance).length
|
||||
|
||||
if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return 0
|
||||
if (depsA === 0 && depsB !== 0) return -1
|
||||
if (depsB === 0 && depsA !== 0) return 1
|
||||
})
|
||||
}
|
||||
|
||||
const expandSlotValue = (value) => {
|
||||
if (typeof value === 'object') return value
|
||||
return {
|
||||
depends: value.startsWith('--') ? [value.substring(2)] : [],
|
||||
default: value.startsWith('#') ? value : undefined
|
||||
}
|
||||
}
|
||||
/**
|
||||
* retrieves opacity slot for given slot. This goes up the depenency graph
|
||||
* to find which parent has opacity slot defined for it.
|
||||
* TODO refactor this
|
||||
*/
|
||||
export const getOpacitySlot = (
|
||||
k,
|
||||
inheritance = SLOT_INHERITANCE,
|
||||
getDeps = getDependencies
|
||||
) => {
|
||||
const value = expandSlotValue(inheritance[k])
|
||||
if (value.opacity === null) return
|
||||
if (value.opacity) return value.opacity
|
||||
const findInheritedOpacity = (key, visited = [k]) => {
|
||||
const depSlot = getDeps(key, inheritance)[0]
|
||||
if (depSlot === undefined) return
|
||||
const dependency = inheritance[depSlot]
|
||||
if (dependency === undefined) return
|
||||
if (dependency.opacity || dependency === null) {
|
||||
return dependency.opacity
|
||||
} else if (dependency.depends && visited.includes(depSlot)) {
|
||||
return findInheritedOpacity(depSlot, [...visited, depSlot])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (value.depends) {
|
||||
return findInheritedOpacity(k)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves layer slot for given slot. This goes up the depenency graph
|
||||
* to find which parent has opacity slot defined for it.
|
||||
* this is basically copypaste of getOpacitySlot except it checks if key is
|
||||
* in LAYERS
|
||||
* TODO refactor this
|
||||
*/
|
||||
export const getLayerSlot = (
|
||||
k,
|
||||
inheritance = SLOT_INHERITANCE,
|
||||
getDeps = getDependencies
|
||||
) => {
|
||||
const value = expandSlotValue(inheritance[k])
|
||||
if (LAYERS[k]) return k
|
||||
if (value.layer === null) return
|
||||
if (value.layer) return value.layer
|
||||
const findInheritedLayer = (key, visited = [k]) => {
|
||||
const depSlot = getDeps(key, inheritance)[0]
|
||||
if (depSlot === undefined) return
|
||||
const dependency = inheritance[depSlot]
|
||||
if (dependency === undefined) return
|
||||
if (dependency.layer || dependency === null) {
|
||||
return dependency.layer
|
||||
} else if (dependency.depends) {
|
||||
return findInheritedLayer(dependency, [...visited, depSlot])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (value.depends) {
|
||||
return findInheritedLayer(k)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* topologically sorted SLOT_INHERITANCE
|
||||
*/
|
||||
export const SLOT_ORDERED = topoSort(
|
||||
Object.entries(SLOT_INHERITANCE)
|
||||
.sort(([aK, aV], [bK, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0))
|
||||
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
|
||||
)
|
||||
|
||||
/**
|
||||
* All opacity slots used in color slots, their default values and affected
|
||||
* color slots.
|
||||
*/
|
||||
export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k, v]) => {
|
||||
const opacity = getOpacitySlot(k, SLOT_INHERITANCE, getDependencies)
|
||||
if (opacity) {
|
||||
return {
|
||||
...acc,
|
||||
[opacity]: {
|
||||
defaultValue: DEFAULT_OPACITY[opacity] || 1,
|
||||
affectedSlots: [...((acc[opacity] && acc[opacity].affectedSlots) || []), k]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, {})
|
||||
|
||||
/**
|
||||
* Handle dynamic color
|
||||
*/
|
||||
export const computeDynamicColor = (sourceColor, getColor, mod) => {
|
||||
if (typeof sourceColor !== 'string' || !sourceColor.startsWith('--')) return sourceColor
|
||||
let targetColor = null
|
||||
// Color references other color
|
||||
const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
|
||||
const variableSlot = variable.substring(2)
|
||||
targetColor = getColor(variableSlot)
|
||||
if (modifier) {
|
||||
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
|
||||
}
|
||||
return targetColor
|
||||
}
|
||||
|
||||
/**
|
||||
* THE function you want to use. Takes provided colors and opacities
|
||||
* value and uses inheritance data to figure out color needed for the slot.
|
||||
*/
|
||||
export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ colors, opacity }, key) => {
|
||||
const sourceColor = sourceColors[key]
|
||||
const value = expandSlotValue(SLOT_INHERITANCE[key])
|
||||
const deps = getDependencies(key, SLOT_INHERITANCE)
|
||||
const isTextColor = !!value.textColor
|
||||
const variant = value.variant || value.layer
|
||||
|
||||
let backgroundColor = null
|
||||
|
||||
if (isTextColor) {
|
||||
backgroundColor = alphaBlendLayers(
|
||||
{ ...(colors[deps[0]] || convert(sourceColors[key] || '#FF00FF').rgb) },
|
||||
getLayers(
|
||||
getLayerSlot(key) || 'bg',
|
||||
variant || 'bg',
|
||||
getOpacitySlot(variant),
|
||||
colors,
|
||||
opacity
|
||||
)
|
||||
)
|
||||
} else if (variant && variant !== key) {
|
||||
backgroundColor = colors[variant] || convert(sourceColors[variant]).rgb
|
||||
} else {
|
||||
backgroundColor = colors.bg || convert(sourceColors.bg)
|
||||
}
|
||||
|
||||
const isLightOnDark = relativeLuminance(backgroundColor) < 0.5
|
||||
const mod = isLightOnDark ? 1 : -1
|
||||
|
||||
let outputColor = null
|
||||
if (sourceColor) {
|
||||
// Color is defined in source color
|
||||
let targetColor = sourceColor
|
||||
if (targetColor === 'transparent') {
|
||||
// We take only layers below current one
|
||||
const layers = getLayers(
|
||||
getLayerSlot(key),
|
||||
key,
|
||||
getOpacitySlot(key) || key,
|
||||
colors,
|
||||
opacity
|
||||
).slice(0, -1)
|
||||
targetColor = {
|
||||
...alphaBlendLayers(
|
||||
convert('#FF00FF').rgb,
|
||||
layers
|
||||
),
|
||||
a: 0
|
||||
}
|
||||
} else if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) {
|
||||
targetColor = computeDynamicColor(
|
||||
sourceColor,
|
||||
variableSlot => colors[variableSlot] || sourceColors[variableSlot],
|
||||
mod
|
||||
)
|
||||
} else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) {
|
||||
targetColor = convert(targetColor).rgb
|
||||
}
|
||||
outputColor = { ...targetColor }
|
||||
} else if (value.default) {
|
||||
// same as above except in object form
|
||||
outputColor = convert(value.default).rgb
|
||||
} else {
|
||||
// calculate color
|
||||
const defaultColorFunc = (mod, dep) => ({ ...dep })
|
||||
const colorFunc = value.color || defaultColorFunc
|
||||
|
||||
if (value.textColor) {
|
||||
if (value.textColor === 'bw') {
|
||||
outputColor = contrastRatio(backgroundColor).rgb
|
||||
} else {
|
||||
let color = { ...colors[deps[0]] }
|
||||
if (value.color) {
|
||||
color = colorFunc(mod, ...deps.map((dep) => ({ ...colors[dep] })))
|
||||
}
|
||||
outputColor = getTextColor(
|
||||
backgroundColor,
|
||||
{ ...color },
|
||||
value.textColor === 'preserve'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// background color case
|
||||
outputColor = colorFunc(
|
||||
mod,
|
||||
...deps.map((dep) => ({ ...colors[dep] }))
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!outputColor) {
|
||||
throw new Error('Couldn\'t generate color for ' + key)
|
||||
}
|
||||
const opacitySlot = getOpacitySlot(key)
|
||||
if (opacitySlot && outputColor.a === undefined) {
|
||||
const deps = getDependencies(key, SLOT_INHERITANCE)
|
||||
const dependencySlot = deps && deps[0]
|
||||
if (dependencySlot && sourceColors[dependencySlot] === 'transparent') {
|
||||
outputColor.a = 0
|
||||
} else {
|
||||
outputColor.a = Number(sourceOpacity[opacitySlot]) || OPACITIES[opacitySlot].defaultValue || 1
|
||||
}
|
||||
}
|
||||
if (opacitySlot) {
|
||||
return {
|
||||
colors: { ...colors, [key]: outputColor },
|
||||
opacity: { ...opacity, [opacitySlot]: outputColor.a }
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
colors: { ...colors, [key]: outputColor },
|
||||
opacity
|
||||
}
|
||||
}
|
||||
}, { colors: {}, opacity: {} })
|
||||
Loading…
Add table
Add a link
Reference in a new issue