diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 55dd01a9c..9c2bbb520 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -327,7 +327,11 @@ const setConfig = async ({ store }) => {
const checkOAuthToken = async ({ store }) => {
if (store.getters.getUserToken()) {
- return store.dispatch('loginUser', store.getters.getUserToken())
+ try {
+ await store.dispatch('loginUser', store.getters.getUserToken())
+ } catch (e) {
+ console.error(e)
+ }
}
return Promise.resolve()
}
@@ -345,26 +349,19 @@ const afterStoreSetup = async ({ store, i18n }) => {
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server })
- document.querySelector('#status').textContent = i18n.global.t('splash.settings')
await setConfig({ store })
- document.querySelector('#status').textContent = i18n.global.t('splash.theme')
- try {
- await store.dispatch('setTheme').catch((e) => { console.error('Error setting theme', e) })
- } catch (e) {
- return Promise.reject(e)
- }
+ await store.dispatch('applyTheme', { recompile: false })
- applyConfig(store.state.config, i18n.global)
+ applyConfig(store.state.config)
// Now we can try getting the server settings and logging in
// Most of these are preloaded into the index.html so blocking is minimized
- document.querySelector('#status').textContent = i18n.global.t('splash.instance')
await Promise.all([
checkOAuthToken({ store }),
getInstancePanel({ store }),
getNodeInfo({ store }),
getInstanceConfig({ store })
- ]).catch(e => Promise.reject(e))
+ ])
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
@@ -398,9 +395,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
// remove after vue 3.3
app.config.unwrapInjectedRef = true
- document.querySelector('#status').textContent = i18n.global.t('splash.almost')
app.mount('#app')
+
return app
}
diff --git a/src/components/alert.style.js b/src/components/alert.style.js
index abbeb5baa..868514764 100644
--- a/src/components/alert.style.js
+++ b/src/components/alert.style.js
@@ -14,6 +14,10 @@ export default {
warning: '.warning',
success: '.success'
},
+ editor: {
+ border: 1,
+ aspect: '3 / 1'
+ },
defaultRules: [
{
directives: {
diff --git a/src/components/border.style.js b/src/components/border.style.js
index a87ee9c87..7f2c30163 100644
--- a/src/components/border.style.js
+++ b/src/components/border.style.js
@@ -5,7 +5,7 @@ export default {
defaultRules: [
{
directives: {
- textColor: '$mod(--parent, 10)',
+ textColor: '$mod(--parent 10)',
textAuto: 'no-auto'
}
}
diff --git a/src/components/button.style.js b/src/components/button.style.js
index 1423d5c78..95ef3e403 100644
--- a/src/components/button.style.js
+++ b/src/components/button.style.js
@@ -9,9 +9,9 @@ export default {
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants
// normal: '' // normal state is implicitly added, it is always included
toggled: '.toggled',
- pressed: ':active',
+ focused: ':focus-visible',
+ pressed: ':focus:active',
hover: ':hover:not(:disabled)',
- focused: ':focus-within',
disabled: ':disabled'
},
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
@@ -22,6 +22,9 @@ export default {
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
// This (currently) is further multipled by number of places where component can exist.
},
+ editor: {
+ aspect: '2 / 1'
+ },
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
validInnerComponents: [
'Text',
@@ -32,10 +35,11 @@ export default {
{
component: 'Root',
directives: {
- '--defaultButtonHoverGlow': 'shadow | 0 0 4 --text',
+ '--defaultButtonHoverGlow': 'shadow | 0 0 4 --text / 0.5',
+ '--defaultButtonFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
'--defaultButtonShadow': 'shadow | 0 0 2 #000000',
- '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2), $borderSide(#000000, bottom, 0.2)',
- '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
+ '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF top 0.2 2), $borderSide(#000000 bottom 0.2 2)',
+ '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2 2), $borderSide(#000000 top 0.2 2)'
}
},
{
@@ -53,6 +57,12 @@ export default {
shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel']
}
},
+ {
+ state: ['focused'],
+ directives: {
+ shadow: ['--defaultButtonFocusGlow', '--defaultButtonBevel']
+ }
+ },
{
state: ['pressed'],
directives: {
@@ -60,9 +70,9 @@ export default {
}
},
{
- state: ['hover', 'pressed'],
+ state: ['pressed', 'hover'],
directives: {
- shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel']
+ shadow: ['--pressedButtonBevel', '--defaultButtonHoverGlow']
}
},
{
@@ -82,7 +92,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/button_unstyled.style.js b/src/components/button_unstyled.style.js
index 65b5c57bf..a4f0f6f99 100644
--- a/src/components/button_unstyled.style.js
+++ b/src/components/button_unstyled.style.js
@@ -1,6 +1,7 @@
export default {
name: 'ButtonUnstyled',
selector: '.button-unstyled',
+ notEditable: true,
states: {
toggled: '.toggled',
disabled: ':disabled',
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index b6e84629f..31dfa23ef 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -15,7 +15,7 @@
:model-value="present"
:disabled="disabled"
class="opt"
- @update:modelValue="update(typeof modelValue === 'undefined' ? fallback : undefined)"
+ @update:modelValue="updateValue(typeof modelValue === 'undefined' ? fallback : undefined)"
/>
+
-
diff --git a/src/components/icon.style.js b/src/components/icon.style.js
index 6cb9e4e39..4d30f389b 100644
--- a/src/components/icon.style.js
+++ b/src/components/icon.style.js
@@ -6,7 +6,7 @@ export default {
{
component: 'Icon',
directives: {
- textColor: '$blend(--stack, 0.5, --parent--text)',
+ textColor: '$blend(--stack 0.5 --parent--text)',
textAuto: 'no-auto'
}
}
diff --git a/src/components/input.style.js b/src/components/input.style.js
index 6ad6cf903..c60ac1e4e 100644
--- a/src/components/input.style.js
+++ b/src/components/input.style.js
@@ -1,12 +1,3 @@
-const hoverGlow = {
- x: 0,
- y: 0,
- blur: 4,
- spread: 0,
- color: '--text',
- alpha: 1
-}
-
export default {
name: 'Input',
selector: '.input',
@@ -27,7 +18,9 @@ export default {
{
component: 'Root',
directives: {
- '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
+ '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2)',
+ '--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
+ '--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5'
}
},
{
@@ -54,7 +47,19 @@ export default {
{
state: ['hover'],
directives: {
- shadow: [hoverGlow, '--defaultInputBevel']
+ shadow: ['--defaultInputHoverGlow', '--defaultInputBevel']
+ }
+ },
+ {
+ state: ['focused'],
+ directives: {
+ shadow: ['--defaultInputFocusGlow', '--defaultInputBevel']
+ }
+ },
+ {
+ state: ['focused', 'hover'],
+ directives: {
+ shadow: ['--defaultInputFocusGlow', '--defaultInputHoverGlow', '--defaultInputBevel']
}
},
{
diff --git a/src/components/menu_item.style.js b/src/components/menu_item.style.js
index 51388155d..5b3ff699c 100644
--- a/src/components/menu_item.style.js
+++ b/src/components/menu_item.style.js
@@ -24,21 +24,21 @@ export default {
{
state: ['hover'],
directives: {
- background: '$mod(--bg, 5)',
+ background: '$mod(--bg 5)',
opacity: 1
}
},
{
state: ['active'],
directives: {
- background: '$mod(--bg, 10)',
+ background: '$mod(--bg 10)',
opacity: 1
}
},
{
state: ['active', 'hover'],
directives: {
- background: '$mod(--bg, 15)',
+ background: '$mod(--bg 15)',
opacity: 1
}
},
diff --git a/src/components/modal/modals.style.js b/src/components/modal/modals.style.js
index c401a0cda..f53a5f90f 100644
--- a/src/components/modal/modals.style.js
+++ b/src/components/modal/modals.style.js
@@ -2,6 +2,7 @@ export default {
name: 'Modals',
selector: '.modal-view',
lazy: true,
+ notEditable: true,
validInnerComponents: [
'Panel'
],
diff --git a/src/components/palette_editor/palette_editor.vue b/src/components/palette_editor/palette_editor.vue
new file mode 100644
index 000000000..b6350b0f2
--- /dev/null
+++ b/src/components/palette_editor/palette_editor.vue
@@ -0,0 +1,123 @@
+
+
+
+ updatePalette(key, value)"
+ />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/root.style.js b/src/components/root.style.js
index 4bd735aa5..25b2b6653 100644
--- a/src/components/root.style.js
+++ b/src/components/root.style.js
@@ -1,6 +1,7 @@
export default {
name: 'Root',
selector: ':root',
+ notEditable: true,
validInnerComponents: [
'Underlay',
'Modals',
@@ -42,7 +43,7 @@ export default {
// Selection colors
'--selectionBackground': 'color | --accent',
- '--selectionText': 'color | $textColor(--accent, --text, no-preserve)'
+ '--selectionText': 'color | $textColor(--accent --text no-preserve)'
}
}
]
diff --git a/src/components/scrollbar.style.js b/src/components/scrollbar.style.js
index 94e6135d7..714e9c81e 100644
--- a/src/components/scrollbar.style.js
+++ b/src/components/scrollbar.style.js
@@ -1,6 +1,7 @@
export default {
name: 'Scrollbar',
selector: '::-webkit-scrollbar',
+ notEditable: true, // for now
defaultRules: [
{
directives: {
diff --git a/src/components/scrollbar_element.style.js b/src/components/scrollbar_element.style.js
index da942ab21..caa239aab 100644
--- a/src/components/scrollbar_element.style.js
+++ b/src/components/scrollbar_element.style.js
@@ -31,6 +31,7 @@ const hoverGlow = {
export default {
name: 'ScrollbarElement',
selector: '::-webkit-scrollbar-button',
+ notEditable: true, // for now
states: {
pressed: ':active',
hover: ':hover:not(:disabled)',
@@ -82,7 +83,7 @@ export default {
{
state: ['disabled'],
directives: {
- background: '$blend(--inheritedBackground, 0.25, --parent)',
+ background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: [...buttonInsetFakeBorders]
}
},
diff --git a/src/components/select/select.vue b/src/components/select/select.vue
index 39d3ca647..0fb6fcc0e 100644
--- a/src/components/select/select.vue
+++ b/src/components/select/select.vue
@@ -61,12 +61,13 @@ label.Select {
&:disabled {
background-color: var(--background);
opacity: 1; /* override browser */
+ color: var(--faint);
select {
&[multiple],
&[size] {
option.-active {
- color: var(--text);
+ color: var(--faint);
background: transparent;
}
}
diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js
index 3b3e6268d..6c5dd76e3 100644
--- a/src/components/settings_modal/helpers/setting.js
+++ b/src/components/settings_modal/helpers/setting.js
@@ -10,9 +10,13 @@ export default {
ProfileSettingIndicator
},
props: {
+ modelValue: {
+ type: String,
+ default: null
+ },
path: {
type: [String, Array],
- required: true
+ required: false
},
disabled: {
type: Boolean,
@@ -68,7 +72,7 @@ export default {
}
},
created () {
- if (this.realDraftMode && this.realSource !== 'admin') {
+ if (this.realDraftMode && (this.realSource !== 'admin' || this.path == null)) {
this.draft = this.state
}
},
@@ -76,14 +80,14 @@ export default {
draft: {
// TODO allow passing shared draft object?
get () {
- if (this.realSource === 'admin') {
+ if (this.realSource === 'admin' || this.path == null) {
return get(this.$store.state.adminSettings.draft, this.canonPath)
} else {
return this.localDraft
}
},
set (value) {
- if (this.realSource === 'admin') {
+ if (this.realSource === 'admin' || this.path == null) {
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
} else {
this.localDraft = value
@@ -91,6 +95,9 @@ export default {
}
},
state () {
+ if (this.path == null) {
+ return this.modelValue
+ }
const value = get(this.configSource, this.canonPath)
if (value === undefined) {
return this.defaultState
@@ -145,6 +152,9 @@ export default {
return this.backendDescription?.suggestions
},
shouldBeDisabled () {
+ if (this.path == null) {
+ return this.disabled
+ }
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
},
@@ -159,6 +169,9 @@ export default {
}
},
configSink () {
+ if (this.path == null) {
+ return (k, v) => this.$emit('modelValue:update', v)
+ }
switch (this.realSource) {
case 'profile':
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
@@ -184,6 +197,7 @@ export default {
return this.realSource === 'profile'
},
isChanged () {
+ if (this.path == null) return false
switch (this.realSource) {
case 'profile':
case 'admin':
@@ -193,9 +207,11 @@ export default {
}
},
canonPath () {
+ if (this.path == null) return null
return Array.isArray(this.path) ? this.path : this.path.split('.')
},
isDirty () {
+ if (this.path == null) return false
if (this.realSource === 'admin' && this.canonPath.length > 3) {
return false // should not show draft buttons for "grouped" values
} else {
diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue
index 7b30d1b9f..fbea0b508 100644
--- a/src/components/settings_modal/helpers/string_setting.vue
+++ b/src/components/settings_modal/helpers/string_setting.vue
@@ -15,6 +15,7 @@
+ {{ ' ' }}
div,
> label {
- display: block;
margin-bottom: 0.5em;
&:last-child {
diff --git a/src/components/settings_modal/settings_modal_user_content.vue b/src/components/settings_modal/settings_modal_user_content.vue
index 1441d892d..695102bd8 100644
--- a/src/components/settings_modal/settings_modal_user_content.vue
+++ b/src/components/settings_modal/settings_modal_user_content.vue
@@ -20,6 +20,13 @@
>
+
+
+
({
key: mode,
@@ -64,30 +79,50 @@ const AppearanceTab = {
Preview
},
mounted () {
- getThemes()
- .then((promises) => {
- return Promise.all(
- Object.entries(promises)
- .map(([k, v]) => v.then(res => [k, res]))
- )
+ const updateIndex = (resource) => {
+ const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
+ const currentIndex = this.$store.state.instance[`${resource}sIndex`]
+
+ let promise
+ if (currentIndex) {
+ promise = Promise.resolve(currentIndex)
+ } else {
+ promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`)
+ }
+
+ return promise.then(index => {
+ return Object
+ .entries(index)
+ .map(([k, func]) => [k, func()])
})
- .then(themes => themes.reduce((acc, [k, v]) => {
- if (v) {
- return [
- ...acc,
- {
- name: v.name || v[0],
- key: k,
- data: v
- }
- ]
+ }
+
+ updateIndex('theme').then(themes => {
+ themes.forEach(([key, themePromise]) => themePromise.then(data => {
+ this.availableStyles.push({ key, data, name: data.name, version: 'v2' })
+ }))
+ })
+
+ updateIndex('palette').then(palettes => {
+ palettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
+ if (Array.isArray(v)) {
+ const [
+ name,
+ background,
+ foreground,
+ text,
+ link,
+ cRed = '#FF0000',
+ cGreen = '#00FF00',
+ cBlue = '#0000FF',
+ cOrange = '#E3FF00'
+ ] = v
+ this.availablePalettes.push({ key, name, background, foreground, text, link, cRed, cBlue, cGreen, cOrange })
} else {
- return acc
+ this.availablePalettes.push({ key, ...v })
}
- }, []))
- .then((themesComplete) => {
- this.availableStyles = themesComplete
- })
+ }))
+ })
if (window.IntersectionObserver) {
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
@@ -146,13 +181,16 @@ const AppearanceTab = {
},
isCustomThemeUsed () {
const { theme } = this.mergedConfig
- return theme === 'custom' || theme === null
+ return theme === 'custom'
+ },
+ isCustomStyleUsed (name) {
+ const { style } = this.mergedConfig
+ return style === 'custom'
},
...SharedComputedObject()
},
methods: {
updateFont (key, value) {
- console.log(key, value)
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
@@ -164,25 +202,76 @@ const AppearanceTab = {
}
})
},
+ importTheme () {
+ this.themeImporter.importData()
+ },
+ onImport (parsed, filename) {
+ if (filename.endsWith('.json')) {
+ this.$store.dispatch('setThemeCustom', parsed.source || parsed.theme)
+ this.$store.dispatch('applyTheme')
+ }
+
+ // this.loadTheme(parsed, 'file', forceSource)
+ },
+ onImportFailure (result) {
+ this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
+ },
+ importValidator (parsed, filename) {
+ if (filename.endsWith('.json')) {
+ const version = parsed._pleroma_theme_version
+ return version >= 1 || version <= 2
+ }
+ },
isThemeActive (key) {
const { theme } = this.mergedConfig
return key === theme
+ },
+ isStyleActive (key) {
+ const { style } = this.mergedConfig
+ return key === style
+ },
+ isPaletteActive (key) {
+ const { palette } = this.mergedConfig
+ return key === palette
+ },
+ importStyle () {
+
},
setTheme (name) {
- this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true })
+ this.$store.dispatch('setTheme', name)
+ this.$store.dispatch('applyTheme')
+ },
+ setPalette (name) {
+ this.$store.dispatch('setPalette', name)
+ this.$store.dispatch('applyTheme')
+ },
+ resetTheming (name) {
+ this.$store.dispatch('resetThemeV2')
+ this.$store.dispatch('resetThemeV3')
+ this.$store.dispatch('setStyle', 'stock')
+ this.$store.dispatch('applyTheme')
},
previewTheme (key, input) {
- const style = normalizeThemeData(input)
- const x = 2
- if (x === 1) return
- const theme2 = convertTheme2To3(style)
- const theme3 = init({
- inputRuleset: theme2,
- ultimateBackgroundColor: '#000000',
- liteMode: true,
- debug: true,
- onlyNormalState: true
- })
+ let theme3
+ if (input) {
+ const style = normalizeThemeData(input)
+ const theme2 = convertTheme2To3(style)
+ theme3 = init({
+ inputRuleset: theme2,
+ ultimateBackgroundColor: '#000000',
+ liteMode: true,
+ debug: true,
+ onlyNormalState: true
+ })
+ } else {
+ theme3 = init({
+ inputRuleset: [],
+ ultimateBackgroundColor: '#000000',
+ liteMode: true,
+ debug: true,
+ onlyNormalState: true
+ })
+ }
return getScopedVersion(
getCssRules(theme3.eager),
diff --git a/src/components/settings_modal/tabs/appearance_tab.scss b/src/components/settings_modal/tabs/appearance_tab.scss
new file mode 100644
index 000000000..77d668ecb
--- /dev/null
+++ b/src/components/settings_modal/tabs/appearance_tab.scss
@@ -0,0 +1,91 @@
+.appearance-tab {
+ .palette,
+ .theme-notice {
+ padding: 0.5em;
+ margin: 1em;
+ }
+
+ .setting-item {
+ padding-bottom: 0;
+
+ &.heading {
+ display: grid;
+ align-items: baseline;
+ grid-template-columns: 1fr auto auto auto;
+ grid-gap: 0.5em;
+
+ h2 {
+ flex: 1 0 auto;
+ }
+ }
+ }
+
+ .palettes {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 0.5em;
+ }
+
+ .palette-entry {
+ display: flex;
+ align-items: center;
+
+ > label {
+ flex: 1 0 auto;
+ }
+
+ .palette-square {
+ flex: 0 0 auto;
+ display: inline-block;
+ min-width: 1em;
+ min-height: 1em;
+ }
+ }
+
+ .column-settings {
+ display: flex;
+ justify-content: space-evenly;
+ flex-wrap: wrap;
+ }
+
+ .column-settings .size-label {
+ display: block;
+ margin-bottom: 0.5em;
+ margin-top: 0.5em;
+ }
+
+ .theme-list {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ margin: -0.5em 0;
+ height: 25em;
+ overflow-x: hidden;
+ overflow-y: auto;
+ scrollbar-gutter: stable;
+ border-radius: var(--roundness);
+ border: 1px solid var(--border);
+ padding: 0;
+
+ .theme-preview {
+ font-size: 1rem; // fix for firefox
+ width: 19rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 0.5em;
+
+ &.placeholder {
+ opacity: 0.2;
+ }
+
+ .theme-preview-container {
+ pointer-events: none;
+ zoom: 0.5;
+ border: none;
+ border-radius: var(--roundness);
+ text-align: left;
+ }
+ }
+ }
+}
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index de6eb8e70..d27f43fe1 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -1,23 +1,56 @@
-
-
+
+
{{ $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.appearance_tab_note") }}
@@ -60,7 +116,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..ecddf9d56
--- /dev/null
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.js
@@ -0,0 +1,550 @@
+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(() => {
+ const scoped = getCssRules(previewRules)
+ .map(simulatePseudoSelectors)
+ return scoped.join('\n')
+ })
+
+ // ### 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 = () => {
+ previewRules.splice(0, previewRules.length)
+ previewRules.push(...init({
+ inputRuleset: editorFriendlyToOriginal.value,
+ initialStaticVars: {
+ ...palette.value
+ },
+ ultimateBackgroundColor: '#000000',
+ rootComponentName: selectedComponentName.value,
+ editMode: true,
+ debug: true
+ }).eager)
+ }
+
+ 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..cd9d3e474
--- /dev/null
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue
@@ -0,0 +1,282 @@
+
+
+
+
+
+
{{ $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 9b946e906..4521305ea 100644
--- a/src/components/shadow_control/shadow_control.js
+++ b/src/components/shadow_control/shadow_control.js
@@ -34,9 +34,9 @@ const toModel = (object = {}) => ({
export default {
props: [
- 'modelValue', 'fallback', 'separateInset', 'noPreview'
+ 'modelValue', 'fallback', 'separateInset', 'noPreview', 'disabled'
],
- emits: ['update:modelValue'],
+ emits: ['update:modelValue', 'subShadowSelected'],
data () {
return {
selectedId: 0,
@@ -93,9 +93,13 @@ export default {
}
}
},
+ watch: {
+ selected (value) {
+ this.$emit('subShadowSelected', this.selectedId)
+ }
+ },
methods: {
updateProperty: throttle(function (prop, value) {
- console.log(prop, value)
this.cValue[this.selectedId][prop] = value
if (prop === 'inset' && value === false && this.separateInset) {
this.cValue[this.selectedId].spread = 0
diff --git a/src/components/shadow_control/shadow_control.scss b/src/components/shadow_control/shadow_control.scss
index a374184b3..dd0490235 100644
--- a/src/components/shadow_control/shadow_control.scss
+++ b/src/components/shadow_control/shadow_control.scss
@@ -71,9 +71,13 @@
&.-no-preview {
.shadow-tweak {
order: 0;
- flex: 2 0 20em;
+ flex: 2 0 8em;
max-width: 100%;
}
+
+ .input-range {
+ min-width: 5em;
+ }
}
.inset-alert {
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index 050cf820f..e1d201914 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -1,7 +1,7 @@
updateProperty(axis, value)"
/>
@@ -34,7 +34,7 @@
>