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%;
+ }
+ }
+}