Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1eea810
Update checklist according to latest proposal
ghengeveld Oct 30, 2025
8e471bf
Update checklistData according to latest concept
ghengeveld Nov 1, 2025
c0eb34b
Handle narrow width for ChecklistModule by introducing an Optional co…
ghengeveld Nov 1, 2025
94c5c62
Update checklist criteria and checks
ghengeveld Nov 3, 2025
5f1bd57
Persist autocompleted items in node_modules/.cache and add telemetry
ghengeveld Nov 4, 2025
5ba33eb
Support conditional guide items
ghengeveld Nov 4, 2025
9d28bb0
Use simpler navigation API
ghengeveld Nov 4, 2025
cd97967
Add guided tour and intent survey to checklist
ghengeveld Nov 4, 2025
10917ad
Show completion message instead of mute toggle when all tasks are com…
ghengeveld Nov 4, 2025
589f1f0
Prevent onboarding from returning to splash screen
ghengeveld Nov 4, 2025
624bfc6
Fix build command
ghengeveld Nov 4, 2025
5845f57
Prevent guided tour from showing intent survey if it has already been…
ghengeveld Nov 4, 2025
1411869
Ensure subscription callback is a function
ghengeveld Nov 4, 2025
70914fe
Revert back to using ReactDOM.render
ghengeveld Nov 5, 2025
af5996a
Completion detection for install-vitest and run-tests tasks
ghengeveld Nov 6, 2025
571eb37
Don't skip checklist tasks when closing tour tooltip
ghengeveld Nov 6, 2025
da9c7b8
Better looking code blocks
ghengeveld Nov 6, 2025
8a27b33
Fix mocks
ghengeveld Nov 6, 2025
2adfb83
Add items for installing dependencies
ghengeveld Nov 6, 2025
870ab94
Use createRoot rather than deprecated ReactDOM.render
ghengeveld Nov 7, 2025
d6e6f09
Fix testing widget overflow to allow highlighting for onboarding
ghengeveld Nov 7, 2025
85b9fa1
Reduce update frequency by throttling index updates
ghengeveld Nov 7, 2025
8c5c660
Pass index to available function to receive updates, and don't pass i…
ghengeveld Nov 7, 2025
02917c4
Make content a function
ghengeveld Nov 7, 2025
073778c
Ignore example stories where appropriate
ghengeveld Nov 7, 2025
bc7ac39
Prevent clicking on underlying collapse toggle when using checklist r…
ghengeveld Nov 7, 2025
9aef937
Fix addon identifier
ghengeveld Nov 7, 2025
e311a22
Rename Tour to TourGuide and make it a generic component
ghengeveld Nov 7, 2025
1f79889
Make renderTourGuide a static method on the TourGuide component
ghengeveld Nov 7, 2025
1c8bb66
Fix GuidePage sidebar toggle
ghengeveld Nov 7, 2025
056a80f
Refactor onboarding components: consolidate HighlightElement and Tour…
ghengeveld Nov 10, 2025
f0dfab7
Refactor useChecklist to provide all data needed in Checklist component
ghengeveld Nov 12, 2025
d9df904
Animate the completion of a checklist item in the widget
ghengeveld Nov 12, 2025
2fd404c
Animate strikethrough
ghengeveld Nov 13, 2025
bb87ae0
Hide certain checklist items once completed, and update API for items…
ghengeveld Nov 13, 2025
7591d75
Reset itemIndex to only include ready items, in order to fix round-ro…
ghengeveld Nov 13, 2025
88afdc1
Hide action buttons in checklist module unless hovering the item
ghengeveld Nov 13, 2025
64c5d96
Update FocusProxy to use a data attribute rather than id attribute
ghengeveld Nov 13, 2025
760764f
Merge branch 'onboarding-guide' into checklist-tasks
ghengeveld Nov 13, 2025
bc804e3
Merge branch 'onboarding-checklist' into checklist-tasks
ghengeveld Nov 14, 2025
aad4c79
Remove storybook/internal/manager/manager-stores
ghengeveld Nov 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions code/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ const config = defineMain({
directory: '../addons/onboarding/src',
titlePrefix: 'addons/onboarding',
},
{
directory: '../addons/onboarding/example-stories',
},
Comment on lines +84 to +86
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the purpose of these stories?

{
directory: '../addons/pseudo-states/src',
titlePrefix: 'addons/pseudo-states',
Expand All @@ -100,6 +103,7 @@ const config = defineMain({
},
],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-themes',
'@storybook/addon-docs',
'@storybook/addon-designs',
Expand Down
75 changes: 75 additions & 0 deletions code/addons/onboarding/example-stories/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Meta, StoryObj } from '@storybook/react-vite';

import { fn } from 'storybook/test';

import { Button } from './Button';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Example/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
args: { onClick: fn() },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};

