From a96f5337775328f729caabf6fde3e8ea0f977c26 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 20 Nov 2025 02:07:00 +0200 Subject: [PATCH] vertical tab switcher initial implementation --- .../settings_modal_admin_content.js | 4 +- .../settings_modal_admin_content.vue | 4 +- .../settings_modal_user_content.js | 4 +- .../settings_modal_user_content.vue | 4 +- .../settings_modal/tabs/appearance_tab.js | 4 +- .../settings_modal/tabs/appearance_tab.vue | 4 +- .../settings_modal/tabs/general_tab.js | 4 +- .../settings_modal/tabs/general_tab.vue | 6 +- .../tab_switcher/vertical_tab_switcher.jsx | 166 ++++++++++++++++++ .../tab_switcher/vertical_tab_switcher.scss | 72 ++++++++ 10 files changed, 254 insertions(+), 18 deletions(-) create mode 100644 src/components/tab_switcher/vertical_tab_switcher.jsx create mode 100644 src/components/tab_switcher/vertical_tab_switcher.scss diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js index 593318ec4..fa6b7f8ad 100644 --- a/src/components/settings_modal/settings_modal_admin_content.js +++ b/src/components/settings_modal/settings_modal_admin_content.js @@ -1,4 +1,4 @@ -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' +import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx' import InstanceTab from './admin_tabs/instance_tab.vue' import LimitsTab from './admin_tabs/limits_tab.vue' @@ -31,7 +31,7 @@ library.add( const SettingsModalAdminContent = { components: { - TabSwitcher, + VerticalTabSwitcher, InstanceTab, LimitsTab, diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue index 39ef74f64..501a3acf6 100644 --- a/src/components/settings_modal/settings_modal_admin_content.vue +++ b/src/components/settings_modal/settings_modal_admin_content.vue @@ -1,5 +1,5 @@ diff --git a/src/components/settings_modal/settings_modal_user_content.js b/src/components/settings_modal/settings_modal_user_content.js index c46b477d8..ed39b30b2 100644 --- a/src/components/settings_modal/settings_modal_user_content.js +++ b/src/components/settings_modal/settings_modal_user_content.js @@ -1,4 +1,4 @@ -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' +import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx' import DataImportExportTab from './tabs/data_import_export_tab.vue' import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue' @@ -42,7 +42,7 @@ library.add( const SettingsModalContent = { components: { - TabSwitcher, + VerticalTabSwitcher, DataImportExportTab, MutesAndBlocksTab, diff --git a/src/components/settings_modal/settings_modal_user_content.vue b/src/components/settings_modal/settings_modal_user_content.vue index f9a1e99bc..6bf8dc5ee 100644 --- a/src/components/settings_modal/settings_modal_user_content.vue +++ b/src/components/settings_modal/settings_modal_user_content.vue @@ -1,5 +1,5 @@ diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js index 56e3ea10c..2978142ef 100644 --- a/src/components/settings_modal/tabs/appearance_tab.js +++ b/src/components/settings_modal/tabs/appearance_tab.js @@ -1,4 +1,4 @@ -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' +import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx' import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' @@ -97,7 +97,7 @@ const AppearanceTab = { FontControl, Preview, PaletteEditor, - TabSwitcher + VerticalTabSwitcher }, mounted () { useInterfaceStore().getThemeData() diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue index 05517af0c..9d45c3c19 100644 --- a/src/components/settings_modal/tabs/appearance_tab.vue +++ b/src/components/settings_modal/tabs/appearance_tab.vue @@ -1,5 +1,5 @@ diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index b98b7195e..68dd31941 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -1,6 +1,6 @@ import { mapState } from 'vuex' -import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' +import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx' import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' @@ -92,7 +92,7 @@ const GeneralTab = { ProfileSettingIndicator, ScopeSelector, Select, - TabSwitcher, + VerticalTabSwitcher, FontControl }, computed: { diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index ce48d923d..3c16c5cdf 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -1,10 +1,8 @@ diff --git a/src/components/tab_switcher/vertical_tab_switcher.jsx b/src/components/tab_switcher/vertical_tab_switcher.jsx new file mode 100644 index 000000000..7182c944d --- /dev/null +++ b/src/components/tab_switcher/vertical_tab_switcher.jsx @@ -0,0 +1,166 @@ +// eslint-disable-next-line no-unused +import { h, Fragment } from 'vue' +import { mapState } from 'pinia' +import { throttle } from 'lodash' +import { mapState as mapPiniaState } from 'pinia' +import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' + +import './vertical_tab_switcher.scss' +import { useInterfaceStore } from 'src/stores/interface' + +const findFirstUsable = (slots) => slots.findIndex(_ => _.props) + +export default { + name: 'VerticalTabSwitcher', + props: { + renderOnlyFocused: { + required: false, + type: Boolean, + default: false + }, + onSwitch: { + required: false, + type: Function, + default: undefined + }, + activeTab: { + required: false, + type: String, + default: undefined + }, + bodyScrollLock: { + required: false, + type: Boolean, + default: false + } + }, + data () { + return { + active: findFirstUsable(this.slots()), + resizeHandler: null + } + }, + computed: { + activeIndex () { + // In case of controlled component + if (this.activeTab) { + return this.slots().findIndex(slot => slot && slot.props && this.activeTab === slot.props.key) + } else { + return this.active + } + }, + isActive () { + return tabName => { + const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName + return this.$slots.default().findIndex(isWanted) === this.activeIndex + } + }, + ...mapPiniaState(useInterfaceStore, { + mobileLayout: store => store.layoutType === 'mobile' + }), + }, + beforeUpdate () { + const currentSlot = this.slots()[this.active] + if (!currentSlot.props) { + this.active = findFirstUsable(this.slots()) + } + }, + methods: { + clickTab (index) { + return (e) => { + e.preventDefault() + this.setTab(index) + console.log(index) + } + }, + // DO NOT put it to computed, it doesn't work (caching?) + slots () { + if (this.$slots.default()[0].type === Fragment) { + return this.$slots.default()[0].children + } + return this.$slots.default() + }, + setTab (index) { + if (typeof this.onSwitch === 'function') { + this.onSwitch.call(null, this.slots()[index].key) + } + this.active = index + this.$refs.contents.scrollTop = 0 + } + }, + render () { + const tabs = this.slots() + .map((slot, index) => { + const props = slot.props + if (!props) return + const classesTab = ['vertical-tab menu-item'] + if (this.activeIndex === index) { + classesTab.push('-active') + } + return ( + + ) + }) + + const contents = this.slots().map((slot, index) => { + const props = slot.props + if (!props) return + const active = this.activeIndex === index + const classes = [ active ? 'active' : 'hidden' ] + if (props.fullHeight) { + classes.push('full-height') + } + let delayRender = slot.props['delay-render'] + if (delayRender && active) { + slot.props['delay-render'] = false + delayRender = false + } + const renderSlot = (!delayRender && (!this.renderOnlyFocused || active)) + ? slot + : '' + + const header = ( +

+ {props.label} +

+ ) + + return ( +
+ {header} + {renderSlot} +
+ ) + }) + + return ( +
+
+ {tabs} +
+
+ {contents} +
+
+ ) + } +} diff --git a/src/components/tab_switcher/vertical_tab_switcher.scss b/src/components/tab_switcher/vertical_tab_switcher.scss new file mode 100644 index 000000000..c5454d52d --- /dev/null +++ b/src/components/tab_switcher/vertical_tab_switcher.scss @@ -0,0 +1,72 @@ +.vertical-tab-switcher { + display: flex; + flex-direction: row; + + > .tabs { + flex: 0 0 auto; + overflow: hidden auto; + flex-direction: column; + border-right: 1px solid; + border-right-color: var(--border); + min-width: 10em; + + > .menu-item { + padding: 0.5em 1em; + + .tab-icon { + vertical-align: middle; + margin-right: 0.75em; + } + } + } + + > .contents { + flex: 1 0 auto; + + .hidden { + display: none; + } + + .full-height:not(.hidden) { + height: 100%; + display: flex; + flex-direction: column; + + > *:not(.tab-content-label) { + flex: 1; + } + } + + .tab-content-label { + font-size: 1.5em; + padding: 0.5em 1em; + margin: 0; + border-bottom: 1px solid var(--border); + } + } + + > .tabs, + > .content { + transition: width 2s ease-in; + } + + &.-tabs { + > .tabs { + width: 100%; + } + + > .content { + width: 0%; + } + } + + &.-content { + > .tabs { + width: 0%; + } + + > .content { + width: 100%; + } + } +}