diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 9a6964d15..20a1a591a 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -350,7 +350,7 @@ const afterStoreSetup = async ({ store, i18n }) => { 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) }) + await store.dispatch('applyTheme').catch((e) => { console.error('Error setting theme', e) }) } catch (e) { return Promise.reject(e) } 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)" />
-
- - -
+ + +
+ class="preview-window" + :class="{ '-light-grid': lightGrid }" + > +
+ TEST +
+
+ + + + {{ $t('settings.style.shadows.light_grid') }} +
- - - - {{ $t('settings.style.shadows.light_grid') }} - -
+ - 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 @@ + + + + + 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_motion.vue b/src/components/select/select_motion.vue new file mode 100644 index 000000000..be42b040d --- /dev/null +++ b/src/components/select/select_motion.vue @@ -0,0 +1,115 @@ + + + + + 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) => { @@ -144,15 +179,22 @@ const AppearanceTab = { this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) } }, + customThemeVersion () { + const { themeVersion } = this.$store.state.interface + return themeVersion + }, 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 +206,80 @@ const AppearanceTab = { } }) }, + importFile () { + this.fileImporter.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 + }, + setStyle (name) { + this.$store.dispatch('resetThemeV2') + this.$store.dispatch('setTheme', name) + this.$store.dispatch('applyTheme') + }, setTheme (name) { - this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true }) + this.$store.dispatch('resetThemeV3') + this.$store.dispatch('setTheme', name) + this.$store.dispatch('applyTheme') + }, + setPalette (name) { + this.$store.dispatch('resetThemeV2') + 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..b95ef07db --- /dev/null +++ b/src/components/settings_modal/tabs/appearance_tab.scss @@ -0,0 +1,95 @@ +.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; + + .unsupported-theme-v2 { + grid-column: 1 / span 2; + } + } + + .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..32f6f1a72 100644 --- a/src/components/settings_modal/tabs/appearance_tab.vue +++ b/src/components/settings_modal/tabs/appearance_tab.vue @@ -1,23 +1,56 @@