Add a "back" button to the onboarding buttons, move the dismiss button to a little corner x button (#5296)

* Add previous button to OnboardingButtons, move dismiss to popover corner

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Clean up diagnostics

I am thoroughly enjoying nvim now

* Amend "click through" test to also click back

* fmt

* Set this test back to fixme, that work should be its own PR

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2025-02-07 11:30:36 -05:00
committed by GitHub
parent b82eec85fd
commit f6e975db84
14 changed files with 146 additions and 187 deletions

View File

@ -27,7 +27,7 @@ test.describe('Onboarding tests', () => {
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
@ -68,7 +68,7 @@ test.describe('Onboarding tests', () => {
},
cleanProjectDir: true,
},
async ({ page, homePage }, testInfo) => {
async ({ page }) => {
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
@ -154,7 +154,7 @@ test.describe('Onboarding tests', () => {
)
test(
'Click through each onboarding step',
'Click through each onboarding step and back',
{
appSettings: {
app: {
@ -187,15 +187,21 @@ test.describe('Onboarding tests', () => {
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
}
// Finish the onboarding
await nextButton.hover()
await nextButton.click()
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
}
// Dismiss the onboarding
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
@ -269,7 +275,7 @@ test.describe('Onboarding tests', () => {
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
async ({ page, homePage }) => {
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
@ -336,10 +342,10 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene()
// Test that the text in this step is correct
const avatarLocator = await page
const avatarLocator = page
.getByTestId('user-sidebar-toggle')
.locator('img')
const onboardingOverlayLocator = await page
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
@ -447,7 +453,7 @@ test.fixme(
},
cleanProjectDir: true,
},
async ({ context, page, homePage }, testInfo) => {
async ({ context, page }) => {
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
@ -486,10 +492,6 @@ test.fixme(
})
await test.step('Navigate into project', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await expect(
page.getByRole('heading', { name: 'Your Projects' })
).toBeVisible()

View File

@ -26,7 +26,7 @@ export default function Units() {
<div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none">
<div
className={
'pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<SettingsSection
@ -72,9 +72,7 @@ export default function Units() {
</SettingsSection>
<OnboardingButtons
currentSlug={onboardingPaths.CAMERA}
dismiss={dismiss}
next={next}
nextText="Next: Streaming"
dismissClassName="right-auto left-full"
/>
</div>
</div>

View File

@ -1,19 +1,17 @@
import usePlatform from 'hooks/usePlatform'
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.'
import { OnboardingButtons, kbdClasses } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from 'components/CommandBar/CommandBar'
export default function CmdK() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.USER_MENU)
const platformName = usePlatform()
return (
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
<div
className={
'pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<h2 className="text-2xl font-bold">Command Bar</h2>
@ -38,12 +36,7 @@ export default function CmdK() {
. You can control settings, authentication, and file management from
the command bar, as well as a growing number of modeling commands.
</p>
<OnboardingButtons
currentSlug={onboardingPaths.COMMAND_K}
dismiss={dismiss}
next={next}
nextText="Next: User Menu"
/>
<OnboardingButtons currentSlug={onboardingPaths.COMMAND_K} />
</div>
</div>
)

View File

@ -1,22 +1,14 @@
import {
kbdClasses,
OnboardingButtons,
useDemoCode,
useDismiss,
useNextClick,
} from '.'
import { kbdClasses, OnboardingButtons, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
export default function OnboardingCodeEditor() {
useDemoCode()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PARAMETRIC_MODELING)
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto">
@ -73,12 +65,7 @@ export default function OnboardingCodeEditor() {
pressing <kbd className={kbdClasses}>Shift + C</kbd>.
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.EDITOR}
dismiss={dismiss}
next={next}
nextText="Next: Parametric Modeling"
/>
<OnboardingButtons currentSlug={onboardingPaths.EDITOR} />
</div>
</div>
)

View File

@ -1,16 +1,13 @@
import { APP_NAME } from 'lib/constants'
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
export default function Export() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.SKETCHING)
return (
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1">
@ -52,12 +49,7 @@ export default function Export() {
!
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.EXPORT}
next={next}
dismiss={dismiss}
nextText="Next: Sketching"
/>
<OnboardingButtons currentSlug={onboardingPaths.EXPORT} />
</div>
</div>
)

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss } from '.'
import { OnboardingButtons, useDemoCode } from '.'
import { useEffect } from 'react'
import { useModelingContext } from 'hooks/useModelingContext'
import { APP_NAME } from 'lib/constants'
@ -7,7 +7,6 @@ import { sceneInfra } from 'lib/singletons'
export default function FutureWork() {
const { send } = useModelingContext()
const dismiss = useDismiss()
// Reset the code, the camera, and the modeling state
useDemoCode()
@ -19,7 +18,7 @@ export default function FutureWork() {
return (
<div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50">
<div className="max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
<div className="relative max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
<h1 className="text-2xl font-bold">Future Work</h1>
<p className="my-4">
We have curves, cuts, multi-profile sketch mode, and many more CAD
@ -59,9 +58,6 @@ export default function FutureWork() {
<OnboardingButtons
currentSlug={onboardingPaths.FUTURE_WORK}
className="mt-6"
dismiss={dismiss}
next={dismiss}
nextText="Finish"
/>
</div>
</div>

View File

@ -1,23 +1,15 @@
import {
OnboardingButtons,
kbdClasses,
useDemoCode,
useDismiss,
useNextClick,
} from '.'
import { OnboardingButtons, kbdClasses, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { bracketWidthConstantLine } from 'lib/exampleKcl'
export default function OnboardingInteractiveNumbers() {
useDemoCode()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.COMMAND_K)
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto mb-6">
@ -88,12 +80,7 @@ export default function OnboardingInteractiveNumbers() {
your ideas for how to make it better.
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.INTERACTIVE_NUMBERS}
dismiss={dismiss}
next={next}
nextText="Next: Command Bar"
/>
<OnboardingButtons currentSlug={onboardingPaths.INTERACTIVE_NUMBERS} />
</div>
</div>
)

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme'
@ -8,13 +8,12 @@ import { isDesktop } from 'lib/isDesktop'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons'
import { APP_NAME } from 'lib/constants'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { useFileContext } from 'hooks/useFileContext'
import { useLspContext } from 'components/LspProvider'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
/**
* Show either a welcome screen or a warning screen
@ -39,7 +38,7 @@ interface OnboardingResetWarningProps {
function OnboardingResetWarning(props: OnboardingResetWarningProps) {
return (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
{!isDesktop() ? (
<OnboardingWarningWeb {...props} />
) : (
@ -52,7 +51,6 @@ function OnboardingResetWarning(props: OnboardingResetWarningProps) {
function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
const navigate = useNavigate()
const dismiss = useDismiss()
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { context: fileContext } = useFileContext()
const { onProjectClose, onProjectOpen } = useLspContext()
@ -81,17 +79,28 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
</section>
<OnboardingButtons
className="mt-6"
dismiss={dismiss}
next={toSync(onAccept, reportRejection)}
nextText="Make a new project"
onNextOverride={() => {
onAccept().catch(reportRejection)
}}
/>
</>
)
}
function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
const dismiss = useDismiss()
useEffect(() => {
async function beforeNavigate() {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
await kclManager.executeCode(true)
props.setShouldShowWarning(false)
}
return () => {
beforeNavigate().catch(reportRejection)
}
}, [])
return (
<>
<h1 className="text-3xl font-bold text-warn-80 dark:text-warn-10">
@ -101,19 +110,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
We see you have some of your own code written in this project. Please
save it somewhere else before continuing the onboarding.
</p>
<OnboardingButtons
className="mt-6"
dismiss={dismiss}
next={toSync(async () => {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
await kclManager.executeCode(true)
props.setShouldShowWarning(false)
}, reportRejection)}
nextText="Overwrite code and continue"
/>
<OnboardingButtons className="mt-6" />
</>
)
}
@ -136,12 +133,10 @@ function OnboardingIntroductionInner() {
(theme.current === Themes.System && getSystemTheme() === Themes.Light)
? '-dark'
: ''
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.CAMERA)
return (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
<img
src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
@ -192,9 +187,6 @@ function OnboardingIntroductionInner() {
<OnboardingButtons
currentSlug={onboardingPaths.INDEX}
className="mt-6"
dismiss={dismiss}
next={next}
nextText="Mouse Controls"
/>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { Themes, getSystemTheme } from 'lib/theme'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -21,14 +21,12 @@ export default function OnboardingParametricModeling() {
(theme === Themes.System && getSystemTheme() === Themes.Light)
? '-dark'
: ''
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.INTERACTIVE_NUMBERS)
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto mb-6">
@ -77,12 +75,7 @@ export default function OnboardingParametricModeling() {
</figcaption>
</figure>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.PARAMETRIC_MODELING}
dismiss={dismiss}
next={next}
nextText="Next: Interactive Numbers"
/>
<OnboardingButtons currentSlug={onboardingPaths.PARAMETRIC_MODELING} />
</div>
</div>
)

View File

@ -1,17 +1,15 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { isDesktop } from 'lib/isDesktop'
export default function ProjectMenu() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.EXPORT)
const onDesktop = isDesktop()
return (
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1">
@ -57,12 +55,7 @@ export default function ProjectMenu() {
</>
)}
</section>
<OnboardingButtons
currentSlug={onboardingPaths.PROJECT_MENU}
next={next}
dismiss={dismiss}
nextText="Next: Export"
/>
<OnboardingButtons currentSlug={onboardingPaths.PROJECT_MENU} />
</div>
</div>
)

View File

@ -1,12 +1,9 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect } from 'react'
import { codeManager, kclManager } from 'lib/singletons'
export default function Sketching() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.FUTURE_WORK)
useEffect(() => {
async function clearEditor() {
// We do want to update both the state and editor here.
@ -22,7 +19,7 @@ export default function Sketching() {
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<h1 className="text-2xl font-bold">Sketching</h1>
@ -45,9 +42,6 @@ export default function Sketching() {
<OnboardingButtons
currentSlug={onboardingPaths.SKETCHING}
className="mt-6"
next={next}
dismiss={dismiss}
nextText="Next: Future Work"
/>
</div>
</div>

View File

@ -1,15 +1,12 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
export default function Streaming() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.EDITOR)
return (
<div className="fixed grid justify-start items-center inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto">
@ -44,9 +41,7 @@ export default function Streaming() {
</section>
<OnboardingButtons
currentSlug={onboardingPaths.STREAMING}
dismiss={dismiss}
next={next}
nextText="Next: Code Editor"
dismissClassName="right-auto left-full"
/>
</div>
</div>

View File

@ -1,12 +1,10 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect, useState } from 'react'
import { useUser } from 'machines/appMachine'
export default function UserMenu() {
const user = useUser()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PROJECT_MENU)
const [avatarErrored, setAvatarErrored] = useState(false)
const errorOrNoImage = !user?.image || avatarErrored
@ -32,7 +30,7 @@ export default function UserMenu() {
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1">
@ -48,12 +46,7 @@ export default function UserMenu() {
only apply to the current project.
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.USER_MENU}
dismiss={dismiss}
next={next}
nextText="Next: Project Menu"
/>
<OnboardingButtons currentSlug={onboardingPaths.USER_MENU} />
</div>
</div>
)

View File

@ -26,6 +26,9 @@ import { reportRejection } from 'lib/trap'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { commandBarActor } from 'machines/commandBarMachine'
export const kbdClasses =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
@ -163,58 +166,99 @@ export function useStepNumber(
: onboardingRoutes.findIndex(
(r) => r.path === makeUrlPathRelative(slug)
) + 1
: undefined
: 1
}
export function OnboardingButtons({
next,
nextText,
dismiss,
currentSlug,
className,
dismissClassName,
onNextOverride,
...props
}: {
next: () => void
nextText?: string
dismiss: () => void
currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
className?: string
dismissClassName?: string
onNextOverride?: () => void
} & React.HTMLAttributes<HTMLDivElement>) {
const dismiss = useDismiss()
const stepNumber = useStepNumber(currentSlug)
const previousStep =
!stepNumber || stepNumber === 0 ? null : onboardingRoutes[stepNumber - 2]
const goToPrevious = useNextClick(
onboardingPaths.INDEX + (previousStep?.path ?? '')
)
const nextStep =
!stepNumber || stepNumber === onboardingRoutes.length
? null
: onboardingRoutes[stepNumber]
const goToNext = useNextClick(onboardingPaths.INDEX + (nextStep?.path ?? ''))
return (
<div
className={'flex items-center justify-between ' + (className ?? '')}
{...props}
>
<ActionButton
Element="button"
<>
<button
onClick={dismiss}
iconStart={{
icon: 'close',
className: 'text-chalkboard-10',
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
}}
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
className={
'group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ' +
dismissClassName
}
data-testid="onboarding-dismiss"
>
Dismiss
</ActionButton>
{stepNumber !== undefined && (
<p className="font-mono text-xs text-center m-0">
{stepNumber} / {onboardingRoutes.length}
</p>
)}
<ActionButton
autoFocus
Element="button"
onClick={next}
iconStart={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }}
className="dark:hover:bg-chalkboard-80/50"
data-testid="onboarding-next"
<CustomIcon
name="close"
className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
/>
<Tooltip position="bottom" delay={500}>
Dismiss <kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
</Tooltip>
</button>
<div
className={'flex items-center justify-between ' + (className ?? '')}
{...props}
>
{nextText ?? 'Next'}
</ActionButton>
</div>
<ActionButton
Element="button"
onClick={() =>
previousStep?.path || previousStep?.index
? goToPrevious()
: dismiss()
}
iconStart={{
icon: previousStep ? 'arrowLeft' : 'close',
className: 'text-chalkboard-10',
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
}}
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
data-testid="onboarding-prev"
>
{previousStep ? `Back` : 'Dismiss'}
</ActionButton>
{stepNumber !== undefined && (
<p className="font-mono text-xs text-center m-0">
{stepNumber} / {onboardingRoutes.length}
</p>
)}
<ActionButton
autoFocus
Element="button"
onClick={() => {
if (nextStep?.path) {
onNextOverride ? onNextOverride() : goToNext()
} else {
dismiss()
}
}}
iconStart={{
icon: nextStep ? 'arrowRight' : 'checkmark',
bgClassName: 'dark:bg-chalkboard-80',
}}
className="dark:hover:bg-chalkboard-80/50"
data-testid="onboarding-next"
>
{nextStep ? `Next` : 'Finish'}
</ActionButton>
</div>
</>
)
}