From e5a34870f0f7154712783fb6d9c20edf4c06ad35 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sat, 28 Dec 2019 15:55:42 +0200
Subject: Accent works

---
 src/components/style_switcher/style_switcher.vue | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index ad032041..8bbdc7b7 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -114,9 +114,18 @@
               :label="$t('settings.text')"
             />
             <ContrastRatio :contrast="previewContrast.bgText" />
+            <ColorInput
+              v-model="accentColorLocal"
+              name="accentColor"
+              :fallback="previewTheme.colors.link"
+              :showOptionalTickbox="typeof linkColorLocal !== 'undefined'"
+              :label="$t('settings.accent')"
+            />
             <ColorInput
               v-model="linkColorLocal"
               name="linkColor"
+              :fallback="previewTheme.colors.accent"
+              :showOptionalTickbox="typeof accentColorLocal !== 'undefined'"
               :label="$t('settings.links')"
             />
             <ContrastRatio :contrast="previewContrast.bgLink" />
@@ -336,7 +345,7 @@
             <ColorInput
               v-model="faintColorLocal"
               name="faintColor"
-              :fallback="previewTheme.colors.faint || 1"
+              :fallback="previewTheme.colors.faint"
               :label="$t('settings.text')"
             />
             <ColorInput
-- 
cgit v1.2.3-70-g09d2


From 1a78461443e4f85461283c1921694a5fb28ea39f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sat, 28 Dec 2019 17:02:34 +0200
Subject: fixed import-export

---
 src/components/export_import/export_import.vue   |  2 +-
 src/components/style_switcher/style_switcher.js  | 34 +++++++++++++++++++-----
 src/components/style_switcher/style_switcher.vue |  4 +--
 src/services/style_setter/style_setter.js        | 27 +++++++++++++++++--
 4 files changed, 56 insertions(+), 11 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue
index 20c6f569..ae00487f 100644
--- a/src/components/export_import/export_import.vue
+++ b/src/components/export_import/export_import.vue
@@ -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')
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 1e512407..b450dc8e 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,6 +1,7 @@
 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 { merge } from 'lodash'
+import { generateCompat, generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.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'
@@ -123,6 +124,15 @@ export default {
     selectedVersion () {
       return Array.isArray(this.selected) ? 1 : 2
     },
+    currentCompat () {
+      return generateCompat({
+        shadows: this.shadowsLocal,
+        fonts: this.fontsLocal,
+        opacity: this.currentOpacity,
+        colors: this.currentColors,
+        radii: this.currentRadii
+      })
+    },
     currentColors () {
       return {
         bg: this.bgColorLocal,
@@ -332,7 +342,7 @@ export default {
 
       return {
         // To separate from other random JSON files and possible future theme formats
-        _pleroma_theme_version: 2, theme
+        _pleroma_theme_version: 2, theme: merge(theme, this.currentCompat)
       }
     }
   },
@@ -364,7 +374,7 @@ export default {
     onImport (parsed) {
       if (parsed._pleroma_theme_version === 1) {
         this.normalizeLocalState(parsed, 1)
-      } else if (parsed._pleroma_theme_version === 2) {
+      } else if (parsed._pleroma_theme_version >= 2) {
         this.normalizeLocalState(parsed.theme, 2)
       }
     },
@@ -414,6 +424,7 @@ export default {
 
     /**
      * This applies stored theme data onto form. Supports three versions of data:
+     * v3 (version = 3) - same as 2 but with some incompatible changes
      * 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)
@@ -421,12 +432,21 @@ export default {
      * @param {Object} input - input data
      * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
      */
-    normalizeLocalState (input, version = 0) {
-      const colors = input.colors || input
+    normalizeLocalState (originalInput, version = 0) {
+      let input
+      if (typeof originalInput.v3compat !== undefined) {
+        version = 3
+        input = merge(originalInput, originalInput.v3compat)
+      } else {
+        input = originalInput
+      }
+
+      const compat = input.v3compat
       const radii = input.radii || input
       const opacity = input.opacity
       const shadows = input.shadows || {}
       const fonts = input.fonts || {}
+      const colors = input.colors || input
 
       if (version === 0) {
         if (input.version) version = input.version
@@ -530,6 +550,7 @@ export default {
     currentColors () {
       try {
         this.previewColors = generateColors({
+          v3compat: this.currentCompat,
           opacity: this.currentOpacity,
           colors: this.currentColors
         })
@@ -542,8 +563,9 @@ export default {
     currentOpacity () {
       try {
         this.previewColors = generateColors({
+          v3compat: this.currentCompat,
           opacity: this.currentOpacity,
-          colors: this.currentColors
+          colors: this.currentColors,
         })
       } catch (e) {
         console.warn(e)
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 8bbdc7b7..2ecd275a 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -2,7 +2,7 @@
   <div class="style-switcher">
     <div class="presets-container">
       <div class="save-load">
-        <export-import
+        <ExportImport
           :export-object="exportedTheme"
           :export-label="$t(&quot;settings.export_theme&quot;)"
           :import-label="$t(&quot;settings.import_theme&quot;)"
@@ -38,7 +38,7 @@
               </label>
             </div>
           </template>
-        </export-import>
+        </ExportImport>
       </div>
       <div class="save-load-options">
         <span class="keep-option">
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 1e4bc59e..19a06587 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -148,6 +148,26 @@ const getCssColor = (input, a) => {
   return rgb2rgba({ ...rgb, a })
 }
 
+// Generates a "patch" for theme to make it compatible with v2
+export const generateCompat = (input) => {
+  const { colors } = input
+  const v3compat = {
+    colors: {}
+  }
+  const v2colorsPatch = {}
+
+  // # Link became optional in v3
+  if (typeof colors.link === 'undefined') {
+    v2colorsPatch.link = colors.accent
+    v3compat.colors.link = null
+  }
+
+  return {
+    v3compat,
+    colors: v2colorsPatch
+  }
+}
+
 const generateColors = (input) => {
   const colors = {}
   const opacity = Object.assign({
@@ -164,7 +184,10 @@ const generateColors = (input) => {
   const inputColors = input.colors || input
 
   const compat = input.v3compat || {}
-  const compatColors = compat.colors || {}
+  const compatColors = Object.entries(compat.colors || {}).reduce((acc, [key, value]) => {
+    const newVal = value === null ? undefined : value
+    return { ...acc, [key]: newVal }
+  }, {})
 
   const col = Object.entries({ ...inputColors, ...compatColors }).reduce((acc, [k, v]) => {
     if (typeof v === 'object') {
@@ -210,7 +233,7 @@ const generateColors = (input) => {
   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.faintLink = col.faintLink || Object.assign({}, col.link || col.accent)
   colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg)
 
   colors.icon = mixrgb(colors.bg, colors.text)
-- 
cgit v1.2.3-70-g09d2


From 6e11924c27288e590de8f5b675fb53704581bcc8 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sat, 28 Dec 2019 18:49:35 +0200
Subject: underlay customization, updated contrast calculations to account for
 alpha blending

---
 src/App.scss                                     |  1 +
 src/components/style_switcher/style_switcher.js  | 47 ++++++++++++++----------
 src/components/style_switcher/style_switcher.vue | 14 +++++++
 src/services/color_convert/color_convert.js      | 23 ++++++++++++
 src/services/style_setter/style_setter.js        | 37 +++++++++++++------
 5 files changed, 91 insertions(+), 31 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index 3b03a761..b6d4943a 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -32,6 +32,7 @@ h4 {
   min-height: 100vh;
   max-width: 980px;
   background-color: rgba(0,0,0,0.15);
+  background-color: var(--underlay, rgba(0,0,0,0.15));
   align-content: flex-start;
 }
 
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index b450dc8e..602a635e 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,4 +1,4 @@
-import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js'
+import { rgb2hex, hex2rgb, getContrastRatio, getContrastRatioLayers, alphaBlend } from '../../services/color_convert/color_convert.js'
 import { set, delete as del } from 'vue'
 import { merge } from 'lodash'
 import { generateCompat, generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js'
@@ -53,6 +53,9 @@ export default {
       bgColorLocal: '',
       bgOpacityLocal: undefined,
 
+      underlayColorLocal: '',
+      underlayOpacityLocal: undefined,
+
       fgColorLocal: '',
       fgTextColorLocal: undefined,
       fgLinkColorLocal: undefined,
@@ -145,6 +148,8 @@ export default {
 
         accent: this.accentColorLocal,
 
+        underlay: this.underlayColorLocal,
+
         panel: this.panelColorLocal,
         panelText: this.panelTextColorLocal,
         panelLink: this.panelLinkColorLocal,
@@ -182,7 +187,8 @@ export default {
         panel: this.panelOpacityLocal,
         topBar: this.topBarOpacityLocal,
         border: this.borderOpacityLocal,
-        faint: this.faintOpacityLocal
+        faint: this.faintOpacityLocal,
+        underlay: this.underlayOpacityLocal
       }
     },
     currentRadii () {
@@ -240,6 +246,7 @@ export default {
 
       const bgs = {
         bg: hex2rgb(colors.bg),
+        underlay: hex2rgb(colors.underlay),
         btn: hex2rgb(colors.btn),
         panel: hex2rgb(colors.panel),
         topBar: hex2rgb(colors.topBar),
@@ -249,29 +256,31 @@ export default {
         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),
+      const bg = [bgs.bg, opacity.bg]
+      const underlay = [bgs.underlay, opacity.underlay]
 
+      const panel = [underlay, bg]
+
+      const ratios = {
+        bgText: getContrastRatioLayers(fgs.text, panel, fgs.text),
+        bgLink: getContrastRatioLayers(fgs.link, panel, fgs.link),
+        bgRed: getContrastRatioLayers(fgs.red, panel, fgs.red),
+        bgGreen: getContrastRatioLayers(fgs.green, panel, fgs.green),
+        bgBlue: getContrastRatioLayers(fgs.blue, panel, fgs.blue),
+        bgOrange: getContrastRatioLayers(fgs.orange, panel, fgs.orange),
+
+        // TODO what's this?
         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),
+        panelText: getContrastRatioLayers(fgs.text, [...panel, [bgs.panel, opacity.panel]], fgs.panelText),
+        panelLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.panel, opacity.panel]], fgs.panelLink),
 
-        btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText),
+        btnText: getContrastRatioLayers(fgs.text, [...panel, [bgs.btn, opacity.btn]], fgs.btnText),
 
-        inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText),
+        inputText: getContrastRatioLayers(fgs.text, [...panel, [bgs.input, opacity.input]], fgs.inputText),
 
-        topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText),
-        topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink)
+        topBarText: getContrastRatioLayers(fgs.text, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarText),
+        topBarLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarLink)
       }
 
       return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 2ecd275a..38ca2017 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -366,6 +366,20 @@
               :fallback="previewTheme.opacity.faint || 0.5"
             />
           </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='#000000'
+            />
+            <OpacityInput
+              v-model="underlayOpacityLocal"
+              name="underlayOpacity"
+              :fallback="previewTheme.opacity.underlay || 0.15"
+            />
+          </div>
         </div>
 
         <div
diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js
index d1b17c61..757a36bd 100644
--- a/src/services/color_convert/color_convert.js
+++ b/src/services/color_convert/color_convert.js
@@ -1,5 +1,8 @@
 import { map } from 'lodash'
 
+// useful for visualizing color when debugging
+export const consoleColor = (color) => console.log('%c##########', 'background: ' + color + '; color: ' + color)
+
 const rgb2hex = (r, g, b) => {
   if (r === null || typeof r === 'undefined') {
     return undefined
@@ -78,6 +81,16 @@ 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
@@ -97,6 +110,16 @@ const alphaBlend = (fg, fga, bg) => {
   }, {})
 }
 
+/**
+ * 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)
+
 const invert = (rgb) => {
   return 'rgb'.split('').reduce((acc, c) => {
     acc[c] = 255 - rgb[c]
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 19a06587..8740fc55 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,6 +1,6 @@
 import { times } from 'lodash'
 import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
-import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } from '../color_convert/color_convert.js'
+import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend, alphaBlendLayers } from '../color_convert/color_convert.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
@@ -64,8 +64,10 @@ const getTextColor = function (bg, text, preserve) {
     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
@@ -173,7 +175,8 @@ const generateColors = (input) => {
   const opacity = Object.assign({
     alert: 0.5,
     input: 0.5,
-    faint: 0.5
+    faint: 0.5,
+    underlay: 0.15
   }, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => {
     if (typeof v !== 'undefined') {
       acc[k] = v
@@ -210,28 +213,37 @@ const generateColors = (input) => {
   colors.faint = col.faint || Object.assign({}, col.text)
 
   colors.bg = col.bg
-  colors.lightBg = col.lightBg || brightness(5, colors.bg).rgb
+  colors.lightBg = col.lightBg || brightness(5 * mod, colors.bg).rgb
+
+  const underlay = [col.underlay, opacity.underlay]
+  const fg = [col.fg, opacity.fg]
+  const bg = [col.bg, opacity.bg]
 
   colors.fg = col.fg
-  colors.fgText = col.fgText || getTextColor(colors.fg, colors.text)
-  colors.fgLink = col.fgLink || getTextColor(colors.fg, colors.link, true)
+  colors.fgText = col.fgText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, fg]), colors.text)
+  colors.fgLink = col.fgLink || getTextColor(alphaBlendLayers(colors.link, [underlay, bg, fg]), colors.link, true)
+  colors.underlay = col.underlay || hex2rgb('#000000')
 
   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)
+  const btn = [colors.btn, opacity.btn || 1]
+  colors.btnText = col.btnText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, fg, btn]), colors.fgText)
 
   colors.input = col.input || Object.assign({}, col.fg)
-  colors.inputText = col.inputText || getTextColor(colors.input, colors.lightText)
+  const inputCol = [colors.input, opacity.input]
+  colors.inputText = col.inputText || getTextColor(alphaBlendLayers(colors.lightText, [underlay, bg, fg, inputCol]), 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)
+  const panel = [colors.panel, opacity.panel]
+  colors.panelText = col.panelText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, panel]), colors.fgText)
+  colors.panelLink = col.panelLink || getTextColor(alphaBlendLayers(colors.fgLink, [underlay, bg, panel]), colors.fgLink)
+  colors.panelFaint = col.panelFaint || getTextColor(alphaBlendLayers(colors.faint, [underlay, bg, 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)
+  const topBar = [colors.topBar, opacity.topBar]
+  colors.topBarText = col.topBarText || getTextColor(alphaBlendLayers(colors.fgText, [topBar]), colors.fgText)
+  colors.topBarLink = col.topBarLink || getTextColor(alphaBlendLayers(colors.fgLink, [topBar]), colors.fgLink)
 
   colors.faintLink = col.faintLink || Object.assign({}, col.link || col.accent)
   colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg)
@@ -255,6 +267,7 @@ const generateColors = (input) => {
   colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb
 
   Object.entries(opacity).forEach(([ k, v ]) => {
+    console.log(k)
     if (typeof v === 'undefined') return
     if (k === 'alert') {
       colors.alertError.a = v
-- 
cgit v1.2.3-70-g09d2


From 4bb1c98e0f28bcf1d0dff2d90d01013cd5487522 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 2 Jan 2020 20:36:10 +0200
Subject: Replaced `v3compat` with `source` to reduce code complexity. Made
 more slots customizable. `theme` now contains a snapshot of theme generated
 for better compatiblity and future-proofing

---
 src/components/style_switcher/style_switcher.js  |  68 ++++---
 src/components/style_switcher/style_switcher.vue |  14 +-
 src/services/style_setter/style_setter.js        |  42 ++---
 static/themes/breezy-dark.json                   | 224 ++++++++++++++++++++++-
 static/themes/breezy-light.json                  | 220 +++++++++++++++++++++-
 5 files changed, 488 insertions(+), 80 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 9fe1bf59..f751260a 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,7 +1,15 @@
 import { rgb2hex, hex2rgb, getContrastRatio, getContrastRatioLayers, alphaBlend } from '../../services/color_convert/color_convert.js'
 import { set, delete as del } from 'vue'
 import { merge } from 'lodash'
-import { generateCompat, generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js'
+import {
+  generateColors,
+  generateShadows,
+  generateRadii,
+  generateFonts,
+  composePreset,
+  getThemes,
+  CURRENT_VERSION
+} from '../../services/style_setter/style_setter.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'
@@ -135,15 +143,6 @@ export default {
     selectedVersion () {
       return Array.isArray(this.selected) ? 1 : 2
     },
-    currentCompat () {
-      return generateCompat({
-        shadows: this.shadowsLocal,
-        fonts: this.fontsLocal,
-        opacity: this.currentOpacity,
-        colors: this.currentColors,
-        radii: this.currentRadii
-      })
-    },
     currentColors () {
       return {
         bg: this.bgColorLocal,
@@ -339,27 +338,32 @@ 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 = this.previewTheme
+
+      console.log(source)
       return {
-        // To separate from other random JSON files and possible future theme formats
-        _pleroma_theme_version: 2, theme: merge(theme, this.currentCompat)
+        // To separate from other random JSON files and possible future source formats
+        _pleroma_theme_version: 2, theme, source
       }
     }
   },
@@ -392,7 +396,7 @@ export default {
       if (parsed._pleroma_theme_version === 1) {
         this.normalizeLocalState(parsed, 1)
       } else if (parsed._pleroma_theme_version >= 2) {
-        this.normalizeLocalState(parsed.theme, 2)
+        this.normalizeLocalState(parsed.theme, 2, parsed.source)
       }
     },
     importValidator (parsed) {
@@ -402,7 +406,7 @@ export default {
     clearAll () {
       const state = this.$store.getters.mergedConfig.customTheme
       const version = state.colors ? 2 : 'l1'
-      this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version)
+      this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version, this.$store.getters.mergedConfig.customThemeSource)
     },
 
     // Clears all the extra stuff when loading V1 theme
@@ -441,24 +445,30 @@ export default {
 
     /**
      * This applies stored theme data onto form. Supports three versions of data:
-     * v3 (version = 3) - same as 2 but with some incompatible changes
+     * 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 (originalInput, version = 0) {
+    normalizeLocalState (theme, version = 0, source, forceSource = false) {
       let input
-      if (typeof originalInput.v3compat !== 'undefined') {
-        version = 3
-        input = merge(originalInput, originalInput.v3compat)
+      if (typeof source !== 'undefined') {
+        if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
+          input = source
+          version = source.themeEngineVersion
+        } else {
+          input = theme
+        }
       } else {
-        input = originalInput
+        input = theme
       }
 
-      const compat = input.v3compat
       const radii = input.radii || input
       const opacity = input.opacity
       const shadows = input.shadows || {}
@@ -615,7 +625,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)
       }
     }
   }
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 38ca2017..2eadbe25 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -106,7 +106,7 @@
             <OpacityInput
               v-model="bgOpacityLocal"
               name="bgOpacity"
-              :fallback="previewTheme.opacity.bg || 1"
+              :fallback="previewTheme.opacity.bg"
             />
             <ColorInput
               v-model="textColorLocal"
@@ -238,7 +238,7 @@
             <OpacityInput
               v-model="panelOpacityLocal"
               name="panelOpacity"
-              :fallback="previewTheme.opacity.panel || 1"
+              :fallback="previewTheme.opacity.panel"
             />
             <ColorInput
               v-model="panelTextColorLocal"
@@ -295,7 +295,7 @@
             <OpacityInput
               v-model="inputOpacityLocal"
               name="inputOpacity"
-              :fallback="previewTheme.opacity.input || 1"
+              :fallback="previewTheme.opacity.input"
             />
             <ColorInput
               v-model="inputTextColorLocal"
@@ -316,7 +316,7 @@
             <OpacityInput
               v-model="btnOpacityLocal"
               name="btnOpacity"
-              :fallback="previewTheme.opacity.btn || 1"
+              :fallback="previewTheme.opacity.btn"
             />
             <ColorInput
               v-model="btnTextColorLocal"
@@ -337,7 +337,7 @@
             <OpacityInput
               v-model="borderOpacityLocal"
               name="borderOpacity"
-              :fallback="previewTheme.opacity.border || 1"
+              :fallback="previewTheme.opacity.border"
             />
           </div>
           <div class="color-item">
@@ -363,7 +363,7 @@
             <OpacityInput
               v-model="faintOpacityLocal"
               name="faintOpacity"
-              :fallback="previewTheme.opacity.faint || 0.5"
+              :fallback="previewTheme.opacity.faint"
             />
           </div>
           <div class="color-item">
@@ -377,7 +377,7 @@
             <OpacityInput
               v-model="underlayOpacityLocal"
               name="underlayOpacity"
-              :fallback="previewTheme.opacity.underlay || 0.15"
+              :fallback="previewTheme.opacity.underlay"
             />
           </div>
         </div>
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index df22f94f..e8a64517 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -2,6 +2,8 @@ import { times } from 'lodash'
 import { brightness, invertLightness, convert, contrastRatio } from 'chromatism'
 import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend, alphaBlendLayers } from '../color_convert/color_convert.js'
 
+export const CURRENT_VERSION = 3
+
 // 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.
@@ -150,29 +152,13 @@ const getCssColor = (input, a) => {
   return rgb2rgba({ ...rgb, a })
 }
 
-// Generates a "patch" for theme to make it compatible with v2
-export const generateCompat = (input) => {
-  const { colors } = input
-  const v3compat = {
-    colors: {}
-  }
-  const v2colorsPatch = {}
-
-  // # Link became optional in v3
-  if (typeof colors.link === 'undefined') {
-    v2colorsPatch.link = colors.accent
-    v3compat.colors.link = null
-  }
-
-  return {
-    v3compat,
-    colors: v2colorsPatch
-  }
-}
-
 const generateColors = (themeData) => {
   const colors = {}
   const rawOpacity = Object.assign({
+    panel: 1,
+    btn: 1,
+    border: 1,
+    bg: 1,
     alert: 0.5,
     input: 0.5,
     faint: 0.5,
@@ -207,6 +193,7 @@ const generateColors = (themeData) => {
     } else {
       let value = v
       if (v === 'transparent') {
+        // TODO: hack to keep rest of the code from complaining
         value = '#FF00FF'
       }
       acc[k] = hex2rgb(value)
@@ -221,7 +208,7 @@ const generateColors = (themeData) => {
   const isLightOnDark = convert(colors.bg).hsl.l < convert(colors.text).hsl.l
   const mod = isLightOnDark ? 1 : -1
 
-  colors.lightText = brightness(20 * mod, colors.text).rgb
+  colors.lightText = col.lightText || brightness(20 * mod, colors.text).rgb
 
   colors.accent = col.accent || col.link
   colors.link = col.link || col.accent
@@ -231,7 +218,8 @@ const generateColors = (themeData) => {
   colors.lightBg = col.lightBg || brightness(5 * mod, colors.bg).rgb
 
   const underlay = [colors.underlay, opacity.underlay]
-  const fg = [col.fg, opacity.fg]
+  // Technically, foreground can't be transparent (descendants can) but let's keep it just in case
+  const fg = [col.fg, opacity.fg || 1]
   const bg = [col.bg, opacity.bg]
 
   colors.fg = col.fg
@@ -271,16 +259,16 @@ const generateColors = (themeData) => {
 
   colors.alertError = col.alertError || Object.assign({}, colors.cRed)
   const alertError = [colors.alertError, opacity.alert]
-  colors.alertErrorText = getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertError]), colors.text)
-  colors.alertErrorPanelText = getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertError]), colors.panelText)
+  colors.alertErrorText = col.alertErrorText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertError]), colors.text)
+  colors.alertErrorPanelText = col.alertErrorPanelText || getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertError]), colors.panelText)
 
   colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange)
   const alertWarning = [colors.alertWarning, opacity.alert]
-  colors.alertWarningText = getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertWarning]), colors.text)
-  colors.alertWarningPanelText = getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertWarning]), colors.panelText)
+  colors.alertWarningText = col.alertWarningText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertWarning]), colors.text)
+  colors.alertWarningPanelText = col.alertWarningPanelText || getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertWarning]), colors.panelText)
 
   colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed)
-  colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb
+  colors.badgeNotificationText = colors.badgeNotificationText || contrastRatio(colors.badgeNotification).rgb
 
   Object.entries(opacity).forEach(([ k, v ]) => {
     console.log(k)
diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json
index 0ed55184..d447005f 100644
--- a/static/themes/breezy-dark.json
+++ b/static/themes/breezy-dark.json
@@ -2,6 +2,218 @@
   "_pleroma_theme_version": 2,
   "name": "Breezy Dark (beta)",
   "theme": {
+    "shadows": {
+      "panel": [
+        {
+          "x": "1",
+          "y": "2",
+          "blur": "6",
+          "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": [
+        {
+          "x": 0,
+          "y": "40",
+          "blur": "40",
+          "spread": "-40",
+          "inset": true,
+          "color": "#ffffff",
+          "alpha": "0.1"
+        }
+      ],
+      "button": [
+        {
+          "x": 0,
+          "y": "0",
+          "blur": "0",
+          "spread": "1",
+          "color": "#ffffff",
+          "alpha": "0.15",
+          "inset": true
+        },
+        {
+          "x": "1",
+          "y": "1",
+          "blur": "1",
+          "spread": 0,
+          "color": "#000000",
+          "alpha": "0.3",
+          "inset": false
+        }
+      ],
+      "buttonHover": [
+        {
+          "x": 0,
+          "y": "0",
+          "blur": 0,
+          "spread": "1",
+          "color": "--accent",
+          "alpha": "0.3",
+          "inset": true
+        },
+        {
+          "x": "1",
+          "y": "1",
+          "blur": "1",
+          "spread": 0,
+          "color": "#000000",
+          "alpha": "0.3",
+          "inset": false
+        }
+      ],
+      "buttonPressed": [
+        {
+          "x": 0,
+          "y": 0,
+          "blur": "0",
+          "spread": "50",
+          "color": "--faint",
+          "alpha": 1,
+          "inset": true
+        },
+        {
+          "x": 0,
+          "y": "0",
+          "blur": 0,
+          "spread": "1",
+          "color": "#ffffff",
+          "alpha": 0.2,
+          "inset": true
+        },
+        {
+          "x": "1",
+          "y": "1",
+          "blur": 0,
+          "spread": 0,
+          "color": "#000000",
+          "alpha": "0.3",
+          "inset": false
+        }
+      ],
+      "input": [
+        {
+          "x": 0,
+          "y": "0",
+          "blur": 0,
+          "spread": "1",
+          "color": "#FFFFFF",
+          "alpha": "0.2",
+          "inset": true
+        }
+      ]
+    },
+    "colors": {
+      "bg": "#31363b",
+      "underlay": "#000000",
+      "text": "#eff0f1",
+      "lightText": "#ffffff",
+      "accent": "#3daee9",
+      "link": "#3daee9",
+      "faint": "#eff0f1",
+      "lightBg": "#3d4349",
+      "fg": "#31363b",
+      "fgText": "#eff0f1",
+      "fgLink": "#3daee9",
+      "border": "#4c545b",
+      "btn": "#31363b",
+      "btnText": "#eff0f1",
+      "input": "#232629",
+      "inputText": "#ffffff",
+      "panel": "#ff00ff",
+      "panelText": "#eff0f1",
+      "panelLink": "#3daee9",
+      "panelFaint": "#eff0f1",
+      "topBar": "#31363b",
+      "topBarText": "#eff0f1",
+      "topBarLink": "#eff0f1",
+      "faintLink": "#3daee9",
+      "linkBg": "#366681",
+      "icon": "#909396",
+      "cBlue": "#3daee9",
+      "cRed": "#da4453",
+      "cGreen": "#27ae60",
+      "cOrange": "#f67400",
+      "alertError": "#da4453",
+      "alertErrorText": "#eff0f1",
+      "alertErrorPanelText": "#eff0f1",
+      "alertWarning": "#f67400",
+      "alertWarningText": "#eff0f1",
+      "alertWarningPanelText": "#eff0f1",
+      "badgeNotification": "#da4453",
+      "badgeNotificationText": "#ffffff"
+    },
+    "opacity": {
+      "panel": 0,
+      "btn": 1,
+      "border": 1,
+      "bg": 1,
+      "alert": 0.5,
+      "input": 0.5,
+      "faint": 0.5,
+      "underlay": 0.15
+    },
+    "radii": {
+      "btn": "2",
+      "input": "2",
+      "checkbox": "1",
+      "panel": "2",
+      "avatar": "2",
+      "avatarAlt": "2",
+      "tooltip": "2",
+      "attachment": "2"
+    },
+    "fonts": {
+      "interface": {
+        "family": "sans-serif"
+      },
+      "input": {
+        "family": "inherit"
+      },
+      "post": {
+        "family": "inherit"
+      },
+      "postCode": {
+        "family": "monospace"
+      }
+    }
+  },
+  "source": {
+    "themeEngineVersion": 3,
+    "fonts": {},
     "shadows": {
       "panel": [
         {
@@ -105,21 +317,13 @@
         }
       ]
     },
-    "fonts": {},
-    "opacity": {
-      "input": "1"
-    },
-    "v3compat": {
-      "colors": {
-        "panel": "transparent"
-      }
-    },
+    "opacity": {},
     "colors": {
       "bg": "#31363b",
       "text": "#eff0f1",
       "link": "#3daee9",
       "fg": "#31363b",
-      "panel": "#31363b",
+      "panel": "transparent",
       "input": "#232629",
       "topBarLink": "#eff0f1",
       "btn": "#31363b",
diff --git a/static/themes/breezy-light.json b/static/themes/breezy-light.json
index 5db185dd..243b8593 100644
--- a/static/themes/breezy-light.json
+++ b/static/themes/breezy-light.json
@@ -2,6 +2,218 @@
   "_pleroma_theme_version": 2,
   "name": "Breezy Light (beta)",
   "theme": {
+    "shadows": {
+      "panel": [
+        {
+          "x": "1",
+          "y": "2",
+          "blur": "6",
+          "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": [
+        {
+          "x": 0,
+          "y": "40",
+          "blur": "40",
+          "spread": "-40",
+          "inset": true,
+          "color": "#ffffff",
+          "alpha": "0.1"
+        }
+      ],
+      "button": [
+        {
+          "x": 0,
+          "y": "0",
+          "blur": "0",
+          "spread": "1",
+          "color": "#000000",
+          "alpha": "0.3",
+          "inset": true
+        },
+        {
+          "x": "1",
+          "y": "1",
+          "blur": "1",
+          "spread": 0,
+          "color": "#000000",
+          "alpha": "0.3",
+          "inset": false
+        }
+      ],
+      "buttonHover": [
+        {
+          "x": 0,
+          "y": "0",
+          "blur": 0,
+          "spread": "1",
+          "color": "--accent",
+          "alpha": "0.3",
+          "inset": true
+        },
+        {
+          "x": "1",
+          "y": "1",
+          "blur": "1",
+          "spread": 0,
+          "color": "#000000",
+          "alpha": "0.3",
+          "inset": false
+        }
+      ],
+      "buttonPressed": [
+        {
+          "x": 0,
+          "y": 0,
+          "blur": "0",
+          "spread": "50",
+          "color": "--faint",
+          "alpha": 1,
+          "inset": true
+        },
+        {
+          "x": 0,
+          "y": "0",
+          "blur": 0,
+          "spread": "1",
+          "color": "#ffffff",
+          "alpha": 0.2,
+          "inset": true
+        },
+        {
+          "x": "1",
+          "y": "1",
+          "blur": 0,
+          "spread": 0,
+          "color": "#000000",
+          "alpha": "0.3",
+          "inset": false
+        }
+      ],
+      "input": [
+        {
+          "x": 0,
+          "y": "0",
+          "blur": 0,
+          "spread": "1",
+          "color": "#000000",
+          "alpha": "0.2",
+          "inset": true
+        }
+      ]
+    },
+    "colors": {
+      "bg": "#eff0f1",
+      "underlay": "#000000",
+      "text": "#232627",
+      "lightText": "#000000",
+      "accent": "#2980b9",
+      "link": "#2980b9",
+      "faint": "#232627",
+      "lightBg": "#e2e4e6",
+      "fg": "#bcc2c7",
+      "fgText": "#232627",
+      "fgLink": "#2980b9",
+      "border": "#b7bdc3",
+      "btn": "#eff0f1",
+      "btnText": "#232627",
+      "input": "#fcfcfc",
+      "inputText": "#000000",
+      "panel": "#475057",
+      "panelText": "#fcfcfc",
+      "panelLink": "#ffffff",
+      "panelFaint": "#d8dbdc",
+      "topBar": "#475057",
+      "topBarText": "#d8dbdc",
+      "topBarLink": "#eff0f1",
+      "faintLink": "#2980b9",
+      "linkBg": "#a0c4db",
+      "icon": "#898b8c",
+      "cBlue": "#2980b9",
+      "cRed": "#da4453",
+      "cGreen": "#27ae60",
+      "cOrange": "#f67400",
+      "alertError": "#da4453",
+      "alertErrorText": "#232627",
+      "alertErrorPanelText": "#fcfcfc",
+      "alertWarning": "#f67400",
+      "alertWarningText": "#232627",
+      "alertWarningPanelText": "#fcfcfc",
+      "badgeNotification": "#da4453",
+      "badgeNotificationText": "#ffffff"
+    },
+    "opacity": {
+      "panel": 1,
+      "btn": 1,
+      "border": 1,
+      "bg": 1,
+      "alert": 0.5,
+      "input": "1",
+      "faint": 0.5,
+      "underlay": 0.15
+    },
+    "radii": {
+      "btn": "2",
+      "input": "2",
+      "checkbox": "1",
+      "panel": "2",
+      "avatar": "2",
+      "avatarAlt": "2",
+      "tooltip": "2",
+      "attachment": "2"
+    },
+    "fonts": {
+      "interface": {
+        "family": "sans-serif"
+      },
+      "input": {
+        "family": "inherit"
+      },
+      "post": {
+        "family": "inherit"
+      },
+      "postCode": {
+        "family": "monospace"
+      }
+    }
+  },
+  "source": {
+    "themeEngineVersion": 3,
+    "fonts": {},
     "shadows": {
       "panel": [
         {
@@ -105,20 +317,14 @@
         }
       ]
     },
-    "fonts": {},
     "opacity": {
       "input": "1"
     },
-    "v3compat": {
-      "colors": {
-        "panel": "transparent"
-      }
-    },
     "colors": {
       "bg": "#eff0f1",
       "text": "#232627",
-      "link": "#2980b9",
       "fg": "#bcc2c7",
+      "accent": "#2980b9",
       "panel": "#475057",
       "panelText": "#fcfcfc",
       "input": "#fcfcfc",
-- 
cgit v1.2.3-70-g09d2


From adbab6ad2a413357b24edbd5b9beec3baae695fa Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 2 Jan 2020 21:36:01 +0200
Subject: added optional checkbox for opacity, similar to color input

---
 src/components/opacity_input/opacity_input.vue   | 37 +++++++++++++++++++++---
 src/components/style_switcher/style_switcher.vue |  9 ++++--
 2 files changed, 40 insertions(+), 6 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
index c677f18c..f7b10d30 100644
--- a/src/components/opacity_input/opacity_input.vue
+++ b/src/components/opacity_input/opacity_input.vue
@@ -18,7 +18,7 @@
       @input="$emit('input', !present ? fallback : undefined)"
     >
     <label
-      v-if="typeof fallback !== 'undefined'"
+      v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
       class="opt-l"
       :for="name + '-o'"
     />
@@ -38,9 +38,38 @@
 
 <script>
 export default {
-  props: [
-    'name', 'value', 'fallback', 'disabled'
-  ],
+  props: {
+    // Name of opacity, used for identifying
+    name: {
+      required: true,
+      type: String
+    },
+    // Opacity value, should be required but vue cannot tell the difference
+    // between "property missing" and "property set to undefined"
+    value: {
+      required: false,
+      type: Number,
+      default: undefined
+    },
+    // Opacity fallback to use when value is not defeind
+    fallback: {
+      required: false,
+      type: Number,
+      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'
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 2eadbe25..f5b12e7e 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -118,15 +118,15 @@
               v-model="accentColorLocal"
               name="accentColor"
               :fallback="previewTheme.colors.link"
-              :showOptionalTickbox="typeof linkColorLocal !== 'undefined'"
               :label="$t('settings.accent')"
+              :showOptionalTickbox="typeof linkColorLocal !== 'undefined'"
             />
             <ColorInput
               v-model="linkColorLocal"
               name="linkColor"
               :fallback="previewTheme.colors.accent"
-              :showOptionalTickbox="typeof accentColorLocal !== 'undefined'"
               :label="$t('settings.links')"
+              :showOptionalTickbox="typeof accentColorLocal !== 'undefined'"
             />
             <ContrastRatio :contrast="previewContrast.bgLink" />
           </div>
@@ -239,6 +239,7 @@
               v-model="panelOpacityLocal"
               name="panelOpacity"
               :fallback="previewTheme.opacity.panel"
+              :showOptionalTickbox="panelColorLocal !== 'transparent'"
             />
             <ColorInput
               v-model="panelTextColorLocal"
@@ -296,6 +297,7 @@
               v-model="inputOpacityLocal"
               name="inputOpacity"
               :fallback="previewTheme.opacity.input"
+              :showOptionalTickbox="inputColorLocal !== 'transparent'"
             />
             <ColorInput
               v-model="inputTextColorLocal"
@@ -317,6 +319,7 @@
               v-model="btnOpacityLocal"
               name="btnOpacity"
               :fallback="previewTheme.opacity.btn"
+              :showOptionalTickbox="btnColorLocal !== 'transparent'"
             />
             <ColorInput
               v-model="btnTextColorLocal"
@@ -338,6 +341,7 @@
               v-model="borderOpacityLocal"
               name="borderOpacity"
               :fallback="previewTheme.opacity.border"
+              :showOptionalTickbox="borderColorLocal !== 'transparent'"
             />
           </div>
           <div class="color-item">
@@ -378,6 +382,7 @@
               v-model="underlayOpacityLocal"
               name="underlayOpacity"
               :fallback="previewTheme.opacity.underlay"
+              :showOptionalTickbox="underlayOpacityLocal !== 'transparent'"
             />
           </div>
         </div>
-- 
cgit v1.2.3-70-g09d2


From c7e9f21da0eb035b73a165e16f77a9cd18036305 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 2 Jan 2020 22:44:54 +0200
Subject: color input and opacity input now use Checkbox component. Cleanup.

---
 src/components/color_input/color_input.vue        | 20 ++++-----
 src/components/opacity_input/opacity_input.vue    | 51 +++++------------------
 src/components/range_input/range_input.vue        |  2 +-
 src/components/style_switcher/style_switcher.scss | 12 +++---
 src/components/style_switcher/style_switcher.vue  | 10 ++---
 5 files changed, 30 insertions(+), 65 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index fa26d079..aa289288 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -9,18 +9,12 @@
     >
       {{ label }}
     </label>
-    <input
-      v-if="typeof fallback !== 'undefined'"
-      :id="name + '-o'"
-      class="opt exlcude-disabled"
-      type="checkbox"
-      :checked="present"
-      @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
-    >
-    <label
+    <Checkbox
       v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
-      class="opt-l"
-      :for="name + '-o'"
+      :checked="present"
+      :disabled="disabled"
+      @change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
+      class="opt"
     />
     <input
       :id="name"
@@ -42,6 +36,7 @@
 </template>
 
 <script>
+import Checkbox from '../checkbox/checkbox.vue'
 export default {
   props: {
     // Name of color, used for identifying
@@ -80,6 +75,9 @@ export default {
       default: true
     }
   },
+  components: {
+    Checkbox
+  },
   computed: {
     present () {
       return typeof this.value !== 'undefined'
diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
index f7b10d30..cfda9926 100644
--- a/src/components/opacity_input/opacity_input.vue
+++ b/src/components/opacity_input/opacity_input.vue
@@ -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' && showOptionalTickbox"
-      class="opt-l"
-      :for="name + '-o'"
+      :disabled="disabled"
+      @change="$emit('input', !present ? fallback : undefined)"
+      class="opt"
     />
     <input
       :id="name"
@@ -37,38 +31,13 @@
 </template>
 
 <script>
+import Checkbox from '../checkbox/checkbox.vue'
 export default {
-  props: {
-    // Name of opacity, used for identifying
-    name: {
-      required: true,
-      type: String
-    },
-    // Opacity value, should be required but vue cannot tell the difference
-    // between "property missing" and "property set to undefined"
-    value: {
-      required: false,
-      type: Number,
-      default: undefined
-    },
-    // Opacity fallback to use when value is not defeind
-    fallback: {
-      required: false,
-      type: Number,
-      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
-    }
+  props: [
+    'name', 'value', 'fallback', 'disabled'
+  ],
+  components: {
+    Checkbox
   },
   computed: {
     present () {
diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue
index aaa2ed26..5857a5c1 100644
--- a/src/components/range_input/range_input.vue
+++ b/src/components/range_input/range_input.vue
@@ -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)"
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss
index 135c113a..7291a884 100644
--- a/src/components/style_switcher/style_switcher.scss
+++ b/src/components/style_switcher/style_switcher.scss
@@ -15,12 +15,14 @@
 
     &.disabled {
       input, select {
-        &:not(.exclude-disabled) {
-          opacity: .5
-        }
+        opacity: .5
       }
     }
 
+    .opt {
+      margin: .5em;
+    }
+
     input, select {
       min-width: 3em;
       margin: 0;
@@ -44,10 +46,6 @@
         min-width: 3em;
       }
 
-      &[type=checkbox] + label {
-        margin: 6px 0;
-      }
-
       &:not([type=number]):not([type=text]) {
         align-self: flex-start;
       }
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index f5b12e7e..f993e070 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -239,7 +239,7 @@
               v-model="panelOpacityLocal"
               name="panelOpacity"
               :fallback="previewTheme.opacity.panel"
-              :showOptionalTickbox="panelColorLocal !== 'transparent'"
+              :disabled="panelColorLocal === 'transparent'"
             />
             <ColorInput
               v-model="panelTextColorLocal"
@@ -297,7 +297,7 @@
               v-model="inputOpacityLocal"
               name="inputOpacity"
               :fallback="previewTheme.opacity.input"
-              :showOptionalTickbox="inputColorLocal !== 'transparent'"
+              :disabled="inputColorLocal === 'transparent'"
             />
             <ColorInput
               v-model="inputTextColorLocal"
@@ -319,7 +319,7 @@
               v-model="btnOpacityLocal"
               name="btnOpacity"
               :fallback="previewTheme.opacity.btn"
-              :showOptionalTickbox="btnColorLocal !== 'transparent'"
+              :disabled="btnColorLocal === 'transparent'"
             />
             <ColorInput
               v-model="btnTextColorLocal"
@@ -341,7 +341,7 @@
               v-model="borderOpacityLocal"
               name="borderOpacity"
               :fallback="previewTheme.opacity.border"
-              :showOptionalTickbox="borderColorLocal !== 'transparent'"
+              :disabled="borderColorLocal === 'transparent'"
             />
           </div>
           <div class="color-item">
@@ -382,7 +382,7 @@
               v-model="underlayOpacityLocal"
               name="underlayOpacity"
               :fallback="previewTheme.opacity.underlay"
-              :showOptionalTickbox="underlayOpacityLocal !== 'transparent'"
+              :disabled="underlayOpacityLocal === 'transparent'"
             />
           </div>
         </div>
-- 
cgit v1.2.3-70-g09d2


From 4418baf62aa7a8bef05568e7590b9b4303c29dc7 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 12 Jan 2020 04:16:04 +0200
Subject: fix

---
 src/components/style_switcher/style_switcher.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index f993e070..82b85bd5 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -27,8 +27,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 }}
-- 
cgit v1.2.3-70-g09d2


From 88f83fc9fa3652efdbe6aa622d3e0089883e8057 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 12 Jan 2020 17:46:07 +0200
Subject: overhaul how style-switcher makes state, removed tons of copy-paste

---
 src/components/style_switcher/style_switcher.js  | 214 ++++++-----------------
 src/components/style_switcher/style_switcher.vue |  12 ++
 src/services/style_setter/style_setter.js        |  15 +-
 src/services/theme_data/theme_data.service.js    |  15 +-
 4 files changed, 85 insertions(+), 171 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 4749d3f7..9c6f3266 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -14,7 +14,12 @@ import {
   composePreset,
   getThemes
 } from '../../services/style_setter/style_setter.js'
-import { CURRENT_VERSION } from '../../services/theme_data/theme_data.service.js'
+import {
+  CURRENT_VERSION,
+  SLOT_INHERITANCE,
+  DEFAULT_OPACITY,
+  getLayers
+} 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'
@@ -67,55 +72,13 @@ export default {
       keepRoundness: false,
       keepFonts: false,
 
-      textColorLocal: '',
-      accentColorLocal: undefined,
-      linkColorLocal: undefined,
-
-      bgColorLocal: '',
-      bgOpacityLocal: undefined,
-
-      underlayColorLocal: '',
-      underlayOpacityLocal: 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,
+      ...Object.keys(SLOT_INHERITANCE)
+        .map(key => [key, ''])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
 
-      borderColorLocal: undefined,
-      borderOpacityLocal: undefined,
-
-      faintColorLocal: undefined,
-      faintOpacityLocal: undefined,
-      faintLinkColorLocal: undefined,
-
-      cRedColorLocal: '',
-      cBlueColorLocal: '',
-      cGreenColorLocal: '',
-      cOrangeColorLocal: '',
+      ...Object.keys(DEFAULT_OPACITY)
+        .map(key => [key, undefined])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
 
       shadowSelected: undefined,
       shadowsLocal: {},
@@ -149,59 +112,14 @@ export default {
       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,
-
-        accent: this.accentColorLocal,
-
-        underlay: this.underlayColorLocal,
-
-        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,
-        underlay: this.underlayOpacityLocal
-      }
+      return Object.keys(DEFAULT_OPACITY)
+        .map(key => [key, this[key + 'OpacityLocal']])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {})
     },
     currentRadii () {
       return {
@@ -237,63 +155,45 @@ export default {
         laa: ratio >= 3,
         laaa: ratio >= 4.5
       })
-
-      // fgsfds :DDDD
-      const fgs = {
-        text: colorConvert(colors.text),
-        panelText: colorConvert(colors.panelText),
-        panelLink: colorConvert(colors.panelLink),
-        btnText: colorConvert(colors.btnText),
-        topBarText: colorConvert(colors.topBarText),
-        inputText: colorConvert(colors.inputText),
-
-        link: colorConvert(colors.link),
-        topBarLink: colorConvert(colors.topBarLink),
-
-        red: colorConvert(colors.cRed),
-        green: colorConvert(colors.cGreen),
-        blue: colorConvert(colors.cBlue),
-        orange: colorConvert(colors.cOrange)
-      }
-
-      const bgs = {
-        bg: colorConvert(colors.bg),
-        underlay: colorConvert(colors.underlay),
-        btn: colorConvert(colors.btn),
-        panel: colorConvert(colors.panel),
-        topBar: colorConvert(colors.topBar),
-        input: colorConvert(colors.input),
-        alertError: colorConvert(colors.alertError),
-        alertWarning: colorConvert(colors.alertWarning),
-        badgeNotification: colorConvert(colors.badgeNotification)
-      }
-
-      const bg = [bgs.bg, opacity.bg]
-      const underlay = [bgs.underlay || colorConvert('#000000'), opacity.underlay]
-
-      const panel = [underlay, bg]
-
-      const ratios = {
-        bgText: getContrastRatioLayers(fgs.text, panel, fgs.text),
-        bgLink: getContrastRatioLayers(fgs.link, panel, fgs.link),
-        bgRed: getContrastRatioLayers(fgs.red, panel, fgs.red),
-        bgGreen: getContrastRatioLayers(fgs.green, panel, fgs.green),
-        bgBlue: getContrastRatioLayers(fgs.blue, panel, fgs.blue),
-        bgOrange: getContrastRatioLayers(fgs.orange, panel, fgs.orange),
-
-        // TODO what's this?
-        tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text),
-
-        panelText: getContrastRatioLayers(fgs.text, [...panel, [bgs.panel, opacity.panel]], fgs.panelText),
-        panelLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.panel, opacity.panel]], fgs.panelLink),
-
-        btnText: getContrastRatioLayers(fgs.text, [...panel, [bgs.btn, opacity.btn]], fgs.btnText),
-
-        inputText: getContrastRatioLayers(fgs.text, [...panel, [bgs.input, opacity.input]], fgs.inputText),
-
-        topBarText: getContrastRatioLayers(fgs.text, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarText),
-        topBarLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarLink)
-      }
+      const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
+
+      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 textColors = [
+          key,
+          ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
+        ]
+
+        const layers = getLayers(
+          layer,
+          variant || layer,
+          colorsConverted,
+          opacity
+        )
+
+        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 }, {})
     },
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 82b85bd5..b059eb8a 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -209,6 +209,12 @@
               :label="$t('settings.style.advanced_colors.alert_error')"
               :fallback="previewTheme.colors.alertError"
             />
+            <ColorInput
+              v-model="alertErrorTextColorLocal"
+              name="alertErrorText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.alertErrorText"
+            />
             <ContrastRatio :contrast="previewContrast.alertError" />
             <ColorInput
               v-model="alertWarningColorLocal"
@@ -217,6 +223,12 @@
               :fallback="previewTheme.colors.alertWarning"
             />
             <ContrastRatio :contrast="previewContrast.alertWarning" />
+            <ColorInput
+              v-model="alertWarningTextColorLocal"
+              name="alertWarningText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.alertWarningText"
+            />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 516fb5f6..e11516c0 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,7 +1,7 @@
 import { times } from 'lodash'
 import { convert } from 'chromatism'
 import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js'
-import { getColors } from '../theme_data/theme_data.service.js'
+import { getColors, DEFAULT_OPACITY } 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
@@ -115,18 +115,7 @@ const getCssShadowFilter = (input) => {
 }
 
 export const generateColors = (themeData) => {
-  const rawOpacity = Object.assign({
-    panel: 1,
-    btn: 1,
-    border: 1,
-    bg: 1,
-    badge: 1,
-    text: 1,
-    alert: 0.5,
-    input: 0.5,
-    faint: 0.5,
-    underlay: 0.15
-  }, Object.entries(themeData.opacity || {}).reduce((acc, [k, v]) => {
+  const rawOpacity = Object.assign({ ...DEFAULT_OPACITY }, Object.entries(themeData.opacity || {}).reduce((acc, [k, v]) => {
     if (typeof v !== 'undefined') {
       acc[k] = v
     }
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index 1117ab05..297d0223 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -23,6 +23,19 @@ export const LAYERS = {
   alertPanel: 'panel'
 }
 
+export const DEFAULT_OPACITY = {
+  panel: 1,
+  btn: 1,
+  border: 1,
+  bg: 1,
+  badge: 1,
+  text: 1,
+  alert: 0.5,
+  input: 0.5,
+  faint: 0.5,
+  underlay: 0.15
+}
+
 export const SLOT_INHERITANCE = {
   bg: null,
   fg: null,
@@ -289,7 +302,7 @@ export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.redu
       if (value.textColor === 'bw') {
         return {
           ...acc,
-          [key]: contrastRatio(bg)
+          [key]: contrastRatio(bg).rgb
         }
       } else {
         return {
-- 
cgit v1.2.3-70-g09d2


From 3492d7f81ec3f99c4f15f9ab75f658b3d1db7799 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 12 Jan 2020 17:59:41 +0200
Subject: eslint

---
 src/components/color_input/color_input.vue       | 8 ++++----
 src/components/opacity_input/opacity_input.vue   | 8 ++++----
 src/components/settings/settings.vue             | 4 ++--
 src/components/style_switcher/style_switcher.vue | 6 +++---
 4 files changed, 13 insertions(+), 13 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index 7fe04433..e54409fe 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -13,8 +13,8 @@
       v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
       :checked="present"
       :disabled="disabled"
-      @change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
       class="opt"
+      @change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
     />
     <div class="input color-input-field">
       <input
@@ -46,6 +46,9 @@
 import Checkbox from '../checkbox/checkbox.vue'
 import { hex2rgb } from '../../services/color_convert/color_convert.js'
 export default {
+  components: {
+    Checkbox
+  },
   props: {
     // Name of color, used for identifying
     name: {
@@ -83,9 +86,6 @@ export default {
       default: true
     }
   },
-  components: {
-    Checkbox
-  },
   computed: {
     present () {
       return typeof this.value !== 'undefined'
diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
index cfda9926..3cc3942b 100644
--- a/src/components/opacity_input/opacity_input.vue
+++ b/src/components/opacity_input/opacity_input.vue
@@ -13,8 +13,8 @@
       v-if="typeof fallback !== 'undefined'"
       :checked="present"
       :disabled="disabled"
-      @change="$emit('input', !present ? fallback : undefined)"
       class="opt"
+      @change="$emit('input', !present ? fallback : undefined)"
     />
     <input
       :id="name"
@@ -33,12 +33,12 @@
 <script>
 import Checkbox from '../checkbox/checkbox.vue'
 export default {
-  props: [
-    'name', 'value', 'fallback', 'disabled'
-  ],
   components: {
     Checkbox
   },
+  props: [
+    'name', 'value', 'fallback', 'disabled'
+  ],
   computed: {
     present () {
       return typeof this.value !== 'undefined'
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index b40c85dd..e118dbcc 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -76,9 +76,9 @@
                 <li>
                   <Checkbox v-model="useStreamingApi">
                     {{ $t('settings.useStreamingApi') }}
-                    <br/>
+                    <br>
                     <small>
-                    {{ $t('settings.useStreamingApiWarning') }}
+                      {{ $t('settings.useStreamingApiWarning') }}
                     </small>
                   </Checkbox>
                 </li>
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index b059eb8a..ff6a8264 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -119,14 +119,14 @@
               name="accentColor"
               :fallback="previewTheme.colors.link"
               :label="$t('settings.accent')"
-              :showOptionalTickbox="typeof linkColorLocal !== 'undefined'"
+              :show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
             />
             <ColorInput
               v-model="linkColorLocal"
               name="linkColor"
               :fallback="previewTheme.colors.accent"
               :label="$t('settings.links')"
-              :showOptionalTickbox="typeof accentColorLocal !== 'undefined'"
+              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
             />
             <ContrastRatio :contrast="previewContrast.bgLink" />
           </div>
@@ -388,7 +388,7 @@
               v-model="underlayColorLocal"
               name="underlay"
               :label="$t('settings.style.advanced_colors.underlay')"
-              fallback='#000000'
+              fallback="#000000"
             />
             <OpacityInput
               v-model="underlayOpacityLocal"
-- 
cgit v1.2.3-70-g09d2


From 40ba3e225209539639b65c2f636fa1b5af9861ff Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 12 Jan 2020 23:41:11 +0200
Subject: added more slots to UI

---
 src/components/poll/poll.vue                     |  4 ++-
 src/components/style_switcher/style_switcher.vue | 37 ++++++++++++++++++++++--
 src/i18n/en.json                                 |  4 ++-
 src/services/theme_data/theme_data.service.js    | 15 +++++++---
 4 files changed, 51 insertions(+), 9 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
index db8e33b3..56e91cca 100644
--- a/src/components/poll/poll.vue
+++ b/src/components/poll/poll.vue
@@ -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;
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index ff6a8264..e0894b6d 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -215,20 +215,20 @@
               :label="$t('settings.text')"
               :fallback="previewTheme.colors.alertErrorText"
             />
-            <ContrastRatio :contrast="previewContrast.alertError" />
+            <ContrastRatio :contrast="previewContrast.alertErrorText" large="1"/>
             <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="1"/>
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
@@ -238,6 +238,13 @@
               :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="1" />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
@@ -388,7 +395,7 @@
               v-model="underlayColorLocal"
               name="underlay"
               :label="$t('settings.style.advanced_colors.underlay')"
-              fallback="#000000"
+              :fallback="previewTheme.colors.underlay"
             />
             <OpacityInput
               v-model="underlayOpacityLocal"
@@ -397,6 +404,30 @@
               :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="poll"
+              :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="poll"
+              :label="$t('settings.style.advanced_colors.icons')"
+              :fallback="previewTheme.colors.icon"
+            />
+          </div>
         </div>
 
         <div
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d97b4909..bd3d7413 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -431,7 +431,9 @@
         "buttons": "Buttons",
         "inputs": "Input fields",
         "faint_text": "Faded text",
-        "underlay": "Underlay"
+        "underlay": "Underlay",
+        "poll": "Poll graph",
+        "icons": "Icons"
       },
       "radii": {
         "_tab_label": "Roundness"
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index e2e9331c..21bab1a2 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -20,7 +20,8 @@ export const LAYERS = {
   inputPanel: 'panel',
   inputTopBar: 'topBar',
   alert: 'bg',
-  alertPanel: 'panel'
+  alertPanel: 'panel',
+  poll: 'bg'
 }
 
 export const DEFAULT_OPACITY = {
@@ -33,7 +34,8 @@ export const DEFAULT_OPACITY = {
   alert: 0.5,
   input: 0.5,
   faint: 0.5,
-  underlay: 0.15
+  underlay: 0.15,
+  poll: 1
 }
 
 export const SLOT_INHERITANCE = {
@@ -65,9 +67,14 @@ export const SLOT_INHERITANCE = {
     color: (mod, fg) => brightness(2 * mod, fg).rgb
   },
 
-  linkBg: {
+  poll: {
     depends: ['accent', 'bg'],
-    color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg).rgb
+    color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg)
+  },
+  pollText: {
+    depends: ['text'],
+    layer: 'poll',
+    textColor: true
   },
 
   icon: {
-- 
cgit v1.2.3-70-g09d2


From 8f63bbb64fa2860e73d8562de9d6c62629a8668f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jan 2020 00:33:04 +0200
Subject: poll slot renamed, lightBg customization implemented

---
 src/App.scss                                       |  4 ++++
 src/components/autosuggest/autosuggest.vue         |  4 ++--
 src/components/dialog_modal/dialog_modal.vue       | 12 +++++-----
 src/components/emoji_input/emoji_input.vue         |  4 ++++
 src/components/nav_panel/nav_panel.vue             | 10 ++++++++
 src/components/selectable_list/selectable_list.vue |  4 ++++
 src/components/side_drawer/side_drawer.vue         |  5 ++++
 src/components/status/status.vue                   |  7 ++++--
 src/components/style_switcher/style_switcher.vue   | 27 ++++++++++++++++++++--
 src/services/style_setter/style_setter.js          |  4 +++-
 src/services/theme_data/theme_data.service.js      | 26 +++++++++++++++++++++
 11 files changed, 94 insertions(+), 13 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index 7c9c91af..7da3688a 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -495,6 +495,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;
diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue
index 1f86e996..f283ab82 100644
--- a/src/components/autosuggest/autosuggest.vue
+++ b/src/components/autosuggest/autosuggest.vue
@@ -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;
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
index 55d7a7d2..3241ce3e 100644
--- a/src/components/dialog_modal/dialog_modal.vue
+++ b/src/components/dialog_modal/dialog_modal.vue
@@ -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;
 
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index a7215670..dcf51ff9 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -158,6 +158,10 @@
       &.highlighted {
         background-color: $fallback--fg;
         background-color: var(--lightBg, $fallback--fg);
+        color: var(--lightBgText, $fallback--text);
+        --faint: var(--lightBgFaintText, $fallback--faint);
+        --faintLink: var(--lightBgFaintLink, $fallback--faint);
+        --icon: var(--lightBgIcon, $fallback--icon);
       }
     }
   }
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 034259d9..0886bf8c 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -101,12 +101,22 @@
   &:hover {
     background-color: $fallback--lightBg;
     background-color: var(--lightBg, $fallback--lightBg);
+    color: $fallback--link;
+    color: var(--lightBgText, $fallback--link);
+    --faint: var(--lightBgFaintText, $fallback--faint);
+    --faintLink: var(--lightBgFaintLink, $fallback--faint);
+    --icon: var(--lightBgIcon, $fallback--icon);
   }
 
   &.router-link-active {
     font-weight: bolder;
     background-color: $fallback--lightBg;
     background-color: var(--lightBg, $fallback--lightBg);
+    color: $fallback--text;
+    color: var(--lightBgText, $fallback--text);
+    --faint: var(--lightBgFaintText, $fallback--faint);
+    --faintLink: var(--lightBgFaintLink, $fallback--faint);
+    --icon: var(--lightBgIcon, $fallback--icon);
 
     &:hover {
       text-decoration: underline;
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index d9ec7ece..416c9b6a 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -69,6 +69,10 @@
   &-item-selected-inner {
     background-color: $fallback--lightBg;
     background-color: var(--lightBg, $fallback--lightBg);
+    color: var(--lightBgText, $fallback--text);
+    --faint: var(--lightBgFaintText, $fallback--faint);
+    --faintLink: var(--lightBgFaintLink, $fallback--faint);
+    --icon: var(--lightBgIcon, $fallback--icon);
   }
 
   &-header {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 3fba9058..6d75221f 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -290,6 +290,11 @@
     &:hover {
       background-color: $fallback--lightBg;
       background-color: var(--lightBg, $fallback--lightBg);
+      color: $fallback--text;
+      color: var(--lightBgText, $fallback--text);
+      --faint: var(--lightBgFaintText, $fallback--faint);
+      --faintLink: var(--lightBgFaintLink, $fallback--faint);
+      --icon: var(--lightBgIcon, $fallback--icon);
     }
   }
 }
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index d291e762..72e9b25a 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -446,6 +446,11 @@ $status-margin: 0.75em;
   &_focused {
     background-color: $fallback--lightBg;
     background-color: var(--lightBg, $fallback--lightBg);
+    color: $fallback--text;
+    color: var(--lightBgText, $fallback--text);
+    --faint: var(--lightBgFaintText, $fallback--faint);
+    --faintLink: var(--lightBgFaintLink, $fallback--faint);
+    --icon: var(--lightBgIcon, $fallback--icon);
   }
 
   .timeline & {
@@ -573,8 +578,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 {
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index e0894b6d..1381f1fb 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -414,7 +414,7 @@
             />
             <ColorInput
               v-model="pollTextColorLocal"
-              name="poll"
+              name="pollText"
               :label="$t('settings.text')"
               :fallback="previewTheme.colors.pollText"
             />
@@ -423,11 +423,34 @@
             <h4>{{ $t('settings.style.advanced_colors.icons') }}</h4>
             <ColorInput
               v-model="iconColorLocal"
-              name="poll"
+              name="icon"
               :label="$t('settings.style.advanced_colors.icons')"
               :fallback="previewTheme.colors.icon"
             />
           </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.lightBg') }}</h4>
+            <ColorInput
+              v-model="lightBgColorLocal"
+              name="lightBg"
+              :label="$t('settings.style.advanced_colors.lightBg')"
+              :fallback="previewTheme.colors.lightBg"
+            />
+            <ColorInput
+              v-model="lightBgTextColorLocal"
+              name="lightBgText"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.lightBgText"
+            />
+            <ContrastRatio :contrast="previewContrast.lightBgText" />
+            <ColorInput
+              v-model="lightBgLinkColorLocal"
+              name="lightBgLink"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.lightBgLink"
+            />
+            <ContrastRatio :contrast="previewContrast.lightBgLink" />
+          </div>
         </div>
 
         <div
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index e11516c0..204c162b 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -163,8 +163,10 @@ export const generateColors = (themeData) => {
       return
     }
     if (k === 'faint') {
-      colors[k + 'Link'].a = v
+      colors['faintLink'].a = v
       colors['panelFaint'].a = v
+      colors['lightBgFaintText'].a = v
+      colors['lightBgFaintLink'].a = v
     }
     if (k === 'bg') {
       colors['lightBg'].a = v
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index 21bab1a2..808f67d5 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -12,6 +12,7 @@ export const LAYERS = {
   badge: null, //  no transparency support
   fg: null,
   bg: 'underlay',
+  lightBg: 'bg',
   panel: 'bg',
   btn: 'bg',
   btnPanel: 'panel',
@@ -57,6 +58,31 @@ export const SLOT_INHERITANCE = {
     depends: ['bg'],
     color: (mod, bg) => brightness(5 * mod, bg).rgb
   },
+  lightBgFaintText: {
+    depends: ['faint'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  lightBgFaintLink: {
+    depends: ['faintLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  lightBgText: {
+    depends: ['text'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  lightBgLink: {
+    depends: ['link'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  lightBgIcon: {
+    depends: ['lightBg', 'lightBgText'],
+    color: (mod, bg, text) => mixrgb(bg, text)
+  },
+
   lightText: {
     depends: ['text'],
     color: (mod, text) => brightness(20 * mod, text).rgb
-- 
cgit v1.2.3-70-g09d2


From fa2fcc882737c8c057fd12d4cc1a85d67f3b672e Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jan 2020 01:54:56 +0200
Subject: better fallbacks in UI

---
 src/components/style_switcher/style_switcher.vue | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 1381f1fb..c1ec7c9a 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -251,7 +251,7 @@
             <ColorInput
               v-model="panelColorLocal"
               name="panelColor"
-              :fallback="fgColorLocal"
+              :fallback="previewTheme.colors.panel"
               :label="$t('settings.background')"
             />
             <OpacityInput
@@ -286,7 +286,7 @@
             <ColorInput
               v-model="topBarColorLocal"
               name="topBarColor"
-              :fallback="fgColorLocal"
+              :fallback="previewTheme.colors.topBar"
               :label="$t('settings.background')"
             />
             <ColorInput
@@ -309,7 +309,7 @@
             <ColorInput
               v-model="inputColorLocal"
               name="inputColor"
-              :fallback="fgColorLocal"
+              :fallback="previewTheme.colors.input"
               :label="$t('settings.background')"
             />
             <OpacityInput
@@ -331,7 +331,7 @@
             <ColorInput
               v-model="btnColorLocal"
               name="btnColor"
-              :fallback="fgColorLocal"
+              :fallback="previewTheme.colors.btn"
               :label="$t('settings.background')"
             />
             <OpacityInput
-- 
cgit v1.2.3-70-g09d2


From 8d2f2866f6d32b4ada155d76e07910b92c218146 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jan 2020 01:56:29 +0200
Subject: computed colors support

---
 src/App.scss                                     |  4 ++
 src/components/style_switcher/style_switcher.js  |  6 ++-
 src/components/style_switcher/style_switcher.vue | 16 +++++-
 src/i18n/en.json                                 |  4 +-
 src/services/style_setter/style_setter.js        |  6 ++-
 src/services/theme_data/theme_data.service.js    | 69 +++++++++++++++++++++---
 6 files changed, 92 insertions(+), 13 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index 7da3688a..ef139e88 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -99,6 +99,10 @@ 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 {
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 49b34405..b84d2489 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -42,7 +42,7 @@ const v1OnlyNames = [
 ].map(_ => _ + 'ColorLocal')
 
 const colorConvert = (color) => {
-  if (color === 'transparent') {
+  if (color.startsWith('--') || color === 'transparent') {
     return color
   } else {
     return hex2rgb(color)
@@ -409,7 +409,9 @@ 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
         })
       }
 
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index c1ec7c9a..f2fdfea2 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -347,6 +347,20 @@
               :label="$t('settings.text')"
             />
             <ContrastRatio :contrast="previewContrast.btnText" />
+            <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.btnText" />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
@@ -433,7 +447,7 @@
             <ColorInput
               v-model="lightBgColorLocal"
               name="lightBg"
-              :label="$t('settings.style.advanced_colors.lightBg')"
+              :label="$t('settings.background')"
               :fallback="previewTheme.colors.lightBg"
             />
             <ColorInput
diff --git a/src/i18n/en.json b/src/i18n/en.json
index bd3d7413..544bc2ba 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -433,7 +433,9 @@
         "faint_text": "Faded text",
         "underlay": "Underlay",
         "poll": "Poll graph",
-        "icons": "Icons"
+        "icons": "Icons",
+        "lightBg": "Highlighted elements",
+        "pressed": "Pressed"
       },
       "radii": {
         "_tab_label": "Roundness"
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 204c162b..61612cf4 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -144,7 +144,11 @@ export const generateColors = (themeData) => {
         // TODO: hack to keep rest of the code from complaining
         value = '#FF00FF'
       }
-      acc[k] = hex2rgb(value)
+      if (!value || value.startsWith('--')) {
+        acc[k] = value
+      } else {
+        acc[k] = hex2rgb(value)
+      }
     }
     return acc
   }, {})
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index 808f67d5..221c3b48 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -40,12 +40,27 @@ export const DEFAULT_OPACITY = {
 }
 
 export const SLOT_INHERITANCE = {
-  bg: null,
-  fg: null,
-  text: null,
+  bg: {
+    depends: [],
+    priority: 1
+  },
+  fg: {
+    depends: [],
+    priority: 1
+  },
+  text: {
+    depends: [],
+    priority: 1
+  },
   underlay: '#000000',
-  link: '--accent',
-  accent: '--link',
+  link: {
+    depends: ['accent'],
+    priority: 1
+  },
+  accent: {
+    depends: ['link'],
+    priority: 1
+  },
   faint: '--text',
   faintLink: '--link',
 
@@ -170,6 +185,26 @@ export const SLOT_INHERITANCE = {
     textColor: true
   },
 
+  btnPressed: '--btn',
+  btnPressedText: {
+    depends: ['btnText'],
+    layer: 'btn',
+    variant: 'btnPressed',
+    textColor: true
+  },
+  btnPressedPanelText: {
+    depends: ['btnPanelText'],
+    layer: 'btnPanel',
+    variant: 'btnPressed',
+    textColor: true
+  },
+  btnPressedTopBarText: {
+    depends: ['btnTopBarText'],
+    layer: 'btnTopBar',
+    variant: 'btnPressed',
+    textColor: true
+  },
+
   // Input fields
   input: '--fg',
   inputText: {
@@ -308,12 +343,30 @@ export const topoSort = (
   return output
 }
 
-export const SLOT_ORDERED = topoSort(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 }), {})
+)
+
+console.log(SLOT_ORDERED)
 
 export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.reduce((acc, key) => {
   const value = SLOT_INHERITANCE[key]
-  if (sourceColors[key]) {
-    return { ...acc, [key]: { ...sourceColors[key] } }
+  const sourceColor = sourceColors[key]
+  if (sourceColor) {
+    let targetColor = sourceColor
+    if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) {
+      const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
+      const variableSlot = variable.substring(2)
+      targetColor = acc[variableSlot] || sourceColors[variableSlot]
+      if (modifier) {
+        console.log(targetColor, acc, variableSlot)
+        targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
+      }
+      console.log(targetColor, acc, variableSlot)
+    }
+    return { ...acc, [key]: { ...targetColor } }
   } else if (typeof value === 'string' && value.startsWith('#')) {
     return { ...acc, [key]: convert(value).rgb }
   } else {
-- 
cgit v1.2.3-70-g09d2


From 8a7f3fc16a511e6c5917c0e25cba1f6163659264 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jan 2020 20:40:16 +0200
Subject: selectedPost and selectedMenu support

---
 src/components/emoji_input/emoji_input.vue         | 10 ++---
 src/components/nav_panel/nav_panel.vue             | 20 ++++-----
 src/components/selectable_list/selectable_list.vue | 10 ++---
 src/components/side_drawer/side_drawer.vue         | 10 ++---
 src/components/status/status.vue                   | 10 ++---
 src/components/style_switcher/style_switcher.vue   | 46 +++++++++++++++++++
 src/services/theme_data/theme_data.service.js      | 52 ++++++++++++++++++++++
 7 files changed, 128 insertions(+), 30 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index dcf51ff9..46ed6f25 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -157,11 +157,11 @@
 
       &.highlighted {
         background-color: $fallback--fg;
-        background-color: var(--lightBg, $fallback--fg);
-        color: var(--lightBgText, $fallback--text);
-        --faint: var(--lightBgFaintText, $fallback--faint);
-        --faintLink: var(--lightBgFaintLink, $fallback--faint);
-        --icon: var(--lightBgIcon, $fallback--icon);
+        background-color: var(--selectedMenu, $fallback--fg);
+        color: var(--selectedMenuText, $fallback--text);
+        --faint: var(--selectedMenuFaintText, $fallback--faint);
+        --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+        --icon: var(--selectedMenuIcon, $fallback--icon);
       }
     }
   }
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 0886bf8c..afc611ea 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -100,23 +100,23 @@
 
   &:hover {
     background-color: $fallback--lightBg;
-    background-color: var(--lightBg, $fallback--lightBg);
+    background-color: var(--selectedMenu, $fallback--lightBg);
     color: $fallback--link;
-    color: var(--lightBgText, $fallback--link);
-    --faint: var(--lightBgFaintText, $fallback--faint);
-    --faintLink: var(--lightBgFaintLink, $fallback--faint);
-    --icon: var(--lightBgIcon, $fallback--icon);
+    color: var(--selectedMenuText, $fallback--link);
+    --faint: var(--selectedMenuFaintText, $fallback--faint);
+    --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --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(--lightBgText, $fallback--text);
-    --faint: var(--lightBgFaintText, $fallback--faint);
-    --faintLink: var(--lightBgFaintLink, $fallback--faint);
-    --icon: var(--lightBgIcon, $fallback--icon);
+    color: var(--selectedMenuText, $fallback--text);
+    --faint: var(--selectedMenuFaintText, $fallback--faint);
+    --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --icon: var(--selectedMenuIcon, $fallback--icon);
 
     &:hover {
       text-decoration: underline;
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index 416c9b6a..2d1e44a3 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -68,11 +68,11 @@
 
   &-item-selected-inner {
     background-color: $fallback--lightBg;
-    background-color: var(--lightBg, $fallback--lightBg);
-    color: var(--lightBgText, $fallback--text);
-    --faint: var(--lightBgFaintText, $fallback--faint);
-    --faintLink: var(--lightBgFaintLink, $fallback--faint);
-    --icon: var(--lightBgIcon, $fallback--icon);
+    background-color: var(--selectedMenu, $fallback--lightBg);
+    color: var(--selectedMenuText, $fallback--text);
+    --faint: var(--selectedMenuFaintText, $fallback--faint);
+    --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+    --icon: var(--selectedMenuIcon, $fallback--icon);
   }
 
   &-header {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 6d75221f..3188fd59 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -289,12 +289,12 @@
 
     &:hover {
       background-color: $fallback--lightBg;
-      background-color: var(--lightBg, $fallback--lightBg);
+      background-color: var(--selectedMenu, $fallback--lightBg);
       color: $fallback--text;
-      color: var(--lightBgText, $fallback--text);
-      --faint: var(--lightBgFaintText, $fallback--faint);
-      --faintLink: var(--lightBgFaintLink, $fallback--faint);
-      --icon: var(--lightBgIcon, $fallback--icon);
+      color: var(--selectedMenuText, $fallback--text);
+      --faint: var(--selectedMenuFaintText, $fallback--faint);
+      --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+      --icon: var(--selectedMenuIcon, $fallback--icon);
     }
   }
 }
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 72e9b25a..38d091ed 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -445,12 +445,12 @@ $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(--lightBgText, $fallback--text);
-    --faint: var(--lightBgFaintText, $fallback--faint);
-    --faintLink: var(--lightBgFaintLink, $fallback--faint);
-    --icon: var(--lightBgIcon, $fallback--icon);
+    color: var(--selectedPostText, $fallback--text);
+    --faint: var(--selectedPostFaintText, $fallback--faint);
+    --faintLink: var(--selectedPostFaintLink, $fallback--faint);
+    --icon: var(--selectedPostIcon, $fallback--icon);
   }
 
   .timeline & {
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index f2fdfea2..613abd1f 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -465,6 +465,52 @@
             />
             <ContrastRatio :contrast="previewContrast.lightBgLink" />
           </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
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index 221c3b48..dd5f8fd0 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -98,6 +98,58 @@ export const SLOT_INHERITANCE = {
     color: (mod, bg, text) => mixrgb(bg, text)
   },
 
+  selectedPost: '--lightBg',
+  selectedPostFaintText: {
+    depends: ['lightBgFaintText'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  selectedPostFaintLink: {
+    depends: ['lightBgFaintLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  selectedPostText: {
+    depends: ['lightBgText'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  selectedPostLink: {
+    depends: ['lightBgLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  selectedPostIcon: {
+    depends: ['selectedPost', 'selectedPostText'],
+    color: (mod, bg, text) => mixrgb(bg, text)
+  },
+
+  selectedMenu: '--lightBg',
+  selectedMenuFaintText: {
+    depends: ['lightBgFaintText'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  selectedMenuFaintLink: {
+    depends: ['lightBgFaintLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  selectedMenuText: {
+    depends: ['lightBgText'],
+    layer: 'lightBg',
+    textColor: true
+  },
+  selectedMenuLink: {
+    depends: ['lightBgLink'],
+    layer: 'lightBg',
+    textColor: 'preserve'
+  },
+  selectedMenuIcon: {
+    depends: ['selectedMenu', 'selectedMenuText'],
+    color: (mod, bg, text) => mixrgb(bg, text)
+  },
+
   lightText: {
     depends: ['text'],
     color: (mod, text) => brightness(20 * mod, text).rgb
-- 
cgit v1.2.3-70-g09d2


From 4b8e0f0afa2c7f1d0f4f0c2f39f289b75b3cae08 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jan 2020 21:30:55 +0200
Subject: buttonPressed & buttonDisabled slots

---
 src/App.scss                                     | 44 ++++++++++++++++++++++-
 src/components/style_switcher/style_switcher.vue | 16 ++++++++-
 src/services/theme_data/theme_data.service.js    | 46 +++++++++++++++++++++---
 3 files changed, 100 insertions(+), 6 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index ef139e88..7f404bc6 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -107,7 +107,10 @@ button {
 
   &: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 {
@@ -365,6 +368,26 @@ 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(--btnTopBarPressed, $fallback--fg);
+      color: $fallback--text;
+      color: var(--btnTopBarPressedText, $fallback--text);
+    }
+
+    &:disabled {
+      color: $fallback--text;
+      color: var(--btnTopBarDisabledText, $fallback--text);
+    }
+  }
+
+
   .logo {
     display: flex;
     position: absolute;
@@ -525,6 +548,25 @@ 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(--btnPanelPressed, $fallback--fg);
+      color: $fallback--text;
+      color: var(--btnPanelPressedText, $fallback--text);
+    }
+
+    &:disabled {
+      color: $fallback--text;
+      color: var(--btnPanelDisabledText, $fallback--text);
+    }
+  }
+
   a {
     color: $fallback--link;
     color: var(--panelLink, $fallback--link)
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 613abd1f..3921c953 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -360,7 +360,21 @@
               :fallback="previewTheme.colors.btnPressedText"
               :label="$t('settings.text')"
             />
-            <ContrastRatio :contrast="previewContrast.btnText" />
+            <ContrastRatio :contrast="previewContrast.btnPressedText" />
+            <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')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnDisabledText" />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index dd5f8fd0..c5dd8047 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -36,7 +36,8 @@ export const DEFAULT_OPACITY = {
   input: 0.5,
   faint: 0.5,
   underlay: 0.15,
-  poll: 1
+  poll: 1,
+  topBar: 1
 }
 
 export const SLOT_INHERITANCE = {
@@ -222,7 +223,8 @@ export const SLOT_INHERITANCE = {
   btn: '--fg',
   btnText: {
     depends: ['fgText'],
-    layer: 'btn'
+    layer: 'btn',
+    textColor: true
   },
   btnPanelText: {
     depends: ['panelText'],
@@ -257,6 +259,32 @@ export const SLOT_INHERITANCE = {
     textColor: true
   },
 
+  btnDisabled: {
+    depends: ['btn', 'bg'],
+    color: (mod, btn, bg) => alphaBlend(btn, 0.5, bg)
+  },
+  btnDisabledText: {
+    depends: ['btnText'],
+    layer: 'btn',
+    variant: 'btnDisabled',
+    textColor: true,
+    color: (mod, text) => brightness(mod * -60, text).rgb
+  },
+  btnDisabledPanelText: {
+    depends: ['btnPanelText'],
+    layer: 'btnPanel',
+    variant: 'btnDisabled',
+    textColor: true,
+    color: (mod, text) => brightness(mod * -60, text).rgb
+  },
+  btnDisabledTopBarText: {
+    depends: ['btnTopBarText'],
+    layer: 'btnTopBar',
+    variant: 'btnDisabled',
+    textColor: true,
+    color: (mod, text) => brightness(mod * -60, text).rgb
+  },
+
   // Input fields
   input: '--fg',
   inputText: {
@@ -329,7 +357,10 @@ export const getLayers = (layer, variant = layer, colors, opacity) => {
     currentLayer === layer
       ? colors[variant]
       : colors[currentLayer],
-    opacity[currentLayer]
+    // TODO: Remove this hack when opacities/layers system is improved
+    currentLayer.startsWith('btn')
+      ? opacity.btn
+      : opacity[currentLayer]
   ]))
 }
 
@@ -443,11 +474,18 @@ export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.redu
           [key]: contrastRatio(bg).rgb
         }
       } else {
+        let color = { ...acc[deps[0]] }
+        if (value.color) {
+          const isLightOnDark = convert(bg).hsl.l < convert(color).hsl.l
+          const mod = isLightOnDark ? 1 : -1
+          color = value.color(mod, ...deps.map((dep) => ({ ...acc[dep] })))
+        }
+
         return {
           ...acc,
           [key]: getTextColor(
             bg,
-            { ...acc[deps[0]] },
+            { ...color },
             value.textColor === 'preserve'
           )
         }
-- 
cgit v1.2.3-70-g09d2


From 9f7af191e8a77a0c5620c8698cc7b26dde02868b Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 13 Jan 2020 22:19:19 +0200
Subject: tabs & toggled (ex pressed) buttons

---
 src/App.scss                                       | 22 +++++++++---
 src/components/follow_button/follow_button.vue     |  2 +-
 .../moderation_tools/moderation_tools.vue          |  2 +-
 src/components/style_switcher/style_switcher.vue   | 23 +++++++++++++
 src/components/tab_switcher/tab_switcher.scss      |  7 ++++
 src/components/user_card/user_card.vue             | 10 ++----
 src/services/theme_data/theme_data.service.js      | 39 ++++++++++++++++++++++
 7 files changed, 90 insertions(+), 15 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index 7f404bc6..70983fe2 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -113,11 +113,11 @@ button {
     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)
   }
 
   &.danger {
@@ -385,6 +385,13 @@ i[class*=icon-] {
       color: $fallback--text;
       color: var(--btnTopBarDisabledText, $fallback--text);
     }
+
+    &.toggled {
+      color: $fallback--text;
+      color: var(--btnTopBarToggledText, $fallback--text);
+      background-color: $fallback--fg;
+      background-color: var(--btnTopBarToggled, $fallback--fg)
+    }
   }
 
 
@@ -565,6 +572,11 @@ main-router {
       color: $fallback--text;
       color: var(--btnPanelDisabledText, $fallback--text);
     }
+
+    &.toggled {
+      color: $fallback--text;
+      color: var(--btnPanelToggledText, $fallback--text);
+    }
   }
 
   a {
diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue
index f0cbb94b..bfdc137b 100644
--- a/src/components/follow_button/follow_button.vue
+++ b/src/components/follow_button/follow_button.vue
@@ -1,7 +1,7 @@
 <template>
   <button
     class="btn btn-default follow-button"
-    :class="{ pressed: isPressed }"
+    :class="{ toggled: isPressed }"
     :disabled="inProgress"
     :title="title"
     @click="onClick"
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index 006d6373..e78e05f1 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -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>
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 3921c953..2fca5570 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -376,6 +376,29 @@
             />
             <ContrastRatio :contrast="previewContrast.btnDisabledText" />
           </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>
             <ColorInput
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 3e5eacd5..df585faa 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -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 {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 93d55fff..e286ceea 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -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') }}
@@ -538,12 +538,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);
-      }
     }
   }
 }
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index c5dd8047..a345d996 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -219,6 +219,19 @@ export const SLOT_INHERITANCE = {
     textColor: 'preserve'
   },
 
+  // Tabs
+  tab: '--btn',
+  tabText: {
+    depends: ['btnText'],
+    layer: 'btn',
+    textColor: true
+  },
+  tabActiveText: {
+    depends: ['text'],
+    layer: 'bg',
+    textColor: true
+  },
+
   // Buttons
   btn: '--fg',
   btnText: {
@@ -239,6 +252,7 @@ export const SLOT_INHERITANCE = {
     textColor: true
   },
 
+  // Buttons: pressed
   btnPressed: '--btn',
   btnPressedText: {
     depends: ['btnText'],
@@ -259,6 +273,31 @@ export const SLOT_INHERITANCE = {
     textColor: true
   },
 
+  // Buttons: toggled
+  btnToggled: {
+    depends: ['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)
-- 
cgit v1.2.3-70-g09d2


From 24a7a9bfd8dbdaae8b5a2e5dde5cb754a122905b Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 16 Jan 2020 23:30:13 +0200
Subject: lint

---
 src/components/style_switcher/style_switcher.vue | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 2fca5570..e3b899f0 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -215,7 +215,10 @@
               :label="$t('settings.text')"
               :fallback="previewTheme.colors.alertErrorText"
             />
-            <ContrastRatio :contrast="previewContrast.alertErrorText" large="1"/>
+            <ContrastRatio
+              :contrast="previewContrast.alertErrorText"
+              large="1"
+            />
             <ColorInput
               v-model="alertWarningColorLocal"
               name="alertWarning"
@@ -228,7 +231,10 @@
               :label="$t('settings.text')"
               :fallback="previewTheme.colors.alertWarningText"
             />
-            <ContrastRatio :contrast="previewContrast.alertWarningText" large="1"/>
+            <ContrastRatio
+              :contrast="previewContrast.alertWarningText"
+              large="1"
+            />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
@@ -244,7 +250,10 @@
               :label="$t('settings.text')"
               :fallback="previewTheme.colors.badgeNotificationText"
             />
-            <ContrastRatio :contrast="previewContrast.badgeNotificationText" large="1" />
+            <ContrastRatio
+              :contrast="previewContrast.badgeNotificationText"
+              large="1"
+            />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
-- 
cgit v1.2.3-70-g09d2


From 7d7ccf729855283ad0f5b9d6943b8d86ed1f180d Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 19 Jan 2020 22:44:35 +0200
Subject: fix some contrast ratios not displaying

---
 src/components/contrast_ratio/contrast_ratio.vue | 12 +++++++++---
 src/components/style_switcher/style_switcher.vue | 18 +++++++++---------
 2 files changed, 18 insertions(+), 12 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue
index 15a450a2..5e79ef41 100644
--- a/src/components/contrast_ratio/contrast_ratio.vue
+++ b/src/components/contrast_ratio/contrast_ratio.vue
@@ -37,9 +37,15 @@
 
 <script>
 export default {
-  props: [
-    'large', 'contrast'
-  ],
+  props: {
+    large: {
+      required: false
+    },
+    contrast: {
+      required: true,
+      type: Object
+    }
+  },
   computed: {
     hint () {
       const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index e3b899f0..c8c02b8d 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -157,13 +157,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
@@ -171,13 +171,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>
@@ -217,7 +217,7 @@
             />
             <ContrastRatio
               :contrast="previewContrast.alertErrorText"
-              large="1"
+              large="true"
             />
             <ColorInput
               v-model="alertWarningColorLocal"
@@ -233,7 +233,7 @@
             />
             <ContrastRatio
               :contrast="previewContrast.alertWarningText"
-              large="1"
+              large="true"
             />
           </div>
           <div class="color-item">
@@ -252,7 +252,7 @@
             />
             <ContrastRatio
               :contrast="previewContrast.badgeNotificationText"
-              large="1"
+              large="true"
             />
           </div>
           <div class="color-item">
@@ -277,7 +277,7 @@
             />
             <ContrastRatio
               :contrast="previewContrast.panelText"
-              large="1"
+              large="true"
             />
             <ColorInput
               v-model="panelLinkColorLocal"
@@ -287,7 +287,7 @@
             />
             <ContrastRatio
               :contrast="previewContrast.panelLink"
-              large="1"
+              large="true"
             />
           </div>
           <div class="color-item">
-- 
cgit v1.2.3-70-g09d2


From e4033c85e2066d8a575a1cd8e5a59bb685a3adf0 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 20 Jan 2020 00:34:49 +0200
Subject: removed console logs

---
 src/components/shadow_control/shadow_control.js  |  15 ++
 src/components/shadow_control/shadow_control.vue |   2 +
 src/components/style_switcher/style_switcher.js  |  49 ++++---
 src/components/style_switcher/style_switcher.vue |   2 +-
 src/services/style_setter/style_setter.js        | 178 +++++++++++++----------
 src/services/theme_data/theme_data.service.js    |  28 +++-
 6 files changed, 166 insertions(+), 108 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
index 44e4a22f..653d5a9b 100644
--- a/src/components/shadow_control/shadow_control.js
+++ b/src/components/shadow_control/shadow_control.js
@@ -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
     },
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index de8a42d1..efbb980e 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -191,6 +191,8 @@
         v-model="selected.color"
         :disabled="!present"
         :label="$t('settings.style.common.color')"
+        :fallback="currentFallback.color"
+        :showOptionalTickbox="false"
         name="shadow"
       />
       <OpacityInput
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 52ece3a1..2984873f 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -5,6 +5,7 @@ import {
   getContrastRatioLayers
 } from '../../services/color_convert/color_convert.js'
 import {
+  DEFAULT_SHADOWS,
   generateColors,
   generateShadows,
   generateRadii,
@@ -76,7 +77,7 @@ export default {
         .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
 
       ...Object.keys(OPACITIES)
-        .map(key => console.log(key) || [key, ''])
+        .map(key => [key, ''])
         .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
 
       shadowSelected: undefined,
@@ -134,7 +135,7 @@ export default {
     },
     currentOpacity () {
       return Object.keys(OPACITIES)
-        .map(key => console.log(key) || [key, this[key + 'OpacityLocal']])
+        .map(key => [key, this[key + 'OpacityLocal']])
         .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
     },
     currentRadii () {
@@ -224,7 +225,7 @@ export default {
       ].join(';')
     },
     shadowsAvailable () {
-      return Object.keys(this.previewTheme.shadows).sort()
+      return Object.keys(DEFAULT_SHADOWS).sort()
     },
     currentShadowOverriden: {
       get () {
@@ -239,7 +240,7 @@ export default {
       }
     },
     currentShadowFallback () {
-      return this.previewTheme.shadows[this.shadowSelected]
+      return (this.previewTheme.shadows || {})[this.shadowSelected]
     },
     currentShadow: {
       get () {
@@ -314,6 +315,17 @@ export default {
         }
       })
     },
+    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) {
       if (parsed._pleroma_theme_version === 1) {
         this.normalizeLocalState(parsed, 1)
@@ -435,6 +447,14 @@ export default {
         })
       }
 
+      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
+        })
+      }
+
       if (!this.keepRoundness) {
         this.clearRoundness()
         Object.entries(radii).forEach(([k, v]) => {
@@ -454,14 +474,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: {
@@ -476,8 +488,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
@@ -500,10 +513,7 @@ export default {
     },
     currentColors () {
       try {
-        this.previewColors = generateColors({
-          opacity: this.currentOpacity,
-          colors: this.currentColors
-        })
+        this.updatePreviewColorsAndShadows()
         this.colorsInvalid = false
       } catch (e) {
         this.colorsInvalid = true
@@ -512,10 +522,7 @@ export default {
     },
     currentOpacity () {
       try {
-        this.previewColors = generateColors({
-          opacity: this.currentOpacity,
-          colors: this.currentColors
-        })
+        this.updatePreviewColorsAndShadows()
       } catch (e) {
         console.warn(e)
       }
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index c8c02b8d..287d31b7 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -691,7 +691,7 @@
               {{ $t('settings.style.switcher.clear_all') }}
             </button>
           </div>
-          <shadow-control
+          <ShadowControl
             v-model="currentShadow"
             :ready="!!currentShadowFallback"
             :fallback="currentShadowFallback"
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 872dd393..74af190c 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,7 +1,7 @@
 import { times } from 'lodash'
 import { convert } from 'chromatism'
 import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js'
-import { getColors } from '../theme_data/theme_data.service.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
@@ -139,7 +139,8 @@ export const generateColors = (themeData) => {
     theme: {
       colors: htmlColors.solid,
       opacity
-    }
+    },
+    mod
   }
 }
 
@@ -211,83 +212,99 @@ export const generateFonts = (input) => {
   }
 }
 
-export 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
-  }
-
-  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
-    }],
+  }, ...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, mod) => {
+  const shadows = Object.entries({
+    ...DEFAULT_SHADOWS,
     ...(input.shadows || {})
-  }
+  }).reduce((shadowsAcc, [slotName, shadowdefs]) => {
+    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: {
@@ -325,12 +342,15 @@ export const composePreset = (colors, radii, shadows, fonts) => {
   }
 }
 
-export const generatePreset = (input) => composePreset(
-  generateColors(input),
-  generateRadii(input),
-  generateShadows(input),
-  generateFonts(input)
-)
+export const generatePreset = (input) => {
+  const colors = generateColors(input)
+  return composePreset(
+    colors,
+    generateRadii(input),
+    generateShadows(input, colors.theme.colors, colors.mod),
+    generateFonts(input)
+  )
+}
 
 export const getThemes = () => {
   return window.fetch('/static/styles.json')
@@ -362,7 +382,7 @@ export const getThemes = () => {
 
 export const setPreset = (val, commit) => {
   return getThemes()
-    .then((themes) => console.log(themes) || themes[val] ? themes[val] : themes['pleroma-dark'])
+    .then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
     .then((theme) => {
       const isV1 = Array.isArray(theme)
       const data = isV1 ? {} : theme.theme
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index 9f010fdf..e4456b29 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -697,6 +697,22 @@ export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k, v]) =
   }
 }, {})
 
+/**
+ * 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, mod
  * value and uses inheritance data to figure out color needed for the slot.
@@ -728,13 +744,11 @@ export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.redu
         a: 0
       }
     } else if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) {
-      // Color references other color
-      const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
-      const variableSlot = variable.substring(2)
-      targetColor = colors[variableSlot] || sourceColors[variableSlot]
-      if (modifier) {
-        targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
-      }
+      targetColor = computeDynamicColor(
+        sourceColor,
+        variableSlot => colors[variableSlot] || sourceColors[variableSlot],
+        mod
+      )
     } else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) {
       targetColor = convert(targetColor).rgb
     }
-- 
cgit v1.2.3-70-g09d2


From 9336140486f50159b935001b7ebadf3d9bda89ec Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 22 Jan 2020 00:37:19 +0200
Subject: massively improved initial theme loading code, added checks and
 warnings when loading theme files (import/localStorage/defaults)

---
 src/boot/after_store.js                           |  32 +++--
 src/components/style_switcher/style_switcher.js   | 153 ++++++++++++++++++++--
 src/components/style_switcher/style_switcher.scss |   4 +
 src/components/style_switcher/style_switcher.vue  |  79 +++++++----
 src/i18n/en.json                                  |  12 +-
 src/modules/config.js                             |   7 +-
 src/modules/instance.js                           |  17 ++-
 src/services/style_setter/style_setter.js         |  27 ++--
 8 files changed, 259 insertions(+), 72 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 228a0497..6c4f0e1b 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -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 {
@@ -261,7 +263,7 @@ const checkOAuthToken = async ({ store }) => {
       try {
         await store.dispatch('loginUser', store.getters.getUserToken())
       } catch (e) {
-        console.log(e)
+        console.error(e)
       }
     }
     resolve()
@@ -269,23 +271,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.version === 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 }),
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 799646b1..0ef02f39 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -57,6 +57,8 @@ export default {
     return {
       availableStyles: [],
       selected: this.$store.getters.mergedConfig.theme,
+      themeWarning: undefined,
+      tempImportFile: undefined,
 
       previewShadows: {},
       previewColors: {},
@@ -120,12 +122,62 @@ export default {
       })
   },
   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') {
+        // FE upgraded from v2
+        if (themeEngineVersion === 2) {
+          return 'upgraded_from_v2'
+        }
+        // Admin downgraded FE
+        if (themeEngineVersion > CURRENT_VERSION) {
+          return noActionsPossible
+            ? 'downgraded_theme'
+            : 'downgraded_theme_missing_snapshot'
+        }
+        // Admin upgraded FE
+        if (themeEngineVersion < CURRENT_VERSION) {
+          return noActionsPossible
+            ? 'upgraded_theme'
+            : 'upgraded_theme_missing_snapshot'
+        }
+      }
+    },
     selectedVersion () {
       return Array.isArray(this.selected) ? 1 : 2
     },
@@ -308,10 +360,96 @@ 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 themeEngineVersion = (source || {}).themeEngineVersion || 2
+      const versionsMatch = themeEngineVersion === CURRENT_VERSION
+      // Force loading of source if user requested it or if snapshot
+      // is unavailable
+      const forcedSourceLoad = (source && forceUseSource) || !theme
+      if (!versionsMatch &&
+          !forcedSourceLoad &&
+          version !== 'l1' &&
+          origin !== 'defaults'
+      ) {
+        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
+      }
+    },
+    loadThemeFromLocalStorage (confirmLoadSource = 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 },
+          'localStorage',
+          confirmLoadSource
+        )
+      }
+    },
     setCustomTheme () {
       this.$store.dispatch('setOption', {
         name: 'customTheme',
+        value: this.previewTheme
+      })
+      this.$store.dispatch('setOption', {
+        name: 'customThemeSource',
         value: {
+          themeEngineVersion: CURRENT_VERSION,
           shadows: this.shadowsLocal,
           fonts: this.fontsLocal,
           opacity: this.currentOpacity,
@@ -331,21 +469,16 @@ export default {
         this.previewColors.mod
       )
     },
-    onImport (parsed) {
-      if (parsed._pleroma_theme_version === 1) {
-        this.normalizeLocalState(parsed, 1)
-      } else if (parsed._pleroma_theme_version >= 2) {
-        this.normalizeLocalState(parsed.theme, 2, parsed.source)
-      }
+    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.$store.getters.mergedConfig.customThemeSource)
+      this.loadThemeFromLocalStorage()
     },
 
     // Clears all the extra stuff when loading V1 theme
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss
index 987245a2..71d0f05e 100644
--- a/src/components/style_switcher/style_switcher.scss
+++ b/src/components/style_switcher/style_switcher.scss
@@ -1,5 +1,9 @@
 @import '../../_variables.scss';
 .style-switcher {
+  .theme-warning {
+    display: flex;
+    align-items: baseline;
+  }
   .preset-switcher {
     margin-right: 1em;
   }
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 287d31b7..61f8800a 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -1,31 +1,60 @@
 <template>
-  <div class="style-switcher">
-    <div class="presets-container">
-      <div class="save-load">
-        <ExportImport
-          :export-object="exportedTheme"
-          :export-label="$t(&quot;settings.export_theme&quot;)"
-          :import-label="$t(&quot;settings.import_theme&quot;)"
-          :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
-          :on-import="onImport"
-          :validator="importValidator"
-        >
-          <template slot="before">
-            <div class="presets">
-              {{ $t('settings.presets') }}
-              <label
-                for="preset-switcher"
-                class="select"
+<div class="style-switcher">
+  <div class="presets-container">
+    <div class="save-load">
+      <div class="theme-warning" v-if="themeWarning">
+        <div class="alert warning">
+        {{ themeWarningHelp }}
+        </div>
+        <div class="buttons">
+          <template v-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.use_snapshot') }}
+            </button>
+          </template>
+        </div>
+      </div>
+      <ExportImport
+        :export-object="exportedTheme"
+        :export-label="$t(&quot;settings.export_theme&quot;)"
+        :import-label="$t(&quot;settings.import_theme&quot;)"
+        :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
+        :on-import="onImport"
+        :validator="importValidator"
+      >
+        <template slot="before">
+          <div class="presets">
+            {{ $t('settings.presets') }}
+            <label
+              for="preset-switcher"
+              class="select"
               >
-                <select
-                  id="preset-switcher"
-                  v-model="selected"
-                  class="preset-switcher"
+              <select
+                id="preset-switcher"
+                v-model="selected"
+                class="preset-switcher"
                 >
-                  <option
-                    v-for="style in availableStyles"
-                    :key="style.name"
-                    :value="style"
+                <option
+                  v-for="style in availableStyles"
+                  :key="style.name"
+                  :value="style"
                     :style="{
                       backgroundColor: style[1] || (style.theme || style.source).colors.bg,
                       color: style[3] || (style.theme || style.source).colors.text
diff --git a/src/i18n/en.json b/src/i18n/en.json
index f624a1c5..6994d62f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -46,6 +46,7 @@
     "optional": "optional",
     "show_more": "Show more",
     "show_less": "Show less",
+    "dismiss": "Dismiss",
     "cancel": "Cancel",
     "disable": "Disable",
     "enable": "Enable",
@@ -394,7 +395,16 @@
         "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",
+        "use_snapshot": "Keep as is",
+        "help": {
+          "v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsitencies.",
+          "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.",
+          "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."
+        }
       },
       "common": {
         "color": "Color",
diff --git a/src/modules/config.js b/src/modules/config.js
index 74025db1..ee474b16 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -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
@@ -93,10 +96,10 @@ const config = {
       commit('setOption', { name, value })
       switch (name) {
         case 'theme':
-          setPreset(value, commit)
+          setPreset(value)
           break
         case 'customTheme':
-          applyTheme(value, commit)
+          applyTheme(value)
       }
     }
   }
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 625323b9..8781646d 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -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,
@@ -96,6 +97,9 @@ const instance = {
             dispatch('initializeSocket')
           }
           break
+        case 'theme':
+          dispatch('setTheme', value)
+          break
       }
     },
     async getStaticEmoji ({ commit }) {
@@ -147,9 +151,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) {
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index ee264c49..9610d799 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -7,7 +7,7 @@ import { getColors, computeDynamicColor } from '../theme_data/theme_data.service
 // 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.
 
-export const setStyle = (href, commit) => {
+export const setStyle = (href) => {
   /***
       What's going on here?
       I want to make it easy for admins to style this application. To have
@@ -53,8 +53,8 @@ export const setStyle = (href, commit) => {
   cssEl.addEventListener('load', setDynamic)
 }
 
-export 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')
@@ -69,11 +69,6 @@ export 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 })
 }
 
 export const getCssShadow = (input, usesDropShadow) => {
@@ -387,7 +382,7 @@ export const getThemes = () => {
  */
 export const shadows2to3 = (shadows) => {
   return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
-    const isDynamic = ({ color }) => console.log(color) || color.startsWith('--')
+    const isDynamic = ({ color }) => color.startsWith('--')
     const newShadow = shadowDefs.reduce((shadowAcc, def) => [
       ...shadowAcc,
       {
@@ -399,7 +394,7 @@ export const shadows2to3 = (shadows) => {
   }, {})
 }
 
-export const setPreset = (val, commit) => {
+export const getPreset = (val) => {
   return getThemes()
     .then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
     .then((theme) => {
@@ -420,14 +415,8 @@ export const setPreset = (val, commit) => {
         data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
       }
 
-      // This is a hack, this function is only called during initial load.
-      // We want to cancel loading the theme from config.json if we're already
-      // loading a theme from the persisted state.
-      // Needed some way of dealing with the async way of things.
-      // load config -> set preset -> wait for styles.json to load ->
-      // load persisted state -> set colors -> styles.json loaded -> set colors
-      if (!window.themeLoaded) {
-        applyTheme(data, commit)
-      }
+      return { theme: data, source: theme.source }
     })
 }
+
+export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme))
-- 
cgit v1.2.3-70-g09d2


From 644ce497a0f118b2e5050a2e45b67450915c53f0 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 22 Jan 2020 01:28:46 +0200
Subject: lightBg -> highlight because it causes issues with v2

---
 src/components/style_switcher/style_switcher.vue | 24 +++++-----
 src/i18n/en.json                                 |  2 +-
 src/services/theme_data/theme_data.service.js    | 60 ++++++++++++------------
 static/themes/breezy-dark.json                   |  2 +-
 static/themes/breezy-light.json                  |  2 +-
 static/themes/redmond-xx-se.json                 |  2 +-
 static/themes/redmond-xx.json                    |  2 +-
 static/themes/redmond-xxi.json                   |  2 +-
 8 files changed, 48 insertions(+), 48 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 61f8800a..79c2fcd3 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -518,27 +518,27 @@
             />
           </div>
           <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.lightBg') }}</h4>
+            <h4>{{ $t('settings.style.advanced_colors.highlight') }}</h4>
             <ColorInput
-              v-model="lightBgColorLocal"
-              name="lightBg"
+              v-model="highlightColorLocal"
+              name="highlight"
               :label="$t('settings.background')"
-              :fallback="previewTheme.colors.lightBg"
+              :fallback="previewTheme.colors.highlight"
             />
             <ColorInput
-              v-model="lightBgTextColorLocal"
-              name="lightBgText"
+              v-model="highlightTextColorLocal"
+              name="highlightText"
               :label="$t('settings.text')"
-              :fallback="previewTheme.colors.lightBgText"
+              :fallback="previewTheme.colors.highlightText"
             />
-            <ContrastRatio :contrast="previewContrast.lightBgText" />
+            <ContrastRatio :contrast="previewContrast.highlightText" />
             <ColorInput
-              v-model="lightBgLinkColorLocal"
-              name="lightBgLink"
+              v-model="highlightLinkColorLocal"
+              name="highlightLink"
               :label="$t('settings.links')"
-              :fallback="previewTheme.colors.lightBgLink"
+              :fallback="previewTheme.colors.highlightLink"
             />
-            <ContrastRatio :contrast="previewContrast.lightBgLink" />
+            <ContrastRatio :contrast="previewContrast.highlightLink" />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.selectedPost') }}</h4>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 815c11b1..2622157a 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -449,7 +449,7 @@
         "underlay": "Underlay",
         "poll": "Poll graph",
         "icons": "Icons",
-        "lightBg": "Highlighted elements",
+        "highlight": "Highlighted elements",
         "pressed": "Pressed",
         "selectedPost": "Selected post",
         "selectedMenu": "Selected menu item",
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index 63bfb5af..8c114004 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -47,7 +47,7 @@ export const LAYERS = {
   badge: null, //  no transparency support
   fg: null,
   bg: 'underlay',
-  lightBg: 'bg',
+  highlight: 'bg',
   panel: 'bg',
   btn: 'bg',
   btnPanel: 'panel',
@@ -148,57 +148,57 @@ export const SLOT_INHERITANCE = {
   cGreen: '#00FF00',
   cOrange: '#E3FF00',
 
-  lightBg: {
+  highlight: {
     depends: ['bg'],
     color: (mod, bg) => brightness(5 * mod, bg).rgb
   },
-  lightBgFaintText: {
+  highlightFaintText: {
     depends: ['faint'],
-    layer: 'lightBg',
+    layer: 'highlight',
     textColor: true
   },
-  lightBgFaintLink: {
+  highlightFaintLink: {
     depends: ['faintLink'],
-    layer: 'lightBg',
+    layer: 'highlight',
     textColor: 'preserve'
   },
-  lightBgText: {
+  highlightText: {
     depends: ['text'],
-    layer: 'lightBg',
+    layer: 'highlight',
     textColor: true
   },
-  lightBgLink: {
+  highlightLink: {
     depends: ['link'],
-    layer: 'lightBg',
+    layer: 'highlight',
     textColor: 'preserve'
   },
-  lightBgIcon: {
-    depends: ['lightBg', 'lightBgText'],
+  highlightIcon: {
+    depends: ['highlight', 'highlightText'],
     color: (mod, bg, text) => mixrgb(bg, text)
   },
 
-  selectedPost: '--lightBg',
+  selectedPost: '--highlight',
   selectedPostFaintText: {
-    depends: ['lightBgFaintText'],
-    layer: 'lightBg',
+    depends: ['highlightFaintText'],
+    layer: 'highlight',
     variant: 'selectedPost',
     textColor: true
   },
   selectedPostFaintLink: {
-    depends: ['lightBgFaintLink'],
-    layer: 'lightBg',
+    depends: ['highlightFaintLink'],
+    layer: 'highlight',
     variant: 'selectedPost',
     textColor: 'preserve'
   },
   selectedPostText: {
-    depends: ['lightBgText'],
-    layer: 'lightBg',
+    depends: ['highlightText'],
+    layer: 'highlight',
     variant: 'selectedPost',
     textColor: true
   },
   selectedPostLink: {
-    depends: ['lightBgLink'],
-    layer: 'lightBg',
+    depends: ['highlightLink'],
+    layer: 'highlight',
     variant: 'selectedPost',
     textColor: 'preserve'
   },
@@ -207,28 +207,28 @@ export const SLOT_INHERITANCE = {
     color: (mod, bg, text) => mixrgb(bg, text)
   },
 
-  selectedMenu: '--lightBg',
+  selectedMenu: '--highlight',
   selectedMenuFaintText: {
-    depends: ['lightBgFaintText'],
-    layer: 'lightBg',
+    depends: ['highlightFaintText'],
+    layer: 'highlight',
     variant: 'selectedMenu',
     textColor: true
   },
   selectedMenuFaintLink: {
-    depends: ['lightBgFaintLink'],
-    layer: 'lightBg',
+    depends: ['highlightFaintLink'],
+    layer: 'highlight',
     variant: 'selectedMenu',
     textColor: 'preserve'
   },
   selectedMenuText: {
-    depends: ['lightBgText'],
-    layer: 'lightBg',
+    depends: ['highlightText'],
+    layer: 'highlight',
     variant: 'selectedMenu',
     textColor: true
   },
   selectedMenuLink: {
-    depends: ['lightBgLink'],
-    layer: 'lightBg',
+    depends: ['highlightLink'],
+    layer: 'highlight',
     variant: 'selectedMenu',
     textColor: 'preserve'
   },
diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json
index 3aafda52..c3bdcf65 100644
--- a/static/themes/breezy-dark.json
+++ b/static/themes/breezy-dark.json
@@ -114,7 +114,7 @@
       "cGreen": "#27ae60",
       "cOrange": "#f67400",
       "btnPressed": "--accent",
-      "lightBg": "--accent",
+      "highlight": "--accent",
       "selectedPost": "--bg,10"
     },
     "radii": {
diff --git a/static/themes/breezy-light.json b/static/themes/breezy-light.json
index df6e1a66..478407c9 100644
--- a/static/themes/breezy-light.json
+++ b/static/themes/breezy-light.json
@@ -117,7 +117,7 @@
       "cGreen": "#27ae60",
       "cOrange": "#f67400",
       "btnPressed": "--accent",
-      "lightBg": "--accent",
+      "highlight": "--accent",
       "selectedPost": "--bg,10"
     },
     "radii": {
diff --git a/static/themes/redmond-xx-se.json b/static/themes/redmond-xx-se.json
index 13ed3b0e..1a867fcc 100644
--- a/static/themes/redmond-xx-se.json
+++ b/static/themes/redmond-xx-se.json
@@ -283,7 +283,7 @@
       "cBlue": "#008080",
       "cGreen": "#008000",
       "cOrange": "#808000",
-      "lightBg": "--accent",
+      "highlight": "--accent",
       "selectedPost": "--bg,-10"
     },
     "radii": {
diff --git a/static/themes/redmond-xx.json b/static/themes/redmond-xx.json
index 7c687ae0..78c92f10 100644
--- a/static/themes/redmond-xx.json
+++ b/static/themes/redmond-xx.json
@@ -274,7 +274,7 @@
       "cBlue": "#008080",
       "cGreen": "#008000",
       "cOrange": "#808000",
-      "lightBg": "--accent",
+      "highlight": "--accent",
       "selectedPost": "--bg,-10"
     },
     "radii": {
diff --git a/static/themes/redmond-xxi.json b/static/themes/redmond-xxi.json
index 5371ee64..28f68351 100644
--- a/static/themes/redmond-xxi.json
+++ b/static/themes/redmond-xxi.json
@@ -256,7 +256,7 @@
       "cBlue": "#6699cc",
       "cGreen": "#669966",
       "cOrange": "#cc6633",
-      "lightBg": "--accent",
+      "highlight": "--accent",
       "selectedPost": "--bg,-10"
     },
     "radii": {
-- 
cgit v1.2.3-70-g09d2


From d98e31af458ed42e678072879265267e8148c627 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 22 Jan 2020 02:44:39 +0200
Subject: snapshot/source mismatch case

---
 src/components/style_switcher/style_switcher.js  | 53 +++++++++++++++++++++---
 src/components/style_switcher/style_switcher.vue | 18 +++++++-
 src/i18n/en.json                                 |  7 +++-
 3 files changed, 68 insertions(+), 10 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 195c6b35..3b538ac7 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -160,6 +160,9 @@ export default {
             )
         }
       } 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')
@@ -345,7 +348,10 @@ export default {
         source.radii = this.currentRadii
       }
 
-      const theme = this.previewTheme
+      const theme = {
+        themeEngineVersion: CURRENT_VERSION,
+        ...this.previewTheme
+      }
 
       return {
         // To separate from other random JSON files and possible future source formats
@@ -381,17 +387,34 @@ export default {
       const version = (origin === 'localstorage' && !theme.colors)
         ? 'l1'
         : fileVersion
+      const snapshotEngineVersion = (theme || {}).themeEngineVersion
       const themeEngineVersion = (source || {}).themeEngineVersion || 2
       const versionsMatch = themeEngineVersion === CURRENT_VERSION
+      console.log(
+        theme !== undefined,
+        source !== undefined,
+        themeEngineVersion !== snapshotEngineVersion
+      )
+      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 &&
+      if (!(versionsMatch && !sourceSnapshotMismatch) &&
           !forcedSourceLoad &&
           version !== 'l1' &&
           origin !== 'defaults'
       ) {
-        if (!theme) {
+        if (sourceSnapshotMismatch) {
+          this.themeWarning = {
+            origin,
+            themeEngineVersion,
+            type: 'snapshot_source_mismatch'
+          }
+        } else if (!theme) {
           this.themeWarning = {
             origin,
             noActionsPossible: true,
@@ -428,7 +451,19 @@ export default {
       }
       this.dismissWarning()
     },
-    loadThemeFromLocalStorage (confirmLoadSource = false) {
+    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
@@ -442,7 +477,10 @@ export default {
         )
       } else {
         this.loadTheme(
-          { theme, source },
+          {
+            theme,
+            source: forceSnapshot ? theme : source
+          },
           'localStorage',
           confirmLoadSource
         )
@@ -451,7 +489,10 @@ export default {
     setCustomTheme () {
       this.$store.dispatch('setOption', {
         name: 'customTheme',
-        value: this.previewTheme
+        value: {
+          themeEngineVersion: CURRENT_VERSION,
+          ...this.previewTheme
+        }
       })
       this.$store.dispatch('setOption', {
         name: 'customThemeSource',
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 79c2fcd3..793d68f1 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -7,7 +7,21 @@
         {{ themeWarningHelp }}
         </div>
         <div class="buttons">
-          <template v-if="themeWarning.noActionsPossible">
+          <template v-if="themeWarning.type === 'snapshot_source_mismatch'">
+            <button
+              class="btn"
+              @click="forceLoad"
+            >
+              {{ $t('settings.style.switcher.use_source') }}
+            </button>
+            <button
+              class="btn"
+              @click="dismissWarning"
+            >
+              {{ $t('settings.style.switcher.use_snapshot') }}
+            </button>
+          </template>
+          <template v-else-if="themeWarning.noActionsPossible">
             <button
               class="btn"
               @click="dismissWarning"
@@ -26,7 +40,7 @@
               class="btn"
               @click="dismissWarning"
             >
-              {{ $t('settings.style.switcher.use_snapshot') }}
+              {{ $t('settings.style.switcher.keep_as_is') }}
             </button>
           </template>
         </div>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 2622157a..81dde663 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -397,7 +397,9 @@
         "clear_all": "Clear all",
         "clear_opacity": "Clear opacity",
         "load_theme": "Load theme",
-        "use_snapshot": "Keep as is",
+        "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.",
@@ -408,7 +410,8 @@
           "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."
+          "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": {
-- 
cgit v1.2.3-70-g09d2


From c7f42b7799d4a0484f7d40ac0a7377021caf9cb6 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Wed, 22 Jan 2020 02:53:40 +0200
Subject: made it actually work, the forceSnapshot

---
 src/components/style_switcher/style_switcher.js  | 11 +++--------
 src/components/style_switcher/style_switcher.vue |  2 +-
 2 files changed, 4 insertions(+), 9 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 3b538ac7..27df0d10 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -390,11 +390,6 @@ export default {
       const snapshotEngineVersion = (theme || {}).themeEngineVersion
       const themeEngineVersion = (source || {}).themeEngineVersion || 2
       const versionsMatch = themeEngineVersion === CURRENT_VERSION
-      console.log(
-        theme !== undefined,
-        source !== undefined,
-        themeEngineVersion !== snapshotEngineVersion
-      )
       const sourceSnapshotMismatch = (
         theme !== undefined &&
           source !== undefined &&
@@ -442,7 +437,7 @@ export default {
     forceLoad () {
       const { origin } = this.themeWarning
       switch (origin) {
-        case 'localstorage':
+        case 'localStorage':
           this.loadThemeFromLocalStorage(true)
           break
         case 'file':
@@ -451,10 +446,10 @@ export default {
       }
       this.dismissWarning()
     },
-    forceSnapshot() {
+    forceSnapshot () {
       const { origin } = this.themeWarning
       switch (origin) {
-        case 'localstorage':
+        case 'localStorage':
           this.loadThemeFromLocalStorage(false, true)
           break
         case 'file':
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 793d68f1..ff7f4710 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -16,7 +16,7 @@
             </button>
             <button
               class="btn"
-              @click="dismissWarning"
+              @click="forceSnapshot"
             >
               {{ $t('settings.style.switcher.use_snapshot') }}
             </button>
-- 
cgit v1.2.3-70-g09d2


From 53576df72aff5405ab30764e5cfcf2162bde37bc Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 24 Jan 2020 00:02:16 +0200
Subject: popover/selected menu improvements

---
 src/components/emoji_input/emoji_input.vue       | 11 +++---
 src/components/popper/popper.scss                |  7 +++-
 src/components/side_drawer/side_drawer.vue       |  7 +++-
 src/components/style_switcher/style_switcher.vue | 29 ++++++++++++++++
 src/services/theme_data/pleromafe.js             | 44 +++++++++++++++++++++---
 5 files changed, 87 insertions(+), 11 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 46ed6f25..9c2501a9 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -109,10 +109,13 @@
         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);
+        --icon: var(--popoverIcon, $fallback--icon);
       }
     }
 
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
index 9c2ce25f..e70b277d 100644
--- a/src/components/popper/popper.scss
+++ b/src/components/popper/popper.scss
@@ -9,7 +9,12 @@
     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);
+    --icon: var(--popoverIcon, $fallback--icon);
   }
 
   .popover-arrow {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 3188fd59..a5b754d7 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -223,7 +223,12 @@
   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);
+  --icon: var(--popoverIcon, $fallback--icon);
 
   .button-icon:before {
     width: 1.1em;
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index ff7f4710..1c39a806 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -554,6 +554,35 @@
             />
             <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
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 0deb829f..484874d7 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -12,6 +12,8 @@ export const LAYERS = {
   bg: 'underlay',
   highlight: 'bg',
   panel: 'bg',
+  popover: 'bg',
+  selectedMenu: 'popover',
   btn: 'bg',
   btnPanel: 'panel',
   btnTopBar: 'topBar',
@@ -140,6 +142,35 @@ export const SLOT_INHERITANCE = {
     color: (mod, bg, text) => mixrgb(bg, text)
   },
 
+  popover: {
+    depends: ['bg'],
+    opacity: 'popover'
+  },
+  popoverFaintText: {
+    depends: ['faint'],
+    layer: 'popover',
+    textColor: true
+  },
+  popoverFaintLink: {
+    depends: ['faintLink'],
+    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'],
@@ -170,28 +201,31 @@ export const SLOT_INHERITANCE = {
     color: (mod, bg, text) => mixrgb(bg, text)
   },
 
-  selectedMenu: '--highlight',
+  selectedMenu: {
+    depends: ['popover'],
+    color: (mod, bg) => brightness(5 * mod, bg).rgb
+  },
   selectedMenuFaintText: {
     depends: ['highlightFaintText'],
-    layer: 'highlight',
+    layer: 'selectedMenu',
     variant: 'selectedMenu',
     textColor: true
   },
   selectedMenuFaintLink: {
     depends: ['highlightFaintLink'],
-    layer: 'highlight',
+    layer: 'selectedMenu',
     variant: 'selectedMenu',
     textColor: 'preserve'
   },
   selectedMenuText: {
     depends: ['highlightText'],
-    layer: 'highlight',
+    layer: 'selectedMenu',
     variant: 'selectedMenu',
     textColor: true
   },
   selectedMenuLink: {
     depends: ['highlightLink'],
-    layer: 'highlight',
+    layer: 'selectedMenu',
     variant: 'selectedMenu',
     textColor: 'preserve'
   },
-- 
cgit v1.2.3-70-g09d2


From 5313833d80221bbad667aeda39ca9e7321489e30 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 27 Jan 2020 04:24:00 +0200
Subject: lint

---
 src/components/style_switcher/style_switcher.vue   | 137 +++++++++++----------
 .../specs/services/theme_data/theme_data.spec.js   |   1 -
 2 files changed, 70 insertions(+), 68 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 1c39a806..6e38bd8e 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -1,74 +1,77 @@
 <template>
-<div class="style-switcher">
-  <div class="presets-container">
-    <div class="save-load">
-      <div class="theme-warning" v-if="themeWarning">
-        <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 class="style-switcher">
+    <div class="presets-container">
+      <div class="save-load">
+        <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>
-      </div>
-      <ExportImport
-        :export-object="exportedTheme"
-        :export-label="$t(&quot;settings.export_theme&quot;)"
-        :import-label="$t(&quot;settings.import_theme&quot;)"
-        :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
-        :on-import="onImport"
-        :validator="importValidator"
-      >
-        <template slot="before">
-          <div class="presets">
-            {{ $t('settings.presets') }}
-            <label
-              for="preset-switcher"
-              class="select"
+        <ExportImport
+          :export-object="exportedTheme"
+          :export-label="$t(&quot;settings.export_theme&quot;)"
+          :import-label="$t(&quot;settings.import_theme&quot;)"
+          :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
+          :on-import="onImport"
+          :validator="importValidator"
+        >
+          <template slot="before">
+            <div class="presets">
+              {{ $t('settings.presets') }}
+              <label
+                for="preset-switcher"
+                class="select"
               >
-              <select
-                id="preset-switcher"
-                v-model="selected"
-                class="preset-switcher"
+                <select
+                  id="preset-switcher"
+                  v-model="selected"
+                  class="preset-switcher"
                 >
-                <option
-                  v-for="style in availableStyles"
-                  :key="style.name"
-                  :value="style"
+                  <option
+                    v-for="style in availableStyles"
+                    :key="style.name"
+                    :value="style"
                     :style="{
                       backgroundColor: style[1] || (style.theme || style.source).colors.bg,
                       color: style[3] || (style.theme || style.source).colors.text
diff --git a/test/unit/specs/services/theme_data/theme_data.spec.js b/test/unit/specs/services/theme_data/theme_data.spec.js
index 67f4fd5a..c634cade 100644
--- a/test/unit/specs/services/theme_data/theme_data.spec.js
+++ b/test/unit/specs/services/theme_data/theme_data.spec.js
@@ -86,5 +86,4 @@ describe('Theme Data utility functions', () => {
       expect(out.indexOf('a')).to.be.below(out.indexOf('c'))
     })
   })
-
 })
-- 
cgit v1.2.3-70-g09d2


From 526b43eba66c597c78857da6ded3c7f672f96e49 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 28 Jan 2020 01:29:20 +0200
Subject: Kenomo (see: #624) theme. Ability to define link color for post
 contents. Fixes

---
 src/components/emoji_input/emoji_input.vue       |  2 +
 src/components/notifications/notifications.scss  |  3 +
 src/components/popper/popper.scss                |  2 +
 src/components/status/status.vue                 |  7 +++
 src/components/style_switcher/style_switcher.vue | 17 ++++++
 src/components/user_card/user_card.vue           |  5 ++
 src/services/theme_data/pleromafe.js             | 34 ++++++++++++
 static/themes/kenomo.json                        | 71 ++++++++++++++++++++++++
 8 files changed, 141 insertions(+)
 create mode 100644 static/themes/kenomo.json

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 94eae560..e9ac09c3 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -116,6 +116,8 @@
         --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);
       }
     }
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 71876b14..ca762432 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -68,6 +68,9 @@
         a {
           color: var(--faintLink);
         }
+        .status-content a {
+          color: var(--postFaintLink);
+        }
       }
       padding: 0;
       .media-body {
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
index 341416c1..8e2dcd55 100644
--- a/src/components/popper/popper.scss
+++ b/src/components/popper/popper.scss
@@ -15,6 +15,8 @@
     --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);
   }
 
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 1997e187..b1048832 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -451,6 +451,8 @@ $status-margin: 0.75em;
     --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);
   }
 
@@ -640,6 +642,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;
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 6e38bd8e..0b3b0fbf 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -248,6 +248,23 @@
             </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')"
+              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
+            />
+            <ContrastRatio :contrast="previewContrast.bgPostLink" />
+            <ColorInput
+              v-model="iconColorLocal"
+              name="iconColor"
+              :fallback="previewTheme.colors.accent"
+              :label="$t('settings.links')"
+              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
+            />
+            <ContrastRatio :contrast="previewContrast.bgIcon" />
             <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
             <ColorInput
               v-model="alertErrorColorLocal"
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index e286ceea..72656435 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -299,6 +299,11 @@
   &-bio {
     text-align: center;
 
+    a {
+      color: $fallback--link;
+      color: var(--postLink, $fallback--link);
+    }
+
     img {
       object-fit: contain;
       vertical-align: middle;
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index a40d08a6..74e6fc1c 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -107,6 +107,10 @@ export const SLOT_INHERITANCE = {
     depends: ['link'],
     opacity: 'faint'
   },
+  postFaintLink: {
+    depends: ['postLink'],
+    opacity: 'faint'
+  },
 
   cBlue: '#0000ff',
   cRed: '#FF0000',
@@ -122,6 +126,11 @@ export const SLOT_INHERITANCE = {
     layer: 'highlight',
     textColor: true
   },
+  highlightPostLink: {
+    depends: ['postLink'],
+    layer: 'highlight',
+    textColor: 'preserve'
+  },
   highlightFaintText: {
     depends: ['faint'],
     layer: 'highlight',
@@ -132,6 +141,11 @@ export const SLOT_INHERITANCE = {
     layer: 'highlight',
     textColor: 'preserve'
   },
+  highlightPostFaintLink: {
+    depends: ['postFaintLink'],
+    layer: 'highlight',
+    textColor: 'preserve'
+  },
   highlightText: {
     depends: ['text'],
     layer: 'highlight',
@@ -156,6 +170,11 @@ export const SLOT_INHERITANCE = {
     layer: 'popover',
     textColor: true
   },
+  popoverPostLink: {
+    depends: ['postLink'],
+    layer: 'popover',
+    textColor: 'preserve'
+  },
   popoverFaintText: {
     depends: ['faint'],
     layer: 'popover',
@@ -166,6 +185,11 @@ export const SLOT_INHERITANCE = {
     layer: 'popover',
     textColor: 'preserve'
   },
+  popoverPostFaintLink: {
+    depends: ['postFaintLink'],
+    layer: 'popover',
+    textColor: 'preserve'
+  },
   popoverText: {
     depends: ['text'],
     layer: 'popover',
@@ -194,6 +218,12 @@ export const SLOT_INHERITANCE = {
     variant: 'selectedPost',
     textColor: true
   },
+  selectedPostPostLink: {
+    depends: ['highlightPostLink'],
+    layer: 'highlight',
+    variant: 'selectedPost',
+    textColor: 'preserve'
+  },
   selectedPostFaintLink: {
     depends: ['highlightFaintLink'],
     layer: 'highlight',
@@ -300,6 +330,10 @@ export const SLOT_INHERITANCE = {
     color: (mod, text) => brightness(20 * mod, text).rgb
   },
 
+  postLink: {
+    depends: ['link']
+  },
+
   border: {
     depends: ['fg'],
     opacity: 'border',
diff --git a/static/themes/kenomo.json b/static/themes/kenomo.json
new file mode 100644
index 00000000..98ddf974
--- /dev/null
+++ b/static/themes/kenomo.json
@@ -0,0 +1,71 @@
+{
+  "_pleroma_theme_version": 2,
+  "name": "Kenomo",
+  "source": {
+    "themeEngineVersion": 3,
+    "fonts": {},
+    "shadows": {
+      "panel": [],
+      "topBar": [],
+      "button": [
+        {
+          "x": 0,
+          "y": 1,
+          "blur": 0,
+          "spread": 0,
+          "color": "#FFFFFF",
+          "alpha": 0.2,
+          "inset": true
+        },
+        {
+          "x": 0,
+          "y": -1,
+          "blur": 0,
+          "spread": 0,
+          "color": "#000000",
+          "alpha": 0.2,
+          "inset": true
+        }
+      ],
+      "input": [
+        {
+          "x": 0,
+          "y": "0",
+          "blur": 0,
+          "spread": "1",
+          "color": "#576574",
+          "alpha": "1",
+          "inset": true
+        }
+      ]
+    },
+    "opacity": {
+      "underlay": "1",
+      "border": "0"
+    },
+    "colors": {
+      "bg": "#ffffff",
+      "fg": "#f6f6f6",
+      "text": "#494949",
+      "underlay": "#ffffff",
+      "link": "#818181",
+      "accent": "#818181",
+      "cBlue": "#2e86de",
+      "cRed": "#c96248",
+      "cGreen": "#0fa00f",
+      "cOrange": "#aa7623",
+      "postLink": "#2e86de",
+      "border": "#ffffff",
+      "icon": "#8a8a8a",
+      "panel": "transparent",
+      "topBarText": "#4b4b4b",
+      "tab": "--btn,-30",
+      "btn": "#576574"
+    },
+    "radii": {
+      "panel": "0",
+      "avatar": "6",
+      "avatarAlt": "6"
+    }
+  }
+}
-- 
cgit v1.2.3-70-g09d2


From b63e679a31a573c30868477f17322d6ed040af58 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 28 Jan 2020 02:03:21 +0200
Subject: removed one color TODO

---
 src/components/popper/popper.scss                |  2 ++
 src/components/style_switcher/style_switcher.vue | 29 +++++++++++++++++-------
 src/components/user_card/user_card.vue           |  5 ++--
 src/i18n/en.json                                 |  3 +++
 src/services/theme_data/pleromafe.js             | 20 +++++++++++++++-
 5 files changed, 47 insertions(+), 12 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
index 8e2dcd55..99b7e6fc 100644
--- a/src/components/popper/popper.scss
+++ b/src/components/popper/popper.scss
@@ -137,6 +137,8 @@
     width: 100%;
     height: 100%;
 
+    --btnText: var(--popoverText, $fallback--text);
+
     &-icon {
       padding-left: 0.5rem;
 
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 0b3b0fbf..9de3a96c 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -257,14 +257,6 @@
               :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
             />
             <ContrastRatio :contrast="previewContrast.bgPostLink" />
-            <ColorInput
-              v-model="iconColorLocal"
-              name="iconColor"
-              :fallback="previewTheme.colors.accent"
-              :label="$t('settings.links')"
-              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
-            />
-            <ContrastRatio :contrast="previewContrast.bgIcon" />
             <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
             <ColorInput
               v-model="alertErrorColorLocal"
@@ -298,6 +290,27 @@
               :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>
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 72656435..3988ff1c 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -465,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);
     }
   }
 
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 81dde663..06cc12f0 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -441,7 +441,10 @@
         "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",
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 74e6fc1c..354009ee 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -1,4 +1,4 @@
-import { brightness } from 'chromatism'
+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
@@ -574,6 +574,24 @@ export const SLOT_INHERITANCE = {
     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'],
-- 
cgit v1.2.3-70-g09d2


From 846285326900a497c68dc994ce06dcb1d5d1ca9c Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 28 Jan 2020 22:40:15 +0200
Subject: update button toggled state, apply it to emoji reactions

---
 src/App.scss                                       |  4 +++-
 src/components/emoji_reactions/emoji_reactions.vue |  8 +-------
 src/components/style_switcher/style_switcher.vue   | 13 +++++++++++++
 static/themes/redmond-xx-se.json                   |  2 ++
 static/themes/redmond-xx.json                      |  2 ++
 static/themes/redmond-xxi.json                     |  2 ++
 6 files changed, 23 insertions(+), 8 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index dd994a38..1fabaaa9 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -117,7 +117,9 @@ button {
     color: $fallback--text;
     color: var(--btnToggledText, $fallback--text);
     background-color: $fallback--fg;
-    background-color: var(--btnToggled, $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 {
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
index 00d6d2b7..f5d2e79a 100644
--- a/src/components/emoji_reactions/emoji_reactions.vue
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -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>
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 9de3a96c..5a42bba7 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -460,6 +460,19 @@
               :label="$t('settings.text')"
             />
             <ContrastRatio :contrast="previewContrast.btnDisabledText" />
+            <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')"
+            />
           </div>
           <div class="color-item">
             <h4>{{ $t('settings.style.advanced_colors.tabs') }}</h4>
diff --git a/static/themes/redmond-xx-se.json b/static/themes/redmond-xx-se.json
index 8deab7b7..7a4a29da 100644
--- a/static/themes/redmond-xx-se.json
+++ b/static/themes/redmond-xx-se.json
@@ -2,6 +2,7 @@
   "_pleroma_theme_version": 2,
   "name": "Redmond XX SE",
   "source": {
+    "themeEngineVersion": 3,
     "shadows": {
       "panel": [
         {
@@ -276,6 +277,7 @@
       "topBar": "#000080",
       "topBarLink": "#ffffff",
       "btn": "#c0c0c0",
+      "btnToggled": "--btn",
       "faint": "#3f3f3f",
       "faintLink": "#404080",
       "border": "#808080",
diff --git a/static/themes/redmond-xx.json b/static/themes/redmond-xx.json
index cdb89fe3..ff95b1e0 100644
--- a/static/themes/redmond-xx.json
+++ b/static/themes/redmond-xx.json
@@ -2,6 +2,7 @@
   "_pleroma_theme_version": 2,
   "name": "Redmond XX",
   "source": {
+    "themeEngineVersion": 3,
     "shadows": {
       "panel": [
         {
@@ -267,6 +268,7 @@
       "topBar": "#000080",
       "topBarLink": "#ffffff",
       "btn": "#c0c0c0",
+      "btnToggled": "--btn",
       "faint": "#3f3f3f",
       "faintLink": "#404080",
       "border": "#808080",
diff --git a/static/themes/redmond-xxi.json b/static/themes/redmond-xxi.json
index 79a2cb26..f788bdb8 100644
--- a/static/themes/redmond-xxi.json
+++ b/static/themes/redmond-xxi.json
@@ -2,6 +2,7 @@
   "_pleroma_theme_version": 2,
   "name": "Redmond XXI",
   "source": {
+    "themeEngineVersion": 3,
     "shadows": {
       "panel": [
         {
@@ -249,6 +250,7 @@
       "topBar": "#042967",
       "topBarLink": "#ffffff",
       "btn": "#d6d6ce",
+      "btnToggled": "--btn",
       "faint": "#3f3f3f",
       "faintLink": "#404080",
       "border": "#808080",
-- 
cgit v1.2.3-70-g09d2


From e46bb942260d192beb601727519ed90df5c62494 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 28 Jan 2020 23:45:14 +0200
Subject: updated preview to account for underlay

---
 src/App.scss                                     |   4 +-
 src/App.vue                                      |   2 +-
 src/components/style_switcher/preview.vue        | 188 ++++++++++++-----------
 src/components/style_switcher/style_switcher.vue |   4 +-
 4 files changed, 107 insertions(+), 91 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index 1fabaaa9..85fd0b47 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -31,9 +31,11 @@ h4 {
   margin: auto;
   min-height: 100vh;
   max-width: 980px;
+  align-content: flex-start;
+}
+.underlay {
   background-color: rgba(0,0,0,0.15);
   background-color: var(--underlay, rgba(0,0,0,0.15));
-  align-content: flex-start;
 }
 
 .text-center {
diff --git a/src/App.vue b/src/App.vue
index 1b1c2648..ff62fc51 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -78,7 +78,7 @@
     </nav>
     <div
       id="content"
-      class="container"
+      class="container underlay"
     >
       <div class="sidebar-flexer mobile-hidden">
         <div class="sidebar-bounds">
diff --git a/src/components/style_switcher/preview.vue b/src/components/style_switcher/preview.vue
index 101a32bd..5b8c3839 100644
--- a/src/components/style_switcher/preview.vue
+++ b/src/components/style_switcher/preview.vue
@@ -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>
+        <span class="alert error">
+          {{ $t('settings.style.preview.error') }}
         </span>
+        <button class="btn">
+          {{ $t('settings.style.preview.button') }}
+        </button>
       </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>
+      <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>
+            <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 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>
 
-      <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 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>
-      <div class="separator" />
+        <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>
+        <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>
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 5a42bba7..a3f9b488 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -116,9 +116,7 @@
       </div>
     </div>
 
-    <div class="preview-container">
-      <preview :style="previewRules" />
-    </div>
+    <preview :style="previewRules" />
 
     <keep-alive>
       <tab-switcher key="style-tweak">
-- 
cgit v1.2.3-70-g09d2


From 611da13a4b252c10f1613d70d877e2d039ba64b7 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 7 Feb 2020 01:25:26 +0200
Subject: Better Disabled buttons support. Mammal theme fixes. Implemented
 proper context-aware `mod` argument - now checks lightness of "variant"
 color. needs retesting tho

---
 src/App.scss                                     |  4 +-
 src/components/checkbox/checkbox.vue             |  4 +-
 src/components/style_switcher/style_switcher.vue | 57 ++++++++++++++++++++++++
 src/i18n/en.json                                 |  1 +
 src/services/style_setter/style_setter.js        | 17 ++-----
 src/services/theme_data/pleromafe.js             | 31 +++++++------
 src/services/theme_data/theme_data.service.js    |  9 ++--
 7 files changed, 89 insertions(+), 34 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index 85fd0b47..9f37ff12 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -179,7 +179,7 @@ input, textarea, .select, .input {
     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;
@@ -254,7 +254,7 @@ input, textarea, .select, .input {
     display: none;
     &:checked + label::before {
       color: $fallback--text;
-      color: var(--text, $fallback--text);
+      color: var(--inputText, $fallback--text);
     }
     &:disabled {
       &,
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index 1113f81d..03375b2f 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -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);
     }
 
   }
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index a3f9b488..705a60a2 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -430,6 +430,20 @@
               :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"
@@ -444,6 +458,20 @@
               :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"
@@ -458,6 +486,20 @@
               :label="$t('settings.text')"
             />
             <ContrastRatio :contrast="previewContrast.btnDisabledText" />
+            <ColorInput
+              v-model="btnDisabledPanelTextColorLocal"
+              name="btnDisabledPanelTextColor"
+              :fallback="previewTheme.colors.btnDisabledPanelText"
+              :label="$t('settings.style.advanced_colors.panel_header')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnDisabledPanelText" />
+            <ColorInput
+              v-model="btnDisabledTopBarTextColorLocal"
+              name="btnDisabledTopBarTextColor"
+              :fallback="previewTheme.colors.btnDisabledTopBarText"
+              :label="$t('settings.style.advanced_colors.top_bar')"
+            />
+            <ContrastRatio :contrast="previewContrast.btnDisabledTopBarText" />
             <h4>{{ $t('settings.style.advanced_colors.toggled') }}</h4>
             <ColorInput
               v-model="btnToggledColorLocal"
@@ -471,6 +513,21 @@
               :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>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4efa41c3..771805e9 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -472,6 +472,7 @@
         "selectedPost": "Selected post",
         "selectedMenu": "Selected menu item",
         "disabled": "Disabled",
+        "toggled": "Toggled",
         "tabs": "Tabs"
       },
       "radii": {
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 533145d4..b9a23ad7 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -114,10 +114,7 @@ export const generateColors = (themeData) => {
     ? colors2to3(themeData.colors || themeData)
     : themeData.colors || themeData
 
-  const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l
-  const mod = isLightOnDark ? 1 : -1
-
-  const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}, mod)
+  const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
 
   const htmlColors = Object.entries(colors)
     .reduce((acc, [k, v]) => {
@@ -381,25 +378,17 @@ export const getThemes = () => {
 }
 export const colors2to3 = (colors) => {
   return Object.entries(colors).reduce((acc, [slotName, color]) => {
-    const btnStates = ['', 'Pressed', 'Disabled', 'Toggled']
     const btnPositions = ['', 'Panel', 'TopBar']
     switch (slotName) {
       case 'lightBg':
         return { ...acc, highlight: color }
-      case 'btn':
-        return {
-          ...acc,
-          ...btnStates.reduce((stateAcc, state) => ({ ...stateAcc, ['btn' + state]: color }), {})
-        }
       case 'btnText':
         return {
           ...acc,
           ...btnPositions
-            .map(position => btnStates.map(state => state + position))
-            .flat()
             .reduce(
-              (statePositionAcc, statePosition) =>
-                ({ ...statePositionAcc, ['btn' + statePosition + 'Text']: color })
+              (statePositionAcc, position) =>
+                ({ ...statePositionAcc, ['btn' + position + 'Text']: color })
               , {}
             )
         }
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 354009ee..98fba5ef 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -85,6 +85,8 @@ export const SLOT_INHERITANCE = {
   },
   text: {
     depends: [],
+    layer: 'bg',
+    opacity: null,
     priority: 1
   },
   underlay: {
@@ -422,6 +424,7 @@ export const SLOT_INHERITANCE = {
   // Buttons
   btn: {
     depends: ['fg'],
+    variant: 'btn',
     opacity: 'btn'
   },
   btnText: {
@@ -430,20 +433,23 @@ export const SLOT_INHERITANCE = {
     textColor: true
   },
   btnPanelText: {
-    depends: ['panelText'],
+    depends: ['btnText'],
     layer: 'btnPanel',
     variant: 'btn',
     textColor: true
   },
   btnTopBarText: {
-    depends: ['topBarText'],
+    depends: ['btnText'],
     layer: 'btnTopBar',
     variant: 'btn',
     textColor: true
   },
 
   // Buttons: pressed
-  btnPressed: '--btn',
+  btnPressed: {
+    depends: ['btn'],
+    layer: 'btn'
+  },
   btnPressedText: {
     depends: ['btnText'],
     layer: 'btn',
@@ -451,7 +457,8 @@ export const SLOT_INHERITANCE = {
     textColor: true
   },
   btnPressedPanel: {
-    depends: ['btnPressed']
+    depends: ['btnPressed'],
+    layer: 'btn'
   },
   btnPressedPanelText: {
     depends: ['btnPanelText'],
@@ -469,6 +476,7 @@ export const SLOT_INHERITANCE = {
   // Buttons: toggled
   btnToggled: {
     depends: ['btn'],
+    layer: 'btn',
     color: (mod, btn) => brightness(mod * 20, btn).rgb
   },
   btnToggledText: {
@@ -496,25 +504,22 @@ export const SLOT_INHERITANCE = {
     color: (mod, btn, bg) => alphaBlend(btn, 0.5, bg)
   },
   btnDisabledText: {
-    depends: ['btnText'],
+    depends: ['btnText', 'btnDisabled'],
     layer: 'btn',
     variant: 'btnDisabled',
-    textColor: true,
-    color: (mod, text) => brightness(mod * -60, text).rgb
+    color: (mod, text, btn) => alphaBlend(text, 0.5, btn)
   },
   btnDisabledPanelText: {
-    depends: ['btnPanelText'],
+    depends: ['btnPanelText', 'btnDisabled'],
     layer: 'btnPanel',
     variant: 'btnDisabled',
-    textColor: true,
-    color: (mod, text) => brightness(mod * -60, text).rgb
+    color: (mod, text, btn) => alphaBlend(text, 0.5, btn)
   },
   btnDisabledTopBarText: {
-    depends: ['btnTopBarText'],
+    depends: ['btnTopBarText', 'btnDisabled'],
     layer: 'btnTopBar',
     variant: 'btnDisabled',
-    textColor: true,
-    color: (mod, text) => brightness(mod * -60, text).rgb
+    color: (mod, text, btn) => alphaBlend(text, 0.5, btn)
   },
 
   // Input fields
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js
index 49b99148..e9ed3781 100644
--- a/src/services/theme_data/theme_data.service.js
+++ b/src/services/theme_data/theme_data.service.js
@@ -255,14 +255,17 @@ export const computeDynamicColor = (sourceColor, getColor, mod) => {
 }
 
 /**
- * THE function you want to use. Takes provided colors and opacities, mod
+ * 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, mod) => SLOT_ORDERED.reduce(({ colors, opacity }, key) => {
+export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ colors, opacity }, key) => {
   const value = SLOT_INHERITANCE[key]
   const isObject = typeof value === 'object'
   const isString = typeof value === 'string'
   const sourceColor = sourceColors[key]
+  const variant = value.variant || value.layer || 'bg'
+  const isLightOnDark = relativeLuminance(colors[variant] || sourceColors[variant]) < 0.5
+  const mod = isLightOnDark ? 1 : -1
   let outputColor = null
   if (sourceColor) {
     // Color is defined in source color
@@ -318,7 +321,7 @@ export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.redu
           opacity
         )
       )
-      const isLightOnDark = relativeLuminance(bg) > 127
+      const isLightOnDark = relativeLuminance(bg) > 0.5
       const mod = isLightOnDark ? 1 : -1
 
       if (value.textColor === 'bw') {
-- 
cgit v1.2.3-70-g09d2


From 43bddc79e0abc35fd310d9a92051bb2d5b529984 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 11 Feb 2020 01:13:59 +0200
Subject: fix/remove contrast ratios

---
 src/components/style_switcher/style_switcher.vue | 6 +-----
 src/services/theme_data/pleromafe.js             | 6 +++++-
 2 files changed, 6 insertions(+), 6 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 705a60a2..205f325c 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -252,9 +252,8 @@
               name="postLinkColor"
               :fallback="previewTheme.colors.accent"
               :label="$t('settings.links')"
-              :show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
             />
-            <ContrastRatio :contrast="previewContrast.bgPostLink" />
+            <ContrastRatio :contrast="previewContrast.postLink" />
             <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
             <ColorInput
               v-model="alertErrorColorLocal"
@@ -485,21 +484,18 @@
               :fallback="previewTheme.colors.btnDisabledText"
               :label="$t('settings.text')"
             />
-            <ContrastRatio :contrast="previewContrast.btnDisabledText" />
             <ColorInput
               v-model="btnDisabledPanelTextColorLocal"
               name="btnDisabledPanelTextColor"
               :fallback="previewTheme.colors.btnDisabledPanelText"
               :label="$t('settings.style.advanced_colors.panel_header')"
             />
-            <ContrastRatio :contrast="previewContrast.btnDisabledPanelText" />
             <ColorInput
               v-model="btnDisabledTopBarTextColorLocal"
               name="btnDisabledTopBarTextColor"
               :fallback="previewTheme.colors.btnDisabledTopBarText"
               :label="$t('settings.style.advanced_colors.top_bar')"
             />
-            <ContrastRatio :contrast="previewContrast.btnDisabledTopBarText" />
             <h4>{{ $t('settings.style.advanced_colors.toggled') }}</h4>
             <ColorInput
               v-model="btnToggledColorLocal"
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 98fba5ef..9b33e4a9 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -329,11 +329,15 @@ export const SLOT_INHERITANCE = {
 
   lightText: {
     depends: ['text'],
+    layer: 'bg',
+    textColor: 'preserve',
     color: (mod, text) => brightness(20 * mod, text).rgb
   },
 
   postLink: {
-    depends: ['link']
+    depends: ['link'],
+    layer: 'bg',
+    textColor: 'preserve'
   },
 
   border: {
-- 
cgit v1.2.3-70-g09d2


From 171f4c7ddbc2f55136ff57742470ea9752e4da05 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 18 Feb 2020 00:01:48 +0200
Subject: update headers in switcher to better separate the subsections

---
 src/components/style_switcher/style_switcher.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 205f325c..62c8e634 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -443,7 +443,7 @@
               :label="$t('settings.style.advanced_colors.top_bar')"
             />
             <ContrastRatio :contrast="previewContrast.btnTopBarText" />
-            <h4>{{ $t('settings.style.advanced_colors.pressed') }}</h4>
+            <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
             <ColorInput
               v-model="btnPressedColorLocal"
               name="btnPressedColor"
@@ -471,7 +471,7 @@
               :label="$t('settings.style.advanced_colors.top_bar')"
             />
             <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" />
-            <h4>{{ $t('settings.style.advanced_colors.disabled') }}</h4>
+            <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
             <ColorInput
               v-model="btnDisabledColorLocal"
               name="btnDisabledColor"
@@ -496,7 +496,7 @@
               :fallback="previewTheme.colors.btnDisabledTopBarText"
               :label="$t('settings.style.advanced_colors.top_bar')"
             />
-            <h4>{{ $t('settings.style.advanced_colors.toggled') }}</h4>
+            <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
             <ColorInput
               v-model="btnToggledColorLocal"
               name="btnToggledColor"
-- 
cgit v1.2.3-70-g09d2


From bcebec478e43b3851e85c94335940e8fc7546cc8 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sun, 10 May 2020 06:46:06 +0300
Subject: moved stuff from settings, cleaned up naming for tabs, added close
 and peek

---
 src/App.scss                                       |  48 --
 src/components/modal/modal.vue                     |  16 +-
 src/components/settings/settings.js                | 128 ---
 src/components/settings/settings.vue               | 424 ---------
 src/components/settings_modal/settings_modal.js    |  42 +-
 src/components/settings_modal/settings_modal.scss  |  30 +-
 src/components/settings_modal/settings_modal.vue   |  26 +-
 .../settings_modal/tabs/data_import_export.js      |  65 --
 .../settings_modal/tabs/data_import_export.vue     |  43 -
 .../settings_modal/tabs/data_import_export_tab.js  |  65 ++
 .../settings_modal/tabs/data_import_export_tab.vue |  43 +
 .../settings_modal/tabs/filtering_tab.js           |  26 +
 .../settings_modal/tabs/filtering_tab.vue          |  86 ++
 src/components/settings_modal/tabs/general_tab.js  |  32 +
 src/components/settings_modal/tabs/general_tab.vue | 272 ++++++
 .../tabs/helpers/shared_computed_object.js         |  69 ++
 .../settings_modal/tabs/mutes_and_blocks.js        | 124 ---
 .../settings_modal/tabs/mutes_and_blocks.vue       | 173 ----
 .../settings_modal/tabs/mutes_and_blocks_tab.js    | 124 +++
 .../settings_modal/tabs/mutes_and_blocks_tab.vue   | 173 ++++
 .../settings_modal/tabs/notifications.js           |  27 -
 .../settings_modal/tabs/notifications.vue          |  42 -
 .../settings_modal/tabs/notifications_tab.js       |  27 +
 .../settings_modal/tabs/notifications_tab.vue      |  42 +
 src/components/settings_modal/tabs/profile.js      | 179 ----
 src/components/settings_modal/tabs/profile.scss    |  82 --
 src/components/settings_modal/tabs/profile.vue     | 213 -----
 src/components/settings_modal/tabs/profile_tab.js  | 179 ++++
 .../settings_modal/tabs/profile_tab.scss           |  82 ++
 src/components/settings_modal/tabs/profile_tab.vue | 213 +++++
 src/components/settings_modal/tabs/security.js     | 106 ---
 src/components/settings_modal/tabs/security.vue    | 143 ---
 .../settings_modal/tabs/security_tab/confirm.js    |   9 +
 .../settings_modal/tabs/security_tab/confirm.vue   |  22 +
 .../settings_modal/tabs/security_tab/mfa.js        | 155 ++++
 .../settings_modal/tabs/security_tab/mfa.vue       | 174 ++++
 .../tabs/security_tab/mfa_backup_codes.js          |  17 +
 .../tabs/security_tab/mfa_backup_codes.vue         |  35 +
 .../settings_modal/tabs/security_tab/mfa_totp.js   |  49 ++
 .../settings_modal/tabs/security_tab/mfa_totp.vue  |  43 +
 .../tabs/security_tab/security_tab.js              | 106 +++
 .../tabs/security_tab/security_tab.vue             | 143 +++
 .../settings_modal/tabs/theme_tab/preview.vue      | 117 +++
 .../settings_modal/tabs/theme_tab/theme_tab.js     | 759 ++++++++++++++++
 .../settings_modal/tabs/theme_tab/theme_tab.scss   | 339 ++++++++
 .../settings_modal/tabs/theme_tab/theme_tab.vue    | 956 +++++++++++++++++++++
 src/components/settings_modal/tabs/version_tab.js  |  24 +
 src/components/settings_modal/tabs/version_tab.vue |  31 +
 src/components/style_switcher/preview.vue          | 117 ---
 src/components/style_switcher/style_switcher.js    | 758 ----------------
 src/components/style_switcher/style_switcher.scss  | 335 --------
 src/components/style_switcher/style_switcher.vue   | 956 ---------------------
 src/components/tab_switcher/tab_switcher.scss      |   6 +
 src/components/user_card/user_card.vue             |  11 +-
 src/components/user_settings/confirm.js            |   9 -
 src/components/user_settings/confirm.vue           |  22 -
 src/components/user_settings/mfa.js                | 155 ----
 src/components/user_settings/mfa.vue               | 173 ----
 src/components/user_settings/mfa_backup_codes.js   |  17 -
 src/components/user_settings/mfa_backup_codes.vue  |  33 -
 src/components/user_settings/mfa_totp.js           |  49 --
 src/components/user_settings/mfa_totp.vue          |  43 -
 src/components/user_settings/user_settings.js      | 140 ---
 src/components/user_settings/user_settings.vue     | 119 ---
 src/i18n/en.json                                   |   4 +-
 65 files changed, 4509 insertions(+), 4761 deletions(-)
 delete mode 100644 src/components/settings/settings.js
 delete mode 100644 src/components/settings/settings.vue
 delete mode 100644 src/components/settings_modal/tabs/data_import_export.js
 delete mode 100644 src/components/settings_modal/tabs/data_import_export.vue
 create mode 100644 src/components/settings_modal/tabs/data_import_export_tab.js
 create mode 100644 src/components/settings_modal/tabs/data_import_export_tab.vue
 create mode 100644 src/components/settings_modal/tabs/filtering_tab.js
 create mode 100644 src/components/settings_modal/tabs/filtering_tab.vue
 create mode 100644 src/components/settings_modal/tabs/general_tab.js
 create mode 100644 src/components/settings_modal/tabs/general_tab.vue
 create mode 100644 src/components/settings_modal/tabs/helpers/shared_computed_object.js
 delete mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.js
 delete mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.vue
 create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks_tab.js
 create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
 delete mode 100644 src/components/settings_modal/tabs/notifications.js
 delete mode 100644 src/components/settings_modal/tabs/notifications.vue
 create mode 100644 src/components/settings_modal/tabs/notifications_tab.js
 create mode 100644 src/components/settings_modal/tabs/notifications_tab.vue
 delete mode 100644 src/components/settings_modal/tabs/profile.js
 delete mode 100644 src/components/settings_modal/tabs/profile.scss
 delete mode 100644 src/components/settings_modal/tabs/profile.vue
 create mode 100644 src/components/settings_modal/tabs/profile_tab.js
 create mode 100644 src/components/settings_modal/tabs/profile_tab.scss
 create mode 100644 src/components/settings_modal/tabs/profile_tab.vue
 delete mode 100644 src/components/settings_modal/tabs/security.js
 delete mode 100644 src/components/settings_modal/tabs/security.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/confirm.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/confirm.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_totp.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_totp.vue
 create mode 100644 src/components/settings_modal/tabs/security_tab/security_tab.js
 create mode 100644 src/components/settings_modal/tabs/security_tab/security_tab.vue
 create mode 100644 src/components/settings_modal/tabs/theme_tab/preview.vue
 create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.js
 create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.scss
 create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.vue
 create mode 100644 src/components/settings_modal/tabs/version_tab.js
 create mode 100644 src/components/settings_modal/tabs/version_tab.vue
 delete mode 100644 src/components/style_switcher/preview.vue
 delete mode 100644 src/components/style_switcher/style_switcher.js
 delete mode 100644 src/components/style_switcher/style_switcher.scss
 delete mode 100644 src/components/style_switcher/style_switcher.vue
 delete mode 100644 src/components/user_settings/confirm.js
 delete mode 100644 src/components/user_settings/confirm.vue
 delete mode 100644 src/components/user_settings/mfa.js
 delete mode 100644 src/components/user_settings/mfa.vue
 delete mode 100644 src/components/user_settings/mfa_backup_codes.js
 delete mode 100644 src/components/user_settings/mfa_backup_codes.vue
 delete mode 100644 src/components/user_settings/mfa_totp.js
 delete mode 100644 src/components/user_settings/mfa_totp.vue
 delete mode 100644 src/components/user_settings/user_settings.js
 delete mode 100644 src/components/user_settings/user_settings.vue

(limited to 'src/components/style_switcher/style_switcher.vue')

diff --git a/src/App.scss b/src/App.scss
index 7db9461c..120eea53 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -860,54 +860,6 @@ nav {
   }
 }
 
-// DELETE
-.setting-item {
-  border-bottom: 2px solid var(--fg, $fallback--fg);
-  margin: 1em 1em 1.4em;
-  padding-bottom: 1.4em;
-
-  > div {
-    margin-bottom: .5em;
-    &:last-child {
-      margin-bottom: 0;
-    }
-  }
-
-  &:last-child {
-    border-bottom: none;
-    padding-bottom: 0;
-    margin-bottom: 1em;
-  }
-
-  select {
-    min-width: 10em;
-  }
-
-
-  textarea {
-    width: 100%;
-    max-width: 100%;
-    height: 100px;
-  }
-
-  .unavailable,
-  .unavailable i {
-    color: var(--cRed, $fallback--cRed);
-    color: $fallback--cRed;
-  }
-
-  .btn {
-    min-height: 28px;
-    min-width: 10em;
-    padding: 0 2em;
-  }
-
-  .number-input {
-    max-width: 6em;
-  }
-}
-// DELETE
-
 .select-multiple {
   display: flex;
   .option-list {
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index cee24241..e5ecc0c0 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -3,6 +3,7 @@
     v-show="isOpen"
     v-body-scroll-lock="isOpen"
     class="modal-view"
+    :class="{ 'modal-background': !noBackground }"
     @click.self="$emit('backdropClicked')"
   >
     <slot />
@@ -15,6 +16,10 @@ export default {
     isOpen: {
       type: Boolean,
       default: true
+    },
+    noBackground: {
+      type: Boolean,
+      default: false
     }
   }
 }
@@ -32,10 +37,19 @@ export default {
   justify-content: center;
   align-items: center;
   overflow: auto;
+  pointer-events: none;
   animation-duration: 0.2s;
-  background-color: rgba(0, 0, 0, 0.5);
   animation-name: modal-background-fadein;
 
+  > * {
+    pointer-events: initial;
+  }
+
+  &.modal-background {
+    pointer-events: initial;
+    background-color: rgba(0, 0, 0, 0.5);
+  }
+
   body:not(.scroll-locked) & {
     opacity: 0;
   }
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
deleted file mode 100644
index 31a9e9be..00000000
--- a/src/components/settings/settings.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/* eslint-env browser */
-import { filter, trim } from 'lodash'
-
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import StyleSwitcher from '../style_switcher/style_switcher.vue'
-import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
-import { extractCommit } from '../../services/version/version.service'
-import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
-import Checkbox from '../checkbox/checkbox.vue'
-
-const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
-const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
-
-const multiChoiceProperties = [
-  'postContentType',
-  'subjectLineBehavior'
-]
-
-const settings = {
-  data () {
-    const instance = this.$store.state.instance
-
-    return {
-      loopSilentAvailable:
-        // Firefox
-        Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
-        // Chrome-likes
-        Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
-        // Future spec, still not supported in Nightly 63 as of 08/2018
-        Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
-
-      backendVersion: instance.backendVersion,
-      frontendVersion: instance.frontendVersion
-    }
-  },
-  components: {
-    TabSwitcher,
-    StyleSwitcher,
-    InterfaceLanguageSwitcher,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    },
-    currentSaveStateNotice () {
-      return this.$store.state.interface.settings.currentSaveStateNotice
-    },
-    postFormats () {
-      return this.$store.state.instance.postFormats || []
-    },
-    instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
-    frontendVersionLink () {
-      return pleromaFeCommitUrl + this.frontendVersion
-    },
-    backendVersionLink () {
-      return pleromaBeCommitUrl + extractCommit(this.backendVersion)
-    },
-    // Getting localized values for instance-default properties
-    ...instanceDefaultProperties
-      .filter(key => multiChoiceProperties.includes(key))
-      .map(key => [
-        key + 'DefaultValue',
-        function () {
-          return this.$store.getters.instanceDefaultConfig[key]
-        }
-      ])
-      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
-    ...instanceDefaultProperties
-      .filter(key => !multiChoiceProperties.includes(key))
-      .map(key => [
-        key + 'LocalizedValue',
-        function () {
-          return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
-        }
-      ])
-      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
-    // Generating computed values for vuex properties
-    ...Object.keys(configDefaultState)
-      .map(key => [key, {
-        get () { return this.$store.getters.mergedConfig[key] },
-        set (value) {
-          this.$store.dispatch('setOption', { name: key, value })
-        }
-      }])
-      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
-    // Special cases (need to transform values or perform actions first)
-    muteWordsString: {
-      get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
-      set (value) {
-        this.$store.dispatch('setOption', {
-          name: 'muteWords',
-          value: filter(value.split('\n'), (word) => trim(word).length > 0)
-        })
-      }
-    },
-    useStreamingApi: {
-      get () { return this.$store.getters.mergedConfig.useStreamingApi },
-      set (value) {
-        const promise = value
-          ? this.$store.dispatch('enableMastoSockets')
-          : this.$store.dispatch('disableMastoSockets')
-
-        promise.then(() => {
-          this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
-        }).catch((e) => {
-          console.error('Failed starting MastoAPI Streaming socket', e)
-          this.$store.dispatch('disableMastoSockets')
-          this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
-        })
-      }
-    }
-  },
-  // Updating nested properties
-  watch: {
-    notificationVisibility: {
-      handler (value) {
-        this.$store.dispatch('setOption', {
-          name: 'notificationVisibility',
-          value: this.$store.getters.mergedConfig.notificationVisibility
-        })
-      },
-      deep: true
-    }
-  }
-}
-
-export default settings
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
deleted file mode 100644
index 9e14b449..00000000
--- a/src/components/settings/settings.vue
+++ /dev/null
@@ -1,424 +0,0 @@
-<template>
-  <div class="settings panel panel-default">
-    <div class="panel-heading">
-      <div class="title">
-        {{ $t('settings.settings') }}
-      </div>
-
-      <transition name="fade">
-        <template v-if="currentSaveStateNotice">
-          <div
-            v-if="currentSaveStateNotice.error"
-            class="alert error"
-            @click.prevent
-          >
-            {{ $t('settings.saving_err') }}
-          </div>
-
-          <div
-            v-if="!currentSaveStateNotice.error"
-            class="alert transparent"
-            @click.prevent
-          >
-            {{ $t('settings.saving_ok') }}
-          </div>
-        </template>
-      </transition>
-    </div>
-    <div class="panel-body">
-      <keep-alive>
-        <tab-switcher>
-          <div :label="$t('settings.general')">
-            <div class="setting-item">
-              <h2>{{ $t('settings.interface') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <interface-language-switcher />
-                </li>
-                <li v-if="instanceSpecificPanelPresent">
-                  <Checkbox v-model="hideISP">
-                    {{ $t('settings.hide_isp') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-            <div class="setting-item">
-              <h2>{{ $t('nav.timeline') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="hideMutedPosts">
-                    {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="collapseMessageWithSubject">
-                    {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="streaming">
-                    {{ $t('settings.streaming') }}
-                  </Checkbox>
-                  <ul
-                    class="setting-list suboptions"
-                    :class="[{disabled: !streaming}]"
-                  >
-                    <li>
-                      <Checkbox
-                        v-model="pauseOnUnfocused"
-                        :disabled="!streaming"
-                      >
-                        {{ $t('settings.pause_on_unfocused') }}
-                      </Checkbox>
-                    </li>
-                  </ul>
-                </li>
-                <li>
-                  <Checkbox v-model="useStreamingApi">
-                    {{ $t('settings.useStreamingApi') }}
-                    <br>
-                    <small>
-                      {{ $t('settings.useStreamingApiWarning') }}
-                    </small>
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="autoLoad">
-                    {{ $t('settings.autoload') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="hoverPreview">
-                    {{ $t('settings.reply_link_preview') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="emojiReactionsOnTimeline">
-                    {{ $t('settings.emoji_reactions_on_timeline') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-
-            <div class="setting-item">
-              <h2>{{ $t('settings.composing') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="scopeCopy">
-                    {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="alwaysShowSubjectInput">
-                    {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <div>
-                    {{ $t('settings.subject_line_behavior') }}
-                    <label
-                      for="subjectLineBehavior"
-                      class="select"
-                    >
-                      <select
-                        id="subjectLineBehavior"
-                        v-model="subjectLineBehavior"
-                      >
-                        <option value="email">
-                          {{ $t('settings.subject_line_email') }}
-                          {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
-                        </option>
-                        <option value="masto">
-                          {{ $t('settings.subject_line_mastodon') }}
-                          {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
-                        </option>
-                        <option value="noop">
-                          {{ $t('settings.subject_line_noop') }}
-                          {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
-                        </option>
-                      </select>
-                      <i class="icon-down-open" />
-                    </label>
-                  </div>
-                </li>
-                <li v-if="postFormats.length > 0">
-                  <div>
-                    {{ $t('settings.post_status_content_type') }}
-                    <label
-                      for="postContentType"
-                      class="select"
-                    >
-                      <select
-                        id="postContentType"
-                        v-model="postContentType"
-                      >
-                        <option
-                          v-for="postFormat in postFormats"
-                          :key="postFormat"
-                          :value="postFormat"
-                        >
-                          {{ $t(`post_status.content_type["${postFormat}"]`) }}
-                          {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
-                        </option>
-                      </select>
-                      <i class="icon-down-open" />
-                    </label>
-                  </div>
-                </li>
-                <li>
-                  <Checkbox v-model="minimalScopesMode">
-                    {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="autohideFloatingPostButton">
-                    {{ $t('settings.autohide_floating_post_button') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="padEmoji">
-                    {{ $t('settings.pad_emoji') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-
-            <div class="setting-item">
-              <h2>{{ $t('settings.attachments') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="hideAttachments">
-                    {{ $t('settings.hide_attachments_in_tl') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="hideAttachmentsInConv">
-                    {{ $t('settings.hide_attachments_in_convo') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <label for="maxThumbnails">
-                    {{ $t('settings.max_thumbnails') }}
-                  </label>
-                  <input
-                    id="maxThumbnails"
-                    v-model.number="maxThumbnails"
-                    class="number-input"
-                    type="number"
-                    min="0"
-                    step="1"
-                  >
-                </li>
-                <li>
-                  <Checkbox v-model="hideNsfw">
-                    {{ $t('settings.nsfw_clickthrough') }}
-                  </Checkbox>
-                </li>
-                <ul class="setting-list suboptions">
-                  <li>
-                    <Checkbox
-                      v-model="preloadImage"
-                      :disabled="!hideNsfw"
-                    >
-                      {{ $t('settings.preload_images') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox
-                      v-model="useOneClickNsfw"
-                      :disabled="!hideNsfw"
-                    >
-                      {{ $t('settings.use_one_click_nsfw') }}
-                    </Checkbox>
-                  </li>
-                </ul>
-                <li>
-                  <Checkbox v-model="stopGifs">
-                    {{ $t('settings.stop_gifs') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="loopVideo">
-                    {{ $t('settings.loop_video') }}
-                  </Checkbox>
-                  <ul
-                    class="setting-list suboptions"
-                    :class="[{disabled: !streaming}]"
-                  >
-                    <li>
-                      <Checkbox
-                        v-model="loopVideoSilentOnly"
-                        :disabled="!loopVideo || !loopSilentAvailable"
-                      >
-                        {{ $t('settings.loop_video_silent_only') }}
-                      </Checkbox>
-                      <div
-                        v-if="!loopSilentAvailable"
-                        class="unavailable"
-                      >
-                        <i class="icon-globe" />! {{ $t('settings.limited_availability') }}
-                      </div>
-                    </li>
-                  </ul>
-                </li>
-                <li>
-                  <Checkbox v-model="playVideosInModal">
-                    {{ $t('settings.play_videos_in_modal') }}
-                  </Checkbox>
-                </li>
-                <li>
-                  <Checkbox v-model="useContainFit">
-                    {{ $t('settings.use_contain_fit') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-
-            <div class="setting-item">
-              <h2>{{ $t('settings.notifications') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="webPushNotifications">
-                    {{ $t('settings.enable_web_push_notifications') }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-
-            <div class="setting-item">
-              <h2>{{ $t('settings.fun') }}</h2>
-              <ul class="setting-list">
-                <li>
-                  <Checkbox v-model="greentext">
-                    {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
-                  </Checkbox>
-                </li>
-              </ul>
-            </div>
-          </div>
-
-          <div :label="$t('settings.theme')">
-            <div class="setting-item">
-              <style-switcher />
-            </div>
-          </div>
-
-          <div :label="$t('settings.filtering')">
-            <div class="setting-item">
-              <div class="select-multiple">
-                <span class="label">{{ $t('settings.notification_visibility') }}</span>
-                <ul class="option-list">
-                  <li>
-                    <Checkbox v-model="notificationVisibility.likes">
-                      {{ $t('settings.notification_visibility_likes') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.repeats">
-                      {{ $t('settings.notification_visibility_repeats') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.follows">
-                      {{ $t('settings.notification_visibility_follows') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.mentions">
-                      {{ $t('settings.notification_visibility_mentions') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.moves">
-                      {{ $t('settings.notification_visibility_moves') }}
-                    </Checkbox>
-                  </li>
-                  <li>
-                    <Checkbox v-model="notificationVisibility.emojiReactions">
-                      {{ $t('settings.notification_visibility_emoji_reactions') }}
-                    </Checkbox>
-                  </li>
-                </ul>
-              </div>
-              <div>
-                {{ $t('settings.replies_in_timeline') }}
-                <label
-                  for="replyVisibility"
-                  class="select"
-                >
-                  <select
-                    id="replyVisibility"
-                    v-model="replyVisibility"
-                  >
-                    <option
-                      value="all"
-                      selected
-                    >{{ $t('settings.reply_visibility_all') }}</option>
-                    <option value="following">{{ $t('settings.reply_visibility_following') }}</option>
-                    <option value="self">{{ $t('settings.reply_visibility_self') }}</option>
-                  </select>
-                  <i class="icon-down-open" />
-                </label>
-              </div>
-              <div>
-                <Checkbox v-model="hidePostStats">
-                  {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
-                </Checkbox>
-              </div>
-              <div>
-                <Checkbox v-model="hideUserStats">
-                  {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
-                </Checkbox>
-              </div>
-            </div>
-            <div class="setting-item">
-              <div>
-                <p>{{ $t('settings.filtering_explanation') }}</p>
-                <textarea
-                  id="muteWords"
-                  v-model="muteWordsString"
-                />
-              </div>
-              <div>
-                <Checkbox v-model="hideFilteredStatuses">
-                  {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
-                </Checkbox>
-              </div>
-            </div>
-          </div>
-          <div :label="$t('settings.version.title')">
-            <div class="setting-item">
-              <ul class="setting-list">
-                <li>
-                  <p>{{ $t('settings.version.backend_version') }}</p>
-                  <ul class="option-list">
-                    <li>
-                      <a
-                        :href="backendVersionLink"
-                        target="_blank"
-                      >{{ backendVersion }}</a>
-                    </li>
-                  </ul>
-                </li>
-                <li>
-                  <p>{{ $t('settings.version.frontend_version') }}</p>
-                  <ul class="option-list">
-                    <li>
-                      <a
-                        :href="frontendVersionLink"
-                        target="_blank"
-                      >{{ frontendVersion }}</a>
-                    </li>
-                  </ul>
-                </li>
-              </ul>
-            </div>
-          </div>
-        </tab-switcher>
-      </keep-alive>
-    </div>
-  </div>
-</template>
-
-<script src="./settings.js">
-</script>
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 1f4c038f..d60babf6 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -1,21 +1,30 @@
-import Modal from '../modal/modal.vue'
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
+import Modal from 'src/components/modal/modal.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
 
-import Profile from './tabs/profile.vue'
-import Security from './tabs/security.vue'
-import Notifications from './tabs/notifications.vue'
-import DataImportExport from './tabs/data_import_export.vue'
-import MutesAndBlocks from './tabs/mutes_and_blocks.vue'
+import DataImportExportTab from './tabs/data_import_export_tab.vue'
+import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
+import NotificationsTab from './tabs/notifications_tab.vue'
+import FilteringTab from './tabs/filtering_tab.vue'
+import SecurityTab from './tabs/security_tab/security_tab.vue'
+import ProfileTab from './tabs/profile_tab.vue'
+import GeneralTab from './tabs/general_tab.vue'
+import VersionTab from './tabs/version_tab.vue'
+import ThemeTab from './tabs/theme_tab/theme_tab.vue'
 
 const SettingsModal = {
   components: {
     Modal,
     TabSwitcher,
-    Profile,
-    Security,
-    Notifications,
-    DataImportExport,
-    MutesAndBlocks
+
+    DataImportExportTab,
+    MutesAndBlocksTab,
+    NotificationsTab,
+    FilteringTab,
+    SecurityTab,
+    ProfileTab,
+    GeneralTab,
+    VersionTab,
+    ThemeTab
   },
   data () {
     return {
@@ -28,11 +37,20 @@ const SettingsModal = {
     },
     modalActivated () {
       return this.$store.state.interface.settingsModalState !== 'hidden'
+    },
+    modalPeeked () {
+      return this.$store.state.interface.settingsModalState === 'minimized'
     }
   },
   watch: {
   },
   methods: {
+    closeModal () {
+      this.$store.dispatch('closeSettingsModal')
+    },
+    peekModal () {
+      this.$store.dispatch('togglePeekSettingsModal')
+    }
   }
 }
 
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 8cea52d2..3efbe205 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -1,14 +1,29 @@
-@import '../../_variables.scss';
+@import 'src/_variables.scss';
 .settings-modal {
+
   .settings_tab-switcher {
     height: 100%;
   }
+  &.peek {
+    .settings-modal-panel {
+      transform: translateY(calc(100% - 50px));
+    }
+  }
+
   .settings-modal-panel {
+    transition: transform;
+    transition-timing-function: ease-in-out;
+    transition-duration: 300ms;
     width: 1000px;
     max-width: 90vw;
     height: 90vh;
+    @media all and (max-width: 800px) {
+      max-width: 100vw;
+      height: 100vh;
+    }
   }
   .panel-body {
+    height: 100%;
     overflow-y: hidden;
   }
   .setting-item {
@@ -16,6 +31,12 @@
     margin: 1em 1em 1.4em;
     padding-bottom: 1.4em;
 
+    .btn {
+      min-height: 28px;
+      min-width: 10em;
+      padding: 0 2em;
+    }
+
     > div {
       margin-bottom: .5em;
       &:last-child {
@@ -33,7 +54,6 @@
       min-width: 10em;
     }
 
-
     textarea {
       width: 100%;
       max-width: 100%;
@@ -46,12 +66,6 @@
       color: $fallback--cRed;
     }
 
-    .btn {
-      min-height: 28px;
-      min-width: 10em;
-      padding: 0 2em;
-    }
-
     .number-input {
       max-width: 6em;
     }
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 9e35d3f6..53481bdd 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -3,10 +3,20 @@
   v-if="isLoggedIn && !resettingForm"
   :is-open="modalActivated"
   class="settings-modal"
+  :class="{ peek: modalPeeked }"
+  :no-background="modalPeeked"
   >
   <div class="settings-modal-panel panel">
     <div class="panel-heading">
-      {{ $t('settings.settings') }}
+      <span class="title">
+        {{ $t('settings.settings') }}
+      </span>
+      <button class="btn" @click="peekModal">
+        {{ $t('general.peek') }}
+      </button>
+      <button class="btn" @click="closeModal">
+        {{ $t('general.close') }}
+      </button>
     </div>
     <div class="panel-body">
       <tab-switcher
@@ -15,11 +25,15 @@
         :scrollableTabs="true"
         ref="tabSwitcher"
         >
-        <div :label="$t('settings.profile_tab')"><Profile /></div>
-        <div :label="$t('settings.security_tab')"><Security /></div>
-        <div :label="$t('settings.notifications')"><Notifications /></div>
-        <div :label="$t('settings.data_import_export_tab')"><DataImportExport /></div>
-        <div :label="$t('settings.mutes_and_blocks')"><MutesAndBlocks /></div>
+        <div :label="$t('settings.general')"><GeneralTab /></div>
+        <div :label="$t('settings.profile_tab')"><ProfileTab /></div>
+        <div :label="$t('settings.security_tab')"><SecurityTab /></div>
+        <div :label="$t('settings.filtering')"><FilteringTab /></div>
+        <div :label="$t('settings.theme')"><ThemeTab /></div>
+        <div :label="$t('settings.notifications')"><NotificationsTab /></div>
+        <div :label="$t('settings.data_import_export_tab')"><DataImportExportTab /></div>
+        <div :label="$t('settings.mutes_and_blocks')"><MutesAndBlocksTab /></div>
+        <div :label="$t('settings.version.title')"><VersionTab /></div>
       </tab-switcher>
     </div>
   </div>
diff --git a/src/components/settings_modal/tabs/data_import_export.js b/src/components/settings_modal/tabs/data_import_export.js
deleted file mode 100644
index f68d12e9..00000000
--- a/src/components/settings_modal/tabs/data_import_export.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import Importer from '../../importer/importer.vue'
-import Exporter from '../../exporter/exporter.vue'
-import Checkbox from '../../checkbox/checkbox.vue'
-
-const DataImportExport = {
-  data () {
-    return {
-      activeTab: 'profile',
-      newDomainToMute: ''
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    Importer,
-    Exporter,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    }
-  },
-  methods: {
-    getFollowsContent () {
-      return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
-        .then(this.generateExportableUsersContent)
-    },
-    getBlocksContent () {
-      return this.$store.state.api.backendInteractor.fetchBlocks()
-        .then(this.generateExportableUsersContent)
-    },
-    importFollows (file) {
-      return this.$store.state.api.backendInteractor.importFollows({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    importBlocks (file) {
-      return this.$store.state.api.backendInteractor.importBlocks({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    generateExportableUsersContent (users) {
-      // Get addresses
-      return users.map((user) => {
-        // check is it's a local user
-        if (user && user.is_local) {
-          // append the instance address
-          // eslint-disable-next-line no-undef
-          return user.screen_name + '@' + location.hostname
-        }
-        return user.screen_name
-      }).join('\n')
-    }
-  }
-}
-
-export default DataImportExport
diff --git a/src/components/settings_modal/tabs/data_import_export.vue b/src/components/settings_modal/tabs/data_import_export.vue
deleted file mode 100644
index 464df6d3..00000000
--- a/src/components/settings_modal/tabs/data_import_export.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-<div
-  :label="$t('settings.data_import_export_tab')"
-  >
-  <div class="setting-item">
-    <h2>{{ $t('settings.follow_import') }}</h2>
-    <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
-    <Importer
-      :submit-handler="importFollows"
-      :success-message="$t('settings.follows_imported')"
-      :error-message="$t('settings.follow_import_error')"
-      />
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.follow_export') }}</h2>
-    <Exporter
-      :get-content="getFollowsContent"
-      filename="friends.csv"
-      :export-button-label="$t('settings.follow_export_button')"
-      />
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.block_import') }}</h2>
-    <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
-    <Importer
-      :submit-handler="importBlocks"
-      :success-message="$t('settings.blocks_imported')"
-      :error-message="$t('settings.block_import_error')"
-      />
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.block_export') }}</h2>
-    <Exporter
-      :get-content="getBlocksContent"
-      filename="blocks.csv"
-      :export-button-label="$t('settings.block_export_button')"
-      />
-  </div>
-</div>
-</template>
-
-<script src="./data_import_export.js"></script>
-<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js
new file mode 100644
index 00000000..168f89e1
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export_tab.js
@@ -0,0 +1,65 @@
+import Importer from 'src/components/importer/importer.vue'
+import Exporter from 'src/components/exporter/exporter.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const DataImportExportTab = {
+  data () {
+    return {
+      activeTab: 'profile',
+      newDomainToMute: ''
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    Importer,
+    Exporter,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    }
+  },
+  methods: {
+    getFollowsContent () {
+      return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
+        .then(this.generateExportableUsersContent)
+    },
+    getBlocksContent () {
+      return this.$store.state.api.backendInteractor.fetchBlocks()
+        .then(this.generateExportableUsersContent)
+    },
+    importFollows (file) {
+      return this.$store.state.api.backendInteractor.importFollows({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    importBlocks (file) {
+      return this.$store.state.api.backendInteractor.importBlocks({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    generateExportableUsersContent (users) {
+      // Get addresses
+      return users.map((user) => {
+        // check is it's a local user
+        if (user && user.is_local) {
+          // append the instance address
+          // eslint-disable-next-line no-undef
+          return user.screen_name + '@' + location.hostname
+        }
+        return user.screen_name
+      }).join('\n')
+    }
+  }
+}
+
+export default DataImportExportTab
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue
new file mode 100644
index 00000000..3ddc8b03
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export_tab.vue
@@ -0,0 +1,43 @@
+<template>
+<div
+  :label="$t('settings.data_import_export_tab')"
+  >
+  <div class="setting-item">
+    <h2>{{ $t('settings.follow_import') }}</h2>
+    <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
+    <Importer
+      :submit-handler="importFollows"
+      :success-message="$t('settings.follows_imported')"
+      :error-message="$t('settings.follow_import_error')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.follow_export') }}</h2>
+    <Exporter
+      :get-content="getFollowsContent"
+      filename="friends.csv"
+      :export-button-label="$t('settings.follow_export_button')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.block_import') }}</h2>
+    <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
+    <Importer
+      :submit-handler="importBlocks"
+      :success-message="$t('settings.blocks_imported')"
+      :error-message="$t('settings.block_import_error')"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.block_export') }}</h2>
+    <Exporter
+      :get-content="getBlocksContent"
+      filename="blocks.csv"
+      :export-button-label="$t('settings.block_export_button')"
+      />
+  </div>
+</div>
+</template>
+
+<script src="./data_import_export_tab.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
new file mode 100644
index 00000000..ec330667
--- /dev/null
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -0,0 +1,26 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+import SharedComputedObject from './helpers/shared_computed_object.js'
+
+const FilteringTab = {
+  components: {
+    Checkbox
+  },
+  computed: {
+    ...SharedComputedObject()
+  },
+  // Updating nested properties
+  watch: {
+    notificationVisibility: {
+      handler (value) {
+        this.$store.dispatch('setOption', {
+          name: 'notificationVisibility',
+          value: this.$store.getters.mergedConfig.notificationVisibility
+        })
+      },
+      deep: true
+    }
+  }
+}
+
+export default FilteringTab
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
new file mode 100644
index 00000000..647ec7b4
--- /dev/null
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -0,0 +1,86 @@
+<template>
+<div :label="$t('settings.filtering')">
+  <div class="setting-item">
+    <div class="select-multiple">
+      <span class="label">{{ $t('settings.notification_visibility') }}</span>
+      <ul class="option-list">
+        <li>
+          <Checkbox v-model="notificationVisibility.likes">
+            {{ $t('settings.notification_visibility_likes') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.repeats">
+            {{ $t('settings.notification_visibility_repeats') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.follows">
+            {{ $t('settings.notification_visibility_follows') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.mentions">
+            {{ $t('settings.notification_visibility_mentions') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.moves">
+            {{ $t('settings.notification_visibility_moves') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationVisibility.emojiReactions">
+            {{ $t('settings.notification_visibility_emoji_reactions') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+    <div>
+      {{ $t('settings.replies_in_timeline') }}
+      <label
+        for="replyVisibility"
+        class="select"
+        >
+        <select
+          id="replyVisibility"
+          v-model="replyVisibility"
+          >
+          <option
+            value="all"
+            selected
+            >{{ $t('settings.reply_visibility_all') }}</option>
+          <option value="following">{{ $t('settings.reply_visibility_following') }}</option>
+          <option value="self">{{ $t('settings.reply_visibility_self') }}</option>
+        </select>
+        <i class="icon-down-open" />
+      </label>
+    </div>
+    <div>
+      <Checkbox v-model="hidePostStats">
+        {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
+      </Checkbox>
+    </div>
+    <div>
+      <Checkbox v-model="hideUserStats">
+        {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
+      </Checkbox>
+    </div>
+  </div>
+  <div class="setting-item">
+    <div>
+      <p>{{ $t('settings.filtering_explanation') }}</p>
+      <textarea
+        id="muteWords"
+        v-model="muteWordsString"
+        />
+    </div>
+    <div>
+      <Checkbox v-model="hideFilteredStatuses">
+        {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
+      </Checkbox>
+    </div>
+  </div>
+</div>
+</template>
+<script src="./filtering_tab.js"></script>
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
new file mode 100644
index 00000000..82bf6862
--- /dev/null
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -0,0 +1,32 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
+
+import SharedComputedObject from './helpers/shared_computed_object.js'
+
+const GeneralTab = {
+  data () {
+    const instance = this.$store.state.instance
+    return {
+      loopSilentAvailable:
+      // Firefox
+      Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
+      // Chrome-likes
+      Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
+      // Future spec, still not supported in Nightly 63 as of 08/2018
+      Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
+    }
+  },
+  components: {
+    Checkbox,
+    InterfaceLanguageSwitcher
+  },
+  computed: {
+    postFormats () {
+      return this.$store.state.instance.postFormats || []
+    },
+    instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
+    ...SharedComputedObject()
+  }
+}
+
+export default GeneralTab
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
new file mode 100644
index 00000000..0d2da07a
--- /dev/null
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -0,0 +1,272 @@
+<template>
+<div :label="$t('settings.general')">
+  <div class="setting-item">
+    <h2>{{ $t('settings.interface') }}</h2>
+    <ul class="setting-list">
+      <li>
+        <interface-language-switcher />
+      </li>
+      <li v-if="instanceSpecificPanelPresent">
+        <Checkbox v-model="hideISP">
+          {{ $t('settings.hide_isp') }}
+        </Checkbox>
+      </li>
+    </ul>
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('nav.timeline') }}</h2>
+    <ul class="setting-list">
+      <li>
+        <Checkbox v-model="hideMutedPosts">
+          {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="collapseMessageWithSubject">
+          {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="streaming">
+          {{ $t('settings.streaming') }}
+        </Checkbox>
+        <ul
+          class="setting-list suboptions"
+          :class="[{disabled: !streaming}]"
+          >
+          <li>
+            <Checkbox
+              v-model="pauseOnUnfocused"
+              :disabled="!streaming"
+              >
+              {{ $t('settings.pause_on_unfocused') }}
+            </Checkbox>
+          </li>
+        </ul>
+      </li>
+      <li>
+        <Checkbox v-model="useStreamingApi">
+          {{ $t('settings.useStreamingApi') }}
+          <br>
+          <small>
+            {{ $t('settings.useStreamingApiWarning') }}
+          </small>
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="autoLoad">
+          {{ $t('settings.autoload') }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="hoverPreview">
+          {{ $t('settings.reply_link_preview') }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="emojiReactionsOnTimeline">
+          {{ $t('settings.emoji_reactions_on_timeline') }}
+        </Checkbox>
+      </li>
+    </ul>
+  </div>
+
+  <div class="setting-item">
+    <h2>{{ $t('settings.composing') }}</h2>
+    <ul class="setting-list">
+      <li>
+        <Checkbox v-model="scopeCopy">
+          {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="alwaysShowSubjectInput">
+          {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <div>
+          {{ $t('settings.subject_line_behavior') }}
+          <label
+            for="subjectLineBehavior"
+            class="select"
+            >
+            <select
+              id="subjectLineBehavior"
+              v-model="subjectLineBehavior"
+              >
+              <option value="email">
+                {{ $t('settings.subject_line_email') }}
+                {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
+              </option>
+              <option value="masto">
+                {{ $t('settings.subject_line_mastodon') }}
+                {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
+              </option>
+              <option value="noop">
+                {{ $t('settings.subject_line_noop') }}
+                {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
+              </option>
+            </select>
+            <i class="icon-down-open" />
+          </label>
+        </div>
+      </li>
+      <li v-if="postFormats.length > 0">
+        <div>
+          {{ $t('settings.post_status_content_type') }}
+          <label
+            for="postContentType"
+            class="select"
+            >
+            <select
+              id="postContentType"
+              v-model="postContentType"
+              >
+              <option
+                v-for="postFormat in postFormats"
+                :key="postFormat"
+                :value="postFormat"
+                >
+                {{ $t(`post_status.content_type["${postFormat}"]`) }}
+                {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
+              </option>
+            </select>
+            <i class="icon-down-open" />
+          </label>
+        </div>
+      </li>
+      <li>
+        <Checkbox v-model="minimalScopesMode">
+          {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="autohideFloatingPostButton">
+          {{ $t('settings.autohide_floating_post_button') }}
+        </Checkbox>
+      </li>
+      <li>
+        <Checkbox v-model="padEmoji">
+          {{ $t('settings.pad_emoji') }}
+        </Checkbox>
+      </li>
+    </ul>
+  </div>
+
+  <div class="setting-item">
+      <h2>{{ $t('settings.attachments') }}</h2>
+      <ul class="setting-list">
+        <li>
+          <Checkbox v-model="hideAttachments">
+            {{ $t('settings.hide_attachments_in_tl') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="hideAttachmentsInConv">
+            {{ $t('settings.hide_attachments_in_convo') }}
+          </Checkbox>
+        </li>
+        <li>
+          <label for="maxThumbnails">
+            {{ $t('settings.max_thumbnails') }}
+          </label>
+          <input
+            id="maxThumbnails"
+            v-model.number="maxThumbnails"
+            class="number-input"
+            type="number"
+            min="0"
+            step="1"
+            >
+        </li>
+        <li>
+          <Checkbox v-model="hideNsfw">
+            {{ $t('settings.nsfw_clickthrough') }}
+          </Checkbox>
+        </li>
+        <ul class="setting-list suboptions">
+          <li>
+            <Checkbox
+              v-model="preloadImage"
+              :disabled="!hideNsfw"
+              >
+              {{ $t('settings.preload_images') }}
+            </Checkbox>
+          </li>
+          <li>
+            <Checkbox
+              v-model="useOneClickNsfw"
+              :disabled="!hideNsfw"
+              >
+              {{ $t('settings.use_one_click_nsfw') }}
+            </Checkbox>
+          </li>
+        </ul>
+        <li>
+          <Checkbox v-model="stopGifs">
+            {{ $t('settings.stop_gifs') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="loopVideo">
+            {{ $t('settings.loop_video') }}
+          </Checkbox>
+          <ul
+            class="setting-list suboptions"
+            :class="[{disabled: !streaming}]"
+            >
+            <li>
+              <Checkbox
+                v-model="loopVideoSilentOnly"
+                :disabled="!loopVideo || !loopSilentAvailable"
+                >
+                {{ $t('settings.loop_video_silent_only') }}
+              </Checkbox>
+              <div
+                v-if="!loopSilentAvailable"
+                class="unavailable"
+                >
+                <i class="icon-globe" />! {{ $t('settings.limited_availability') }}
+              </div>
+            </li>
+          </ul>
+        </li>
+        <li>
+          <Checkbox v-model="playVideosInModal">
+            {{ $t('settings.play_videos_in_modal') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="useContainFit">
+            {{ $t('settings.use_contain_fit') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+
+    <div class="setting-item">
+      <h2>{{ $t('settings.notifications') }}</h2>
+      <ul class="setting-list">
+        <li>
+          <Checkbox v-model="webPushNotifications">
+            {{ $t('settings.enable_web_push_notifications') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+
+    <div class="setting-item">
+      <h2>{{ $t('settings.fun') }}</h2>
+      <ul class="setting-list">
+        <li>
+          <Checkbox v-model="greentext">
+            {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script src="./general_tab.js"></script>
diff --git a/src/components/settings_modal/tabs/helpers/shared_computed_object.js b/src/components/settings_modal/tabs/helpers/shared_computed_object.js
new file mode 100644
index 00000000..61643e3b
--- /dev/null
+++ b/src/components/settings_modal/tabs/helpers/shared_computed_object.js
@@ -0,0 +1,69 @@
+import { filter, trim } from 'lodash'
+import { instanceDefaultProperties, defaultState as configDefaultState } from 'src/modules/config.js'
+
+const multiChoiceProperties = [
+  'postContentType',
+  'subjectLineBehavior'
+]
+
+const SharedComputedObject = () => ({
+  user () {
+    return this.$store.state.users.currentUser
+  },
+  // Getting localized values for instance-default properties
+  ...instanceDefaultProperties
+    .filter(key => multiChoiceProperties.includes(key))
+    .map(key => [
+      key + 'DefaultValue',
+      function () {
+        return this.$store.getters.instanceDefaultConfig[key]
+      }
+    ])
+    .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+  ...instanceDefaultProperties
+    .filter(key => !multiChoiceProperties.includes(key))
+    .map(key => [
+      key + 'LocalizedValue',
+      function () {
+        return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
+      }
+    ])
+    .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+  // Generating computed values for vuex properties
+  ...Object.keys(configDefaultState)
+    .map(key => [key, {
+      get () { return this.$store.getters.mergedConfig[key] },
+      set (value) {
+        this.$store.dispatch('setOption', { name: key, value })
+      }
+    }])
+    .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+  // Special cases (need to transform values or perform actions first)
+  muteWordsString: {
+    get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
+    set (value) {
+      this.$store.dispatch('setOption', {
+        name: 'muteWords',
+        value: filter(value.split('\n'), (word) => trim(word).length > 0)
+      })
+    }
+  },
+  useStreamingApi: {
+    get () { return this.$store.getters.mergedConfig.useStreamingApi },
+    set (value) {
+      const promise = value
+        ? this.$store.dispatch('enableMastoSockets')
+        : this.$store.dispatch('disableMastoSockets')
+
+      promise.then(() => {
+        this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
+      }).catch((e) => {
+        console.error('Failed starting MastoAPI Streaming socket', e)
+        this.$store.dispatch('disableMastoSockets')
+        this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
+      })
+    }
+  }
+})
+
+export default SharedComputedObject
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.js b/src/components/settings_modal/tabs/mutes_and_blocks.js
deleted file mode 100644
index 51895ddc..00000000
--- a/src/components/settings_modal/tabs/mutes_and_blocks.js
+++ /dev/null
@@ -1,124 +0,0 @@
-import get from 'lodash/get'
-import map from 'lodash/map'
-import reject from 'lodash/reject'
-import Autosuggest from '../../autosuggest/autosuggest.vue'
-import TabSwitcher from '../../tab_switcher/tab_switcher.js'
-import BlockCard from '../../block_card/block_card.vue'
-import MuteCard from '../../mute_card/mute_card.vue'
-import DomainMuteCard from '../../domain_mute_card/domain_mute_card.vue'
-import SelectableList from '../../selectable_list/selectable_list.vue'
-import ProgressButton from '../../progress_button/progress_button.vue'
-import withSubscription from '../../../hocs/with_subscription/with_subscription'
-import Checkbox from '../../checkbox/checkbox.vue'
-
-const BlockList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchBlocks'),
-  select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const MuteList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchMutes'),
-  select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const DomainMuteList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
-  select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const MutesAndBlocks = {
-  data () {
-    return {
-      activeTab: 'profile',
-      newDomainToMute: ''
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    TabSwitcher,
-    BlockList,
-    MuteList,
-    DomainMuteList,
-    BlockCard,
-    MuteCard,
-    DomainMuteCard,
-    ProgressButton,
-    Autosuggest,
-    Checkbox
-  },
-  methods: {
-    importFollows (file) {
-      return this.$store.state.api.backendInteractor.importFollows({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    importBlocks (file) {
-      return this.$store.state.api.backendInteractor.importBlocks({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    generateExportableUsersContent (users) {
-      // Get addresses
-      return users.map((user) => {
-        // check is it's a local user
-        if (user && user.is_local) {
-          // append the instance address
-          // eslint-disable-next-line no-undef
-          return user.screen_name + '@' + location.hostname
-        }
-        return user.screen_name
-      }).join('\n')
-    },
-    activateTab (tabName) {
-      this.activeTab = tabName
-    },
-    filterUnblockedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
-      })
-    },
-    filterUnMutedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.muted || user.id === this.$store.state.users.currentUser.id
-      })
-    },
-    queryUserIds (query) {
-      return this.$store.dispatch('searchUsers', query)
-        .then((users) => map(users, 'id'))
-    },
-    blockUsers (ids) {
-      return this.$store.dispatch('blockUsers', ids)
-    },
-    unblockUsers (ids) {
-      return this.$store.dispatch('unblockUsers', ids)
-    },
-    muteUsers (ids) {
-      return this.$store.dispatch('muteUsers', ids)
-    },
-    unmuteUsers (ids) {
-      return this.$store.dispatch('unmuteUsers', ids)
-    },
-    unmuteDomains (domains) {
-      return this.$store.dispatch('unmuteDomains', domains)
-    },
-    muteDomain () {
-      return this.$store.dispatch('muteDomain', this.newDomainToMute)
-        .then(() => { this.newDomainToMute = '' })
-    }
-  }
-}
-
-export default MutesAndBlocks
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.vue b/src/components/settings_modal/tabs/mutes_and_blocks.vue
deleted file mode 100644
index 3aff47a0..00000000
--- a/src/components/settings_modal/tabs/mutes_and_blocks.vue
+++ /dev/null
@@ -1,173 +0,0 @@
-<template>
-  <tab-switcher>
-    <div :label="$t('settings.blocks_tab')">
-      <div class="profile-edit-usersearch-wrapper">
-        <Autosuggest
-          :filter="filterUnblockedUsers"
-          :query="queryUserIds"
-          :placeholder="$t('settings.search_user_to_block')"
-          >
-          <BlockCard
-            slot-scope="row"
-            :user-id="row.item"
-            />
-        </Autosuggest>
-      </div>
-      <BlockList
-        :refresh="true"
-        :get-key="identity"
-        >
-        <template
-          slot="header"
-          slot-scope="{selected}"
-          >
-          <div class="profile-edit-bulk-actions">
-            <ProgressButton
-              v-if="selected.length > 0"
-              class="btn btn-default"
-              :click="() => blockUsers(selected)"
-              >
-              {{ $t('user_card.block') }}
-              <template slot="progress">
-                {{ $t('user_card.block_progress') }}
-              </template>
-            </ProgressButton>
-            <ProgressButton
-              v-if="selected.length > 0"
-              class="btn btn-default"
-              :click="() => unblockUsers(selected)"
-              >
-              {{ $t('user_card.unblock') }}
-              <template slot="progress">
-                {{ $t('user_card.unblock_progress') }}
-              </template>
-            </ProgressButton>
-          </div>
-        </template>
-        <template
-          slot="item"
-          slot-scope="{item}"
-          >
-          <BlockCard :user-id="item" />
-        </template>
-        <template slot="empty">
-          {{ $t('settings.no_blocks') }}
-        </template>
-      </BlockList>
-    </div>
-
-    <div :label="$t('settings.mutes_tab')">
-      <tab-switcher>
-        <div label="Users">
-          <div class="profile-edit-usersearch-wrapper">
-            <Autosuggest
-              :filter="filterUnMutedUsers"
-              :query="queryUserIds"
-              :placeholder="$t('settings.search_user_to_mute')"
-              >
-              <MuteCard
-                slot-scope="row"
-                :user-id="row.item"
-                />
-            </Autosuggest>
-          </div>
-          <MuteList
-            :refresh="true"
-            :get-key="identity"
-            >
-            <template
-              slot="header"
-              slot-scope="{selected}"
-              >
-              <div class="profile-edit-bulk-actions">
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => muteUsers(selected)"
-                  >
-                  {{ $t('user_card.mute') }}
-                  <template slot="progress">
-                    {{ $t('user_card.mute_progress') }}
-                  </template>
-                </ProgressButton>
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => unmuteUsers(selected)"
-                  >
-                  {{ $t('user_card.unmute') }}
-                  <template slot="progress">
-                    {{ $t('user_card.unmute_progress') }}
-                  </template>
-                </ProgressButton>
-              </div>
-            </template>
-            <template
-              slot="item"
-              slot-scope="{item}"
-              >
-              <MuteCard :user-id="item" />
-            </template>
-            <template slot="empty">
-              {{ $t('settings.no_mutes') }}
-            </template>
-          </MuteList>
-        </div>
-
-        <div :label="$t('settings.domain_mutes')">
-          <div class="profile-edit-domain-mute-form">
-            <input
-              v-model="newDomainToMute"
-              :placeholder="$t('settings.type_domains_to_mute')"
-              type="text"
-              @keyup.enter="muteDomain"
-              >
-            <ProgressButton
-              class="btn btn-default"
-              :click="muteDomain"
-              >
-              {{ $t('domain_mute_card.mute') }}
-              <template slot="progress">
-                {{ $t('domain_mute_card.mute_progress') }}
-              </template>
-            </ProgressButton>
-          </div>
-          <DomainMuteList
-            :refresh="true"
-            :get-key="identity"
-            >
-            <template
-              slot="header"
-              slot-scope="{selected}"
-              >
-              <div class="profile-edit-bulk-actions">
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => unmuteDomains(selected)"
-                  >
-                  {{ $t('domain_mute_card.unmute') }}
-                  <template slot="progress">
-                    {{ $t('domain_mute_card.unmute_progress') }}
-                  </template>
-                </ProgressButton>
-              </div>
-            </template>
-            <template
-              slot="item"
-              slot-scope="{item}"
-              >
-              <DomainMuteCard :domain="item" />
-            </template>
-            <template slot="empty">
-              {{ $t('settings.no_mutes') }}
-            </template>
-          </DomainMuteList>
-        </div>
-      </tab-switcher>
-    </div>
-  </tab-switcher>
-</template>
-
-<script src="./mutes_and_blocks.js"></script>
-<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
new file mode 100644
index 00000000..3f6b7205
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
@@ -0,0 +1,124 @@
+import get from 'lodash/get'
+import map from 'lodash/map'
+import reject from 'lodash/reject'
+import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import BlockCard from 'src/components/block_card/block_card.vue'
+import MuteCard from 'src/components/mute_card/mute_card.vue'
+import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
+import SelectableList from 'src/components/selectable_list/selectable_list.vue'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const BlockList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchBlocks'),
+  select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const MuteList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchMutes'),
+  select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const DomainMuteList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+  select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+  childPropName: 'items'
+})(SelectableList)
+
+const MutesAndBlocks = {
+  data () {
+    return {
+      activeTab: 'profile',
+      newDomainToMute: ''
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    TabSwitcher,
+    BlockList,
+    MuteList,
+    DomainMuteList,
+    BlockCard,
+    MuteCard,
+    DomainMuteCard,
+    ProgressButton,
+    Autosuggest,
+    Checkbox
+  },
+  methods: {
+    importFollows (file) {
+      return this.$store.state.api.backendInteractor.importFollows({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    importBlocks (file) {
+      return this.$store.state.api.backendInteractor.importBlocks({ file })
+        .then((status) => {
+          if (!status) {
+            throw new Error('failed')
+          }
+        })
+    },
+    generateExportableUsersContent (users) {
+      // Get addresses
+      return users.map((user) => {
+        // check is it's a local user
+        if (user && user.is_local) {
+          // append the instance address
+          // eslint-disable-next-line no-undef
+          return user.screen_name + '@' + location.hostname
+        }
+        return user.screen_name
+      }).join('\n')
+    },
+    activateTab (tabName) {
+      this.activeTab = tabName
+    },
+    filterUnblockedUsers (userIds) {
+      return reject(userIds, (userId) => {
+        const user = this.$store.getters.findUser(userId)
+        return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
+      })
+    },
+    filterUnMutedUsers (userIds) {
+      return reject(userIds, (userId) => {
+        const user = this.$store.getters.findUser(userId)
+        return !user || user.muted || user.id === this.$store.state.users.currentUser.id
+      })
+    },
+    queryUserIds (query) {
+      return this.$store.dispatch('searchUsers', query)
+        .then((users) => map(users, 'id'))
+    },
+    blockUsers (ids) {
+      return this.$store.dispatch('blockUsers', ids)
+    },
+    unblockUsers (ids) {
+      return this.$store.dispatch('unblockUsers', ids)
+    },
+    muteUsers (ids) {
+      return this.$store.dispatch('muteUsers', ids)
+    },
+    unmuteUsers (ids) {
+      return this.$store.dispatch('unmuteUsers', ids)
+    },
+    unmuteDomains (domains) {
+      return this.$store.dispatch('unmuteDomains', domains)
+    },
+    muteDomain () {
+      return this.$store.dispatch('muteDomain', this.newDomainToMute)
+        .then(() => { this.newDomainToMute = '' })
+    }
+  }
+}
+
+export default MutesAndBlocks
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
new file mode 100644
index 00000000..7fce7b78
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -0,0 +1,173 @@
+<template>
+  <tab-switcher>
+    <div :label="$t('settings.blocks_tab')">
+      <div class="profile-edit-usersearch-wrapper">
+        <Autosuggest
+          :filter="filterUnblockedUsers"
+          :query="queryUserIds"
+          :placeholder="$t('settings.search_user_to_block')"
+          >
+          <BlockCard
+            slot-scope="row"
+            :user-id="row.item"
+            />
+        </Autosuggest>
+      </div>
+      <BlockList
+        :refresh="true"
+        :get-key="i => i"
+        >
+        <template
+          slot="header"
+          slot-scope="{selected}"
+          >
+          <div class="profile-edit-bulk-actions">
+            <ProgressButton
+              v-if="selected.length > 0"
+              class="btn btn-default"
+              :click="() => blockUsers(selected)"
+              >
+              {{ $t('user_card.block') }}
+              <template slot="progress">
+                {{ $t('user_card.block_progress') }}
+              </template>
+            </ProgressButton>
+            <ProgressButton
+              v-if="selected.length > 0"
+              class="btn btn-default"
+              :click="() => unblockUsers(selected)"
+              >
+              {{ $t('user_card.unblock') }}
+              <template slot="progress">
+                {{ $t('user_card.unblock_progress') }}
+              </template>
+            </ProgressButton>
+          </div>
+        </template>
+        <template
+          slot="item"
+          slot-scope="{item}"
+          >
+          <BlockCard :user-id="item" />
+        </template>
+        <template slot="empty">
+          {{ $t('settings.no_blocks') }}
+        </template>
+      </BlockList>
+    </div>
+
+    <div :label="$t('settings.mutes_tab')">
+      <tab-switcher>
+        <div label="Users">
+          <div class="profile-edit-usersearch-wrapper">
+            <Autosuggest
+              :filter="filterUnMutedUsers"
+              :query="queryUserIds"
+              :placeholder="$t('settings.search_user_to_mute')"
+              >
+              <MuteCard
+                slot-scope="row"
+                :user-id="row.item"
+                />
+            </Autosuggest>
+          </div>
+          <MuteList
+            :refresh="true"
+            :get-key="i => i"
+            >
+            <template
+              slot="header"
+              slot-scope="{selected}"
+              >
+              <div class="profile-edit-bulk-actions">
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => muteUsers(selected)"
+                  >
+                  {{ $t('user_card.mute') }}
+                  <template slot="progress">
+                    {{ $t('user_card.mute_progress') }}
+                  </template>
+                </ProgressButton>
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => unmuteUsers(selected)"
+                  >
+                  {{ $t('user_card.unmute') }}
+                  <template slot="progress">
+                    {{ $t('user_card.unmute_progress') }}
+                  </template>
+                </ProgressButton>
+              </div>
+            </template>
+            <template
+              slot="item"
+              slot-scope="{item}"
+              >
+              <MuteCard :user-id="item" />
+            </template>
+            <template slot="empty">
+              {{ $t('settings.no_mutes') }}
+            </template>
+          </MuteList>
+        </div>
+
+        <div :label="$t('settings.domain_mutes')">
+          <div class="profile-edit-domain-mute-form">
+            <input
+              v-model="newDomainToMute"
+              :placeholder="$t('settings.type_domains_to_mute')"
+              type="text"
+              @keyup.enter="muteDomain"
+              >
+            <ProgressButton
+              class="btn btn-default"
+              :click="muteDomain"
+              >
+              {{ $t('domain_mute_card.mute') }}
+              <template slot="progress">
+                {{ $t('domain_mute_card.mute_progress') }}
+              </template>
+            </ProgressButton>
+          </div>
+          <DomainMuteList
+            :refresh="true"
+            :get-key="i => i"
+            >
+            <template
+              slot="header"
+              slot-scope="{selected}"
+              >
+              <div class="profile-edit-bulk-actions">
+                <ProgressButton
+                  v-if="selected.length > 0"
+                  class="btn btn-default"
+                  :click="() => unmuteDomains(selected)"
+                  >
+                  {{ $t('domain_mute_card.unmute') }}
+                  <template slot="progress">
+                    {{ $t('domain_mute_card.unmute_progress') }}
+                  </template>
+                </ProgressButton>
+              </div>
+            </template>
+            <template
+              slot="item"
+              slot-scope="{item}"
+              >
+              <DomainMuteCard :domain="item" />
+            </template>
+            <template slot="empty">
+              {{ $t('settings.no_mutes') }}
+            </template>
+          </DomainMuteList>
+        </div>
+      </tab-switcher>
+    </div>
+  </tab-switcher>
+</template>
+
+<script src="./mutes_and_blocks_tab.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/notifications.js b/src/components/settings_modal/tabs/notifications.js
deleted file mode 100644
index 0a870b3f..00000000
--- a/src/components/settings_modal/tabs/notifications.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Checkbox from '../../checkbox/checkbox.vue'
-
-const Notifications = {
-  data () {
-    return {
-      activeTab: 'profile',
-      notificationSettings: this.$store.state.users.currentUser.notification_settings,
-      newDomainToMute: ''
-    }
-  },
-  components: {
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    }
-  },
-  methods: {
-    updateNotificationSettings () {
-      this.$store.state.api.backendInteractor
-        .updateNotificationSettings({ settings: this.notificationSettings })
-    }
-  }
-}
-
-export default Notifications
diff --git a/src/components/settings_modal/tabs/notifications.vue b/src/components/settings_modal/tabs/notifications.vue
deleted file mode 100644
index f9a7c17b..00000000
--- a/src/components/settings_modal/tabs/notifications.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<div :label="$t('settings.notifications')">
-  <div class="setting-item">
-    <div class="select-multiple">
-      <span class="label">{{ $t('settings.notification_setting') }}</span>
-      <ul class="option-list">
-        <li>
-          <Checkbox v-model="notificationSettings.follows">
-            {{ $t('settings.notification_setting_follows') }}
-          </Checkbox>
-        </li>
-        <li>
-          <Checkbox v-model="notificationSettings.followers">
-            {{ $t('settings.notification_setting_followers') }}
-          </Checkbox>
-        </li>
-        <li>
-          <Checkbox v-model="notificationSettings.non_follows">
-            {{ $t('settings.notification_setting_non_follows') }}
-          </Checkbox>
-        </li>
-        <li>
-          <Checkbox v-model="notificationSettings.non_followers">
-            {{ $t('settings.notification_setting_non_followers') }}
-          </Checkbox>
-        </li>
-      </ul>
-    </div>
-    <p>{{ $t('settings.notification_mutes') }}</p>
-    <p>{{ $t('settings.notification_blocks') }}</p>
-    <button
-      class="btn btn-default"
-      @click="updateNotificationSettings"
-      >
-      {{ $t('general.submit') }}
-    </button>
-  </div>
-</div>
-</template>
-
-<script src="./notifications.js"></script>
-<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js
new file mode 100644
index 00000000..3e44c95d
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications_tab.js
@@ -0,0 +1,27 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const NotificationsTab = {
+  data () {
+    return {
+      activeTab: 'profile',
+      notificationSettings: this.$store.state.users.currentUser.notification_settings,
+      newDomainToMute: ''
+    }
+  },
+  components: {
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    }
+  },
+  methods: {
+    updateNotificationSettings () {
+      this.$store.state.api.backendInteractor
+        .updateNotificationSettings({ settings: this.notificationSettings })
+    }
+  }
+}
+
+export default NotificationsTab
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
new file mode 100644
index 00000000..ab33a6a5
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -0,0 +1,42 @@
+<template>
+<div :label="$t('settings.notifications')">
+  <div class="setting-item">
+    <div class="select-multiple">
+      <span class="label">{{ $t('settings.notification_setting') }}</span>
+      <ul class="option-list">
+        <li>
+          <Checkbox v-model="notificationSettings.follows">
+            {{ $t('settings.notification_setting_follows') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.followers">
+            {{ $t('settings.notification_setting_followers') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.non_follows">
+            {{ $t('settings.notification_setting_non_follows') }}
+          </Checkbox>
+        </li>
+        <li>
+          <Checkbox v-model="notificationSettings.non_followers">
+            {{ $t('settings.notification_setting_non_followers') }}
+          </Checkbox>
+        </li>
+      </ul>
+    </div>
+    <p>{{ $t('settings.notification_mutes') }}</p>
+    <p>{{ $t('settings.notification_blocks') }}</p>
+    <button
+      class="btn btn-default"
+      @click="updateNotificationSettings"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+</div>
+</template>
+
+<script src="./notifications_tab.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/profile.js b/src/components/settings_modal/tabs/profile.js
deleted file mode 100644
index 18c44024..00000000
--- a/src/components/settings_modal/tabs/profile.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import unescape from 'lodash/unescape'
-import ImageCropper from '../../image_cropper/image_cropper.vue'
-import ScopeSelector from '../../scope_selector/scope_selector.vue'
-import fileSizeFormatService from '../../../services/file_size_format/file_size_format.js'
-import ProgressButton from '../../progress_button/progress_button.vue'
-import EmojiInput from '../../emoji_input/emoji_input.vue'
-import suggestor from '../../emoji_input/suggestor.js'
-import Autosuggest from '../../autosuggest/autosuggest.vue'
-import Checkbox from '../../checkbox/checkbox.vue'
-
-const ProfileTab = {
-  data () {
-    return {
-      newName: this.$store.state.users.currentUser.name,
-      newBio: unescape(this.$store.state.users.currentUser.description),
-      newLocked: this.$store.state.users.currentUser.locked,
-      newNoRichText: this.$store.state.users.currentUser.no_rich_text,
-      newDefaultScope: this.$store.state.users.currentUser.default_scope,
-      hideFollows: this.$store.state.users.currentUser.hide_follows,
-      hideFollowers: this.$store.state.users.currentUser.hide_followers,
-      hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
-      hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
-      showRole: this.$store.state.users.currentUser.show_role,
-      role: this.$store.state.users.currentUser.role,
-      discoverable: this.$store.state.users.currentUser.discoverable,
-      allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
-      pickAvatarBtnVisible: true,
-      bannerUploading: false,
-      backgroundUploading: false,
-      banner: null,
-      bannerPreview: null,
-      background: null,
-      backgroundPreview: null,
-      bannerUploadError: null,
-      backgroundUploadError: null,
-    }
-  },
-  components: {
-    ScopeSelector,
-    ImageCropper,
-    EmojiInput,
-    Autosuggest,
-    ProgressButton,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    },
-    emojiUserSuggestor () {
-      return suggestor({
-        emoji: [
-          ...this.$store.state.instance.emoji,
-          ...this.$store.state.instance.customEmoji
-        ],
-        users: this.$store.state.users.users,
-        updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
-      })
-    },
-    emojiSuggestor () {
-      return suggestor({ emoji: [
-        ...this.$store.state.instance.emoji,
-        ...this.$store.state.instance.customEmoji
-      ] })
-    }
-  },
-  methods: {
-    updateProfile () {
-      this.$store.state.api.backendInteractor
-        .updateProfile({
-          params: {
-            note: this.newBio,
-            locked: this.newLocked,
-            // Backend notation.
-            /* eslint-disable camelcase */
-            display_name: this.newName,
-            default_scope: this.newDefaultScope,
-            no_rich_text: this.newNoRichText,
-            hide_follows: this.hideFollows,
-            hide_followers: this.hideFollowers,
-            discoverable: this.discoverable,
-            allow_following_move: this.allowFollowingMove,
-            hide_follows_count: this.hideFollowsCount,
-            hide_followers_count: this.hideFollowersCount,
-            show_role: this.showRole
-            /* eslint-enable camelcase */
-          } }).then((user) => {
-          this.$store.commit('addNewUsers', [user])
-          this.$store.commit('setCurrentUser', user)
-        })
-    },
-    changeVis (visibility) {
-      this.newDefaultScope = visibility
-    },
-    uploadFile (slot, e) {
-      const file = e.target.files[0]
-      if (!file) { return }
-      if (file.size > this.$store.state.instance[slot + 'limit']) {
-        const filesize = fileSizeFormatService.fileSizeFormat(file.size)
-        const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
-        this[slot + 'UploadError'] = [
-          this.$t('upload.error.base'),
-          this.$t(
-            'upload.error.file_too_big',
-            {
-              filesize: filesize.num,
-              filesizeunit: filesize.unit,
-              allowedsize: allowedsize.num,
-              allowedsizeunit: allowedsize.unit
-            }
-          )
-        ].join(' ')
-        return
-      }
-      // eslint-disable-next-line no-undef
-      const reader = new FileReader()
-      reader.onload = ({ target }) => {
-        const img = target.result
-        this[slot + 'Preview'] = img
-        this[slot] = file
-      }
-      reader.readAsDataURL(file)
-    },
-    submitAvatar (cropper, file) {
-      const that = this
-      return new Promise((resolve, reject) => {
-        function updateAvatar (avatar) {
-          that.$store.state.api.backendInteractor.updateAvatar({ avatar })
-            .then((user) => {
-              that.$store.commit('addNewUsers', [user])
-              that.$store.commit('setCurrentUser', user)
-              resolve()
-            })
-            .catch((err) => {
-              reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
-            })
-        }
-
-        if (cropper) {
-          cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
-        } else {
-          updateAvatar(file)
-        }
-      })
-    },
-    submitBanner () {
-      if (!this.bannerPreview) { return }
-
-      this.bannerUploading = true
-      this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
-        .then((user) => {
-          this.$store.commit('addNewUsers', [user])
-          this.$store.commit('setCurrentUser', user)
-          this.bannerPreview = null
-        })
-        .catch((err) => {
-          this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
-        })
-        .then(() => { this.bannerUploading = false })
-    },
-    submitBg () {
-      if (!this.backgroundPreview) { return }
-      let background = this.background
-      this.backgroundUploading = true
-      this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
-        if (!data.error) {
-          this.$store.commit('addNewUsers', [data])
-          this.$store.commit('setCurrentUser', data)
-          this.backgroundPreview = null
-        } else {
-          this.backgroundUploadError = this.$t('upload.error.base') + data.error
-        }
-        this.backgroundUploading = false
-      })
-    }
-  }
-}
-
-export default ProfileTab
diff --git a/src/components/settings_modal/tabs/profile.scss b/src/components/settings_modal/tabs/profile.scss
deleted file mode 100644
index 4aab81eb..00000000
--- a/src/components/settings_modal/tabs/profile.scss
+++ /dev/null
@@ -1,82 +0,0 @@
-@import '../../../_variables.scss';
-.profile-tab {
-  .bio {
-    margin: 0;
-  }
-
-  .visibility-tray {
-    padding-top: 5px;
-  }
-
-  input[type=file] {
-    padding: 5px;
-    height: auto;
-  }
-
-  .banner {
-    max-width: 100%;
-  }
-
-  .uploading {
-    font-size: 1.5em;
-    margin: 0.25em;
-  }
-
-  .name-changer {
-    width: 100%;
-  }
-
-  .bg {
-    max-width: 100%;
-  }
-
-  .current-avatar {
-    display: block;
-    width: 150px;
-    height: 150px;
-    border-radius: $fallback--avatarRadius;
-    border-radius: var(--avatarRadius, $fallback--avatarRadius);
-  }
-
-  .oauth-tokens {
-    width: 100%;
-
-    th {
-      text-align: left;
-    }
-
-    .actions {
-      text-align: right;
-    }
-  }
-
-  &-usersearch-wrapper {
-    padding: 1em;
-  }
-
-  &-bulk-actions {
-    text-align: right;
-    padding: 0 1em;
-    min-height: 28px;
-
-    button {
-      width: 10em;
-    }
-  }
-
-  &-domain-mute-form {
-    padding: 1em;
-    display: flex;
-    flex-direction: column;
-
-    button {
-      align-self: flex-end;
-      margin-top: 1em;
-      width: 10em;
-    }
-  }
-
-  .setting-subitem {
-    margin-left: 1.75em;
-  }
-}
diff --git a/src/components/settings_modal/tabs/profile.vue b/src/components/settings_modal/tabs/profile.vue
deleted file mode 100644
index 335fc12e..00000000
--- a/src/components/settings_modal/tabs/profile.vue
+++ /dev/null
@@ -1,213 +0,0 @@
-<template>
-<div class="profile-tab">
-  <div class="setting-item">
-    <h2>{{ $t('settings.name_bio') }}</h2>
-    <p>{{ $t('settings.name') }}</p>
-    <EmojiInput
-      v-model="newName"
-      enable-emoji-picker
-      :suggest="emojiSuggestor"
-      >
-      <input
-        id="username"
-        v-model="newName"
-        classname="name-changer"
-        >
-    </EmojiInput>
-    <p>{{ $t('settings.bio') }}</p>
-    <EmojiInput
-      v-model="newBio"
-      enable-emoji-picker
-      :suggest="emojiUserSuggestor"
-      >
-      <textarea
-        v-model="newBio"
-        classname="bio"
-        />
-    </EmojiInput>
-    <p>
-      <Checkbox v-model="newLocked">
-        {{ $t('settings.lock_account_description') }}
-      </Checkbox>
-    </p>
-    <div>
-      <label for="default-vis">{{ $t('settings.default_vis') }}</label>
-      <div
-        id="default-vis"
-        class="visibility-tray"
-        >
-        <scope-selector
-          :show-all="true"
-          :user-default="newDefaultScope"
-          :initial-scope="newDefaultScope"
-          :on-scope-change="changeVis"
-          />
-      </div>
-    </div>
-    <p>
-      <Checkbox v-model="newNoRichText">
-        {{ $t('settings.no_rich_text_description') }}
-      </Checkbox>
-    </p>
-    <p>
-      <Checkbox v-model="hideFollows">
-        {{ $t('settings.hide_follows_description') }}
-      </Checkbox>
-    </p>
-    <p class="setting-subitem">
-      <Checkbox
-        v-model="hideFollowsCount"
-        :disabled="!hideFollows"
-        >
-        {{ $t('settings.hide_follows_count_description') }}
-      </Checkbox>
-    </p>
-    <p>
-      <Checkbox v-model="hideFollowers">
-        {{ $t('settings.hide_followers_description') }}
-      </Checkbox>
-    </p>
-    <p class="setting-subitem">
-      <Checkbox
-        v-model="hideFollowersCount"
-        :disabled="!hideFollowers"
-        >
-        {{ $t('settings.hide_followers_count_description') }}
-      </Checkbox>
-    </p>
-    <p>
-      <Checkbox v-model="allowFollowingMove">
-        {{ $t('settings.allow_following_move') }}
-      </Checkbox>
-    </p>
-    <p v-if="role === 'admin' || role === 'moderator'">
-      <Checkbox v-model="showRole">
-        <template v-if="role === 'admin'">
-          {{ $t('settings.show_admin_badge') }}
-        </template>
-        <template v-if="role === 'moderator'">
-          {{ $t('settings.show_moderator_badge') }}
-        </template>
-      </Checkbox>
-    </p>
-    <p>
-      <Checkbox v-model="discoverable">
-        {{ $t('settings.discoverable') }}
-      </Checkbox>
-    </p>
-    <button
-      :disabled="newName && newName.length === 0"
-      class="btn btn-default"
-      @click="updateProfile"
-      >
-      {{ $t('general.submit') }}
-    </button>
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.avatar') }}</h2>
-    <p class="visibility-notice">
-      {{ $t('settings.avatar_size_instruction') }}
-    </p>
-    <p>{{ $t('settings.current_avatar') }}</p>
-    <img
-      :src="user.profile_image_url_original"
-      class="current-avatar"
-      >
-    <p>{{ $t('settings.set_new_avatar') }}</p>
-    <button
-      v-show="pickAvatarBtnVisible"
-      id="pick-avatar"
-      class="btn"
-      type="button"
-      >
-      {{ $t('settings.upload_a_photo') }}
-    </button>
-    <image-cropper
-      trigger="#pick-avatar"
-      :submit-handler="submitAvatar"
-      @open="pickAvatarBtnVisible=false"
-      @close="pickAvatarBtnVisible=true"
-      />
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.profile_banner') }}</h2>
-    <p>{{ $t('settings.current_profile_banner') }}</p>
-    <img
-      :src="user.cover_photo"
-      class="banner"
-      >
-    <p>{{ $t('settings.set_new_profile_banner') }}</p>
-    <img
-      v-if="bannerPreview"
-      class="banner"
-      :src="bannerPreview"
-      >
-    <div>
-      <input
-        type="file"
-        @change="uploadFile('banner', $event)"
-        >
-    </div>
-    <i
-      v-if="bannerUploading"
-      class=" icon-spin4 animate-spin uploading"
-      />
-    <button
-      v-else-if="bannerPreview"
-      class="btn btn-default"
-      @click="submitBanner"
-      >
-      {{ $t('general.submit') }}
-    </button>
-    <div
-      v-if="bannerUploadError"
-      class="alert error"
-      >
-      Error: {{ bannerUploadError }}
-      <i
-        class="button-icon icon-cancel"
-        @click="clearUploadError('banner')"
-        />
-    </div>
-  </div>
-  <div class="setting-item">
-    <h2>{{ $t('settings.profile_background') }}</h2>
-    <p>{{ $t('settings.set_new_profile_background') }}</p>
-    <img
-      v-if="backgroundPreview"
-      class="bg"
-      :src="backgroundPreview"
-      >
-    <div>
-      <input
-        type="file"
-        @change="uploadFile('background', $event)"
-        >
-    </div>
-    <i
-      v-if="backgroundUploading"
-      class=" icon-spin4 animate-spin uploading"
-      />
-    <button
-      v-else-if="backgroundPreview"
-      class="btn btn-default"
-      @click="submitBg"
-      >
-      {{ $t('general.submit') }}
-    </button>
-    <div
-      v-if="backgroundUploadError"
-      class="alert error"
-      >
-      Error: {{ backgroundUploadError }}
-      <i
-        class="button-icon icon-cancel"
-        @click="clearUploadError('background')"
-        />
-    </div>
-  </div>
-</div>
-</template>
-
-<script src="./profile.js"></script>
-<style lang="scss" src="./profile.scss"></style>
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
new file mode 100644
index 00000000..949b480b
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -0,0 +1,179 @@
+import unescape from 'lodash/unescape'
+import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
+import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
+import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
+import suggestor from 'src/components/emoji_input/suggestor.js'
+import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const ProfileTab = {
+  data () {
+    return {
+      newName: this.$store.state.users.currentUser.name,
+      newBio: unescape(this.$store.state.users.currentUser.description),
+      newLocked: this.$store.state.users.currentUser.locked,
+      newNoRichText: this.$store.state.users.currentUser.no_rich_text,
+      newDefaultScope: this.$store.state.users.currentUser.default_scope,
+      hideFollows: this.$store.state.users.currentUser.hide_follows,
+      hideFollowers: this.$store.state.users.currentUser.hide_followers,
+      hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
+      hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
+      showRole: this.$store.state.users.currentUser.show_role,
+      role: this.$store.state.users.currentUser.role,
+      discoverable: this.$store.state.users.currentUser.discoverable,
+      allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
+      pickAvatarBtnVisible: true,
+      bannerUploading: false,
+      backgroundUploading: false,
+      banner: null,
+      bannerPreview: null,
+      background: null,
+      backgroundPreview: null,
+      bannerUploadError: null,
+      backgroundUploadError: null,
+    }
+  },
+  components: {
+    ScopeSelector,
+    ImageCropper,
+    EmojiInput,
+    Autosuggest,
+    ProgressButton,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    },
+    emojiUserSuggestor () {
+      return suggestor({
+        emoji: [
+          ...this.$store.state.instance.emoji,
+          ...this.$store.state.instance.customEmoji
+        ],
+        users: this.$store.state.users.users,
+        updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
+      })
+    },
+    emojiSuggestor () {
+      return suggestor({ emoji: [
+        ...this.$store.state.instance.emoji,
+        ...this.$store.state.instance.customEmoji
+      ] })
+    }
+  },
+  methods: {
+    updateProfile () {
+      this.$store.state.api.backendInteractor
+        .updateProfile({
+          params: {
+            note: this.newBio,
+            locked: this.newLocked,
+            // Backend notation.
+            /* eslint-disable camelcase */
+            display_name: this.newName,
+            default_scope: this.newDefaultScope,
+            no_rich_text: this.newNoRichText,
+            hide_follows: this.hideFollows,
+            hide_followers: this.hideFollowers,
+            discoverable: this.discoverable,
+            allow_following_move: this.allowFollowingMove,
+            hide_follows_count: this.hideFollowsCount,
+            hide_followers_count: this.hideFollowersCount,
+            show_role: this.showRole
+            /* eslint-enable camelcase */
+          } }).then((user) => {
+          this.$store.commit('addNewUsers', [user])
+          this.$store.commit('setCurrentUser', user)
+        })
+    },
+    changeVis (visibility) {
+      this.newDefaultScope = visibility
+    },
+    uploadFile (slot, e) {
+      const file = e.target.files[0]
+      if (!file) { return }
+      if (file.size > this.$store.state.instance[slot + 'limit']) {
+        const filesize = fileSizeFormatService.fileSizeFormat(file.size)
+        const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
+        this[slot + 'UploadError'] = [
+          this.$t('upload.error.base'),
+          this.$t(
+            'upload.error.file_too_big',
+            {
+              filesize: filesize.num,
+              filesizeunit: filesize.unit,
+              allowedsize: allowedsize.num,
+              allowedsizeunit: allowedsize.unit
+            }
+          )
+        ].join(' ')
+        return
+      }
+      // eslint-disable-next-line no-undef
+      const reader = new FileReader()
+      reader.onload = ({ target }) => {
+        const img = target.result
+        this[slot + 'Preview'] = img
+        this[slot] = file
+      }
+      reader.readAsDataURL(file)
+    },
+    submitAvatar (cropper, file) {
+      const that = this
+      return new Promise((resolve, reject) => {
+        function updateAvatar (avatar) {
+          that.$store.state.api.backendInteractor.updateAvatar({ avatar })
+            .then((user) => {
+              that.$store.commit('addNewUsers', [user])
+              that.$store.commit('setCurrentUser', user)
+              resolve()
+            })
+            .catch((err) => {
+              reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
+            })
+        }
+
+        if (cropper) {
+          cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
+        } else {
+          updateAvatar(file)
+        }
+      })
+    },
+    submitBanner () {
+      if (!this.bannerPreview) { return }
+
+      this.bannerUploading = true
+      this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
+        .then((user) => {
+          this.$store.commit('addNewUsers', [user])
+          this.$store.commit('setCurrentUser', user)
+          this.bannerPreview = null
+        })
+        .catch((err) => {
+          this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
+        })
+        .then(() => { this.bannerUploading = false })
+    },
+    submitBg () {
+      if (!this.backgroundPreview) { return }
+      let background = this.background
+      this.backgroundUploading = true
+      this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
+        if (!data.error) {
+          this.$store.commit('addNewUsers', [data])
+          this.$store.commit('setCurrentUser', data)
+          this.backgroundPreview = null
+        } else {
+          this.backgroundUploadError = this.$t('upload.error.base') + data.error
+        }
+        this.backgroundUploading = false
+      })
+    }
+  }
+}
+
+export default ProfileTab
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
new file mode 100644
index 00000000..4aab81eb
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -0,0 +1,82 @@
+@import '../../../_variables.scss';
+.profile-tab {
+  .bio {
+    margin: 0;
+  }
+
+  .visibility-tray {
+    padding-top: 5px;
+  }
+
+  input[type=file] {
+    padding: 5px;
+    height: auto;
+  }
+
+  .banner {
+    max-width: 100%;
+  }
+
+  .uploading {
+    font-size: 1.5em;
+    margin: 0.25em;
+  }
+
+  .name-changer {
+    width: 100%;
+  }
+
+  .bg {
+    max-width: 100%;
+  }
+
+  .current-avatar {
+    display: block;
+    width: 150px;
+    height: 150px;
+    border-radius: $fallback--avatarRadius;
+    border-radius: var(--avatarRadius, $fallback--avatarRadius);
+  }
+
+  .oauth-tokens {
+    width: 100%;
+
+    th {
+      text-align: left;
+    }
+
+    .actions {
+      text-align: right;
+    }
+  }
+
+  &-usersearch-wrapper {
+    padding: 1em;
+  }
+
+  &-bulk-actions {
+    text-align: right;
+    padding: 0 1em;
+    min-height: 28px;
+
+    button {
+      width: 10em;
+    }
+  }
+
+  &-domain-mute-form {
+    padding: 1em;
+    display: flex;
+    flex-direction: column;
+
+    button {
+      align-self: flex-end;
+      margin-top: 1em;
+      width: 10em;
+    }
+  }
+
+  .setting-subitem {
+    margin-left: 1.75em;
+  }
+}
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
new file mode 100644
index 00000000..9dd89b99
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -0,0 +1,213 @@
+<template>
+<div class="profile-tab">
+  <div class="setting-item">
+    <h2>{{ $t('settings.name_bio') }}</h2>
+    <p>{{ $t('settings.name') }}</p>
+    <EmojiInput
+      v-model="newName"
+      enable-emoji-picker
+      :suggest="emojiSuggestor"
+      >
+      <input
+        id="username"
+        v-model="newName"
+        classname="name-changer"
+        >
+    </EmojiInput>
+    <p>{{ $t('settings.bio') }}</p>
+    <EmojiInput
+      v-model="newBio"
+      enable-emoji-picker
+      :suggest="emojiUserSuggestor"
+      >
+      <textarea
+        v-model="newBio"
+        classname="bio"
+        />
+    </EmojiInput>
+    <p>
+      <Checkbox v-model="newLocked">
+        {{ $t('settings.lock_account_description') }}
+      </Checkbox>
+    </p>
+    <div>
+      <label for="default-vis">{{ $t('settings.default_vis') }}</label>
+      <div
+        id="default-vis"
+        class="visibility-tray"
+        >
+        <scope-selector
+          :show-all="true"
+          :user-default="newDefaultScope"
+          :initial-scope="newDefaultScope"
+          :on-scope-change="changeVis"
+          />
+      </div>
+    </div>
+    <p>
+      <Checkbox v-model="newNoRichText">
+        {{ $t('settings.no_rich_text_description') }}
+      </Checkbox>
+    </p>
+    <p>
+      <Checkbox v-model="hideFollows">
+        {{ $t('settings.hide_follows_description') }}
+      </Checkbox>
+    </p>
+    <p class="setting-subitem">
+      <Checkbox
+        v-model="hideFollowsCount"
+        :disabled="!hideFollows"
+        >
+        {{ $t('settings.hide_follows_count_description') }}
+      </Checkbox>
+    </p>
+    <p>
+      <Checkbox v-model="hideFollowers">
+        {{ $t('settings.hide_followers_description') }}
+      </Checkbox>
+    </p>
+    <p class="setting-subitem">
+      <Checkbox
+        v-model="hideFollowersCount"
+        :disabled="!hideFollowers"
+        >
+        {{ $t('settings.hide_followers_count_description') }}
+      </Checkbox>
+    </p>
+    <p>
+      <Checkbox v-model="allowFollowingMove">
+        {{ $t('settings.allow_following_move') }}
+      </Checkbox>
+    </p>
+    <p v-if="role === 'admin' || role === 'moderator'">
+      <Checkbox v-model="showRole">
+        <template v-if="role === 'admin'">
+          {{ $t('settings.show_admin_badge') }}
+        </template>
+        <template v-if="role === 'moderator'">
+          {{ $t('settings.show_moderator_badge') }}
+        </template>
+      </Checkbox>
+    </p>
+    <p>
+      <Checkbox v-model="discoverable">
+        {{ $t('settings.discoverable') }}
+      </Checkbox>
+    </p>
+    <button
+      :disabled="newName && newName.length === 0"
+      class="btn btn-default"
+      @click="updateProfile"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.avatar') }}</h2>
+    <p class="visibility-notice">
+      {{ $t('settings.avatar_size_instruction') }}
+    </p>
+    <p>{{ $t('settings.current_avatar') }}</p>
+    <img
+      :src="user.profile_image_url_original"
+      class="current-avatar"
+      >
+    <p>{{ $t('settings.set_new_avatar') }}</p>
+    <button
+      v-show="pickAvatarBtnVisible"
+      id="pick-avatar"
+      class="btn"
+      type="button"
+      >
+      {{ $t('settings.upload_a_photo') }}
+    </button>
+    <image-cropper
+      trigger="#pick-avatar"
+      :submit-handler="submitAvatar"
+      @open="pickAvatarBtnVisible=false"
+      @close="pickAvatarBtnVisible=true"
+      />
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.profile_banner') }}</h2>
+    <p>{{ $t('settings.current_profile_banner') }}</p>
+    <img
+      :src="user.cover_photo"
+      class="banner"
+      >
+    <p>{{ $t('settings.set_new_profile_banner') }}</p>
+    <img
+      v-if="bannerPreview"
+      class="banner"
+      :src="bannerPreview"
+      >
+    <div>
+      <input
+        type="file"
+        @change="uploadFile('banner', $event)"
+        >
+    </div>
+    <i
+      v-if="bannerUploading"
+      class=" icon-spin4 animate-spin uploading"
+      />
+    <button
+      v-else-if="bannerPreview"
+      class="btn btn-default"
+      @click="submitBanner"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <div
+      v-if="bannerUploadError"
+      class="alert error"
+      >
+      Error: {{ bannerUploadError }}
+      <i
+        class="button-icon icon-cancel"
+        @click="clearUploadError('banner')"
+        />
+    </div>
+  </div>
+  <div class="setting-item">
+    <h2>{{ $t('settings.profile_background') }}</h2>
+    <p>{{ $t('settings.set_new_profile_background') }}</p>
+    <img
+      v-if="backgroundPreview"
+      class="bg"
+      :src="backgroundPreview"
+      >
+    <div>
+      <input
+        type="file"
+        @change="uploadFile('background', $event)"
+        >
+    </div>
+    <i
+      v-if="backgroundUploading"
+      class=" icon-spin4 animate-spin uploading"
+      />
+    <button
+      v-else-if="backgroundPreview"
+      class="btn btn-default"
+      @click="submitBg"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <div
+      v-if="backgroundUploadError"
+      class="alert error"
+      >
+      Error: {{ backgroundUploadError }}
+      <i
+        class="button-icon icon-cancel"
+        @click="clearUploadError('background')"
+        />
+    </div>
+  </div>
+</div>
+</template>
+
+<script src="./profile_tab.js"></script>
+<style lang="scss" src="./profile_tab.scss"></style>
diff --git a/src/components/settings_modal/tabs/security.js b/src/components/settings_modal/tabs/security.js
deleted file mode 100644
index cc791b7a..00000000
--- a/src/components/settings_modal/tabs/security.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import ProgressButton from '../../progress_button/progress_button.vue'
-import Checkbox from '../../checkbox/checkbox.vue'
-import Mfa from '../../user_settings/mfa.vue'
-
-const Security = {
-  data () {
-    return {
-      newEmail: '',
-      changeEmailError: false,
-      changeEmailPassword: '',
-      changedEmail: false,
-      deletingAccount: false,
-      deleteAccountConfirmPasswordInput: '',
-      deleteAccountError: false,
-      changePasswordInputs: [ '', '', '' ],
-      changedPassword: false,
-      changePasswordError: false
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    ProgressButton,
-    Mfa,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    },
-    pleromaBackend () {
-      return this.$store.state.instance.pleromaBackend
-    },
-    oauthTokens () {
-      return this.$store.state.oauthTokens.tokens.map(oauthToken => {
-        return {
-          id: oauthToken.id,
-          appName: oauthToken.app_name,
-          validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
-        }
-      })
-    }
-  },
-  methods: {
-    confirmDelete () {
-      this.deletingAccount = true
-    },
-    deleteAccount () {
-      this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
-        .then((res) => {
-          if (res.status === 'success') {
-            this.$store.dispatch('logout')
-            this.$router.push({ name: 'root' })
-          } else {
-            this.deleteAccountError = res.error
-          }
-        })
-    },
-    changePassword () {
-      const params = {
-        password: this.changePasswordInputs[0],
-        newPassword: this.changePasswordInputs[1],
-        newPasswordConfirmation: this.changePasswordInputs[2]
-      }
-      this.$store.state.api.backendInteractor.changePassword(params)
-        .then((res) => {
-          if (res.status === 'success') {
-            this.changedPassword = true
-            this.changePasswordError = false
-            this.logout()
-          } else {
-            this.changedPassword = false
-            this.changePasswordError = res.error
-          }
-        })
-    },
-    changeEmail () {
-      const params = {
-        email: this.newEmail,
-        password: this.changeEmailPassword
-      }
-      this.$store.state.api.backendInteractor.changeEmail(params)
-        .then((res) => {
-          if (res.status === 'success') {
-            this.changedEmail = true
-            this.changeEmailError = false
-          } else {
-            this.changedEmail = false
-            this.changeEmailError = res.error
-          }
-        })
-    },
-    logout () {
-      this.$store.dispatch('logout')
-      this.$router.replace('/')
-    },
-    revokeToken (id) {
-      if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
-        this.$store.dispatch('revokeToken', id)
-      }
-    }
-  }
-}
-
-export default Security
diff --git a/src/components/settings_modal/tabs/security.vue b/src/components/settings_modal/tabs/security.vue
deleted file mode 100644
index 603c9a04..00000000
--- a/src/components/settings_modal/tabs/security.vue
+++ /dev/null
@@ -1,143 +0,0 @@
-<template>
-<div :label="$t('settings.security_tab')">
-  <div class="setting-item">
-    <h2>{{ $t('settings.change_email') }}</h2>
-    <div>
-      <p>{{ $t('settings.new_email') }}</p>
-      <input
-        v-model="newEmail"
-        type="email"
-        autocomplete="email"
-        >
-    </div>
-    <div>
-      <p>{{ $t('settings.current_password') }}</p>
-      <input
-        v-model="changeEmailPassword"
-        type="password"
-        autocomplete="current-password"
-        >
-    </div>
-    <button
-      class="btn btn-default"
-      @click="changeEmail"
-      >
-      {{ $t('general.submit') }}
-    </button>
-    <p v-if="changedEmail">
-      {{ $t('settings.changed_email') }}
-    </p>
-    <template v-if="changeEmailError !== false">
-      <p>{{ $t('settings.change_email_error') }}</p>
-      <p>{{ changeEmailError }}</p>
-    </template>
-  </div>
-
-  <div class="setting-item">
-    <h2>{{ $t('settings.change_password') }}</h2>
-    <div>
-      <p>{{ $t('settings.current_password') }}</p>
-      <input
-        v-model="changePasswordInputs[0]"
-        type="password"
-        >
-    </div>
-    <div>
-      <p>{{ $t('settings.new_password') }}</p>
-      <input
-        v-model="changePasswordInputs[1]"
-        type="password"
-        >
-    </div>
-    <div>
-      <p>{{ $t('settings.confirm_new_password') }}</p>
-      <input
-        v-model="changePasswordInputs[2]"
-        type="password"
-        >
-    </div>
-    <button
-      class="btn btn-default"
-      @click="changePassword"
-      >
-      {{ $t('general.submit') }}
-    </button>
-    <p v-if="changedPassword">
-      {{ $t('settings.changed_password') }}
-    </p>
-    <p v-else-if="changePasswordError !== false">
-      {{ $t('settings.change_password_error') }}
-    </p>
-    <p v-if="changePasswordError">
-      {{ changePasswordError }}
-    </p>
-  </div>
-
-  <div class="setting-item">
-    <h2>{{ $t('settings.oauth_tokens') }}</h2>
-    <table class="oauth-tokens">
-      <thead>
-        <tr>
-          <th>{{ $t('settings.app_name') }}</th>
-          <th>{{ $t('settings.valid_until') }}</th>
-          <th />
-        </tr>
-      </thead>
-      <tbody>
-        <tr
-          v-for="oauthToken in oauthTokens"
-          :key="oauthToken.id"
-          >
-          <td>{{ oauthToken.appName }}</td>
-          <td>{{ oauthToken.validUntil }}</td>
-          <td class="actions">
-            <button
-              class="btn btn-default"
-              @click="revokeToken(oauthToken.id)"
-              >
-              {{ $t('settings.revoke_token') }}
-            </button>
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
-  <mfa />
-  <div class="setting-item">
-    <h2>{{ $t('settings.delete_account') }}</h2>
-    <p v-if="!deletingAccount">
-      {{ $t('settings.delete_account_description') }}
-    </p>
-    <div v-if="deletingAccount">
-      <p>{{ $t('settings.delete_account_instructions') }}</p>
-      <p>{{ $t('login.password') }}</p>
-      <input
-        v-model="deleteAccountConfirmPasswordInput"
-        type="password"
-        >
-      <button
-        class="btn btn-default"
-        @click="deleteAccount"
-        >
-        {{ $t('settings.delete_account') }}
-      </button>
-    </div>
-    <p v-if="deleteAccountError !== false">
-      {{ $t('settings.delete_account_error') }}
-    </p>
-    <p v-if="deleteAccountError">
-      {{ deleteAccountError }}
-    </p>
-    <button
-      v-if="!deletingAccount"
-      class="btn btn-default"
-      @click="confirmDelete"
-      >
-      {{ $t('general.submit') }}
-    </button>
-  </div>
-</div>
-</template>
-
-<script src="./security.js"></script>
-<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/security_tab/confirm.js b/src/components/settings_modal/tabs/security_tab/confirm.js
new file mode 100644
index 00000000..0f4ddfc9
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/confirm.js
@@ -0,0 +1,9 @@
+const Confirm = {
+  props: ['disabled'],
+  data: () => ({}),
+  methods: {
+    confirm () { this.$emit('confirm') },
+    cancel () { this.$emit('cancel') }
+  }
+}
+export default Confirm
diff --git a/src/components/settings_modal/tabs/security_tab/confirm.vue b/src/components/settings_modal/tabs/security_tab/confirm.vue
new file mode 100644
index 00000000..69b3811b
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/confirm.vue
@@ -0,0 +1,22 @@
+<template>
+  <div>
+    <slot />
+    <button
+      class="btn btn-default"
+      :disabled="disabled"
+      @click="confirm"
+    >
+      {{ $t('general.confirm') }}
+    </button>
+    <button
+      class="btn btn-default"
+      :disabled="disabled"
+      @click="cancel"
+    >
+      {{ $t('general.cancel') }}
+    </button>
+  </div>
+</template>
+
+<script src="./confirm.js">
+</script>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.js b/src/components/settings_modal/tabs/security_tab/mfa.js
new file mode 100644
index 00000000..abf37062
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa.js
@@ -0,0 +1,155 @@
+import RecoveryCodes from './mfa_backup_codes.vue'
+import TOTP from './mfa_totp.vue'
+import Confirm from './confirm.vue'
+import VueQrcode from '@chenfengyuan/vue-qrcode'
+import { mapState } from 'vuex'
+
+const Mfa = {
+  data: () => ({
+    settings: { // current settings of MFA
+      available: false,
+      enabled: false,
+      totp: false
+    },
+    setupState: { // setup mfa
+      state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
+      setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
+    },
+    backupCodes: {
+      getNewCodes: false,
+      inProgress: false, //  progress of fetch codes
+      codes: []
+    },
+    otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
+      provisioning_uri: '',
+      key: ''
+    },
+    currentPassword: null,
+    otpConfirmToken: null,
+    error: null,
+    readyInit: false
+  }),
+  components: {
+    'recovery-codes': RecoveryCodes,
+    'totp-item': TOTP,
+    'qrcode': VueQrcode,
+    'confirm': Confirm
+  },
+  computed: {
+    canSetupOTP () {
+      return (
+        (this.setupInProgress && this.backupCodesPrepared) ||
+          this.settings.enabled
+      ) && !this.settings.totp && !this.setupOTPInProgress
+    },
+    setupInProgress () {
+      return this.setupState.state !== '' && this.setupState.state !== 'complete'
+    },
+    setupOTPInProgress () {
+      return this.setupState.state === 'setupOTP' && !this.completedOTP
+    },
+    prepareOTP () {
+      return this.setupState.setupOTPState === 'prepare'
+    },
+    confirmOTP () {
+      return this.setupState.setupOTPState === 'confirm'
+    },
+    completedOTP () {
+      return this.setupState.setupOTPState === 'completed'
+    },
+    backupCodesPrepared () {
+      return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
+    },
+    confirmNewBackupCodes () {
+      return this.backupCodes.getNewCodes
+    },
+    ...mapState({
+      backendInteractor: (state) => state.api.backendInteractor
+    })
+  },
+
+  methods: {
+    activateOTP () {
+      if (!this.settings.enabled) {
+        this.setupState.state = 'getBackupcodes'
+        this.fetchBackupCodes()
+      }
+    },
+    fetchBackupCodes () {
+      this.backupCodes.inProgress = true
+      this.backupCodes.codes = []
+
+      return this.backendInteractor.generateMfaBackupCodes()
+        .then((res) => {
+          this.backupCodes.codes = res.codes
+          this.backupCodes.inProgress = false
+        })
+    },
+    getBackupCodes () { // get a new backup codes
+      this.backupCodes.getNewCodes = true
+    },
+    confirmBackupCodes () { // confirm getting new backup codes
+      this.fetchBackupCodes().then((res) => {
+        this.backupCodes.getNewCodes = false
+      })
+    },
+    cancelBackupCodes () { // cancel confirm form of new backup codes
+      this.backupCodes.getNewCodes = false
+    },
+
+    // Setup OTP
+    setupOTP () { // prepare setup OTP
+      this.setupState.state = 'setupOTP'
+      this.setupState.setupOTPState = 'prepare'
+      this.backendInteractor.mfaSetupOTP()
+        .then((res) => {
+          this.otpSettings = res
+          this.setupState.setupOTPState = 'confirm'
+        })
+    },
+    doConfirmOTP () { // handler confirm enable OTP
+      this.error = null
+      this.backendInteractor.mfaConfirmOTP({
+        token: this.otpConfirmToken,
+        password: this.currentPassword
+      })
+        .then((res) => {
+          if (res.error) {
+            this.error = res.error
+            return
+          }
+          this.completeSetup()
+        })
+    },
+
+    completeSetup () {
+      this.setupState.setupOTPState = 'complete'
+      this.setupState.state = 'complete'
+      this.currentPassword = null
+      this.error = null
+      this.fetchSettings()
+    },
+    cancelSetup () { // cancel setup
+      this.setupState.setupOTPState = ''
+      this.setupState.state = ''
+      this.currentPassword = null
+      this.error = null
+    },
+    // end Setup OTP
+
+    // fetch settings from server
+    async fetchSettings () {
+      let result = await this.backendInteractor.settingsMFA()
+      if (result.error) return
+      this.settings = result.settings
+      this.settings.available = true
+      return result
+    }
+  },
+  mounted () {
+    this.fetchSettings().then(() => {
+      this.readyInit = true
+    })
+  }
+}
+export default Mfa
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue
new file mode 100644
index 00000000..25c4d1dc
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa.vue
@@ -0,0 +1,174 @@
+<template>
+  <div
+    v-if="readyInit && settings.available"
+    class="setting-item mfa-settings"
+  >
+    <div class="mfa-heading">
+      <h2>{{ $t('settings.mfa.title') }}</h2>
+    </div>
+
+    <div>
+      <div
+        v-if="!setupInProgress"
+        class="setting-item"
+      >
+        <!-- Enabled methods -->
+        <h3>{{ $t('settings.mfa.authentication_methods') }}</h3>
+        <totp-item
+          :settings="settings"
+          @deactivate="fetchSettings"
+          @activate="activateOTP"
+        />
+        <br>
+
+        <div v-if="settings.enabled">
+          <!-- backup codes block-->
+          <recovery-codes
+            v-if="!confirmNewBackupCodes"
+            :backup-codes="backupCodes"
+          />
+          <button
+            v-if="!confirmNewBackupCodes"
+            class="btn btn-default"
+            @click="getBackupCodes"
+          >
+            {{ $t('settings.mfa.generate_new_recovery_codes') }}
+          </button>
+
+          <div v-if="confirmNewBackupCodes">
+            <confirm
+              :disabled="backupCodes.inProgress"
+              @confirm="confirmBackupCodes"
+              @cancel="cancelBackupCodes"
+            >
+              <p class="warning">
+                {{ $t('settings.mfa.warning_of_generate_new_codes') }}
+              </p>
+            </confirm>
+          </div>
+        </div>
+      </div>
+
+      <div v-if="setupInProgress">
+        <!-- setup block-->
+
+        <h3>{{ $t('settings.mfa.setup_otp') }}</h3>
+
+        <recovery-codes
+          v-if="!setupOTPInProgress"
+          :backup-codes="backupCodes"
+        />
+
+        <button
+          v-if="canSetupOTP"
+          class="btn btn-default"
+          @click="cancelSetup"
+        >
+          {{ $t('general.cancel') }}
+        </button>
+
+        <button
+          v-if="canSetupOTP"
+          class="btn btn-default"
+          @click="setupOTP"
+        >
+          {{ $t('settings.mfa.setup_otp') }}
+        </button>
+
+        <template v-if="setupOTPInProgress">
+          <i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i>
+
+          <div v-if="confirmOTP">
+            <div class="setup-otp">
+              <div class="qr-code">
+                <h4>{{ $t('settings.mfa.scan.title') }}</h4>
+                <p>{{ $t('settings.mfa.scan.desc') }}</p>
+                <qrcode
+                  :value="otpSettings.provisioning_uri"
+                  :options="{ width: 200 }"
+                />
+                <p>
+                  {{ $t('settings.mfa.scan.secret_code') }}:
+                  {{ otpSettings.key }}
+                </p>
+              </div>
+
+              <div class="verify">
+                <h4>{{ $t('general.verify') }}</h4>
+                <p>{{ $t('settings.mfa.verify.desc') }}</p>
+                <input
+                  v-model="otpConfirmToken"
+                  type="text"
+                >
+
+                <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p>
+                <input
+                  v-model="currentPassword"
+                  type="password"
+                >
+                <div class="confirm-otp-actions">
+                  <button
+                    class="btn btn-default"
+                    @click="doConfirmOTP"
+                  >
+                    {{ $t('settings.mfa.confirm_and_enable') }}
+                  </button>
+                  <button
+                    class="btn btn-default"
+                    @click="cancelSetup"
+                  >
+                    {{ $t('general.cancel') }}
+                  </button>
+                </div>
+                <div
+                  v-if="error"
+                  class="alert error"
+                >
+                  {{ error }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./mfa.js"></script>
+<style lang="scss">
+@import '../../../../_variables.scss';
+.mfa-settings {
+  .mfa-heading, .method-item {
+    overflow: hidden;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    align-items: baseline;
+  }
+
+  .warning {
+    color: $fallback--cOrange;
+    color: var(--cOrange, $fallback--cOrange);
+  }
+
+  .setup-otp {
+    display: flex;
+    justify-content: center;
+    flex-wrap: wrap;
+    .qr-code {
+      flex: 1;
+      padding-right: 10px;
+    }
+    .verify { flex: 1; }
+    .error { margin: 4px 0 0 0; }
+    .confirm-otp-actions {
+      button {
+        width: 15em;
+        margin-top: 5px;
+      }
+
+    }
+  }
+}
+</style>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
new file mode 100644
index 00000000..f0a984ec
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
@@ -0,0 +1,17 @@
+export default {
+  props: {
+    backupCodes: {
+      type: Object,
+      default: () => ({
+        inProgress: false,
+        codes: []
+      })
+    }
+  },
+  data: () => ({}),
+  computed: {
+    inProgress () { return this.backupCodes.inProgress },
+    ready () { return this.backupCodes.codes.length > 0 },
+    displayTitle () { return this.inProgress || this.ready }
+  }
+}
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
new file mode 100644
index 00000000..d7e98b3c
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
@@ -0,0 +1,35 @@
+<template>
+  <div class="mfa-backup-codes">
+    <h4 v-if="displayTitle">
+      {{ $t('settings.mfa.recovery_codes') }}
+    </h4>
+    <i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i>
+    <template v-if="ready">
+      <p class="alert warning">
+        {{ $t('settings.mfa.recovery_codes_warning') }}
+      </p>
+      <ul class="backup-codes">
+        <li
+          v-for="code in backupCodes.codes"
+          :key="code"
+        >
+          {{ code }}
+        </li>
+      </ul>
+    </template>
+  </div>
+</template>
+<script src="./mfa_backup_codes.js"></script>
+<style lang="scss">
+@import '../../../../_variables.scss';
+
+.mfa-backup-codes {
+  .warning {
+    color: $fallback--cOrange;
+    color: var(--cOrange, $fallback--cOrange);
+  }
+  .backup-codes {
+    font-family: var(--postCodeFont, monospace);
+  }
+}
+</style>
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.js b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
new file mode 100644
index 00000000..8408d8e9
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
@@ -0,0 +1,49 @@
+import Confirm from './confirm.vue'
+import { mapState } from 'vuex'
+
+export default {
+  props: ['settings'],
+  data: () => ({
+    error: false,
+    currentPassword: '',
+    deactivate: false,
+    inProgress: false // progress peform request to disable otp method
+  }),
+  components: {
+    'confirm': Confirm
+  },
+  computed: {
+    isActivated () {
+      return this.settings.totp
+    },
+    ...mapState({
+      backendInteractor: (state) => state.api.backendInteractor
+    })
+  },
+  methods: {
+    doActivate () {
+      this.$emit('activate')
+    },
+    cancelDeactivate () { this.deactivate = false },
+    doDeactivate () {
+      this.error = null
+      this.deactivate = true
+    },
+    confirmDeactivate () { // confirm deactivate TOTP method
+      this.error = null
+      this.inProgress = true
+      this.backendInteractor.mfaDisableOTP({
+        password: this.currentPassword
+      })
+        .then((res) => {
+          this.inProgress = false
+          if (res.error) {
+            this.error = res.error
+            return
+          }
+          this.deactivate = false
+          this.$emit('deactivate')
+        })
+    }
+  }
+}
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
new file mode 100644
index 00000000..c6f2cc7b
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
@@ -0,0 +1,43 @@
+<template>
+  <div>
+    <div class="method-item">
+      <strong>{{ $t('settings.mfa.otp') }}</strong>
+      <button
+        v-if="!isActivated"
+        class="btn btn-default"
+        @click="doActivate"
+      >
+        {{ $t('general.enable') }}
+      </button>
+
+      <button
+        v-if="isActivated"
+        class="btn btn-default"
+        :disabled="deactivate"
+        @click="doDeactivate"
+      >
+        {{ $t('general.disable') }}
+      </button>
+    </div>
+
+    <confirm
+      v-if="deactivate"
+      :disabled="inProgress"
+      @confirm="confirmDeactivate"
+      @cancel="cancelDeactivate"
+    >
+      {{ $t('settings.enter_current_password_to_confirm') }}:
+      <input
+        v-model="currentPassword"
+        type="password"
+      >
+    </confirm>
+    <div
+      v-if="error"
+      class="alert error"
+    >
+      {{ error }}
+    </div>
+  </div>
+</template>
+<script src="./mfa_totp.js"></script>
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js
new file mode 100644
index 00000000..811161a5
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.js
@@ -0,0 +1,106 @@
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import Mfa from './mfa.vue'
+
+const SecurityTab = {
+  data () {
+    return {
+      newEmail: '',
+      changeEmailError: false,
+      changeEmailPassword: '',
+      changedEmail: false,
+      deletingAccount: false,
+      deleteAccountConfirmPasswordInput: '',
+      deleteAccountError: false,
+      changePasswordInputs: [ '', '', '' ],
+      changedPassword: false,
+      changePasswordError: false
+    }
+  },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
+  components: {
+    ProgressButton,
+    Mfa,
+    Checkbox
+  },
+  computed: {
+    user () {
+      return this.$store.state.users.currentUser
+    },
+    pleromaBackend () {
+      return this.$store.state.instance.pleromaBackend
+    },
+    oauthTokens () {
+      return this.$store.state.oauthTokens.tokens.map(oauthToken => {
+        return {
+          id: oauthToken.id,
+          appName: oauthToken.app_name,
+          validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
+        }
+      })
+    }
+  },
+  methods: {
+    confirmDelete () {
+      this.deletingAccount = true
+    },
+    deleteAccount () {
+      this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
+        .then((res) => {
+          if (res.status === 'success') {
+            this.$store.dispatch('logout')
+            this.$router.push({ name: 'root' })
+          } else {
+            this.deleteAccountError = res.error
+          }
+        })
+    },
+    changePassword () {
+      const params = {
+        password: this.changePasswordInputs[0],
+        newPassword: this.changePasswordInputs[1],
+        newPasswordConfirmation: this.changePasswordInputs[2]
+      }
+      this.$store.state.api.backendInteractor.changePassword(params)
+        .then((res) => {
+          if (res.status === 'success') {
+            this.changedPassword = true
+            this.changePasswordError = false
+            this.logout()
+          } else {
+            this.changedPassword = false
+            this.changePasswordError = res.error
+          }
+        })
+    },
+    changeEmail () {
+      const params = {
+        email: this.newEmail,
+        password: this.changeEmailPassword
+      }
+      this.$store.state.api.backendInteractor.changeEmail(params)
+        .then((res) => {
+          if (res.status === 'success') {
+            this.changedEmail = true
+            this.changeEmailError = false
+          } else {
+            this.changedEmail = false
+            this.changeEmailError = res.error
+          }
+        })
+    },
+    logout () {
+      this.$store.dispatch('logout')
+      this.$router.replace('/')
+    },
+    revokeToken (id) {
+      if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
+        this.$store.dispatch('revokeToken', id)
+      }
+    }
+  }
+}
+
+export default SecurityTab
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue
new file mode 100644
index 00000000..45bacec1
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue
@@ -0,0 +1,143 @@
+<template>
+<div :label="$t('settings.security_tab')">
+  <div class="setting-item">
+    <h2>{{ $t('settings.change_email') }}</h2>
+    <div>
+      <p>{{ $t('settings.new_email') }}</p>
+      <input
+        v-model="newEmail"
+        type="email"
+        autocomplete="email"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.current_password') }}</p>
+      <input
+        v-model="changeEmailPassword"
+        type="password"
+        autocomplete="current-password"
+        >
+    </div>
+    <button
+      class="btn btn-default"
+      @click="changeEmail"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <p v-if="changedEmail">
+      {{ $t('settings.changed_email') }}
+    </p>
+    <template v-if="changeEmailError !== false">
+      <p>{{ $t('settings.change_email_error') }}</p>
+      <p>{{ changeEmailError }}</p>
+    </template>
+  </div>
+
+  <div class="setting-item">
+    <h2>{{ $t('settings.change_password') }}</h2>
+    <div>
+      <p>{{ $t('settings.current_password') }}</p>
+      <input
+        v-model="changePasswordInputs[0]"
+        type="password"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.new_password') }}</p>
+      <input
+        v-model="changePasswordInputs[1]"
+        type="password"
+        >
+    </div>
+    <div>
+      <p>{{ $t('settings.confirm_new_password') }}</p>
+      <input
+        v-model="changePasswordInputs[2]"
+        type="password"
+        >
+    </div>
+    <button
+      class="btn btn-default"
+      @click="changePassword"
+      >
+      {{ $t('general.submit') }}
+    </button>
+    <p v-if="changedPassword">
+      {{ $t('settings.changed_password') }}
+    </p>
+    <p v-else-if="changePasswordError !== false">
+      {{ $t('settings.change_password_error') }}
+    </p>
+    <p v-if="changePasswordError">
+      {{ changePasswordError }}
+    </p>
+  </div>
+
+  <div class="setting-item">
+    <h2>{{ $t('settings.oauth_tokens') }}</h2>
+    <table class="oauth-tokens">
+      <thead>
+        <tr>
+          <th>{{ $t('settings.app_name') }}</th>
+          <th>{{ $t('settings.valid_until') }}</th>
+          <th />
+        </tr>
+      </thead>
+      <tbody>
+        <tr
+          v-for="oauthToken in oauthTokens"
+          :key="oauthToken.id"
+          >
+          <td>{{ oauthToken.appName }}</td>
+          <td>{{ oauthToken.validUntil }}</td>
+          <td class="actions">
+            <button
+              class="btn btn-default"
+              @click="revokeToken(oauthToken.id)"
+              >
+              {{ $t('settings.revoke_token') }}
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+  <mfa />
+  <div class="setting-item">
+    <h2>{{ $t('settings.delete_account') }}</h2>
+    <p v-if="!deletingAccount">
+      {{ $t('settings.delete_account_description') }}
+    </p>
+    <div v-if="deletingAccount">
+      <p>{{ $t('settings.delete_account_instructions') }}</p>
+      <p>{{ $t('login.password') }}</p>
+      <input
+        v-model="deleteAccountConfirmPasswordInput"
+        type="password"
+        >
+      <button
+        class="btn btn-default"
+        @click="deleteAccount"
+        >
+        {{ $t('settings.delete_account') }}
+      </button>
+    </div>
+    <p v-if="deleteAccountError !== false">
+      {{ $t('settings.delete_account_error') }}
+    </p>
+    <p v-if="deleteAccountError">
+      {{ deleteAccountError }}
+    </p>
+    <button
+      v-if="!deletingAccount"
+      class="btn btn-default"
+      @click="confirmDelete"
+      >
+      {{ $t('general.submit') }}
+    </button>
+  </div>
+</div>
+</template>
+
+<script src="./security_tab.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
new file mode 100644
index 00000000..9d984659
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/preview.vue
@@ -0,0 +1,117 @@
+<template>
+  <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>
+        <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 still-image">
+            ( ͡° ͜ʖ ͡°)
+          </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>
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
new file mode 100644
index 00000000..9d61b0c4
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -0,0 +1,759 @@
+import { set, delete as del } from 'vue'
+import {
+  rgb2hex,
+  hex2rgb,
+  getContrastRatioLayers
+} from 'src/services/color_convert/color_convert.js'
+import {
+  DEFAULT_SHADOWS,
+  generateColors,
+  generateShadows,
+  generateRadii,
+  generateFonts,
+  composePreset,
+  getThemes,
+  shadows2to3,
+  colors2to3
+} from 'src/services/style_setter/style_setter.js'
+import {
+  SLOT_INHERITANCE
+} from 'src/services/theme_data/pleromafe.js'
+import {
+  CURRENT_VERSION,
+  OPACITIES,
+  getLayers,
+  getOpacitySlot
+} from 'src/services/theme_data/theme_data.service.js'
+import ColorInput from 'src/components/color_input/color_input.vue'
+import RangeInput from 'src/components/range_input/range_input.vue'
+import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
+import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
+import FontControl from 'src/components/font_control/font_control.vue'
+import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import ExportImport from 'src/components/export_import/export_import.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+import Preview from './preview.vue'
+
+// List of color values used in v1
+const v1OnlyNames = [
+  'bg',
+  'fg',
+  'text',
+  'link',
+  'cRed',
+  'cGreen',
+  'cBlue',
+  '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,
+      engineVersion: 0,
+
+      previewShadows: {},
+      previewColors: {},
+      previewRadii: {},
+      previewFonts: {},
+
+      shadowsInvalid: true,
+      colorsInvalid: true,
+      radiiInvalid: true,
+
+      keepColor: false,
+      keepShadows: false,
+      keepOpacity: false,
+      keepRoundness: false,
+      keepFonts: false,
+
+      ...Object.keys(SLOT_INHERITANCE)
+        .map(key => [key, ''])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
+
+      ...Object.keys(OPACITIES)
+        .map(key => [key, ''])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
+
+      shadowSelected: undefined,
+      shadowsLocal: {},
+      fontsLocal: {},
+
+      btnRadiusLocal: '',
+      inputRadiusLocal: '',
+      checkboxRadiusLocal: '',
+      panelRadiusLocal: '',
+      avatarRadiusLocal: '',
+      avatarAltRadiusLocal: '',
+      attachmentRadiusLocal: '',
+      tooltipRadiusLocal: ''
+    }
+  },
+  created () {
+    const self = this
+
+    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.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 Object.keys(SLOT_INHERITANCE)
+        .map(key => [key, this[key + 'ColorLocal']])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
+    },
+    currentOpacity () {
+      return Object.keys(OPACITIES)
+        .map(key => [key, this[key + 'OpacityLocal']])
+        .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
+    },
+    currentRadii () {
+      return {
+        btn: this.btnRadiusLocal,
+        input: this.inputRadiusLocal,
+        checkbox: this.checkboxRadiusLocal,
+        panel: this.panelRadiusLocal,
+        avatar: this.avatarRadiusLocal,
+        avatarAlt: this.avatarAltRadiusLocal,
+        tooltip: this.tooltipRadiusLocal,
+        attachment: this.attachmentRadiusLocal
+      }
+    },
+    preview () {
+      return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
+    },
+    previewTheme () {
+      if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
+      return this.preview.theme
+    },
+    // This needs optimization maybe
+    previewContrast () {
+      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) }), {})
+
+        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'] : [])
+          ]
+
+          const layers = getLayers(
+            layer,
+            variant || layer,
+            opacitySlot,
+            colorsConverted,
+            opacity
+          )
+
+          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)
+      }
+    },
+    previewRules () {
+      if (!this.preview.rules) return ''
+      return [
+        ...Object.values(this.preview.rules),
+        'color: var(--text)',
+        'font-family: var(--interfaceFont, sans-serif)'
+      ].join(';')
+    },
+    shadowsAvailable () {
+      return Object.keys(DEFAULT_SHADOWS).sort()
+    },
+    currentShadowOverriden: {
+      get () {
+        return !!this.currentShadow
+      },
+      set (val) {
+        if (val) {
+          set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
+        } else {
+          del(this.shadowsLocal, this.shadowSelected)
+        }
+      }
+    },
+    currentShadowFallback () {
+      return (this.previewTheme.shadows || {})[this.shadowSelected]
+    },
+    currentShadow: {
+      get () {
+        return this.shadowsLocal[this.shadowSelected]
+      },
+      set (v) {
+        set(this.shadowsLocal, this.shadowSelected, v)
+      }
+    },
+    themeValid () {
+      return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
+    },
+    exportedTheme () {
+      const saveEverything = (
+        !this.keepFonts &&
+        !this.keepShadows &&
+        !this.keepOpacity &&
+        !this.keepRoundness &&
+        !this.keepColor
+      )
+
+      const source = {
+        themeEngineVersion: CURRENT_VERSION
+      }
+
+      if (this.keepFonts || saveEverything) {
+        source.fonts = this.fontsLocal
+      }
+      if (this.keepShadows || saveEverything) {
+        source.shadows = this.shadowsLocal
+      }
+      if (this.keepOpacity || saveEverything) {
+        source.opacity = this.currentOpacity
+      }
+      if (this.keepColor || saveEverything) {
+        source.colors = this.currentColors
+      }
+      if (this.keepRoundness || saveEverything) {
+        source.radii = this.currentRadii
+      }
+
+      const theme = {
+        themeEngineVersion: CURRENT_VERSION,
+        ...this.previewTheme
+      }
+
+      return {
+        // To separate from other random JSON files and possible future source formats
+        _pleroma_theme_version: 2, theme, source
+      }
+    }
+  },
+  components: {
+    ColorInput,
+    OpacityInput,
+    RangeInput,
+    ContrastRatio,
+    ShadowControl,
+    FontControl,
+    TabSwitcher,
+    Preview,
+    ExportImport,
+    Checkbox
+  },
+  methods: {
+    loadTheme (
+      {
+        theme,
+        source,
+        _pleroma_theme_version: fileVersion
+      },
+      origin,
+      forceUseSource = false
+    ) {
+      this.dismissWarning()
+      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,
+          colors: this.currentColors,
+          radii: this.currentRadii
+        }
+      })
+    },
+    updatePreviewColorsAndShadows () {
+      this.previewColors = generateColors({
+        opacity: this.currentOpacity,
+        colors: this.currentColors
+      })
+      this.previewShadows = generateShadows(
+        { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
+        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 () {
+      this.loadThemeFromLocalStorage()
+    },
+
+    // Clears all the extra stuff when loading V1 theme
+    clearV1 () {
+      Object.keys(this.$data)
+        .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
+        .filter(_ => !v1OnlyNames.includes(_))
+        .forEach(key => {
+          set(this.$data, key, undefined)
+        })
+    },
+
+    clearRoundness () {
+      Object.keys(this.$data)
+        .filter(_ => _.endsWith('RadiusLocal'))
+        .forEach(key => {
+          set(this.$data, key, undefined)
+        })
+    },
+
+    clearOpacity () {
+      Object.keys(this.$data)
+        .filter(_ => _.endsWith('OpacityLocal'))
+        .forEach(key => {
+          set(this.$data, key, undefined)
+        })
+    },
+
+    clearShadows () {
+      this.shadowsLocal = {}
+    },
+
+    clearFonts () {
+      this.fontsLocal = {}
+    },
+
+    /**
+     * 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} 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 (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
+        // Old v1 naming: fg is text, btn is foreground
+        if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
+          version = 1
+        }
+        // New v2 naming: text is text, fg is foreground
+        if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
+          version = 2
+        }
+      }
+
+      this.engineVersion = version
+
+      // Stuff that differs between V1 and V2
+      if (version === 1) {
+        this.fgColorLocal = rgb2hex(colors.btn)
+        this.textColorLocal = rgb2hex(colors.fg)
+      }
+
+      if (!this.keepColor) {
+        this.clearV1()
+        const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
+        if (version === 1 || version === 'l1') {
+          keys
+            .add('bg')
+            .add('link')
+            .add('cRed')
+            .add('cBlue')
+            .add('cGreen')
+            .add('cOrange')
+        }
+
+        keys.forEach(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
+        })
+      }
+
+      if (!this.keepRoundness) {
+        this.clearRoundness()
+        Object.entries(radii).forEach(([k, v]) => {
+          // 'Radius' is kept mostly for v1->v2 localstorage transition
+          const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
+          this[key + 'RadiusLocal'] = v
+        })
+      }
+
+      if (!this.keepShadows) {
+        this.clearShadows()
+        if (version === 2) {
+          this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
+        } else {
+          this.shadowsLocal = shadows
+        }
+        this.shadowSelected = this.shadowsAvailable[0]
+      }
+
+      if (!this.keepFonts) {
+        this.clearFonts()
+        this.fontsLocal = fonts
+      }
+    }
+  },
+  watch: {
+    currentRadii () {
+      try {
+        this.previewRadii = generateRadii({ radii: this.currentRadii })
+        this.radiiInvalid = false
+      } catch (e) {
+        this.radiiInvalid = true
+        console.warn(e)
+      }
+    },
+    shadowsLocal: {
+      handler () {
+        if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
+        try {
+          this.updatePreviewColorsAndShadows()
+          this.shadowsInvalid = false
+        } catch (e) {
+          this.shadowsInvalid = true
+          console.warn(e)
+        }
+      },
+      deep: true
+    },
+    fontsLocal: {
+      handler () {
+        try {
+          this.previewFonts = generateFonts({ fonts: this.fontsLocal })
+          this.fontsInvalid = false
+        } catch (e) {
+          this.fontsInvalid = true
+          console.warn(e)
+        }
+      },
+      deep: true
+    },
+    currentColors () {
+      try {
+        this.updatePreviewColorsAndShadows()
+        this.colorsInvalid = false
+        this.shadowsInvalid = false
+      } catch (e) {
+        this.colorsInvalid = true
+        this.shadowsInvalid = true
+        console.warn(e)
+      }
+    },
+    currentOpacity () {
+      try {
+        this.updatePreviewColorsAndShadows()
+      } catch (e) {
+        console.warn(e)
+      }
+    },
+    selected () {
+      this.dismissWarning()
+      if (this.selectedVersion === 1) {
+        if (!this.keepRoundness) {
+          this.clearRoundness()
+        }
+
+        if (!this.keepShadows) {
+          this.clearShadows()
+        }
+
+        if (!this.keepOpacity) {
+          this.clearOpacity()
+        }
+
+        if (!this.keepColor) {
+          this.clearV1()
+
+          this.bgColorLocal = this.selected[1]
+          this.fgColorLocal = this.selected[2]
+          this.textColorLocal = this.selected[3]
+          this.linkColorLocal = this.selected[4]
+          this.cRedColorLocal = this.selected[5]
+          this.cGreenColorLocal = this.selected[6]
+          this.cBlueColorLocal = this.selected[7]
+          this.cOrangeColorLocal = this.selected[8]
+        }
+      } else if (this.selectedVersion >= 2) {
+        this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
+      }
+    }
+  }
+}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
new file mode 100644
index 00000000..75b3017d
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -0,0 +1,339 @@
+@import 'src/_variables.scss';
+.theme-tab {
+  .theme-warning {
+    display: flex;
+    align-items: baseline;
+    margin-bottom: .5em;
+    .buttons {
+      .btn {
+        margin-bottom: .5em;
+      }
+    }
+  }
+  .preset-switcher {
+    margin-right: 1em;
+  }
+
+  .style-control {
+    display: flex;
+    align-items: baseline;
+    margin-bottom: 5px;
+
+    .label {
+      flex: 1;
+    }
+
+    &.disabled {
+      input, select {
+        opacity: .5
+      }
+    }
+
+    .opt {
+      margin: .5em;
+    }
+
+    .color-input {
+      flex: 0 0 0;
+    }
+
+    input, select {
+      min-width: 3em;
+      margin: 0;
+      flex: 0;
+
+      &[type=number] {
+        min-width: 5em;
+      }
+
+      &[type=range] {
+        flex: 1;
+        min-width: 3em;
+        align-self: flex-start;
+      }
+    }
+  }
+
+  .reset-container {
+    flex-wrap: wrap;
+  }
+
+  .fonts-container,
+  .reset-container,
+  .apply-container,
+  .radius-container,
+  .color-container,
+  {
+    display: flex;
+  }
+
+  .fonts-container,
+  .radius-container {
+    flex-direction: column;
+  }
+
+  .color-container{
+    > h4 {
+      width: 99%;
+    }
+    flex-wrap: wrap;
+    justify-content: space-between;
+  }
+
+  .fonts-container,
+  .color-container,
+  .shadow-container,
+  .radius-container,
+  .presets-container {
+    margin: 1em 1em 0;
+  }
+
+  .tab-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: baseline;
+    width: 100%;
+    min-height: 30px;
+
+    .btn {
+      min-width: 1px;
+      flex: 0 auto;
+      padding: 0 1em;
+    }
+
+    p {
+      flex: 1;
+      margin: 0;
+      margin-right: .5em;
+    }
+
+    margin-bottom: 1em;
+  }
+
+  .shadow-selector {
+    .override {
+      flex: 1;
+      margin-left: .5em;
+    }
+    .select-container {
+      margin-top: -4px;
+      margin-bottom: -3px;
+    }
+  }
+
+  .save-load,
+  .save-load-options {
+    display: flex;
+    justify-content: center;
+    align-items: baseline;
+    flex-wrap: wrap;
+
+    .presets,
+    .import-export {
+      margin-bottom: .5em;
+    }
+
+    .import-export {
+      display: flex;
+    }
+
+    .override {
+      margin-left: .5em;
+    }
+  }
+
+  .save-load-options {
+    flex-wrap: wrap;
+    margin-top: .5em;
+    justify-content: center;
+    .keep-option {
+      margin: 0 .5em .5em;
+      min-width: 25%;
+    }
+  }
+
+  .preview-container {
+    border-top: 1px dashed;
+    border-bottom: 1px dashed;
+    border-color: $fallback--border;
+    border-color: var(--border, $fallback--border);
+    margin: 1em 0;
+    padding: 1em;
+    background: var(--body-background-image);
+    background-size: cover;
+    background-position: 50% 50%;
+
+    .dummy {
+      .post {
+        font-family: var(--postFont);
+        display: flex;
+
+        .content {
+          flex: 1;
+
+          h4 {
+            margin-bottom: .25em;
+          }
+
+          .icons {
+            margin-top: .5em;
+            display: flex;
+
+            i {
+              margin-right: 1em;
+            }
+          }
+        }
+      }
+
+      .after-post {
+        margin-top: 1em;
+        display: flex;
+        align-items: center;
+      }
+
+      .avatar, .avatar-alt{
+        background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
+        color: black;
+        font-family: sans-serif;
+        text-align: center;
+        margin-right: 1em;
+      }
+
+      .avatar-alt {
+        flex: 0 auto;
+        margin-left: 28px;
+        font-size: 12px;
+        min-width: 20px;
+        min-height: 20px;
+        line-height: 20px;
+        border-radius: $fallback--avatarAltRadius;
+        border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+      }
+
+      .avatar {
+        flex: 0 auto;
+        width: 48px;
+        height: 48px;
+        font-size: 14px;
+        line-height: 48px;
+      }
+
+      .actions {
+        display: flex;
+        align-items: baseline;
+
+        .checkbox {
+          display: inline-flex;
+          align-items: baseline;
+          margin-right: 1em;
+          flex: 1;
+        }
+      }
+
+      .separator {
+        margin: 1em;
+        border-bottom: 1px solid;
+        border-color: $fallback--border;
+        border-color: var(--border, $fallback--border);
+      }
+
+      .panel-heading {
+        .badge, .alert, .btn, .faint {
+          margin-left: 1em;
+          white-space: nowrap;
+        }
+        .faint {
+          text-overflow: ellipsis;
+          min-width: 2em;
+          overflow-x: hidden;
+        }
+        .flex-spacer {
+          flex: 1;
+        }
+      }
+      .btn {
+        margin-left: 0;
+        padding: 0 1em;
+        min-width: 3em;
+        min-height: 30px;
+      }
+    }
+  }
+
+  .apply-container {
+    justify-content: center;
+  }
+
+  .radius-item,
+  .color-item {
+    min-width: 20em;
+    margin: 5px 6px 0 0;
+    display:flex;
+    flex-direction: column;
+    flex: 1 1 0;
+
+    &.wide {
+      min-width: 60%
+    }
+
+    &:not(.wide):nth-child(2n+1) {
+      margin-right: 7px;
+
+    }
+
+    .color, .opacity {
+      display:flex;
+      align-items: baseline;
+    }
+  }
+
+  .radius-item {
+    flex-basis: auto;
+  }
+
+  .theme-radius-rn,
+  .theme-color-cl {
+    border: 0;
+    box-shadow: none;
+    background: transparent;
+    color: var(--faint, $fallback--faint);
+    align-self: stretch;
+  }
+
+  .theme-color-cl,
+  .theme-radius-in,
+  .theme-color-in {
+    margin-left: 4px;
+  }
+
+  .theme-radius-in {
+    min-width: 1em;
+  }
+
+  .theme-radius-in {
+    max-width: 7em;
+    flex: 1;
+  }
+
+  .theme-radius-lb{
+    max-width: 50em;
+  }
+
+  .theme-preview-content {
+    padding: 20px;
+  }
+
+  .apply-container {
+    .btn {
+      min-height: 28px;
+      min-width: 10em;
+      padding: 0 2em;
+    }
+  }
+
+  .btn {
+    margin-left: .25em;
+    margin-right: .25em;
+  }
+}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
new file mode 100644
index 00000000..6f6cf1d6
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -0,0 +1,956 @@
+<template>
+<div class="theme-tab">
+  <div class="presets-container">
+    <div class="save-load">
+      <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;)"
+        :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
+        :on-import="onImport"
+        :validator="importValidator"
+        >
+        <template slot="before">
+          <div class="presets">
+            {{ $t('settings.presets') }}
+            <label
+              for="preset-switcher"
+              class="select"
+              >
+              <select
+                id="preset-switcher"
+                v-model="selected"
+                class="preset-switcher"
+                >
+                <option
+                  v-for="style in availableStyles"
+                  :key="style.name"
+                  :value="style"
+                  :style="{
+                          backgroundColor: style[1] || (style.theme || style.source).colors.bg,
+                          color: style[3] || (style.theme || style.source).colors.text
+                          }"
+                  >
+                  {{ style[0] || style.name }}
+                </option>
+              </select>
+              <i class="icon-down-open" />
+            </label>
+          </div>
+        </template>
+      </ExportImport>
+      </div>
+      <div class="save-load-options">
+        <span class="keep-option">
+          <Checkbox v-model="keepColor">
+            {{ $t('settings.style.switcher.keep_color') }}
+          </Checkbox>
+        </span>
+        <span class="keep-option">
+          <Checkbox v-model="keepShadows">
+            {{ $t('settings.style.switcher.keep_shadows') }}
+          </Checkbox>
+        </span>
+        <span class="keep-option">
+          <Checkbox v-model="keepOpacity">
+            {{ $t('settings.style.switcher.keep_opacity') }}
+          </Checkbox>
+        </span>
+        <span class="keep-option">
+          <Checkbox v-model="keepRoundness">
+            {{ $t('settings.style.switcher.keep_roundness') }}
+          </Checkbox>
+        </span>
+        <span class="keep-option">
+          <Checkbox v-model="keepFonts">
+            {{ $t('settings.style.switcher.keep_fonts') }}
+          </Checkbox>
+        </span>
+        <p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
+      </div>
+    </div>
+
+    <preview :style="previewRules" />
+
+    <keep-alive>
+      <tab-switcher key="style-tweak">
+        <div
+          :label="$t('settings.style.common_colors._tab_label')"
+          class="color-container"
+        >
+          <div class="tab-header">
+            <p>{{ $t('settings.theme_help') }}</p>
+            <button
+              class="btn"
+              @click="clearOpacity"
+            >
+              {{ $t('settings.style.switcher.clear_opacity') }}
+            </button>
+            <button
+              class="btn"
+              @click="clearV1"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <p>{{ $t('settings.theme_help_v2_1') }}</p>
+          <h4>{{ $t('settings.style.common_colors.main') }}</h4>
+          <div class="color-item">
+            <ColorInput
+              v-model="bgColorLocal"
+              name="bgColor"
+              :label="$t('settings.background')"
+            />
+            <OpacityInput
+              v-model="bgOpacityLocal"
+              name="bgOpacity"
+              :fallback="previewTheme.opacity.bg"
+            />
+            <ColorInput
+              v-model="textColorLocal"
+              name="textColor"
+              :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>
+          <div class="color-item">
+            <ColorInput
+              v-model="fgColorLocal"
+              name="fgColor"
+              :label="$t('settings.foreground')"
+            />
+            <ColorInput
+              v-model="fgTextColorLocal"
+              name="fgTextColor"
+              :label="$t('settings.text')"
+              :fallback="previewTheme.colors.fgText"
+            />
+            <ColorInput
+              v-model="fgLinkColorLocal"
+              name="fgLinkColor"
+              :label="$t('settings.links')"
+              :fallback="previewTheme.colors.fgLink"
+            />
+            <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
+          </div>
+          <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
+          <div class="color-item">
+            <ColorInput
+              v-model="cRedColorLocal"
+              name="cRedColor"
+              :label="$t('settings.cRed')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgCRed" />
+            <ColorInput
+              v-model="cBlueColorLocal"
+              name="cBlueColor"
+              :label="$t('settings.cBlue')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgCBlue" />
+          </div>
+          <div class="color-item">
+            <ColorInput
+              v-model="cGreenColorLocal"
+              name="cGreenColor"
+              :label="$t('settings.cGreen')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgCGreen" />
+            <ColorInput
+              v-model="cOrangeColorLocal"
+              name="cOrangeColor"
+              :label="$t('settings.cOrange')"
+            />
+            <ContrastRatio :contrast="previewContrast.bgCOrange" />
+          </div>
+          <p>{{ $t('settings.theme_help_v2_2') }}</p>
+        </div>
+
+        <div
+          :label="$t('settings.style.advanced_colors._tab_label')"
+          class="color-container"
+        >
+          <div class="tab-header">
+            <p>{{ $t('settings.theme_help') }}</p>
+            <button
+              class="btn"
+              @click="clearOpacity"
+            >
+              {{ $t('settings.style.switcher.clear_opacity') }}
+            </button>
+            <button
+              class="btn"
+              @click="clearV1"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </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"
+              name="alertError"
+              :label="$t('settings.style.advanced_colors.alert_error')"
+              :fallback="previewTheme.colors.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"
+            />
+            <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>
+            <ColorInput
+              v-model="badgeNotificationColorLocal"
+              name="badgeNotification"
+              :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="previewTheme.colors.panel"
+              :label="$t('settings.background')"
+            />
+            <OpacityInput
+              v-model="panelOpacityLocal"
+              name="panelOpacity"
+              :fallback="previewTheme.opacity.panel"
+              :disabled="panelColorLocal === 'transparent'"
+            />
+            <ColorInput
+              v-model="panelTextColorLocal"
+              name="panelTextColor"
+              :fallback="previewTheme.colors.panelText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio
+              :contrast="previewContrast.panelText"
+              large="true"
+            />
+            <ColorInput
+              v-model="panelLinkColorLocal"
+              name="panelLinkColor"
+              :fallback="previewTheme.colors.panelLink"
+              :label="$t('settings.links')"
+            />
+            <ContrastRatio
+              :contrast="previewContrast.panelLink"
+              large="true"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
+            <ColorInput
+              v-model="topBarColorLocal"
+              name="topBarColor"
+              :fallback="previewTheme.colors.topBar"
+              :label="$t('settings.background')"
+            />
+            <ColorInput
+              v-model="topBarTextColorLocal"
+              name="topBarTextColor"
+              :fallback="previewTheme.colors.topBarText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.topBarText" />
+            <ColorInput
+              v-model="topBarLinkColorLocal"
+              name="topBarLinkColor"
+              :fallback="previewTheme.colors.topBarLink"
+              :label="$t('settings.links')"
+            />
+            <ContrastRatio :contrast="previewContrast.topBarLink" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
+            <ColorInput
+              v-model="inputColorLocal"
+              name="inputColor"
+              :fallback="previewTheme.colors.input"
+              :label="$t('settings.background')"
+            />
+            <OpacityInput
+              v-model="inputOpacityLocal"
+              name="inputOpacity"
+              :fallback="previewTheme.opacity.input"
+              :disabled="inputColorLocal === 'transparent'"
+            />
+            <ColorInput
+              v-model="inputTextColorLocal"
+              name="inputTextColor"
+              :fallback="previewTheme.colors.inputText"
+              :label="$t('settings.text')"
+            />
+            <ContrastRatio :contrast="previewContrast.inputText" />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
+            <ColorInput
+              v-model="btnColorLocal"
+              name="btnColor"
+              :fallback="previewTheme.colors.btn"
+              :label="$t('settings.background')"
+            />
+            <OpacityInput
+              v-model="btnOpacityLocal"
+              name="btnOpacity"
+              :fallback="previewTheme.opacity.btn"
+              :disabled="btnColorLocal === 'transparent'"
+            />
+            <ColorInput
+              v-model="btnTextColorLocal"
+              name="btnTextColor"
+              :fallback="previewTheme.colors.btnText"
+              :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" />
+            <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
+            <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" />
+            <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
+            <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')"
+            />
+            <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
+            <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>
+            <ColorInput
+              v-model="borderColorLocal"
+              name="borderColor"
+              :fallback="previewTheme.colors.border"
+              :label="$t('settings.style.common.color')"
+            />
+            <OpacityInput
+              v-model="borderOpacityLocal"
+              name="borderOpacity"
+              :fallback="previewTheme.opacity.border"
+              :disabled="borderColorLocal === 'transparent'"
+            />
+          </div>
+          <div class="color-item">
+            <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
+            <ColorInput
+              v-model="faintColorLocal"
+              name="faintColor"
+              :fallback="previewTheme.colors.faint"
+              :label="$t('settings.text')"
+            />
+            <ColorInput
+              v-model="faintLinkColorLocal"
+              name="faintLinkColor"
+              :fallback="previewTheme.colors.faintLink"
+              :label="$t('settings.links')"
+            />
+            <ColorInput
+              v-model="panelFaintColorLocal"
+              name="panelFaintColor"
+              :fallback="previewTheme.colors.panelFaint"
+              :label="$t('settings.style.advanced_colors.panel_header')"
+            />
+            <OpacityInput
+              v-model="faintOpacityLocal"
+              name="faintOpacity"
+              :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
+          :label="$t('settings.style.radii._tab_label')"
+          class="radius-container"
+        >
+          <div class="tab-header">
+            <p>{{ $t('settings.radii_help') }}</p>
+            <button
+              class="btn"
+              @click="clearRoundness"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <RangeInput
+            v-model="btnRadiusLocal"
+            name="btnRadius"
+            :label="$t('settings.btnRadius')"
+            :fallback="previewTheme.radii.btn"
+            max="16"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="inputRadiusLocal"
+            name="inputRadius"
+            :label="$t('settings.inputRadius')"
+            :fallback="previewTheme.radii.input"
+            max="9"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="checkboxRadiusLocal"
+            name="checkboxRadius"
+            :label="$t('settings.checkboxRadius')"
+            :fallback="previewTheme.radii.checkbox"
+            max="16"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="panelRadiusLocal"
+            name="panelRadius"
+            :label="$t('settings.panelRadius')"
+            :fallback="previewTheme.radii.panel"
+            max="50"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="avatarRadiusLocal"
+            name="avatarRadius"
+            :label="$t('settings.avatarRadius')"
+            :fallback="previewTheme.radii.avatar"
+            max="28"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="avatarAltRadiusLocal"
+            name="avatarAltRadius"
+            :label="$t('settings.avatarAltRadius')"
+            :fallback="previewTheme.radii.avatarAlt"
+            max="28"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="attachmentRadiusLocal"
+            name="attachmentRadius"
+            :label="$t('settings.attachmentRadius')"
+            :fallback="previewTheme.radii.attachment"
+            max="50"
+            hard-min="0"
+          />
+          <RangeInput
+            v-model="tooltipRadiusLocal"
+            name="tooltipRadius"
+            :label="$t('settings.tooltipRadius')"
+            :fallback="previewTheme.radii.tooltip"
+            max="50"
+            hard-min="0"
+          />
+        </div>
+
+        <div
+          :label="$t('settings.style.shadows._tab_label')"
+          class="shadow-container"
+        >
+          <div class="tab-header shadow-selector">
+            <div class="select-container">
+              {{ $t('settings.style.shadows.component') }}
+              <label
+                for="shadow-switcher"
+                class="select"
+              >
+                <select
+                  id="shadow-switcher"
+                  v-model="shadowSelected"
+                  class="shadow-switcher"
+                >
+                  <option
+                    v-for="shadow in shadowsAvailable"
+                    :key="shadow"
+                    :value="shadow"
+                  >
+                    {{ $t('settings.style.shadows.components.' + shadow) }}
+                  </option>
+                </select>
+                <i class="icon-down-open" />
+              </label>
+            </div>
+            <div class="override">
+              <label
+                for="override"
+                class="label"
+              >
+                {{ $t('settings.style.shadows.override') }}
+              </label>
+              <input
+                id="override"
+                v-model="currentShadowOverriden"
+                name="override"
+                class="input-override"
+                type="checkbox"
+              >
+              <label
+                class="checkbox-label"
+                for="override"
+              />
+            </div>
+            <button
+              class="btn"
+              @click="clearShadows"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <ShadowControl
+            v-model="currentShadow"
+            :ready="!!currentShadowFallback"
+            :fallback="currentShadowFallback"
+          />
+          <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
+            <i18n
+              path="settings.style.shadows.filter_hint.always_drop_shadow"
+              tag="p"
+            >
+              <code>filter: drop-shadow()</code>
+            </i18n>
+            <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
+            <i18n
+              path="settings.style.shadows.filter_hint.drop_shadow_syntax"
+              tag="p"
+            >
+              <code>drop-shadow</code>
+              <code>spread-radius</code>
+              <code>inset</code>
+            </i18n>
+            <i18n
+              path="settings.style.shadows.filter_hint.inset_classic"
+              tag="p"
+            >
+              <code>box-shadow</code>
+            </i18n>
+            <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
+          </div>
+        </div>
+
+        <div
+          :label="$t('settings.style.fonts._tab_label')"
+          class="fonts-container"
+        >
+          <div class="tab-header">
+            <p>{{ $t('settings.style.fonts.help') }}</p>
+            <button
+              class="btn"
+              @click="clearFonts"
+            >
+              {{ $t('settings.style.switcher.clear_all') }}
+            </button>
+          </div>
+          <FontControl
+            v-model="fontsLocal.interface"
+            name="ui"
+            :label="$t('settings.style.fonts.components.interface')"
+            :fallback="previewTheme.fonts.interface"
+            no-inherit="1"
+          />
+          <FontControl
+            v-model="fontsLocal.input"
+            name="input"
+            :label="$t('settings.style.fonts.components.input')"
+            :fallback="previewTheme.fonts.input"
+          />
+          <FontControl
+            v-model="fontsLocal.post"
+            name="post"
+            :label="$t('settings.style.fonts.components.post')"
+            :fallback="previewTheme.fonts.post"
+          />
+          <FontControl
+            v-model="fontsLocal.postCode"
+            name="postCode"
+            :label="$t('settings.style.fonts.components.postCode')"
+            :fallback="previewTheme.fonts.postCode"
+          />
+        </div>
+      </tab-switcher>
+    </keep-alive>
+
+    <div class="apply-container">
+      <button
+        class="btn submit"
+        :disabled="!themeValid"
+        @click="setCustomTheme"
+      >
+        {{ $t('general.apply') }}
+      </button>
+      <button
+        class="btn"
+        @click="clearAll"
+      >
+        {{ $t('settings.style.switcher.reset') }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script src="./theme_tab.js"></script>
+
+<style src="./theme_tab.scss" lang="scss"></style>
diff --git a/src/components/settings_modal/tabs/version_tab.js b/src/components/settings_modal/tabs/version_tab.js
new file mode 100644
index 00000000..616bdadf
--- /dev/null
+++ b/src/components/settings_modal/tabs/version_tab.js
@@ -0,0 +1,24 @@
+import { extractCommit } from 'src/services/version/version.service'
+
+const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
+const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
+
+const VersionTab = {
+  data () {
+    const instance = this.$store.state.instance
+    return {
+      backendVersion: instance.backendVersion,
+      frontendVersion: instance.frontendVersion
+    }
+  },
+  computed: {
+    frontendVersionLink () {
+      return pleromaFeCommitUrl + this.frontendVersion
+    },
+    backendVersionLink () {
+      return pleromaBeCommitUrl + extractCommit(this.backendVersion)
+    }
+  }
+}
+
+export default VersionTab
diff --git a/src/components/settings_modal/tabs/version_tab.vue b/src/components/settings_modal/tabs/version_tab.vue
new file mode 100644
index 00000000..acc43569
--- /dev/null
+++ b/src/components/settings_modal/tabs/version_tab.vue
@@ -0,0 +1,31 @@
+<template>
+<div :label="$t('settings.version.title')">
+  <div class="setting-item">
+    <ul class="setting-list">
+      <li>
+        <p>{{ $t('settings.version.backend_version') }}</p>
+        <ul class="option-list">
+          <li>
+            <a
+              :href="backendVersionLink"
+              target="_blank"
+              >{{ backendVersion }}</a>
+          </li>
+        </ul>
+      </li>
+      <li>
+        <p>{{ $t('settings.version.frontend_version') }}</p>
+        <ul class="option-list">
+          <li>
+            <a
+              :href="frontendVersionLink"
+              target="_blank"
+              >{{ frontendVersion }}</a>
+          </li>
+        </ul>
+      </li>
+    </ul>
+  </div>
+</div>
+</template>
+<script src="./version_tab.js">
diff --git a/src/components/style_switcher/preview.vue b/src/components/style_switcher/preview.vue
deleted file mode 100644
index 9d984659..00000000
--- a/src/components/style_switcher/preview.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<template>
-  <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>
-        <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 still-image">
-            ( ͡° ͜ʖ ͡°)
-          </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>
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
deleted file mode 100644
index a7f586f4..00000000
--- a/src/components/style_switcher/style_switcher.js
+++ /dev/null
@@ -1,758 +0,0 @@
-import { set, delete as del } from 'vue'
-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'
-import ShadowControl from '../shadow_control/shadow_control.vue'
-import FontControl from '../font_control/font_control.vue'
-import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import Preview from './preview.vue'
-import ExportImport from '../export_import/export_import.vue'
-import Checkbox from '../checkbox/checkbox.vue'
-
-// List of color values used in v1
-const v1OnlyNames = [
-  'bg',
-  'fg',
-  'text',
-  'link',
-  'cRed',
-  'cGreen',
-  'cBlue',
-  '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,
-      engineVersion: 0,
-
-      previewShadows: {},
-      previewColors: {},
-      previewRadii: {},
-      previewFonts: {},
-
-      shadowsInvalid: true,
-      colorsInvalid: true,
-      radiiInvalid: true,
-
-      keepColor: false,
-      keepShadows: false,
-      keepOpacity: false,
-      keepRoundness: false,
-      keepFonts: false,
-
-      ...Object.keys(SLOT_INHERITANCE)
-        .map(key => [key, ''])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
-
-      ...Object.keys(OPACITIES)
-        .map(key => [key, ''])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
-
-      shadowSelected: undefined,
-      shadowsLocal: {},
-      fontsLocal: {},
-
-      btnRadiusLocal: '',
-      inputRadiusLocal: '',
-      checkboxRadiusLocal: '',
-      panelRadiusLocal: '',
-      avatarRadiusLocal: '',
-      avatarAltRadiusLocal: '',
-      attachmentRadiusLocal: '',
-      tooltipRadiusLocal: ''
-    }
-  },
-  created () {
-    const self = this
-
-    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.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 Object.keys(SLOT_INHERITANCE)
-        .map(key => [key, this[key + 'ColorLocal']])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
-    },
-    currentOpacity () {
-      return Object.keys(OPACITIES)
-        .map(key => [key, this[key + 'OpacityLocal']])
-        .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
-    },
-    currentRadii () {
-      return {
-        btn: this.btnRadiusLocal,
-        input: this.inputRadiusLocal,
-        checkbox: this.checkboxRadiusLocal,
-        panel: this.panelRadiusLocal,
-        avatar: this.avatarRadiusLocal,
-        avatarAlt: this.avatarAltRadiusLocal,
-        tooltip: this.tooltipRadiusLocal,
-        attachment: this.attachmentRadiusLocal
-      }
-    },
-    preview () {
-      return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
-    },
-    previewTheme () {
-      if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
-      return this.preview.theme
-    },
-    // This needs optimization maybe
-    previewContrast () {
-      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) }), {})
-
-        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'] : [])
-          ]
-
-          const layers = getLayers(
-            layer,
-            variant || layer,
-            opacitySlot,
-            colorsConverted,
-            opacity
-          )
-
-          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)
-      }
-    },
-    previewRules () {
-      if (!this.preview.rules) return ''
-      return [
-        ...Object.values(this.preview.rules),
-        'color: var(--text)',
-        'font-family: var(--interfaceFont, sans-serif)'
-      ].join(';')
-    },
-    shadowsAvailable () {
-      return Object.keys(DEFAULT_SHADOWS).sort()
-    },
-    currentShadowOverriden: {
-      get () {
-        return !!this.currentShadow
-      },
-      set (val) {
-        if (val) {
-          set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
-        } else {
-          del(this.shadowsLocal, this.shadowSelected)
-        }
-      }
-    },
-    currentShadowFallback () {
-      return (this.previewTheme.shadows || {})[this.shadowSelected]
-    },
-    currentShadow: {
-      get () {
-        return this.shadowsLocal[this.shadowSelected]
-      },
-      set (v) {
-        set(this.shadowsLocal, this.shadowSelected, v)
-      }
-    },
-    themeValid () {
-      return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
-    },
-    exportedTheme () {
-      const saveEverything = (
-        !this.keepFonts &&
-        !this.keepShadows &&
-        !this.keepOpacity &&
-        !this.keepRoundness &&
-        !this.keepColor
-      )
-
-      const source = {
-        themeEngineVersion: CURRENT_VERSION
-      }
-
-      if (this.keepFonts || saveEverything) {
-        source.fonts = this.fontsLocal
-      }
-      if (this.keepShadows || saveEverything) {
-        source.shadows = this.shadowsLocal
-      }
-      if (this.keepOpacity || saveEverything) {
-        source.opacity = this.currentOpacity
-      }
-      if (this.keepColor || saveEverything) {
-        source.colors = this.currentColors
-      }
-      if (this.keepRoundness || saveEverything) {
-        source.radii = this.currentRadii
-      }
-
-      const theme = {
-        themeEngineVersion: CURRENT_VERSION,
-        ...this.previewTheme
-      }
-
-      return {
-        // To separate from other random JSON files and possible future source formats
-        _pleroma_theme_version: 2, theme, source
-      }
-    }
-  },
-  components: {
-    ColorInput,
-    OpacityInput,
-    RangeInput,
-    ContrastRatio,
-    ShadowControl,
-    FontControl,
-    TabSwitcher,
-    Preview,
-    ExportImport,
-    Checkbox
-  },
-  methods: {
-    loadTheme (
-      {
-        theme,
-        source,
-        _pleroma_theme_version: fileVersion
-      },
-      origin,
-      forceUseSource = false
-    ) {
-      this.dismissWarning()
-      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,
-          colors: this.currentColors,
-          radii: this.currentRadii
-        }
-      })
-    },
-    updatePreviewColorsAndShadows () {
-      this.previewColors = generateColors({
-        opacity: this.currentOpacity,
-        colors: this.currentColors
-      })
-      this.previewShadows = generateShadows(
-        { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
-        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 () {
-      this.loadThemeFromLocalStorage()
-    },
-
-    // Clears all the extra stuff when loading V1 theme
-    clearV1 () {
-      Object.keys(this.$data)
-        .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
-        .filter(_ => !v1OnlyNames.includes(_))
-        .forEach(key => {
-          set(this.$data, key, undefined)
-        })
-    },
-
-    clearRoundness () {
-      Object.keys(this.$data)
-        .filter(_ => _.endsWith('RadiusLocal'))
-        .forEach(key => {
-          set(this.$data, key, undefined)
-        })
-    },
-
-    clearOpacity () {
-      Object.keys(this.$data)
-        .filter(_ => _.endsWith('OpacityLocal'))
-        .forEach(key => {
-          set(this.$data, key, undefined)
-        })
-    },
-
-    clearShadows () {
-      this.shadowsLocal = {}
-    },
-
-    clearFonts () {
-      this.fontsLocal = {}
-    },
-
-    /**
-     * 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} 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 (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
-        // Old v1 naming: fg is text, btn is foreground
-        if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
-          version = 1
-        }
-        // New v2 naming: text is text, fg is foreground
-        if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
-          version = 2
-        }
-      }
-
-      this.engineVersion = version
-
-      // Stuff that differs between V1 and V2
-      if (version === 1) {
-        this.fgColorLocal = rgb2hex(colors.btn)
-        this.textColorLocal = rgb2hex(colors.fg)
-      }
-
-      if (!this.keepColor) {
-        this.clearV1()
-        const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
-        if (version === 1 || version === 'l1') {
-          keys
-            .add('bg')
-            .add('link')
-            .add('cRed')
-            .add('cBlue')
-            .add('cGreen')
-            .add('cOrange')
-        }
-
-        keys.forEach(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
-        })
-      }
-
-      if (!this.keepRoundness) {
-        this.clearRoundness()
-        Object.entries(radii).forEach(([k, v]) => {
-          // 'Radius' is kept mostly for v1->v2 localstorage transition
-          const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
-          this[key + 'RadiusLocal'] = v
-        })
-      }
-
-      if (!this.keepShadows) {
-        this.clearShadows()
-        if (version === 2) {
-          this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
-        } else {
-          this.shadowsLocal = shadows
-        }
-        this.shadowSelected = this.shadowsAvailable[0]
-      }
-
-      if (!this.keepFonts) {
-        this.clearFonts()
-        this.fontsLocal = fonts
-      }
-    }
-  },
-  watch: {
-    currentRadii () {
-      try {
-        this.previewRadii = generateRadii({ radii: this.currentRadii })
-        this.radiiInvalid = false
-      } catch (e) {
-        this.radiiInvalid = true
-        console.warn(e)
-      }
-    },
-    shadowsLocal: {
-      handler () {
-        if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
-        try {
-          this.updatePreviewColorsAndShadows()
-          this.shadowsInvalid = false
-        } catch (e) {
-          this.shadowsInvalid = true
-          console.warn(e)
-        }
-      },
-      deep: true
-    },
-    fontsLocal: {
-      handler () {
-        try {
-          this.previewFonts = generateFonts({ fonts: this.fontsLocal })
-          this.fontsInvalid = false
-        } catch (e) {
-          this.fontsInvalid = true
-          console.warn(e)
-        }
-      },
-      deep: true
-    },
-    currentColors () {
-      try {
-        this.updatePreviewColorsAndShadows()
-        this.colorsInvalid = false
-        this.shadowsInvalid = false
-      } catch (e) {
-        this.colorsInvalid = true
-        this.shadowsInvalid = true
-        console.warn(e)
-      }
-    },
-    currentOpacity () {
-      try {
-        this.updatePreviewColorsAndShadows()
-      } catch (e) {
-        console.warn(e)
-      }
-    },
-    selected () {
-      this.dismissWarning()
-      if (this.selectedVersion === 1) {
-        if (!this.keepRoundness) {
-          this.clearRoundness()
-        }
-
-        if (!this.keepShadows) {
-          this.clearShadows()
-        }
-
-        if (!this.keepOpacity) {
-          this.clearOpacity()
-        }
-
-        if (!this.keepColor) {
-          this.clearV1()
-
-          this.bgColorLocal = this.selected[1]
-          this.fgColorLocal = this.selected[2]
-          this.textColorLocal = this.selected[3]
-          this.linkColorLocal = this.selected[4]
-          this.cRedColorLocal = this.selected[5]
-          this.cGreenColorLocal = this.selected[6]
-          this.cBlueColorLocal = this.selected[7]
-          this.cOrangeColorLocal = this.selected[8]
-        }
-      } else if (this.selectedVersion >= 2) {
-        this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
-      }
-    }
-  }
-}
diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss
deleted file mode 100644
index d2a40d13..00000000
--- a/src/components/style_switcher/style_switcher.scss
+++ /dev/null
@@ -1,335 +0,0 @@
-@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;
-  }
-
-  .style-control {
-    display: flex;
-    align-items: baseline;
-    margin-bottom: 5px;
-
-    .label {
-      flex: 1;
-    }
-
-    &.disabled {
-      input, select {
-        opacity: .5
-      }
-    }
-
-    .opt {
-      margin: .5em;
-    }
-
-    .color-input {
-      flex: 0 0 0;
-    }
-
-    input, select {
-      min-width: 3em;
-      margin: 0;
-      flex: 0;
-
-      &[type=number] {
-        min-width: 5em;
-      }
-
-      &[type=range] {
-        flex: 1;
-        min-width: 3em;
-        align-self: flex-start;
-      }
-    }
-  }
-
-  .tab-switcher {
-    margin: 0 -1em;
-  }
-
-  .reset-container {
-    flex-wrap: wrap;
-  }
-
-  .fonts-container,
-  .reset-container,
-  .apply-container,
-  .radius-container,
-  .color-container,
-  {
-    display: flex;
-  }
-
-  .fonts-container,
-  .radius-container {
-    flex-direction: column;
-  }
-
-  .color-container{
-    > h4 {
-      width: 99%;
-    }
-    flex-wrap: wrap;
-    justify-content: space-between;
-  }
-
-  .fonts-container,
-  .color-container,
-  .shadow-container,
-  .radius-container,
-  .presets-container {
-    margin: 1em 1em 0;
-  }
-
-  .tab-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: baseline;
-    width: 100%;
-    min-height: 30px;
-
-    .btn {
-      min-width: 1px;
-      flex: 0 auto;
-      padding: 0 1em;
-    }
-
-    p {
-      flex: 1;
-      margin: 0;
-      margin-right: .5em;
-    }
-
-    margin-bottom: 1em;
-  }
-
-  .shadow-selector {
-    .override {
-      flex: 1;
-      margin-left: .5em;
-    }
-    .select-container {
-      margin-top: -4px;
-      margin-bottom: -3px;
-    }
-  }
-
-  .save-load,
-  .save-load-options {
-    display: flex;
-    justify-content: center;
-    align-items: baseline;
-    flex-wrap: wrap;
-
-    .presets,
-    .import-export {
-      margin-bottom: .5em;
-    }
-
-    .import-export {
-      display: flex;
-    }
-
-    .override {
-      margin-left: .5em;
-    }
-  }
-
-  .save-load-options {
-    flex-wrap: wrap;
-    margin-top: .5em;
-    justify-content: center;
-    .keep-option {
-      margin: 0 .5em .5em;
-      min-width: 25%;
-    }
-  }
-
-  .preview-container {
-    border-top: 1px dashed;
-    border-bottom: 1px dashed;
-    border-color: $fallback--border;
-    border-color: var(--border, $fallback--border);
-    margin: 1em -1em 0;
-    padding: 1em;
-    background: var(--body-background-image);
-    background-size: cover;
-    background-position: 50% 50%;
-
-    .dummy {
-      .post {
-        font-family: var(--postFont);
-        display: flex;
-
-        .content {
-          flex: 1;
-
-          h4 {
-            margin-bottom: .25em;
-          }
-
-          .icons {
-            margin-top: .5em;
-            display: flex;
-
-            i {
-              margin-right: 1em;
-            }
-          }
-        }
-      }
-
-      .after-post {
-        margin-top: 1em;
-        display: flex;
-        align-items: center;
-      }
-
-      .avatar, .avatar-alt{
-        background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
-        color: black;
-        font-family: sans-serif;
-        text-align: center;
-        margin-right: 1em;
-      }
-
-      .avatar-alt {
-        flex: 0 auto;
-        margin-left: 28px;
-        font-size: 12px;
-        min-width: 20px;
-        min-height: 20px;
-        line-height: 20px;
-        border-radius: $fallback--avatarAltRadius;
-        border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
-      }
-
-      .avatar {
-        flex: 0 auto;
-        width: 48px;
-        height: 48px;
-        font-size: 14px;
-        line-height: 48px;
-      }
-
-      .actions {
-        display: flex;
-        align-items: baseline;
-
-        .checkbox {
-          display: inline-flex;
-          align-items: baseline;
-          margin-right: 1em;
-          flex: 1;
-        }
-      }
-
-      .separator {
-        margin: 1em;
-        border-bottom: 1px solid;
-        border-color: $fallback--border;
-        border-color: var(--border, $fallback--border);
-      }
-
-      .panel-heading {
-        .badge, .alert, .btn, .faint {
-          margin-left: 1em;
-          white-space: nowrap;
-        }
-        .faint {
-          text-overflow: ellipsis;
-          min-width: 2em;
-          overflow-x: hidden;
-        }
-        .flex-spacer {
-          flex: 1;
-        }
-      }
-      .btn {
-        margin-left: 0;
-        padding: 0 1em;
-        min-width: 3em;
-        min-height: 30px;
-      }
-    }
-  }
-
-  .apply-container {
-    justify-content: center;
-  }
-
-  .radius-item,
-  .color-item {
-    min-width: 20em;
-    margin: 5px 6px 0 0;
-    display:flex;
-    flex-direction: column;
-    flex: 1 1 0;
-
-    &.wide {
-      min-width: 60%
-    }
-
-    &:not(.wide):nth-child(2n+1) {
-      margin-right: 7px;
-
-    }
-
-    .color, .opacity {
-      display:flex;
-      align-items: baseline;
-    }
-  }
-
-  .radius-item {
-    flex-basis: auto;
-  }
-
-  .theme-radius-rn,
-  .theme-color-cl {
-    border: 0;
-    box-shadow: none;
-    background: transparent;
-    color: var(--faint, $fallback--faint);
-    align-self: stretch;
-  }
-
-  .theme-color-cl,
-  .theme-radius-in,
-  .theme-color-in {
-    margin-left: 4px;
-  }
-
-  .theme-radius-in {
-    min-width: 1em;
-  }
-
-  .theme-radius-in {
-    max-width: 7em;
-    flex: 1;
-  }
-
-  .theme-radius-lb{
-    max-width: 50em;
-  }
-
-  .theme-preview-content {
-    padding: 20px;
-  }
-
-  .btn {
-    margin-left: .25em;
-    margin-right: .25em;
-  }
-}
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
deleted file mode 100644
index 62c8e634..00000000
--- a/src/components/style_switcher/style_switcher.vue
+++ /dev/null
@@ -1,956 +0,0 @@
-<template>
-  <div class="style-switcher">
-    <div class="presets-container">
-      <div class="save-load">
-        <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;)"
-          :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
-          :on-import="onImport"
-          :validator="importValidator"
-        >
-          <template slot="before">
-            <div class="presets">
-              {{ $t('settings.presets') }}
-              <label
-                for="preset-switcher"
-                class="select"
-              >
-                <select
-                  id="preset-switcher"
-                  v-model="selected"
-                  class="preset-switcher"
-                >
-                  <option
-                    v-for="style in availableStyles"
-                    :key="style.name"
-                    :value="style"
-                    :style="{
-                      backgroundColor: style[1] || (style.theme || style.source).colors.bg,
-                      color: style[3] || (style.theme || style.source).colors.text
-                    }"
-                  >
-                    {{ style[0] || style.name }}
-                  </option>
-                </select>
-                <i class="icon-down-open" />
-              </label>
-            </div>
-          </template>
-        </ExportImport>
-      </div>
-      <div class="save-load-options">
-        <span class="keep-option">
-          <Checkbox v-model="keepColor">
-            {{ $t('settings.style.switcher.keep_color') }}
-          </Checkbox>
-        </span>
-        <span class="keep-option">
-          <Checkbox v-model="keepShadows">
-            {{ $t('settings.style.switcher.keep_shadows') }}
-          </Checkbox>
-        </span>
-        <span class="keep-option">
-          <Checkbox v-model="keepOpacity">
-            {{ $t('settings.style.switcher.keep_opacity') }}
-          </Checkbox>
-        </span>
-        <span class="keep-option">
-          <Checkbox v-model="keepRoundness">
-            {{ $t('settings.style.switcher.keep_roundness') }}
-          </Checkbox>
-        </span>
-        <span class="keep-option">
-          <Checkbox v-model="keepFonts">
-            {{ $t('settings.style.switcher.keep_fonts') }}
-          </Checkbox>
-        </span>
-        <p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
-      </div>
-    </div>
-
-    <preview :style="previewRules" />
-
-    <keep-alive>
-      <tab-switcher key="style-tweak">
-        <div
-          :label="$t('settings.style.common_colors._tab_label')"
-          class="color-container"
-        >
-          <div class="tab-header">
-            <p>{{ $t('settings.theme_help') }}</p>
-            <button
-              class="btn"
-              @click="clearOpacity"
-            >
-              {{ $t('settings.style.switcher.clear_opacity') }}
-            </button>
-            <button
-              class="btn"
-              @click="clearV1"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <p>{{ $t('settings.theme_help_v2_1') }}</p>
-          <h4>{{ $t('settings.style.common_colors.main') }}</h4>
-          <div class="color-item">
-            <ColorInput
-              v-model="bgColorLocal"
-              name="bgColor"
-              :label="$t('settings.background')"
-            />
-            <OpacityInput
-              v-model="bgOpacityLocal"
-              name="bgOpacity"
-              :fallback="previewTheme.opacity.bg"
-            />
-            <ColorInput
-              v-model="textColorLocal"
-              name="textColor"
-              :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>
-          <div class="color-item">
-            <ColorInput
-              v-model="fgColorLocal"
-              name="fgColor"
-              :label="$t('settings.foreground')"
-            />
-            <ColorInput
-              v-model="fgTextColorLocal"
-              name="fgTextColor"
-              :label="$t('settings.text')"
-              :fallback="previewTheme.colors.fgText"
-            />
-            <ColorInput
-              v-model="fgLinkColorLocal"
-              name="fgLinkColor"
-              :label="$t('settings.links')"
-              :fallback="previewTheme.colors.fgLink"
-            />
-            <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
-          </div>
-          <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
-          <div class="color-item">
-            <ColorInput
-              v-model="cRedColorLocal"
-              name="cRedColor"
-              :label="$t('settings.cRed')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgCRed" />
-            <ColorInput
-              v-model="cBlueColorLocal"
-              name="cBlueColor"
-              :label="$t('settings.cBlue')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgCBlue" />
-          </div>
-          <div class="color-item">
-            <ColorInput
-              v-model="cGreenColorLocal"
-              name="cGreenColor"
-              :label="$t('settings.cGreen')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgCGreen" />
-            <ColorInput
-              v-model="cOrangeColorLocal"
-              name="cOrangeColor"
-              :label="$t('settings.cOrange')"
-            />
-            <ContrastRatio :contrast="previewContrast.bgCOrange" />
-          </div>
-          <p>{{ $t('settings.theme_help_v2_2') }}</p>
-        </div>
-
-        <div
-          :label="$t('settings.style.advanced_colors._tab_label')"
-          class="color-container"
-        >
-          <div class="tab-header">
-            <p>{{ $t('settings.theme_help') }}</p>
-            <button
-              class="btn"
-              @click="clearOpacity"
-            >
-              {{ $t('settings.style.switcher.clear_opacity') }}
-            </button>
-            <button
-              class="btn"
-              @click="clearV1"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </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"
-              name="alertError"
-              :label="$t('settings.style.advanced_colors.alert_error')"
-              :fallback="previewTheme.colors.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"
-            />
-            <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>
-            <ColorInput
-              v-model="badgeNotificationColorLocal"
-              name="badgeNotification"
-              :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="previewTheme.colors.panel"
-              :label="$t('settings.background')"
-            />
-            <OpacityInput
-              v-model="panelOpacityLocal"
-              name="panelOpacity"
-              :fallback="previewTheme.opacity.panel"
-              :disabled="panelColorLocal === 'transparent'"
-            />
-            <ColorInput
-              v-model="panelTextColorLocal"
-              name="panelTextColor"
-              :fallback="previewTheme.colors.panelText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio
-              :contrast="previewContrast.panelText"
-              large="true"
-            />
-            <ColorInput
-              v-model="panelLinkColorLocal"
-              name="panelLinkColor"
-              :fallback="previewTheme.colors.panelLink"
-              :label="$t('settings.links')"
-            />
-            <ContrastRatio
-              :contrast="previewContrast.panelLink"
-              large="true"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
-            <ColorInput
-              v-model="topBarColorLocal"
-              name="topBarColor"
-              :fallback="previewTheme.colors.topBar"
-              :label="$t('settings.background')"
-            />
-            <ColorInput
-              v-model="topBarTextColorLocal"
-              name="topBarTextColor"
-              :fallback="previewTheme.colors.topBarText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.topBarText" />
-            <ColorInput
-              v-model="topBarLinkColorLocal"
-              name="topBarLinkColor"
-              :fallback="previewTheme.colors.topBarLink"
-              :label="$t('settings.links')"
-            />
-            <ContrastRatio :contrast="previewContrast.topBarLink" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
-            <ColorInput
-              v-model="inputColorLocal"
-              name="inputColor"
-              :fallback="previewTheme.colors.input"
-              :label="$t('settings.background')"
-            />
-            <OpacityInput
-              v-model="inputOpacityLocal"
-              name="inputOpacity"
-              :fallback="previewTheme.opacity.input"
-              :disabled="inputColorLocal === 'transparent'"
-            />
-            <ColorInput
-              v-model="inputTextColorLocal"
-              name="inputTextColor"
-              :fallback="previewTheme.colors.inputText"
-              :label="$t('settings.text')"
-            />
-            <ContrastRatio :contrast="previewContrast.inputText" />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
-            <ColorInput
-              v-model="btnColorLocal"
-              name="btnColor"
-              :fallback="previewTheme.colors.btn"
-              :label="$t('settings.background')"
-            />
-            <OpacityInput
-              v-model="btnOpacityLocal"
-              name="btnOpacity"
-              :fallback="previewTheme.opacity.btn"
-              :disabled="btnColorLocal === 'transparent'"
-            />
-            <ColorInput
-              v-model="btnTextColorLocal"
-              name="btnTextColor"
-              :fallback="previewTheme.colors.btnText"
-              :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" />
-            <h5>{{ $t('settings.style.advanced_colors.pressed') }}</h5>
-            <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" />
-            <h5>{{ $t('settings.style.advanced_colors.disabled') }}</h5>
-            <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')"
-            />
-            <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5>
-            <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>
-            <ColorInput
-              v-model="borderColorLocal"
-              name="borderColor"
-              :fallback="previewTheme.colors.border"
-              :label="$t('settings.style.common.color')"
-            />
-            <OpacityInput
-              v-model="borderOpacityLocal"
-              name="borderOpacity"
-              :fallback="previewTheme.opacity.border"
-              :disabled="borderColorLocal === 'transparent'"
-            />
-          </div>
-          <div class="color-item">
-            <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
-            <ColorInput
-              v-model="faintColorLocal"
-              name="faintColor"
-              :fallback="previewTheme.colors.faint"
-              :label="$t('settings.text')"
-            />
-            <ColorInput
-              v-model="faintLinkColorLocal"
-              name="faintLinkColor"
-              :fallback="previewTheme.colors.faintLink"
-              :label="$t('settings.links')"
-            />
-            <ColorInput
-              v-model="panelFaintColorLocal"
-              name="panelFaintColor"
-              :fallback="previewTheme.colors.panelFaint"
-              :label="$t('settings.style.advanced_colors.panel_header')"
-            />
-            <OpacityInput
-              v-model="faintOpacityLocal"
-              name="faintOpacity"
-              :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
-          :label="$t('settings.style.radii._tab_label')"
-          class="radius-container"
-        >
-          <div class="tab-header">
-            <p>{{ $t('settings.radii_help') }}</p>
-            <button
-              class="btn"
-              @click="clearRoundness"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <RangeInput
-            v-model="btnRadiusLocal"
-            name="btnRadius"
-            :label="$t('settings.btnRadius')"
-            :fallback="previewTheme.radii.btn"
-            max="16"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="inputRadiusLocal"
-            name="inputRadius"
-            :label="$t('settings.inputRadius')"
-            :fallback="previewTheme.radii.input"
-            max="9"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="checkboxRadiusLocal"
-            name="checkboxRadius"
-            :label="$t('settings.checkboxRadius')"
-            :fallback="previewTheme.radii.checkbox"
-            max="16"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="panelRadiusLocal"
-            name="panelRadius"
-            :label="$t('settings.panelRadius')"
-            :fallback="previewTheme.radii.panel"
-            max="50"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="avatarRadiusLocal"
-            name="avatarRadius"
-            :label="$t('settings.avatarRadius')"
-            :fallback="previewTheme.radii.avatar"
-            max="28"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="avatarAltRadiusLocal"
-            name="avatarAltRadius"
-            :label="$t('settings.avatarAltRadius')"
-            :fallback="previewTheme.radii.avatarAlt"
-            max="28"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="attachmentRadiusLocal"
-            name="attachmentRadius"
-            :label="$t('settings.attachmentRadius')"
-            :fallback="previewTheme.radii.attachment"
-            max="50"
-            hard-min="0"
-          />
-          <RangeInput
-            v-model="tooltipRadiusLocal"
-            name="tooltipRadius"
-            :label="$t('settings.tooltipRadius')"
-            :fallback="previewTheme.radii.tooltip"
-            max="50"
-            hard-min="0"
-          />
-        </div>
-
-        <div
-          :label="$t('settings.style.shadows._tab_label')"
-          class="shadow-container"
-        >
-          <div class="tab-header shadow-selector">
-            <div class="select-container">
-              {{ $t('settings.style.shadows.component') }}
-              <label
-                for="shadow-switcher"
-                class="select"
-              >
-                <select
-                  id="shadow-switcher"
-                  v-model="shadowSelected"
-                  class="shadow-switcher"
-                >
-                  <option
-                    v-for="shadow in shadowsAvailable"
-                    :key="shadow"
-                    :value="shadow"
-                  >
-                    {{ $t('settings.style.shadows.components.' + shadow) }}
-                  </option>
-                </select>
-                <i class="icon-down-open" />
-              </label>
-            </div>
-            <div class="override">
-              <label
-                for="override"
-                class="label"
-              >
-                {{ $t('settings.style.shadows.override') }}
-              </label>
-              <input
-                id="override"
-                v-model="currentShadowOverriden"
-                name="override"
-                class="input-override"
-                type="checkbox"
-              >
-              <label
-                class="checkbox-label"
-                for="override"
-              />
-            </div>
-            <button
-              class="btn"
-              @click="clearShadows"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <ShadowControl
-            v-model="currentShadow"
-            :ready="!!currentShadowFallback"
-            :fallback="currentShadowFallback"
-          />
-          <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
-            <i18n
-              path="settings.style.shadows.filter_hint.always_drop_shadow"
-              tag="p"
-            >
-              <code>filter: drop-shadow()</code>
-            </i18n>
-            <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
-            <i18n
-              path="settings.style.shadows.filter_hint.drop_shadow_syntax"
-              tag="p"
-            >
-              <code>drop-shadow</code>
-              <code>spread-radius</code>
-              <code>inset</code>
-            </i18n>
-            <i18n
-              path="settings.style.shadows.filter_hint.inset_classic"
-              tag="p"
-            >
-              <code>box-shadow</code>
-            </i18n>
-            <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
-          </div>
-        </div>
-
-        <div
-          :label="$t('settings.style.fonts._tab_label')"
-          class="fonts-container"
-        >
-          <div class="tab-header">
-            <p>{{ $t('settings.style.fonts.help') }}</p>
-            <button
-              class="btn"
-              @click="clearFonts"
-            >
-              {{ $t('settings.style.switcher.clear_all') }}
-            </button>
-          </div>
-          <FontControl
-            v-model="fontsLocal.interface"
-            name="ui"
-            :label="$t('settings.style.fonts.components.interface')"
-            :fallback="previewTheme.fonts.interface"
-            no-inherit="1"
-          />
-          <FontControl
-            v-model="fontsLocal.input"
-            name="input"
-            :label="$t('settings.style.fonts.components.input')"
-            :fallback="previewTheme.fonts.input"
-          />
-          <FontControl
-            v-model="fontsLocal.post"
-            name="post"
-            :label="$t('settings.style.fonts.components.post')"
-            :fallback="previewTheme.fonts.post"
-          />
-          <FontControl
-            v-model="fontsLocal.postCode"
-            name="postCode"
-            :label="$t('settings.style.fonts.components.postCode')"
-            :fallback="previewTheme.fonts.postCode"
-          />
-        </div>
-      </tab-switcher>
-    </keep-alive>
-
-    <div class="apply-container">
-      <button
-        class="btn submit"
-        :disabled="!themeValid"
-        @click="setCustomTheme"
-      >
-        {{ $t('general.apply') }}
-      </button>
-      <button
-        class="btn"
-        @click="clearAll"
-      >
-        {{ $t('settings.style.switcher.reset') }}
-      </button>
-    </div>
-  </div>
-</template>
-
-<script src="./style_switcher.js"></script>
-
-<style src="./style_switcher.scss" lang="scss"></style>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index a443531e..a7b790a3 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -46,8 +46,14 @@
 
   &.side-tabs {
     flex-direction: row;
+    @media all and (max-width: 800px) {
+      overflow-x: auto;
+    }
     > .contents {
       flex: 0 1 80%;
+      @media all and (max-width: 800px) {
+        min-width: 96vw;
+      }
     }
     > .tabs {
       flex: 1 0 auto;
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 4ee040e8..b4f275d9 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -50,15 +50,6 @@
               >
                 {{ user.name }}
               </div>
-              <router-link
-                v-if="!isOtherUser"
-                :to="{ name: 'user-settings' }"
-              >
-                <i
-                  class="button-icon icon-wrench usersettings"
-                  :title="$t('tool_tip.user_settings')"
-                />
-              </router-link>
               <a
                 v-if="isOtherUser && !user.is_local"
                 :href="user.statusnet_profile_url"
@@ -117,7 +108,7 @@
               type="color"
             >
             <label
-              for="style-switcher"
+              for="theme_tab"
               class="userHighlightSel select"
             >
               <select
diff --git a/src/components/user_settings/confirm.js b/src/components/user_settings/confirm.js
deleted file mode 100644
index 0f4ddfc9..00000000
--- a/src/components/user_settings/confirm.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const Confirm = {
-  props: ['disabled'],
-  data: () => ({}),
-  methods: {
-    confirm () { this.$emit('confirm') },
-    cancel () { this.$emit('cancel') }
-  }
-}
-export default Confirm
diff --git a/src/components/user_settings/confirm.vue b/src/components/user_settings/confirm.vue
deleted file mode 100644
index 69b3811b..00000000
--- a/src/components/user_settings/confirm.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-<template>
-  <div>
-    <slot />
-    <button
-      class="btn btn-default"
-      :disabled="disabled"
-      @click="confirm"
-    >
-      {{ $t('general.confirm') }}
-    </button>
-    <button
-      class="btn btn-default"
-      :disabled="disabled"
-      @click="cancel"
-    >
-      {{ $t('general.cancel') }}
-    </button>
-  </div>
-</template>
-
-<script src="./confirm.js">
-</script>
diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js
deleted file mode 100644
index abf37062..00000000
--- a/src/components/user_settings/mfa.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import RecoveryCodes from './mfa_backup_codes.vue'
-import TOTP from './mfa_totp.vue'
-import Confirm from './confirm.vue'
-import VueQrcode from '@chenfengyuan/vue-qrcode'
-import { mapState } from 'vuex'
-
-const Mfa = {
-  data: () => ({
-    settings: { // current settings of MFA
-      available: false,
-      enabled: false,
-      totp: false
-    },
-    setupState: { // setup mfa
-      state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
-      setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
-    },
-    backupCodes: {
-      getNewCodes: false,
-      inProgress: false, //  progress of fetch codes
-      codes: []
-    },
-    otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
-      provisioning_uri: '',
-      key: ''
-    },
-    currentPassword: null,
-    otpConfirmToken: null,
-    error: null,
-    readyInit: false
-  }),
-  components: {
-    'recovery-codes': RecoveryCodes,
-    'totp-item': TOTP,
-    'qrcode': VueQrcode,
-    'confirm': Confirm
-  },
-  computed: {
-    canSetupOTP () {
-      return (
-        (this.setupInProgress && this.backupCodesPrepared) ||
-          this.settings.enabled
-      ) && !this.settings.totp && !this.setupOTPInProgress
-    },
-    setupInProgress () {
-      return this.setupState.state !== '' && this.setupState.state !== 'complete'
-    },
-    setupOTPInProgress () {
-      return this.setupState.state === 'setupOTP' && !this.completedOTP
-    },
-    prepareOTP () {
-      return this.setupState.setupOTPState === 'prepare'
-    },
-    confirmOTP () {
-      return this.setupState.setupOTPState === 'confirm'
-    },
-    completedOTP () {
-      return this.setupState.setupOTPState === 'completed'
-    },
-    backupCodesPrepared () {
-      return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
-    },
-    confirmNewBackupCodes () {
-      return this.backupCodes.getNewCodes
-    },
-    ...mapState({
-      backendInteractor: (state) => state.api.backendInteractor
-    })
-  },
-
-  methods: {
-    activateOTP () {
-      if (!this.settings.enabled) {
-        this.setupState.state = 'getBackupcodes'
-        this.fetchBackupCodes()
-      }
-    },
-    fetchBackupCodes () {
-      this.backupCodes.inProgress = true
-      this.backupCodes.codes = []
-
-      return this.backendInteractor.generateMfaBackupCodes()
-        .then((res) => {
-          this.backupCodes.codes = res.codes
-          this.backupCodes.inProgress = false
-        })
-    },
-    getBackupCodes () { // get a new backup codes
-      this.backupCodes.getNewCodes = true
-    },
-    confirmBackupCodes () { // confirm getting new backup codes
-      this.fetchBackupCodes().then((res) => {
-        this.backupCodes.getNewCodes = false
-      })
-    },
-    cancelBackupCodes () { // cancel confirm form of new backup codes
-      this.backupCodes.getNewCodes = false
-    },
-
-    // Setup OTP
-    setupOTP () { // prepare setup OTP
-      this.setupState.state = 'setupOTP'
-      this.setupState.setupOTPState = 'prepare'
-      this.backendInteractor.mfaSetupOTP()
-        .then((res) => {
-          this.otpSettings = res
-          this.setupState.setupOTPState = 'confirm'
-        })
-    },
-    doConfirmOTP () { // handler confirm enable OTP
-      this.error = null
-      this.backendInteractor.mfaConfirmOTP({
-        token: this.otpConfirmToken,
-        password: this.currentPassword
-      })
-        .then((res) => {
-          if (res.error) {
-            this.error = res.error
-            return
-          }
-          this.completeSetup()
-        })
-    },
-
-    completeSetup () {
-      this.setupState.setupOTPState = 'complete'
-      this.setupState.state = 'complete'
-      this.currentPassword = null
-      this.error = null
-      this.fetchSettings()
-    },
-    cancelSetup () { // cancel setup
-      this.setupState.setupOTPState = ''
-      this.setupState.state = ''
-      this.currentPassword = null
-      this.error = null
-    },
-    // end Setup OTP
-
-    // fetch settings from server
-    async fetchSettings () {
-      let result = await this.backendInteractor.settingsMFA()
-      if (result.error) return
-      this.settings = result.settings
-      this.settings.available = true
-      return result
-    }
-  },
-  mounted () {
-    this.fetchSettings().then(() => {
-      this.readyInit = true
-    })
-  }
-}
-export default Mfa
diff --git a/src/components/user_settings/mfa.vue b/src/components/user_settings/mfa.vue
deleted file mode 100644
index 14ea10a1..00000000
--- a/src/components/user_settings/mfa.vue
+++ /dev/null
@@ -1,173 +0,0 @@
-<template>
-  <div
-    v-if="readyInit && settings.available"
-    class="setting-item mfa-settings"
-  >
-    <div class="mfa-heading">
-      <h2>{{ $t('settings.mfa.title') }}</h2>
-    </div>
-
-    <div>
-      <div
-        v-if="!setupInProgress"
-        class="setting-item"
-      >
-        <!-- Enabled methods -->
-        <h3>{{ $t('settings.mfa.authentication_methods') }}</h3>
-        <totp-item
-          :settings="settings"
-          @deactivate="fetchSettings"
-          @activate="activateOTP"
-        />
-        <br>
-
-        <div v-if="settings.enabled">
-          <!-- backup codes block-->
-          <recovery-codes
-            v-if="!confirmNewBackupCodes"
-            :backup-codes="backupCodes"
-          />
-          <button
-            v-if="!confirmNewBackupCodes"
-            class="btn btn-default"
-            @click="getBackupCodes"
-          >
-            {{ $t('settings.mfa.generate_new_recovery_codes') }}
-          </button>
-
-          <div v-if="confirmNewBackupCodes">
-            <confirm
-              :disabled="backupCodes.inProgress"
-              @confirm="confirmBackupCodes"
-              @cancel="cancelBackupCodes"
-            >
-              <p class="warning">
-                {{ $t('settings.mfa.warning_of_generate_new_codes') }}
-              </p>
-            </confirm>
-          </div>
-        </div>
-      </div>
-
-      <div v-if="setupInProgress">
-        <!-- setup block-->
-
-        <h3>{{ $t('settings.mfa.setup_otp') }}</h3>
-
-        <recovery-codes
-          v-if="!setupOTPInProgress"
-          :backup-codes="backupCodes"
-        />
-
-        <button
-          v-if="canSetupOTP"
-          class="btn btn-default"
-          @click="cancelSetup"
-        >
-          {{ $t('general.cancel') }}
-        </button>
-
-        <button
-          v-if="canSetupOTP"
-          class="btn btn-default"
-          @click="setupOTP"
-        >
-          {{ $t('settings.mfa.setup_otp') }}
-        </button>
-
-        <template v-if="setupOTPInProgress">
-          <i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i>
-
-          <div v-if="confirmOTP">
-            <div class="setup-otp">
-              <div class="qr-code">
-                <h4>{{ $t('settings.mfa.scan.title') }}</h4>
-                <p>{{ $t('settings.mfa.scan.desc') }}</p>
-                <qrcode
-                  :value="otpSettings.provisioning_uri"
-                  :options="{ width: 200 }"
-                />
-                <p>
-                  {{ $t('settings.mfa.scan.secret_code') }}:
-                  {{ otpSettings.key }}
-                </p>
-              </div>
-
-              <div class="verify">
-                <h4>{{ $t('general.verify') }}</h4>
-                <p>{{ $t('settings.mfa.verify.desc') }}</p>
-                <input
-                  v-model="otpConfirmToken"
-                  type="text"
-                >
-
-                <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p>
-                <input
-                  v-model="currentPassword"
-                  type="password"
-                >
-                <div class="confirm-otp-actions">
-                  <button
-                    class="btn btn-default"
-                    @click="doConfirmOTP"
-                  >
-                    {{ $t('settings.mfa.confirm_and_enable') }}
-                  </button>
-                  <button
-                    class="btn btn-default"
-                    @click="cancelSetup"
-                  >
-                    {{ $t('general.cancel') }}
-                  </button>
-                </div>
-                <div
-                  v-if="error"
-                  class="alert error"
-                >
-                  {{ error }}
-                </div>
-              </div>
-            </div>
-          </div>
-        </template>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script src="./mfa.js"></script>
-<style lang="scss">
-@import '../../_variables.scss';
-.warning {
-  color: $fallback--cOrange;
-  color: var(--cOrange, $fallback--cOrange);
-}
-.mfa-settings {
-  .mfa-heading, .method-item {
-    overflow: hidden;
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
-    align-items: baseline;
-  }
-
-  .setup-otp {
-    display: flex;
-    justify-content: center;
-    flex-wrap: wrap;
-    .qr-code {
-      flex: 1;
-      padding-right: 10px;
-    }
-    .verify { flex: 1; }
-    .error { margin: 4px 0 0 0; }
-    .confirm-otp-actions {
-      button {
-        width: 15em;
-        margin-top: 5px;
-      }
-
-    }
-  }
-}
-</style>
diff --git a/src/components/user_settings/mfa_backup_codes.js b/src/components/user_settings/mfa_backup_codes.js
deleted file mode 100644
index f0a984ec..00000000
--- a/src/components/user_settings/mfa_backup_codes.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default {
-  props: {
-    backupCodes: {
-      type: Object,
-      default: () => ({
-        inProgress: false,
-        codes: []
-      })
-    }
-  },
-  data: () => ({}),
-  computed: {
-    inProgress () { return this.backupCodes.inProgress },
-    ready () { return this.backupCodes.codes.length > 0 },
-    displayTitle () { return this.inProgress || this.ready }
-  }
-}
diff --git a/src/components/user_settings/mfa_backup_codes.vue b/src/components/user_settings/mfa_backup_codes.vue
deleted file mode 100644
index e6c8ede2..00000000
--- a/src/components/user_settings/mfa_backup_codes.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<template>
-  <div>
-    <h4 v-if="displayTitle">
-      {{ $t('settings.mfa.recovery_codes') }}
-    </h4>
-    <i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i>
-    <template v-if="ready">
-      <p class="alert warning">
-        {{ $t('settings.mfa.recovery_codes_warning') }}
-      </p>
-      <ul class="backup-codes">
-        <li
-          v-for="code in backupCodes.codes"
-          :key="code"
-        >
-          {{ code }}
-        </li>
-      </ul>
-    </template>
-  </div>
-</template>
-<script src="./mfa_backup_codes.js"></script>
-<style lang="scss">
-@import '../../_variables.scss';
-
-.warning {
-  color: $fallback--cOrange;
-  color: var(--cOrange, $fallback--cOrange);
-}
-.backup-codes {
-  font-family: var(--postCodeFont, monospace);
-}
-</style>
diff --git a/src/components/user_settings/mfa_totp.js b/src/components/user_settings/mfa_totp.js
deleted file mode 100644
index 8408d8e9..00000000
--- a/src/components/user_settings/mfa_totp.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import Confirm from './confirm.vue'
-import { mapState } from 'vuex'
-
-export default {
-  props: ['settings'],
-  data: () => ({
-    error: false,
-    currentPassword: '',
-    deactivate: false,
-    inProgress: false // progress peform request to disable otp method
-  }),
-  components: {
-    'confirm': Confirm
-  },
-  computed: {
-    isActivated () {
-      return this.settings.totp
-    },
-    ...mapState({
-      backendInteractor: (state) => state.api.backendInteractor
-    })
-  },
-  methods: {
-    doActivate () {
-      this.$emit('activate')
-    },
-    cancelDeactivate () { this.deactivate = false },
-    doDeactivate () {
-      this.error = null
-      this.deactivate = true
-    },
-    confirmDeactivate () { // confirm deactivate TOTP method
-      this.error = null
-      this.inProgress = true
-      this.backendInteractor.mfaDisableOTP({
-        password: this.currentPassword
-      })
-        .then((res) => {
-          this.inProgress = false
-          if (res.error) {
-            this.error = res.error
-            return
-          }
-          this.deactivate = false
-          this.$emit('deactivate')
-        })
-    }
-  }
-}
diff --git a/src/components/user_settings/mfa_totp.vue b/src/components/user_settings/mfa_totp.vue
deleted file mode 100644
index c6f2cc7b..00000000
--- a/src/components/user_settings/mfa_totp.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-  <div>
-    <div class="method-item">
-      <strong>{{ $t('settings.mfa.otp') }}</strong>
-      <button
-        v-if="!isActivated"
-        class="btn btn-default"
-        @click="doActivate"
-      >
-        {{ $t('general.enable') }}
-      </button>
-
-      <button
-        v-if="isActivated"
-        class="btn btn-default"
-        :disabled="deactivate"
-        @click="doDeactivate"
-      >
-        {{ $t('general.disable') }}
-      </button>
-    </div>
-
-    <confirm
-      v-if="deactivate"
-      :disabled="inProgress"
-      @confirm="confirmDeactivate"
-      @cancel="cancelDeactivate"
-    >
-      {{ $t('settings.enter_current_password_to_confirm') }}:
-      <input
-        v-model="currentPassword"
-        type="password"
-      >
-    </confirm>
-    <div
-      v-if="error"
-      class="alert error"
-    >
-      {{ error }}
-    </div>
-  </div>
-</template>
-<script src="./mfa_totp.js"></script>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
deleted file mode 100644
index e07d4e56..00000000
--- a/src/components/user_settings/user_settings.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import get from 'lodash/get'
-import map from 'lodash/map'
-import reject from 'lodash/reject'
-import Autosuggest from '../autosuggest/autosuggest.vue'
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import BlockCard from '../block_card/block_card.vue'
-import MuteCard from '../mute_card/mute_card.vue'
-import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
-import SelectableList from '../selectable_list/selectable_list.vue'
-import ProgressButton from '../progress_button/progress_button.vue'
-import Importer from '../importer/importer.vue'
-import Exporter from '../exporter/exporter.vue'
-import withSubscription from '../../hocs/with_subscription/with_subscription'
-import Checkbox from '../checkbox/checkbox.vue'
-
-const BlockList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchBlocks'),
-  select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const MuteList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchMutes'),
-  select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const DomainMuteList = withSubscription({
-  fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
-  select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
-  childPropName: 'items'
-})(SelectableList)
-
-const UserSettings = {
-  data () {
-    return {
-      activeTab: 'profile',
-      newDomainToMute: ''
-    }
-  },
-  created () {
-    this.$store.dispatch('fetchTokens')
-  },
-  components: {
-    TabSwitcher,
-    BlockList,
-    MuteList,
-    DomainMuteList,
-    BlockCard,
-    MuteCard,
-    DomainMuteCard,
-    ProgressButton,
-    Autosuggest,
-    Checkbox
-  },
-  computed: {
-    user () {
-      return this.$store.state.users.currentUser
-    },
-    pleromaBackend () {
-      return this.$store.state.instance.pleromaBackend
-    },
-    currentSaveStateNotice () {
-      return this.$store.state.interface.settings.currentSaveStateNotice
-    }
-  },
-  methods: {
-    importFollows (file) {
-      return this.$store.state.api.backendInteractor.importFollows({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    importBlocks (file) {
-      return this.$store.state.api.backendInteractor.importBlocks({ file })
-        .then((status) => {
-          if (!status) {
-            throw new Error('failed')
-          }
-        })
-    },
-    generateExportableUsersContent (users) {
-      // Get addresses
-      return users.map((user) => {
-        // check is it's a local user
-        if (user && user.is_local) {
-          // append the instance address
-          // eslint-disable-next-line no-undef
-          return user.screen_name + '@' + location.hostname
-        }
-        return user.screen_name
-      }).join('\n')
-    },
-    activateTab (tabName) {
-      this.activeTab = tabName
-    },
-    filterUnblockedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
-      })
-    },
-    filterUnMutedUsers (userIds) {
-      return reject(userIds, (userId) => {
-        const user = this.$store.getters.findUser(userId)
-        return !user || user.muted || user.id === this.$store.state.users.currentUser.id
-      })
-    },
-    queryUserIds (query) {
-      return this.$store.dispatch('searchUsers', query)
-        .then((users) => map(users, 'id'))
-    },
-    blockUsers (ids) {
-      return this.$store.dispatch('blockUsers', ids)
-    },
-    unblockUsers (ids) {
-      return this.$store.dispatch('unblockUsers', ids)
-    },
-    muteUsers (ids) {
-      return this.$store.dispatch('muteUsers', ids)
-    },
-    unmuteUsers (ids) {
-      return this.$store.dispatch('unmuteUsers', ids)
-    },
-    unmuteDomains (domains) {
-      return this.$store.dispatch('unmuteDomains', domains)
-    },
-    muteDomain () {
-      return this.$store.dispatch('muteDomain', this.newDomainToMute)
-        .then(() => { this.newDomainToMute = '' })
-    },
-    identity (value) {
-      return value
-    }
-  }
-}
-
-export default UserSettings
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
deleted file mode 100644
index 2a88714f..00000000
--- a/src/components/user_settings/user_settings.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-<template>
-  <div class="settings panel panel-default">
-    <div class="panel-heading">
-      <div class="title">
-        {{ $t('settings.user_settings') }}
-      </div>
-      <transition name="fade">
-        <template v-if="currentSaveStateNotice">
-          <div
-            v-if="currentSaveStateNotice.error"
-            class="alert error"
-            @click.prevent
-          >
-            {{ $t('settings.saving_err') }}
-          </div>
-
-          <div
-            v-if="!currentSaveStateNotice.error"
-            class="alert transparent"
-            @click.prevent
-          >
-            {{ $t('settings.saving_ok') }}
-          </div>
-        </template>
-      </transition>
-    </div>
-    <div class="panel-body profile-edit">
-    </div>
-  </div>
-</template>
-
-<script src="./user_settings.js">
-</script>
-
-<style lang="scss">
-@import '../../_variables.scss';
-
-.profile-edit {
-  .bio {
-    margin: 0;
-  }
-
-  .visibility-tray {
-    padding-top: 5px;
-  }
-
-  input[type=file] {
-    padding: 5px;
-    height: auto;
-  }
-
-  .banner {
-    max-width: 100%;
-  }
-
-  .uploading {
-    font-size: 1.5em;
-    margin: 0.25em;
-  }
-
-  .name-changer {
-    width: 100%;
-  }
-
-  .bg {
-    max-width: 100%;
-  }
-
-  .current-avatar {
-    display: block;
-    width: 150px;
-    height: 150px;
-    border-radius: $fallback--avatarRadius;
-    border-radius: var(--avatarRadius, $fallback--avatarRadius);
-  }
-
-  .oauth-tokens {
-    width: 100%;
-
-    th {
-      text-align: left;
-    }
-
-    .actions {
-      text-align: right;
-    }
-  }
-
-  &-usersearch-wrapper {
-    padding: 1em;
-  }
-
-  &-bulk-actions {
-    text-align: right;
-    padding: 0 1em;
-    min-height: 28px;
-
-    button {
-      width: 10em;
-    }
-  }
-
-  &-domain-mute-form {
-    padding: 1em;
-    display: flex;
-    flex-direction: column;
-
-    button {
-      align-self: flex-end;
-      margin-top: 1em;
-      width: 10em;
-    }
-  }
-
-  .setting-subitem {
-    margin-left: 1.75em;
-  }
-}
-</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 312f7283..d42be00f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -68,7 +68,9 @@
     "disable": "Disable",
     "enable": "Enable",
     "confirm": "Confirm",
-    "verify": "Verify"
+    "verify": "Verify",
+    "close": "Close",
+    "peek": "Peek"
   },
   "image_cropper": {
     "crop_picture": "Crop picture",
-- 
cgit v1.2.3-70-g09d2

