fix(stage-*): onboarding model selection footer overlap and list height (#1583)

---------

Co-authored-by-agent: Cursor
This commit is contained in:
Lulu
2026-04-06 17:09:04 +08:00
committed by GitHub
parent 97b3cb9d34
commit bb9a602767
5 changed files with 119 additions and 73 deletions
@@ -47,10 +47,14 @@ async function handleConfigured() {
</script>
<template>
<div class="onboarding-root" h-full w-full flex flex-col overflow-x-hidden overflow-y-auto overscroll-none drag-region :class="bgClass">
<div :class="bgClass" w="100dvw" min-h="12" w-full flex-shrink-0 select-none />
<div class="onboarding-scroll" w-full flex-1 px-10>
<div class="onboarding-content" h-full>
<!-- Same flex/min-h-0 chain as OnboardingDialog so model step grid scrolls inside the viewport (not the whole page). -->
<div
class="onboarding-root h-full min-h-0 w-full flex flex-col overflow-hidden overscroll-none drag-region"
:class="bgClass"
>
<div class="min-h-12 w-full flex-shrink-0 select-none" :class="bgClass" />
<div class="onboarding-scroll min-h-0 w-full flex flex-1 flex-col overflow-hidden px-10">
<div class="onboarding-content min-h-0 flex flex-1 flex-col overflow-hidden">
<OnboardingScreen :extra-steps="extraSteps" @skipped="handleSkipped" @configured="handleConfigured" />
</div>
</div>
@@ -73,7 +77,6 @@ async function handleConfigured() {
.onboarding-scroll {
padding-top: 8px;
padding-bottom: 20px;
overflow-y: auto;
}
</style>
@@ -28,6 +28,11 @@ interface Props {
allowCustom?: boolean
customOptionDescription?: string
expandedClass?: string
/**
* When true, root fills a flex parent (`flex-1 min-h-0`) and only the model grid scrolls;
* expand/collapse stays visible above the grid scroll area (e.g. onboarding modal).
*/
fillAvailableHeight?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@@ -44,6 +49,7 @@ const props = withDefaults(defineProps<Props>(), {
listClass: '',
allowCustom: false,
customOptionDescription: 'Custom Value',
fillAvailableHeight: false,
})
const emit = defineEmits<{
@@ -80,7 +86,6 @@ const filteredItems = computed(() => {
// Add "Use custom: ..." option if searching and custom input is allowed
if (props.allowCustom && searchQuery.value) {
const query = searchQuery.value
// Check against checks if the exact ID exists to avoid duplicates
const exactMatch = result.some(i => i.id.toLowerCase() === query.toLowerCase())
if (!exactMatch) {
result.push({
@@ -99,6 +104,41 @@ const showExpandCollapseBtn = computed(() => {
return filteredItems.value.length > props.columns
})
/**
* Centralized layout classes for every structural element.
* `fillAvailableHeight` fills a flex parent and scrolls only the grid;
* the default mode uses a max-height scroll cap or a horizontal collapsed strip.
*/
const layout = computed(() => {
const fill = props.fillAvailableHeight
const expanded = isListExpanded.value
let scrollContainer: string
if (props.listClass) {
scrollContainer = `mb-2 ${props.listClass}`
}
else if (expanded) {
scrollContainer = fill
? 'mb-2 min-h-0 flex-1 overflow-y-auto'
: 'mb-2 max-h-[calc(100dvh-22lh)] overflow-y-auto snap-y snap-proximity'
}
else {
scrollContainer = fill ? 'mb-2 flex-shrink-0' : 'mb-2'
}
return {
root: fill ? 'min-h-0 flex flex-1 flex-col' : '',
itemsArea: fill ? 'flex min-h-0 flex-1 flex-col gap-2' : 'space-y-2',
gridArea: fill ? 'flex min-h-0 flex-1 flex-col' : '',
scrollContainer,
grid: expanded
? 'grid grid-cols-1 gap-4 md:grid-cols-[repeat(var(--cols),minmax(0,1fr))]'
: 'grid gap-4 grid-flow-col auto-cols-[calc((100%-(var(--cols)-1)*1rem)/var(--cols))] overflow-x-auto scrollbar-none snap-x snap-proximity',
gridItem: expanded && !fill ? 'snap-start' : '',
expandWrapper: fill ? 'flex-shrink-0' : '',
}
})
function updateCustomValue(value: string) {
customValue.value = value
emit('update:customValue', value)
@@ -109,27 +149,37 @@ function updateCustomValue(value: string) {
<div
:class="[
'radio-card-detail-many-select',
layout.root,
isListExpanded ? props.expandedClass : '',
]"
>
<!-- Search bar -->
<div v-if="searchable" class="relative" inline-flex="~" w-full items-center>
<div
v-if="searchable"
:class="['relative inline-flex w-full flex-shrink-0 items-center']"
>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<div i-solar:magnifer-line-duotone class="text-neutral-500 dark:text-neutral-400" />
<div class="i-solar:magnifer-line-duotone text-neutral-500 dark:text-neutral-400" />
</div>
<input
v-model="searchQuery"
type="search"
class="w-full rounded-xl p-2.5 pl-10 text-sm outline-none"
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
transition="all duration-200 ease-in-out"
bg="white dark:neutral-900"
:class="[
'w-full rounded-xl border-2 border-solid border-neutral-200 bg-white p-2.5 pl-10 text-sm outline-none',
'transition-all duration-200 ease-in-out',
'focus:border-primary-100 dark:border-neutral-800 dark:bg-neutral-900 dark:focus:border-primary-400/50',
]"
:placeholder="searchPlaceholder"
>
</div>
<!-- Items list with search results info -->
<div class="mt-4 space-y-2">
<div
:class="[
'mt-4',
layout.itemsArea,
]"
>
<!-- Search results info -->
<div v-if="searchQuery" class="text-sm text-neutral-500 dark:text-neutral-400">
{{ searchResultsText.replace('{count}', filteredItems.length.toString()).replace('{total}', items.length.toString()) }}
@@ -146,70 +196,61 @@ function updateCustomValue(value: string) {
</Alert>
<!-- Items grid -->
<div class="relative">
<!-- Responsive grid container -->
<div
:class="[
'grid gap-4 mb-2',
isListExpanded
? 'grid-cols-1 md:grid-cols-[repeat(var(--cols),minmax(0,1fr))] snap-y snap-proximity'
: 'grid-flow-col auto-cols-[calc((100%-(var(--cols)-1)*1rem)/var(--cols))] overflow-x-auto scrollbar-none snap-x snap-proximity',
...(props.listClass
? (typeof props.listClass === 'string'
? [props.listClass]
: props.listClass
)
: isListExpanded
? ['max-h-[calc(100dvh-22lh)] overflow-y-auto'] // TODO: hardcoded 22lh doesn't work in many hearing/vision components
: []
),
]"
transition="all duration-200 ease-in-out"
:style="{ '--cols': props.columns }"
>
<RadioCardDetail
v-for="item in filteredItems"
:id="item.id"
:key="item.id"
v-model="modelValue"
:value="item.id"
:title="item.name"
:description="item.description"
:deprecated="item.deprecated"
:show-expand-collapse="showMore"
:expand-collapse-threshold="100"
:show-custom-input="item.customizable"
:custom-input-value="customValue"
:custom-input-placeholder="customInputPlaceholder"
name="radio-card-detail-many-select"
class="snap-start"
@update:custom-input-value="updateCustomValue($event)"
/>
<div class="relative" :class="layout.gridArea">
<!-- Scroll container wraps the grid to avoid display:grid + flex-1 overflow rendering bugs -->
<div :class="layout.scrollContainer">
<div
:class="layout.grid"
class="transition-all duration-200 ease-in-out"
:style="{ '--cols': props.columns }"
>
<RadioCardDetail
v-for="item in filteredItems"
:id="item.id"
:key="item.id"
v-model="modelValue"
:value="item.id"
:title="item.name"
:description="item.description"
:deprecated="item.deprecated"
:show-expand-collapse="showMore"
:expand-collapse-threshold="100"
:show-custom-input="item.customizable"
:custom-input-value="customValue"
:custom-input-placeholder="customInputPlaceholder"
name="radio-card-detail-many-select"
:class="layout.gridItem"
@update:custom-input-value="updateCustomValue($event)"
/>
</div>
</div>
<!-- Expand/collapse handle -->
<div
v-if="showExpandCollapseBtn"
bg="neutral-100 dark:[rgba(0,0,0,0.3)]"
rounded-xl
:class="[
'rounded-xl bg-neutral-100 dark:bg-[rgba(0,0,0,0.3)]',
isListExpanded ? 'w-full' : 'mt-4 w-full rounded-lg',
layout.expandWrapper,
]"
>
<button
w-full
flex items-center justify-center gap-2 rounded-lg py-2 transition="all duration-200 ease-in-out"
:class="[
isListExpanded ? 'bg-primary-500 hover:bg-primary-600 text-white' : 'bg-white dark:bg-neutral-900 hover:bg-neutral-100 dark:hover:bg-neutral-800',
isListExpanded ? 'absolute bottom--14' : '',
'w-full flex items-center justify-center gap-2 rounded-lg py-2',
'transition-all duration-200 ease-in-out',
isListExpanded
? 'bg-primary-500 text-white hover:bg-primary-600'
: 'bg-white hover:bg-neutral-100 dark:bg-neutral-900 dark:hover:bg-neutral-800',
]"
@click="isListExpanded = !isListExpanded"
>
<span>{{ isListExpanded ? collapseButtonText : expandButtonText }}</span>
<div
:class="isListExpanded ? 'rotate-180' : ''"
i-solar:alt-arrow-down-linear transition="transform duration-200 ease-in-out"
class="text-lg"
:class="[
'i-solar:alt-arrow-down-linear text-lg',
'transition-transform duration-200 ease-in-out',
isListExpanded ? 'rotate-180' : '',
]"
/>
</button>
</div>
@@ -32,11 +32,13 @@ onMounted(() => screenSafeArea.update())
<DialogRoot v-if="isDesktop" :open="showDialog" @update:open="value => showDialog = value">
<DialogPortal>
<DialogOverlay class="fixed inset-0 z-9999 bg-black/50 backdrop-blur-sm data-[state=closed]:animate-fadeOut data-[state=open]:animate-fadeIn" />
<DialogContent class="fixed left-1/2 top-1/2 z-9999 max-h-full max-w-2xl w-[92dvw] transform overflow-y-scroll rounded-2xl bg-white p-6 shadow-xl outline-none backdrop-blur-md scrollbar-none -translate-x-1/2 -translate-y-1/2 data-[state=closed]:animate-contentHide data-[state=open]:animate-contentShow dark:bg-neutral-900">
<DialogContent class="fixed left-1/2 top-1/2 z-9999 max-h-full max-w-2xl w-[92dvw] flex flex-col overflow-hidden rounded-2xl bg-white p-6 shadow-xl outline-none backdrop-blur-md scrollbar-none -translate-x-1/2 -translate-y-1/2 data-[state=closed]:animate-contentHide data-[state=open]:animate-contentShow dark:bg-neutral-900">
<VisuallyHidden>
<DialogTitle>Onboarding</DialogTitle>
</VisuallyHidden>
<Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" />
<div class="min-h-0 min-w-0 w-full flex flex-1 flex-col overflow-hidden">
<Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" />
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
@@ -45,7 +47,9 @@ onMounted(() => screenSafeArea.update())
<DrawerOverlay class="fixed inset-0" />
<DrawerContent class="fixed bottom-0 left-0 right-0 z-1000 mt-20 h-full max-h-[96%] flex flex-col rounded-t-2xl bg-neutral-50 px-4 pt-4 outline-none backdrop-blur-md dark:bg-neutral-900/95" :style="{ paddingBottom: `${Math.max(Number.parseFloat(screenSafeArea.bottom.value.replace('px', '')), 24)}px` }">
<DrawerHandle />
<Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" />
<div class="min-h-0 min-w-0 w-full flex flex-1 flex-col overflow-hidden">
<Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" />
</div>
</DrawerContent>
</DrawerPortal>
</DrawerRoot>
@@ -189,12 +189,13 @@ async function navigatePrevious() {
</script>
<template>
<div class="onboarding-step-container" h-full w-full>
<div class="onboarding-step-container" min-h-0 w-full flex flex-1 flex-col overflow-hidden>
<Transition :name="direction === 'next' ? 'slide-next' : 'slide-prev'" mode="out-in">
<component
:is="currentStep.component"
v-if="currentStep"
:key="currentStep.id"
class="min-h-0 min-w-0 flex flex-1 flex-col overflow-hidden"
v-bind="currentStepProps"
:on-next="requestNextStep"
:on-previous="requestPreviousStep"
@@ -29,7 +29,7 @@ const {
<template>
<div
:class="[
'h-full flex flex-col gap-4 relative overflow-y-hidden',
'min-h-0 flex min-w-0 flex-1 flex-col gap-4',
]"
>
<div sticky top-0 z-100 flex flex-shrink-0 items-center gap-2>
@@ -43,7 +43,7 @@ const {
</div>
<!-- Using the new RadioCardManySelect component -->
<div flex flex-1 flex-col gap-4>
<div class="min-h-0 flex flex-1 flex-col gap-4 overflow-hidden">
<Alert
v-if="providerModels.length === 0 && !isLoadingActiveProviderModels"
type="error"
@@ -61,6 +61,8 @@ const {
<RadioCardManySelect
v-model="activeModel"
v-model:search-query="modelSearchQuery"
class="min-h-0 flex flex-1 flex-col"
fill-available-height
:items="providerModels.toSorted((a, b) => a.id === activeModel ? -1 : b.id === activeModel ? 1 : 0)"
:searchable="true"
:allow-custom="true"
@@ -71,7 +73,6 @@ const {
:custom-input-placeholder="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.custom_model_placeholder')"
:expand-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.expand')"
:collapse-button-text="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.collapse')"
list-class="max-h-[calc(100dvh-20rem)] overflow-y-auto"
/>
<Alert v-if="activeProviderModelError" type="error">
@@ -86,11 +87,7 @@ const {
</Alert>
</div>
<div
:class="[
'absolute bottom-0 w-full',
]"
>
<div class="w-full flex-shrink-0">
<!-- Action Buttons -->
<Button
variant="primary"