+
+
{{ $t('settings.theme') }}
-
+
+ {{ $t('settings.style.themes3.editor.load_style') }}
+
+
+
+
+
-
- {{ style.name }}
+
+
+ {{ style.name }}
+ {{ style.version }}
+
+
{{ $t('settings.style.themes3.palette.label') }}
+
+
+
+
+
+
+ {{ $t('settings.style.themes3.palette.v2_unsupported') }}
+
+
+
{{ $t("settings.style.appearance_tab_note") }}
@@ -60,7 +123,7 @@
px
rem
-
+
-
+
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js
new file mode 100644
index 000000000..cbd25dfc2
--- /dev/null
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.js
@@ -0,0 +1,559 @@
+import { ref, reactive, computed, watch } from 'vue'
+import { get, set } from 'lodash'
+
+import Select from 'src/components/select/select.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import ComponentPreview from 'src/components/component_preview/component_preview.vue'
+import StringSetting from '../../helpers/string_setting.vue'
+import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
+import ColorInput from 'src/components/color_input/color_input.vue'
+import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
+import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+import Tooltip from 'src/components/tooltip/tooltip.vue'
+import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
+
+import { init } from 'src/services/theme_data/theme_data_3.service.js'
+import { getCssRules } from 'src/services/theme_data/css_utils.js'
+import { serialize } from 'src/services/theme_data/iss_serializer.js'
+import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
+import {
+ // rgb2hex,
+ hex2rgb,
+ getContrastRatio
+} from 'src/services/color_convert/color_convert.js'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faFloppyDisk, faFolderOpen, faFile } from '@fortawesome/free-solid-svg-icons'
+
+// helper for debugging
+// eslint-disable-next-line no-unused-vars
+const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
+
+// helper to make states comparable
+const normalizeStates = (states) => ['normal', ...(states?.filter(x => x !== 'normal') || [])].join(':')
+
+library.add(
+ faFile,
+ faFloppyDisk,
+ faFolderOpen
+)
+
+export default {
+ components: {
+ Select,
+ Checkbox,
+ Tooltip,
+ StringSetting,
+ ComponentPreview,
+ TabSwitcher,
+ ShadowControl,
+ ColorInput,
+ PaletteEditor,
+ OpacityInput,
+ ContrastRatio
+ },
+ setup () {
+ // ### Meta stuff
+ const name = ref('')
+ const author = ref('')
+ const license = ref('')
+ const website = ref('')
+
+ const metaOut = computed(() => {
+ return `@meta {
+ name: ${name.value};
+ author: ${author.value};
+ license: ${license.value};
+ website: ${website.value};
+}`
+ })
+
+ // ### Palette stuff
+ const palettes = reactive({
+ light: {
+ bg: '#f2f6f9',
+ fg: '#d6dfed',
+ text: '#304055',
+ underlay: '#5d6086',
+ accent: '#f55b1b',
+ cBlue: '#0095ff',
+ cRed: '#d31014',
+ cGreen: '#0fa00f',
+ cOrange: '#ffa500',
+ border: '#d8e6f9'
+ },
+ dark: {
+ bg: '#121a24',
+ fg: '#182230',
+ text: '#b9b9ba',
+ link: '#d8a070',
+ accent: '#d8a070',
+ cRed: '#FF0000',
+ cBlue: '#0095ff',
+ cGreen: '#0fa00f',
+ cOrange: '#ffa500'
+ }
+ })
+
+ const palettesOut = computed(() => {
+ return Object.entries(palettes).map(([name, palette]) => {
+ const entries = Object
+ .entries(palette)
+ .map(([slot, data]) => ` ${slot}: ${data};`)
+ .join('\n')
+
+ return `@palette.${name} {\n${entries}\n}`
+ }).join('\n\n')
+ })
+
+ const editedPalette = ref('dark')
+ const palette = computed({
+ get () {
+ return palettes[editedPalette.value]
+ },
+ set (newPalette) {
+ palettes[editedPalette.value] = newPalette
+ }
+ })
+
+ // ### I18n stuff
+ // The paths in i18n are getting ridicously long, this effectively shortens them
+ const getI18nPath = (componentName) => `settings.style.themes3.editor.components.${componentName}`
+ // vue i18n doesn't seem to have (working) mechanic to have a fallback so we have to
+ // make do ourselves
+ const fallbackI18n = (translated, fallback) => {
+ if (translated.startsWith('settings.style.themes3')) {
+ return fallback
+ }
+ return translated
+ }
+ const getFriendlyNamePath = (componentName) => getI18nPath(componentName) + '.friendlyName'
+ const getVariantPath = (componentName, variant) => {
+ return variant === 'normal'
+ ? 'settings.style.themes3.editor.components.normal.variant'
+ : `${getI18nPath(componentName)}.variants.${variant}`
+ }
+ const getStatePath = (componentName, state) => {
+ return state === 'normal'
+ ? 'settings.style.themes3.editor.components.normal.state'
+ : `${getI18nPath(componentName)}.states.${state}`
+ }
+
+ // ### Initialization stuff
+ // Getting existing components
+ const componentsContext = require.context('src', true, /\.style.js(on)?$/)
+ const componentKeysAll = componentsContext.keys()
+
+ const componentsMap = new Map(
+ componentKeysAll
+ .map(
+ key => [key, componentsContext(key).default]
+ ).filter(([key, component]) => !component.virtual && !component.notEditable)
+ )
+ const componentKeys = [...componentsMap.keys()]
+
+ // Initializing selected component and its computed descendants
+ const selectedComponentKey = ref(componentsMap.keys().next().value)
+ const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
+ const selectedComponentName = computed(() => selectedComponent.value.name)
+
+ const selectedVariant = ref('normal')
+ const selectedComponentVariantsAll = computed(() => {
+ return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
+ })
+
+ const selectedState = reactive(new Set())
+ const selectedComponentStatesAll = computed(() => {
+ return Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
+ })
+ const selectedComponentStates = computed(() => {
+ return selectedComponentStatesAll.value.filter(x => x !== 'normal')
+ })
+ const updateSelectedStates = (state, v) => {
+ if (v) {
+ selectedState.add(state)
+ } else {
+ selectedState.delete(state)
+ }
+ }
+
+ // ### Preview stuff
+ const editorHintStyle = computed(() => {
+ const editorHint = selectedComponent.value.editor
+ const styles = []
+ if (editorHint && Object.keys(editorHint).length > 0) {
+ if (editorHint.aspect != null) {
+ styles.push(`aspect-ratio: ${editorHint.aspect} !important;`)
+ }
+ if (editorHint.border != null) {
+ styles.push(`border-width: ${editorHint.border}px !important;`)
+ }
+ }
+ return styles.join('; ')
+ })
+
+ // Apart from "hover" we can't really show how component looks like in
+ // certain states, so we have to fake them.
+ const simulatePseudoSelectors = css => css
+ .replace(selectedComponent.value.selector, '.ComponentPreview .preview-block')
+ .replace(':active', '.preview-active')
+ .replace(':hover', '.preview-hover')
+ .replace(':active', '.preview-active')
+ .replace(':focus', '.preview-focus')
+ .replace(':focus-within', '.preview-focus-within')
+ .replace(':disabled', '.preview-disabled')
+
+ const previewRules = reactive([])
+ const previewClass = computed(() => {
+ const selectors = []
+ if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') {
+ selectors.push(selectedComponent.value.variants[selectedVariant.value])
+ }
+ if (selectedState.size > 0) {
+ selectedState.forEach(state => {
+ const original = selectedComponent.value.states[state]
+ selectors.push(simulatePseudoSelectors(original))
+ })
+ }
+ return selectors.map(x => x.substring(1)).join('')
+ })
+ const previewCss = computed(() => {
+ try {
+ const scoped = getCssRules(previewRules).map(simulatePseudoSelectors)
+ return scoped.join('\n')
+ } catch (e) {
+ console.error('Invalid ruleset', e)
+ return null
+ }
+ })
+
+ // ### Rules stuff aka meat and potatoes
+ // The native structure of separate rules and the child -> parent
+ // relation isn't very convenient for editor, we replace the array
+ // and child -> parent structure with map and parent -> child structure
+ const editorFriendlyFallbackStructure = computed(() => {
+ const root = {}
+
+ componentKeys.forEach((componentKey) => {
+ const componentValue = componentsMap.get(componentKey)
+ const { defaultRules } = componentValue
+ defaultRules.forEach((rule) => {
+ const { parent: rParent } = rule
+ const parent = rParent ?? rule
+ const hasChildren = !!rParent
+ const child = hasChildren ? rule : null
+
+ const {
+ component: pComponent,
+ variant: pVariant = 'normal',
+ state: pState = [] // no relation to Intel CPUs whatsoever
+ } = parent
+
+ const pPath = `${hasChildren ? pComponent : componentValue.name}.${pVariant}.${normalizeStates(pState)}`
+
+ let output = get(root, pPath)
+ if (!output) {
+ set(root, pPath, {})
+ output = get(root, pPath)
+ }
+
+ if (hasChildren) {
+ output._children = output._children ?? {}
+ const {
+ component: cComponent,
+ variant: cVariant = 'normal',
+ state: cState = [],
+ directives
+ } = child
+
+ const cPath = `${cComponent}.${cVariant}.${normalizeStates(cState)}`
+ set(output._children, cPath, directives)
+ } else {
+ output.directives = parent.directives
+ }
+ })
+ })
+
+ return root
+ })
+
+ // All rules that are made by editor
+ const allEditedRules = reactive({})
+
+ // Checkging whether component can support some "directives" which
+ // are actually virtual subcomponents, i.e. Text, Link etc
+ const componentHas = (subComponent) => {
+ return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
+ }
+
+ // Path is path for lodash's get and set
+ const getPath = (component, directive) => {
+ const pathSuffix = component ? `._children.${component}.normal.normal` : ''
+ const path = `${selectedComponentName.value}.${selectedVariant.value}.${normalizeStates([...selectedState])}${pathSuffix}.directives.${directive}`
+ return path
+ }
+
+ // Templates for directives
+ const isElementPresent = (component, directive, defaultValue = '') => computed({
+ get () {
+ return get(allEditedRules, getPath(component, directive)) != null
+ },
+ set (value) {
+ if (value) {
+ const fallback = get(
+ editorFriendlyFallbackStructure.value,
+ getPath(component, directive)
+ )
+ set(allEditedRules, getPath(component, directive), fallback ?? defaultValue)
+ } else {
+ set(allEditedRules, getPath(component, directive), null)
+ }
+ }
+ })
+
+ const getEditedElement = (component, directive) => computed({
+ get () {
+ let usedRule
+ const fallback = editorFriendlyFallbackStructure.value
+ const real = allEditedRules
+ const path = getPath(component, directive)
+
+ usedRule = get(real, path) // get real
+ if (!usedRule) {
+ usedRule = get(fallback, path)
+ }
+
+ return usedRule
+ },
+ set (value) {
+ set(allEditedRules, getPath(component, directive), value)
+ }
+ })
+
+ // All the editable stuff for the component
+ const editedBackgroundColor = getEditedElement(null, 'background')
+ const editedOpacity = getEditedElement(null, 'opacity')
+ const editedTextColor = getEditedElement('Text', 'textColor')
+ const editedTextAuto = getEditedElement('Text', 'textAuto')
+ const editedLinkColor = getEditedElement('Link', 'textColor')
+ const editedIconColor = getEditedElement('Icon', 'textColor')
+ const editedShadow = getEditedElement(null, 'shadow')
+
+ // Shadow is partially edited outside the ShadowControl
+ // for better space utilization
+ const editedSubShadowId = ref(null)
+ const editedSubShadow = computed(() => {
+ if (editedShadow.value == null || editedSubShadowId.value == null) return null
+ return editedShadow.value[editedSubShadowId.value]
+ })
+
+ const updateSubShadow = (axis, value) => {
+ if (!editedSubShadow.value || editedSubShadowId.value == null) return
+ const newEditedShadow = [...editedShadow.value]
+
+ newEditedShadow[editedSubShadowId.value] = {
+ ...newEditedShadow[editedSubShadowId.value],
+ [axis]: value
+ }
+
+ editedShadow.value = newEditedShadow
+ }
+
+ const onSubShadow = (id) => {
+ if (id != null) {
+ editedSubShadowId.value = id
+ } else {
+ editedSubShadow.value = null
+ }
+ }
+
+ // Whether specific directives present in the edited rule or not
+ // Somewhat serves double-duty as it creates/removes the directive
+ // when set
+ const isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
+ const isOpacityPresent = isElementPresent(null, 'opacity', 1)
+ const isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
+ const isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
+ const isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
+ const isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
+ const isShadowPresent = isElementPresent(null, 'shadow', [])
+
+ const editorFriendlyToOriginal = computed(() => {
+ const resultRules = []
+
+ const convert = (component, data = {}, parent) => {
+ const variants = Object.entries(data || {})
+
+ variants.forEach(([variant, variantData]) => {
+ const states = Object.entries(variantData)
+
+ states.forEach(([jointState, stateData]) => {
+ const state = jointState.split(/:/g)
+ const result = {
+ component,
+ variant,
+ state,
+ directives: stateData.directives || {}
+ }
+
+ if (parent) {
+ result.parent = {
+ component: parent
+ }
+ }
+
+ resultRules.push(result)
+
+ // Currently we only support single depth for simplicity's sake
+ if (!parent) {
+ Object.entries(stateData._children || {}).forEach(([cName, child]) => convert(cName, child, component))
+ }
+ })
+ })
+ }
+
+ convert(selectedComponentName.value, allEditedRules[selectedComponentName.value])
+
+ return resultRules
+ })
+
+ const updatePreview = () => {
+ try {
+ const rules = init({
+ inputRuleset: editorFriendlyToOriginal.value,
+ initialStaticVars: {
+ ...palette.value
+ },
+ ultimateBackgroundColor: '#000000',
+ rootComponentName: selectedComponentName.value,
+ editMode: true,
+ debug: true
+ }).eager
+ previewRules.splice(0, previewRules.length)
+ previewRules.push(...rules)
+ } catch (e) {
+ console.error('Could not compile preview theme', e)
+ }
+ }
+
+ const updateSelectedComponent = () => {
+ selectedVariant.value = 'normal'
+ selectedState.clear()
+ updatePreview()
+ }
+ updateSelectedComponent()
+
+ watch(
+ allEditedRules,
+ updatePreview
+ )
+
+ watch(
+ palettes,
+ updatePreview
+ )
+
+ watch(
+ editedPalette,
+ updatePreview
+ )
+
+ watch(
+ selectedComponentName,
+ updateSelectedComponent
+ )
+
+ // TODO this is VERY primitive right now, need to make it
+ // support variables, fallbacks etc.
+ const getContrast = (bg, text) => {
+ try {
+ const bgRgb = hex2rgb(bg)
+ const textRgb = hex2rgb(text)
+
+ const ratio = getContrastRatio(bgRgb, textRgb)
+ return {
+ // TODO this ideally should be part of
+ 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
+ }
+ } catch (e) {
+ console.warn('Failure computing contrast', e)
+ return { error: e }
+ }
+ }
+
+ const isShadowTabOpen = ref(false)
+ const onTabSwitch = (tab) => {
+ isShadowTabOpen.value = tab === 'shadow'
+ }
+
+ const exportStyle = () => {
+ console.log('ORIG', toValue(editorFriendlyToOriginal.value))
+ console.log('SERI', serialize(editorFriendlyToOriginal.value))
+
+ const result = [
+ metaOut.value,
+ palettesOut.value,
+ serialize(editorFriendlyToOriginal.value)
+ ].join('\n\n')
+
+ console.log('RESULT', result)
+ console.log('DESERI', deserialize(result))
+ }
+
+ return {
+ name,
+ author,
+ license,
+ website,
+ palette,
+ editedPalette,
+ componentKeys,
+ componentsMap,
+ selectedComponent,
+ selectedComponentName,
+ selectedComponentKey,
+ selectedComponentVariantsAll,
+ selectedComponentStates,
+ selectedVariant,
+ selectedState,
+ updateSelectedStates,
+ editedBackgroundColor,
+ editedOpacity,
+ editedTextColor,
+ editedTextAuto,
+ editedLinkColor,
+ editedIconColor,
+ editedShadow,
+ editedSubShadow,
+ onSubShadow,
+ updateSubShadow,
+ getContrast,
+ isBackgroundColorPresent,
+ isOpacityPresent,
+ isTextColorPresent,
+ isTextAutoPresent,
+ isLinkColorPresent,
+ isIconColorPresent,
+ isShadowPresent,
+ previewCss,
+ previewClass,
+ editorHintStyle,
+ getFriendlyNamePath,
+ fallbackI18n,
+ getVariantPath,
+ getStatePath,
+ componentHas,
+ isShadowTabOpen,
+ onTabSwitch,
+ exportStyle
+ }
+ }
+}
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.scss b/src/components/settings_modal/tabs/style_tab/style_tab.scss
new file mode 100644
index 000000000..0306454fc
--- /dev/null
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.scss
@@ -0,0 +1,178 @@
+.StyleTab {
+ .style-control {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ margin-bottom: 0.5em;
+
+ .label {
+ margin-right: 0.5em;
+ flex: 1 1 0;
+ line-height: 2;
+ min-height: 2em;
+ }
+
+ &.suboption {
+ margin-left: 1em;
+ }
+
+ .color-input {
+ flex: 0 0 0;
+ }
+
+ input,
+ select {
+ min-width: 3em;
+ margin: 0;
+ flex: 0;
+
+ &[type="number"] {
+ min-width: 9em;
+
+ &.-small {
+ min-width: 5em;
+ }
+ }
+
+ &[type="range"] {
+ flex: 1;
+ min-width: 9em;
+ align-self: center;
+ margin: 0 0.25em;
+ }
+
+ &[type="checkbox"] + i {
+ height: 1.1em;
+ align-self: center;
+ }
+ }
+ }
+
+ .setting-item {
+ padding-bottom: 0;
+
+ .btn {
+ padding: 0 0.5em;
+ }
+
+ &:not(:first-child) {
+ margin-top: 0.5em;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 0.5em;
+ }
+
+ &.heading {
+ display: grid;
+ align-items: baseline;
+ grid-template-columns: 1fr auto auto auto;
+ grid-gap: 0.5em;
+
+ h2 {
+ flex: 1 0 auto;
+ }
+ }
+
+ &.metadata {
+ display: flex;
+
+ .setting-item {
+ flex: 2 0 auto;
+ }
+
+ li {
+ text-align: right;
+ }
+ }
+ }
+
+ .palette-editor {
+ > .label:not(.Select) {
+ font-weight: bold;
+ justify-self: right;
+ }
+ }
+
+ .component-editor {
+ display: grid;
+ grid-template-columns: 6fr 3fr 4fr;
+ grid-template-rows: auto auto 1fr;
+ grid-gap: 0.5em;
+ grid-template-areas:
+ "component component variant"
+ "state state state"
+ "preview settings settings";
+
+ .component-selector {
+ grid-area: component;
+ align-self: center;
+ }
+
+ .component-selector,
+ .state-selector,
+ .variant-selector {
+ display: grid;
+ grid-template-columns: 1fr minmax(1fr, 10em);
+ grid-template-rows: auto;
+ grid-auto-flow: column;
+ grid-gap: 0.5em;
+ align-items: baseline;
+
+ > label:not(.Select) {
+ font-weight: bold;
+ justify-self: right;
+ }
+ }
+
+ .state-selector {
+ grid-area: state;
+ grid-template-columns: minmax(min-content, 7em) 1fr;
+ }
+
+ .variant-selector {
+ grid-area: variant;
+ }
+
+ .state-selector-list {
+ display: grid;
+ list-style: none;
+ grid-auto-flow: dense;
+ grid-template-columns: repeat(5, minmax(min-content, 1fr));
+ grid-auto-rows: 1fr;
+ grid-gap: 0.5em;
+ padding: 0;
+ margin: 0;
+ }
+
+ .preview-container {
+ --border: none;
+ --shadow: none;
+ --roundness: none;
+
+ grid-area: preview;
+ }
+
+ .component-settings {
+ grid-area: settings;
+ }
+
+ .editor-tab {
+ display: grid;
+ grid-template-columns: 1fr 2em;
+ grid-column-gap: 0.5em;
+ align-items: center;
+ grid-auto-rows: min-content;
+ grid-auto-flow: dense;
+ border-left: 1px solid var(--border);
+ border-right: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ padding: 0.5em;
+ }
+
+ .shadow-tab {
+ grid-template-columns: 1fr;
+ justify-items: center;
+ }
+ }
+}
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.vue b/src/components/settings_modal/tabs/style_tab/style_tab.vue
new file mode 100644
index 000000000..b73732f8a
--- /dev/null
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+
{{ $t('settings.style.themes3.editor.title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ updateSelectedStates(state, v)"
+ >
+ {{ fallbackI18n($t(getStatePath(selectedComponentName, state)), state) }}
+
+
+
+
+
+
+
+
+ updateSubShadow(axis, value)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.style.themes3.editor.include_in_rule') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index 64de28bc3..00418b03c 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -4,9 +4,6 @@ import {
getContrastRatioLayers,
relativeLuminance
} from 'src/services/color_convert/color_convert.js'
-import {
- getThemes
-} from 'src/services/style_setter/style_setter.js'
import {
newImporter,
newExporter
@@ -123,28 +120,22 @@ export default {
}
},
created () {
- const self = this
+ const currentIndex = this.$store.state.instance.themesIndex
- 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
- })
+ let promise
+ if (currentIndex) {
+ promise = Promise.resolve(currentIndex)
+ } else {
+ promise = this.$store.dispatch('fetchThemesIndex')
+ }
+
+ promise.then(themesIndex => {
+ Object
+ .values(themesIndex)
+ .forEach(themeFunc => {
+ themeFunc().then(themeData => this.availableStyles.push(themeData))
+ })
+ })
},
mounted () {
this.loadThemeFromLocalStorage()
@@ -412,9 +403,6 @@ export default {
forceUseSource = false
) {
this.dismissWarning()
- if (!source && !theme) {
- throw new Error('Can\'t load theme: empty')
- }
const version = (origin === 'localStorage' && !theme.colors)
? 'l1'
: fileVersion
@@ -494,14 +482,7 @@ export default {
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 {
+ if (theme || source) {
this.loadTheme(
{
theme,
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
index e86e61dab..c2e33e75e 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -45,12 +45,16 @@
flex: 0;
&[type="number"] {
- min-width: 5em;
+ min-width: 9em;
+
+ &.-small {
+ min-width: 5em;
+ }
}
&[type="range"] {
flex: 1;
- min-width: 2em;
+ min-width: 9em;
align-self: center;
margin: 0 0.5em;
}
diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js
index 4521305ea..fc227b5be 100644
--- a/src/components/shadow_control/shadow_control.js
+++ b/src/components/shadow_control/shadow_control.js
@@ -1,6 +1,7 @@
import ColorInput from 'src/components/color_input/color_input.vue'
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
import Select from 'src/components/select/select.vue'
+import SelectMotion from 'src/components/select/select_motion.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue'
import ComponentPreview from 'src/components/component_preview/component_preview.vue'
@@ -21,16 +22,22 @@ library.add(
faPlus
)
-const toModel = (object = {}) => ({
- x: 0,
- y: 0,
- blur: 0,
- spread: 0,
- inset: false,
- color: '#000000',
- alpha: 1,
- ...object
-})
+const toModel = (input) => {
+ if (typeof input === 'object') {
+ return {
+ x: 0,
+ y: 0,
+ blur: 0,
+ spread: 0,
+ inset: false,
+ color: '#000000',
+ alpha: 1,
+ ...input
+ }
+ } else if (typeof input === 'string') {
+ return input
+ }
+}
export default {
props: [
@@ -48,20 +55,34 @@ export default {
ColorInput,
OpacityInput,
Select,
+ SelectMotion,
Checkbox,
Popover,
ComponentPreview
},
- beforeUpdate () {
- this.cValue = (this.modelValue ?? this.fallback ?? []).map(toModel)
- },
computed: {
- selected () {
- const selected = this.cValue[this.selectedId]
- if (selected) {
- return { ...selected }
+ selectedType: {
+ get () {
+ return typeof this.selected
+ },
+ set (newType) {
+ this.selected = toModel(newType === 'object' ? {} : '')
+ }
+ },
+ selected: {
+ get () {
+ const selected = this.cValue[this.selectedId]
+ if (selected && typeof selected === 'object') {
+ return { ...selected }
+ } else if (typeof selected === 'string') {
+ return selected
+ }
+ return null
+ },
+ set (value) {
+ this.cValue[this.selectedId] = toModel(value)
+ this.$emit('update:modelValue', this.cValue)
}
- return null
},
present () {
return this.selected != null && !this.usingFallback
@@ -72,61 +93,55 @@ export default {
currentFallback () {
return this.fallback?.[this.selectedId]
},
- moveUpValid () {
- return this.selectedId > 0
- },
- moveDnValid () {
- return this.selectedId < this.cValue.length - 1
- },
usingFallback () {
return this.modelValue == null
},
style () {
- if (this.separateInset) {
- return {
- filter: getCssShadowFilter(this.cValue),
- boxShadow: getCssShadow(this.cValue, true)
+ try {
+ if (this.separateInset) {
+ return {
+ filter: getCssShadowFilter(this.cValue),
+ boxShadow: getCssShadow(this.cValue, true)
+ }
+ }
+ return {
+ boxShadow: getCssShadow(this.cValue)
+ }
+ } catch (e) {
+ return {
+ border: '1px solid red'
}
- }
- return {
- boxShadow: getCssShadow(this.cValue)
}
}
},
watch: {
+ modelValue (value) {
+ if (!value) this.cValue = (this.modelValue ?? this.fallback ?? []).map(toModel)
+ },
selected (value) {
this.$emit('subShadowSelected', this.selectedId)
}
},
methods: {
+ getNewSubshadow () {
+ return toModel(this.selected)
+ },
+ onSelectChange (id) {
+ this.selectedId = id
+ },
+ getSubshadowLabel (shadow, index) {
+ if (typeof shadow === 'object') {
+ return shadow?.name ?? this.$t('settings.style.shadows.shadow_id', { value: index })
+ } else if (typeof shadow === 'string') {
+ return shadow || this.$t('settings.style.shadows.empty_expression')
+ }
+ },
updateProperty: throttle(function (prop, value) {
this.cValue[this.selectedId][prop] = value
if (prop === 'inset' && value === false && this.separateInset) {
this.cValue[this.selectedId].spread = 0
}
this.$emit('update:modelValue', this.cValue)
- }, 100),
- add () {
- this.cValue.push(toModel(this.selected))
- this.selectedId = Math.max(this.cValue.length - 1, 0)
- this.$emit('update:modelValue', this.cValue)
- },
- del () {
- this.cValue.splice(this.selectedId, 1)
- this.selectedId = this.cValue.length === 0 ? undefined : Math.max(this.selectedId - 1, 0)
- this.$emit('update:modelValue', this.cValue)
- },
- moveUp () {
- const movable = this.cValue.splice(this.selectedId, 1)[0]
- this.cValue.splice(this.selectedId - 1, 0, movable)
- this.selectedId -= 1
- this.$emit('update:modelValue', this.cValue)
- },
- moveDn () {
- const movable = this.cValue.splice(this.selectedId, 1)[0]
- this.cValue.splice(this.selectedId + 1, 0, movable)
- this.selectedId += 1
- this.$emit('update:modelValue', this.cValue)
- }
+ }, 100)
}
}
diff --git a/src/components/shadow_control/shadow_control.scss b/src/components/shadow_control/shadow_control.scss
index dd0490235..de4159c11 100644
--- a/src/components/shadow_control/shadow_control.scss
+++ b/src/components/shadow_control/shadow_control.scss
@@ -4,6 +4,7 @@
justify-content: stretch;
grid-gap: 0.25em;
margin-bottom: 1em;
+ width: 100%;
.shadow-switcher {
order: 1;
@@ -16,19 +17,6 @@
.shadow-list {
flex: 1 0 auto;
}
-
- .arrange-buttons {
- flex: 0 0 auto;
- display: grid;
- grid-auto-columns: 1fr;
- grid-auto-flow: column;
- margin-top: 0.25em;
-
- .button-default {
- margin: 0;
- padding: 0;
- }
- }
}
.shadow-tweak {
@@ -37,6 +25,9 @@
min-width: 10em;
margin-left: 0.125em;
margin-right: 0.125em;
+ display: grid;
+ grid-template-rows: auto 1fr;
+ grid-gap: 0.25em;
/* hack */
.input-boolean {
@@ -52,6 +43,11 @@
flex: 1 0 5em;
}
+ .shadow-expression {
+ width: 100%;
+ height: 100%;
+ }
+
.id-control {
align-items: stretch;
@@ -100,6 +96,5 @@
}
.inset-tooltip {
- padding: 0.5em;
max-width: 30em;
}
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index e1d201914..29adfff4a 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -8,7 +8,6 @@
class="shadow-preview"
:shadow-control="true"
:shadow="selected"
- :preview-style="style"
:disabled="disabled || !present"
@update:shadow="({ axis, value }) => updateProperty(axis, value)"
/>
@@ -18,7 +17,7 @@
v-model="selectedId"
class="shadow-list"
size="10"
- :disabled="shadowsAreNull"
+ :disabled="disabled || shadowsAreNull"
>
-
-
-
-
-
-
+
-
-
-
+ {{ $t('settings.style.shadows.raw') }}
+
+
+
+
+
+
+
+ updateProperty('name', e.target.value)"
+ class="name-control style-control"
>
-
-
- updateProperty('inset', e.target.checked)"
- >
-
- {{ $t('settings.style.shadows.inset') }}
-
-
-
-
-
- updateProperty('blur', e.target.value)"
- >
- updateProperty('blur', e.target.value)"
- >
-
-
-
- updateProperty('spread', e.target.value)"
- >
- updateProperty('spread', e.target.value)"
- >
-
- updateProperty('color', e)"
- />
- updateProperty('alpha', e)"
- />
-
- --variable,mod
-
-
-
-
-
-
- {{ $t('settings.style.shadows.filter_hint.avatar_inset_short') }}
-
-
-
-
-
+ updateProperty('name', e.target.value)"
+ >
+
+
+ updateProperty('inset', e.target.checked)"
+ >
+
+ {{ $t('settings.style.shadows.inset') }}
+
+
+
+
+
+ updateProperty('blur', e.target.value)"
+ >
+ updateProperty('blur', e.target.value)"
+ >
+
+
+
+ updateProperty('spread', e.target.value)"
+ >
+ updateProperty('spread', e.target.value)"
+ >
+
+ updateProperty('color', e)"
+ />
+ updateProperty('alpha', e)"
+ />
+
+ --variable,mod
+
+
+
+
-
filter: drop-shadow()
-
-
{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}
-
- drop-shadow
- spread-radius
- inset
-
-
- box-shadow
-
-
{{ $t('settings.style.shadows.filter_hint.spread_zero') }}
-
-
-
+
+
+ {{ $t('settings.style.shadows.filter_hint.avatar_inset_short') }}
+
+
+
+
+
+
+
diff --git a/src/components/tab_switcher/tab.style.js b/src/components/tab_switcher/tab.style.js
index eac8aaeb5..217d42ebc 100644
--- a/src/components/tab_switcher/tab.style.js
+++ b/src/components/tab_switcher/tab.style.js
@@ -39,7 +39,7 @@ export default {
{
state: ['disabled'],
directives: {
- background: '$blend(--inheritedBackground, 0.25, --parent)',
+ background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--defaultButtonBevel']
}
},
diff --git a/src/components/tooltip/tooltip.vue b/src/components/tooltip/tooltip.vue
new file mode 100644
index 000000000..db0a8bef9
--- /dev/null
+++ b/src/components/tooltip/tooltip.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ {{ props.text }}
+
+
+
+
+
+
+
+
diff --git a/src/components/user_card/user_card.style.js b/src/components/user_card/user_card.style.js
index 34eaa1769..dff0315bd 100644
--- a/src/components/user_card/user_card.style.js
+++ b/src/components/user_card/user_card.style.js
@@ -25,7 +25,7 @@ export default {
color: '#000000',
alpha: 0.6
}],
- '--profileTint': 'color | $alpha(--background, 0.5)'
+ '--profileTint': 'color | $alpha(--background 0.5)'
}
},
{
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 74cddea8b..10414ad4d 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -750,11 +750,97 @@
"more_settings": "More settings",
"style": {
"custom_theme_used": "(Custom theme)",
+ "stock_theme_used": "(Stock theme)",
"themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.",
"appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI",
"update_preview": "Update preview",
"themes3": {
"define": "Override",
+ "palette": {
+ "label": "Palette",
+ "import": "Import",
+ "export": "Export",
+ "dark": "Dark mode",
+ "light": "Light mode",
+ "bg": "Panel background",
+ "fg": "Buttons etc.",
+ "text": "Text",
+ "link": "Links",
+ "accent": "Accent color",
+ "cRed": "Red color",
+ "cBlue": "Blue color",
+ "cGreen": "Green color",
+ "cOrange": "Orange color",
+ "extra1": "Extra 1",
+ "extra2": "Extra 2",
+ "extra3": "Extra 3",
+ "v2_unsupported": "Older v2 themes don't support palettes. Switch to v3 theme to make use of palettes",
+ },
+ "editor": {
+ "title": "Style",
+ "new_style": "New",
+ "load_style": "Open",
+ "save_style": "Save",
+ "style_name": "Stylesheet name",
+ "style_author": "Made by",
+ "style_license": "License",
+ "style_website": "Website",
+ "component_selector": "Component",
+ "variant_selector": "Variant",
+ "states_selector": "States",
+ "main_tab": "Main",
+ "shadows_tab": "Shadows",
+ "background": "Background color",
+ "text_color": "Text color",
+ "icon_color": "Icon color",
+ "link_color": "Link color",
+ "include_in_rule": "Add to rule",
+ "text_auto": {
+ "label": "Auto-contrast",
+ "no-preserve": "Black or White",
+ "preserve": "Keep color",
+ "no-auto": "Disabled"
+ },
+ "components": {
+ "normal": {
+ "state": "Normal",
+ "variant": "Default"
+ },
+ "Alert": {
+ "friendlyName": "Alert",
+ "variants": {
+ "error": "Error",
+ "warning": "Warning",
+ "success": "Success"
+ }
+ },
+ "Button": {
+ "friendlyName": "Button",
+ "variants": {
+ "danger": "Dangerous"
+ },
+ "states": {
+ "toggled": "Toggled",
+ "pressed": "Pressed",
+ "hover": "Hovered",
+ "focused": "Has focus",
+ "disabled": "Disabled"
+ }
+ },
+ "Input": {
+ "friendlyName": "Input fields",
+ "variants": {
+ "checkbox": "Checkbox",
+ "radio": "Radio"
+ },
+ "states": {
+ "hover": "Hovered",
+ "focus": "Focused",
+ "disabled": "Disabled"
+ }
+ }
+ }
+ },
"hacks": {
"underlay_overrides": "Change underlay",
"underlay_override_mode_none": "Theme default",
@@ -882,6 +968,9 @@
"blur": "Blur",
"spread": "Spread",
"inset": "Inset",
+ "raw": "Plain shadow",
+ "expression": "Expression (advanced)",
+ "empty_expression": "Empty expression",
"hintV3": "For shadows you can also use the {0} notation to use other color slot.",
"filter_hint": {
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
diff --git a/src/modules/config.js b/src/modules/config.js
index 835dcce40..d046425c4 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -47,6 +47,8 @@ export const defaultState = {
customThemeSource: undefined, // "source", stores original theme data
// V3
+ style: null,
+ palette: null,
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
theme3hacks: { // Hacks, user overrides that are independent of theme used
diff --git a/src/modules/instance.js b/src/modules/instance.js
index dac167933..5cadc844a 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -42,6 +42,9 @@ const defaultState = {
registrationOpen: true,
server: 'http://localhost:4040/',
textlimit: 5000,
+ themesIndex: undefined,
+ stylesIndex: undefined,
+ palettesIndex: undefined,
themeData: undefined, // used for theme editor v2
vapidPublicKey: undefined,
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 57bfe0c61..917610599 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,10 +1,11 @@
-import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
+import { getResourcesIndex, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
const defaultState = {
localFonts: null,
themeApplied: false,
+ themeVersion: 'v3',
temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout
temporaryChangesConfirm: () => {}, // used for applying temporary options
temporaryChangesRevert: () => {}, // used for reverting temporary options
@@ -212,142 +213,400 @@ const interfaceMod = {
setLastTimeline ({ commit }, value) {
commit('setLastTimeline', value)
},
- setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) {
+ async fetchPalettesIndex ({ commit, state }) {
+ try {
+ const value = await getResourcesIndex('/static/palettes/index.json')
+ commit('setInstanceOption', { name: 'palettesIndex', value })
+ return value
+ } catch (e) {
+ console.error('Could not fetch palettes index', e)
+ commit('setInstanceOption', { name: 'palettesIndex', value: { _error: e } })
+ return Promise.resolve({})
+ }
+ },
+ setPalette ({ dispatch, commit }, value) {
+ dispatch('resetThemeV3Palette')
+ dispatch('resetThemeV2')
+
+ commit('setOption', { name: 'palette', value })
+
+ dispatch('applyTheme')
+ },
+ setPaletteCustom ({ dispatch, commit }, value) {
+ dispatch('resetThemeV3Palette')
+ dispatch('resetThemeV2')
+
+ commit('setOption', { name: 'paletteCustomData', value })
+
+ dispatch('applyTheme')
+ },
+ async fetchStylesIndex ({ commit, state }) {
+ try {
+ const value = await getResourcesIndex('/static/styles/index.json')
+ commit('setInstanceOption', { name: 'stylesIndex', value })
+ return value
+ } catch (e) {
+ console.error('Could not fetch styles index', e)
+ commit('setInstanceOption', { name: 'stylesIndex', value: { _error: e } })
+ return Promise.resolve({})
+ }
+ },
+ setStyle ({ dispatch, commit }, value) {
+ dispatch('resetThemeV3')
+ dispatch('resetThemeV2')
+
+ commit('setOption', { name: 'style', value })
+
+ dispatch('applyTheme')
+ },
+ setStyleCustom ({ dispatch, commit }, value) {
+ dispatch('resetThemeV3')
+ dispatch('resetThemeV2')
+
+ commit('setOption', { name: 'styleCustomData', value })
+
+ dispatch('applyTheme')
+ },
+ async fetchThemesIndex ({ commit, state }) {
+ try {
+ const value = await getResourcesIndex('/static/styles.json')
+ commit('setInstanceOption', { name: 'themesIndex', value })
+ return value
+ } catch (e) {
+ console.error('Could not fetch themes index', e)
+ commit('setInstanceOption', { name: 'themesIndex', value: { _error: e } })
+ return Promise.resolve({})
+ }
+ },
+ setTheme ({ dispatch, commit }, value) {
+ dispatch('resetThemeV3')
+ dispatch('resetThemeV3Palette')
+ dispatch('resetThemeV2')
+
+ commit('setOption', { name: 'theme', value })
+
+ dispatch('applyTheme')
+ },
+ setThemeCustom ({ dispatch, commit }, value) {
+ dispatch('resetThemeV3')
+ dispatch('resetThemeV3Palette')
+ dispatch('resetThemeV2')
+
+ commit('setOption', { name: 'customTheme', value })
+ commit('setOption', { name: 'customThemeSource', value })
+
+ dispatch('applyTheme')
+ },
+ resetThemeV3 ({ dispatch, commit }) {
+ commit('setOption', { name: 'style', value: null })
+ commit('setOption', { name: 'styleCustomData', value: null })
+ },
+ resetThemeV3Palette ({ dispatch, commit }) {
+ commit('setOption', { name: 'palette', value: null })
+ commit('setOption', { name: 'paletteCustomData', value: null })
+ },
+ resetThemeV2 ({ dispatch, commit }) {
+ commit('setOption', { name: 'theme', value: null })
+ commit('setOption', { name: 'customTheme', value: null })
+ commit('setOption', { name: 'customThemeSource', value: null })
+ },
+ async applyTheme (
+ { dispatch, commit, rootState, state },
+ { recompile = true } = {}
+ ) {
+ // If we're not not forced to recompile try using
+ // cache (tryLoadCache return true if load successful)
+
const {
- theme: instanceThemeName
+ style: instanceStyleName,
+ palette: instancePaletteName
+ } = rootState.instance
+ let {
+ theme: instanceThemeV2Name,
+ themesIndex,
+ stylesIndex,
+ palettesIndex
} = rootState.instance
const {
- theme: userThemeName,
- customTheme: userThemeSnapshot,
- customThemeSource: userThemeSource,
+ style: userStyleName,
+ styleCustomData: userStyleCustomData,
+ palette: userPaletteName,
+ paletteCustomData: userPaletteCustomData,
forceThemeRecompilation,
themeDebug,
theme3hacks
} = rootState.config
-
- const actualThemeName = userThemeName || instanceThemeName
+ let {
+ theme: userThemeV2Name,
+ customTheme: userThemeV2Snapshot,
+ customThemeSource: userThemeV2Source
+ } = rootState.config
const forceRecompile = forceThemeRecompilation || recompile
-
- let promise = null
-
- if (themeData) {
- promise = Promise.resolve(normalizeThemeData(themeData))
- } else if (themeName) {
- promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData))
- } else if (userThemeSource || userThemeSnapshot) {
- promise = Promise.resolve(normalizeThemeData({
- _pleroma_theme_version: 2,
- theme: userThemeSnapshot,
- source: userThemeSource
- }))
- } else if (actualThemeName && actualThemeName !== 'custom') {
- promise = getPreset(actualThemeName).then(themeData => {
- const realThemeData = normalizeThemeData(themeData)
- if (actualThemeName === instanceThemeName) {
- // This sole line is the reason why this whole block is above the recompilation check
- commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } })
- }
- return realThemeData
- })
- } else {
- throw new Error('Cannot load any theme!')
- }
-
- // If we're not not forced to recompile try using
- // cache (tryLoadCache return true if load successful)
if (!forceRecompile && !themeDebug && tryLoadCache()) {
- commit('setThemeApplied')
- return
+ return commit('setThemeApplied')
}
- promise
- .then(realThemeData => {
- const theme2ruleset = convertTheme2To3(realThemeData)
+ let majorVersionUsed
- if (saveData) {
- commit('setOption', { name: 'theme', value: themeName || actualThemeName })
- commit('setOption', { name: 'customTheme', value: realThemeData })
- commit('setOption', { name: 'customThemeSource', value: realThemeData })
- }
- const hacks = []
+ console.log(
+ `USER V3 palette: ${userPaletteName}, style: ${userStyleName} `
+ )
+ console.log(
+ `USER V2 name: ${userThemeV2Name}, source: ${userThemeV2Source}, snapshot: ${userThemeV2Snapshot}`
+ )
- Object.entries(theme3hacks).forEach(([key, value]) => {
- switch (key) {
- case 'fonts': {
- Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
- if (!font?.family) return
- switch (fontKey) {
- case 'interface':
- hacks.push({
- component: 'Root',
- directives: {
- '--font': 'generic | ' + font.family
- }
- })
- break
- case 'input':
- hacks.push({
- component: 'Input',
- directives: {
- '--font': 'generic | ' + font.family
- }
- })
- break
- case 'post':
- hacks.push({
- component: 'RichContent',
- directives: {
- '--font': 'generic | ' + font.family
- }
- })
- break
- case 'monospace':
- hacks.push({
- component: 'Root',
- directives: {
- '--monoFont': 'generic | ' + font.family
- }
- })
- break
- }
- })
- break
- }
- case 'underlay': {
- if (value !== 'none') {
- const newRule = {
- component: 'Underlay',
- directives: {}
- }
- if (value === 'opaque') {
- newRule.directives.opacity = 1
- newRule.directives.background = '--wallpaper'
- }
- if (value === 'transparent') {
- newRule.directives.opacity = 0
- }
- hacks.push(newRule)
- }
- break
- }
- }
- })
+ console.log(`INST V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`)
+ console.log('INST V2 theme: ' + instanceThemeV2Name)
- const ruleset = [
- ...theme2ruleset,
- ...hacks
- ]
-
- applyTheme(
- ruleset,
- () => commit('setThemeApplied'),
- themeDebug
+ if (userPaletteName || userPaletteCustomData ||
+ userStyleName || userStyleCustomData ||
+ (
+ // User V2 overrides instance V3
+ (instancePaletteName ||
+ instanceStyleName) &&
+ instanceThemeV2Name == null &&
+ userThemeV2Name == null
)
- })
+ ) {
+ // Palette and/or style overrides V2 themes
+ instanceThemeV2Name = null
+ userThemeV2Name = null
+ userThemeV2Source = null
+ userThemeV2Snapshot = null
- return promise
+ majorVersionUsed = 'v3'
+ } else if (
+ (userThemeV2Name ||
+ userThemeV2Snapshot ||
+ userThemeV2Source ||
+ instanceThemeV2Name)
+ ) {
+ majorVersionUsed = 'v2'
+ } else {
+ // if all fails fallback to v3
+ majorVersionUsed = 'v3'
+ }
+
+ if (majorVersionUsed === 'v3') {
+ const result = await Promise.all([
+ dispatch('fetchPalettesIndex'),
+ dispatch('fetchStylesIndex')
+ ])
+
+ palettesIndex = result[0]
+ stylesIndex = result[1]
+ } else {
+ // Promise.all just to be uniform with v3
+ const result = await Promise.all([
+ dispatch('fetchThemesIndex')
+ ])
+
+ themesIndex = result[0]
+ }
+
+ state.themeVersion = majorVersionUsed
+
+ let styleDataUsed = null
+ let styleNameUsed = null
+ let paletteDataUsed = null
+ let paletteNameUsed = null
+ let themeNameUsed = null
+ let themeDataUsed = null
+
+ const getData = async (resource, index, customData, name) => {
+ const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
+ const result = {}
+
+ if (customData) {
+ result.nameUsed = 'custom' // custom data overrides name
+ result.dataUsed = customData
+ } else {
+ result.nameUsed = name
+
+ if (result.nameUsed === 'stock') {
+ result.dataUsed = null
+ return result
+ }
+
+ let fetchFunc = index[result.nameUsed]
+ // Fallbacks
+ if (!fetchFunc) {
+ const newName = Object.keys(index)[0]
+ fetchFunc = index[newName]
+ console.warn(`${capitalizedResource} with id '${styleNameUsed}' not found, trying back to '${newName}'`)
+ if (!fetchFunc) {
+ console.warn(`${capitalizedResource} doesn't have a fallback, defaulting to stock.`)
+ fetchFunc = () => Promise.resolve(null)
+ }
+ }
+ result.dataUsed = await fetchFunc()
+ }
+ return result
+ }
+
+ console.log('VERSION', majorVersionUsed)
+
+ if (majorVersionUsed === 'v3') {
+ const palette = await getData(
+ 'palette',
+ palettesIndex,
+ userPaletteCustomData,
+ userPaletteName || instancePaletteName
+ )
+ paletteNameUsed = palette.nameUsed
+ paletteDataUsed = palette.dataUsed
+ if (Array.isArray(paletteDataUsed)) {
+ const [
+ name,
+ background,
+ foreground,
+ text,
+ link,
+ cRed = '#FF0000',
+ cGreen = '#00FF00',
+ cBlue = '#0000FF',
+ cOrange = '#E3FF00'
+ ] = paletteDataUsed
+ paletteDataUsed = { name, background, foreground, text, link, cRed, cBlue, cGreen, cOrange }
+ }
+ console.log('PAL', userPaletteName, paletteNameUsed)
+ console.log('PAL', paletteDataUsed)
+
+ const style = await getData(
+ 'style',
+ stylesIndex,
+ userStyleCustomData,
+ userStyleName || instanceStyleName
+ )
+ styleNameUsed = style.nameUsed
+ styleDataUsed = style.dataUsed
+ } else {
+ const theme = await getData(
+ 'theme',
+ themesIndex,
+ userThemeV2Source || userThemeV2Snapshot,
+ userThemeV2Name || instanceThemeV2Name
+ )
+ themeNameUsed = theme.nameUsed
+ themeDataUsed = theme.dataUsed
+
+ // Themes v2 editor support
+ commit('setInstanceOption', { name: 'themeData', value: themeDataUsed })
+ }
+
+ console.log('STYLE', styleNameUsed, paletteNameUsed, themeNameUsed)
+
+ // commit('setOption', { name: 'palette', value: paletteNameUsed })
+ // commit('setOption', { name: 'style', value: styleNameUsed })
+ // commit('setOption', { name: 'theme', value: themeNameUsed })
+
+ const paletteIss = (() => {
+ if (!paletteDataUsed) return null
+ const result = {
+ component: 'Root',
+ directives: {}
+ }
+
+ console.log('PALETTE', paletteDataUsed)
+ Object
+ .entries(paletteDataUsed)
+ .filter(([k]) => k !== 'name')
+ .forEach(([k, v]) => {
+ let issRootDirectiveName
+ switch (k) {
+ case 'background':
+ issRootDirectiveName = 'bg'
+ break
+ case 'foreground':
+ issRootDirectiveName = 'fg'
+ break
+ default:
+ issRootDirectiveName = k
+ }
+ result.directives['--' + issRootDirectiveName] = 'color | ' + v
+ })
+ return result
+ })()
+
+ const theme2ruleset = themeDataUsed && convertTheme2To3(generatePreset(themeDataUsed).source)
+ const hacks = []
+
+ Object.entries(theme3hacks).forEach(([key, value]) => {
+ switch (key) {
+ case 'fonts': {
+ Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
+ if (!font?.family) return
+ switch (fontKey) {
+ case 'interface':
+ hacks.push({
+ component: 'Root',
+ directives: {
+ '--font': 'generic | ' + font.family
+ }
+ })
+ break
+ case 'input':
+ hacks.push({
+ component: 'Input',
+ directives: {
+ '--font': 'generic | ' + font.family
+ }
+ })
+ break
+ case 'post':
+ hacks.push({
+ component: 'RichContent',
+ directives: {
+ '--font': 'generic | ' + font.family
+ }
+ })
+ break
+ case 'monospace':
+ hacks.push({
+ component: 'Root',
+ directives: {
+ '--monoFont': 'generic | ' + font.family
+ }
+ })
+ break
+ }
+ })
+ break
+ }
+ case 'underlay': {
+ if (value !== 'none') {
+ const newRule = {
+ component: 'Underlay',
+ directives: {}
+ }
+ if (value === 'opaque') {
+ newRule.directives.opacity = 1
+ newRule.directives.background = '--wallpaper'
+ }
+ if (value === 'transparent') {
+ newRule.directives.opacity = 0
+ }
+ hacks.push(newRule)
+ }
+ break
+ }
+ }
+ })
+
+ const rulesetArray = [
+ theme2ruleset,
+ styleDataUsed,
+ paletteIss,
+ hacks
+ ].filter(x => x)
+
+ return applyTheme(
+ rulesetArray.flat(),
+ () => commit('setThemeApplied'),
+ themeDebug
+ )
}
}
}
@@ -365,7 +624,7 @@ export const normalizeThemeData = (input) => {
themeData.colors.cGreen = input[6]
themeData.colors.cBlue = input[7]
themeData.colors.cOrange = input[8]
- return generatePreset(themeData).theme
+ return generatePreset(themeData).source || generatePreset(themeData).theme
}
let themeData, themeSource
diff --git a/src/services/export_import/export_import.js b/src/services/export_import/export_import.js
index 7fee0ad33..e1ac6a425 100644
--- a/src/services/export_import/export_import.js
+++ b/src/services/export_import/export_import.js
@@ -20,6 +20,7 @@ export const newExporter = ({
})
export const newImporter = ({
+ accept = '.json',
onImport,
onImportFailure,
validator = () => true
@@ -27,18 +28,19 @@ export const newImporter = ({
importData () {
const filePicker = document.createElement('input')
filePicker.setAttribute('type', 'file')
- filePicker.setAttribute('accept', '.json')
+ filePicker.setAttribute('accept', accept)
filePicker.addEventListener('change', event => {
if (event.target.files[0]) {
+ const filename = event.target.files[0].name
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({ target }) => {
try {
const parsed = JSON.parse(target.result)
- const validationResult = validator(parsed)
+ const validationResult = validator(parsed, filename)
if (validationResult === true) {
- onImport(parsed)
+ onImport(parsed, filename)
} else {
onImportFailure({ validationResult })
}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index c1603f39f..441b89654 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,4 +1,3 @@
-import { hex2rgb } from '../color_convert/color_convert.js'
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
import { getCssRules } from '../theme_data/css_utils.js'
import { defaultState } from '../../modules/config.js'
@@ -52,8 +51,6 @@ export const generateTheme = (inputRuleset, callbacks, debug) => {
const themes3 = init({
inputRuleset,
- // Assuming that "worst case scenario background" is panel background since it's the most likely one
- ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(),
debug
})
@@ -259,57 +256,36 @@ export const applyConfig = (input, i18n) => {
}
}
-export const getThemes = () => {
+export const getResourcesIndex = async (url) => {
const cache = 'no-store'
- return window.fetch('/static/styles.json', { cache })
- .then((data) => data.json())
- .then((themes) => {
- return Object.entries(themes).map(([k, v]) => {
- let promise = null
- if (typeof v === 'object') {
- promise = Promise.resolve(v)
- } else if (typeof v === 'string') {
- promise = window.fetch(v, { cache })
- .then((data) => data.json())
- .catch((e) => {
- console.error(e)
- return null
- })
- }
- return [k, promise]
- })
- })
- .then((promises) => {
- return promises
- .reduce((acc, [k, v]) => {
- acc[k] = v
- return acc
- }, {})
- })
-}
-
-export const getPreset = (val) => {
- return getThemes()
- .then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
- .then((theme) => {
- const isV1 = Array.isArray(theme)
- const data = isV1 ? {} : theme.theme
-
- if (isV1) {
- const bg = hex2rgb(theme[1])
- const fg = hex2rgb(theme[2])
- const text = hex2rgb(theme[3])
- const link = hex2rgb(theme[4])
-
- const cRed = hex2rgb(theme[5] || '#FF0000')
- const cGreen = hex2rgb(theme[6] || '#00FF00')
- const cBlue = hex2rgb(theme[7] || '#0000FF')
- const cOrange = hex2rgb(theme[8] || '#E3FF00')
-
- data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
- }
-
- return { theme: data, source: theme.source }
- })
+ try {
+ const data = await window.fetch(url, { cache })
+ const resources = await data.json()
+ return Object.fromEntries(
+ Object
+ .entries(resources)
+ .map(([k, v]) => {
+ if (typeof v === 'object') {
+ return [k, () => Promise.resolve(v)]
+ } else if (typeof v === 'string') {
+ return [
+ k,
+ () => window
+ .fetch(v, { cache })
+ .then((data) => data.json())
+ .catch((e) => {
+ console.error(e)
+ return null
+ })
+ ]
+ } else {
+ console.error(`Unknown resource format - ${k} is a ${typeof v}`)
+ return [k, null]
+ }
+ })
+ )
+ } catch (e) {
+ return Promise.reject(e)
+ }
}
diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js
index 9bce48340..e33960831 100644
--- a/src/services/theme_data/css_utils.js
+++ b/src/services/theme_data/css_utils.js
@@ -2,25 +2,6 @@ import { convert } from 'chromatism'
import { hex2rgb, rgba2css } from '../color_convert/color_convert.js'
-export const parseCssShadow = (text) => {
- const dimensions = /(\d[a-z]*\s?){2,4}/.exec(text)?.[0]
- const inset = /inset/.exec(text)?.[0]
- const color = text.replace(dimensions, '').replace(inset, '')
-
- const [x, y, blur = 0, spread = 0] = dimensions.split(/ /).filter(x => x).map(x => x.trim())
- const isInset = inset?.trim() === 'inset'
- const colorString = color.split(/ /).filter(x => x).map(x => x.trim())[0]
-
- return {
- x,
- y,
- blur,
- spread,
- inset: isInset,
- color: colorString
- }
-}
-
export const getCssColorString = (color, alpha = 1) => rgba2css({ ...convert(color).rgb, a: alpha })
export const getCssShadow = (input, usesDropShadow) => {
@@ -84,6 +65,9 @@ export const getCssRules = (rules, debug) => rules.map(rule => {
].join(';\n ')
}
case 'shadow': {
+ if (!rule.dynamicVars.shadow) {
+ return ''
+ }
return ' ' + [
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
'--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow),
diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js
index 909e9411a..3ac8826f2 100644
--- a/src/services/theme_data/iss_deserializer.js
+++ b/src/services/theme_data/iss_deserializer.js
@@ -1,6 +1,6 @@
import { flattenDeep } from 'lodash'
-const parseShadow = string => {
+export const parseShadow = string => {
const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha']
const regexPrep = [
// inset keyword (optional)
@@ -26,7 +26,12 @@ const parseShadow = string => {
const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha'])
const { x, y, blur, spread, alpha, inset, color } = Object.fromEntries(modes.map((mode, i) => {
if (numeric.has(mode)) {
- return [mode, Number(result[i])]
+ const number = Number(result[i])
+ if (Number.isNaN(number)) {
+ if (mode === 'alpha') return [mode, 1]
+ return [mode, 0]
+ }
+ return [mode, number]
} else if (mode === 'inset') {
return [mode, !!result[i]]
} else {
@@ -136,7 +141,7 @@ export const deserialize = (input) => {
output.directives = Object.fromEntries(content.map(d => {
const [property, value] = d.split(':')
- let realValue = value.trim()
+ let realValue = (value || '').trim()
if (property === 'shadow') {
if (realValue === 'none') {
realValue = []
diff --git a/src/services/theme_data/theme3_slot_functions.js b/src/services/theme_data/theme3_slot_functions.js
index 074a88f0c..24652429e 100644
--- a/src/services/theme_data/theme3_slot_functions.js
+++ b/src/services/theme_data/theme3_slot_functions.js
@@ -3,7 +3,7 @@ import { alphaBlend, getTextColor, relativeLuminance } from '../color_convert/co
export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => {
const { funcName, argsString } = /\$(?
\w+)\((?[#a-zA-Z0-9-,.'"\s]*)\)/.exec(text).groups
- const args = argsString.split(/,/g).map(a => a.trim())
+ const args = argsString.split(/ /g).map(a => a.trim())
const func = functions[funcName]
if (args.length < func.argsNeeded) {
diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js
index 3c2f8a637..e45d2cefd 100644
--- a/src/services/theme_data/theme_data_3.service.js
+++ b/src/services/theme_data/theme_data_3.service.js
@@ -22,7 +22,7 @@ import {
normalizeCombination,
findRules
} from './iss_utils.js'
-import { parseCssShadow } from './css_utils.js'
+import { parseShadow } from './iss_deserializer.js'
// Ensuring the order of components
const components = {
@@ -44,11 +44,11 @@ const findShadow = (shadows, { dynamicVars, staticVars }) => {
if (shadow.startsWith('$')) {
targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars })
} else if (shadow.startsWith('--')) {
- const [variable] = shadow.split(/,/g).map(str => str.trim()) // discarding modifier since it's not supported
- const variableSlot = variable.substring(2)
+ // modifiers are completely unsupported here
+ const variableSlot = shadow.substring(2)
return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
} else {
- targetShadow = parseCssShadow(shadow)
+ targetShadow = parseShadow(shadow)
}
} else {
targetShadow = shadow
@@ -66,6 +66,7 @@ const findColor = (color, { dynamicVars, staticVars }) => {
if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
let targetColor = null
if (color.startsWith('--')) {
+ // Modifier support is pretty much for v2 themes only
const [variable, modifier] = color.split(/,/g).map(str => str.trim())
const variableSlot = variable.substring(2)
if (variableSlot === 'stack') {
@@ -172,11 +173,13 @@ export const init = ({
ultimateBackgroundColor,
debug = false,
liteMode = false,
+ editMode = false,
onlyNormalState = false,
- rootComponentName = 'Root'
+ rootComponentName = 'Root',
+ initialStaticVars = {}
}) => {
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
- const staticVars = {}
+ const staticVars = { ...initialStaticVars }
const stacked = {}
const computed = {}
@@ -228,7 +231,14 @@ export const init = ({
})
.map(({ data }) => data)
+ if (!ultimateBackgroundColor) {
+ console.warn('No ultimate background color provided, falling back to panel color')
+ const rootRule = ruleset.findLast((x) => (x.component === 'Root' && x.directives?.['--bg']))
+ ultimateBackgroundColor = rootRule.directives['--bg'].split('|')[1].trim()
+ }
+
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
+ const nonEditableComponents = new Set(Object.values(components).filter(c => c.notEditable).map(c => c.name))
const processCombination = (combination) => {
const selector = ruleToSelector(combination, true)
@@ -238,7 +248,11 @@ export const init = ({
const soloSelector = selector.split(/ /g).slice(-1)[0]
const lowerLevelSelector = parentSelector
- const lowerLevelBackground = computed[lowerLevelSelector]?.background
+ let lowerLevelBackground = computed[lowerLevelSelector]?.background
+ if (editMode && !lowerLevelBackground) {
+ // FIXME hack for editor until it supports handling component backgrounds
+ lowerLevelBackground = '#00FFFF'
+ }
const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
@@ -397,27 +411,27 @@ export const init = ({
const dynamicSlots = Object.entries(computedDirectives).filter(([k, v]) => k.startsWith('--'))
dynamicSlots.forEach(([k, v]) => {
- const [type, ...value] = v.split('|').map(x => x.trim()) // woah, Extreme!
+ const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme!
switch (type) {
case 'color': {
- const color = findColor(value[0], { dynamicVars, staticVars })
+ const color = findColor(value, { dynamicVars, staticVars })
dynamicVars[k] = color
- if (combination.component === 'Root') {
+ if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = color
}
break
}
case 'shadow': {
- const shadow = value
+ const shadow = value.split(/,/g).map(s => s.trim()).filter(x => x)
dynamicVars[k] = shadow
- if (combination.component === 'Root') {
+ if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = shadow
}
break
}
case 'generic': {
dynamicVars[k] = value
- if (combination.component === 'Root') {
+ if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = value
}
break
@@ -443,11 +457,15 @@ export const init = ({
variants: originalVariants = {}
} = component
- const validInnerComponents = (
- liteMode
- ? (component.validInnerComponentsLite || component.validInnerComponents)
- : component.validInnerComponents
- ) || []
+ let validInnerComponents
+ if (editMode) {
+ const temp = (component.validInnerComponentsLite || component.validInnerComponents || [])
+ validInnerComponents = temp.filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
+ } else if (liteMode) {
+ validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
+ } else {
+ validInnerComponents = component.validInnerComponents || []
+ }
// Normalizing states and variants to always include "normal"
const states = { normal: '', ...originalStates }
diff --git a/static/config.json b/static/config.json
index fb39ff77f..46a4857d1 100644
--- a/static/config.json
+++ b/static/config.json
@@ -24,6 +24,6 @@
"showInstanceSpecificPanel": false,
"sidebarRight": false,
"subjectLineBehavior": "email",
- "theme": "pleroma-dark",
+ "theme": null,
"webPushNotifications": false
}
diff --git a/static/palettes/index.json b/static/palettes/index.json
new file mode 100644
index 000000000..63fbad5a5
--- /dev/null
+++ b/static/palettes/index.json
@@ -0,0 +1,32 @@
+{
+ "pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "classic-dark": {
+ "name": "Classic Dark",
+ "background": "#161c20",
+ "foreground": "#282e32",
+ "text": "#b9b9b9",
+ "link": "#baaa9c",
+ "cRed": "#d31014",
+ "cGreen": "#0fa00f",
+ "cBlue": "#0095ff",
+ "cOrange": "#ffa500"
+ },
+ "bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
+ "pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
+ "tomorrow-night": {
+ "name": "Tomorrow Night",
+ "background": "#1d1f21",
+ "foreground": "#373b41",
+ "link": "#81a2be",
+ "text": "#c5c8c6",
+ "cRed": "#cc6666",
+ "cBlue": "#8abeb7",
+ "cGreen": "#b5bd68",
+ "cOrange": "#de935f",
+ "_cYellow": "#f0c674",
+ "_cPurple": "#b294bb"
+ },
+ "ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
+ "monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ]
+}
diff --git a/static/styles.json b/static/styles.json
index 23f57c65e..2f836a47a 100644
--- a/static/styles.json
+++ b/static/styles.json
@@ -1,12 +1,6 @@
{
"pleroma-dark": "/static/themes/pleroma-dark.json",
"pleroma-light": "/static/themes/pleroma-light.json",
- "pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
- "classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
- "bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
- "ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
- "monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
-
"redmond-xx": "/static/themes/redmond-xx.json",
"redmond-xx-se": "/static/themes/redmond-xx-se.json",
"redmond-xxi": "/static/themes/redmond-xxi.json",