diff --git a/bun.lockb b/bun.lockb index cbbdcf3caa..ad4dc4e814 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/react/.storybook/main.css b/packages/react/.storybook/main.css index 63365cacef..f59f08b588 100644 --- a/packages/react/.storybook/main.css +++ b/packages/react/.storybook/main.css @@ -33,4 +33,5 @@ @import url("./styles/toast.css"); @import url("./styles/toggle-group.css"); @import url("./styles/tooltip.css"); +@import url("./styles/tour.css"); @import url("./styles/tree-view.css"); diff --git a/packages/react/.storybook/styles/tour.css b/packages/react/.storybook/styles/tour.css new file mode 100644 index 0000000000..d2ef3bac18 --- /dev/null +++ b/packages/react/.storybook/styles/tour.css @@ -0,0 +1,130 @@ +[data-scope="tour"][data-part="positioner"][data-type="floating"] { + position: absolute; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="bottom"] { + bottom: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="top"] { + top: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="end"] { + inset-inline-end: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="start"] { + inset-inline-start: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="dialog"] { + width: 100%; + position: fixed; + inset: 0; + margin: auto; + display: flex; + align-items: center; + justify-content: center; +} + +[data-scope="tour"][data-part="content"] { + --arrow-background: white; + --arrow-size: 10px; + background: white; + padding: 24px; + position: relative; + border-radius: 4px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + width: 300px; +} + +[data-scope="tour"][data-part="content"][data-type="dialog"] { + width: 500px; + background: lightblue; +} + +[data-scope="tour"][data-part="content"][data-type="floating"] { + width: 500px; + background: rgb(15, 39, 136); + color: white; +} + +[data-scope="tour"][data-part="arrow"] { + --arrow-background: white; + --arrow-shadow-color: #ebebeb; + box-shadow: var(--box-shadow); +} + +[data-scope="tour"][data-part="title"] { + font-weight: 600; +} + +[data-scope="tour"][data-part="description"] { + margin-bottom: 20px; +} + +[data-scope="tour"][data-part="progress-text"] { + margin-bottom: 20px; + opacity: 0.72; +} + +[data-scope="tour"][data-part="backdrop"] { + background-color: rgba(0, 0, 0, 0.5); +} + +[data-scope="tour"][data-part="spotlight"] { + border: 3px solid pink; +} + +[data-scope="tour"][data-part="close-trigger"] { + font-family: inherit; + height: 25px; + width: 25px; + display: inline-flex; + align-items: center; + justify-content: center; + position: absolute; + top: 10px; + right: 10px; +} + +.tour.button__group { + display: flex; + align-items: flex-end; + gap: 10px; +} + +.tour .steps__container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 50vh; +} + +.tour .overflow__container { + width: 500px; + height: 400px; + max-height: 200px; + overflow: auto; + border: 2px solid teal; + position: relative; +} + +.tour .overflow__container::before { + content: "Overflow"; + display: block; + position: sticky; + background-color: teal; + color: white; + padding: 2px 4px 3px; + top: 0px; +} + +.tour .overflow__container .h-200px { + height: 200px; +} + +.tour .overflow__container .h-100px { + height: 100px; +} diff --git a/packages/react/package.json b/packages/react/package.json index 42fe3e73fc..356a8a48b1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -213,6 +213,7 @@ "@zag-js/toast": "0.81.0", "@zag-js/toggle-group": "0.81.0", "@zag-js/tooltip": "0.81.0", + "@zag-js/tour": "0.81.0", "@zag-js/tree-view": "0.81.0", "@zag-js/types": "0.81.0" }, diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 68f8cbd4ee..db6b5dac19 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -44,4 +44,5 @@ export * from './toast' export * from './toggle' export * from './toggle-group' export * from './tooltip' +export * from './tour' export * from './tree-view' diff --git a/packages/react/src/components/tour/examples/basic.tsx b/packages/react/src/components/tour/examples/basic.tsx new file mode 100644 index 0000000000..d17fbe8645 --- /dev/null +++ b/packages/react/src/components/tour/examples/basic.tsx @@ -0,0 +1,29 @@ +import { Frame } from '@ark-ui/react/frame' +import { DemoTour } from './tour' + +export const Basic = () => { + return ( +
+ +
+
+

Step 1

+
+
+

Step 2

+
+
+ +

Iframe Content

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. +

+ +

Step 3

+

Step 4

+
+
+
+ ) +} diff --git a/packages/react/src/components/tour/examples/steps.tsx b/packages/react/src/components/tour/examples/steps.tsx new file mode 100644 index 0000000000..b3005969e7 --- /dev/null +++ b/packages/react/src/components/tour/examples/steps.tsx @@ -0,0 +1,90 @@ +import type { TourStepDetails } from '@ark-ui/react/tour' + +export const steps: TourStepDetails[] = [ + { + type: 'dialog', + id: 'step-0', + title: 'Centered tour (no target)', + description: 'This is the center of the world. Ready to start the tour?', + actions: [{ label: 'Next', action: 'next' }], + }, + { + type: 'tooltip', + id: 'step-1', + title: 'Step 1. Welcome', + description: 'To the new world', + target: () => document.querySelector('#step-1'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + effect({ show, update }) { + const abort = new AbortController() + + fetch('https://api.github.com/users/octocat', { signal: abort.signal }) + .then((res) => res.json()) + .then((data) => { + update({ title: data.name }) + show() + }) + + return () => { + abort.abort() + } + }, + }, + { + type: 'tooltip', + id: 'step-2', + title: 'Step 2. Inside a scrollable container', + description: 'Using scrollIntoView(...) rocks!', + target: () => document.querySelector('#step-2'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-2a', + title: 'Step 2a. Inside an Iframe container', + description: 'It calculates the offset rect correctly. Thanks to floating UI!', + target: () => { + const [frameEl] = Array.from(frames) + return frameEl?.document.querySelector('#step-2a') + }, + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-3', + title: 'Step 3. Normal scrolling', + description: 'The new world is a great place', + target: () => document.querySelector('#step-3'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-4', + title: 'Step 4. Close to bottom', + description: 'So nice to see the scrolling works!', + target: () => document.querySelector('#step-4'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'dialog', + id: 'step-5', + title: "You're all sorted! (no target)", + description: 'Thanks for trying out the tour. Enjoy the app!', + actions: [{ label: 'Finish', action: 'dismiss' }], + }, +] diff --git a/packages/react/src/components/tour/examples/tour.tsx b/packages/react/src/components/tour/examples/tour.tsx new file mode 100644 index 0000000000..f52139781b --- /dev/null +++ b/packages/react/src/components/tour/examples/tour.tsx @@ -0,0 +1,41 @@ +import { Tour, useTour } from '@ark-ui/react/tour' +import { XIcon } from 'lucide-react' +import { useEffect } from 'react' +import { steps } from './steps' + +export const DemoTour = () => { + const tour = useTour({ steps }) + + // Start the tour when the component mounts + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + tour.start() + }, []) + + return ( + + + + + + + + + + + + + + + + + {(actions) => + actions.map((action) => ) + } + + + + + + ) +} diff --git a/packages/react/src/components/tour/index.ts b/packages/react/src/components/tour/index.ts new file mode 100644 index 0000000000..6f43e102e1 --- /dev/null +++ b/packages/react/src/components/tour/index.ts @@ -0,0 +1,56 @@ +export type { StepDetails as TourStepDetails } from '@zag-js/tour' +export { + TourActionTrigger, + type TourActionTriggerBaseProps, + type TourActionTriggerProps, +} from './tour-action-trigger' +export { + TourActions, + type TourActionsProps, +} from './tour-actions' +export { TourArrow, type TourArrowBaseProps, type TourArrowProps } from './tour-arrow' +export { + TourArrowTip, + type TourArrowTipBaseProps, + type TourArrowTipProps, +} from './tour-arrow-tip' +export { TourBackdrop, type TourBackdropBaseProps, type TourBackdropProps } from './tour-backdrop' +export { + TourCloseTrigger, + type TourCloseTriggerBaseProps, + type TourCloseTriggerProps, +} from './tour-close-trigger' +export { TourContent, type TourContentBaseProps, type TourContentProps } from './tour-content' +export { TourContext, type TourContextProps } from './tour-context' +export { + TourControl, + type TourControlBaseProps, + type TourControlProps, +} from './tour-control' +export { + TourDescription, + type TourDescriptionBaseProps, + type TourDescriptionProps, +} from './tour-description' +export { + TourPositioner, + type TourPositionerBaseProps, + type TourPositionerProps, +} from './tour-positioner' +export { + TourProgressText, + type TourProgressTextBaseProps, + type TourProgressTextProps, +} from './tour-progress-text' +export { TourRoot, type TourRootBaseProps, type TourRootProps } from './tour-root' +export { + TourSpotlight, + type TourSpotlightBaseProps, + type TourSpotlightProps, +} from './tour-spotlight' +export { TourTitle, type TourTitleBaseProps, type TourTitleProps } from './tour-title' +export { tourAnatomy } from './tour.anatomy' +export { useTour, type UseTourProps, type UseTourReturn } from './use-tour' +export { useTourContext, type UseTourContext } from './use-tour-context' + +export * as Tour from './tour' diff --git a/packages/react/src/components/tour/tour-action-trigger.tsx b/packages/react/src/components/tour/tour-action-trigger.tsx new file mode 100644 index 0000000000..47cb8fcd9e --- /dev/null +++ b/packages/react/src/components/tour/tour-action-trigger.tsx @@ -0,0 +1,27 @@ +import { mergeProps } from '@zag-js/react' +import type { StepActionTriggerProps } from '@zag-js/tour' +import { forwardRef } from 'react' +import { createSplitProps } from '../../utils/create-split-props' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourActionTriggerBaseProps extends PolymorphicProps, StepActionTriggerProps {} +export interface TourActionTriggerProps extends HTMLProps<'button'>, TourActionTriggerBaseProps {} + +export const TourActionTrigger = forwardRef( + (props, ref) => { + const [actionTriggerProps, localProps] = createSplitProps()(props, [ + 'action', + ]) + const tour = useTourContext() + const mergedProps = mergeProps(tour.getActionTriggerProps(actionTriggerProps), localProps) + + return ( + + {mergedProps.children || actionTriggerProps.action.label} + + ) + }, +) + +TourActionTrigger.displayName = 'TourActionTrigger' diff --git a/packages/react/src/components/tour/tour-actions.tsx b/packages/react/src/components/tour/tour-actions.tsx new file mode 100644 index 0000000000..d4e8883c3d --- /dev/null +++ b/packages/react/src/components/tour/tour-actions.tsx @@ -0,0 +1,10 @@ +import type { StepAction } from '@zag-js/tour' +import type { ReactNode } from 'react' +import { useTourContext } from './use-tour-context' + +export interface TourActionsProps { + children: (actions: StepAction[]) => ReactNode +} + +export const TourActions = (props: TourActionsProps) => + props.children(useTourContext().step?.actions ?? []) diff --git a/packages/react/src/components/tour/tour-arrow-tip.tsx b/packages/react/src/components/tour/tour-arrow-tip.tsx new file mode 100644 index 0000000000..406ecc8b64 --- /dev/null +++ b/packages/react/src/components/tour/tour-arrow-tip.tsx @@ -0,0 +1,16 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourArrowTipBaseProps extends PolymorphicProps {} +export interface TourArrowTipProps extends HTMLProps<'div'>, TourArrowTipBaseProps {} + +export const TourArrowTip = forwardRef((props, ref) => { + const tour = useTourContext() + const mergedProps = mergeProps(tour.getArrowTipProps(), props) + + return +}) + +TourArrowTip.displayName = 'TourArrowTip' diff --git a/packages/react/src/components/tour/tour-arrow.tsx b/packages/react/src/components/tour/tour-arrow.tsx new file mode 100644 index 0000000000..2fe6904062 --- /dev/null +++ b/packages/react/src/components/tour/tour-arrow.tsx @@ -0,0 +1,16 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourArrowBaseProps extends PolymorphicProps {} +export interface TourArrowProps extends HTMLProps<'div'>, TourArrowBaseProps {} + +export const TourArrow = forwardRef((props, ref) => { + const tour = useTourContext() + const mergedProps = mergeProps(tour.getArrowProps(), props) + + return tour.step?.arrow ? : null +}) + +TourArrow.displayName = 'TourArrow' diff --git a/packages/react/src/components/tour/tour-backdrop.tsx b/packages/react/src/components/tour/tour-backdrop.tsx new file mode 100644 index 0000000000..ab6f7ecbdf --- /dev/null +++ b/packages/react/src/components/tour/tour-backdrop.tsx @@ -0,0 +1,25 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { composeRefs } from '../../utils/compose-refs' +import { useRenderStrategyPropsContext } from '../../utils/render-strategy' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { usePresence } from '../presence' +import { useTourContext } from './use-tour-context' + +export interface TourBackdropBaseProps extends PolymorphicProps {} +export interface TourBackdropProps extends HTMLProps<'div'>, TourBackdropBaseProps {} + +export const TourBackdrop = forwardRef((props, ref) => { + const tour = useTourContext() + const renderStrategyProps = useRenderStrategyPropsContext() + const presence = usePresence({ ...renderStrategyProps, present: tour.open }) + const mergedProps = mergeProps(tour.getBackdropProps(), presence.getPresenceProps(), props) + + if (presence.unmounted || !tour.step?.backdrop) { + return null + } + + return +}) + +TourBackdrop.displayName = 'TourBackdrop' diff --git a/packages/react/src/components/tour/tour-close-trigger.tsx b/packages/react/src/components/tour/tour-close-trigger.tsx new file mode 100644 index 0000000000..7da48c600d --- /dev/null +++ b/packages/react/src/components/tour/tour-close-trigger.tsx @@ -0,0 +1,18 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourCloseTriggerBaseProps extends PolymorphicProps {} +export interface TourCloseTriggerProps extends HTMLProps<'button'>, TourCloseTriggerBaseProps {} + +export const TourCloseTrigger = forwardRef( + (props, ref) => { + const tour = useTourContext() + const mergedProps = mergeProps(tour.getCloseTriggerProps(), props) + + return + }, +) + +TourCloseTrigger.displayName = 'TourCloseTrigger' diff --git a/packages/react/src/components/tour/tour-content.tsx b/packages/react/src/components/tour/tour-content.tsx new file mode 100644 index 0000000000..adf5ba74b5 --- /dev/null +++ b/packages/react/src/components/tour/tour-content.tsx @@ -0,0 +1,23 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { composeRefs } from '../../utils/compose-refs' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { usePresenceContext } from '../presence' +import { useTourContext } from './use-tour-context' + +export interface TourContentBaseProps extends PolymorphicProps {} +export interface TourContentProps extends HTMLProps<'div'>, TourContentBaseProps {} + +export const TourContent = forwardRef((props, ref) => { + const tour = useTourContext() + const presence = usePresenceContext() + const mergedProps = mergeProps(tour.getContentProps(), presence.getPresenceProps(), props) + + if (presence.unmounted) { + return null + } + + return +}) + +TourContent.displayName = 'TourContent' diff --git a/packages/react/src/components/tour/tour-context.tsx b/packages/react/src/components/tour/tour-context.tsx new file mode 100644 index 0000000000..b3e2dd960c --- /dev/null +++ b/packages/react/src/components/tour/tour-context.tsx @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react' +import { type UseTourContext, useTourContext } from './use-tour-context' + +export interface TourContextProps { + children: (context: UseTourContext) => ReactNode +} + +export const TourContext = (props: TourContextProps) => props.children(useTourContext()) diff --git a/packages/react/src/components/tour/tour-control.tsx b/packages/react/src/components/tour/tour-control.tsx new file mode 100644 index 0000000000..95301baaee --- /dev/null +++ b/packages/react/src/components/tour/tour-control.tsx @@ -0,0 +1,12 @@ +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { tourAnatomy } from './tour.anatomy' + +export interface TourControlBaseProps extends PolymorphicProps {} +export interface TourControlProps extends HTMLProps<'div'>, TourControlBaseProps {} + +export const TourControl = forwardRef((props, ref) => ( + +)) + +TourControl.displayName = 'TourControl' diff --git a/packages/react/src/components/tour/tour-description.tsx b/packages/react/src/components/tour/tour-description.tsx new file mode 100644 index 0000000000..553ddb201a --- /dev/null +++ b/packages/react/src/components/tour/tour-description.tsx @@ -0,0 +1,20 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourDescriptionBaseProps extends PolymorphicProps {} +export interface TourDescriptionProps extends HTMLProps<'div'>, TourDescriptionBaseProps {} + +export const TourDescription = forwardRef((props, ref) => { + const tour = useTourContext() + const mergedProps = mergeProps(tour.getDescriptionProps(), props) + + return ( + + {mergedProps.children || tour.step?.description} + + ) +}) + +TourDescription.displayName = 'TourDescription' diff --git a/packages/react/src/components/tour/tour-positioner.tsx b/packages/react/src/components/tour/tour-positioner.tsx new file mode 100644 index 0000000000..ccc8c41b61 --- /dev/null +++ b/packages/react/src/components/tour/tour-positioner.tsx @@ -0,0 +1,22 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { usePresenceContext } from '../presence' +import { useTourContext } from './use-tour-context' + +export interface TourPositionerBaseProps extends PolymorphicProps {} +export interface TourPositionerProps extends HTMLProps<'div'>, TourPositionerBaseProps {} + +export const TourPositioner = forwardRef((props, ref) => { + const tour = useTourContext() + const mergedProps = mergeProps(tour.getPositionerProps(), props) + const presence = usePresenceContext() + + if (presence.unmounted) { + return null + } + + return +}) + +TourPositioner.displayName = 'TourPositioner' diff --git a/packages/react/src/components/tour/tour-progress-text.tsx b/packages/react/src/components/tour/tour-progress-text.tsx new file mode 100644 index 0000000000..34d1c6b352 --- /dev/null +++ b/packages/react/src/components/tour/tour-progress-text.tsx @@ -0,0 +1,16 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourProgressTextBaseProps extends PolymorphicProps {} +export interface TourProgressTextProps extends HTMLProps<'div'>, TourProgressTextBaseProps {} + +export const TourProgressText = forwardRef((props, ref) => { + const tour = useTourContext() + const mergedProps = mergeProps(tour.getProgressTextProps(), props) + + return +}) + +TourProgressText.displayName = 'TourProgressText' diff --git a/packages/react/src/components/tour/tour-root.tsx b/packages/react/src/components/tour/tour-root.tsx new file mode 100644 index 0000000000..aa8e0b4a21 --- /dev/null +++ b/packages/react/src/components/tour/tour-root.tsx @@ -0,0 +1,32 @@ +import { mergeProps } from '@zag-js/react' +import type { ReactNode } from 'react' +import { RenderStrategyPropsProvider, splitRenderStrategyProps } from '../../utils/render-strategy' +import { + PresenceProvider, + type UsePresenceProps, + splitPresenceProps, + usePresence, +} from '../presence' +import type { UseTourReturn } from './use-tour' +import { TourProvider } from './use-tour-context' + +export interface TourRootBaseProps extends UsePresenceProps { + tour: UseTourReturn +} +export interface TourRootProps extends TourRootBaseProps { + children?: ReactNode +} + +export const TourRoot = (props: TourRootProps) => { + const [presenceProps, { children, tour }] = splitPresenceProps(props) + const [renderStrategyProps] = splitRenderStrategyProps(presenceProps) + const presence = usePresence(mergeProps({ present: tour.open }, presenceProps)) + + return ( + + + {children} + + + ) +} diff --git a/packages/react/src/components/tour/tour-spotlight.tsx b/packages/react/src/components/tour/tour-spotlight.tsx new file mode 100644 index 0000000000..55311a6e80 --- /dev/null +++ b/packages/react/src/components/tour/tour-spotlight.tsx @@ -0,0 +1,25 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { composeRefs } from '../../utils/compose-refs' +import { useRenderStrategyPropsContext } from '../../utils/render-strategy' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { usePresence } from '../presence' +import { useTourContext } from './use-tour-context' + +export interface TourSpotlightBaseProps extends PolymorphicProps {} +export interface TourSpotlightProps extends HTMLProps<'div'>, TourSpotlightBaseProps {} + +export const TourSpotlight = forwardRef((props, ref) => { + const tour = useTourContext() + const renderStrategyProps = useRenderStrategyPropsContext() + const presence = usePresence({ ...renderStrategyProps, present: tour.open }) + const mergedProps = mergeProps(tour.getSpotlightProps(), presence.getPresenceProps(), props) + + if (presence.unmounted) { + return null + } + + return +}) + +TourSpotlight.displayName = 'TourSpotlight' diff --git a/packages/react/src/components/tour/tour-title.tsx b/packages/react/src/components/tour/tour-title.tsx new file mode 100644 index 0000000000..e8c55d3792 --- /dev/null +++ b/packages/react/src/components/tour/tour-title.tsx @@ -0,0 +1,20 @@ +import { mergeProps } from '@zag-js/react' +import { forwardRef } from 'react' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourTitleBaseProps extends PolymorphicProps {} +export interface TourTitleProps extends HTMLProps<'h2'>, TourTitleBaseProps {} + +export const TourTitle = forwardRef((props, ref) => { + const tour = useTourContext() + const mergedProps = mergeProps(tour.getTitleProps(), props) + + return ( + + {mergedProps.children || tour.step?.title} + + ) +}) + +TourTitle.displayName = 'TourTitle' diff --git a/packages/react/src/components/tour/tour.anatomy.ts b/packages/react/src/components/tour/tour.anatomy.ts new file mode 100644 index 0000000000..c7ae3bb0b8 --- /dev/null +++ b/packages/react/src/components/tour/tour.anatomy.ts @@ -0,0 +1,3 @@ +import { anatomy } from '@zag-js/tour' + +export const tourAnatomy = anatomy.extendWith('control') diff --git a/packages/react/src/components/tour/tour.stories.tsx b/packages/react/src/components/tour/tour.stories.tsx new file mode 100644 index 0000000000..c7962d975c --- /dev/null +++ b/packages/react/src/components/tour/tour.stories.tsx @@ -0,0 +1,9 @@ +import type { Meta } from '@storybook/react' + +const meta: Meta = { + title: 'Components / Tour', +} + +export default meta + +export { Basic } from './examples/basic' diff --git a/packages/react/src/components/tour/tour.ts b/packages/react/src/components/tour/tour.ts new file mode 100644 index 0000000000..bc51bb4bde --- /dev/null +++ b/packages/react/src/components/tour/tour.ts @@ -0,0 +1,89 @@ +export type { + Point, + ProgressTextDetails, + StatusChangeDetails, + StepAction, + StepActionMap, + StepActionTriggerProps, + StepBaseDetails, + StepChangeDetails, + StepDetails, + StepEffectArgs, + StepPlacement, + StepStatus, + StepType, + WaitOptions, +} from '@zag-js/tour' +export { + TourActionTrigger as ActionTrigger, + type TourActionTriggerBaseProps as ActionTriggerBaseProps, + type TourActionTriggerProps as ActionTriggerProps, +} from './tour-action-trigger' +export { + TourActions as Actions, + type TourActionsProps as ActionsProps, +} from './tour-actions' +export { + TourArrow as Arrow, + type TourArrowBaseProps as ArrowBaseProps, + type TourArrowProps as ArrowProps, +} from './tour-arrow' +export { + TourArrowTip as ArrowTip, + type TourArrowTipBaseProps as ArrowTipBaseProps, + type TourArrowTipProps as ArrowTipProps, +} from './tour-arrow-tip' +export { + TourBackdrop as Backdrop, + type TourBackdropBaseProps as BackdropBaseProps, + type TourBackdropProps as BackdropProps, +} from './tour-backdrop' +export { + TourCloseTrigger as CloseTrigger, + type TourCloseTriggerBaseProps as CloseTriggerBaseProps, + type TourCloseTriggerProps as CloseTriggerProps, +} from './tour-close-trigger' +export { + TourContent as Content, + type TourContentBaseProps as ContentBaseProps, + type TourContentProps as ContentProps, +} from './tour-content' +export { + TourContext as Context, + type TourContextProps as ContextProps, +} from './tour-context' +export { + TourControl as Control, + type TourControlBaseProps as ControlBaseProps, + type TourControlProps as ControlProps, +} from './tour-control' +export { + TourDescription as Description, + type TourDescriptionBaseProps as DescriptionBaseProps, + type TourDescriptionProps as DescriptionProps, +} from './tour-description' +export { + TourPositioner as Positioner, + type TourPositionerBaseProps as PositionerBaseProps, + type TourPositionerProps as PositionerProps, +} from './tour-positioner' +export { + TourProgressText as ProgressText, + type TourProgressTextBaseProps as ProgressTextBaseProps, + type TourProgressTextProps as ProgressTextProps, +} from './tour-progress-text' +export { + TourRoot as Root, + type TourRootBaseProps as RootBaseProps, + type TourRootProps as RootProps, +} from './tour-root' +export { + TourSpotlight as Spotlight, + type TourSpotlightBaseProps as SpotlightBaseProps, + type TourSpotlightProps as SpotlightProps, +} from './tour-spotlight' +export { + TourTitle as Title, + type TourTitleBaseProps as TitleBaseProps, + type TourTitleProps as TitleProps, +} from './tour-title' diff --git a/packages/react/src/components/tour/use-tour-context.ts b/packages/react/src/components/tour/use-tour-context.ts new file mode 100644 index 0000000000..70d9923010 --- /dev/null +++ b/packages/react/src/components/tour/use-tour-context.ts @@ -0,0 +1,11 @@ +import type { PropTypes } from '@zag-js/react' +import type * as tour from '@zag-js/tour' +import { createContext } from '../../utils/create-context' + +export interface UseTourContext extends tour.Api {} + +export const [TourProvider, useTourContext] = createContext({ + name: 'TourContext', + hookName: 'useTourContext', + providerName: '', +}) diff --git a/packages/react/src/components/tour/use-tour.ts b/packages/react/src/components/tour/use-tour.ts new file mode 100644 index 0000000000..f45672081b --- /dev/null +++ b/packages/react/src/components/tour/use-tour.ts @@ -0,0 +1,29 @@ +import { type PropTypes, normalizeProps, useMachine } from '@zag-js/react' +import * as tour from '@zag-js/tour' +import { useId } from 'react' +import { useEnvironmentContext, useLocaleContext } from '../../providers' +import type { Optional } from '../../types' +import { useEvent } from '../../utils/use-event' + +export interface UseTourProps extends Optional, 'id'> {} +export interface UseTourReturn extends tour.Api {} + +export const useTour = (props: UseTourProps = {}): UseTourReturn => { + const { getRootNode } = useEnvironmentContext() + const { dir } = useLocaleContext() + + const initialContext: tour.Context = { + id: useId(), + dir, + getRootNode, + ...props, + } + + const context: tour.Context = { + ...initialContext, + onStatusChange: useEvent(props.onStatusChange), + } + + const [state, send] = useMachine(tour.machine(initialContext), { context }) + return tour.connect(state, send, normalizeProps) +} diff --git a/packages/solid/.storybook/main.css b/packages/solid/.storybook/main.css index 8881334f01..bdb1c43fe4 100644 --- a/packages/solid/.storybook/main.css +++ b/packages/solid/.storybook/main.css @@ -32,5 +32,6 @@ @import url("./styles/toast.css"); @import url("./styles/toggle-group.css"); @import url("./styles/tooltip.css"); +@import url("./styles/tour.css"); @import url("./styles/tree-view.css"); @import url("./styles/timer.css"); diff --git a/packages/solid/.storybook/styles/tour.css b/packages/solid/.storybook/styles/tour.css new file mode 100644 index 0000000000..d2ef3bac18 --- /dev/null +++ b/packages/solid/.storybook/styles/tour.css @@ -0,0 +1,130 @@ +[data-scope="tour"][data-part="positioner"][data-type="floating"] { + position: absolute; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="bottom"] { + bottom: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="top"] { + top: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="end"] { + inset-inline-end: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="start"] { + inset-inline-start: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="dialog"] { + width: 100%; + position: fixed; + inset: 0; + margin: auto; + display: flex; + align-items: center; + justify-content: center; +} + +[data-scope="tour"][data-part="content"] { + --arrow-background: white; + --arrow-size: 10px; + background: white; + padding: 24px; + position: relative; + border-radius: 4px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + width: 300px; +} + +[data-scope="tour"][data-part="content"][data-type="dialog"] { + width: 500px; + background: lightblue; +} + +[data-scope="tour"][data-part="content"][data-type="floating"] { + width: 500px; + background: rgb(15, 39, 136); + color: white; +} + +[data-scope="tour"][data-part="arrow"] { + --arrow-background: white; + --arrow-shadow-color: #ebebeb; + box-shadow: var(--box-shadow); +} + +[data-scope="tour"][data-part="title"] { + font-weight: 600; +} + +[data-scope="tour"][data-part="description"] { + margin-bottom: 20px; +} + +[data-scope="tour"][data-part="progress-text"] { + margin-bottom: 20px; + opacity: 0.72; +} + +[data-scope="tour"][data-part="backdrop"] { + background-color: rgba(0, 0, 0, 0.5); +} + +[data-scope="tour"][data-part="spotlight"] { + border: 3px solid pink; +} + +[data-scope="tour"][data-part="close-trigger"] { + font-family: inherit; + height: 25px; + width: 25px; + display: inline-flex; + align-items: center; + justify-content: center; + position: absolute; + top: 10px; + right: 10px; +} + +.tour.button__group { + display: flex; + align-items: flex-end; + gap: 10px; +} + +.tour .steps__container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 50vh; +} + +.tour .overflow__container { + width: 500px; + height: 400px; + max-height: 200px; + overflow: auto; + border: 2px solid teal; + position: relative; +} + +.tour .overflow__container::before { + content: "Overflow"; + display: block; + position: sticky; + background-color: teal; + color: white; + padding: 2px 4px 3px; + top: 0px; +} + +.tour .overflow__container .h-200px { + height: 200px; +} + +.tour .overflow__container .h-100px { + height: 100px; +} diff --git a/packages/solid/package.json b/packages/solid/package.json index a68958213e..028e021f3c 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -39,6 +39,7 @@ "toast", "toggle group", "tooltip", + "tour", "tree view" ], "license": "MIT", @@ -173,6 +174,7 @@ "@zag-js/toast": "0.81.0", "@zag-js/toggle-group": "0.81.0", "@zag-js/tooltip": "0.81.0", + "@zag-js/tour": "0.81.0", "@zag-js/tree-view": "0.81.0", "@zag-js/types": "0.81.0" }, diff --git a/packages/solid/src/components/index.tsx b/packages/solid/src/components/index.tsx index d25e4d9554..91e7a5baf5 100644 --- a/packages/solid/src/components/index.tsx +++ b/packages/solid/src/components/index.tsx @@ -43,4 +43,5 @@ export * from './toast' export * from './toggle' export * from './toggle-group' export * from './tooltip' +export * from './tour' export * from './tree-view' diff --git a/packages/solid/src/components/tour/examples/basic.tsx b/packages/solid/src/components/tour/examples/basic.tsx new file mode 100644 index 0000000000..9b38078b63 --- /dev/null +++ b/packages/solid/src/components/tour/examples/basic.tsx @@ -0,0 +1,29 @@ +import { Frame } from '@ark-ui/solid/frame' +import { DemoTour } from './tour' + +export const Basic = () => { + return ( +
+ +
+
+

Step 1

+
+
+

Step 2

+
+
+ +

Iframe Content

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. +

+ +

Step 3

+

Step 4

+
+
+
+ ) +} diff --git a/packages/solid/src/components/tour/examples/steps.tsx b/packages/solid/src/components/tour/examples/steps.tsx new file mode 100644 index 0000000000..24ba37081b --- /dev/null +++ b/packages/solid/src/components/tour/examples/steps.tsx @@ -0,0 +1,90 @@ +import type { TourStepDetails } from '@ark-ui/solid/tour' + +export const steps: TourStepDetails[] = [ + { + type: 'dialog', + id: 'step-0', + title: 'Centered tour (no target)', + description: 'This is the center of the world. Ready to start the tour?', + actions: [{ label: 'Next', action: 'next' }], + }, + { + type: 'tooltip', + id: 'step-1', + title: 'Step 1. Welcome', + description: 'To the new world', + target: () => document.querySelector('#step-1'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + effect({ show, update }) { + const abort = new AbortController() + + fetch('https://api.github.com/users/octocat', { signal: abort.signal }) + .then((res) => res.json()) + .then((data) => { + update({ title: data.name }) + show() + }) + + return () => { + abort.abort() + } + }, + }, + { + type: 'tooltip', + id: 'step-2', + title: 'Step 2. Inside a scrollable container', + description: 'Using scrollIntoView(...) rocks!', + target: () => document.querySelector('#step-2'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-2a', + title: 'Step 2a. Inside an Iframe container', + description: 'It calculates the offset rect correctly. Thanks to floating UI!', + target: () => { + const [frameEl] = Array.from(frames) + return frameEl?.document.querySelector('#step-2a') + }, + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-3', + title: 'Step 3. Normal scrolling', + description: 'The new world is a great place', + target: () => document.querySelector('#step-3'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-4', + title: 'Step 4. Close to bottom', + description: 'So nice to see the scrolling works!', + target: () => document.querySelector('#step-4'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'dialog', + id: 'step-5', + title: "You're all sorted! (no target)", + description: 'Thanks for trying out the tour. Enjoy the app!', + actions: [{ label: 'Finish', action: 'dismiss' }], + }, +] diff --git a/packages/solid/src/components/tour/examples/tour.tsx b/packages/solid/src/components/tour/examples/tour.tsx new file mode 100644 index 0000000000..3050e16e0d --- /dev/null +++ b/packages/solid/src/components/tour/examples/tour.tsx @@ -0,0 +1,38 @@ +import { Tour, useTour } from '@ark-ui/solid/tour' +import { XIcon } from 'lucide-solid' +import { For, onMount } from 'solid-js' +import { steps } from './steps' + +export const DemoTour = () => { + const tour = useTour({ steps }) + + // Start the tour when the component mounts + onMount(() => tour().start()) + + return ( + + + + + + + + + + + + + + + + + {(actions) => ( + {(action) => } + )} + + + + + + ) +} diff --git a/packages/solid/src/components/tour/index.tsx b/packages/solid/src/components/tour/index.tsx new file mode 100644 index 0000000000..6f43e102e1 --- /dev/null +++ b/packages/solid/src/components/tour/index.tsx @@ -0,0 +1,56 @@ +export type { StepDetails as TourStepDetails } from '@zag-js/tour' +export { + TourActionTrigger, + type TourActionTriggerBaseProps, + type TourActionTriggerProps, +} from './tour-action-trigger' +export { + TourActions, + type TourActionsProps, +} from './tour-actions' +export { TourArrow, type TourArrowBaseProps, type TourArrowProps } from './tour-arrow' +export { + TourArrowTip, + type TourArrowTipBaseProps, + type TourArrowTipProps, +} from './tour-arrow-tip' +export { TourBackdrop, type TourBackdropBaseProps, type TourBackdropProps } from './tour-backdrop' +export { + TourCloseTrigger, + type TourCloseTriggerBaseProps, + type TourCloseTriggerProps, +} from './tour-close-trigger' +export { TourContent, type TourContentBaseProps, type TourContentProps } from './tour-content' +export { TourContext, type TourContextProps } from './tour-context' +export { + TourControl, + type TourControlBaseProps, + type TourControlProps, +} from './tour-control' +export { + TourDescription, + type TourDescriptionBaseProps, + type TourDescriptionProps, +} from './tour-description' +export { + TourPositioner, + type TourPositionerBaseProps, + type TourPositionerProps, +} from './tour-positioner' +export { + TourProgressText, + type TourProgressTextBaseProps, + type TourProgressTextProps, +} from './tour-progress-text' +export { TourRoot, type TourRootBaseProps, type TourRootProps } from './tour-root' +export { + TourSpotlight, + type TourSpotlightBaseProps, + type TourSpotlightProps, +} from './tour-spotlight' +export { TourTitle, type TourTitleBaseProps, type TourTitleProps } from './tour-title' +export { tourAnatomy } from './tour.anatomy' +export { useTour, type UseTourProps, type UseTourReturn } from './use-tour' +export { useTourContext, type UseTourContext } from './use-tour-context' + +export * as Tour from './tour' diff --git a/packages/solid/src/components/tour/tour-action-trigger.tsx b/packages/solid/src/components/tour/tour-action-trigger.tsx new file mode 100644 index 0000000000..9e8ef96daa --- /dev/null +++ b/packages/solid/src/components/tour/tour-action-trigger.tsx @@ -0,0 +1,24 @@ +import { mergeProps } from '@zag-js/solid' +import type { StepActionTriggerProps } from '@zag-js/tour' +import { createSplitProps } from '../../utils/create-split-props' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourActionTriggerBaseProps + extends PolymorphicProps<'button'>, + StepActionTriggerProps {} +export interface TourActionTriggerProps extends HTMLProps<'button'>, TourActionTriggerBaseProps {} + +export const TourActionTrigger = (props: TourActionTriggerProps) => { + const [actionTriggerProps, localProps] = createSplitProps()(props, [ + 'action', + ]) + const tour = useTourContext() + const mergedProps = mergeProps(() => tour().getActionTriggerProps(actionTriggerProps), localProps) + + return ( + + {mergedProps.children || actionTriggerProps.action.label} + + ) +} diff --git a/packages/solid/src/components/tour/tour-actions.tsx b/packages/solid/src/components/tour/tour-actions.tsx new file mode 100644 index 0000000000..d4f8df68cd --- /dev/null +++ b/packages/solid/src/components/tour/tour-actions.tsx @@ -0,0 +1,13 @@ +import type { StepAction } from '@zag-js/tour' +import type { Accessor, JSX } from 'solid-js' + +import { useTourContext } from './use-tour-context' + +export interface TourActionsProps { + children: (actions: Accessor) => JSX.Element +} + +export const TourActions = (props: TourActionsProps) => { + const tour = useTourContext() + return props.children(() => tour().step?.actions ?? []) +} diff --git a/packages/solid/src/components/tour/tour-arrow-tip.tsx b/packages/solid/src/components/tour/tour-arrow-tip.tsx new file mode 100644 index 0000000000..ea5531f1d0 --- /dev/null +++ b/packages/solid/src/components/tour/tour-arrow-tip.tsx @@ -0,0 +1,13 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourArrowTipBaseProps extends PolymorphicProps<'div'> {} +export interface TourArrowTipProps extends HTMLProps<'div'>, TourArrowTipBaseProps {} + +export const TourArrowTip = (props: TourArrowTipProps) => { + const tour = useTourContext() + const mergedProps = mergeProps(() => tour().getArrowTipProps(), props) + + return +} diff --git a/packages/solid/src/components/tour/tour-arrow.tsx b/packages/solid/src/components/tour/tour-arrow.tsx new file mode 100644 index 0000000000..5845f5b9a6 --- /dev/null +++ b/packages/solid/src/components/tour/tour-arrow.tsx @@ -0,0 +1,18 @@ +import { mergeProps } from '@zag-js/solid' +import { Show } from 'solid-js' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourArrowBaseProps extends PolymorphicProps<'div'> {} +export interface TourArrowProps extends HTMLProps<'div'>, TourArrowBaseProps {} + +export const TourArrow = (props: TourArrowProps) => { + const tour = useTourContext() + const mergedProps = mergeProps(() => tour().getArrowProps(), props) + + return ( + + + + ) +} diff --git a/packages/solid/src/components/tour/tour-backdrop.tsx b/packages/solid/src/components/tour/tour-backdrop.tsx new file mode 100644 index 0000000000..9138c76a2a --- /dev/null +++ b/packages/solid/src/components/tour/tour-backdrop.tsx @@ -0,0 +1,26 @@ +import { mergeProps } from '@zag-js/solid' +import { Show } from 'solid-js' +import { useRenderStrategyContext } from '../../utils/render-strategy' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { usePresence } from '../presence' +import { useTourContext } from './use-tour-context' + +export interface TourBackdropBaseProps extends PolymorphicProps<'div'> {} +export interface TourBackdropProps extends HTMLProps<'div'>, TourBackdropBaseProps {} + +export const TourBackdrop = (props: TourBackdropProps) => { + const tour = useTourContext() + const renderStrategyProps = useRenderStrategyContext() + const presence = usePresence(mergeProps(renderStrategyProps, () => ({ present: tour().open }))) + const mergedProps = mergeProps( + () => tour().getBackdropProps(), + () => presence().presenceProps, + props, + ) + + return ( + + + + ) +} diff --git a/packages/solid/src/components/tour/tour-close-trigger.tsx b/packages/solid/src/components/tour/tour-close-trigger.tsx new file mode 100644 index 0000000000..c0f2ebde66 --- /dev/null +++ b/packages/solid/src/components/tour/tour-close-trigger.tsx @@ -0,0 +1,13 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourCloseTriggerBaseProps extends PolymorphicProps<'button'> {} +export interface TourCloseTriggerProps extends HTMLProps<'button'>, TourCloseTriggerBaseProps {} + +export const TourCloseTrigger = (props: TourCloseTriggerProps) => { + const tour = useTourContext() + const mergedProps = mergeProps(() => tour().getCloseTriggerProps(), props) + + return +} diff --git a/packages/solid/src/components/tour/tour-content.tsx b/packages/solid/src/components/tour/tour-content.tsx new file mode 100644 index 0000000000..e1d4b4f1ce --- /dev/null +++ b/packages/solid/src/components/tour/tour-content.tsx @@ -0,0 +1,26 @@ +import { mergeProps } from '@zag-js/solid' +import { Show } from 'solid-js' +import { useRenderStrategyContext } from '../../utils/render-strategy' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { usePresence } from '../presence' +import { useTourContext } from './use-tour-context' + +export interface TourContentBaseProps extends PolymorphicProps<'div'> {} +export interface TourContentProps extends HTMLProps<'div'>, TourContentBaseProps {} + +export const TourContent = (props: TourContentProps) => { + const tour = useTourContext() + const renderStrategyProps = useRenderStrategyContext() + const presence = usePresence(mergeProps(renderStrategyProps, () => ({ present: tour().open }))) + const mergedProps = mergeProps( + () => tour().getContentProps(), + () => presence().presenceProps, + props, + ) + + return ( + + + + ) +} diff --git a/packages/solid/src/components/tour/tour-context.tsx b/packages/solid/src/components/tour/tour-context.tsx new file mode 100644 index 0000000000..050fb0b15d --- /dev/null +++ b/packages/solid/src/components/tour/tour-context.tsx @@ -0,0 +1,9 @@ +import type { JSX } from 'solid-js' + +import { type UseTourContext, useTourContext } from './use-tour-context' + +export interface TourContextProps { + children: (context: UseTourContext) => JSX.Element +} + +export const TourContext = (props: TourContextProps) => props.children(useTourContext()) diff --git a/packages/solid/src/components/tour/tour-control.tsx b/packages/solid/src/components/tour/tour-control.tsx new file mode 100644 index 0000000000..a028165001 --- /dev/null +++ b/packages/solid/src/components/tour/tour-control.tsx @@ -0,0 +1,12 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { tourAnatomy } from './tour.anatomy' + +export interface TourControlBaseProps extends PolymorphicProps<'div'> {} +export interface TourControlProps extends HTMLProps<'div'>, TourControlBaseProps {} + +export const TourControl = (props: TourControlProps) => { + const mergedProps = mergeProps(() => tourAnatomy.build().control.attrs, props) + + return +} diff --git a/packages/solid/src/components/tour/tour-description.tsx b/packages/solid/src/components/tour/tour-description.tsx new file mode 100644 index 0000000000..a8dacb5ea4 --- /dev/null +++ b/packages/solid/src/components/tour/tour-description.tsx @@ -0,0 +1,13 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourDescriptionBaseProps extends PolymorphicProps<'div'> {} +export interface TourDescriptionProps extends HTMLProps<'div'>, TourDescriptionBaseProps {} + +export const TourDescription = (props: TourDescriptionProps) => { + const tour = useTourContext() + const mergedProps = mergeProps(() => tour().getDescriptionProps(), props) + + return {mergedProps.children || tour().step?.description} +} diff --git a/packages/solid/src/components/tour/tour-positioner.tsx b/packages/solid/src/components/tour/tour-positioner.tsx new file mode 100644 index 0000000000..63a6e3f7e0 --- /dev/null +++ b/packages/solid/src/components/tour/tour-positioner.tsx @@ -0,0 +1,20 @@ +import { mergeProps } from '@zag-js/solid' +import { Show } from 'solid-js' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { usePresenceContext } from '../presence' +import { useTourContext } from './use-tour-context' + +export interface TourPositionerBaseProps extends PolymorphicProps<'div'> {} +export interface TourPositionerProps extends HTMLProps<'div'>, TourPositionerBaseProps {} + +export const TourPositioner = (props: TourPositionerProps) => { + const tour = useTourContext() + const presence = usePresenceContext() + const mergedProps = mergeProps(() => tour().getPositionerProps(), props) + + return ( + + + + ) +} diff --git a/packages/solid/src/components/tour/tour-progress-text.tsx b/packages/solid/src/components/tour/tour-progress-text.tsx new file mode 100644 index 0000000000..7d8c93e863 --- /dev/null +++ b/packages/solid/src/components/tour/tour-progress-text.tsx @@ -0,0 +1,13 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourProgressTextBaseProps extends PolymorphicProps<'div'> {} +export interface TourProgressTextProps extends HTMLProps<'div'>, TourProgressTextBaseProps {} + +export const TourProgressText = (props: TourProgressTextProps) => { + const tour = useTourContext() + const mergedProps = mergeProps(() => tour().getProgressTextProps(), props) + + return +} diff --git a/packages/solid/src/components/tour/tour-root.tsx b/packages/solid/src/components/tour/tour-root.tsx new file mode 100644 index 0000000000..9662db08bd --- /dev/null +++ b/packages/solid/src/components/tour/tour-root.tsx @@ -0,0 +1,37 @@ +import { mergeProps } from '@zag-js/solid' +import type { JSX } from 'solid-js' +import { RenderStrategyProvider, splitRenderStrategyProps } from '../../utils/render-strategy' +import { + PresenceProvider, + type UsePresenceProps, + splitPresenceProps, + usePresence, +} from '../presence' +import type { UseTourReturn } from './use-tour' +import { TourProvider } from './use-tour-context' + +interface RootProps { + tour: UseTourReturn +} + +export interface TourRootBaseProps extends RootProps, UsePresenceProps {} +export interface TourRootProps extends TourRootBaseProps { + children?: JSX.Element +} + +export const TourRoot = (props: TourRootProps) => { + const [presenceProps, rootProps] = splitPresenceProps(props) + const [renderStrategyProps] = splitRenderStrategyProps(presenceProps) + + const presence = usePresence( + mergeProps(presenceProps, () => ({ present: rootProps.tour().open })), + ) + + return ( + + + {rootProps.children} + + + ) +} diff --git a/packages/solid/src/components/tour/tour-spotlight.tsx b/packages/solid/src/components/tour/tour-spotlight.tsx new file mode 100644 index 0000000000..e32f46e84a --- /dev/null +++ b/packages/solid/src/components/tour/tour-spotlight.tsx @@ -0,0 +1,26 @@ +import { mergeProps } from '@zag-js/solid' +import { Show } from 'solid-js' +import { useRenderStrategyContext } from '../../utils/render-strategy' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { usePresence } from '../presence' +import { useTourContext } from './use-tour-context' + +export interface TourSpotlightBaseProps extends PolymorphicProps<'div'> {} +export interface TourSpotlightProps extends HTMLProps<'div'>, TourSpotlightBaseProps {} + +export const TourSpotlight = (props: TourSpotlightProps) => { + const tour = useTourContext() + const renderStrategyProps = useRenderStrategyContext() + const presenceApi = usePresence(mergeProps(renderStrategyProps, () => ({ present: tour().open }))) + const mergedProps = mergeProps( + () => tour().getSpotlightProps(), + () => presenceApi().presenceProps, + props, + ) + + return ( + + + + ) +} diff --git a/packages/solid/src/components/tour/tour-title.tsx b/packages/solid/src/components/tour/tour-title.tsx new file mode 100644 index 0000000000..a3b08d3d23 --- /dev/null +++ b/packages/solid/src/components/tour/tour-title.tsx @@ -0,0 +1,13 @@ +import { mergeProps } from '@zag-js/solid' +import { type HTMLProps, type PolymorphicProps, ark } from '../factory' +import { useTourContext } from './use-tour-context' + +export interface TourTitleBaseProps extends PolymorphicProps<'h2'> {} +export interface TourTitleProps extends HTMLProps<'h2'>, TourTitleBaseProps {} + +export const TourTitle = (props: TourTitleProps) => { + const tour = useTourContext() + const mergedProps = mergeProps(() => tour().getTitleProps(), props) + + return {mergedProps.children || tour().step?.title} +} diff --git a/packages/solid/src/components/tour/tour.anatomy.ts b/packages/solid/src/components/tour/tour.anatomy.ts new file mode 100644 index 0000000000..c7ae3bb0b8 --- /dev/null +++ b/packages/solid/src/components/tour/tour.anatomy.ts @@ -0,0 +1,3 @@ +import { anatomy } from '@zag-js/tour' + +export const tourAnatomy = anatomy.extendWith('control') diff --git a/packages/solid/src/components/tour/tour.stories.tsx b/packages/solid/src/components/tour/tour.stories.tsx new file mode 100644 index 0000000000..4d05a8783f --- /dev/null +++ b/packages/solid/src/components/tour/tour.stories.tsx @@ -0,0 +1,9 @@ +import type { Meta } from 'storybook-solidjs' + +const meta: Meta = { + title: 'Components / Tour', +} + +export default meta + +export { Basic } from './examples/basic' diff --git a/packages/solid/src/components/tour/tour.ts b/packages/solid/src/components/tour/tour.ts new file mode 100644 index 0000000000..bc51bb4bde --- /dev/null +++ b/packages/solid/src/components/tour/tour.ts @@ -0,0 +1,89 @@ +export type { + Point, + ProgressTextDetails, + StatusChangeDetails, + StepAction, + StepActionMap, + StepActionTriggerProps, + StepBaseDetails, + StepChangeDetails, + StepDetails, + StepEffectArgs, + StepPlacement, + StepStatus, + StepType, + WaitOptions, +} from '@zag-js/tour' +export { + TourActionTrigger as ActionTrigger, + type TourActionTriggerBaseProps as ActionTriggerBaseProps, + type TourActionTriggerProps as ActionTriggerProps, +} from './tour-action-trigger' +export { + TourActions as Actions, + type TourActionsProps as ActionsProps, +} from './tour-actions' +export { + TourArrow as Arrow, + type TourArrowBaseProps as ArrowBaseProps, + type TourArrowProps as ArrowProps, +} from './tour-arrow' +export { + TourArrowTip as ArrowTip, + type TourArrowTipBaseProps as ArrowTipBaseProps, + type TourArrowTipProps as ArrowTipProps, +} from './tour-arrow-tip' +export { + TourBackdrop as Backdrop, + type TourBackdropBaseProps as BackdropBaseProps, + type TourBackdropProps as BackdropProps, +} from './tour-backdrop' +export { + TourCloseTrigger as CloseTrigger, + type TourCloseTriggerBaseProps as CloseTriggerBaseProps, + type TourCloseTriggerProps as CloseTriggerProps, +} from './tour-close-trigger' +export { + TourContent as Content, + type TourContentBaseProps as ContentBaseProps, + type TourContentProps as ContentProps, +} from './tour-content' +export { + TourContext as Context, + type TourContextProps as ContextProps, +} from './tour-context' +export { + TourControl as Control, + type TourControlBaseProps as ControlBaseProps, + type TourControlProps as ControlProps, +} from './tour-control' +export { + TourDescription as Description, + type TourDescriptionBaseProps as DescriptionBaseProps, + type TourDescriptionProps as DescriptionProps, +} from './tour-description' +export { + TourPositioner as Positioner, + type TourPositionerBaseProps as PositionerBaseProps, + type TourPositionerProps as PositionerProps, +} from './tour-positioner' +export { + TourProgressText as ProgressText, + type TourProgressTextBaseProps as ProgressTextBaseProps, + type TourProgressTextProps as ProgressTextProps, +} from './tour-progress-text' +export { + TourRoot as Root, + type TourRootBaseProps as RootBaseProps, + type TourRootProps as RootProps, +} from './tour-root' +export { + TourSpotlight as Spotlight, + type TourSpotlightBaseProps as SpotlightBaseProps, + type TourSpotlightProps as SpotlightProps, +} from './tour-spotlight' +export { + TourTitle as Title, + type TourTitleBaseProps as TitleBaseProps, + type TourTitleProps as TitleProps, +} from './tour-title' diff --git a/packages/solid/src/components/tour/use-tour-context.ts b/packages/solid/src/components/tour/use-tour-context.ts new file mode 100644 index 0000000000..8de7a24f86 --- /dev/null +++ b/packages/solid/src/components/tour/use-tour-context.ts @@ -0,0 +1,9 @@ +import { createContext } from '../../utils/create-context' +import type { UseTourReturn } from './use-tour' + +export interface UseTourContext extends UseTourReturn {} + +export const [TourProvider, useTourContext] = createContext({ + hookName: 'useTourContext', + providerName: '', +}) diff --git a/packages/solid/src/components/tour/use-tour.ts b/packages/solid/src/components/tour/use-tour.ts new file mode 100644 index 0000000000..000ff05ef4 --- /dev/null +++ b/packages/solid/src/components/tour/use-tour.ts @@ -0,0 +1,27 @@ +import { type PropTypes, normalizeProps, useMachine } from '@zag-js/solid' +import * as tour from '@zag-js/tour' +import { type Accessor, createMemo, createUniqueId } from 'solid-js' +import { useEnvironmentContext, useLocaleContext } from '../../providers' +import type { Optional } from '../../types' + +export interface UseTourProps extends Optional, 'id'> {} +export interface UseTourReturn extends Accessor> {} + +export const useTour = (props: UseTourProps = {}): UseTourReturn => { + const id = createUniqueId() + const locale = useLocaleContext() + const environment = useEnvironmentContext() + + const context = createMemo(() => ({ + id, + dir: locale().dir, + getRootNode: environment().getRootNode, + ...props, + })) + + const [state, send] = useMachine(tour.machine(context()), { + context, + }) + + return createMemo(() => tour.connect(state, send, normalizeProps)) +} diff --git a/packages/svelte/src/lib/components/progress/progress-circular.stories.ts b/packages/svelte/src/lib/components/progress/progress-circular.stories.ts index babfd44eec..1a24d0ae30 100644 --- a/packages/svelte/src/lib/components/progress/progress-circular.stories.ts +++ b/packages/svelte/src/lib/components/progress/progress-circular.stories.ts @@ -42,7 +42,6 @@ export const RootProvider = { }), } - export const ValueText = { render: () => ({ Component: ValueTextExample, diff --git a/packages/svelte/src/lib/components/progress/progress-linear.stories.ts b/packages/svelte/src/lib/components/progress/progress-linear.stories.ts index 7493e96fa8..45c661721e 100644 --- a/packages/svelte/src/lib/components/progress/progress-linear.stories.ts +++ b/packages/svelte/src/lib/components/progress/progress-linear.stories.ts @@ -42,7 +42,6 @@ export const RootProvider = { }), } - export const ValueText = { render: () => ({ Component: ValueTextExample, diff --git a/packages/vue/.storybook/main.css b/packages/vue/.storybook/main.css index 5c10e0751a..c6cd923fe3 100644 --- a/packages/vue/.storybook/main.css +++ b/packages/vue/.storybook/main.css @@ -15,8 +15,8 @@ @import url("./styles/pagination.css"); @import url("./styles/pin-input.css"); @import url("./styles/popover.css"); -@import url("./styles/progress.css"); @import url("./styles/presence.css"); +@import url("./styles/progress.css"); @import url("./styles/qr-code.css"); @import url("./styles/radio-group.css"); @import url("./styles/segment-group.css"); @@ -28,8 +28,9 @@ @import url("./styles/tabs.css"); @import url("./styles/tags-input.css"); @import url("./styles/time-picker.css"); +@import url("./styles/timer.css"); @import url("./styles/toast.css"); @import url("./styles/toggle-group.css"); @import url("./styles/tooltip.css"); +@import url("./styles/tour.css"); @import url("./styles/tree-view.css"); -@import url("./styles/timer.css"); diff --git a/packages/vue/.storybook/styles/tour.css b/packages/vue/.storybook/styles/tour.css new file mode 100644 index 0000000000..d2ef3bac18 --- /dev/null +++ b/packages/vue/.storybook/styles/tour.css @@ -0,0 +1,130 @@ +[data-scope="tour"][data-part="positioner"][data-type="floating"] { + position: absolute; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="bottom"] { + bottom: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="top"] { + top: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="end"] { + inset-inline-end: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="floating"][data-placement*="start"] { + inset-inline-start: 24px; +} + +[data-scope="tour"][data-part="positioner"][data-type="dialog"] { + width: 100%; + position: fixed; + inset: 0; + margin: auto; + display: flex; + align-items: center; + justify-content: center; +} + +[data-scope="tour"][data-part="content"] { + --arrow-background: white; + --arrow-size: 10px; + background: white; + padding: 24px; + position: relative; + border-radius: 4px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + width: 300px; +} + +[data-scope="tour"][data-part="content"][data-type="dialog"] { + width: 500px; + background: lightblue; +} + +[data-scope="tour"][data-part="content"][data-type="floating"] { + width: 500px; + background: rgb(15, 39, 136); + color: white; +} + +[data-scope="tour"][data-part="arrow"] { + --arrow-background: white; + --arrow-shadow-color: #ebebeb; + box-shadow: var(--box-shadow); +} + +[data-scope="tour"][data-part="title"] { + font-weight: 600; +} + +[data-scope="tour"][data-part="description"] { + margin-bottom: 20px; +} + +[data-scope="tour"][data-part="progress-text"] { + margin-bottom: 20px; + opacity: 0.72; +} + +[data-scope="tour"][data-part="backdrop"] { + background-color: rgba(0, 0, 0, 0.5); +} + +[data-scope="tour"][data-part="spotlight"] { + border: 3px solid pink; +} + +[data-scope="tour"][data-part="close-trigger"] { + font-family: inherit; + height: 25px; + width: 25px; + display: inline-flex; + align-items: center; + justify-content: center; + position: absolute; + top: 10px; + right: 10px; +} + +.tour.button__group { + display: flex; + align-items: flex-end; + gap: 10px; +} + +.tour .steps__container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 50vh; +} + +.tour .overflow__container { + width: 500px; + height: 400px; + max-height: 200px; + overflow: auto; + border: 2px solid teal; + position: relative; +} + +.tour .overflow__container::before { + content: "Overflow"; + display: block; + position: sticky; + background-color: teal; + color: white; + padding: 2px 4px 3px; + top: 0px; +} + +.tour .overflow__container .h-200px { + height: 200px; +} + +.tour .overflow__container .h-100px { + height: 100px; +} diff --git a/packages/vue/package.json b/packages/vue/package.json index e29da1d70d..f983bd9560 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -39,6 +39,7 @@ "toast", "toggle group", "tooltip", + "tour", "tree view" ], "license": "MIT", @@ -206,6 +207,7 @@ "@zag-js/toast": "0.81.0", "@zag-js/toggle-group": "0.81.0", "@zag-js/tooltip": "0.81.0", + "@zag-js/tour": "0.81.0", "@zag-js/tree-view": "0.81.0", "@zag-js/types": "0.81.0", "@zag-js/utils": "0.81.0", diff --git a/packages/vue/src/components/index.ts b/packages/vue/src/components/index.ts index d25e4d9554..91e7a5baf5 100644 --- a/packages/vue/src/components/index.ts +++ b/packages/vue/src/components/index.ts @@ -43,4 +43,5 @@ export * from './toast' export * from './toggle' export * from './toggle-group' export * from './tooltip' +export * from './tour' export * from './tree-view' diff --git a/packages/vue/src/components/tour/examples/basic.vue b/packages/vue/src/components/tour/examples/basic.vue new file mode 100644 index 0000000000..68fd24c4d9 --- /dev/null +++ b/packages/vue/src/components/tour/examples/basic.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/vue/src/components/tour/examples/steps.tsx b/packages/vue/src/components/tour/examples/steps.tsx new file mode 100644 index 0000000000..3e0228008c --- /dev/null +++ b/packages/vue/src/components/tour/examples/steps.tsx @@ -0,0 +1,90 @@ +import type { TourStepDetails } from '@ark-ui/vue/tour' + +export const steps: TourStepDetails[] = [ + { + type: 'dialog', + id: 'step-0', + title: 'Centered tour (no target)', + description: 'This is the center of the world. Ready to start the tour?', + actions: [{ label: 'Next', action: 'next' }], + }, + { + type: 'tooltip', + id: 'step-1', + title: 'Step 1. Welcome', + description: 'To the new world', + target: () => document.querySelector('#step-1'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + effect({ show, update }) { + const abort = new AbortController() + + fetch('https://api.github.com/users/octocat', { signal: abort.signal }) + .then((res) => res.json()) + .then((data) => { + update({ title: data.name }) + show() + }) + + return () => { + abort.abort() + } + }, + }, + { + type: 'tooltip', + id: 'step-2', + title: 'Step 2. Inside a scrollable container', + description: 'Using scrollIntoView(...) rocks!', + target: () => document.querySelector('#step-2'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-2a', + title: 'Step 2a. Inside an Iframe container', + description: 'It calculates the offset rect correctly. Thanks to floating UI!', + target: () => { + const [frameEl] = Array.from(frames) + return frameEl?.document.querySelector('#step-2a') + }, + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-3', + title: 'Step 3. Normal scrolling', + description: 'The new world is a great place', + target: () => document.querySelector('#step-3'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'tooltip', + id: 'step-4', + title: 'Step 4. Close to bottom', + description: 'So nice to see the scrolling works!', + target: () => document.querySelector('#step-4'), + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + type: 'dialog', + id: 'step-5', + title: "You're all sorted! (no target)", + description: 'Thanks for trying out the tour. Enjoy the app!', + actions: [{ label: 'Finish', action: 'dismiss' }], + }, +] diff --git a/packages/vue/src/components/tour/examples/tour.vue b/packages/vue/src/components/tour/examples/tour.vue new file mode 100644 index 0000000000..b2706308b9 --- /dev/null +++ b/packages/vue/src/components/tour/examples/tour.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/vue/src/components/tour/index.ts b/packages/vue/src/components/tour/index.ts new file mode 100644 index 0000000000..8b6c9c007d --- /dev/null +++ b/packages/vue/src/components/tour/index.ts @@ -0,0 +1,80 @@ +export type { StepDetails as TourStepDetails } from '@zag-js/tour' +export { + default as TourActionTrigger, + type TourActionTriggerBaseProps, + type TourActionTriggerProps, +} from './tour-action-trigger.vue' +export { + default as TourActions, + type TourActionsProps, +} from './tour-actions.vue' +export { + default as TourArrowTip, + type TourArrowTipBaseProps, + type TourArrowTipProps, +} from './tour-arrow-tip.vue' +export { + default as TourArrow, + type TourArrowBaseProps, + type TourArrowProps, +} from './tour-arrow.vue' +export { + default as TourBackdrop, + type TourBackdropBaseProps, + type TourBackdropProps, +} from './tour-backdrop.vue' +export { + default as TourCloseTrigger, + type TourCloseTriggerBaseProps, + type TourCloseTriggerProps, +} from './tour-close-trigger.vue' +export { + default as TourContent, + type TourContentBaseProps, + type TourContentProps, +} from './tour-content.vue' +export { + default as TourContext, + type TourContextProps, +} from './tour-context.vue' +export { + default as TourControl, + type TourControlBaseProps, + type TourControlProps, +} from './tour-control.vue' +export { + default as TourDescription, + type TourDescriptionBaseProps, + type TourDescriptionProps, +} from './tour-description.vue' +export { + default as TourPositioner, + type TourPositionerBaseProps, + type TourPositionerProps, +} from './tour-positioner.vue' +export { + default as TourProgressText, + type TourProgressTextBaseProps, + type TourProgressTextProps, +} from './tour-progress-text.vue' +export { + default as TourRoot, + type TourRootBaseProps, + type TourRootEmits, + type TourRootProps, +} from './tour-root.vue' +export { + default as TourSpotlight, + type TourSpotlightBaseProps, + type TourSpotlightProps, +} from './tour-spotlight.vue' +export { + default as TourTitle, + type TourTitleBaseProps, + type TourTitleProps, +} from './tour-title.vue' +export { tourAnatomy } from './tour.anatomy' +export { useTour, type UseTourProps, type UseTourReturn } from './use-tour' +export { useTourContext, type UseTourContext } from './use-tour-context' + +export * as Tour from './tour' diff --git a/packages/vue/src/components/tour/tour-action-trigger.vue b/packages/vue/src/components/tour/tour-action-trigger.vue new file mode 100644 index 0000000000..501c1ef71b --- /dev/null +++ b/packages/vue/src/components/tour/tour-action-trigger.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-actions.vue b/packages/vue/src/components/tour/tour-actions.vue new file mode 100644 index 0000000000..10bb49e05b --- /dev/null +++ b/packages/vue/src/components/tour/tour-actions.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-arrow-tip.vue b/packages/vue/src/components/tour/tour-arrow-tip.vue new file mode 100644 index 0000000000..985420ed63 --- /dev/null +++ b/packages/vue/src/components/tour/tour-arrow-tip.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-arrow.vue b/packages/vue/src/components/tour/tour-arrow.vue new file mode 100644 index 0000000000..9f031764a1 --- /dev/null +++ b/packages/vue/src/components/tour/tour-arrow.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-backdrop.vue b/packages/vue/src/components/tour/tour-backdrop.vue new file mode 100644 index 0000000000..bf01c3106b --- /dev/null +++ b/packages/vue/src/components/tour/tour-backdrop.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-close-trigger.vue b/packages/vue/src/components/tour/tour-close-trigger.vue new file mode 100644 index 0000000000..b13749b9b7 --- /dev/null +++ b/packages/vue/src/components/tour/tour-close-trigger.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-content.vue b/packages/vue/src/components/tour/tour-content.vue new file mode 100644 index 0000000000..49eacbab8c --- /dev/null +++ b/packages/vue/src/components/tour/tour-content.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-context.vue b/packages/vue/src/components/tour/tour-context.vue new file mode 100644 index 0000000000..beddb57911 --- /dev/null +++ b/packages/vue/src/components/tour/tour-context.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-control.vue b/packages/vue/src/components/tour/tour-control.vue new file mode 100644 index 0000000000..5cb4358a56 --- /dev/null +++ b/packages/vue/src/components/tour/tour-control.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-description.vue b/packages/vue/src/components/tour/tour-description.vue new file mode 100644 index 0000000000..9c08014cd1 --- /dev/null +++ b/packages/vue/src/components/tour/tour-description.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-positioner.vue b/packages/vue/src/components/tour/tour-positioner.vue new file mode 100644 index 0000000000..692546ab0f --- /dev/null +++ b/packages/vue/src/components/tour/tour-positioner.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-progress-text.vue b/packages/vue/src/components/tour/tour-progress-text.vue new file mode 100644 index 0000000000..fb7b8e0d50 --- /dev/null +++ b/packages/vue/src/components/tour/tour-progress-text.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-root.vue b/packages/vue/src/components/tour/tour-root.vue new file mode 100644 index 0000000000..49106b082b --- /dev/null +++ b/packages/vue/src/components/tour/tour-root.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-spotlight.vue b/packages/vue/src/components/tour/tour-spotlight.vue new file mode 100644 index 0000000000..15388d9ccd --- /dev/null +++ b/packages/vue/src/components/tour/tour-spotlight.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour-title.vue b/packages/vue/src/components/tour/tour-title.vue new file mode 100644 index 0000000000..f4e23eab7e --- /dev/null +++ b/packages/vue/src/components/tour/tour-title.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/vue/src/components/tour/tour.anatomy.ts b/packages/vue/src/components/tour/tour.anatomy.ts new file mode 100644 index 0000000000..c7ae3bb0b8 --- /dev/null +++ b/packages/vue/src/components/tour/tour.anatomy.ts @@ -0,0 +1,3 @@ +import { anatomy } from '@zag-js/tour' + +export const tourAnatomy = anatomy.extendWith('control') diff --git a/packages/vue/src/components/tour/tour.stories.vue b/packages/vue/src/components/tour/tour.stories.vue new file mode 100644 index 0000000000..706b413780 --- /dev/null +++ b/packages/vue/src/components/tour/tour.stories.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/vue/src/components/tour/tour.ts b/packages/vue/src/components/tour/tour.ts new file mode 100644 index 0000000000..2352a78b14 --- /dev/null +++ b/packages/vue/src/components/tour/tour.ts @@ -0,0 +1,90 @@ +export type { + Point, + ProgressTextDetails, + StatusChangeDetails, + StepAction, + StepActionMap, + StepActionTriggerProps, + StepBaseDetails, + StepChangeDetails, + StepDetails, + StepEffectArgs, + StepPlacement, + StepStatus, + StepType, + WaitOptions, +} from '@zag-js/tour' +export { + default as ActionTrigger, + type TourActionTriggerBaseProps as ActionTriggerBaseProps, + type TourActionTriggerProps as ActionTriggerProps, +} from './tour-action-trigger.vue' +export { + default as Actions, + type TourActionsProps as ActionsProps, +} from './tour-actions.vue' +export { + default as Arrow, + type TourArrowBaseProps as ArrowBaseProps, + type TourArrowProps as ArrowProps, +} from './tour-arrow.vue' +export { + default as ArrowTip, + type TourArrowTipBaseProps as ArrowTipBaseProps, + type TourArrowTipProps as ArrowTipProps, +} from './tour-arrow-tip.vue' +export { + default as Backdrop, + type TourBackdropBaseProps as BackdropBaseProps, + type TourBackdropProps as BackdropProps, +} from './tour-backdrop.vue' +export { + default as CloseTrigger, + type TourCloseTriggerBaseProps as CloseTriggerBaseProps, + type TourCloseTriggerProps as CloseTriggerProps, +} from './tour-close-trigger.vue' +export { + default as Content, + type TourContentBaseProps as ContentBaseProps, + type TourContentProps as ContentProps, +} from './tour-content.vue' +export { + default as Context, + type TourContextProps as ContextProps, +} from './tour-context.vue' +export { + default as Control, + type TourControlBaseProps as ControlBaseProps, + type TourControlProps as ControlProps, +} from './tour-control.vue' +export { + default as Description, + type TourDescriptionBaseProps as DescriptionBaseProps, + type TourDescriptionProps as DescriptionProps, +} from './tour-description.vue' +export { + default as Positioner, + type TourPositionerBaseProps as PositionerBaseProps, + type TourPositionerProps as PositionerProps, +} from './tour-positioner.vue' +export { + default as ProgressText, + type TourProgressTextBaseProps as ProgressTextBaseProps, + type TourProgressTextProps as ProgressTextProps, +} from './tour-progress-text.vue' +export { + default as Root, + type TourRootBaseProps as RootBaseProps, + type TourRootProps as RootProps, + type TourRootEmits as RootEmits, +} from './tour-root.vue' +export { + default as Spotlight, + type TourSpotlightBaseProps as SpotlightBaseProps, + type TourSpotlightProps as SpotlightProps, +} from './tour-spotlight.vue' +export { + default as Title, + type TourTitleBaseProps as TitleBaseProps, + type TourTitleProps as TitleProps, +} from './tour-title.vue' diff --git a/packages/vue/src/components/tour/tour.types.ts b/packages/vue/src/components/tour/tour.types.ts new file mode 100644 index 0000000000..e259ddc34a --- /dev/null +++ b/packages/vue/src/components/tour/tour.types.ts @@ -0,0 +1,84 @@ +import type * as tour from '@zag-js/tour' + +export interface RootProps { + /** + * Whether to close the tour when the user presses the escape key + * @default true + */ + closeOnEscape?: boolean + /** + * Whether to close the tour when the user clicks outside the tour + * @default true + */ + closeOnInteractOutside?: boolean + /** + * The unique identifier of the machine. + */ + id?: string + /** + * The ids of the elements in the tour. Useful for composition. + */ + ids?: Partial<{ + content: string + title: string + description: string + positioner: string + backdrop: string + arrow: string + }> + /** + * Whether to allow keyboard navigation (right/left arrow keys to navigate between steps) + * @default true + */ + keyboardNavigation?: boolean + /** + * Prevents interaction with the rest of the page while the tour is open + * @default false + */ + preventInteraction?: boolean + /** + * The offsets to apply to the spotlight + * @default "{ x: 10, y: 10 }" + */ + spotlightOffset?: tour.Point + /** + * The radius of the spotlight clip path + * @default 4 + */ + spotlightRadius?: number + /** + * The id of the currently highlighted step + */ + stepId?: string + /** + * The steps of the tour + */ + steps?: tour.StepDetails[] + /** + * The translations for the tour + */ + translations?: tour.IntlTranslations +} + +export type RootEmits = { + /** + * Function called when the focus is moved outside the component + */ + focusOutside: [event: tour.FocusOutsideEvent] + /** + * Function called when an interaction happens outside the component + */ + interactOutside: [event: tour.InteractOutsideEvent] + /** + * Function called when the pointer is pressed down outside the component + */ + pointerDownOutside: [event: tour.PointerDownOutsideEvent] + /** + * Callback when the tour is opened or closed + */ + statusChange: [details: tour.StatusChangeDetails] + /** + * Callback when the highlighted step changes + */ + stepChange: [details: tour.StepChangeDetails] +} diff --git a/packages/vue/src/components/tour/use-tour-context.ts b/packages/vue/src/components/tour/use-tour-context.ts new file mode 100644 index 0000000000..f2c574dfde --- /dev/null +++ b/packages/vue/src/components/tour/use-tour-context.ts @@ -0,0 +1,6 @@ +import { createContext } from '../../utils' +import type { UseTourReturn } from './use-tour' + +export interface UseTourContext extends UseTourReturn {} + +export const [TourProvider, useTourContext] = createContext('TourContext') diff --git a/packages/vue/src/components/tour/use-tour.ts b/packages/vue/src/components/tour/use-tour.ts new file mode 100644 index 0000000000..6460a52413 --- /dev/null +++ b/packages/vue/src/components/tour/use-tour.ts @@ -0,0 +1,32 @@ +import * as tour from '@zag-js/tour' +import { type PropTypes, normalizeProps, useMachine } from '@zag-js/vue' +import { type ComputedRef, computed, useId } from 'vue' +import { DEFAULT_LOCALE, useEnvironmentContext, useLocaleContext } from '../../providers' +import type { EmitFn, Optional } from '../../types' +import { cleanProps } from '../../utils' +import type { RootEmits } from './tour' + +export interface UseTourProps extends Optional, 'id'> {} +export interface UseTourReturn extends ComputedRef> {} + +export const useTour = (props: UseTourProps = {}, emit?: EmitFn) => { + const id = useId() + const env = useEnvironmentContext() + const locale = useLocaleContext(DEFAULT_LOCALE) + + const context = computed(() => ({ + id, + dir: locale.value.dir, + getRootNode: env?.value.getRootNode, + onFocusOutside: (details) => emit?.('focusOutside', details), + onInteractOutside: (details) => emit?.('interactOutside', details), + onPointerDownOutside: (details) => emit?.('pointerDownOutside', details), + onStatusChange: (details) => emit?.('statusChange', details), + onStepChange: (details) => emit?.('stepChange', details), + ...cleanProps(props), + })) + + const [state, send] = useMachine(tour.machine(context.value), { context }) + + return computed(() => tour.connect(state.value, send, normalizeProps)) +} diff --git a/website/panda.config.ts b/website/panda.config.ts index 99ad1630e9..7ab8297e19 100644 --- a/website/panda.config.ts +++ b/website/panda.config.ts @@ -3,6 +3,8 @@ import { createPreset } from '@park-ui/panda-preset' import sand from '@park-ui/panda-preset/colors/sand' import typographyPreset from 'pandacss-preset-typography' import { coral } from '~/coral' +import { field } from '~/theme/recipes/field' +import { tour } from '~/theme/recipes/tour' export default defineConfig({ preflight: true, @@ -58,6 +60,9 @@ export default defineConfig({ flexGrow: '1', fontFamily: 'body', }, + '*::selection': { + bg: 'gray.3', + }, pre: { background: 'transparent!', overflowX: 'auto', @@ -96,26 +101,8 @@ export default defineConfig({ }, }, slotRecipes: { - field: { - className: 'field', - slots: ['root', 'label', 'input', 'textarea', 'select', 'helperText', 'errorText'], - base: { - root: { - display: 'flex', - flexDirection: 'column', - gap: '1.5', - }, - label: { - color: 'fg.default', - fontWeight: 'medium', - textStyle: 'sm', - }, - helperText: { - color: 'fg.muted', - textStyle: 'sm', - }, - }, - }, + field, + tour, layout: { className: 'layout', slots: ['aside', 'main'], diff --git a/website/src/components/navigation/navbar.tsx b/website/src/components/navigation/navbar.tsx index 8cc9e99eb6..2bfcdbfd5c 100644 --- a/website/src/components/navigation/navbar.tsx +++ b/website/src/components/navigation/navbar.tsx @@ -26,7 +26,9 @@ export const Navbar = () => { - +
+ +
diff --git a/website/src/components/navigation/sidebar-container.tsx b/website/src/components/navigation/sidebar-container.tsx index b5b8a221df..342808a1ad 100644 --- a/website/src/components/navigation/sidebar-container.tsx +++ b/website/src/components/navigation/sidebar-container.tsx @@ -22,7 +22,9 @@ export const SidebarContainer = async (props: PropsWithChildren) => { - +
+ +
{props.children} diff --git a/website/src/components/ui/primitives/tour.tsx b/website/src/components/ui/primitives/tour.tsx new file mode 100644 index 0000000000..6ea08f8f3c --- /dev/null +++ b/website/src/components/ui/primitives/tour.tsx @@ -0,0 +1,76 @@ +'use client' +import type { Assign } from '@ark-ui/react' +import { Tour } from '@ark-ui/react/tour' +import { type TourVariantProps, tour } from 'styled-system/recipes' +import type { ComponentProps, HTMLStyledProps } from 'styled-system/types' +import { createStyleContext } from '~/lib/create-style-context' + +const { withRootProvider, withContext } = createStyleContext(tour) + +export type RootProps = ComponentProps +export const Root = withRootProvider>(Tour.Root) + +export const Backdrop = withContext< + HTMLDivElement, + Assign, Tour.BackdropBaseProps> +>(Tour.Backdrop, 'backdrop') + +export const Spotlight = withContext< + HTMLDivElement, + Assign, Tour.SpotlightBaseProps> +>(Tour.Spotlight, 'spotlight') + +export const CloseTrigger = withContext< + HTMLButtonElement, + Assign, Tour.CloseTriggerBaseProps> +>(Tour.CloseTrigger, 'closeTrigger') + +export const Content = withContext< + HTMLDivElement, + Assign, Tour.ContentBaseProps> +>(Tour.Content, 'content') + +export const Description = withContext< + HTMLDivElement, + Assign, Tour.DescriptionBaseProps> +>(Tour.Description, 'description') + +export const Positioner = withContext< + HTMLDivElement, + Assign, Tour.PositionerBaseProps> +>(Tour.Positioner, 'positioner') + +export const Title = withContext< + HTMLHeadingElement, + Assign, Tour.TitleBaseProps> +>(Tour.Title, 'title') + +export const Arrow = withContext< + HTMLDivElement, + Assign, Tour.ArrowBaseProps> +>(Tour.Arrow, 'arrow') + +export const ArrowTip = withContext< + HTMLDivElement, + Assign, Tour.ArrowTipBaseProps> +>(Tour.ArrowTip, 'arrowTip') + +export const ProgressText = withContext< + HTMLDivElement, + Assign, Tour.ProgressTextBaseProps> +>(Tour.ProgressText, 'progressText') + +export const ActionTrigger = withContext< + HTMLButtonElement, + Assign, Tour.ActionTriggerBaseProps> +>(Tour.ActionTrigger, 'actionTrigger') + +export const Control = withContext< + HTMLDivElement, + Assign, Tour.ControlBaseProps> +>(Tour.Control, 'control') + +export const Context = Tour.Context +export const Actions = Tour.Actions + +export type StepDetails = Tour.StepDetails diff --git a/website/src/components/ui/tour.tsx b/website/src/components/ui/tour.tsx new file mode 100644 index 0000000000..9ea14bb9c4 --- /dev/null +++ b/website/src/components/ui/tour.tsx @@ -0,0 +1,2 @@ +export { useTour } from '@ark-ui/react/tour' +export * as Tour from './primitives/tour' diff --git a/website/src/content/pages/components/tour.mdx b/website/src/content/pages/components/tour.mdx new file mode 100644 index 0000000000..13793f8d38 --- /dev/null +++ b/website/src/content/pages/components/tour.mdx @@ -0,0 +1,211 @@ +--- +id: tour +title: Tour +description: A guided tour that helps users understand the interface. +status: new +--- + + + +## Features + +- Support for different step types such as "dialog", "floating", "tooltip" or "wait" +- Support for customizable content per step +- Wait steps for waiting for a specific selector to appear on the page before showing the next step +- Flexible positioning of the tour dialog per step +- Progress tracking shows users their progress through the tour + +## Anatomy + +To set up the tour correctly, it's essential to understand its anatomy and the naming of its parts. + +> Each part includes a `data-part` attribute to help identify them in the DOM. + + + + + + + +## Steps + + +### Using step types + +The tour machine supports different types of steps, allowing you to create a +diverse and interactive tour experience. The available step types are defined in +the `StepType` type: + +- `tooltip`: Displays the step content as a tooltip, typically positioned near + the target element. + +- `dialog`: Shows the step content in a modal dialog centered on screen, + useful for starting or ending the tour. This usually don't have a `target` + defined. + +- `floating`: Presents the step content as a floating element, which can be + positioned flexibly on the screen. This usually don't have a `target` defined. + +- `wait`: A special type that waits for a specific condition before proceeding + to the next step. + + +```tsx +const steps: TourStepDetails[] = [ + { + id: "step-1", + type: "tooltip", + placement: "top-start", + target: () => document.querySelector("#target-1"), + title: "Tooltip Step", + description: "This is a tooltip step", + }, + { + id: "step-2", + type: "dialog", + title: "Dialog Step", + description: "This is a dialog step", + }, + { + id: "step-3", + type: "floating", + placement: "top-start", + title: "Floating Step", + description: "This is a floating step", + }, + { + id: "step-4", + type: "wait", + title: "Wait Step", + description: "This is a wait step", + effect({ next }) { + // do something and go next + // you can also return a cleanup + }, + }, +] +``` + +### Configuring actions + +Every step supports a list of actions that are rendered in the step footer.Use +the `actions` property to define each action. + +```tsx +const steps: TourStepDetails[] = [ + { + id: "step-1", + type: "dialog", + title: "Dialog Step", + description: "This is a dialog step", + actions: [{ label: "Show me a tour!", action: "next" }], + }, +] +``` + +### Changing tooltip placement + +Use the `placement` property to define the placement of the tooltip. + +```tsx {5} +const steps: TourStepDetails[] = [ + { + id: "step-1", + type: "tooltip", + placement: "top-start", + // ... + }, +] +``` + +### Hiding the arrow + +Set `arrow: false` in the step property to hide the tooltip arrow. This is only +useful for tooltip steps. + +```tsx {5} +const steps: TourStepDetails[] = [ + { + id: "step-1", + type: "tooltip", + arrow: false, + }, +] +``` + +### Hiding the backdrop + +Set `backdrop: false` in the step property to hide the backdrop. This applies to +all step types except the `wait` step. + +```tsx {5} +const steps: TourStepDetails[] = [ + { + id: "step-1", + type: "dialog", + backdrop: false, + }, +] +``` + +### Step Effects + +Step effects are functions that are called before a step is opened. They are +useful for adding custom logic to a step. + +This function provides the following methods: + +- `next()`: Call this method to move to the next step. +- `show()`: Call this method to show the current step. +- `update(details: StepDetails)`: Call this method to update the details of the + current step (say, after data has been fetched). + +```tsx +const steps: TourStepDetails[] = [ + { + id: "step-1", + type: "tooltip", + effect({ next, show, update }) { + fetchData().then((res) => { + // update the step details + update({ title: res.title }) + // then show show the step + show() + }) + + return () => { + // cleanup fetch data + } + }, + }, +] +``` + +### Wait Steps + +Wait steps are useful when you need to wait for a specific condition before +proceeding to the next step. + +Use the step `effect` function to perform an action and then call `next()` to +move to the next step. + +> **Note:** You cannot call `show()` in a wait step. + +```tsx +const steps: TourStepDetails[] = [ + { + id: "step-1", + type: "wait", + effect({ next }) { + const button = document.querySelector("#button") + const listener = () => next() + button.addEventListener("click", listener) + return () => button.removeEventListener("click", listener) + }, + }, +] +``` + +## API Reference + + \ No newline at end of file diff --git a/website/src/content/types/react/tour.types.json b/website/src/content/types/react/tour.types.json new file mode 100644 index 0000000000..95f4b71c98 --- /dev/null +++ b/website/src/content/types/react/tour.types.json @@ -0,0 +1,145 @@ +{ + "ActionTrigger": { + "props": { + "action": { "type": "StepAction", "isRequired": true }, + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLButtonElement" + }, + "Arrow": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLDivElement" + }, + "ArrowTip": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLDivElement" + }, + "Backdrop": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLDivElement" + }, + "CloseTrigger": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLButtonElement" + }, + "Content": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLDivElement" + }, + "Description": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLDivElement" + }, + "Positioner": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLDivElement" + }, + "ProgressText": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLDivElement" + }, + "Root": { + "props": { + "tour": { "type": "UseTourReturn", "isRequired": true }, + "immediate": { + "type": "boolean", + "isRequired": false, + "description": "Whether to synchronize the present change immediately or defer it to the next frame" + }, + "lazyMount": { + "type": "boolean", + "defaultValue": "false", + "isRequired": false, + "description": "Whether to enable lazy mounting" + }, + "onExitComplete": { + "type": "() => void", + "isRequired": false, + "description": "Function called when the animation ends in the closed state" + }, + "present": { + "type": "boolean", + "isRequired": false, + "description": "Whether the node is present (controlled by the user)" + }, + "unmountOnExit": { + "type": "boolean", + "defaultValue": "false", + "isRequired": false, + "description": "Whether to unmount on exit." + } + } + }, + "Spotlight": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLDivElement" + }, + "Title": { + "props": { + "asChild": { + "type": "boolean", + "isRequired": false, + "description": "Use the provided child element as the default rendered element, combining their props and behavior." + } + }, + "element": "HTMLHeadingElement" + } +} diff --git a/website/src/demos/index.ts b/website/src/demos/index.ts index 656138df18..135184b670 100644 --- a/website/src/demos/index.ts +++ b/website/src/demos/index.ts @@ -44,4 +44,5 @@ export { Demo as Toast } from './toast.demo' export { Demo as ToggleGroup } from './toggle-group.demo' export { Demo as Toggle } from './toggle.demo' export { Demo as Tooltip } from './tooltip.demo' +export { Demo as Tour } from './tour.demo' export { Demo as TreeView } from './tree-view.demo' diff --git a/website/src/demos/tour.demo.tsx b/website/src/demos/tour.demo.tsx new file mode 100644 index 0000000000..af857cdb12 --- /dev/null +++ b/website/src/demos/tour.demo.tsx @@ -0,0 +1,85 @@ +'use client' +import { Portal } from '@ark-ui/react/portal' +import { XIcon } from 'lucide-react' +import { Button } from '~/components/ui/button' +import { IconButton } from '~/components/ui/icon-button' +import { Tour, useTour } from '~/components/ui/tour' + +export const Demo = () => { + const tour = useTour({ steps }) + + return ( + <> + + + + {/* + */} + + + + + + + + + + + + + + + + {(actions) => + actions.map((action) => ( + + + + )) + } + + + + + + + + ) +} + +const steps: Tour.StepDetails[] = [ + { + id: 'step-1', + type: 'dialog', + title: 'Step 1', + description: 'This is the first step', + actions: [{ label: 'Next', action: 'next' }], + }, + { + id: 'step-2', + type: 'tooltip', + title: 'Step 2. Inside a scrollable container', + description: 'Using scrollIntoView(...) rocks!', + target: () => document.querySelector('#framework-select'), + backdrop: false, + arrow: false, + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Next', action: 'next' }, + ], + }, + { + id: 'step-3', + type: 'tooltip', + title: 'Step 2. Inside a scrollable container', + description: 'Using scrollIntoView(...) rocks!', + target: () => document.querySelector('#version-select'), + backdrop: false, + actions: [ + { label: 'Prev', action: 'prev' }, + { label: 'Done', action: 'dismiss' }, + ], + }, +] diff --git a/website/src/theme/recipes/field.ts b/website/src/theme/recipes/field.ts new file mode 100644 index 0000000000..13dc391346 --- /dev/null +++ b/website/src/theme/recipes/field.ts @@ -0,0 +1,22 @@ +import { defineSlotRecipe } from '@pandacss/dev' + +export const field = defineSlotRecipe({ + className: 'field', + slots: ['helperText', 'label', 'root'], + base: { + root: { + display: 'flex', + flexDirection: 'column', + gap: '1.5', + }, + label: { + color: 'fg.default', + fontWeight: 'medium', + textStyle: 'sm', + }, + helperText: { + color: 'fg.muted', + textStyle: 'sm', + }, + }, +}) diff --git a/website/src/theme/recipes/tour.ts b/website/src/theme/recipes/tour.ts new file mode 100644 index 0000000000..e9e1303fc9 --- /dev/null +++ b/website/src/theme/recipes/tour.ts @@ -0,0 +1,83 @@ +import { defineSlotRecipe } from '@pandacss/dev' + +export const tour = defineSlotRecipe({ + className: 'tour', + slots: [ + 'actionTrigger', + 'arrow', + 'arrowTip', + 'backdrop', + 'closeTrigger', + 'content', + 'control', + 'description', + 'positioner', + 'progressText', + 'spotlight', + 'title', + ], + base: { + backdrop: { + backdropFilter: 'blur(4px)', + background: { + _light: 'white.a10', + _dark: 'black.a10', + }, + height: '100vh', + left: '0', + position: 'fixed', + top: '0', + width: '100vw', + zIndex: 'overlay', + _open: { + animation: 'backdrop-in', + }, + _closed: { + animation: 'backdrop-out', + }, + }, + content: { + position: 'relative', + background: 'bg.default', + borderRadius: 'l3', + boxShadow: 'lg', + display: 'flex', + flexDirection: 'column', + maxWidth: 'sm', + p: '4', + }, + positioner: { + alignItems: 'center', + display: 'flex', + justifyContent: 'center', + overflow: 'auto', + position: 'fixed', + inset: '0', + }, + control: { + display: 'flex', + gap: '3', + }, + arrow: { + '--arrow-size': 'var(--sizes-3)', + '--arrow-background': 'var(--colors-bg-default)', + }, + arrowTip: { + borderTopWidth: '1px', + borderLeftWidth: '1px', + }, + title: { + fontWeight: 'medium', + textStyle: 'sm', + }, + description: { + color: 'fg.muted', + textStyle: 'sm', + }, + closeTrigger: { + position: 'absolute', + top: '3', + right: '3', + }, + }, +})