@ui-registries/guide

TourFrame

Frame of the modal guide runs: step content, counter and Skip / Previous / Next / Finish, with CSS entry animations driven by data-side.

Installation

Terminal
pnpm dlx shadcn@latest add https://ui-registry.com/r/guide/tour-frame

Source

The file you get once installed. It is yours: edit the markup, the labels and the styles.

tour-frame.tsx
"use client";
import type { GuideRun } from "@ui-registries/guide";import { GuideFrame, type WithGuideContext } from "@ui-registries/guide/react";import type { ComponentProps, ReactNode } from "react";import { Button } from "@/components/ui/button";import { Card, CardContent, CardFooter } from "@/components/ui/card";import { cn } from "@/lib/utils";
export interface TourFrameLabels {  finish: ReactNode;  next: ReactNode;  previous: ReactNode;  skip: ReactNode;}
const DEFAULT_LABELS: TourFrameLabels = {  skip: "Skip",  previous: "Previous",  next: "Next",  finish: "Finish",};
const defaultStepLabel = ({ current, total }: StepLabelMeta) =>  `Step ${current} of ${total}`;
const isModal = (run: GuideRun) => run.guide.mode === "modal";
export interface StepLabelMeta {  current: number;  total: number;}
export interface TourFrameProps {  className?: string;  /** Button labels; English by default. */  labels?: Partial<TourFrameLabels>;  /** "Step 1 of 3" by default; `null` hides it. */  stepLabel?: ((meta: StepLabelMeta) => ReactNode) | null;}
/** * Frame of the modal runs: the step's content, a counter and the * Skip / Previous / Next / Finish controls. Mount once, inside `<GuideRoot>`, * next to `<GuideSpotlight />`. */export function TourFrame({  className,  labels,  stepLabel = defaultStepLabel,}: TourFrameProps) {  const text = { ...DEFAULT_LABELS, ...labels };  return (    <GuideFrame select={isModal}>      {(frame) => (        <Card          {...frame.floatingProps}          className={cn(            "fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-80 max-w-[calc(100vw-2rem)] animate-in pb-0 shadow-lg outline-none duration-200 motion-reduce:animate-none",            className          )}          data-slot="tour-frame"          size="sm"        >          <CardContent className="flex flex-col gap-2">            {stepLabel ? (              <p                className="text-muted-foreground text-xs"                data-slot="tour-frame-step"              >                {stepLabel({ current: frame.index + 1, total: frame.total })}              </p>            ) : null}            <frame.Content />          </CardContent>          <CardFooter className="gap-2">            {frame.dismissible ? (              <Button                onClick={() => frame.end("dismissed")}                size="sm"                variant="ghost"              >                {text.skip}              </Button>            ) : null}            <div className="ml-auto flex gap-2">              {frame.isFirst ? null : (                <Button onClick={frame.prev} size="sm" variant="outline">                  {text.previous}                </Button>              )}              {frame.isLast ? (                <Button onClick={() => frame.end("completed")} size="sm">                  {text.finish}                </Button>              ) : (                <Button onClick={frame.next} size="sm">                  {text.next}                </Button>              )}            </div>          </CardFooter>        </Card>      )}    </GuideFrame>  );}
type TourTextProps<T extends "h2" | "p"> = WithGuideContext<  Omit<ComponentProps<T>, "id">>;
/** Title of a step's content, labelling the Frame (`aria-labelledby`). */export function TourTitle({ ctx, className, ...props }: TourTextProps<"h2">) {  return (    <h2      className={cn("font-medium text-base leading-snug", className)}      data-slot="tour-title"      id={ctx.ids.title}      {...props}    />  );}
/** Description of a step's content, describing the Frame (`aria-describedby`). */export function TourDescription({  ctx,  className,  ...props}: TourTextProps<"p">) {  return (    <p      className={cn("text-muted-foreground text-sm", className)}      data-slot="tour-description"      id={ctx.ids.description}      {...props}    />  );}