From c129d15a42e857e3ae8f15eb06280f010f480f3b Mon Sep 17 00:00:00 2001 From: robertlinde Date: Mon, 12 Oct 2026 20:09:05 +0200 Subject: [PATCH 1/2] feat: loading animation --- CHANGELOG.md | 6 ++++ README.md | 2 +- .../layer/native-renderer.tsx | 35 +++++++++++++++++-- src/react-platform/layer/styles.ts | 6 ++++ src/react-platform/layer/web-renderer.tsx | 4 ++- src/shared/renderer/motion.ts | 7 +++- src/shared/renderer/renderer.type.ts | 2 ++ src/shared/renderer/tour-overlay.tsx | 9 ++++- test/support/tour-behavior.suite.tsx | 20 +++++++++++ 9 files changed, 85 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f53eb..129da16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Releases are published automatically from `main`; each GitHub release lists its commits. This file describes the larger changes. +## 1.0.4 + +### Fixed + +- When the next step takes a moment to prepare (`onBeforeStep` loading data, a target that is still rendering), the tooltip now shows that the tour is working: it dims, on the web with a progress cursor. Before, it stayed unchanged and Next seemed to do nothing until the step appeared. Applies to the built-in and custom tooltips, on the web and React Native. + ## 1.0.3 ### Fixed diff --git a/README.md b/README.md index 8952685..4b0a912 100644 --- a/README.md +++ b/README.md @@ -573,7 +573,7 @@ The tour is rendered as a modal `` in the browser's top layer, so it is ## What the tour does for you - **Keeps everything in sight.** Before a step is shown, its target is scrolled into view — through every scroll container around it, including horizontal ones — with room for the tooltip. Elements that stay put while the page scrolls (sticky headers, cookie banners, floating buttons; any number, any shape) are detected and the target is scrolled clear of them, in both directions. Targets larger than the screen are aligned to their start. The tooltip is placed inside the visible screen minus safe areas and the on-screen keyboard, flips to another side when there is no room, and scrolls its own content if it is taller than the screen. When the window is resized or rotated, or your app or an animation moves the target, the tour follows. -- **Waits instead of guessing.** Targets that are not rendered yet (lazy pages, animations, data loading) are awaited, event-driven, for up to 5 seconds. +- **Waits instead of guessing.** Targets that are not rendered yet (lazy pages, animations, data loading) are awaited, event-driven, for up to 5 seconds. When the next step takes a moment, the tooltip dims to show that the tour is working; presses wait until the step is there. - **Animates.** The tour fades in, the highlight glides from target to target together with the scroll, and the tooltip enters from the side it is on. On React Native every animation runs on the UI thread, so it stays smooth while JavaScript is busy. With reduced motion turned on in the system, movement is replaced by short fades. - **Is accessible.** On the web the tour is a modal dialog (`aria-modal`, labelled by the step title, described by its content): focus moves into the tooltip and back to where it was when the tour ends, the rest of the page is inert, arrow keys work (but leave text fields, sliders, radio groups and other controls in a custom tooltip alone), Esc closes the tour, a long tooltip scrolls with the keyboard, and step changes are announced. Buttons have accessible names and large enough touch targets. In Windows high-contrast mode the highlight is drawn as an outline. On React Native, screen-reader focus moves to the tooltip, the app behind it is hidden from screen readers (in a `TourHost`, see [Modal screens](#modal-screens)), and the back button closes the tour. - **Costs nothing while idle.** Right after a step appears, the tour checks its target every frame until it has stayed put for a second (three at most), because animations can move it without any event (a React Native screen sliding in, for example). After that, while nothing moves, the tour does no work at all: no timers, no measuring, no re-rendering. Scrolling and resizing are followed once per frame, without re-rendering your app. `useTour()` only re-renders components when the tour's state changes. diff --git a/src/react-native-platform/layer/native-renderer.tsx b/src/react-native-platform/layer/native-renderer.tsx index 3f11831..11cdfce 100644 --- a/src/react-native-platform/layer/native-renderer.tsx +++ b/src/react-native-platform/layer/native-renderer.tsx @@ -421,14 +421,45 @@ function Spotlight({hole, transition, theme}: SpotlightProps): ReactNode { // --------------------------------------------------------------------------------------------- /** Positions the tooltip with native-driven transforms, plays its entrance, reports its size and takes accessibility focus. */ -function TooltipFrame({children, layout, entrance, isBusy, isHidden, onSize, onIntent}: TooltipFrameProps): ReactNode { +function TooltipFrame({ + children, + layout, + entrance, + isBusy, + isWaiting, + isHidden, + onSize, + onIntent, +}: TooltipFrameProps): ReactNode { const [values] = useState(() => ({ x: new Animated.Value(layout.x), y: new Animated.Value(layout.y), progress: new Animated.Value(0), + contentOpacity: new Animated.Value(1), })); const containerRef = useRef | null>(null); + // The next step takes a moment to prepare: dim the content, the tour is working. Nothing runs + // until the first wait. + const hasWaited = useRef(false); + useEffect(() => { + if (!isWaiting && !hasWaited.current) { + return undefined; + } + + hasWaited.current = true; + const animation = Animated.timing(values.contentOpacity, { + toValue: isWaiting ? motion.waitingOpacity : 1, + duration: entrance.durationMs, + easing: standardEasing, + useNativeDriver, + }); + animation.start(); + return (): void => { + animation.stop(); + }; + }, [values, isWaiting, entrance.durationMs]); + // Follow layout changes instantly. useIsomorphicLayoutEffect(() => { values.x.setValue(layout.x); @@ -506,7 +537,7 @@ function TooltipFrame({children, layout, entrance, isBusy, isHidden, onSize, onI onSize({width, height}); }} > - {children} + {children} ); diff --git a/src/react-platform/layer/styles.ts b/src/react-platform/layer/styles.ts index 022fb74..2487906 100644 --- a/src/react-platform/layer/styles.ts +++ b/src/react-platform/layer/styles.ts @@ -82,6 +82,12 @@ export const tourCss = ` .rtk-tooltip[data-entering] { animation: rtk-enter var(--rtk-enter-duration, 200ms) var(--rtk-ease) var(--rtk-enter-delay, 0ms) both; } +/* The next step takes a moment to prepare: the tour is working, presses wait. */ +.rtk-tooltip[data-waiting] > * { + opacity: var(--rtk-waiting-opacity, 0.6); + transition: opacity var(--rtk-fade-duration, 200ms) var(--rtk-ease); +} +.rtk-tooltip[data-waiting], .rtk-tooltip[data-waiting] * { cursor: progress !important; } .rtk-measure { position: fixed !important; top: 0 !important; diff --git a/src/react-platform/layer/web-renderer.tsx b/src/react-platform/layer/web-renderer.tsx index 4b27bbe..91dd65d 100644 --- a/src/react-platform/layer/web-renderer.tsx +++ b/src/react-platform/layer/web-renderer.tsx @@ -387,7 +387,7 @@ function overflowsFrame(frame: HTMLElement): boolean { } /** Positions the tooltip (with `translate`, so the entrance's `transform` never moves it), reports its size and takes focus. */ -function TooltipFrame({children, layout, entrance, isBusy, isHidden, onSize}: TooltipFrameProps): ReactNode { +function TooltipFrame({children, layout, entrance, isBusy, isWaiting, isHidden, onSize}: TooltipFrameProps): ReactNode { const ref = useRef(null); const [isEntering, setIsEntering] = useState(true); const [isOverflowing, setIsOverflowing] = useState(false); @@ -447,6 +447,7 @@ function TooltipFrame({children, layout, entrance, isBusy, isHidden, onSize}: To '--rtk-enter-scale': String(entrance.scale), '--rtk-enter-duration': `${entrance.durationMs}ms`, '--rtk-enter-delay': `${entrance.delayMs}ms`, + '--rtk-waiting-opacity': String(motion.waitingOpacity), }; return ( @@ -457,6 +458,7 @@ function TooltipFrame({children, layout, entrance, isBusy, isHidden, onSize}: To data-placement={layout.placement} data-entering={isEntering ? '' : undefined} data-hidden={isHidden ? '' : undefined} + data-waiting={isWaiting ? '' : undefined} data-overflowing={isOverflowing ? '' : undefined} aria-busy={isBusy ? 'true' : undefined} tabIndex={-1} diff --git a/src/shared/renderer/motion.ts b/src/shared/renderer/motion.ts index bf9ebd9..952c548 100644 --- a/src/shared/renderer/motion.ts +++ b/src/shared/renderer/motion.ts @@ -17,8 +17,13 @@ export const motion = { /** Distance (from the target's side) and scale the tooltip enters from. */ enterOffset: 6, enterScale: 0.96, - /** If the first step takes longer than this to prepare (navigation, data), the screen is dimmed meanwhile. */ + /** + * If a step takes longer than this to prepare (navigation, data), the tour shows that it is + * working: before the first step the screen is dimmed, later the tooltip is. + */ waitingDimDelayMs: 300, + /** Opacity of the tooltip meanwhile. */ + waitingOpacity: 0.6, /** Control points of the standard easing curve (`cubic-bezier`). */ easing: [0.2, 0, 0, 1], } as const; diff --git a/src/shared/renderer/renderer.type.ts b/src/shared/renderer/renderer.type.ts index d320e14..de3c03e 100644 --- a/src/shared/renderer/renderer.type.ts +++ b/src/shared/renderer/renderer.type.ts @@ -89,6 +89,8 @@ export type TooltipFrameProps = { readonly entrance: Entrance; /** A step change is being prepared: ignore presses. */ readonly isBusy: boolean; + /** The step change has taken a moment: show that the tour is working (dim the tooltip to `motion.waitingOpacity`). */ + readonly isWaiting: boolean; /** The next target is being scrolled into view: hide the tooltip (keep its size). */ readonly isHidden: boolean; readonly onSize: (size: Size) => void; diff --git a/src/shared/renderer/tour-overlay.tsx b/src/shared/renderer/tour-overlay.tsx index 1b77ab2..fe93c7e 100644 --- a/src/shared/renderer/tour-overlay.tsx +++ b/src/shared/renderer/tour-overlay.tsx @@ -108,6 +108,9 @@ function OverlayContent({ visibleSince.current = isVisible ? Date.now() : undefined; }, [isVisible]); const spotlight = useSpotlight(view, isVisible, isReducedMotion); + // Presses are ignored while the next step is prepared; when that takes a moment, the tooltip shows it. + const isBusy = phase === 'preparing' && !isClosing; + const isWaiting = useDelayedFlag(isBusy && shown !== undefined, motion.waitingDimDelayMs); const announcement = useAnnouncement(shown, totalSteps, i18n, isClosing); // Fade out, then unmount. @@ -199,7 +202,8 @@ function OverlayContent({ Tooltip={Tooltip} shown={shown} totalSteps={totalSteps} - isBusy={phase === 'preparing' && !isClosing} + isBusy={isBusy} + isWaiting={isWaiting} isHidden={phase === 'revealing'} isDelayed={spotlight.isMorphing} isReducedMotion={isReducedMotion} @@ -230,6 +234,7 @@ type ShownTooltipProps = { readonly shown: ShownStep; readonly totalSteps: number; readonly isBusy: boolean; + readonly isWaiting: boolean; readonly isHidden: boolean; /** Enters a moment later, while the spotlight is still moving to the target. */ readonly isDelayed: boolean; @@ -247,6 +252,7 @@ function ShownTooltip({ shown, totalSteps, isBusy, + isWaiting, isHidden, isDelayed, isReducedMotion, @@ -272,6 +278,7 @@ function ShownTooltip({ layout={layout} entrance={entrance} isBusy={isBusy} + isWaiting={isWaiting} isHidden={isHidden} onSize={onSize} onIntent={onIntent} diff --git a/test/support/tour-behavior.suite.tsx b/test/support/tour-behavior.suite.tsx index 3d01770..ed0122c 100644 --- a/test/support/tour-behavior.suite.tsx +++ b/test/support/tour-behavior.suite.tsx @@ -3,6 +3,7 @@ import {afterEach, describe, expect, it, vi} from 'vitest'; import {page} from 'vitest/browser'; import type {Insets} from '../../src/shared/geometry/geometry.type.js'; import type {TourProviderBaseProps} from '../../src/shared/react/tour-provider-core.js'; +import {motion} from '../../src/shared/renderer/motion.js'; import {useTour} from '../../src/shared/react/use-tour.hook.js'; import type {TourContextType} from '../../src/shared/types/tour-context.type.js'; import type {TourEndInfo, TourEndReason} from '../../src/shared/types/tour-end-info.type.js'; @@ -235,6 +236,25 @@ export function describeTourBehavior(harness: TourBehaviorHarness): void { expect(startedTour().currentStep).toBe(1); }); + it('dims the tooltip when the next step takes a moment to prepare, and only then', async () => { + await renderScreen(); + const gate = deferred(); + startedTour().startTour([steps[0]!, {...steps[1]!, onBeforeStep: async () => gate.promise}], 'intro'); + await harness.waitForStep('First step'); + const contentOpacity = (): number => Number(getComputedStyle(harness.tooltip()!.firstElementChild!).opacity); + expect(contentOpacity()).toBe(1); + await page.getByRole('button', {name: 'Next'}).click(); + // A quick step change shows nothing. + await sleep(motion.waitingDimDelayMs / 2); + expect(contentOpacity()).toBe(1); + await vi.waitFor(() => { + expect(contentOpacity()).toBeCloseTo(motion.waitingOpacity, 2); + }); + gate.resolve(); + await harness.waitForStep('Second step'); + expect(contentOpacity()).toBe(1); + }); + it('stays open when the dimmed area is pressed with closeOnOverlayClick={false}', async () => { await renderScreen({closeOnOverlayClick: false}); startedTour().startTour(steps); From 49d793404e06fbd0f826ef0ee39716e18b89b520 Mon Sep 17 00:00:00 2001 From: robertlinde Date: Mon, 12 Oct 2026 20:59:23 +0200 Subject: [PATCH 2/2] feat: waiting state --- CHANGELOG.md | 7 +- README.md | 4 +- demos/react-native/App.tsx | 79 +++++++++++++- demos/react/src/App.css | 49 ++++++++- demos/react/src/App.tsx | 13 +++ demos/react/src/orders.ts | 49 +++++++++ demos/react/src/tour.tsx | 10 ++ .../layer/native-renderer.tsx | 71 +++++++------ src/react-platform/layer/styles.ts | 48 +++++++-- src/react-platform/layer/web-renderer.tsx | 16 ++- src/shared/renderer/motion.ts | 8 +- src/shared/renderer/renderer.type.ts | 10 +- src/shared/renderer/tour-overlay.tsx | 100 ++++++++++++------ src/shared/types/tour-i18n.type.ts | 4 +- src/shared/utils/i18n.util.ts | 2 + test/e2e/tour.spec.ts | 19 +++- test/maestro/back.yaml | 50 +++++++-- test/maestro/close.yaml | 2 +- test/maestro/tour.yaml | 10 +- test/native/tour-behavior.test.tsx | 8 +- test/support/tour-behavior.suite.tsx | 32 ++++-- test/web/tour-overlay.test.tsx | 1 + 22 files changed, 476 insertions(+), 116 deletions(-) create mode 100644 demos/react/src/orders.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 129da16..eb0b436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ Releases are published automatically from `main`; each GitHub release lists its commits. This file describes the larger changes. -## 1.0.4 +## 1.1.0 -### Fixed +### Added -- When the next step takes a moment to prepare (`onBeforeStep` loading data, a target that is still rendering), the tooltip now shows that the tour is working: it dims, on the web with a progress cursor. Before, it stayed unchanged and Next seemed to do nothing until the step appeared. Applies to the built-in and custom tooltips, on the web and React Native. +- A waiting state: when a step takes a moment to prepare (`onBeforeStep` loading data or navigating, a target that is still rendering), the highlight closes, the screen is dimmed and a spinner appears in the middle, on the web and React Native, with built-in and custom tooltips alike. Before, the previous step stayed unchanged and Next seemed to do nothing until the next one appeared. Pressing the dimmed area meanwhile leaves the tour. +- `i18n.loadingAriaLabel` (default `'Loading…'`): what screen readers announce while the tour waits, and the spinner's accessible name. ## 1.0.3 diff --git a/README.md b/README.md index 4b0a912..d3217f0 100644 --- a/README.md +++ b/README.md @@ -489,6 +489,7 @@ The defaults meet WCAG AA contrast. A dark theme only needs a dark background an stepCounterTemplate: '{current} von {total}', closeAriaLabel: 'Tour schließen', dotAriaLabelTemplate: 'Zu Schritt {step}', + loadingAriaLabel: 'Wird geladen…', }} > ``` @@ -501,6 +502,7 @@ The defaults meet WCAG AA contrast. A dark theme only needs a dark background an | `stepCounterTemplate` | `'{current} of {total}'` | | `closeAriaLabel` | `'Close tour'` | | `dotAriaLabelTemplate` | `'Go to step {step}'` | +| `loadingAriaLabel` | `'Loading…'` | ## Custom tooltip @@ -573,7 +575,7 @@ The tour is rendered as a modal `` in the browser's top layer, so it is ## What the tour does for you - **Keeps everything in sight.** Before a step is shown, its target is scrolled into view — through every scroll container around it, including horizontal ones — with room for the tooltip. Elements that stay put while the page scrolls (sticky headers, cookie banners, floating buttons; any number, any shape) are detected and the target is scrolled clear of them, in both directions. Targets larger than the screen are aligned to their start. The tooltip is placed inside the visible screen minus safe areas and the on-screen keyboard, flips to another side when there is no room, and scrolls its own content if it is taller than the screen. When the window is resized or rotated, or your app or an animation moves the target, the tour follows. -- **Waits instead of guessing.** Targets that are not rendered yet (lazy pages, animations, data loading) are awaited, event-driven, for up to 5 seconds. When the next step takes a moment, the tooltip dims to show that the tour is working; presses wait until the step is there. +- **Waits instead of guessing.** Targets that are not rendered yet (lazy pages, animations, data loading) are awaited, event-driven, for up to 5 seconds. When a step takes a moment to prepare (more than 300 ms), the tour shows that it is waiting: the highlight closes, the screen is dimmed and a spinner appears in the middle (announced to screen readers as `loadingAriaLabel`). Pressing the dimmed area meanwhile leaves the tour. - **Animates.** The tour fades in, the highlight glides from target to target together with the scroll, and the tooltip enters from the side it is on. On React Native every animation runs on the UI thread, so it stays smooth while JavaScript is busy. With reduced motion turned on in the system, movement is replaced by short fades. - **Is accessible.** On the web the tour is a modal dialog (`aria-modal`, labelled by the step title, described by its content): focus moves into the tooltip and back to where it was when the tour ends, the rest of the page is inert, arrow keys work (but leave text fields, sliders, radio groups and other controls in a custom tooltip alone), Esc closes the tour, a long tooltip scrolls with the keyboard, and step changes are announced. Buttons have accessible names and large enough touch targets. In Windows high-contrast mode the highlight is drawn as an outline. On React Native, screen-reader focus moves to the tooltip, the app behind it is hidden from screen readers (in a `TourHost`, see [Modal screens](#modal-screens)), and the back button closes the tour. - **Costs nothing while idle.** Right after a step appears, the tour checks its target every frame until it has stayed put for a second (three at most), because animations can move it without any event (a React Native screen sliding in, for example). After that, while nothing moves, the tour does no work at all: no timers, no measuring, no re-rendering. Scrolling and resizing are followed once per frame, without re-rendering your app. `useTour()` only re-renders components when the tour's state changes. diff --git a/demos/react-native/App.tsx b/demos/react-native/App.tsx index da4daa7..78f787b 100644 --- a/demos/react-native/App.tsx +++ b/demos/react-native/App.tsx @@ -1,4 +1,4 @@ -import {type ComponentRef, createContext, createRef, type ReactNode, use, useState} from 'react'; +import {type ComponentRef, createContext, createRef, type ReactNode, use, useState, useSyncExternalStore} from 'react'; import {StatusBar} from 'expo-status-bar'; import {FlatList, Pressable, ScrollView, StyleSheet, Switch, Text, View} from 'react-native'; import {SafeAreaProvider, useSafeAreaInsets} from 'react-native-safe-area-context'; @@ -49,6 +49,53 @@ function on(screen: keyof Screens, step: TourStep): TourStep { }; } +type Order = {readonly id: number; readonly table: string; readonly items: string}; +type OrdersState = {readonly orders: readonly Order[]; readonly isLoading: boolean}; + +const sampleOrders: readonly Order[] = [ + {id: 1, table: 'Table 4', items: '2 × Pasta, 1 × Salad'}, + {id: 2, table: 'Table 7', items: '3 × Pizza'}, + {id: 3, table: 'Terrace 2', items: '1 × Soup, 2 × Lemonade'}, +]; + +/** How long the demo's "server" takes to answer. */ +const ordersServerMs = 1500; + +/** Recent orders, as if from a server: refreshing them takes a moment. */ +let ordersState: OrdersState = {orders: sampleOrders, isLoading: false}; +const ordersListeners = new Set<() => void>(); + +function setOrdersState(next: OrdersState): void { + ordersState = next; + for (const listener of ordersListeners) { + listener(); + } +} + +function subscribeToOrders(listener: () => void): () => void { + ordersListeners.add(listener); + return () => { + ordersListeners.delete(listener); + }; +} + +/** Loads the orders again; resolves once they are there. */ +async function refreshOrders(): Promise { + setOrdersState({...ordersState, isLoading: true}); + await new Promise((resolve) => { + setTimeout(resolve, ordersServerMs); + }); + setOrdersState({orders: sampleOrders, isLoading: false}); +} + +function useOrders(): OrdersState { + return useSyncExternalStore( + subscribeToOrders, + () => ordersState, + () => ordersState, + ); +} + const demoTour: TourStep[] = [ on('Home', { target: 'header', @@ -66,6 +113,15 @@ const demoTour: TourStep[] = [ content: 'The tooltip picks the side with room and never leaves the safe area.', placement: 'top', }), + { + target: 'orders', + title: 'Waits for your data', + content: 'onBeforeStep refreshed these orders first. While that took a moment, the tour showed a spinner.', + async onBeforeStep() { + show('Home'); + await refreshOrders(); + }, + }, on('Home', { target: 'integration-last', title: 'Horizontal lists', @@ -242,6 +298,21 @@ function Integrations(): ReactNode { ); } +function RecentOrders(): ReactNode { + const ref = useTourTarget('orders'); + const {orders, isLoading} = useOrders(); + return ( + + Recent orders{isLoading ? ' · updating…' : ''} + {orders.map((order) => ( + + {order.table} {order.items} + + ))} + + ); +} + function HomeScreen(): ReactNode { const {startTour, isActive, currentStep, steps} = useTour(); // A ref the app already has: pass it in. @@ -292,6 +363,8 @@ function HomeScreen(): ReactNode { description="Screen-reader focus moves to the tooltip, the back button closes the tour." /> + + Integrations @@ -480,6 +553,10 @@ const styles = StyleSheet.create({ textAlign: 'center', lineHeight: 20, }, + orderTable: { + fontWeight: '600', + color: '#f9fafb', + }, integrations: { gap: 12, paddingHorizontal: 24, diff --git a/demos/react/src/App.css b/demos/react/src/App.css index a114b89..a5193a8 100644 --- a/demos/react/src/App.css +++ b/demos/react/src/App.css @@ -156,6 +156,39 @@ border-radius: 12px; } +.orders-section { + max-width: 480px; + margin: 0 auto 3rem; + padding: 1.5rem 2rem; + background: #f8f9fa; + border-radius: 12px; +} + +.orders-section h2 { + margin: 0 0 1rem; + font-size: 1.25rem; +} + +.orders { + margin: 0; + padding: 0; + list-style: none; +} + +.orders li { + padding: 0.5rem 0; + color: #666; +} + +.orders li + li { + border-top: 1px solid #e2e8f0; +} + +.orders strong { + margin-right: 0.5rem; + color: #333; +} + .install-section h2 { margin: 0 0 1rem; font-size: 1.5rem; @@ -334,10 +367,24 @@ color: #a0aec0; } - .install-section { + .install-section, + .orders-section { background: #2d3748; } + .orders-section h2, + .orders strong { + color: #f7fafc; + } + + .orders li { + color: #a0aec0; + } + + .orders li + li { + border-top-color: #4a5568; + } + .install-section h2 { color: #f7fafc; } diff --git a/demos/react/src/App.tsx b/demos/react/src/App.tsx index 9601c99..fa4590f 100644 --- a/demos/react/src/App.tsx +++ b/demos/react/src/App.tsx @@ -2,6 +2,7 @@ import {type ReactNode, useEffect, useState} from 'react'; import {BrowserRouter, Link, Route, Routes, useNavigate} from 'react-router-dom'; import {TourProvider, type TourTheme, type TourTooltipProps, useTour} from '@robertlinde/react-tour-kit/react'; import {Lab} from './lab.tsx'; +import {useOrders} from './orders.ts'; import {createDemoTour} from './tour.tsx'; import './App.css'; @@ -116,6 +117,7 @@ function Navigation({ function HomePage(): ReactNode { const {startTour, isActive, currentStep, steps} = useTour(); const navigate = useNavigate(); + const {orders, isLoading} = useOrders(); return (
@@ -146,6 +148,17 @@ function HomePage(): ReactNode {
+
+

Recent orders{isLoading ? ' · updating…' : ''}

+
    + {orders.map((order) => ( +
  • + {order.table} {order.items} +
  • + ))} +
+
+