export const Secondary: Story = {
args: {
label: 'Button',
},
};

export const Large: Story = {
args: {
size: 'large',
label: 'Button',
},
};

export const Small: Story = {
args: {
size: 'small',
label: 'Button',
},
};

export const Ad: Story = {
args: {
primary: false,
label: 'Button',
},
};

export const Df: Story = {
args: {
primary: false,
label: 'Button',
},
};

export const Gdf: Story = {
args: {
primary: false,
label: 'Button',
},
};
37 changes: 37 additions & 0 deletions code/addons/onboarding/example-stories/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';

import './button.css';

export interface ButtonProps {
/** Is this the principal call to action on the page? */
primary?: boolean;
/** What background color to use */
backgroundColor?: string;
/** How large should the button be? */
size?: 'small' | 'medium' | 'large';
/** Button contents */
label: string;
/** Optional click handler */
onClick?: () => void;
}

/** Primary UI component for user interaction */
export const Button = ({
primary = false,
size = 'medium',
backgroundColor,
label,
...props
}: ButtonProps) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={{ backgroundColor }}
{...props}
>
{label}
</button>
);
};
30 changes: 30 additions & 0 deletions code/addons/onboarding/example-stories/button.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.storybook-button {
display: inline-block;
cursor: pointer;
border: 0;
border-radius: 3em;
font-weight: 700;
line-height: 1;
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.storybook-button--primary {
background-color: #555ab9;
color: white;
}
.storybook-button--secondary {
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
background-color: transparent;
color: #333;
}
.storybook-button--small {
padding: 10px 16px;
font-size: 12px;
}
.storybook-button--medium {
padding: 11px 20px;
font-size: 14px;
}
.storybook-button--large {
padding: 12px 24px;
font-size: 16px;
}
137 changes: 81 additions & 56 deletions code/addons/onboarding/src/Onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import React, { useCallback, useEffect, useState } from 'react';

import { SyntaxHighlighter } from 'storybook/internal/components';
import { HighlightElement, SyntaxHighlighter, TourGuide } from 'storybook/internal/components';
import { SAVE_STORY_RESPONSE } from 'storybook/internal/core-events';

import type { Step } from 'react-joyride';
import { type API } from 'storybook/manager-api';
import { ThemeProvider, convert, styled, themes } from 'storybook/theming';

import { Confetti } from './components/Confetti/Confetti';
import { HighlightElement } from './components/HighlightElement/HighlightElement';
import type { STORYBOOK_ADDON_ONBOARDING_STEPS } from './constants';
import { ADDON_CONTROLS_ID, STORYBOOK_ADDON_ONBOARDING_CHANNEL } from './constants';
import { GuidedTour } from './features/GuidedTour/GuidedTour';
import { IntentSurvey } from './features/IntentSurvey/IntentSurvey';
import { SplashScreen } from './features/SplashScreen/SplashScreen';

Expand Down Expand Up @@ -44,30 +41,14 @@ const CodeWrapper = styled.div(({ theme }) => ({
const theme = convert();

export type StepKey = (typeof STORYBOOK_ADDON_ONBOARDING_STEPS)[number];
export type StepDefinition = {
key: StepKey;
hideNextButton?: boolean;
onNextButtonClick?: () => void;
} & Partial<
Pick<
// Unfortunately we can't use ts-expect-error here for some reason
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Ignore circular reference
Step,
| 'content'
| 'disableBeacon'
| 'disableOverlay'
| 'floaterProps'
| 'offset'
| 'placement'
| 'spotlightClicks'
| 'styles'
| 'target'
| 'title'
>
>;

export default function Onboarding({ api }: { api: API }) {
export default function Onboarding({
api,
hasCompletedSurvey,
}: {
api: API;
hasCompletedSurvey: boolean;
}) {
const [enabled, setEnabled] = useState(true);
const [showConfetti, setShowConfetti] = useState(false);
const [step, setStep] = useState<StepKey>('1:Intro');
Expand Down Expand Up @@ -98,33 +79,38 @@ export default function Onboarding({ api }: { api: API }) {
[api]
);

const disableOnboarding = useCallback(() => {
// remove onboarding query parameter from current url
const url = new URL(window.location.href);
// @ts-expect-error (not strict)
const path = decodeURIComponent(url.searchParams.get('path'));
url.search = `?path=${path}&onboarding=false`;
history.replaceState({}, '', url.href);
api.setQueryParams({ onboarding: 'false' });
setEnabled(false);
}, [api, setEnabled]);
const disableOnboarding = useCallback(
(dismissedStep?: StepKey) => {
if (dismissedStep) {
api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, {
dismissedStep,
type: 'dismiss',
userAgent,
});
}
// remove onboarding query parameter from current url
const url = new URL(window.location.href);
// @ts-expect-error (not strict)
const path = decodeURIComponent(url.searchParams.get('path'));
url.search = `?path=${path}&onboarding=false`;
history.replaceState({}, '', url.href);
api.setQueryParams({ onboarding: 'false' });
setEnabled(false);
},
[api, setEnabled, userAgent]
);

const completeOnboarding = useCallback(
const completeSurvey = useCallback(
(answers: Record<string, unknown>) => {
api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, {
step: '7:FinishedOnboarding' satisfies StepKey,
type: 'telemetry',
userAgent,
});
api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, {
answers,
type: 'survey',
userAgent,
});
setStep('7:FinishedOnboarding');
selectStory('configure-your-project--docs');
disableOnboarding();
},
[api, selectStory, disableOnboarding, userAgent]
[api, selectStory, userAgent]
);

useEffect(() => {
Expand All @@ -148,6 +134,10 @@ export default function Onboarding({ api }: { api: API }) {

useEffect(() => {
setStep((current) => {
if (hasCompletedSurvey && current === '6:IntentSurvey') {
return '7:FinishedOnboarding';
}

if (
['1:Intro', '5:StoryCreated', '6:IntentSurvey', '7:FinishedOnboarding'].includes(current)
) {
Expand All @@ -162,12 +152,13 @@ export default function Onboarding({ api }: { api: API }) {
return '3:SaveFromControls';
}

if (primaryControl) {
if (primaryControl || current === '2:Controls') {
return '2:Controls';
}

return '1:Intro';
});
}, [createNewStoryForm, primaryControl, saveFromControls]);
}, [hasCompletedSurvey, createNewStoryForm, primaryControl, saveFromControls]);

useEffect(() => {
return api.on(SAVE_STORY_RESPONSE, ({ payload, success }) => {
Expand Down Expand Up @@ -196,7 +187,7 @@ export default function Onboarding({ api }: { api: API }) {
const snippet = source?.slice(startIndex).trim();
const startingLineNumber = source?.slice(0, startIndex).split('\n').length;

const steps: StepDefinition[] = [
const controlsTour = [
{
key: '2:Controls',
target: '#control-primary',
Expand All @@ -213,7 +204,7 @@ export default function Onboarding({ api }: { api: API }) {
disableBeacon: true,
disableOverlay: true,
spotlightClicks: true,
onNextButtonClick: () => {
onNext: () => {
const input = document.querySelector('#control-primary') as HTMLInputElement;
input.click();
},
Expand All @@ -234,7 +225,7 @@ export default function Onboarding({ api }: { api: API }) {
disableBeacon: true,
disableOverlay: true,
spotlightClicks: true,
onNextButtonClick: () => {
onNext: () => {
const button = document.querySelector(
'button[aria-label="Create new story with these settings"]'
) as HTMLButtonElement;
Expand Down Expand Up @@ -280,21 +271,55 @@ export default function Onboarding({ api }: { api: API }) {
},
},
},
] as const;
];

const checklistTour = [
{
key: '7:FinishedOnboarding',
target: '#storybook-checklist-module',
title: 'Continue at your own pace using the guide',
content: (
<>
Nice! You've got the essentials. You can continue at your own pace using the guide to
discover more of Storybook's capabilities.
<HighlightElement targetSelector="#storybook-checklist-module" pulsating />
</>
),
offset: 0,
placement: 'right-start',
disableBeacon: true,
disableOverlay: true,
styles: {
tooltip: {
width: 350,
},
},
},
];

return (
<ThemeProvider theme={theme}>
{showConfetti && <Confetti />}
{step === '1:Intro' ? (
<SplashScreen onDismiss={() => setStep('2:Controls')} />
) : step === '6:IntentSurvey' ? (
<IntentSurvey onComplete={completeOnboarding} onDismiss={disableOnboarding} />
<IntentSurvey
onComplete={completeSurvey}
onDismiss={() => disableOnboarding('6:IntentSurvey')}
/>
) : step === '7:FinishedOnboarding' ? (
<TourGuide
step={step}
steps={checklistTour}
onComplete={() => disableOnboarding()}
onDismiss={() => disableOnboarding(step)}
/>
) : (
<GuidedTour
<TourGuide
step={step}
steps={steps}
onClose={disableOnboarding}
onComplete={() => setStep('6:IntentSurvey')}
steps={controlsTour}
onComplete={() => setStep(hasCompletedSurvey ? '7:FinishedOnboarding' : '6:IntentSurvey')}
onDismiss={() => disableOnboarding(step)}
/>
)}
</ThemeProvider>
Expand Down
Loading
Loading