Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/www/public/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 63 additions & 54 deletions apps/www/src/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Flex, Headline, Text } from '@raystack/apsara';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { DemoContextProvider } from '@/components/demo/demo-context';
import DocsFooter from '@/components/docs/footer';
import DocsNavbar from '@/components/docs/navbar';
import { mdxComponents } from '@/components/mdx';
Expand All @@ -15,69 +16,77 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
if (!page) notFound();

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

return (
<Flex
direction='column'
justify='center'
align='center'
className={styles.container}
data-article-content
>
<DocsNavbar
url={page.url}
title={page.data.title}
pageTree={docs.pageTree}
source={page.data.source}
/>
<Flex width='full' align='start'>
<Flex direction='column' align='center' justify='center' width='full'>
<Flex direction='column' className={styles.content} justify='between'>
<Flex direction='column' gap={6}>
<Flex direction='column' gap={3}>
<Headline size='t4'>{page.data.title}</Headline>
<Text size='regular' variant='secondary'>
{page.data.description}
</Text>
</Flex>
<Flex direction='column' className='prose'>
<MDX
components={{
...mdxComponents,
// this allows you to link to other pages with relative file paths
a: createRelativeLink(docs, page)
}}
/>
<DemoContextProvider hasPlayground={hasPlayground} title={page.data.title}>
<Flex
direction='column'
justify='center'
align='center'
className={styles.container}
data-article-content
>
<DocsNavbar
url={page.url}
title={page.data.title}
pageTree={docs.pageTree}
source={page.data.source}
/>
<Flex width='full' align='start'>
<Flex direction='column' align='center' justify='center' width='full'>
<Flex
direction='column'
className={styles.content}
justify='between'
>
<Flex direction='column' gap={6}>
<Flex direction='column' gap={3}>
<Headline size='t4'>{page.data.title}</Headline>
<Text size='regular' variant='secondary'>
{page.data.description}
</Text>
</Flex>
<Flex direction='column' className='prose'>
<MDX
components={{
...mdxComponents,
// this allows you to link to other pages with relative file paths
a: createRelativeLink(docs, page)
}}
/>
</Flex>
</Flex>
<DocsFooter url={page.url} />
</Flex>
<DocsFooter url={page.url} />
</Flex>
</Flex>
<aside
style={{
width: '300px',
height: 'calc(100vh - 50px)',
position: 'sticky',
top: '50px',
padding: '40px 0',
paddingRight: 'var(--rs-space-7)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}}
>
<div
<aside
style={{
width: '100%',
height: '70vh'
width: '300px',
height: 'calc(100vh - 50px)',
position: 'sticky',
top: '50px',
padding: '40px 0',
paddingRight: 'var(--rs-space-7)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}}
>
<TableOfContents headings={page.data.toc} />
</div>
</aside>
<div
style={{
width: '100%',
height: '70vh'
}}
>
<TableOfContents headings={page.data.toc} />
</div>
</aside>
</Flex>
</Flex>
</Flex>
</DemoContextProvider>
);
}

Expand Down
37 changes: 37 additions & 0 deletions apps/www/src/components/demo/demo-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';
import { createContext, ReactNode, useContext, useState } from 'react';

export const DemoContext = createContext<{
openPlayground: boolean;
setOpenPlayground: (open: boolean) => void;
hasPlayground: boolean;
title: string;
}>({
openPlayground: false,
setOpenPlayground: () => null,
hasPlayground: false,
title: ''
});

export const DemoContextProvider = ({
children,
hasPlayground,
title
}: {
children: ReactNode;
hasPlayground: boolean;
title: string;
}) => {
const [openPlayground, setOpenPlayground] = useState(false);
return (
<DemoContext.Provider
value={{ openPlayground, setOpenPlayground, hasPlayground, title }}
>
{children}
</DemoContext.Provider>
);
};

export const useDemoContext = () => {
return useContext(DemoContext);
};
6 changes: 4 additions & 2 deletions apps/www/src/components/demo/demo-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type PropControlsProps = {
controls: ControlsType;
componentProps: ComponentPropsType;
onPropChange: PropChangeHandlerType;
className?: string;
};

const ICONS_MAP = {
Expand All @@ -39,10 +40,11 @@ const ICONS_MAP = {
export default function DemoControls({
controls,
componentProps,
onPropChange
onPropChange,
className
}: PropControlsProps) {
return (
<div className={styles.form}>
<div className={cx(styles.form, className)}>
{Object.entries(controls).map(([prop, control]) => {
const propLabel = camelCaseToWords(prop);
const propValue = componentProps?.[prop] ?? '';
Expand Down
113 changes: 82 additions & 31 deletions apps/www/src/components/demo/demo-playground.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
'use client';

import { IconButton } from '@raystack/apsara';
import { RefreshCw } from 'lucide-react';
import { Cross2Icon } from '@radix-ui/react-icons';
import { Dialog, Flex, IconButton } from '@raystack/apsara';
import { ResetIcon } from '@raystack/apsara/icons';
import { cx } from 'class-variance-authority';
import {
ReadonlyURLSearchParams,
useRouter,
useSearchParams
} from 'next/navigation';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { LiveProvider } from 'react-live';
import Editor from '../editor';
import Preview from '../preview';
import { useDemoContext } from './demo-context';
import DemoControls from './demo-controls';
import DemoPreview from './demo-preview';
import DemoTitle from './demo-title';
import styles from './styles.module.css';
import {
ComponentPropsType,
Expand All @@ -36,6 +41,16 @@ const getInitialProps = (
return initialProps;
};

const getUpdatedProps = (
componentProps: ComponentPropsType,
controls: ControlsType
) => {
return Object.fromEntries(
Object.entries(componentProps).filter(
([key, value]) => value !== controls[key]?.defaultValue
)
);
};
export default function DemoPlayground({
scope,
controls,
Expand All @@ -48,12 +63,16 @@ export default function DemoPlayground({
getInitialProps(controls, searchParams)
);

const updatedProps = Object.fromEntries(
Object.entries(componentProps).filter(
([key, value]) => value !== controls[key]?.defaultValue
)
);
const code = getCode(updatedProps, componentProps).trim();
const code = useMemo(() => {
const updatedProps = getUpdatedProps(componentProps, controls);
return getCode(updatedProps, componentProps).trim();
}, [componentProps, controls, getCode]);

const previewCode = useMemo(() => {
const props = getInitialProps(controls);
const updatedProps = getUpdatedProps(props, controls);
return getCode(updatedProps, props).trim();
}, []);

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

return (
<LiveProvider code={code} scope={scope} disabled>
<div className={styles.container} data-demo>
<div className={styles.previewContainer}>
<div className={styles.preview}>
<Preview />
<IconButton
size={1}
className={styles.previewReset}
onClick={resetProps}
aria-label='Reset to default props'
<>
<DemoPreview type='code' code={previewCode} scope={scope} />
<Dialog open={openPlayground} onOpenChange={setOpenPlayground}>
<Dialog.Content className={styles.playgroundDialog}>
<Dialog.Header className={styles.playgroundHeader}>
<DemoTitle className={styles.playgroundTitle} />
<Flex gap={3} align='center'>
<IconButton
size={2}
onClick={resetProps}
aria-label='Reset to default props'
>
<ResetIcon />
</IconButton>
<IconButton
size={2}
onClick={() => setOpenPlayground(false)}
aria-label='Close playground'
>
<Cross2Icon />
</IconButton>
</Flex>
</Dialog.Header>
<LiveProvider code={code} scope={scope} disabled>
<div
className={cx(styles.container, styles.playgroundContent)}
data-demo
>
<RefreshCw size={12} />
</IconButton>
</div>
<DemoControls
controls={controls}
componentProps={componentProps}
onPropChange={handlePropChange}
/>
</div>
<Editor code={code} />
</div>
</LiveProvider>
<div
className={cx(
styles.previewContainer,
styles.playgroundPreviewContainer
)}
>
<div className={cx(styles.preview, styles.playgroundPreview)}>
<Preview className={styles.playgroundPreviewContent} />
</div>
<DemoControls
controls={controls}
componentProps={componentProps}
onPropChange={handlePropChange}
className={styles.playgroundControls}
/>
</div>
<Editor
code={code}
className={styles.playgroundEditor}
maxLines={undefined}
/>
</div>
</LiveProvider>
</Dialog.Content>
</Dialog>
</>
);
}
13 changes: 13 additions & 0 deletions apps/www/src/components/demo/demo-title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';

import { Dialog } from '@raystack/apsara';
import { useDemoContext } from './demo-context';

type Props = {
className?: string;
};

export default function DemoTitle({ className }: Props) {
const { title } = useDemoContext();
return <Dialog.Title className={className}>{title}</Dialog.Title>;
}
Loading