Skip to content

Commit 629a2ec

Browse files
feat: docs playground redesign (#566)
* feat: docs playground redesign * fix: playground bug
1 parent ab26056 commit 629a2ec

File tree

13 files changed

+313
-111
lines changed

13 files changed

+313
-111
lines changed

apps/www/public/assets/logo.svg

Lines changed: 1 addition & 1 deletion
Loading

apps/www/src/app/docs/[[...slug]]/page.tsx

Lines changed: 63 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Flex, Headline, Text } from '@raystack/apsara';
22
import { createRelativeLink } from 'fumadocs-ui/mdx';
33
import type { Metadata } from 'next';
44
import { notFound } from 'next/navigation';
5+
import { DemoContextProvider } from '@/components/demo/demo-context';
56
import DocsFooter from '@/components/docs/footer';
67
import DocsNavbar from '@/components/docs/navbar';
78
import { mdxComponents } from '@/components/mdx';
@@ -15,69 +16,77 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
1516
if (!page) notFound();
1617

1718
const MDX = page.data.body;
19+
const content = (page.data._exports?._markdown ?? '') as string;
20+
const hasPlayground = content.includes('<Demo data={playground}');
1821

1922
return (
20-
<Flex
21-
direction='column'
22-
justify='center'
23-
align='center'
24-
className={styles.container}
25-
data-article-content
26-
>
27-
<DocsNavbar
28-
url={page.url}
29-
title={page.data.title}
30-
pageTree={docs.pageTree}
31-
source={page.data.source}
32-
/>
33-
<Flex width='full' align='start'>
34-
<Flex direction='column' align='center' justify='center' width='full'>
35-
<Flex direction='column' className={styles.content} justify='between'>
36-
<Flex direction='column' gap={6}>
37-
<Flex direction='column' gap={3}>
38-
<Headline size='t4'>{page.data.title}</Headline>
39-
<Text size='regular' variant='secondary'>
40-
{page.data.description}
41-
</Text>
42-
</Flex>
43-
<Flex direction='column' className='prose'>
44-
<MDX
45-
components={{
46-
...mdxComponents,
47-
// this allows you to link to other pages with relative file paths
48-
a: createRelativeLink(docs, page)
49-
}}
50-
/>
23+
<DemoContextProvider hasPlayground={hasPlayground} title={page.data.title}>
24+
<Flex
25+
direction='column'
26+
justify='center'
27+
align='center'
28+
className={styles.container}
29+
data-article-content
30+
>
31+
<DocsNavbar
32+
url={page.url}
33+
title={page.data.title}
34+
pageTree={docs.pageTree}
35+
source={page.data.source}
36+
/>
37+
<Flex width='full' align='start'>
38+
<Flex direction='column' align='center' justify='center' width='full'>
39+
<Flex
40+
direction='column'
41+
className={styles.content}
42+
justify='between'
43+
>
44+
<Flex direction='column' gap={6}>
45+
<Flex direction='column' gap={3}>
46+
<Headline size='t4'>{page.data.title}</Headline>
47+
<Text size='regular' variant='secondary'>
48+
{page.data.description}
49+
</Text>
50+
</Flex>
51+
<Flex direction='column' className='prose'>
52+
<MDX
53+
components={{
54+
...mdxComponents,
55+
// this allows you to link to other pages with relative file paths
56+
a: createRelativeLink(docs, page)
57+
}}
58+
/>
59+
</Flex>
5160
</Flex>
61+
<DocsFooter url={page.url} />
5262
</Flex>
53-
<DocsFooter url={page.url} />
5463
</Flex>
55-
</Flex>
56-
<aside
57-
style={{
58-
width: '300px',
59-
height: 'calc(100vh - 50px)',
60-
position: 'sticky',
61-
top: '50px',
62-
padding: '40px 0',
63-
paddingRight: 'var(--rs-space-7)',
64-
display: 'flex',
65-
flexDirection: 'column',
66-
justifyContent: 'center',
67-
alignItems: 'center'
68-
}}
69-
>
70-
<div
64+
<aside
7165
style={{
72-
width: '100%',
73-
height: '70vh'
66+
width: '300px',
67+
height: 'calc(100vh - 50px)',
68+
position: 'sticky',
69+
top: '50px',
70+
padding: '40px 0',
71+
paddingRight: 'var(--rs-space-7)',
72+
display: 'flex',
73+
flexDirection: 'column',
74+
justifyContent: 'center',
75+
alignItems: 'center'
7476
}}
7577
>
76-
<TableOfContents headings={page.data.toc} />
77-
</div>
78-
</aside>
78+
<div
79+
style={{
80+
width: '100%',
81+
height: '70vh'
82+
}}
83+
>
84+
<TableOfContents headings={page.data.toc} />
85+
</div>
86+
</aside>
87+
</Flex>
7988
</Flex>
80-
</Flex>
89+
</DemoContextProvider>
8190
);
8291
}
8392

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client';
2+
import { createContext, ReactNode, useContext, useState } from 'react';
3+
4+
export const DemoContext = createContext<{
5+
openPlayground: boolean;
6+
setOpenPlayground: (open: boolean) => void;
7+
hasPlayground: boolean;
8+
title: string;
9+
}>({
10+
openPlayground: false,
11+
setOpenPlayground: () => null,
12+
hasPlayground: false,
13+
title: ''
14+
});
15+
16+
export const DemoContextProvider = ({
17+
children,
18+
hasPlayground,
19+
title
20+
}: {
21+
children: ReactNode;
22+
hasPlayground: boolean;
23+
title: string;
24+
}) => {
25+
const [openPlayground, setOpenPlayground] = useState(false);
26+
return (
27+
<DemoContext.Provider
28+
value={{ openPlayground, setOpenPlayground, hasPlayground, title }}
29+
>
30+
{children}
31+
</DemoContext.Provider>
32+
);
33+
};
34+
35+
export const useDemoContext = () => {
36+
return useContext(DemoContext);
37+
};

