diff --git a/bun.lockb b/bun.lockb index 0d17016f35..865d73fbdd 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 dc463aeaca..9200eb89f8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -213,6 +213,7 @@ "@zag-js/toast": "0.79.1", "@zag-js/toggle-group": "0.79.1", "@zag-js/tooltip": "0.79.1", + "@zag-js/tour": "0.79.1", "@zag-js/tree-view": "0.79.1", "@zag-js/types": "0.79.1" }, 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..7d62ce564f --- /dev/null +++ b/packages/react/src/components/tour/examples/tour.tsx @@ -0,0 +1,38 @@ +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 + useEffect(() => { + tour.start() + }, [tour]) + + 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..2188bb1451 --- /dev/null +++ b/packages/react/src/components/tour/index.ts @@ -0,0 +1,51 @@ +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 { + 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..fd844a4996 --- /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 ( + + {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..b4896b747d --- /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: (context: 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..b434fd7bf7 --- /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 +}) + +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..6286f579ca --- /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) { + 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-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..6651dea3bc --- /dev/null +++ b/packages/react/src/components/tour/tour-progress-text.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 TourProgressTextBaseProps extends PolymorphicProps {} +export interface TourProgressTextProps extends HTMLProps<'div'>, TourProgressTextBaseProps {} + +export const TourProgressText = forwardRef((props, ref) => { + const tour = useTourContext() + const presence = usePresenceContext() + const mergedProps = mergeProps(tour.getProgressTextProps(), presence.getPresenceProps(), props) + + if (presence.unmounted) { + return null + } + + 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..6bbb3bffb7 --- /dev/null +++ b/packages/react/src/components/tour/tour.anatomy.ts @@ -0,0 +1 @@ +export { anatomy as tourAnatomy } from '@zag-js/tour' 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..3848c00866 --- /dev/null +++ b/packages/react/src/components/tour/tour.ts @@ -0,0 +1,68 @@ +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 { + 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/package.json b/packages/solid/package.json index c0b83d2094..508635ff5d 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.79.1", "@zag-js/toggle-group": "0.79.1", "@zag-js/tooltip": "0.79.1", + "@zag-js/tour": "0.79.1", "@zag-js/tree-view": "0.79.1", "@zag-js/types": "0.79.1" }, diff --git a/packages/vue/package.json b/packages/vue/package.json index 5b60b2a31d..eee0f1a4a9 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.79.1", "@zag-js/toggle-group": "0.79.1", "@zag-js/tooltip": "0.79.1", + "@zag-js/tour": "0.79.1", "@zag-js/tree-view": "0.79.1", "@zag-js/types": "0.79.1", "@zag-js/utils": "0.79.1",