after 9000 hours it finally works

This commit is contained in:
Henry Jameson 2025-11-20 12:12:14 +02:00
commit e6f025bf6e
7 changed files with 131 additions and 49 deletions

View file

@ -88,6 +88,12 @@ const SettingsModalContent = {
// Clear the state of target tab, so that next time settings is opened // Clear the state of target tab, so that next time settings is opened
// it doesn't force it. // it doesn't force it.
useInterfaceStore().clearSettingsModalTargetTab() useInterfaceStore().clearSettingsModalTargetTab()
},
nestedTooBig () {
this.$refs.tabSwitcher.showNav()
},
nestedTooSmall () {
this.$refs.tabSwitcher.hideNav()
} }
}, },
mounted () { mounted () {

View file

@ -2,24 +2,33 @@
<vertical-tab-switcher <vertical-tab-switcher
ref="tabSwitcher" ref="tabSwitcher"
class="settings_tab-switcher" class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true" :scrollable-tabs="true"
:body-scroll-lock="bodyLock" :body-scroll-lock="bodyLock"
> >
<div <div
:full-width="true"
:label="$t('settings.general')" :label="$t('settings.general')"
icon="wrench" icon="wrench"
data-tab-name="general" data-tab-name="general"
> >
<GeneralTab /> <GeneralTab
class="inner-tab -middle"
@too-small="() => nestedTooSmall()"
@too-big="() => nestedTooBig()"
/>
</div> </div>
<div <div
:full-width="true"
:label="$t('settings.appearance')" :label="$t('settings.appearance')"
icon="window-restore" icon="window-restore"
data-tab-name="appearance" data-tab-name="appearance"
:delay-render="true" :delay-render="true"
> >
<AppearanceTab /> <AppearanceTab
class="inner-tab -middle"
@too-small="() => nestedTooSmall()"
@too-big="() => nestedTooBig()"
/>
</div> </div>
<div <div
v-if="expertLevel > 0" v-if="expertLevel > 0"

View file

@ -5,6 +5,8 @@
ref="tabSwitcher" ref="tabSwitcher"
:side-tab-bar="true" :side-tab-bar="true"
:scrollable-tabs="true" :scrollable-tabs="true"
@too-small="() => console.log('small') || $emit('tooSmall')"
@too-big="() => $emit('tooBig')"
> >
<div <div
:label="$t('settings.interface')" :label="$t('settings.interface')"

View file

@ -3,6 +3,8 @@
:label="$t('settings.general')" :label="$t('settings.general')"
ref="tabSwitcher" ref="tabSwitcher"
class="settings_tab-switcher" class="settings_tab-switcher"
@too-small="() => $emit('tooSmall')"
@too-big="() => $emit('tooBig')"
> >
<div <div
:label="$t('settings.behavior')" :label="$t('settings.behavior')"

View file

@ -158,7 +158,10 @@ export default {
}) })
return ( return (
<div class="tab-switcher top-tabs"> <div
class="tab-switcher top-tabs"
ref="root"
>
<div <div
class="tabs" class="tabs"
role="tablist" role="tablist"
@ -170,6 +173,7 @@ export default {
role="tabpanel" role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock} v-body-scroll-lock={this.bodyScrollLock}
ref="content"
> >
{contents} {contents}
</div> </div>

View file

@ -34,10 +34,12 @@ export default {
default: false default: false
} }
}, },
emits: ['tooBig', 'tooSmall'],
data () { data () {
return { return {
active: findFirstUsable(this.slots()), active: findFirstUsable(this.slots()),
resizeHandler: null resizeHandler: null,
navMode: false
} }
}, },
computed: { computed: {
@ -59,6 +61,16 @@ export default {
mobileLayout: store => store.layoutType === 'mobile' mobileLayout: store => store.layoutType === 'mobile'
}), }),
}, },
created () {
this.resizeHandler = throttle(this.onResize, 200)
window.addEventListener('resize', this.resizeHandler)
},
mounted () {
this.resizeHandler()
},
unmounted () {
window.removeEventListener('resize', this.resizeHandler)
},
beforeUpdate () { beforeUpdate () {
const currentSlot = this.slots()[this.active] const currentSlot = this.slots()[this.active]
if (!currentSlot.props) { if (!currentSlot.props) {
@ -70,7 +82,20 @@ export default {
return (e) => { return (e) => {
e.preventDefault() e.preventDefault()
this.setTab(index) this.setTab(index)
console.log(index) }
},
onResize (index) {
const tabContent = this.$refs.contents?.querySelector('.tab-content-wrapper.-active .tab-content')
const tabContentWidth = tabContent.clientWidth
const rootWidth = this.$refs.root?.clientWidth
const navWidth = this.$refs.nav?.clientWidth
const contentsWidth = this.$refs.contents?.clientWidth
if (contentsWidth < tabContentWidth) {
this.$emit('tooSmall')
} else if (contentsWidth - navWidth >= tabContentWidth){
this.$emit('tooBig')
} }
}, },
// DO NOT put it to computed, it doesn't work (caching?) // DO NOT put it to computed, it doesn't work (caching?)
@ -85,7 +110,12 @@ export default {
this.onSwitch.call(null, this.slots()[index].key) this.onSwitch.call(null, this.slots()[index].key)
} }
this.active = index this.active = index
this.$refs.contents.scrollTop = 0 },
showNav () {
this.navMode = false
},
hideNav () {
this.navMode = true
} }
}, },
render () { render () {
@ -93,7 +123,7 @@ export default {
.map((slot, index) => { .map((slot, index) => {
const props = slot.props const props = slot.props
if (!props) return if (!props) return
const classesTab = ['vertical-tab menu-item'] const classesTab = ['vertical-tab', 'menu-item']
if (this.activeIndex === index) { if (this.activeIndex === index) {
classesTab.push('-active') classesTab.push('-active')
} }
@ -104,6 +134,7 @@ export default {
class={classesTab.join(' ')} class={classesTab.join(' ')}
type="button" type="button"
role="tab" role="tab"
title={props.label}
> >
{!props.icon ? '' : (<FAIcon class="tab-icon" size="1x" fixed-width icon={props.icon}/>)} {!props.icon ? '' : (<FAIcon class="tab-icon" size="1x" fixed-width icon={props.icon}/>)}
<span class="text"> <span class="text">
@ -117,9 +148,9 @@ export default {
const props = slot.props const props = slot.props
if (!props) return if (!props) return
const active = this.activeIndex === index const active = this.activeIndex === index
const classes = [ active ? 'active' : 'hidden' ] const classes = ['tab-content-wrapper', active ? '-active' : '-hidden' ]
if (props.fullHeight) { if (props.fullHeight) {
classes.push('full-height') classes.push('-full-height')
} }
let delayRender = slot.props['delay-render'] let delayRender = slot.props['delay-render']
if (delayRender && active) { if (delayRender && active) {
@ -137,17 +168,25 @@ export default {
) )
return ( return (
<div ref="contents" class={classes}> <div class={classes} >
{header} {header}
<div class={ ['tab-content', props['full-width'] ? '-full-width' : null].join(' ') } >
{renderSlot} {renderSlot}
</div> </div>
</div>
) )
}) })
const rootClasses = ['vertical-tab-switcher']
if (this.navMode) {
rootClasses.push('-nav-mode')
rootClasses.push('-nav-content')
}
return ( return (
<div ref="root" class="vertical-tab-switcher"> <div ref="root" class={ rootClasses.join(' ') }>
<div <div
class="tabs -navigation-mode -tabs" class="tabs"
role="tablist" role="tablist"
ref="nav" ref="nav"
> >
@ -157,6 +196,7 @@ export default {
role="tabpanel" role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock} v-body-scroll-lock={this.bodyScrollLock}
ref="contents"
> >
{contents} {contents}
</div> </div>

View file

@ -4,11 +4,13 @@
> .tabs { > .tabs {
flex: 0 0 auto; flex: 0 0 auto;
overflow: hidden auto;
flex-direction: column; flex-direction: column;
overflow: hidden auto;
white-space: nowrap;
text-overflow: ellipsis;
width: 15em;
border-right: 1px solid; border-right: 1px solid;
border-right-color: var(--border); border-right-color: var(--border);
min-width: 10em;
> .menu-item { > .menu-item {
padding: 0.5em 1em; padding: 0.5em 1em;
@ -21,13 +23,37 @@
} }
> .contents { > .contents {
flex: 1 0 auto; flex: 1 1 auto;
overflow-x: hidden;
.hidden { .tab-content {
align-self: center;
&:not(.-full-width) {
width: 30em;
}
&.-full-width {
align-self: stretch;
}
}
.tab-content-wrapper {
display: flex;
flex-direction: column;
.tab-content-label {
font-size: 1.5em;
padding: 0.5em 1em;
margin: 0;
border-bottom: 1px solid var(--border);
}
&.-hidden {
display: none; display: none;
} }
.full-height:not(.hidden) { &.-full-height:not(.-hidden) {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -36,37 +62,30 @@
flex: 1; flex: 1;
} }
} }
.tab-content-label {
font-size: 1.5em;
padding: 0.5em 1em;
margin: 0;
border-bottom: 1px solid var(--border);
} }
} }
> .tabs, &.-nav-mode {
> .content { &.-nav-tabs {
transition: width 2s ease-in;
}
&.-tabs {
> .tabs { > .tabs {
width: 100%; width: 100%;
} }
> .content { > .nav-content {
width: 0%; width: 0%;
} }
} }
&.-content { &.-nav-content {
> .tabs { > .tabs {
width: 0%; position: absolute;
pointer-events: none;
opacity: 0;
} }
> .content { > .nav-content {
width: 100%; width: 100%;
} }
} }
}
} }