Skip to content

Commit 5c00042

Browse files
committed
feat(web): create user onboarding/tutorial (task-41)
1 parent 9f1f587 commit 5c00042

File tree

5 files changed

+542
-0
lines changed

5 files changed

+542
-0
lines changed

apps/web/src/components/layout/MainLayout.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
3232

3333
import { KeyboardShortcutsButton, KeyboardShortcutsHelp } from "@/components/modals/KeyboardShortcutsHelp";
3434
import { NotificationCenter } from "@/components/notifications/NotificationCenter";
35+
import { OnboardingTutorial } from "@/components/onboarding";
3536
import { SyncStatusIndicator } from "@/components/sync/SyncStatusIndicator";
3637
import { UndoRedoControls } from "@/components/undo-redo/UndoRedoControls";
3738
import { ICON_SIZE } from "@/config/style-constants";
3839
import { useGlobalHotkeys } from "@/hooks/use-hotkeys";
40+
import { useOnboarding } from "@/hooks/useOnboarding";
3941
import { useLayoutStore } from "@/stores/layout-store";
4042
import { sprinkles } from "@/styles/sprinkles";
4143
import {
@@ -87,6 +89,9 @@ export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
8789
// Keyboard shortcuts state
8890
const [shortcutsHelpOpened, setShortcutsHelpOpened] = useState(false);
8991

92+
// User onboarding state
93+
const { showOnboarding, closeOnboarding } = useOnboarding();
94+
9095
// Set up global keyboard shortcuts
9196
useGlobalHotkeys({
9297
enabled: true,
@@ -706,6 +711,12 @@ export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
706711
onClose={() => setShortcutsHelpOpened(false)}
707712
/>
708713

714+
{/* User Onboarding Tutorial */}
715+
<OnboardingTutorial
716+
opened={showOnboarding}
717+
onClose={closeOnboarding}
718+
/>
719+
709720
</AppShell>
710721
);
711722
};
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Feature Tour Component
3+
*
4+
* Highlights specific UI elements during onboarding tour
5+
* Uses CSS positioning to spotlight target elements
6+
*
7+
* @module components/onboarding/FeatureTour
8+
*/
9+
10+
import { Portal, Text, useMantineTheme } from '@mantine/core';
11+
import { useWindowScroll } from '@mantine/hooks';
12+
import { useEffect, useState } from 'react';
13+
14+
export interface TourHighlightProps {
15+
/** CSS selector for target element to highlight */
16+
target?: string;
17+
/** Whether the tour is active */
18+
active: boolean;
19+
/** Tooltip text to display */
20+
tooltip?: string;
21+
/** Position of the tooltip */
22+
position?: 'top' | 'bottom' | 'left' | 'right';
23+
}
24+
25+
/**
26+
* Tour Highlight Component
27+
*
28+
* Creates a spotlight effect on target elements during onboarding
29+
*
30+
* @param props
31+
* @param props.target
32+
* @param props.active
33+
* @param props.tooltip
34+
* @param props.position
35+
*/
36+
export const TourHighlight: React.FC<TourHighlightProps> = ({
37+
target,
38+
active,
39+
tooltip,
40+
position = 'bottom',
41+
}) => {
42+
const [highlightRect, setHighlightRect] = useState<DOMRect | null>(null);
43+
const [visible, setVisible] = useState(false);
44+
const theme = useMantineTheme();
45+
const [scroll] = useWindowScroll();
46+
47+
useEffect(() => {
48+
if (!active || !target) {
49+
setVisible(false);
50+
return;
51+
}
52+
53+
// Find target element
54+
const findElement = () => {
55+
const element = document.querySelector(target);
56+
if (element instanceof HTMLElement) {
57+
const rect = element.getBoundingClientRect();
58+
setHighlightRect(rect);
59+
setVisible(true);
60+
61+
// Scroll element into view
62+
element.scrollIntoView({
63+
behavior: 'smooth',
64+
block: 'center',
65+
inline: 'center',
66+
});
67+
}
68+
};
69+
70+
// Small delay to ensure DOM is ready
71+
const timeoutId = setTimeout(findElement, 100);
72+
return () => clearTimeout(timeoutId);
73+
}, [active, target, scroll]);
74+
75+
if (!active || !highlightRect || !visible) {
76+
return null;
77+
}
78+
79+
const overlayStyle = {
80+
position: 'fixed' as const,
81+
top: 0,
82+
left: 0,
83+
right: 0,
84+
bottom: 0,
85+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
86+
zIndex: 9999,
87+
pointerEvents: 'none' as const,
88+
};
89+
90+
const spotlightStyle = {
91+
position: 'absolute' as const,
92+
left: highlightRect.left - 8,
93+
top: highlightRect.top - 8,
94+
width: highlightRect.width + 16,
95+
height: highlightRect.height + 16,
96+
borderRadius: '8px',
97+
boxShadow: `0 0 0 4000px rgba(0, 0, 0, 0.5), 0 0 0 4px ${theme.colors.blue[5]}`,
98+
transition: 'all 0.3s ease',
99+
};
100+
101+
const getTooltipPosition = () => {
102+
const offset = 16;
103+
const horizontalCenter = highlightRect.width / 2;
104+
const verticalCenter = highlightRect.height / 2;
105+
106+
switch (position) {
107+
case 'top':
108+
return {
109+
bottom: highlightRect.height + offset,
110+
left: horizontalCenter,
111+
transform: 'translateX(-50%)',
112+
};
113+
case 'bottom':
114+
return {
115+
top: highlightRect.height + offset,
116+
left: horizontalCenter,
117+
transform: 'translateX(-50%)',
118+
};
119+
case 'left':
120+
return {
121+
right: highlightRect.width + offset,
122+
top: verticalCenter,
123+
transform: 'translateY(-50%)',
124+
};
125+
case 'right':
126+
return {
127+
left: highlightRect.width + offset,
128+
top: verticalCenter,
129+
transform: 'translateY(-50%)',
130+
};
131+
}
132+
};
133+
134+
const tooltipPosition = getTooltipPosition();
135+
136+
return (
137+
<Portal>
138+
<div style={overlayStyle}>
139+
<div style={spotlightStyle}>
140+
{tooltip && (
141+
<div
142+
style={{
143+
position: 'absolute',
144+
...tooltipPosition,
145+
backgroundColor: 'white',
146+
padding: '12px 16px',
147+
borderRadius: '8px',
148+
boxShadow: theme.shadows.md,
149+
maxWidth: 300,
150+
zIndex: 10000,
151+
}}
152+
>
153+
<Text size="sm">{tooltip}</Text>
154+
</div>
155+
)}
156+
</div>
157+
</div>
158+
</Portal>
159+
);
160+
};
161+
162+
/**
163+
* Feature Tour Component
164+
*
165+
* Orchestrates multi-step tour with highlights
166+
*/
167+
export interface FeatureTourProps {
168+
/** Whether the tour is active */
169+
active: boolean;
170+
/** Current step index */
171+
currentStep: number;
172+
/** On close callback */
173+
onClose: () => void;
174+
}
175+
176+
/**
177+
* Feature Tour with auto-highlighting
178+
*
179+
* @param _props
180+
* @param _props.active
181+
* @param _props.currentStep
182+
* @param _props.onClose
183+
*/
184+
export const FeatureTour: React.FC<FeatureTourProps> = (_props) => {
185+
// This component can be extended to provide more sophisticated tour features
186+
// For now, the TourHighlight component handles individual step highlighting
187+
188+
return null;
189+
};

0 commit comments

Comments
 (0)