diff --git a/apps/www/public/assets/logo.svg b/apps/www/public/assets/logo.svg index 4c3aafa6b..11aba3adf 100644 --- a/apps/www/public/assets/logo.svg +++ b/apps/www/public/assets/logo.svg @@ -1 +1 @@ - + diff --git a/apps/www/src/app/docs/[[...slug]]/page.tsx b/apps/www/src/app/docs/[[...slug]]/page.tsx index 93c7ad96f..5a43853e4 100644 --- a/apps/www/src/app/docs/[[...slug]]/page.tsx +++ b/apps/www/src/app/docs/[[...slug]]/page.tsx @@ -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'; @@ -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(' - - - - - - - {page.data.title} - - {page.data.description} - - - - + + + + + + + + + {page.data.title} + + {page.data.description} + + + + + + - - - +
+ +
+ +
-
+ ); } diff --git a/apps/www/src/components/demo/demo-context.tsx b/apps/www/src/components/demo/demo-context.tsx new file mode 100644 index 000000000..235d7fc3e --- /dev/null +++ b/apps/www/src/components/demo/demo-context.tsx @@ -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 ( + + {children} + + ); +}; + +export const useDemoContext = () => { + return useContext(DemoContext); +}; diff --git a/apps/www/src/components/demo/demo-controls.tsx b/apps/www/src/components/demo/demo-controls.tsx index 604888ebb..f7978db50 100644 --- a/apps/www/src/components/demo/demo-controls.tsx +++ b/apps/www/src/components/demo/demo-controls.tsx @@ -26,6 +26,7 @@ type PropControlsProps = { controls: ControlsType; componentProps: ComponentPropsType; onPropChange: PropChangeHandlerType; + className?: string; }; const ICONS_MAP = { @@ -39,10 +40,11 @@ const ICONS_MAP = { export default function DemoControls({ controls, componentProps, - onPropChange + onPropChange, + className }: PropControlsProps) { return ( -
+
{Object.entries(controls).map(([prop, control]) => { const propLabel = camelCaseToWords(prop); const propValue = componentProps?.[prop] ?? ''; diff --git a/apps/www/src/components/demo/demo-playground.tsx b/apps/www/src/components/demo/demo-playground.tsx index e0cffe294..62ea31e85 100644 --- a/apps/www/src/components/demo/demo-playground.tsx +++ b/apps/www/src/components/demo/demo-playground.tsx @@ -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, @@ -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, @@ -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 }; @@ -75,30 +94,62 @@ export default function DemoPlayground({ router.push(`?`, { scroll: false }); setComponentProps(getInitialProps(controls)); }; + const { openPlayground, setOpenPlayground } = useDemoContext(); return ( - -
-
-
- - + + + + + + + + + + setOpenPlayground(false)} + aria-label='Close playground' + > + + + + + +
- - -
- -
- -
- +
+
+ +
+ +
+ +
+
+ + + ); } diff --git a/apps/www/src/components/demo/demo-title.tsx b/apps/www/src/components/demo/demo-title.tsx new file mode 100644 index 000000000..810794b2c --- /dev/null +++ b/apps/www/src/components/demo/demo-title.tsx @@ -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 {title}; +} diff --git a/apps/www/src/components/demo/styles.module.css b/apps/www/src/components/demo/styles.module.css index 7e0948ade..9851210de 100644 --- a/apps/www/src/components/demo/styles.module.css +++ b/apps/www/src/components/demo/styles.module.css @@ -155,3 +155,50 @@ border-left: none; } } +.playgroundDialog { + height: 88vh; + width: 72%; + width: 1048px; + height: 880px; + border-radius: var(--rs-radius-2); + overflow: hidden; + display: flex; + flex-direction: column; +} +.playgroundHeader { + border: 0.5px solid var(--rs-color-border-base-primary); + background: var(--rs-color-background-base-secondary); + height: 32px; + padding: 0 var(--rs-space-5); +} +.playgroundTitle { + font-size: var(--rs-font-size-small); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); +} +.playgroundContent { + flex: 1; + border-radius: 0; + border: none; +} +.playgroundPreviewContainer { + flex: 2; +} +.playgroundPreview { + border-bottom: none; + padding-bottom: 1px; +} +.playgroundControls { + border-bottom: none; + max-height: none; +} +.playgroundEditor { + flex: 1; + border-top: 0.5px solid var(--rs-color-border-base-primary); + overflow: auto; +} +.playgroundPreviewContent { + padding: var(--rs-space-12); + align-items: safe center; +} diff --git a/apps/www/src/components/docs/navbar.module.css b/apps/www/src/components/docs/navbar.module.css index c762678dd..3cae32775 100644 --- a/apps/www/src/components/docs/navbar.module.css +++ b/apps/www/src/components/docs/navbar.module.css @@ -43,3 +43,6 @@ .breadcrumb-separator { height: var(--rs-space-4); } +.icon { + color: var(--rs-color-foreground-base-secondary); +} diff --git a/apps/www/src/components/docs/navbar.tsx b/apps/www/src/components/docs/navbar.tsx index 5856c5d2d..32d1487ef 100644 --- a/apps/www/src/components/docs/navbar.tsx +++ b/apps/www/src/components/docs/navbar.tsx @@ -1,5 +1,9 @@ 'use client'; -import { CopyIcon, GitHubLogoIcon } from '@radix-ui/react-icons'; +import { + ComponentBooleanIcon, + CopyIcon, + GitHubLogoIcon +} from '@radix-ui/react-icons'; import { Breadcrumb, Button } from '@raystack/apsara'; import { useBreadcrumb } from 'fumadocs-core/breadcrumb'; import { Root } from 'fumadocs-core/page-tree'; @@ -7,6 +11,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Fragment, useState } from 'react'; import { SourceType } from '@/lib/types'; +import { useDemoContext } from '../demo/demo-context'; import styles from './navbar.module.css'; const cache = new Map(); @@ -32,6 +37,7 @@ export default function DocsNavbar({ const [isLoading, setLoading] = useState(false); const [checked, setChecked] = useState(false); const markdownUrl = `${url}.mdx`; + const { setOpenPlayground, hasPlayground } = useDemoContext(); const handleCopyMarkdown = async () => { const cached = cache.get(markdownUrl); @@ -80,13 +86,33 @@ export default function DocsNavbar({ )}
+ {hasPlayground && ( + + )} @@ -96,7 +122,13 @@ export default function DocsNavbar({ variant='outline' color='neutral' size='small' - leadingIcon={} + leadingIcon={ + + } > View source diff --git a/apps/www/src/components/editor/editor.tsx b/apps/www/src/components/editor/editor.tsx index f4107b34d..74f9be929 100644 --- a/apps/www/src/components/editor/editor.tsx +++ b/apps/www/src/components/editor/editor.tsx @@ -1,18 +1,24 @@ import { CodeBlock } from '@raystack/apsara'; -import { useMemo } from 'react'; +import { cx } from 'class-variance-authority'; +import { ComponentPropsWithoutRef, useMemo } from 'react'; import { getFormattedCode } from '@/lib/prettier'; import styles from './editor.module.css'; -type props = { +interface EditorProps extends ComponentPropsWithoutRef { code?: string; -}; + className?: string; +} -export default function Editor({ code = '' }: props) { +export default function Editor({ + code = '', + className, + ...props +}: EditorProps) { const formattedCode = useMemo(() => getFormattedCode(code), [code]); return ( -
- +
+ {formattedCode} diff --git a/apps/www/src/components/preview/preview.tsx b/apps/www/src/components/preview/preview.tsx index 78d4926d2..eddce393b 100644 --- a/apps/www/src/components/preview/preview.tsx +++ b/apps/www/src/components/preview/preview.tsx @@ -2,6 +2,6 @@ import { cx } from 'class-variance-authority'; import { LivePreview } from 'react-live'; import styles from './preview.module.css'; -export default function Preview() { - return ; +export default function Preview({ className }: { className?: string }) { + return ; } diff --git a/packages/raystack/icons/assets/reset.svg b/packages/raystack/icons/assets/reset.svg new file mode 100644 index 000000000..8894e61dc --- /dev/null +++ b/packages/raystack/icons/assets/reset.svg @@ -0,0 +1 @@ + diff --git a/packages/raystack/icons/index.tsx b/packages/raystack/icons/index.tsx index 60719309a..f37cd74e7 100644 --- a/packages/raystack/icons/index.tsx +++ b/packages/raystack/icons/index.tsx @@ -1,15 +1,16 @@ // This file is automatically generated by "npm run build:icons" // Do not edit this file manually -export { ReactComponent as BellSlashIcon } from "./assets/bell-slash.svg"; -export { ReactComponent as BellIcon } from "./assets/bell.svg"; -export { ReactComponent as BuildingsFilledIcon } from "./assets/buildings-filled.svg"; -export { ReactComponent as CheckCircleFilledIcon } from "./assets/check-circle-filled.svg"; -export { ReactComponent as CoinColoredIcon } from "./assets/coin-colored.svg"; -export { ReactComponent as CoinIcon } from "./assets/coin.svg"; -export { ReactComponent as CrossCircleFilledIcon } from "./assets/cross-circle-filled.svg"; -export { ReactComponent as FilterIcon } from "./assets/filter.svg"; -export { ReactComponent as OrganizationIcon } from "./assets/organization.svg"; -export { ReactComponent as ShoppingBagFilledIcon } from "./assets/shopping-bag-filled.svg"; -export { ReactComponent as SidebarIcon } from "./assets/sidebar.svg"; -export { ReactComponent as TriangleRightIcon } from "./assets/triangle-right.svg"; \ No newline at end of file +export { ReactComponent as BellIcon } from './assets/bell.svg'; +export { ReactComponent as BellSlashIcon } from './assets/bell-slash.svg'; +export { ReactComponent as BuildingsFilledIcon } from './assets/buildings-filled.svg'; +export { ReactComponent as CheckCircleFilledIcon } from './assets/check-circle-filled.svg'; +export { ReactComponent as CoinIcon } from './assets/coin.svg'; +export { ReactComponent as CoinColoredIcon } from './assets/coin-colored.svg'; +export { ReactComponent as CrossCircleFilledIcon } from './assets/cross-circle-filled.svg'; +export { ReactComponent as FilterIcon } from './assets/filter.svg'; +export { ReactComponent as OrganizationIcon } from './assets/organization.svg'; +export { ReactComponent as ResetIcon } from './assets/reset.svg'; +export { ReactComponent as ShoppingBagFilledIcon } from './assets/shopping-bag-filled.svg'; +export { ReactComponent as SidebarIcon } from './assets/sidebar.svg'; +export { ReactComponent as TriangleRightIcon } from './assets/triangle-right.svg';