refactor(stage-ui): improve onboarding, scrollable, and animated (#2215)

This commit is contained in:
Neko
2026-08-04 17:51:15 +08:00
committed by GitHub
parent 47c23e6467
commit 7dc8e2c4a3
5 changed files with 47 additions and 53 deletions
@@ -53,10 +53,8 @@ async function handleConfigured() {
:class="bgClass" :class="bgClass"
> >
<div class="min-h-8 w-full flex-shrink-0 select-none drag-region" :class="bgClass" /> <div class="min-h-8 w-full flex-shrink-0 select-none drag-region" :class="bgClass" />
<div class="onboarding-scroll min-h-0 w-full flex flex-1 flex-col overflow-hidden px-10"> <div class="mx-8 mb-8 mt-2 min-h-0 flex flex-1 flex-col">
<div class="onboarding-content min-h-0 flex flex-1 flex-col overflow-hidden"> <OnboardingScreen :extra-steps="extraSteps" @skipped="handleSkipped" @configured="handleConfigured" />
<OnboardingScreen :extra-steps="extraSteps" @skipped="handleSkipped" @configured="handleConfigured" />
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -69,15 +67,6 @@ async function handleConfigured() {
.onboarding-root::-webkit-scrollbar { .onboarding-root::-webkit-scrollbar {
display: none; display: none;
} }
.onboarding-content {
padding: 8px 0 20px 0;
}
.onboarding-scroll {
padding-top: 8px;
padding-bottom: 20px;
}
</style> </style>
<route lang="yaml"> <route lang="yaml">
@@ -36,7 +36,7 @@ onMounted(() => screenSafeArea.update())
<VisuallyHidden> <VisuallyHidden>
<DialogTitle>Onboarding</DialogTitle> <DialogTitle>Onboarding</DialogTitle>
</VisuallyHidden> </VisuallyHidden>
<div class="min-h-0 min-w-0 w-full flex flex-1 flex-col overflow-hidden"> <div class="min-h-0 min-w-0 w-full flex flex-1 flex-col">
<Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" /> <Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" />
</div> </div>
</DialogContent> </DialogContent>
@@ -67,7 +67,7 @@ onMounted(() => screenSafeArea.update())
'[div&]:bg-neutral-400 [div&]:dark:bg-neutral-600', '[div&]:bg-neutral-400 [div&]:dark:bg-neutral-600',
]" ]"
/> />
<div class="min-h-0 min-w-0 w-full flex flex-1 flex-col overflow-hidden"> <div class="min-h-0 min-w-0 w-full flex flex-1 flex-col">
<Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" /> <Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" />
</div> </div>
</DrawerContent> </DrawerContent>
@@ -221,13 +221,13 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="onboarding-step-container" min-h-0 w-full flex flex-1 flex-col overflow-hidden> <div class="onboarding-step-container" min-h-0 flex flex-1 flex-col>
<Transition :name="direction === 'next' ? 'slide-next' : 'slide-prev'" mode="out-in"> <Transition :name="direction === 'next' ? 'slide-next' : 'slide-prev'" mode="out-in">
<component <component
:is="currentStep.component" :is="currentStep.component"
v-if="currentStep" v-if="currentStep"
:key="currentStep.id" :key="currentStep.id"
class="min-h-0 min-w-0 flex flex-1 flex-col overflow-hidden" class="flex flex-1 flex-col"
v-bind="currentStepProps" v-bind="currentStepProps"
:on-next="requestNextStep" :on-next="requestNextStep"
:on-previous="requestPreviousStep" :on-previous="requestPreviousStep"
@@ -237,10 +237,6 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
.onboarding-step-container {
overflow-x: hidden;
}
.slide-next-enter-active, .slide-next-enter-active,
.slide-next-leave-active, .slide-next-leave-active,
.slide-prev-enter-active, .slide-prev-enter-active,
@@ -30,7 +30,7 @@ const selectedProviderIdModel = computed({
</script> </script>
<template> <template>
<div h-full flex flex-col gap-4> <div h-full min-h-0 flex flex-col gap-4>
<div sticky top-0 z-100 flex flex-shrink-0 items-center gap-2> <div sticky top-0 z-100 flex flex-shrink-0 items-center gap-2>
<button outline-none @click="props.onPrevious"> <button outline-none @click="props.onPrevious">
<div class="i-solar:alt-arrow-left-line-duotone h-5 w-5" /> <div class="i-solar:alt-arrow-left-line-duotone h-5 w-5" />
@@ -40,7 +40,7 @@ const selectedProviderIdModel = computed({
</h2> </h2>
<div class="h-5 w-5" /> <div class="h-5 w-5" />
</div> </div>
<div class="flex-1 overflow-y-auto"> <div class="min-h-0 flex-1 overflow-y-auto">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<RadioCardDetail <RadioCardDetail
v-for="provider in props.popularProviders" v-for="provider in props.popularProviders"
@@ -56,6 +56,7 @@ const selectedProviderIdModel = computed({
</div> </div>
</div> </div>
<Button <Button
class="flex-shrink-0"
:label="t('settings.dialogs.onboarding.next')" :label="t('settings.dialogs.onboarding.next')"
:disabled="!selectedProviderIdModel" :disabled="!selectedProviderIdModel"
@click="props.onNext" @click="props.onNext"
@@ -2,7 +2,7 @@
import type { OnboardingStepNextHandler } from './types' import type { OnboardingStepNextHandler } from './types'
import { all } from '@proj-airi/i18n' import { all } from '@proj-airi/i18n'
import { Button } from '@proj-airi/ui' import { AnimatedContent, Button } from '@proj-airi/ui'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { import {
DropdownMenuContent, DropdownMenuContent,
@@ -55,9 +55,11 @@ function handleLocalSetup() {
'h-8 w-8', 'h-8 w-8',
'flex items-center justify-center', 'flex items-center justify-center',
'rounded-lg', 'rounded-lg',
'outline-none',
'text-neutral-500 transition-colors duration-200', 'text-neutral-500 transition-colors duration-200',
'hover:bg-neutral-100/80 hover:text-neutral-700', 'hover:bg-neutral-100/80 hover:text-neutral-700',
'dark:text-neutral-400 dark:hover:bg-neutral-800/80 dark:hover:text-neutral-200', 'dark:text-neutral-400 dark:hover:bg-neutral-800/80 dark:hover:text-neutral-200',
'data-[state=open]:bg-neutral-100/80 dark:data-[state=open]:bg-neutral-800/80',
]" ]"
:aria-label="t('settings.language.title')" :aria-label="t('settings.language.title')"
> >
@@ -65,29 +67,32 @@ function handleLocalSetup() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent <DropdownMenuContent
as-child
align="end" align="end"
side="bottom" side="bottom"
:side-offset="6" :side-offset="6"
:class="[ :class="[
'z-10000 min-w-36 rounded-xl border p-1 shadow-lg outline-none backdrop-blur-md', 'z-10000 min-w-36 rounded-xl border p-1 shadow-lg outline-none backdrop-blur-md',
'border-neutral-200/80 bg-neutral-100/80 text-neutral-700', 'border-neutral-100/80 bg-neutral-100/80 text-neutral-700',
'dark:border-neutral-700/60 dark:bg-neutral-800/80 dark:text-neutral-100', 'dark:border-neutral-800/60 dark:bg-neutral-800/80 dark:text-neutral-100',
]" ]"
> >
<DropdownMenuItem <AnimatedContent>
v-for="lang in languages" <DropdownMenuItem
:key="lang.value" v-for="lang in languages"
:class="[ :key="lang.value"
'flex cursor-pointer select-none items-center rounded-lg px-3 py-2', :class="[
'text-sm leading-none outline-none', 'flex cursor-pointer select-none items-center rounded-lg px-3 py-2',
'data-[highlighted]:bg-primary-50/80 dark:data-[highlighted]:bg-primary-900/40', 'text-sm leading-none outline-none',
'transition-colors duration-150 ease-in-out', 'data-[highlighted]:bg-primary-100/80 dark:data-[highlighted]:bg-primary-900/40',
lang.value === language ? 'text-primary-500 dark:text-primary-300' : '', 'transition-colors duration-150 ease-in-out',
]" lang.value === language ? 'text-primary-500 dark:text-primary-300' : '',
@select="() => language = lang.value" ]"
> @select="() => language = lang.value"
{{ lang.label }} >
</DropdownMenuItem> {{ lang.label }}
</DropdownMenuItem>
</AnimatedContent>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenuPortal> </DropdownMenuPortal>
</DropdownMenuRoot> </DropdownMenuRoot>
@@ -100,7 +105,7 @@ function handleLocalSetup() {
:duration="500" :duration="500"
:class="['mb-1', 'flex', 'justify-center', 'md:mb-4', 'md:pt-8', 'lg:pt-16']" :class="['mb-1', 'flex', 'justify-center', 'md:mb-4', 'md:pt-8', 'lg:pt-16']"
> >
<img :src="onboardingLogo" max-h="50" aspect-square h-auto w-auto object-cover> <img :src="onboardingLogo" max-h="50" aspect-square h-auto w-auto select-none object-cover>
</div> </div>
<h2 <h2
v-motion v-motion
@@ -122,25 +127,28 @@ function handleLocalSetup() {
{{ t('settings.dialogs.onboarding.description') }} {{ t('settings.dialogs.onboarding.description') }}
</p> </p>
</div> </div>
<div :class="['flex', 'flex-col', 'gap-3', 'md:flex-row']"> <div :class="['flex', 'flex-col', 'gap-3', 'md:flex-row', 'm-2']">
<Button <Button
v-motion v-motion="{
:initial="{ opacity: 0 }" initial: { opacity: 0 },
:enter="{ opacity: 1 }" enter: { opacity: 1 },
:duration="500" duration: 500,
:delay="200" delay: 200,
}"
color="primary"
variant="secondary"
:label="t('settings.dialogs.onboarding.loginAction')" :label="t('settings.dialogs.onboarding.loginAction')"
:class="['flex-1']" :class="['flex-1']"
@click="handleLogin" @click="handleLogin"
/> />
<Button <Button
v-if="props.customProviderSetupEnabled" v-if="props.customProviderSetupEnabled"
v-motion v-motion="{
:initial="{ opacity: 0 }" initial: { opacity: 0 },
:enter="{ opacity: 1 }" enter: { opacity: 1 },
:duration="500" duration: 500,
:delay="250" delay: 250,
}"
:label="t('settings.dialogs.onboarding.setupWithoutSigningIn')" :label="t('settings.dialogs.onboarding.setupWithoutSigningIn')"
:class="['flex-1']" :class="['flex-1']"
@click="handleLocalSetup" @click="handleLocalSetup"