From 61eb1dad43bd9ef070c34fc140efdd7c9647253b Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 12 Jun 2024 16:58:05 +0200 Subject: [PATCH 01/11] Onboarding v2 first steps --- code/addons/controls/src/SaveStory.tsx | 4 +- code/addons/onboarding/src/App.tsx | 91 +---- .../HighlightElement.stories.tsx} | 31 +- .../HighlightElement/HighlightElement.tsx | 56 +++ .../PulsatingEffect/PulsatingEffect.tsx | 49 --- .../src/features/GuidedTour/GuidedTour.tsx | 357 ++++++++++-------- .../src/features/GuidedTour/Tooltip.tsx | 101 +++-- 7 files changed, 371 insertions(+), 318 deletions(-) rename code/addons/onboarding/src/components/{PulsatingEffect/PulsatingEffect.stories.tsx => HighlightElement/HighlightElement.stories.tsx} (52%) create mode 100644 code/addons/onboarding/src/components/HighlightElement/HighlightElement.tsx delete mode 100644 code/addons/onboarding/src/components/PulsatingEffect/PulsatingEffect.tsx diff --git a/code/addons/controls/src/SaveStory.tsx b/code/addons/controls/src/SaveStory.tsx index 8f72826c6460..e69a90f65756 100644 --- a/code/addons/controls/src/SaveStory.tsx +++ b/code/addons/controls/src/SaveStory.tsx @@ -137,7 +137,7 @@ export const SaveStory = ({ saveStory, createStory, resetArgs }: SaveStoryProps) }; return ( - + -
+ Create new story diff --git a/code/addons/onboarding/src/App.tsx b/code/addons/onboarding/src/App.tsx index e561c55cea1c..aa9eb8aafd4d 100644 --- a/code/addons/onboarding/src/App.tsx +++ b/code/addons/onboarding/src/App.tsx @@ -9,19 +9,14 @@ import { Confetti } from './components/Confetti/Confetti'; import { STORYBOOK_ADDON_ONBOARDING_CHANNEL } from './constants'; import { useGetProject } from './features/WriteStoriesModal/hooks/useGetProject'; -type Step = - | '1:Welcome' - | '2:StorybookTour' - | '3:WriteYourStory' - | '4:VisitNewStory' - | '5:ConfigureYourProject'; +type Step = '1:Controls' | '2:CreateStory' | '3:StoryCreated' | '4:StoriesList' | '5:NextSteps'; const theme = convert(); export default function App({ api }: { api: API }) { const [enabled, setEnabled] = useState(true); const [showConfetti, setShowConfetti] = useState(false); - const [step, setStep] = useState('1:Welcome'); + const [step, setStep] = useState('1:Controls'); const { data: codeSnippets } = useGetProject(); const skipOnboarding = useCallback(() => { @@ -36,51 +31,25 @@ export default function App({ api }: { api: API }) { }, [setEnabled, api]); useEffect(() => { - api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { - step: '1:Welcome', - type: 'telemetry', - }); - }, []); - - useEffect(() => { - if (step !== '1:Welcome') { - api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { - step, - type: 'telemetry', - }); - } + setShowConfetti(step === '3:StoryCreated'); + api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { step, type: 'telemetry' }); }, [api, step]); useEffect(() => { - let stepTimeout: number; - if (step === '4:VisitNewStory') { - setShowConfetti(true); - stepTimeout = window.setTimeout(() => { - setStep('5:ConfigureYourProject'); - }, 2000); - } - - return () => { - clearTimeout(stepTimeout); - }; - }, [step]); - - useEffect(() => { - const storyId = api.getCurrentStoryData()?.id; + console.log(api.getCurrentStoryData()); + const { id: storyId, refId } = api.getCurrentStoryData() || {}; api.setQueryParams({ onboarding: 'true' }); // make sure the initial state is set correctly: // 1. Selected story is primary button // 2. The addon panel is opened, in the bottom and the controls tab is selected - if (storyId !== 'example-button--primary') { + if (storyId !== 'example-button--primary' || refId !== undefined) { try { - api.selectStory('example-button--primary', undefined, { - ref: undefined, - }); - } catch (e) { - // - } + api.selectStory('example-button--primary', undefined, { ref: undefined }); + } catch (e) {} } - }, []); + }, [api]); + + console.log({ enabled, step }); if (!enabled) { return null; @@ -88,7 +57,7 @@ export default function App({ api }: { api: API }) { return ( - {enabled && showConfetti && ( + {showConfetti && ( )} - {enabled && step === '1:Welcome' && ( - { - setStep('2:StorybookTour'); - }} - skipOnboarding={() => { - skipOnboarding(); - - api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { - step: 'X:SkippedOnboarding', - where: 'WelcomeModal', - type: 'telemetry', - }); - }} - /> - )} - {enabled && (step === '2:StorybookTour' || step === '5:ConfigureYourProject') && ( + {step !== '5:NextSteps' && ( { - setStep('3:WriteYourStory'); + // setStep('3:WriteYourStory'); }} codeSnippets={codeSnippets || undefined} onLastTourDone={() => { @@ -137,19 +89,6 @@ export default function App({ api }: { api: API }) { }} /> )} - {enabled && step === '3:WriteYourStory' && codeSnippets && ( - { - api.selectStory('example-button--warning'); - - setStep('4:VisitNewStory'); - }} - skipOnboarding={skipOnboarding} - /> - )} ); } diff --git a/code/addons/onboarding/src/components/PulsatingEffect/PulsatingEffect.stories.tsx b/code/addons/onboarding/src/components/HighlightElement/HighlightElement.stories.tsx similarity index 52% rename from code/addons/onboarding/src/components/PulsatingEffect/PulsatingEffect.stories.tsx rename to code/addons/onboarding/src/components/HighlightElement/HighlightElement.stories.tsx index 6a87a2147c0a..44d894260a20 100644 --- a/code/addons/onboarding/src/components/PulsatingEffect/PulsatingEffect.stories.tsx +++ b/code/addons/onboarding/src/components/HighlightElement/HighlightElement.stories.tsx @@ -1,10 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { PulsatingEffect } from './PulsatingEffect'; +import { HighlightElement } from './HighlightElement'; import React from 'react'; import { within, expect } from '@storybook/test'; -const meta: Meta = { - component: PulsatingEffect, +const meta: Meta = { + component: HighlightElement, parameters: { layout: 'centered', chromatic: { @@ -20,7 +20,30 @@ type Story = StoryObj; export const Default: Story = { render: () => ( <> - + + + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.parentElement!); + const button = canvas.getByRole('button'); + await expect(button).toHaveStyle('box-shadow: rgba(2,156,253,1) 0 0 2px 1px'); + }, +}; + +export const Pulsating: Story = { + render: () => ( + <> + - - )} + )} + ); }; From 0c0efa074591e35e89e1a3fcf72e0051a0e0f0af Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 18 Jun 2024 21:37:27 +0200 Subject: [PATCH 02/11] Onboarding v2 tour --- code/addons/onboarding/package.json | 2 +- code/addons/onboarding/src/App.tsx | 94 ------ code/addons/onboarding/src/Onboarding.tsx | 240 ++++++++++++++ .../src/components/Button/Button.tsx | 18 +- code/addons/onboarding/src/constants.ts | 9 + .../features/GuidedTour/GuidedTour.styled.tsx | 20 -- .../src/features/GuidedTour/GuidedTour.tsx | 287 +++++------------ .../src/features/GuidedTour/Tooltip.tsx | 16 +- .../WriteStoriesModal.stories.tsx | 108 ------- .../WriteStoriesModal.styled.tsx | 178 ----------- .../WriteStoriesModal/WriteStoriesModal.tsx | 293 ------------------ .../assets/01-title-sidebar.png | Bin 4563 -> 0 bytes .../assets/02-story-name-sidebar.png | Bin 4503 -> 0 bytes .../WriteStoriesModal/assets/03-args.png | Bin 6324 -> 0 bytes .../WriteStoriesModal/code/javascript.tsx | 48 --- .../features/WriteStoriesModal/code/types.ts | 6 - .../WriteStoriesModal/code/typescript.tsx | 56 ---- .../hooks/useGetBackdropBoundary.tsx | 37 --- .../hooks/useGetButtonPath.tsx | 28 -- .../WriteStoriesModal/hooks/useGetProject.tsx | 43 --- .../hooks/useGetWarningButtonStatus.tsx | 39 --- .../src/helpers/textContentMatcher.tsx | 30 -- code/addons/onboarding/src/manager.tsx | 6 +- code/yarn.lock | 32 +- 24 files changed, 352 insertions(+), 1238 deletions(-) delete mode 100644 code/addons/onboarding/src/App.tsx create mode 100644 code/addons/onboarding/src/Onboarding.tsx delete mode 100644 code/addons/onboarding/src/features/GuidedTour/GuidedTour.styled.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.styled.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/assets/01-title-sidebar.png delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/assets/02-story-name-sidebar.png delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/assets/03-args.png delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/code/javascript.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/code/types.ts delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/code/typescript.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/hooks/useGetBackdropBoundary.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/hooks/useGetButtonPath.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/hooks/useGetProject.tsx delete mode 100644 code/addons/onboarding/src/features/WriteStoriesModal/hooks/useGetWarningButtonStatus.tsx delete mode 100644 code/addons/onboarding/src/helpers/textContentMatcher.tsx diff --git a/code/addons/onboarding/package.json b/code/addons/onboarding/package.json index 6a3210ad3f79..fb2a8636770c 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -63,7 +63,7 @@ "framer-motion": "^11.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-joyride": "^2.7.2", + "react-joyride": "^2.8.2", "react-use-measure": "^2.1.1", "typescript": "^5.3.2" }, diff --git a/code/addons/onboarding/src/App.tsx b/code/addons/onboarding/src/App.tsx deleted file mode 100644 index aa9eb8aafd4d..000000000000 --- a/code/addons/onboarding/src/App.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { ThemeProvider, convert } from '@storybook/theming'; -import { addons, type API } from '@storybook/manager-api'; - -import { GuidedTour } from './features/GuidedTour/GuidedTour'; -import { WelcomeModal } from './features/WelcomeModal/WelcomeModal'; -import { WriteStoriesModal } from './features/WriteStoriesModal/WriteStoriesModal'; -import { Confetti } from './components/Confetti/Confetti'; -import { STORYBOOK_ADDON_ONBOARDING_CHANNEL } from './constants'; -import { useGetProject } from './features/WriteStoriesModal/hooks/useGetProject'; - -type Step = '1:Controls' | '2:CreateStory' | '3:StoryCreated' | '4:StoriesList' | '5:NextSteps'; - -const theme = convert(); - -export default function App({ api }: { api: API }) { - const [enabled, setEnabled] = useState(true); - const [showConfetti, setShowConfetti] = useState(false); - const [step, setStep] = useState('1:Controls'); - const { data: codeSnippets } = useGetProject(); - - const skipOnboarding = 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); - }, [setEnabled, api]); - - useEffect(() => { - setShowConfetti(step === '3:StoryCreated'); - api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { step, type: 'telemetry' }); - }, [api, step]); - - useEffect(() => { - console.log(api.getCurrentStoryData()); - const { id: storyId, refId } = api.getCurrentStoryData() || {}; - api.setQueryParams({ onboarding: 'true' }); - // make sure the initial state is set correctly: - // 1. Selected story is primary button - // 2. The addon panel is opened, in the bottom and the controls tab is selected - if (storyId !== 'example-button--primary' || refId !== undefined) { - try { - api.selectStory('example-button--primary', undefined, { ref: undefined }); - } catch (e) {} - } - }, [api]); - - console.log({ enabled, step }); - - if (!enabled) { - return null; - } - - return ( - - {showConfetti && ( - { - confetti?.reset(); - setShowConfetti(false); - }} - /> - )} - {step !== '5:NextSteps' && ( - { - // setStep('3:WriteYourStory'); - }} - codeSnippets={codeSnippets || undefined} - onLastTourDone={() => { - try { - api.selectStory('configure-your-project--docs'); - } catch (e) { - // - } - api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { - step: '6:FinishedOnboarding', - type: 'telemetry', - }); - skipOnboarding(); - }} - /> - )} - - ); -} diff --git a/code/addons/onboarding/src/Onboarding.tsx b/code/addons/onboarding/src/Onboarding.tsx new file mode 100644 index 000000000000..2144d8007b06 --- /dev/null +++ b/code/addons/onboarding/src/Onboarding.tsx @@ -0,0 +1,240 @@ +import { SAVE_STORY_RESPONSE } from '@storybook/core-events'; +import { type API } from '@storybook/manager-api'; +import { ThemeProvider, convert, styled } from '@storybook/theming'; +import React, { useCallback, useEffect, useState } from 'react'; +import type { Step } from 'react-joyride'; + +import { GuidedTour } from './features/GuidedTour/GuidedTour'; +import { Confetti } from './components/Confetti/Confetti'; +import type { STORYBOOK_ADDON_ONBOARDING_STEPS } from './constants'; +import { STORYBOOK_ADDON_ONBOARDING_CHANNEL } from './constants'; + +import { HighlightElement } from './components/HighlightElement/HighlightElement'; + +const SpanHighlight = styled.span(({ theme }) => ({ + display: 'inline-flex', + borderRadius: 3, + padding: '0 5px', + marginBottom: -2, + opacity: 0.8, + fontFamily: theme.typography.fonts.mono, + fontSize: 11, + border: theme.base === 'dark' ? theme.color.darkest : theme.color.lightest, + color: theme.base === 'dark' ? theme.color.lightest : theme.color.darkest, + backgroundColor: theme.base === 'dark' ? 'black' : theme.color.light, + boxSizing: 'border-box', + lineHeight: '17px', +})); + +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 }) { + const [enabled, setEnabled] = useState(true); + const [showConfetti, setShowConfetti] = useState(false); + const [step, setStep] = useState('1:Intro'); + + const [primaryControl, setPrimaryControl] = useState(); + const [saveFromControls, setSaveFromControls] = useState(); + const [createNewStoryForm, setCreateNewStoryForm] = useState(); + const [createdStory, setCreatedStory] = useState<{ + newStoryName: string; + sourceFileName: string; + } | null>(); + + const selectStory = useCallback( + (storyId: string) => { + try { + const { id, refId } = api.getCurrentStoryData() || {}; + if (id !== storyId || refId !== undefined) api.selectStory(storyId); + } catch (e) {} + }, + [api] + ); + + const skipOnboarding = 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 completeOnboarding = useCallback(() => { + selectStory('configure-your-project--docs'); + api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { + step: '6:FinishedOnboarding' satisfies StepKey, + type: 'telemetry', + }); + skipOnboarding(); + }, [api, selectStory, skipOnboarding]); + + useEffect(() => { + api.setQueryParams({ onboarding: 'true' }); + selectStory('example-button--primary'); + api.togglePanel(true); + api.togglePanelPosition('bottom'); + api.setSelectedPanel('addon-controls'); + }, [api, selectStory]); + + useEffect(() => { + const observer = new MutationObserver(() => { + setPrimaryControl(document.getElementById('control-primary')); + setSaveFromControls(document.getElementById('save-from-controls')); + setCreateNewStoryForm(document.getElementById('create-new-story-form')); + }); + + observer.observe(document.body, { childList: true, subtree: true }); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + setStep((current) => { + if (['1:Intro', '5:StoryCreated', '6:FinishedOnboarding'].includes(current)) return current; + if (createNewStoryForm) return '4:CreateStory'; + if (saveFromControls) return '3:SaveFromControls'; + if (primaryControl) return '2:Controls'; + return '1:Intro'; + }); + }, [createNewStoryForm, primaryControl, saveFromControls]); + + useEffect(() => { + return api.on(SAVE_STORY_RESPONSE, ({ payload, success }) => { + if (!success) return; + setCreatedStory(payload); + setStep('5:StoryCreated'); + setTimeout(() => api.clearNotification('save-story-success')); + }); + }, [api]); + + useEffect(() => { + if (step === '1:Intro') setTimeout(() => setStep('2:Controls'), 3000); + if (step === '5:StoryCreated') setShowConfetti(true); + api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { step, type: 'telemetry' }); + }, [api, step]); + + if (!enabled) { + return null; + } + + const steps: StepDefinition[] = [ + { + key: '2:Controls', + target: '#control-primary', + title: 'Interactive story playground', + content: ( + <> + See how a story renders with different data and state without touching code. Try it out by + toggling this button. + + + ), + offset: 20, + placement: 'right', + disableBeacon: true, + disableOverlay: true, + spotlightClicks: true, + onNextButtonClick: () => { + const input = document.querySelector('#control-primary') as HTMLInputElement; + input.click(); + }, + }, + { + key: '3:SaveFromControls', + target: 'button[aria-label="Create new story with these settings"]', + title: 'Save your changes as a new story', + content: ( + <> + Great! Storybook stories represent the key states of each of your components. After + modifying a story, you can save your changes from here or reset it. + + + ), + offset: 6, + placement: 'top', + disableBeacon: true, + disableOverlay: true, + spotlightClicks: true, + onNextButtonClick: () => { + const button = document.querySelector( + 'button[aria-label="Create new story with these settings"]' + ) as HTMLButtonElement; + button.click(); + }, + styles: { + tooltip: { + width: 400, + }, + }, + }, + { + key: '5:StoryCreated', + target: '#storybook-explorer-tree [data-selected="true"]', + title: 'You just added your first story!', + content: ( + <> + Well done! You just created your first story from the Storybook manager. This + automatically added a few lines of code in{' '} + {createdStory?.sourceFileName}. + + ), + offset: 12, + placement: 'right', + disableBeacon: true, + disableOverlay: true, + styles: { + tooltip: { + width: 400, + }, + }, + }, + ] as const; + + return ( + + {showConfetti && ( + { + confetti?.reset(); + setShowConfetti(false); + }} + /> + )} + + + ); +} diff --git a/code/addons/onboarding/src/components/Button/Button.tsx b/code/addons/onboarding/src/components/Button/Button.tsx index 622359c4e31a..699554e1169b 100644 --- a/code/addons/onboarding/src/components/Button/Button.tsx +++ b/code/addons/onboarding/src/components/Button/Button.tsx @@ -5,7 +5,7 @@ import { styled } from '@storybook/theming'; export interface ButtonProps extends ComponentProps<'button'> { children: string; onClick?: (e: React.MouseEvent) => void; - variant?: 'primary' | 'secondary' | 'outline'; + variant?: 'primary' | 'secondary' | 'outline' | 'white'; } const StyledButton = styled.button<{ variant: ButtonProps['variant'] }>` @@ -22,16 +22,17 @@ const StyledButton = styled.button<{ variant: ButtonProps['variant'] }>` if (variant === 'primary') return theme.color.secondary; if (variant === 'secondary') return theme.color.lighter; if (variant === 'outline') return 'transparent'; + if (variant === 'white') return theme.color.lightest; return theme.color.secondary; }}; color: ${({ theme, variant }) => { if (variant === 'primary') return theme.color.lightest; if (variant === 'secondary') return theme.darkest; if (variant === 'outline') return theme.darkest; + if (variant === 'white') return theme.color.secondary; return theme.color.lightest; }}; box-shadow: ${({ variant }) => { - if (variant === 'primary') return 'none'; if (variant === 'secondary') return '#D9E8F2 0 0 0 1px inset'; if (variant === 'outline') return '#D9E8F2 0 0 0 1px inset'; return 'none'; @@ -40,18 +41,26 @@ const StyledButton = styled.button<{ variant: ButtonProps['variant'] }>` font-size: 0.8125rem; font-weight: 700; font-family: ${({ theme }) => theme.typography.fonts.base}; - transition: background-color, box-shadow, opacity; + transition: background-color, box-shadow, color, opacity; transition-duration: 0.16s; transition-timing-function: ease-in-out; text-decoration: none; &:hover { - background-color: ${({ variant }) => { + background-color: ${({ theme, variant }) => { if (variant === 'primary') return '#0b94eb'; if (variant === 'secondary') return '#eef4f9'; if (variant === 'outline') return 'transparent'; + if (variant === 'white') return theme.color.lightest; return '#0b94eb'; }}; + color: ${({ theme, variant }) => { + if (variant === 'primary') return theme.color.lightest; + if (variant === 'secondary') return theme.darkest; + if (variant === 'outline') return theme.darkest; + if (variant === 'white') return theme.color.darkest; + return theme.color.lightest; + }}; } &:focus { @@ -59,6 +68,7 @@ const StyledButton = styled.button<{ variant: ButtonProps['variant'] }>` if (variant === 'primary') return 'inset 0 0 0 1px rgba(0, 0, 0, 0.2)'; if (variant === 'secondary') return 'inset 0 0 0 1px #0b94eb'; if (variant === 'outline') return 'inset 0 0 0 1px #0b94eb'; + if (variant === 'white') return 'none'; return 'inset 0 0 0 2px rgba(0, 0, 0, 0.1)'; }}; } diff --git a/code/addons/onboarding/src/constants.ts b/code/addons/onboarding/src/constants.ts index f81e55b4cf93..fa3cca4032e8 100644 --- a/code/addons/onboarding/src/constants.ts +++ b/code/addons/onboarding/src/constants.ts @@ -1 +1,10 @@ export const STORYBOOK_ADDON_ONBOARDING_CHANNEL = 'STORYBOOK_ADDON_ONBOARDING_CHANNEL'; + +export const STORYBOOK_ADDON_ONBOARDING_STEPS = [ + '1:Intro', + '2:Controls', + '3:SaveFromControls', + '4:CreateStory', + '5:StoryCreated', + '6:FinishedOnboarding', +] as const; diff --git a/code/addons/onboarding/src/features/GuidedTour/GuidedTour.styled.tsx b/code/addons/onboarding/src/features/GuidedTour/GuidedTour.styled.tsx deleted file mode 100644 index 5f666ab74aae..000000000000 --- a/code/addons/onboarding/src/features/GuidedTour/GuidedTour.styled.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { type Theme } from '@storybook/theming'; - -export const getStyles = (theme: Theme) => ({ - border: 0, - borderRadius: '0.25rem', - cursor: 'pointer', - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - padding: '0 0.75rem', - background: theme.color.secondary, - color: '#FFF', - height: 32, - fontSize: '0.8125rem', - fontWeight: 700, - fontFamily: theme.typography.fonts.base, - transition: 'all 0.16s ease-in-out', - textDecoration: 'none', - outline: 'none', -}); diff --git a/code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx b/code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx index 25ed81593f51..d345f3fa0268 100644 --- a/code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx +++ b/code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx @@ -1,238 +1,89 @@ -import type { ComponentProps } from 'react'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import type { CallBackProps } from 'react-joyride'; -import Joyride, { STATUS } from 'react-joyride'; -import type { API } from '@storybook/manager-api'; -import { SAVE_STORY_RESPONSE, UPDATE_STORY_ARGS } from '@storybook/core-events'; +import Joyride, { ACTIONS } from 'react-joyride'; import { useTheme } from '@storybook/theming'; -import { HighlightElement } from '../../components/HighlightElement/HighlightElement'; -import { Confetti } from '../../components/Confetti/Confetti'; import { Tooltip } from './Tooltip'; -import { SpanHighlight } from '../WriteStoriesModal/WriteStoriesModal.styled'; -import type { CodeSnippets } from '../WriteStoriesModal/code/types'; - -type GuidedTourStep = ComponentProps['step'] & { key: string }; - -// const waitForControl = () => { -// let pollInterval: ReturnType; -// let abortTimer: ReturnType; -// return [ -// Promise.race([ -// new Promise((resolve) => { -// if (document.getElementById('control-primary')) resolve(); -// else { -// pollInterval = setInterval(() => { -// if (document.getElementById('control-primary')) resolve(); -// }, 100); -// } -// }), -// new Promise( -// (_, reject) => -// (abortTimer = setTimeout(() => { -// clearTimeout(pollInterval); -// reject(); -// }, 5000)) -// ), -// ]), -// () => { -// clearInterval(pollInterval); -// clearTimeout(abortTimer); -// }, -// ] as const; -// }; +import type { StepDefinition, StepKey } from '../../Onboarding'; export function GuidedTour({ - api, - isFinalStep, - onFirstTourDone, - onLastTourDone, - codeSnippets, + step, + steps, + onClose, + onComplete, }: { - api: API; - isFinalStep?: boolean; - codeSnippets?: CodeSnippets; - onFirstTourDone: () => void; - onLastTourDone: () => void; + step: StepKey; + steps: StepDefinition[]; + onClose: () => void; + onComplete: () => void; }) { const [stepIndex, setStepIndex] = useState(null); const theme = useTheme(); - const [primaryControl, setPrimaryControl] = useState(); - const [saveFromControls, setSaveFromControls] = useState(); - const [createNewStoryForm, setCreateNewStoryForm] = useState(); - const [createdStory, setCreatedStory] = useState<{ - newStoryName: string; - sourceFileName: string; - } | null>(); - - console.log({ stepIndex }); - useEffect(() => { - return api.on(SAVE_STORY_RESPONSE, ({ payload, success }) => { - if (!success) return; - setStepIndex(2); - setCreatedStory(payload); - setTimeout(() => api.clearNotification('save-story-success')); + let timeout: NodeJS.Timeout; + setStepIndex((current) => { + const index = steps.findIndex(({ key }) => key === step); + if (index === -1) return null; + if (index === current) return current; + timeout = setTimeout(setStepIndex, 500, index); + return null; }); - }, [api]); - - useEffect(() => { - setStepIndex((index) => (index && index > 2 ? index : null)); - setTimeout(() => { - setStepIndex((index) => { - if (index && index > 1) return index; - if (saveFromControls) return 1; - if (primaryControl) return 0; - return null; - }); - }, 500); - }, [primaryControl, saveFromControls, createNewStoryForm]); - - useEffect(() => { - const observer = new MutationObserver(() => { - setPrimaryControl(document.getElementById('control-primary')); - setSaveFromControls(document.getElementById('save-from-controls')); - setCreateNewStoryForm(document.getElementById('create-new-story-form')); - }); - - observer.observe(document.body, { childList: true, subtree: true }); - return () => observer.disconnect(); - }, []); - - const steps: GuidedTourStep[] = [ - { - key: '1:Controls', - target: '#control-primary', - title: 'Interactive story playground', - content: ( - <> - See how a story renders with different data and state without touching code. -
-
- Try it out by toggling this button. - - - ), - placement: 'right', - disableBeacon: true, - disableOverlay: true, - spotlightClicks: true, - }, - { - key: '2:SaveFromControls', - target: 'button[aria-label="Create new story with these settings"]', - title: 'Save your changes as a new story', - content: ( - <> - Storybook stories represent the key states of each of your components. After modifying a - story, you can save your changes from here. - - - ), - placement: 'top', - disableBeacon: true, - disableOverlay: true, - spotlightClicks: true, - styles: { - tooltip: { - width: 320, - }, - }, - }, - { - key: '3:StoryCreated', - target: '#storybook-explorer-tree [data-selected="true"]', - title: 'You just added your first story!', - content: ( - <> - Well done! You just created your first story from the Storybook manager. This - automatically added a few lines of code in{' '} - {createdStory?.sourceFileName}. - - ), - placement: 'center', - disableBeacon: true, - styles: { - tooltip: { - width: 600, - }, - }, - }, - ]; - - // useEffect(() => { - // const [controlReady, cleanup] = waitForControl(); - // controlReady.then(() => setReady(true)).catch(() => setReady(false)); - // return cleanup; - // }, []); - - if (stepIndex === null || steps[stepIndex]?.content === null) return null; + return () => clearTimeout(timeout); + }, [step, steps]); - console.log(steps[stepIndex]); + if (stepIndex === null) return null; return ( - <> - {stepIndex === 2 && ( - confetti?.reset()} - /> - )} - { - if (data.action === 'next') { - setStepIndex(null); - setTimeout(() => setStepIndex(data.index + 1), 500); - } - }} - floaterProps={{ - disableAnimation: true, - styles: { - arrow: { - length: 20, - spread: 2, - }, - floater: { - filter: - theme.base === 'light' - ? 'drop-shadow(0px 5px 5px rgba(0,0,0,0.05)) drop-shadow(0 1px 3px rgba(0,0,0,0.1))' - : 'drop-shadow(#fff5 0px 0px 0.5px) drop-shadow(#fff5 0px 0px 0.5px)', - }, - }, - }} - tooltipComponent={Tooltip} - styles={{ - overlay: { - mixBlendMode: 'unset', - backgroundColor: steps[stepIndex]?.target === 'body' ? 'rgba(27, 28, 29, 0.2)' : 'none', - }, - spotlight: { - backgroundColor: 'none', - border: `solid 2px ${theme.color.secondary}`, - boxShadow: '0px 0px 0px 9999px rgba(27, 28, 29, 0.2)', - }, - tooltip: { - width: 280, - color: theme.color.lightest, - background: theme.base === 'dark' ? '#292A2C' : theme.color.secondary, + { + if (data.action === ACTIONS.CLOSE) onClose(); + if (data.action === ACTIONS.NEXT && data.index === data.size - 1) onComplete(); + }} + floaterProps={{ + disableAnimation: true, + styles: { + arrow: { + length: 20, + spread: 2, }, - options: { - zIndex: 9998, - primaryColor: theme.color.secondary, - arrowColor: theme.base === 'dark' ? '#292A2C' : theme.color.secondary, + floater: { + filter: + theme.base === 'light' + ? 'drop-shadow(0px 5px 5px rgba(0,0,0,0.05)) drop-shadow(0 1px 3px rgba(0,0,0,0.1))' + : 'drop-shadow(#fff5 0px 0px 0.5px) drop-shadow(#fff5 0px 0px 0.5px)', }, - }} - /> - + }, + }} + tooltipComponent={Tooltip} + styles={{ + overlay: { + mixBlendMode: 'unset', + backgroundColor: steps[stepIndex]?.target === 'body' ? 'rgba(27, 28, 29, 0.2)' : 'none', + }, + spotlight: { + backgroundColor: 'none', + border: `solid 2px ${theme.color.secondary}`, + boxShadow: '0px 0px 0px 9999px rgba(27, 28, 29, 0.2)', + }, + tooltip: { + width: 280, + color: theme.color.lightest, + background: theme.base === 'dark' ? '#292A2C' : theme.color.secondary, + }, + options: { + zIndex: 9998, + primaryColor: theme.color.secondary, + arrowColor: theme.base === 'dark' ? '#292A2C' : theme.color.secondary, + }, + }} + /> ); } diff --git a/code/addons/onboarding/src/features/GuidedTour/Tooltip.tsx b/code/addons/onboarding/src/features/GuidedTour/Tooltip.tsx index d4fc8e7b6f08..6eca0ec92713 100644 --- a/code/addons/onboarding/src/features/GuidedTour/Tooltip.tsx +++ b/code/addons/onboarding/src/features/GuidedTour/Tooltip.tsx @@ -66,6 +66,7 @@ type TooltipProps = { | 'title' | 'content' | 'target' + | 'offset' | 'placement' | 'disableOverlay' | 'disableBeacon' @@ -77,6 +78,7 @@ type TooltipProps = { onNextButtonClick: () => void; } >; + closeProps: TooltipRenderProps['closeProps']; primaryProps: TooltipRenderProps['primaryProps']; tooltipProps: TooltipRenderProps['tooltipProps']; }; @@ -85,10 +87,9 @@ export const Tooltip: FC = ({ index, size, step, + closeProps, primaryProps, tooltipProps, - closeProps, - ...rest }) => { useEffect(() => { const style = document.createElement('style'); @@ -119,11 +120,10 @@ export const Tooltip: FC = ({ return ( - {console.log({ step, primaryProps, closeProps, rest })} {step.title && {step.title}} - + @@ -134,8 +134,12 @@ export const Tooltip: FC = ({ {index + 1} of {size} {!step.hideNextButton && ( - )} diff --git a/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx b/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx deleted file mode 100644 index 6902466a8e15..000000000000 --- a/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.stories.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; - -import { waitFor, within, expect, fn } from '@storybook/test'; -import { STORY_INDEX_INVALIDATED, STORY_RENDERED } from '@storybook/core-events'; -import { WriteStoriesModal } from './WriteStoriesModal'; -import typescriptSnippet from './code/typescript'; - -const getData = fn(); - -const meta: Meta = { - component: WriteStoriesModal, - args: { - codeSnippets: typescriptSnippet, - // @ts-expect-error (bad) - api: { - getData, - }, - addonsStore: { - // @ts-expect-error (bad) - getChannel: () => ({ - once: (type: string, cb: () => void) => { - if (type === STORY_RENDERED) { - cb(); - } - }, - on: (type: string, cb: () => void) => { - if (type === STORY_INDEX_INVALIDATED) { - storyIndexInvalidatedCb = cb; - } - }, - off: () => {}, - }), - }, - }, - - decorators: [ - (storyFn, context) => { - (context.args.api.getData as typeof getData) - // do not respond to the first call, this would only return the data correctly if the story already exists - // which is not the case in this story, it only makes sense in the real scenario - .mockReturnValueOnce(null) - .mockReturnValueOnce({ some: 'data' }); - return
{storyFn()}
; - }, - (Story, context) => { - const [container, setContainer] = useState(undefined); - - if (context.globals.theme === 'side-by-side') { - return ( -
{ - if (element) { - setContainer(element); - } - }} - style={{ - width: '100%', - height: '100%', - minHeight: '600px', - transform: 'translateZ(0)', - }} - > - {Story({ args: { ...context.args, container } })} -
- ); - } - - return Story(); - }, - ], -}; - -export default meta; - -type Story = StoryObj; - -let storyIndexInvalidatedCb: () => void; - -export const Default: Story = {}; - -export const DefaultPlayed: Story = { - args: { - ...Default.args, - }, - play: async ({ canvasElement, step }) => { - const canvas = within(canvasElement.parentElement!); - const importsText = await canvas.findByText('Imports'); - await step('Wait for modal to be visible', async () => { - const modal = await canvas.findByRole('dialog'); - await waitFor(async () => expect(modal).toBeVisible()); - }); - await expect(importsText).toBeVisible(); - await canvas.getByRole('button', { name: /Next/i }).click(); - const metaText = await canvas.findAllByText('Meta'); - await expect(metaText?.[0]).toBeVisible(); - await canvas.getByRole('button', { name: /Next/i }).click(); - const storyText = await canvas.findAllByText('Story'); - await expect(storyText?.[0]).toBeVisible(); - await canvas.getByRole('button', { name: /Next/i }).click(); - const argsText = await canvas.findAllByText('Args'); - await expect(argsText?.[0]).toBeVisible(); - await canvas.getByRole('button', { name: /Next/i }).click(); - (await canvas.findByRole('button', { name: /Copy code/i })).click(); - storyIndexInvalidatedCb(); - await waitFor(() => expect(canvas.getAllByLabelText('complete')).toHaveLength(3)); - }, -}; diff --git a/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.styled.tsx b/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.styled.tsx deleted file mode 100644 index eea2675c814d..000000000000 --- a/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.styled.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { keyframes, styled } from '@storybook/theming'; -import { Modal } from '@storybook/components'; - -export const ModalWrapper = styled(Modal)``; - -export const ModalContent = styled.div` - display: flex; - flex-direction: row; - height: 100%; - max-height: 85vh; - - &:focus-visible { - outline: none; - } -`; - -export const Main = styled.div` - position: relative; - flex: 1; - display: flex; - flex-direction: column; - background: white; - font-family: ${({ theme }) => theme.typography.fonts.base}; -`; - -export const Header = styled.div` - position: relative; - z-index: 1; - box-sizing: border-box; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 15px; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - height: 44px; -`; - -export const ModalTitle = styled.div` - display: flex; - align-items: center; - gap: 5px; - font-size: 13px; - line-height: 18px; - font-weight: bold; - color: ${({ theme }) => theme.color.darkest}; -`; - -export const Content = styled.div` - font-size: 13px; - line-height: 18px; - padding: 15px; - flex: 1; - display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: space-between; - color: ${({ theme }) => theme.color.darker}; - - h3 { - font-size: 13px; - line-height: 18px; - font-weight: bold; - padding: 0; - margin: 0; - } -`; - -export const SpanHighlight = styled.span(({ theme }) => ({ - display: 'inline-flex', - borderRadius: 3, - padding: '0 5px', - marginBottom: -2, - opacity: 0.8, - fontFamily: theme.typography.fonts.mono, - fontSize: 11, - border: theme.base === 'dark' ? theme.color.darkest : theme.color.lightest, - color: theme.base === 'dark' ? theme.color.lightest : theme.color.darkest, - backgroundColor: theme.base === 'dark' ? 'black' : theme.color.light, - boxSizing: 'border-box', - lineHeight: '17px', -})); - -export const Image = styled.img` - max-width: 100%; - margin-top: 1em; -`; - -export const Background = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - z-index: 0; - pointer-events: none; -`; - -export const circle1Anim = keyframes` - 0% { transform: translate(0px, 0px) } - 50% { transform: translate(120px, 0px) } - 100% { transform: translate(0px, 0px) } -`; - -export const Circle1 = styled.div` - position: absolute; - width: 350px; - height: 350px; - left: -160px; - top: -260px; - background: radial-gradient( - circle at center, - rgba(255, 119, 119, 1) 0%, - rgba(255, 119, 119, 0) 70% - ); - animation: ${circle1Anim} 8s linear infinite; - animation-timing-function: ease-in-out; - z-index: 2; -`; - -export const circle2Anim = keyframes` - 0% { transform: translate(0px, 0px) } - 33% { transform: translate(-64px, 0px) } - 66% { transform: translate(120px, 0px) } - 100% { transform: translate(0px, 0px) } -`; - -export const Circle2 = styled.div` - position: absolute; - width: 350px; - height: 350px; - left: -54px; - top: -250px; - background: radial-gradient( - circle at center, - rgba(253, 255, 147, 1) 0%, - rgba(253, 255, 147, 0) 70% - ); - animation: ${circle2Anim} 12s linear infinite; - animation-timing-function: ease-in-out; - z-index: 3; -`; - -export const circle3Anim = keyframes` - 0% { transform: translate(0px, 0px) } - 50% { transform: translate(-120px, 0px) } - 100% { transform: translate(0px, 0px) } -`; - -export const Circle3 = styled.div` - position: absolute; - width: 350px; - height: 350px; - left: 150px; - top: -220px; - background: radial-gradient( - circle at center, - rgba(119, 255, 247, 0.8) 0%, - rgba(119, 255, 247, 0) 70% - ); - animation: ${circle3Anim} 4s linear infinite; - animation-timing-function: ease-in-out; - z-index: 4; -`; - -export const ButtonsWrapper = styled.div` - box-sizing: border-box; - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin-top: 4px; -`; - -export const Step2Text = styled.div` - margin-bottom: 4px; -`; diff --git a/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.tsx b/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.tsx deleted file mode 100644 index 38b44f00a037..000000000000 --- a/code/addons/onboarding/src/features/WriteStoriesModal/WriteStoriesModal.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import React, { useCallback, useState, type FC } from 'react'; -import useMeasure from 'react-use-measure'; -import { - Background, - ButtonsWrapper, - Circle1, - Circle2, - Circle3, - Content, - Header, - Image, - Main, - ModalContent, - ModalTitle, - ModalWrapper, - SpanHighlight, - Step2Text, -} from './WriteStoriesModal.styled'; -import { Button } from '../../components/Button/Button'; -import { SyntaxHighlighter } from '../../components/SyntaxHighlighter/SyntaxHighlighter'; -import { List } from '../../components/List/List'; -import { ListItem } from '../../components/List/ListItem/ListItem'; -import { useGetButtonPath } from './hooks/useGetButtonPath'; -import { useGetWarningButtonStatus } from './hooks/useGetWarningButtonStatus'; -import { useGetBackdropBoundary } from './hooks/useGetBackdropBoundary'; -import titleSidebarImg from './assets/01-title-sidebar.png'; -import storyNameSidebarImg from './assets/02-story-name-sidebar.png'; -import argsImg from './assets/03-args.png'; -import type { API, AddonStore } from '@storybook/manager-api'; -import { STORYBOOK_ADDON_ONBOARDING_CHANNEL } from '../../constants'; -import { useTheme } from '@storybook/theming'; -import type { CodeSnippets } from './code/types'; -import { BookmarkHollowIcon, CrossIcon } from '@storybook/icons'; -import { Modal } from '@storybook/components'; - -// TODO: Add warning if backdropBoundary && !warningButtonStatus?.data is not true. -// backdropBoundary && !warningButtonStatus?.data - -interface WriteStoriesModalProps { - onFinish: () => void; - api: API; - addonsStore: AddonStore; - codeSnippets: CodeSnippets; - skipOnboarding: () => void; - container?: HTMLElement; -} - -export const WriteStoriesModal: FC = ({ - onFinish, - api, - addonsStore, - skipOnboarding, - codeSnippets, - container, -}) => { - const [step, setStep] = useState<'imports' | 'meta' | 'story' | 'args' | 'customStory'>( - 'imports' - ); - const theme = useTheme(); - - const stepIndex = { - imports: 0, - meta: 1, - story: 2, - args: 3, - customStory: 4, - }; - - const [isWarningStoryCopied, setWarningStoryCopied] = useState(false); - - const [clipboardButtonRef, clipboardButtonBounds] = useMeasure(); - - const buttonPath = useGetButtonPath(); - const warningButtonStatus = useGetWarningButtonStatus(step === 'customStory', api, addonsStore); - const backdropBoundary = useGetBackdropBoundary( - 'syntax-highlighter-backdrop', - step === 'customStory' - ); - - const isJavascript = codeSnippets?.language === 'javascript'; - - const copyWarningStory = () => { - const warningContent = codeSnippets.code[3][0].snippet; - navigator.clipboard.writeText(warningContent.replace('// Copy the code below', '')); - setWarningStoryCopied(true); - }; - - const onModalClose = useCallback(() => { - api.emit(STORYBOOK_ADDON_ONBOARDING_CHANNEL, { - step: 'X:SkippedOnboarding', - where: `HowToWriteAStoryModal:${step}`, - type: 'telemetry', - }); - }, [api, step]); - - return ( - - - {codeSnippets ? ( - - ) : null} - {step === 'customStory' && backdropBoundary && !warningButtonStatus?.data && ( - - )} -
- - - - - - -
- - - - How to write a story - - - - - -
- - - {step === 'imports' && ( - <> -
-

Imports

- {isJavascript ? ( -

Import a component. In this case, the Button component.

- ) : ( - <> -

- First, import Meta and{' '} - StoryObj for type safety and autocompletion - in TypeScript stories. -

-

Next, import a component. In this case, the Button component.

- - )} -
- - - )} - {step === 'meta' && ( - <> -
-

Meta

-

- The default export, Meta, contains metadata about this component's stories. - The title field (optional) controls where stories appear in the sidebar. -

- Title property pointing to Storybook's sidebar -
- - - - - - )} - {step === 'story' && ( - <> -
-

Story

-

- Each named export is a story. Its contents specify how the story is rendered - in addition to other configuration options. -

- Story export pointing to the sidebar entry of the story -
- - - - - - )} - {step === 'args' && ( - <> -
-

Args

-

- Args are inputs that are passed to the component, which Storybook uses to - render the component in different states. In React, args = props. They also - specify the initial control values for the story. -

- Args mapped to their controls in Storybook -
- - - - - - )} - {step === 'customStory' && - (!warningButtonStatus?.error ? ( - <> -
-

Create your first story

-

- Now it's your turn. See how easy it is to create your first story by - following these steps below. -

- - - Copy the Warning story. - - - - Open the Button story in your current working directory. - - {buttonPath?.data && ( - // Replace '/' by '/' to properly break line - - {buttonPath.data.replaceAll('/', '/​').replaceAll('\\', '\\​')} - - )} - - - Paste it at the bottom of the file and save. - - -
- - - {warningButtonStatus?.data ? ( - - ) : null} - - - ) : null)} -
-
-
-
-
- ); -}; diff --git a/code/addons/onboarding/src/features/WriteStoriesModal/assets/01-title-sidebar.png b/code/addons/onboarding/src/features/WriteStoriesModal/assets/01-title-sidebar.png deleted file mode 100644 index 064d9995bf1da4d5057d5bc476d781290a213a37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4563 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 diff --git a/code/addons/onboarding/src/features/WriteStoriesModal/assets/02-story-name-sidebar.png b/code/addons/onboarding/src/features/WriteStoriesModal/assets/02-story-name-sidebar.png deleted file mode 100644 index 1d562959c156836793251fd9795b07f19bb0ca82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4503 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 ZiGK7IPv^n{4FB^@P?A@ZD}86~_dlbN9CrW! diff --git a/code/addons/onboarding/src/features/WriteStoriesModal/code/javascript.tsx b/code/addons/onboarding/src/features/WriteStoriesModal/code/javascript.tsx deleted file mode 100644 index 2a91a7972e14..000000000000 --- a/code/addons/onboarding/src/features/WriteStoriesModal/code/javascript.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { CodeSnippets } from './types'; - -const data: CodeSnippets = { - filename: 'Button.stories.js', - language: 'typescript', - code: [ - [ - { - snippet: `import { Button } from './Button';`, - }, - ], - [ - { - snippet: `export default { - title: 'Example/Button', - component: Button, - // ... - };`, - }, - ], - [ - { snippet: `export const Primary = {` }, - { - snippet: `args: { - primary: true, - label: 'Click', - background: 'red' - }`, - toggle: true, - }, - { snippet: `};` }, - ], - [ - { - snippet: `// Copy the code below -export const Warning = { - args: { - primary: true, - label: 'Delete now', - backgroundColor: 'red', - } -};`, - }, - ], - ], -}; - -export default data; diff --git a/code/addons/onboarding/src/features/WriteStoriesModal/code/types.ts b/code/addons/onboarding/src/features/WriteStoriesModal/code/types.ts deleted file mode 100644 index 130b4428f390..000000000000 --- a/code/addons/onboarding/src/features/WriteStoriesModal/code/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type CodeSnippets = { - framework?: string; - language: 'javascript' | 'typescript'; - filename: string; - code: { snippet: string; toggle?: boolean }[][]; -}; diff --git a/code/addons/onboarding/src/features/WriteStoriesModal/code/typescript.tsx b/code/addons/onboarding/src/features/WriteStoriesModal/code/typescript.tsx deleted file mode 100644 index 45a578d8c6bd..000000000000 --- a/code/addons/onboarding/src/features/WriteStoriesModal/code/typescript.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { CodeSnippets } from './types'; - -const data: CodeSnippets = { - filename: 'Button.stories.ts', - language: 'typescript', - code: [ - [ - { - snippet: `import type { Meta, StoryObj } from '@storybook/react'; - - import { Button } from './Button';`, - }, - ], - [ - { - snippet: `const meta: Meta = { - title: 'Example/Button', - component: Button, - // ... - }; - - export default meta;`, - }, - ], - [ - { - snippet: `type Story = StoryObj - - - Skip tour - - - - - - - - - - + + + + + Welcome to Storybook + + Storybook helps you develop UI components faster. Learn the basics in a few simple + steps. + + + + + Skip tour + + + + + + + + + ); }; From 8d944758b55aeb5b7ff28417d678daf4e3d7d267 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 19 Jun 2024 21:40:31 +0200 Subject: [PATCH 04/11] Send actual story source in creation event and show it during onboarding --- code/addons/onboarding/src/Onboarding.tsx | 33 ++++++++++++++++++- code/lib/core-events/src/data/save-story.ts | 1 + .../src/utils/save-story/save-story.ts | 1 + 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/code/addons/onboarding/src/Onboarding.tsx b/code/addons/onboarding/src/Onboarding.tsx index 840b8aa50d0f..0c6ea75568df 100644 --- a/code/addons/onboarding/src/Onboarding.tsx +++ b/code/addons/onboarding/src/Onboarding.tsx @@ -1,6 +1,7 @@ +import { SyntaxHighlighter } from '@storybook/components'; import { SAVE_STORY_RESPONSE } from '@storybook/core-events'; import { type API } from '@storybook/manager-api'; -import { ThemeProvider, convert, styled } from '@storybook/theming'; +import { ThemeProvider, convert, styled, themes } from '@storybook/theming'; import React, { useCallback, useEffect, useState } from 'react'; import type { Step } from 'react-joyride'; @@ -27,6 +28,17 @@ const SpanHighlight = styled.span(({ theme }) => ({ lineHeight: '17px', })); +const CodeWrapper = styled.div(({ theme }) => ({ + background: theme.background.content, + borderRadius: 3, + marginTop: 15, + padding: 10, + fontSize: theme.typography.size.s1, + '.linenumber': { + opacity: 0.5, + }, +})); + const theme = convert(); export type StepKey = (typeof STORYBOOK_ADDON_ONBOARDING_STEPS)[number]; @@ -63,6 +75,7 @@ export default function Onboarding({ api }: { api: API }) { const [createNewStoryForm, setCreateNewStoryForm] = useState(); const [createdStory, setCreatedStory] = useState<{ newStoryName: string; + newStorySource: string; sourceFileName: string; } | null>(); @@ -144,6 +157,11 @@ export default function Onboarding({ api }: { api: API }) { return null; } + const source = createdStory?.newStorySource; + const startIndex = source?.lastIndexOf(`export const ${createdStory?.newStoryName}`); + const snippet = source?.slice(startIndex); + const startingLineNumber = source?.slice(0, startIndex).split('\n').length; + const steps: StepDefinition[] = [ { key: '2:Controls', @@ -203,6 +221,19 @@ export default function Onboarding({ api }: { api: API }) { Well done! You just created your first story from the Storybook manager. This automatically added a few lines of code in{' '} {createdStory?.sourceFileName}. + {snippet && ( + + + + {snippet} + + + + )} ), offset: 12, diff --git a/code/lib/core-events/src/data/save-story.ts b/code/lib/core-events/src/data/save-story.ts index 8dd4c009f044..0ee0a73cffe3 100644 --- a/code/lib/core-events/src/data/save-story.ts +++ b/code/lib/core-events/src/data/save-story.ts @@ -9,6 +9,7 @@ export interface SaveStoryResponsePayload { csfId: string; newStoryId?: string; newStoryName?: string; + newStorySource?: string; sourceFileName?: string; sourceStoryName?: string; } diff --git a/code/lib/core-server/src/utils/save-story/save-story.ts b/code/lib/core-server/src/utils/save-story/save-story.ts index 03e359e85c03..dbe19c1a586a 100644 --- a/code/lib/core-server/src/utils/save-story/save-story.ts +++ b/code/lib/core-server/src/utils/save-story/save-story.ts @@ -107,6 +107,7 @@ export function initializeSaveStory(channel: Channel, options: Options, coreConf csfId, newStoryId, newStoryName, + newStorySource: code, sourceFileName, sourceStoryName, }, From 7ea47a5cab235b5f4eb1bec3ea63bf4f819a2ff0 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 20 Jun 2024 08:47:31 +0200 Subject: [PATCH 05/11] Add story for guided tour tooltip --- .../GuidedTour/GuidedTour.stories.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 code/addons/onboarding/src/features/GuidedTour/GuidedTour.stories.tsx diff --git a/code/addons/onboarding/src/features/GuidedTour/GuidedTour.stories.tsx b/code/addons/onboarding/src/features/GuidedTour/GuidedTour.stories.tsx new file mode 100644 index 000000000000..c35a02830a5e --- /dev/null +++ b/code/addons/onboarding/src/features/GuidedTour/GuidedTour.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { GuidedTour } from './GuidedTour'; + +const meta = { + component: GuidedTour, + args: { + onClose: fn(), + onComplete: fn(), + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + step: '1:Intro', + steps: [ + { + key: '1:Intro', + title: 'Welcome', + content: 'Welcome to the guided tour!', + target: '#storybook-root', + disableBeacon: true, + disableOverlay: true, + }, + { + key: '2:Controls', + title: 'Controls', + content: "Can't reach this step", + target: '#storybook-root', + disableBeacon: true, + disableOverlay: true, + }, + ], + }, +}; From de082068147328ebf5ebcff677b2146fcac654fb Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 21 Jun 2024 00:41:10 +0200 Subject: [PATCH 06/11] Replace WelcomeModal with SplashScreen --- code/addons/onboarding/src/Onboarding.tsx | 23 +-- .../SplashScreen/SplashScreen.stories.tsx | 15 ++ .../features/SplashScreen/SplashScreen.tsx | 195 ++++++++++++++++++ .../features/WelcomeModal/StorybookLogo.tsx | 26 --- .../WelcomeModal/WelcomeModal.stories.tsx | 42 ---- .../WelcomeModal/WelcomeModal.styled.tsx | 154 -------------- .../features/WelcomeModal/WelcomeModal.tsx | 67 ------ 7 files changed, 221 insertions(+), 301 deletions(-) create mode 100644 code/addons/onboarding/src/features/SplashScreen/SplashScreen.stories.tsx create mode 100644 code/addons/onboarding/src/features/SplashScreen/SplashScreen.tsx delete mode 100644 code/addons/onboarding/src/features/WelcomeModal/StorybookLogo.tsx delete mode 100644 code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.stories.tsx delete mode 100644 code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.styled.tsx delete mode 100644 code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.tsx diff --git a/code/addons/onboarding/src/Onboarding.tsx b/code/addons/onboarding/src/Onboarding.tsx index 0c6ea75568df..b70cde0ef4f4 100644 --- a/code/addons/onboarding/src/Onboarding.tsx +++ b/code/addons/onboarding/src/Onboarding.tsx @@ -11,7 +11,7 @@ import type { STORYBOOK_ADDON_ONBOARDING_STEPS } from './constants'; import { STORYBOOK_ADDON_ONBOARDING_CHANNEL } from './constants'; import { HighlightElement } from './components/HighlightElement/HighlightElement'; -import { WelcomeModal } from './features/WelcomeModal/WelcomeModal'; +import { SplashScreen } from './features/SplashScreen/SplashScreen'; const SpanHighlight = styled.span(({ theme }) => ({ display: 'inline-flex', @@ -261,17 +261,16 @@ export default function Onboarding({ api }: { api: API }) { }} /> )} - setStep('2:Controls')} - onSkip={disableOnboarding} - /> - + {step === '1:Intro' ? ( + setStep('2:Controls')} /> + ) : ( + + )} ); } diff --git a/code/addons/onboarding/src/features/SplashScreen/SplashScreen.stories.tsx b/code/addons/onboarding/src/features/SplashScreen/SplashScreen.stories.tsx new file mode 100644 index 000000000000..02fc3c3bd919 --- /dev/null +++ b/code/addons/onboarding/src/features/SplashScreen/SplashScreen.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { SplashScreen } from './SplashScreen'; + +const meta = { + component: SplashScreen, + args: { + onDismiss: () => {}, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/code/addons/onboarding/src/features/SplashScreen/SplashScreen.tsx b/code/addons/onboarding/src/features/SplashScreen/SplashScreen.tsx new file mode 100644 index 000000000000..a4309f34eeef --- /dev/null +++ b/code/addons/onboarding/src/features/SplashScreen/SplashScreen.tsx @@ -0,0 +1,195 @@ +import { ArrowRightIcon } from '@storybook/icons'; +import { styled, keyframes } from '@storybook/theming'; +import React, { useCallback, useEffect, useState } from 'react'; + +const fadeIn = keyframes({ + from: { + opacity: 0, + }, + to: { + opacity: 1, + }, +}); + +const slideIn = keyframes({ + from: { + transform: 'translate(0, -20px)', + opacity: 0, + }, + to: { + transform: 'translate(0, 0)', + opacity: 1, + }, +}); + +const scaleIn = keyframes({ + from: { + opacity: 0, + transform: 'scale(0.8)', + }, + to: { + opacity: 1, + transform: 'scale(1)', + }, +}); + +const rotate = keyframes({ + '0%': { + transform: 'rotate(0deg)', + }, + '100%': { + transform: 'rotate(360deg)', + }, +}); + +const Wrapper = styled.div<{ visible: boolean }>(({ visible }) => ({ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + display: 'flex', + opacity: visible ? 1 : 0, + alignItems: 'center', + justifyContent: 'center', + zIndex: 1000, + transition: 'opacity 1s 0.5s', +})); + +const Backdrop = styled.div({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + animation: `${fadeIn} 2s`, + background: ` + radial-gradient(90% 90%, #ff4785 0%, #db5698 30%, #1ea7fdcc 100%), + radial-gradient(circle, #ff4785 0%, transparent 80%), + radial-gradient(circle at 30% 40%, #fc521f99 0%, #fc521f66 20%, transparent 40%), + radial-gradient(circle at 75% 75%, #fc521f99 0%, #fc521f77 18%, transparent 30%)`, + '&::before': { + opacity: 0.5, + background: ` + radial-gradient(circle at 30% 40%, #fc521f99 0%, #fc521f66 10%, transparent 20%), + radial-gradient(circle at 75% 75%, #fc521f99 0%, #fc521f77 8%, transparent 20%)`, + content: '""', + position: 'absolute', + top: '-50vw', + left: '-50vh', + transform: 'translate(-50%, -50%)', + width: 'calc(100vw + 100vh)', + height: 'calc(100vw + 100vh)', + animation: `${rotate} 12s linear infinite`, + }, +}); + +const Content = styled.div<{ visible: boolean }>(({ visible }) => ({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + color: 'white', + textAlign: 'center', + maxWidth: 400, + opacity: visible ? 1 : 0, + transition: 'opacity 0.5s', + + h1: { + fontSize: 45, + fontWeight: 'bold', + animation: `${slideIn} 1.5s 1s backwards`, + }, +})); + +const RadialButton = styled.button({ + display: 'inline-flex', + position: 'relative', + alignItems: 'center', + justifyContent: 'center', + marginTop: 40, + width: 48, + height: 48, + padding: 0, + borderRadius: '50%', + border: 0, + outline: 'none', + background: 'rgba(255, 255, 255, 0.3)', + cursor: 'pointer', + transition: 'background 0.2s', + animation: `${scaleIn} 1.5s 1.5s backwards`, + + '&:hover, &:focus': { + background: 'rgba(255, 255, 255, 0.4)', + }, +}); + +const ArrowIcon = styled(ArrowRightIcon)({ + width: 30, + color: 'white', +}); + +const ProgressCircle = styled.svg<{ progress?: boolean; spinner?: boolean }>(({ progress }) => ({ + position: 'absolute', + top: -1, + left: -1, + width: `50px!important`, + height: `50px!important`, + transform: 'rotate(-90deg)', + color: 'white', + circle: { + r: '24', + cx: '25', + cy: '25', + fill: 'transparent', + stroke: progress ? 'currentColor' : 'transparent', + strokeWidth: '1', + strokeLinecap: 'round', + strokeDasharray: Math.PI * 48, + }, +})); + +interface SplashScreenProps { + onDismiss: () => void; +} + +export const SplashScreen = ({ onDismiss }: SplashScreenProps) => { + const [progress, setProgress] = useState(-30); + const [visible, setVisible] = useState(true); + const ready = progress >= 100; + + const dismiss = useCallback(() => { + setVisible(false); + const timeout = setTimeout(onDismiss, 1500); + return () => clearTimeout(timeout); + }, [onDismiss]); + + useEffect(() => { + const interval = setInterval(() => setProgress((prev) => prev + 0.5), 30); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (ready) dismiss(); + }, [ready, dismiss]); + + return ( + + + +

Meet your new frontend workshop

+ + + + + + + + + +
+
+ ); +}; diff --git a/code/addons/onboarding/src/features/WelcomeModal/StorybookLogo.tsx b/code/addons/onboarding/src/features/WelcomeModal/StorybookLogo.tsx deleted file mode 100644 index bcfbba836ab9..000000000000 --- a/code/addons/onboarding/src/features/WelcomeModal/StorybookLogo.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -export function StorybookLogo() { - return ( - - - - - - - - - - - - - ); -} diff --git a/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.stories.tsx b/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.stories.tsx deleted file mode 100644 index 791698211dfe..000000000000 --- a/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.stories.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; - -import { WelcomeModal } from './WelcomeModal'; - -const meta: Meta = { - component: WelcomeModal, - // This decorator is used to show the modal in the side by side view - decorators: [ - (Story, context) => { - const [container, setContainer] = useState(undefined); - - if (context.globals.theme === 'side-by-side') { - return ( -
{ - if (element) { - setContainer(element); - } - }} - style={{ - width: '100%', - height: '100%', - minHeight: '600px', - transform: 'translateZ(0)', - }} - > - {Story({ args: { ...context.args, container } })} -
- ); - } - - return Story(); - }, - ], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.styled.tsx b/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.styled.tsx deleted file mode 100644 index 02d5eba137c9..000000000000 --- a/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.styled.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { ArrowRightIcon } from '@storybook/icons'; -import { keyframes, styled } from '@storybook/theming'; -import { Modal } from '@storybook/components'; - -export const ModalWrapper = styled(Modal)<{ visible?: boolean }>` - background: white; - opacity: ${({ visible }) => (visible ? 1 : 0)}; - transition: opacity 0.2s; - z-index: 10; -`; - -export const ModalContentWrapper = styled.div` - border-radius: 5px; - display: flex; - flex-direction: column; - align-items: center; - height: 100%; - justify-content: space-between; -`; - -export const TopContent = styled.div` - display: flex; - flex: 1; - flex-direction: column; - align-items: center; - justify-content: center; -`; - -export const Title = styled.h1` - margin: 0; - margin-top: 20px; - margin-bottom: 5px; - color: ${({ theme }) => theme.color.darkest}; - font-weight: ${({ theme }) => theme.typography.weight.bold}; - font-size: ${({ theme }) => theme.typography.size.m1}px; - line-height: ${({ theme }) => theme.typography.size.m3}px; -`; - -export const Description = styled.p` - margin: 0; - margin-bottom: 20px; - max-width: 320px; - text-align: center; - font-size: ${({ theme }) => theme.typography.size.s2}px; - font-weight: ${({ theme }) => theme.typography.weight.regular}; - line-height: ${({ theme }) => theme.typography.size.m1}px; - color: ${({ theme }) => theme.color.darker}; -`; - -export const SkipButton = styled.button` - all: unset; - cursor: pointer; - font-size: 13px; - color: #798186; - padding-bottom: 20px; - - &:focus-visible { - outline: auto; - } -`; - -export const StyledIcon = styled(ArrowRightIcon)` - margin-left: 2px; - height: 10px; -`; - -export const Background = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: -1; - overflow: hidden; -`; - -export const circle1Anim = keyframes` - 0% { transform: translate(0px, 0px) } - 50% { transform: translate(-200px, 0px) } - 100% { transform: translate(0px, 0px) } -`; - -export const Circle1 = styled.div` - position: absolute; - width: 1200px; - height: 1200px; - left: -200px; - top: -900px; - background: radial-gradient( - circle at center, - rgba(253, 255, 147, 1) 0%, - rgba(253, 255, 147, 0) 70% - ); - animation: ${circle1Anim} 4s linear infinite; - animation-timing-function: ease-in-out; - z-index: 3; -`; - -export const circle2Anim = keyframes` - 0% { transform: translate(0px, 0px) } - 50% { transform: translate(400px, 0px) } - 100% { transform: translate(0px, 0px) } -`; - -export const Circle2 = styled.div` - position: absolute; - width: 1200px; - height: 1200px; - left: -600px; - top: -840px; - background: radial-gradient( - circle at center, - rgba(255, 119, 119, 1) 0%, - rgba(255, 119, 119, 0) 70% - ); - animation: ${circle2Anim} 6s linear infinite; - animation-timing-function: ease-in-out; - z-index: 2; -`; - -export const circle3Anim = keyframes` - 0% { transform: translate(600px, -40px) } - 50% { transform: translate(600px, -200px) } - 100% { transform: translate(600px, -40px) } -`; - -export const Circle3 = styled.div` - position: absolute; - width: 1200px; - height: 1200px; - left: -600px; - top: -840px; - background: radial-gradient( - circle at center, - rgba(119, 255, 247, 0.8) 0%, - rgba(119, 255, 247, 0) 70% - ); - animation: ${circle3Anim} 4s linear infinite; - animation-timing-function: ease-in-out; - z-index: 4; -`; - -export const StyledTitle = styled.h2` - color: #000; - font-weight: 700; - font-size: 20px; - line-height: 20px; -`; -export const StyledDescription = styled.p` - font-size: 14px; - font-weight: 400; - line-height: 20px; - color: #454e54; -`; diff --git a/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.tsx b/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.tsx deleted file mode 100644 index 353b783d2fc1..000000000000 --- a/code/addons/onboarding/src/features/WelcomeModal/WelcomeModal.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { styled } from '@storybook/theming'; -import React, { useEffect, useState } from 'react'; - -import type { StepKey } from '../../Onboarding'; -import { Button } from '../../components/Button/Button'; -import { StorybookLogo } from './StorybookLogo'; -import { - ModalContentWrapper, - SkipButton, - StyledIcon, - Title, - Description, - Background, - Circle1, - Circle2, - Circle3, - TopContent, - ModalWrapper, -} from './WelcomeModal.styled'; - -interface WelcomeModalProps { - step: StepKey; - onProceed: () => void; - onSkip: () => void; - container?: HTMLElement; -} - -export const WelcomeModal = ({ step, onProceed, onSkip, container }: WelcomeModalProps) => { - const [rendered, setRendered] = useState(true); - const [visible, setVisible] = useState(true); - - useEffect(() => { - if (step !== '1:Intro') { - setVisible(false); - setTimeout(setRendered, 500, false); - } - }, [step]); - - if (!rendered) return null; - - return ( - - - - - Welcome to Storybook - - Storybook helps you develop UI components faster. Learn the basics in a few simple - steps. - - - - - Skip tour - - - - - - - - - - ); -}; From 4a06b583fc9219639eb2dba26eed6b3f0412b1ba Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Mon, 24 Jun 2024 17:03:34 +0200 Subject: [PATCH 07/11] Fix tooltip color on dark mode --- code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx b/code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx index d345f3fa0268..926698641efb 100644 --- a/code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx +++ b/code/addons/onboarding/src/features/GuidedTour/GuidedTour.tsx @@ -76,12 +76,12 @@ export function GuidedTour({ tooltip: { width: 280, color: theme.color.lightest, - background: theme.base === 'dark' ? '#292A2C' : theme.color.secondary, + background: theme.color.secondary, }, options: { zIndex: 9998, primaryColor: theme.color.secondary, - arrowColor: theme.base === 'dark' ? '#292A2C' : theme.color.secondary, + arrowColor: theme.color.secondary, }, }} /> From f153cf4f6ae832a0a4e8204961cdcb8dadc03796 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 25 Jun 2024 11:08:47 +0200 Subject: [PATCH 08/11] Rename property to better reflect its contents --- code/addons/onboarding/src/Onboarding.tsx | 4 ++-- code/lib/core-events/src/data/save-story.ts | 2 +- code/lib/core-server/src/utils/save-story/save-story.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/code/addons/onboarding/src/Onboarding.tsx b/code/addons/onboarding/src/Onboarding.tsx index b70cde0ef4f4..f5a49e23bb00 100644 --- a/code/addons/onboarding/src/Onboarding.tsx +++ b/code/addons/onboarding/src/Onboarding.tsx @@ -75,7 +75,7 @@ export default function Onboarding({ api }: { api: API }) { const [createNewStoryForm, setCreateNewStoryForm] = useState(); const [createdStory, setCreatedStory] = useState<{ newStoryName: string; - newStorySource: string; + sourceFileContent: string; sourceFileName: string; } | null>(); @@ -157,7 +157,7 @@ export default function Onboarding({ api }: { api: API }) { return null; } - const source = createdStory?.newStorySource; + const source = createdStory?.sourceFileContent; const startIndex = source?.lastIndexOf(`export const ${createdStory?.newStoryName}`); const snippet = source?.slice(startIndex); const startingLineNumber = source?.slice(0, startIndex).split('\n').length; diff --git a/code/lib/core-events/src/data/save-story.ts b/code/lib/core-events/src/data/save-story.ts index 0ee0a73cffe3..8bb4091e513e 100644 --- a/code/lib/core-events/src/data/save-story.ts +++ b/code/lib/core-events/src/data/save-story.ts @@ -9,7 +9,7 @@ export interface SaveStoryResponsePayload { csfId: string; newStoryId?: string; newStoryName?: string; - newStorySource?: string; + sourceFileContent?: string; sourceFileName?: string; sourceStoryName?: string; } diff --git a/code/lib/core-server/src/utils/save-story/save-story.ts b/code/lib/core-server/src/utils/save-story/save-story.ts index dbe19c1a586a..5c3f537a9c90 100644 --- a/code/lib/core-server/src/utils/save-story/save-story.ts +++ b/code/lib/core-server/src/utils/save-story/save-story.ts @@ -107,7 +107,7 @@ export function initializeSaveStory(channel: Channel, options: Options, coreConf csfId, newStoryId, newStoryName, - newStorySource: code, + sourceFileContent: code, sourceFileName, sourceStoryName, }, From f1dbb7ec5e83411329c89c27100b482a58736a57 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 25 Jun 2024 11:15:18 +0200 Subject: [PATCH 09/11] Ensure new story was created before continuing to next step --- code/addons/onboarding/src/Onboarding.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/onboarding/src/Onboarding.tsx b/code/addons/onboarding/src/Onboarding.tsx index f5a49e23bb00..6c87566a306b 100644 --- a/code/addons/onboarding/src/Onboarding.tsx +++ b/code/addons/onboarding/src/Onboarding.tsx @@ -140,7 +140,7 @@ export default function Onboarding({ api }: { api: API }) { useEffect(() => { return api.on(SAVE_STORY_RESPONSE, ({ payload, success }) => { - if (!success) return; + if (!success || !payload?.newStoryName) return; setCreatedStory(payload); setShowConfetti(true); setStep('5:StoryCreated'); From b34fcdd682b27c3db39b7dff5788c849da12e7d0 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 25 Jun 2024 11:31:19 +0200 Subject: [PATCH 10/11] Add addon-onboarding to Angular and Vue3 generators --- code/lib/cli/src/generators/ANGULAR/index.ts | 1 + code/lib/cli/src/generators/VUE3/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/code/lib/cli/src/generators/ANGULAR/index.ts b/code/lib/cli/src/generators/ANGULAR/index.ts index 6a770ef1922c..ef257ab35236 100644 --- a/code/lib/cli/src/generators/ANGULAR/index.ts +++ b/code/lib/cli/src/generators/ANGULAR/index.ts @@ -68,6 +68,7 @@ const generator: Generator<{ projectName: string }> = async ( }, 'angular', { + extraAddons: [`@storybook/addon-onboarding`], ...(useCompodoc && { extraPackages: ['@compodoc/compodoc', '@storybook/addon-docs'] }), addScripts: false, componentsDestinationPath: root ? `${root}/src/stories` : undefined, diff --git a/code/lib/cli/src/generators/VUE3/index.ts b/code/lib/cli/src/generators/VUE3/index.ts index 75a632b4072a..c919b81cbbce 100644 --- a/code/lib/cli/src/generators/VUE3/index.ts +++ b/code/lib/cli/src/generators/VUE3/index.ts @@ -4,6 +4,7 @@ import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { await baseGenerator(packageManager, npmOptions, options, 'vue3', { + extraAddons: [`@storybook/addon-onboarding`], extraPackages: async ({ builder }) => { return builder === CoreBuilder.Webpack5 ? ['vue-loader@^17.0.0', '@vue/compiler-sfc@^3.2.0'] From 963ce3fc15bf65b5b8b3af5ac7bcb8656a556038 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 28 Jun 2024 12:03:20 +0200 Subject: [PATCH 11/11] Remove unused SyntaxHighlighter component --- .../Snippet/Snippet.styled.tsx | 9 -- .../SyntaxHighlighter/Snippet/Snippet.tsx | 75 ----------- .../SyntaxHighlighter.stories.tsx | 84 ------------- .../SyntaxHighlighter.styled.tsx | 54 -------- .../SyntaxHighlighter/SyntaxHighlighter.tsx | 117 ------------------ 5 files changed, 339 deletions(-) delete mode 100644 code/addons/onboarding/src/components/SyntaxHighlighter/Snippet/Snippet.styled.tsx delete mode 100644 code/addons/onboarding/src/components/SyntaxHighlighter/Snippet/Snippet.tsx delete mode 100644 code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.stories.tsx delete mode 100644 code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.styled.tsx delete mode 100644 code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx diff --git a/code/addons/onboarding/src/components/SyntaxHighlighter/Snippet/Snippet.styled.tsx b/code/addons/onboarding/src/components/SyntaxHighlighter/Snippet/Snippet.styled.tsx deleted file mode 100644 index 57a22c42d5d8..000000000000 --- a/code/addons/onboarding/src/components/SyntaxHighlighter/Snippet/Snippet.styled.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { styled } from '@storybook/theming'; -import { motion } from 'framer-motion'; - -export const SnippetWrapper = styled(motion.div)` - position: relative; - padding-top: 12px; - padding-bottom: 12px; - min-height: 57px; -`; diff --git a/code/addons/onboarding/src/components/SyntaxHighlighter/Snippet/Snippet.tsx b/code/addons/onboarding/src/components/SyntaxHighlighter/Snippet/Snippet.tsx deleted file mode 100644 index 82f5c94bbeea..000000000000 --- a/code/addons/onboarding/src/components/SyntaxHighlighter/Snippet/Snippet.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { motion } from 'framer-motion'; -import { Fragment, forwardRef } from 'react'; -import { SnippetWrapper } from './Snippet.styled'; -import React from 'react'; -import { SyntaxHighlighter as StorybookSyntaxHighlighter } from '@storybook/components'; - -interface Props { - content: { snippet: string; toggle?: boolean }[]; - active: boolean; - open?: boolean; -} - -const wrapperVariants = { - default: { - filter: 'grayscale(1)', - opacity: 0.5, - }, - active: { - filter: 'grayscale(0)', - opacity: 1, - }, -}; - -export const Snippet = forwardRef(function Snippet( - { active, content, open }, - ref -) { - const customStyle = { - fontSize: '0.8125rem', - lineHeight: '1.1875rem', - }; - - return ( - - {content.map(({ toggle, snippet }, i) => ( - - {toggle === undefined && ( - - {snippet} - - )} - - {toggle && !open && ( - - {` // ...`} - - )} - - {toggle && open && ( - - - {snippet} - - - )} - - ))} - - ); -}); diff --git a/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.stories.tsx b/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.stories.tsx deleted file mode 100644 index 5d29d4a42c22..000000000000 --- a/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.stories.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { SyntaxHighlighter } from './SyntaxHighlighter'; -import React from 'react'; -import type { CodeSnippets } from '../../features/WriteStoriesModal/code/types'; - -const meta: Meta = { - component: SyntaxHighlighter, - parameters: { - chromatic: { delay: 300 }, - }, -}; - -export default meta; - -type Story = StoryObj; - -const data: CodeSnippets['code'] = [ - [ - { - snippet: `import type { Meta, StoryObj } from '@storybook/react'; - -import { Button } from './Button';`, - }, - ], - [ - { - snippet: `const meta: Meta = { - title: 'Example/Button', - component: Button, - // ... -}; - -export default meta;`, - }, - ], - [ - { snippet: `export const Primary: Story = {` }, - { - snippet: `args: { - primary: true, - label: 'Click', - background: 'red' - }`, - toggle: true, - }, - { snippet: `};` }, - ], - [ - { - snippet: `// Copy the code below - -export const Warning: Story = { - args: { - primary: true, - label: 'Delete now', - backgroundColor: 'red', - } -};`, - }, - ], -]; - -export const Default: Story = { - render: (args) => { - const [activeStep, setActiveStep] = React.useState(0); - - return ( -
- - - - -
- ); - }, - args: { - data: data, - activeStep: 0, - width: 480, - filename: 'Button.stories.tsx', - }, -}; diff --git a/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.styled.tsx b/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.styled.tsx deleted file mode 100644 index 29beed376ed0..000000000000 --- a/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.styled.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { styled } from '@storybook/theming'; -import { motion } from 'framer-motion'; - -export const Code = styled(motion.div)` - position: relative; - z-index: 2; -`; - -export const SnippetWrapperFirst = styled(motion.div)` - position: relative; - padding-top: 10px; - padding-bottom: 10px; -`; - -export const SnippetWrapper = styled(motion.div)` - position: relative; - padding-top: 12px; - padding-bottom: 12px; -`; - -export const Container = styled.div<{ width: number }>` - position: relative; - box-sizing: border-box; - background: #171c23; - width: ${({ width }) => width}px; - height: 100%; - overflow: hidden; - padding-left: 15px; - padding-right: 15px; - padding-top: 4px; - border-left: ${({ theme }) => (theme.base === 'dark' ? 1 : 0)}px solid #fff2; - border-bottom: ${({ theme }) => (theme.base === 'dark' ? 1 : 0)}px solid #fff2; - border-top: ${({ theme }) => (theme.base === 'dark' ? 1 : 0)}px solid #fff2; - border-radius: 6px 0 0 6px; - overflow: hidden; - - && { - pre { - background: transparent !important; - margin: 0 !important; - padding: 0 !important; - } - } -`; - -export const Backdrop = styled(motion.div)` - background: #143046; - position: absolute; - z-index: 1; - left: 0; - top: 44px; - width: 100%; - height: 81px; -`; diff --git a/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx b/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx deleted file mode 100644 index a1038465b926..000000000000 --- a/code/addons/onboarding/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { createRef, useCallback, useLayoutEffect, useMemo, useState } from 'react'; -import { Backdrop, Code, Container, SnippetWrapperFirst } from './SyntaxHighlighter.styled'; -import { Snippet } from './Snippet/Snippet'; -import { ThemeProvider, ensure, themes } from '@storybook/theming'; -import { SyntaxHighlighter as StorybookSyntaxHighlighter } from '@storybook/components'; - -type SyntaxHighlighterProps = { - data: { snippet: string; toggle?: boolean }[][]; - activeStep: number; - width: number; - filename: string; -}; - -type StepsProps = { - yPos: number; - backdropHeight: number; - index: number; - open: boolean; -}; - -export const SyntaxHighlighter = ({ - activeStep, - data, - width, - filename, -}: SyntaxHighlighterProps) => { - const [steps, setSteps] = useState([]); - - const refs = useMemo(() => data.map(() => createRef()), [data]); - - const getYPos = (idx: number) => { - let yPos = 0; - for (let i = 0; i < idx; i++) { - yPos -= refs[i].current!.getBoundingClientRect().height; - } - return yPos; - }; - - const setNewSteps = useCallback(() => { - const newSteps = data.flatMap((content, i) => { - const backdropHeight = refs[i].current!.getBoundingClientRect().height; - const finalSteps = [ - { - yPos: getYPos(i), - backdropHeight, - index: i, - open: false, - }, - ]; - - if (content.length > 1) { - finalSteps.push({ - yPos: getYPos(i), - backdropHeight, - index: i, - open: true, - }); - } - - return finalSteps; - }); - - setSteps(newSteps); - }, [data]); - - useLayoutEffect(() => { - // Call setNewSteps every time height of the refs elements changes - const resizeObserver = new ResizeObserver(() => { - setNewSteps(); - }); - - refs.forEach((ref) => { - resizeObserver.observe(ref.current!); - }); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - const customStyle = { - fontSize: '0.8125rem', - lineHeight: '1.1875rem', - }; - - return ( - - - - - - {'// ' + filename} - - - {data.map((content, idx: number) => ( - idx ? true : steps[activeStep]?.open ?? false} - content={content} - /> - ))} - - - - - ); -};