fix(stage-*): onboarding model selection footer overlap and list height (#1583)
--------- Co-authored-by-agent: Cursor
This commit is contained in:
@@ -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"
|
||||
|
||||
+5
-8
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user