refactor: TabbedModal to use NewModal and DI (#5612)

* refactor: tabbed modal to use NewModal

* refactor: use DI for instance settings modal instead of passing down props

* pnpm prepr
This commit is contained in:
Truman Gao
2026-03-19 09:53:53 -07:00
committed by GitHub
parent 93c81631a9
commit 2128fa7ade
11 changed files with 585 additions and 239 deletions

View File

@@ -1,24 +1,40 @@
<script lang="ts"></script>
<script setup lang="ts">
import { type Component, computed, nextTick, ref } from 'vue'
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
import { useScrollIndicator } from '../../composables/scroll-indicator'
const { formatMessage } = useVIntl()
export type Tab<Props> = {
import NewModal from './NewModal.vue'
export interface Tab {
name: MessageDescriptor
icon: Component
content: Component<Props>
props?: Props
content: Component
badge?: MessageDescriptor
shown?: boolean
}
const props = defineProps<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tabs: Tab<any>[]
}>()
const { formatMessage } = useVIntl()
const props = withDefaults(
defineProps<{
tabs: Tab[]
header?: string
maxWidth?: string
width?: string
closable?: boolean
onHide?: () => void
onShow?: () => void
}>(),
{
header: undefined,
maxWidth: undefined,
width: undefined,
closable: true,
onHide: undefined,
onShow: undefined,
},
)
const visibleTabs = computed(() => props.tabs.filter((tab) => tab.shown !== false))
@@ -28,77 +44,98 @@ const scrollContainer = ref<HTMLElement | null>(null)
const { showTopFade, showBottomFade, checkScrollState, forceCheck } =
useScrollIndicator(scrollContainer)
const modal = ref<InstanceType<typeof NewModal> | null>(null)
function setTab(index: number) {
selectedTab.value = index
nextTick(() => forceCheck())
}
defineExpose({ selectedTab, setTab })
function show(event?: MouseEvent) {
modal.value?.show(event)
}
function hide() {
modal.value?.hide()
}
defineExpose({ show, hide, selectedTab, setTab })
</script>
<template>
<div class="grid grid-cols-[auto_1fr]">
<div
class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider min-w-[200px]"
>
<button
v-for="(tab, index) in visibleTabs"
:key="index"
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all ${selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
@click="() => setTab(index)"
>
<component :is="tab.icon" class="w-4 h-4 flex-shrink-0" />
<span>{{ formatMessage(tab.name) }}</span>
<span
v-if="tab.badge"
class="rounded-full px-1.5 py-0.5 text-xs font-bold bg-brand-highlight text-brand-green"
>
{{ formatMessage(tab.badge) }}
</span>
</button>
<slot name="footer" />
</div>
<div class="relative">
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-24"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-24"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showTopFade"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-24 bg-gradient-to-b from-bg-raised to-transparent"
/>
</Transition>
<NewModal
ref="modal"
:header="header"
:max-width="maxWidth"
:width="width"
:closable="closable"
:on-hide="onHide"
:on-show="onShow"
no-padding
>
<template v-if="$slots.title" #title>
<slot name="title" />
</template>
<div class="grid grid-cols-[auto_1fr] p-4">
<div
ref="scrollContainer"
class="min-w-[400px] h-[500px] overflow-y-auto px-4"
@scroll="checkScrollState"
class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider min-w-[200px]"
>
<Suspense>
<component
:is="visibleTabs[selectedTab].content"
v-bind="visibleTabs[selectedTab].props ?? {}"
/>
</Suspense>
</div>
<button
v-for="(tab, index) in visibleTabs"
:key="index"
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all ${selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
@click="() => setTab(index)"
>
<component :is="tab.icon" class="w-4 h-4 flex-shrink-0" />
<span>{{ formatMessage(tab.name) }}</span>
<span
v-if="tab.badge"
class="rounded-full px-1.5 py-0.5 text-xs font-bold bg-brand-highlight text-brand-green"
>
{{ formatMessage(tab.badge) }}
</span>
</button>
<slot name="footer" />
</div>
<div class="relative">
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-10"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-10"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showTopFade"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-10 bg-gradient-to-b from-bg-raised to-transparent"
/>
</Transition>
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-24"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-24"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showBottomFade"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-24 bg-gradient-to-t from-bg-raised to-transparent"
/>
</Transition>
ref="scrollContainer"
class="min-w-[400px] h-[500px] overflow-y-auto px-4"
@scroll="checkScrollState"
>
<Suspense>
<component :is="visibleTabs[selectedTab].content" />
</Suspense>
</div>
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-10"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-10"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showBottomFade"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-10 bg-gradient-to-t from-bg-raised to-transparent"
/>
</Transition>
</div>
</div>
</div>
</NewModal>
</template>