apps/www/src/components/demo/demo-controls.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type PropControlsProps = {
2626
controls: ControlsType;
2727
componentProps: ComponentPropsType;
2828
onPropChange: PropChangeHandlerType;
29+
className?: string;
2930
};
3031

3132
const ICONS_MAP = {
@@ -39,10 +40,11 @@ const ICONS_MAP = {
3940
export default function DemoControls({
4041
controls,
4142
componentProps,
42-
onPropChange
43+
onPropChange,
44+
className
4345
}: PropControlsProps) {
4446
return (
45-
<div className={styles.form}>
47+
<div className={cx(styles.form, className)}>
4648
{Object.entries(controls).map(([prop, control]) => {
4749
const propLabel = camelCaseToWords(prop);
4850
const propValue = componentProps?.[prop] ?? '';

apps/www/src/components/demo/demo-playground.tsx

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
'use client';
22

3-
import { IconButton } from '@raystack/apsara';
4-
import { RefreshCw } from 'lucide-react';
3+
import { Cross2Icon } from '@radix-ui/react-icons';
4+
import { Dialog, Flex, IconButton } from '@raystack/apsara';
5+
import { ResetIcon } from '@raystack/apsara/icons';
6+
import { cx } from 'class-variance-authority';
57
import {
68
ReadonlyURLSearchParams,
79
useRouter,
810
useSearchParams
911
} from 'next/navigation';
10-
import { useState } from 'react';
12+
import { useMemo, useState } from 'react';
1113
import { LiveProvider } from 'react-live';
1214
import Editor from '../editor';
1315
import Preview from '../preview';
16+
import { useDemoContext } from './demo-context';
1417
import DemoControls from './demo-controls';
18+
import DemoPreview from './demo-preview';
19+
import DemoTitle from './demo-title';
1520
import styles from './styles.module.css';
1621
import {
1722
ComponentPropsType,
@@ -36,6 +41,16 @@ const getInitialProps = (
3641
return initialProps;
3742
};
3843

44+
const getUpdatedProps = (
45+
componentProps: ComponentPropsType,
46+
controls: ControlsType
47+
) => {
48+
return Object.fromEntries(
49+
Object.entries(componentProps).filter(
50+
([key, value]) => value !== controls[key]?.defaultValue
51+
)
52+
);
53+
};
3954
export default function DemoPlayground({
4055
scope,
4156
controls,
@@ -48,12 +63,16 @@ export default function DemoPlayground({
4863
getInitialProps(controls, searchParams)
4964
);
5065

51-
const updatedProps = Object.fromEntries(
52-
Object.entries(componentProps).filter(
53-
([key, value]) => value !== controls[key]?.defaultValue
54-
)
55-
);
56-
const code = getCode(updatedProps, componentProps).trim();
66+
const code = useMemo(() => {
67+
const updatedProps = getUpdatedProps(componentProps, controls);
68+
return getCode(updatedProps, componentProps).trim();
69+
}, [componentProps, controls, getCode]);
70+
71+
const previewCode = useMemo(() => {
72+
const props = getInitialProps(controls);
73+
const updatedProps = getUpdatedProps(props, controls);
74+
return getCode(updatedProps, props).trim();
75+
}, []);
5776

5877
const handlePropChange: PropChangeHandlerType = (prop, value) => {
5978
const updatedComponentProps = { ...componentProps, [prop]: value };
@@ -75,30 +94,62 @@ export default function DemoPlayground({
7594
router.push(`?`, { scroll: false });
7695
setComponentProps(getInitialProps(controls));
7796
};
97+
const { openPlayground, setOpenPlayground } = useDemoContext();
7898

7999
return (
80-
<LiveProvider code={code} scope={scope} disabled>
81-
<div className={styles.container} data-demo>
82-
<div className={styles.previewContainer}>
83-
<div className={styles.preview}>
84-
<Preview />
85-
<IconButton
86-
size={1}
87-
className={styles.previewReset}
88-
onClick={resetProps}
89-
aria-label='Reset to default props'
100+
<>
101+
<DemoPreview type='code' code={previewCode} scope={scope} />
102+
<Dialog open={openPlayground} onOpenChange={setOpenPlayground}>
103+
<Dialog.Content className={styles.playgroundDialog}>
104+
<Dialog.Header className={styles.playgroundHeader}>
105+
<DemoTitle className={styles.playgroundTitle} />
106+
<Flex gap={3} align='center'>
107+
<IconButton
108+
size={2}
109+
onClick={resetProps}
110+
aria-label='Reset to default props'
111+
>
112+
<ResetIcon />
113+
</IconButton>
114+
<IconButton
115+
size={2}
116+
onClick={() => setOpenPlayground(false)}
117+
aria-label='Close playground'
118+
>
119+
<Cross2Icon />
120+
</IconButton>
121+
</Flex>
122+
</Dialog.Header>
123+
<LiveProvider code={code} scope={scope} disabled>
124+
<div
125+
className={cx(styles.container, styles.playgroundContent)}
126+
data-demo
90127
>
91-
<RefreshCw size={12} />
92-
</IconButton>
93-
</div>
94-
<DemoControls
95-
controls={controls}
96-
componentProps={componentProps}
97-
onPropChange={handlePropChange}
98-
/>
99-
</div>
100-
<Editor code={code} />
101-
</div>
102-
</LiveProvider>
128+
<div
129+
className={cx(
130+
styles.previewContainer,
131+
styles.playgroundPreviewContainer
132+
)}
133+
>
134+
<div className={cx(styles.preview, styles.playgroundPreview)}>
135+
<Preview className={styles.playgroundPreviewContent} />
136+
</div>
137+
<DemoControls
138+
controls={controls}
139+
componentProps={componentProps}
140+
onPropChange={handlePropChange}
141+
className={styles.playgroundControls}
142+
/>
143+
</div>
144+
<Editor
145+
code={code}
146+
className={styles.playgroundEditor}
147+
maxLines={undefined}
148+
/>
149+
</div>
150+
</LiveProvider>
151+
</Dialog.Content>
152+
</Dialog>
153+
</>
103154
);
104155
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client';
2+
3+
import { Dialog } from '@raystack/apsara';
4+
import { useDemoContext } from './demo-context';
5+
6+
type Props = {
7+
className?: string;
8+
};
9+
10+
export default function DemoTitle({ className }: Props) {
11+
const { title } = useDemoContext();
12+
return <Dialog.Title className={className}>{title}</Dialog.Title>;
13+
}

0 commit comments

Comments
 (0)