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:
Henry Jameson 2020-02-11 10:09:54 +02:00
commit 25f785770c
116 changed files with 2929 additions and 3047 deletions

View file

@ -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;

View file

@ -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);
}
}

View 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;
}
}

View file

@ -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('--')
}
}
}

View file

@ -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')

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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 {

View file

@ -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>

View file

@ -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')

View file

@ -1,7 +1,7 @@
<template>
<button
class="btn btn-default follow-button"
:class="{ pressed: isPressed }"
:class="{ toggled: isPressed }"
:disabled="inProgress"
:title="title"
@click="onClick"

View file

@ -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>

View file

@ -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;

View file

@ -68,6 +68,9 @@
a {
color: var(--faintLink);
}
.status-content a {
color: var(--postFaintLink);
}
}
padding: 0;
.media-body {

View file

@ -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'
],

View file

@ -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;

View file

@ -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);
}
}
}
}

View file

@ -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)"

View file

@ -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 {

View file

@ -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)
} : {}
}
}

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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));
}
}
}

View file

@ -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>

View file

@ -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)
}
}
}

View file

@ -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;
}
}

View file

@ -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(&quot;settings.export_theme&quot;)"
:import-label="$t(&quot;settings.import_theme&quot;)"
@ -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"

View file

@ -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 {

View file

@ -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);
}
}
}
}