Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,10 @@ export const ContentUndefined: Story = {
content: content3,
},
};

export const CopyPrompt: Story = {
args: {
content: content3,
copyPrompt: 'This is a prompt'
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const Link = mdxComponents.A;
interface CodeSnippetsClientProps {
content: CodeSnippetsProps[] | null;
copyEvent?: string;
copyPrompt?: string;
snippetPath?: string;
variant?: "default" | "new-users";
}
Expand Down Expand Up @@ -108,6 +109,7 @@ function ActiveInfo({ activeTab }: { activeTab: string | null }) {
export const CodeSnippetsClient: FC<CodeSnippetsClientProps> = ({
content,
copyEvent,
copyPrompt,
snippetPath,
variant = "default",
}) => {
Expand Down Expand Up @@ -206,6 +208,7 @@ export const CodeSnippetsClient: FC<CodeSnippetsClientProps> = ({
return (
<CodeSnippetsWrapper
copy={activeContent?.raw ?? ''}
copyPrompt={copyPrompt}
top={
tabs && tabs.length > 1 ? (
<Tabs activeTab={activeTab} onTabChange={handleTabChange} tabs={tabs} />
Expand Down
9 changes: 7 additions & 2 deletions apps/frontpage/components/docs/mdx/code-snippets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ interface LocalProps {
* locate it and learn about it. Currently only used for "create" code snippets.
*/
variant?: "default" | "new-users";

/**
* An optional AI prompt to show as a "Copy prompt" button next to the copy button.
*/
copyPrompt?: string;
}

export async function CodeSnippets({ path, activeVersion, copyEvent, variant = "default" }: LocalProps) {
export async function CodeSnippets({ path, activeVersion, copyEvent, variant = "default", copyPrompt }: LocalProps) {
// If there is no path or active version, return null
// TODO: Perhaps we could return a message saying that there are no code snippets
if (!path || !activeVersion) return null;
Expand All @@ -34,5 +39,5 @@ export async function CodeSnippets({ path, activeVersion, copyEvent, variant = "

// Render the Code Snippets component
// This happen on the client since we need to use the context
return <CodeSnippetsClient content={codeSnippetsContent} variant={variant} copyEvent={copyEvent} snippetPath={path} />;
return <CodeSnippetsClient content={codeSnippetsContent} variant={variant} copyEvent={copyEvent} snippetPath={path} copyPrompt={copyPrompt} />;
}
36 changes: 36 additions & 0 deletions apps/frontpage/components/home/hero/copy-prompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CheckIcon, WandIcon } from '@storybook/icons';
import type { FC } from 'react';
import { useState } from 'react';
import copy from 'copy-to-clipboard';
import { useAnalytics } from '../../../lib/analytics';

const PROMPT_TEXT =
'Install Storybook in this project with `npx storybook init` and follow its instructions';

export const CopyPrompt: FC = () => {
const [state, setState] = useState(false);
const track = useAnalytics();

const onClick = () => {
copy(PROMPT_TEXT);
setState(true);
track('CopyPromptClick', {
prompt: PROMPT_TEXT,
});
setTimeout(() => {
setState(false);
}, 2000);
};

return (
<button
className="hidden items-center gap-1.5 font-mono text-sm text-white/60 transition-colors hover:text-white md:flex"
onClick={onClick}
title="Copy AI prompt to install Storybook"
type="button"
>
{ state ? <CheckIcon className="h-3.5 w-3.5" /> : <WandIcon className="h-3.5 w-3.5" /> }
{state ? 'Copied!' : 'Copy agent prompt'}
</button>
);
};
6 changes: 5 additions & 1 deletion apps/frontpage/components/home/hero/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Container, GradientBadge } from '@repo/ui';
import { useAnalytics } from '../../../lib/analytics';
import { Manager } from '../manager';
import { InitCommand } from './init-command';
import { CopyPrompt } from './copy-prompt';
import { Chrome } from './chrome';
import SocialProof from './social-proof';

Expand Down Expand Up @@ -127,7 +128,10 @@ export function Hero({
>
Get Started
</Link>
<InitCommand />
<div className="flex flex-col gap-2">
<InitCommand />
<CopyPrompt />
</div>
</div>
<div className="flex gap-6 sm:gap-10 md:hidden lg:flex">
<a
Expand Down
56 changes: 56 additions & 0 deletions packages/ui/src/code-snippets-wrapper/copy-prompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import { CheckIcon, WandIcon } from '@storybook/icons';
import type { FC } from 'react';
import { useState } from 'react';
import copyToClipboard from 'copy-to-clipboard';
import { cn } from '@repo/utils';

export const CopyPrompt: FC<{
prompt: string;
onClick?: () => void;
variant?: 'default' | 'new-users';
}> = ({ prompt, onClick, variant = 'default' }) => {
const [state, setState] = useState<'idle' | 'copied'>('idle');

const handleClick = (): void => {
copyToClipboard(prompt);
setState('copied');
onClick?.();
setTimeout(() => {
setState('idle');
}, 1000);
};

return (
<button
className={cn(
'ui-flex ui-h-8 ui-select-none ui-items-center ui-gap-1 ui-rounded ui-px-2 ui-text-sm ui-transition-all',
variant === 'default' && 'ui-justify-between',
variant === 'default' &&
'ui-text-slate-600 hover:ui-bg-slate-200 hover:ui-text-slate-900',
variant === 'default' &&
'dark:ui-text-slate-400 dark:hover:ui-bg-slate-800 dark:hover:ui-text-slate-400',
variant === 'new-users' && 'ui-min-w-[10ch] ui-justify-center ui-font-bold',
variant === 'new-users' &&
'ui-bg-zinc-700 hover:ui-bg-zinc-900 ui-text-white',
variant === 'new-users' &&
'dark:ui-bg-slate-100 dark:hover:ui-bg-white dark:ui-text-slate-900 dark:hover:ui-text-black',
)}
onClick={handleClick}
title="Copy AI prompt to install Storybook"
type="button"
aria-label="Copy prompt"
>
{state === 'idle' ? (
<>
<WandIcon /> Copy prompt
</>
) : (
<>
<CheckIcon /> Copied
</>
)}
</button>
);
};
12 changes: 12 additions & 0 deletions packages/ui/src/code-snippets-wrapper/wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import type { FC, ReactNode } from 'react';
import { useAnalytics } from '../analytics';
import { JSIcon, TSIcon, ShellIcon } from './icons';
import { Copy } from './copy';
import { CopyPrompt } from './copy-prompt';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- With an interface, we get this error in ./index: https://github.com/microsoft/TypeScript/issues/5711
type CodeSnippetsWrapperProps = {
children: ReactNode;
copy?: ReactNode;
copyPrompt?: string;
iconLanguage?: 'js' | 'ts' | 'sh' | null;
options?: ReactNode;
title?: string;
Expand All @@ -28,6 +30,7 @@ const languageIcons = {
export const CodeSnippetsWrapper: FC<CodeSnippetsWrapperProps> = ({
children,
copy,
copyPrompt,
iconLanguage = 'js',
options,
title,
Expand Down Expand Up @@ -62,6 +65,15 @@ export const CodeSnippetsWrapper: FC<CodeSnippetsWrapperProps> = ({
variant={variant}
/>
) : null}
{copyPrompt ? (
<CopyPrompt
prompt={copyPrompt}
onClick={() => {
track('CopyPromptClick', { snippetPath });
}}
variant={variant}
/>
) : null}
</div>
</div>
</div>
Expand Down
Loading