diff --git a/docs/site/.eslintrc.json b/docs/site/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/docs/site/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/docs/site/.gitignore b/docs/site/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/docs/site/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/docs/site/README.md b/docs/site/README.md new file mode 100644 index 0000000..9767425 --- /dev/null +++ b/docs/site/README.md @@ -0,0 +1,16 @@ +# Devtools Docs Site +## Getting Started + +This is still a WIP, For now, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Then got to [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/docs/site/components/ColorSchemeSwitch.tsx b/docs/site/components/ColorSchemeSwitch.tsx new file mode 100644 index 0000000..7e1ad54 --- /dev/null +++ b/docs/site/components/ColorSchemeSwitch.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { IconButton, useColorMode, useColorModeValue } from '@chakra-ui/react'; +import { FaMoon, FaSun } from 'react-icons/fa'; + +export const ColorSchemeSwitch = () => { + const { toggleColorMode } = useColorMode(); + const SwitchIcon = useColorModeValue(FaMoon, FaSun); + const text = useColorModeValue('dark', 'light'); + + return ( + } + onClick={toggleColorMode} + /> + ); +}; diff --git a/docs/site/components/Context.tsx b/docs/site/components/Context.tsx new file mode 100644 index 0000000..296d9dd --- /dev/null +++ b/docs/site/components/Context.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +export const DEFAULT_CONTEXT = { + bannerExpanded: true, + setBannerExpanded: (expanded: boolean) => {}, +}; + +export const AppContext = React.createContext(DEFAULT_CONTEXT); + +export const Context = (props: React.PropsWithChildren) => { + const [state, setState] = React.useState(DEFAULT_CONTEXT); + + const withHandlers = React.useMemo(() => { + return { + ...state, + setBannerExpanded: (expanded: boolean) => { + setState({ ...state, bannerExpanded: expanded }); + }, + }; + }, [state]); + + return ( + + {props.children} + + ); +}; diff --git a/docs/site/components/DevtoolsTeam.tsx b/docs/site/components/DevtoolsTeam.tsx new file mode 100644 index 0000000..8dc2de6 --- /dev/null +++ b/docs/site/components/DevtoolsTeam.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Avatar, SimpleGrid, VStack, Text } from '@chakra-ui/react'; +import teamdata from '../config/team.json'; + +/** + * Individual Profile card with avatar, name and domain vertically stacked + */ +const ProfileCard = (props: any) => { + return ( + + + {props.profile.name} + {props.profile.domain.join(', ')} + + ); +}; + +/** + * Component to render Player Team cards + */ +export const DevtoolsTeam = () => { + return ( + + {teamdata + .sort(() => 0.5 - Math.random()) + .map((element) => { + return ; + })} + + ); +}; diff --git a/docs/site/components/Image.tsx b/docs/site/components/Image.tsx new file mode 100644 index 0000000..644cb2b --- /dev/null +++ b/docs/site/components/Image.tsx @@ -0,0 +1,13 @@ +import path from 'path'; + +export function withBasePrefix(location?: string): string | undefined { + if (!location) { + return location; + } + + if (process.env.NEXT_PUBLIC_BASE_PATH) { + return path.join(process.env.NEXT_PUBLIC_BASE_PATH, location); + } + + return location; +} diff --git a/docs/site/components/Navigation.tsx b/docs/site/components/Navigation.tsx new file mode 100644 index 0000000..68cebfa --- /dev/null +++ b/docs/site/components/Navigation.tsx @@ -0,0 +1,226 @@ +import React from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { + Flex, + Button, + Box, + chakra, + Drawer, + DrawerContent, + Heading, + HStack, + Image, + Icon, + IconButton, + UnorderedList, + useColorModeValue, + useDisclosure, + useBreakpointValue, + Text, +} from '@chakra-ui/react'; +import { HamburgerIcon } from '@chakra-ui/icons'; +import type { Route } from '../config/navigation'; +import NAV, { PATH_TO_NAV } from '../config/navigation'; +import { ColorSchemeSwitch } from './ColorSchemeSwitch'; +import { GITHUB_URL } from '../config/constants'; +import { withBasePrefix } from './Image'; +import { GithubIcon } from './gh-icon'; + +const getPathFromRoute = (route: Route): string => { + if (route.path) { + return route.path; + } + + if (route.routes) { + for (const r of route.routes) { + const nestedRoute = getPathFromRoute(r); + if (nestedRoute) { + return nestedRoute; + } + } + } + + return ''; +}; + + +const NavTitleOrLink = (props: { route: Route }) => { + const { route } = props; + + const { pathname } = useRouter(); + const router = useRouter(); + const langpref = router.query.lang; + const selectedButtonColor = useColorModeValue('blue.800', 'blue.600'); + + if (route.path) { + return ( + + + + + + ); + } + + return ( + + {route.title} + + ); +}; + +const SideNavigationList = (props: { route: Route }) => { + const { route } = props; + + return ( + + + + {route.routes && ( + + {route.routes.map((r) => ( + + ))} + + )} + + ); +}; + +export const SideNavigation = () => { + const { pathname } = useRouter(); + const subRoutes = PATH_TO_NAV.get(pathname); + + const route = NAV.routes.find((r) => r.title === subRoutes?.[0]); + + if (!route) { + return null; + } + + return ( + + + + ); +}; + +export const Footer = () => { + return null; +}; + +export const GitHubButton = () => { + return ( + + + } + /> + + ); +}; + +export const TopNavigation = () => { + const { pathname } = useRouter(); + const subRoutes = PATH_TO_NAV.get(pathname); + const mobileNavDisclosure = useDisclosure(); + + const currentTopLevelRoute = NAV.routes.find( + (r) => r.title === subRoutes?.[0] + ); + + const logoSrc = useBreakpointValue({ + base: useColorModeValue( + withBasePrefix('/logo/logo-light-small.png'), + withBasePrefix('/logo/logo-dark-small.png') + ), + lg: useColorModeValue( + withBasePrefix('/logo/logo-light-large.png'), + withBasePrefix('/logo/logo-dark-large.png') + ), + }); + + const selectedButtonColor = useColorModeValue('blue.800', 'blue.600'); + + return ( + + + + } + display={{ base: 'flex', md: 'none' }} + aria-label="Open Side Navigation Menu" + onClick={mobileNavDisclosure.onOpen} + /> + + Player Logo + + + + + + {NAV.routes.map((topRoute) => { + const navRoute = getPathFromRoute(topRoute); + const isSelected = currentTopLevelRoute?.title === topRoute.title; + + return ( + + + + ); + })} + + + + + + + + + + + + + + ); +}; diff --git a/docs/site/components/chakra-theme.ts b/docs/site/components/chakra-theme.ts new file mode 100644 index 0000000..553f077 --- /dev/null +++ b/docs/site/components/chakra-theme.ts @@ -0,0 +1,42 @@ +import { extendTheme } from '@chakra-ui/react'; + +export const colors = { + transparent: 'transparent', + black: '#000', + white: '#fff', + gray: { + 50: '#dee1e3', + 100: '#cfd3d7', + 200: '#bfc5c9', + 300: '#adb4b9', + 400: '#98a1a8', + 500: '#7f8b92', + 600: '#374147', + 700: '#374147', + 800: '#121212', + }, + red: {}, + orange: {}, + yellow: {}, + green: {}, + teal: {}, + blue: { + 50: '#f6fafd', + 100: '#e2eff9', + 200: '#cce4f5', + 300: '#b5d8f0', + 400: '#9bcaeb', + 500: '#7dbae5', + 600: '#5aa7de', + 700: '#2d8fd5', + 800: '#0070b6', + 900: '#055393', + }, + cyan: {}, + purple: {}, + pink: {}, +}; + +export const theme = extendTheme({ + colors, +}); diff --git a/docs/site/components/code-highlight/index.tsx b/docs/site/components/code-highlight/index.tsx new file mode 100644 index 0000000..1d43a13 --- /dev/null +++ b/docs/site/components/code-highlight/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Box, useColorModeValue } from '@chakra-ui/react'; +import { + Prism as SyntaxHighlighter, + SyntaxHighlighterProps, +} from 'react-syntax-highlighter'; + +import { light, dark } from './prism-colors'; + +export const useCodeStyle = () => { + const style = useColorModeValue(light, dark); + + return style; +}; + +export const CodeHighlight = (props: SyntaxHighlighterProps) => { + const borderColor = useColorModeValue('gray.100', 'gray.800'); + + return ( + + + + ); +}; diff --git a/docs/site/components/code-highlight/prism-colors.ts b/docs/site/components/code-highlight/prism-colors.ts new file mode 100644 index 0000000..893f00c --- /dev/null +++ b/docs/site/components/code-highlight/prism-colors.ts @@ -0,0 +1,56 @@ +import { + coy, + vscDarkPlus, +} from 'react-syntax-highlighter/dist/cjs/styles/prism'; + +export const light = { + ...coy, + pre: { + margin: '.5em 0', + }, + 'pre[class*="language-"]': { + fontSize: '13px', + textShadow: 'none', + fontFamily: + 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace', + direction: 'ltr', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + lineHeight: '1.5', + MozTabSize: '4', + OTabSize: '4', + tabSize: '4', + WebkitHyphens: 'none', + MozHyphens: 'none', + msHyphens: 'none', + hyphens: 'none', + padding: '1em', + margin: '.5em 0', + overflow: 'auto', + }, + 'code[class*="language-"]': { + fontSize: '13px', + textShadow: 'none', + fontFamily: + 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace', + direction: 'ltr', + textAlign: 'left', + whiteSpace: 'pre', + wordSpacing: 'normal', + wordBreak: 'normal', + lineHeight: '1.5', + MozTabSize: '4', + OTabSize: '4', + tabSize: '4', + WebkitHyphens: 'none', + MozHyphens: 'none', + msHyphens: 'none', + hyphens: 'none', + }, +}; + +export const dark = { + ...vscDarkPlus, +}; diff --git a/docs/site/components/gh-icon.tsx b/docs/site/components/gh-icon.tsx new file mode 100644 index 0000000..a399b62 --- /dev/null +++ b/docs/site/components/gh-icon.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +// https://github.com/chakra-ui/chakra-ui-docs/blob/main/src/components/header.tsx +export const GithubIcon = React.forwardRef( + (props: React.ComponentProps<'svg'>, ref) => ( + + + + ) +); diff --git a/docs/site/components/mdx-components.tsx b/docs/site/components/mdx-components.tsx new file mode 100644 index 0000000..f6d25a6 --- /dev/null +++ b/docs/site/components/mdx-components.tsx @@ -0,0 +1,257 @@ +import React from 'react'; +import path from 'path'; +import Link from 'next/link'; +import { + Heading, + Text, + UnorderedList, + OrderedList, + ListItem, + Code as ChakraCode, + useColorMode, + useColorModeValue, + Tabs as ChakraTabs, + TabList, + TabPanels, + Tab, + TabPanel, + Image, + HStack, + Table, + Th, + Tr, + Td, + Alert as ChakraAlert, + AlertStatus, + AlertTitle, + AlertDescription, + AlertIcon, + Box, +} from '@chakra-ui/react'; +import { MDXProviderComponents } from '@mdx-js/react'; +import { useRouter } from 'next/router'; +import { CodeHighlight } from './code-highlight'; +import { withBasePrefix } from './Image'; + +/** + * Generic Tab Component that extends Chakra's Tab + */ +const GenericTab = (props: any) => { + const name = props.nameMap?.get(props.mdxType.toLowerCase()) ?? props.mdxType; + return {name}; +}; + +/** + * Tab Component specifically for Gradle to handle its multiple languages + */ +const GradleTab = (props: any) => { + const scriptLang = props.language; + return {`Gradle ${scriptLang}`}; +}; + +const CodeTabsNameMap = new Map([ + ['core', 'Core'], + ['react', 'React'], + ['ios', 'iOS'], + ['android', 'Android'], +]); + +const ContentTabsNameMap = new Map([ + ['json', 'JSON'], + ['tsx', 'TSX'], +]); + +const CodeTabsMap = new Map([['gradle', GradleTab]]); + +/** + * Generic wrapper around Chakra's tab to make use in mdx easier. + */ +const Tabs = (props: any) => { + return ( + props.callback?.(index)} + > + + {React.Children.map(props.children, (child: any) => { + const TabComponent = + CodeTabsMap.get(child.props.mdxType.toLowerCase()) ?? GenericTab; + return ( + + ); + })} + + + {React.Children.map(props.children, (child: any) => { + return ( + + {child.props.children} + + ); + })} + + + ); +}; + + +/** + * Tab section for Content Authoring. This should include tsx and/or example JSON files. + */ +const ContentTabs = (props: React.PropsWithChildren) => { + const children = React.Children.toArray(props.children).filter((c: any) => { + return ContentTabsNameMap.has(c.props.mdxType.toLowerCase()); + }); + + return {children}; +}; + +const langMap: Record = { + js: 'javascript', + ts: 'typescript', +}; + +/** + * Code Block comopnent + */ +const CodeBlock = ({children: {props}}: any) => { + let lang = props.className?.split('-')[1]; + if (langMap[lang] !== undefined) { + lang = langMap[lang]; + } + + return ( + + {props.children.trim()} + + ); +}; + +/** + * Image Component + */ +export const Img = (props: JSX.IntrinsicElements['img']) => { + const darkModeInvert = props.src?.includes('darkModeInvert'); + const darkModeOnly = props.src?.includes('darkModeOnly'); + const lightModeOnly = props.src?.includes('lightModeOnly'); + + const { colorMode } = useColorMode(); + + const filterStyles = useColorModeValue( + undefined, + 'invert(80%) hue-rotate(180deg);' + ); + + if ( + (colorMode === 'light' && darkModeOnly) || + (colorMode === 'dark' && lightModeOnly) + ) { + return null; + } + + return ( + + + + ); +}; + +/** + * Normalize URL to conform to local path rules + */ +export const useNormalizedUrl = (url: string) => { + const router = useRouter(); + + if (!url.startsWith('.')) { + return url; + } + + const ext = path.extname(url); + let withoutExt = url; + if (ext) { + withoutExt = path.join(path.dirname(url), path.basename(url, ext)); + } + + return path.join(path.dirname(router.pathname), withoutExt); +}; + +export const InlineCode = (props: JSX.IntrinsicElements['code']) => { + return ( + + ); +}; + +type ChakraAlertProps = React.PropsWithChildren<{ + status?: AlertStatus; + title?: string; + description?: string; +}> + +export const Alert = (props: ChakraAlertProps) => { + return ( + + + + {props.title && {props.title}} + {props.description && {props.description}} + {props.children} + + + ); +}; + + +/** + * Anchor tab component wrapping Chakra's + */ +const A = (props: JSX.IntrinsicElements['a']) => { + const { href, ...other } = props; + return ( + + + + ); +}; + +export const MDXComponents: MDXProviderComponents = { + h1: (props: any) => , + h2: (props: any) => , + h3: (props: any) => , + h4: (props: any) => , + + p: (props: any) => , + ul: UnorderedList, + ol: OrderedList, + li: ListItem, + img: Img, + code: InlineCode, + pre: CodeBlock, + + a: A, + + Tabs, + + ContentTabs, + + table: Table, + th: Th, + tr: Tr, + td: Td, + + Alert, + AlertTitle, + AlertDescription, +}; diff --git a/docs/site/components/mdx-layout.tsx b/docs/site/components/mdx-layout.tsx new file mode 100644 index 0000000..f310188 --- /dev/null +++ b/docs/site/components/mdx-layout.tsx @@ -0,0 +1,47 @@ +import { Container, Flex, Divider, Box } from '@chakra-ui/react'; +import { MDXProvider } from '@mdx-js/react'; +import { MDXComponents } from './mdx-components'; +import { TopNavigation, SideNavigation } from './Navigation'; + + +export default function MdxLayout({ children }: { children: React.ReactNode }) { + // Create any shared layout or styles here + const maxH = `calc(100vh - 88px)`; + const minH = `calc(100vh - 88px - 105px)`; + + return ( + + + + + + + + + + + + + + + {children} + + + + + + + + + + ) +} \ No newline at end of file diff --git a/docs/site/config/constants.ts b/docs/site/config/constants.ts new file mode 100644 index 0000000..e89000e --- /dev/null +++ b/docs/site/config/constants.ts @@ -0,0 +1 @@ +export const GITHUB_URL = 'https://github.com/player-ui/devtools-assets'; diff --git a/docs/site/config/navigation.ts b/docs/site/config/navigation.ts new file mode 100644 index 0000000..3230926 --- /dev/null +++ b/docs/site/config/navigation.ts @@ -0,0 +1,101 @@ + + +export interface Route { + title: string; + path?: string; + routes?: Array; + metaData?: { + platform?: Array; + }; +} + +interface Navigation { + routes: Array; +} + +const navigation: Navigation = { + routes: [ + { + title: 'Devtools', + routes: [ + { + title: 'Overview', + path: '/overview', + }, + { + title: 'Team', + path: '/team', + }, + { + title: 'Plugin', + path: '/plugin', + }, + { + title: 'Assets', + routes: [ + { + title: 'Action', + path: '/assets/action' + }, + { + title: 'Collection', + path: '/assets/collection', + }, + { + title: 'Console', + path: '/assets/console', + }, + { + title: 'Input', + path: '/assets/input', + }, + { + title: 'List', + path: '/assets/list', + }, + { + title: 'Navigation', + path: '/assets/navigation', + }, + { + title: 'Object Inspector', + path: '/assets/object-inspector', + }, + { + title: 'Stacked View', + path: '/assets/stacked-view', + }, + { + title: 'Table', + path: '/assets/table', + }, + { + title: 'Text', + path: '/assets/text', + }, + ] + }, + ], + }, + ], +}; + +export const PATH_TO_NAV = (() => { + const pathMap = new Map(); + + const expandRoutes = (route: Route, context: string[] = []) => { + if (route.path) { + pathMap.set(route.path, context); + } + + route.routes?.forEach((nestedRoute) => { + expandRoutes(nestedRoute, [...context, route.title]); + }); + }; + + navigation.routes.forEach((r) => expandRoutes(r)); + + return pathMap; +})(); + +export default navigation; diff --git a/docs/site/config/team.json b/docs/site/config/team.json new file mode 100644 index 0000000..7a8cef5 --- /dev/null +++ b/docs/site/config/team.json @@ -0,0 +1,17 @@ +[ + { + "name": "Rafael Campos", + "domain": ["Tools"], + "avatar": "https://avatars.githubusercontent.com/u/26394217?v=4" + }, + { + "name": "Marlon Ercillo", + "domain": ["React"], + "avatar": "https://avatars.githubusercontent.com/u/3643493?v=4" + }, + { + "name": "Alejandro Fimbres", + "domain": ["Tools"], + "avatar": "https://avatars.githubusercontent.com/u/9345943?v=4" + } +] diff --git a/docs/site/next.config.mjs b/docs/site/next.config.mjs new file mode 100644 index 0000000..f5a520f --- /dev/null +++ b/docs/site/next.config.mjs @@ -0,0 +1,34 @@ +/** @type {import('next').NextConfig} */ +import MDX from '@next/mdx'; +import smartypants from 'remark-smartypants' +import remarkGFM from 'remark-gfm' + +export const BASE_PREFIX = + process.env.NODE_ENV === 'production' ? '/DOCS_BASE_PATH' : undefined; + +const withMDX = MDX({ + extension: /\.mdx?$/, + options: { + // If using remark-gfm: you'll need to use next.config.mjs as the package is ESM only + // https://github.com/remarkjs/remark-gfm#install + remarkPlugins: [remarkGFM], + rehypePlugins: [], + // If using `MDXProvider`, uncomment the following line. + providerImportSource: "@mdx-js/react", + }, +}) + + +/** @type {import('next').NextConfig} */ +const nextConfig = { + env: { + NEXT_PUBLIC_BASE_PATH: BASE_PREFIX, + }, + // pageExtensions including md and mdx + pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], + // Any other Next.js config goes below + reactStrictMode: true, +} + +// Combining MDX config with Next.js config +export default withMDX(nextConfig) diff --git a/docs/site/package.json b/docs/site/package.json new file mode 100644 index 0000000..5d382ed --- /dev/null +++ b/docs/site/package.json @@ -0,0 +1,33 @@ +{ + "name": "site", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/react": "^2.8.2", + "@mdx-js/loader": "^3.0.1", + "@mdx-js/react": "^3.0.1", + "@next/mdx": "^14.1.4", + "@types/mdx": "^2.0.12", + "next": "14.1.4", + "react": "^18", + "react-dom": "^18", + "react-icons": "^5.0.1", + "react-syntax-highlighter": "^15.5.0", + "remark-gfm": "^4.0.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.1.4", + "typescript": "^5" + } +} diff --git a/docs/site/pages/_app.tsx b/docs/site/pages/_app.tsx new file mode 100644 index 0000000..0b6eeeb --- /dev/null +++ b/docs/site/pages/_app.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { AppProps } from 'next/app'; +import Head from 'next/head'; +import { ChakraProvider, useColorMode } from '@chakra-ui/react'; +import { theme } from '../components/chakra-theme'; +import { Context } from '../components/Context'; +import MdxLayout from '../components/mdx-layout'; +import './globals.css'; + + +// Sync the chakra theme w/ the document +const HTMLThemeSetter = () => { + const { colorMode } = useColorMode(); + + React.useEffect(() => { + document.documentElement.setAttribute('data-theme', colorMode); + }, [colorMode]); + + return null; +}; + +const MyApp = ({ Component, pageProps, router }: AppProps) => { + return ( + <> + + + + + + Devtools + + + + + + + ); +}; + +export default MyApp; diff --git a/docs/site/pages/assets/action.mdx b/docs/site/pages/assets/action.mdx new file mode 100644 index 0000000..bc8323d --- /dev/null +++ b/docs/site/pages/assets/action.mdx @@ -0,0 +1,103 @@ +# @devtools-ui/action + +## Overview + +`@devtools-ui/action` is a component package designed to be leveraged by a [Player-UI assets plugin](https://player-ui.github.io/next/plugins). + +It provides a `Action` component that can be used to allow a user interaction (e.g., [transition](https://player-ui.github.io/next/content/navigation)), usually rendered as a `