From 65d4b3334e9b28ffc11e573025ee6e8ff833c4a1 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sun, 27 Dec 2020 16:43:44 +0800 Subject: [PATCH] Improve look and feel Add syntax highlighting + improved props table. --- package.json | 1 + packages/react-common-docs/package.json | 2 + packages/react-common-docs/public/global.css | 1 + .../src/components/CodeBlock/CodeBlock.tsx | 71 ++++++ .../src/components/Header/Header.tsx | 4 +- .../src/components/Nav/Nav.tsx | 3 + .../src/components/NavLink/NavLink.tsx | 105 +++++---- .../src/components/Playground/Playground.tsx | 50 +++- .../src/components/Props/Props.tsx | 220 ++++++++++++++---- .../src/components/Sidebar/Sidebar.tsx | 121 ++-------- .../components/ThemeToggle/ThemeToggle.tsx | 96 ++++++++ packages/react-common-docs/src/docgen.json | 2 +- packages/react-common-docs/src/pages/_app.tsx | 23 +- .../react-common-docs/src/pages/_document.tsx | 6 +- .../src/pages/components/TextInput.mdx | 3 +- .../react-common-docs/src/pages/theming.md | 3 +- .../src/utilities/prism-themes/dark.ts | 141 +++++++++++ packages/react-common-docs/yarn.lock | 40 +++- .../react-common/src/components/Icon/Icon.tsx | 2 +- .../src/components/Select/Select.tsx | 2 +- .../src/components/TextInput/TextInput.tsx | 2 +- 21 files changed, 687 insertions(+), 211 deletions(-) create mode 100644 packages/react-common-docs/src/components/CodeBlock/CodeBlock.tsx create mode 100644 packages/react-common-docs/src/components/ThemeToggle/ThemeToggle.tsx create mode 100644 packages/react-common-docs/src/utilities/prism-themes/dark.ts diff --git a/package.json b/package.json index f081b37..dd247b4 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "@tesseract-design/react-common", + "title": "React Common", "version": "0.2.3", "description": "Common front-end components for Web using the Tesseract design system, written in React.", "directories": { diff --git a/packages/react-common-docs/package.json b/packages/react-common-docs/package.json index e6e2c4b..dda5c36 100644 --- a/packages/react-common-docs/package.json +++ b/packages/react-common-docs/package.json @@ -16,6 +16,8 @@ "next": "10.0.1", "next-mdx-frontmatter": "^0.0.3", "pascal-case": "^3.1.1", + "prism-react-renderer": "^1.1.1", + "prismjs": "^1.22.0", "react-docgen-typescript": "^1.20.5", "react-live": "^2.2.3", "remark-parse": "^9.0.0", diff --git a/packages/react-common-docs/public/global.css b/packages/react-common-docs/public/global.css index 4ef079a..7dbe935 100644 --- a/packages/react-common-docs/public/global.css +++ b/packages/react-common-docs/public/global.css @@ -55,4 +55,5 @@ pre { overflow: auto; margin: 0 -1rem; padding: 0 1rem; + line-height: 1.2; } diff --git a/packages/react-common-docs/src/components/CodeBlock/CodeBlock.tsx b/packages/react-common-docs/src/components/CodeBlock/CodeBlock.tsx new file mode 100644 index 0000000..745849f --- /dev/null +++ b/packages/react-common-docs/src/components/CodeBlock/CodeBlock.tsx @@ -0,0 +1,71 @@ +import * as React from 'react' +import styled from 'styled-components' +import Highlight, { defaultProps } from 'prism-react-renderer' +import PrismTheme from '../../utilities/prism-themes/dark' + +const Base = styled('figure')({ + margin: 0, +}) + +const Title = styled('figcaption')({ + textTransform: 'uppercase', + fontWeight: 'bolder', + fontSize: '0.75rem', +}) + +const CodeBlock = ({ children }) => { + const { props, } = children + const { children: code, className, metastring } = props + const language = typeof className as string === 'string' ? className.split('-')[1] : 'plain' + const meta = (typeof metastring as string === 'string' && metastring.length > 0) + ? (metastring as string) + .split(' ') + .reduce( + (m, s) => { + const [key, value = ''] = s.split('=') + if (value.trim().length > 0) { + return { + ...m, + [key.trim()]: value.trim(), + } + } + return { + ...m, + [key.trim()]: true, + } + }, + {} + ) + : {} + return ( + + { + meta['title'] && ( + + {meta['title']} + + ) + } + + {({ style, tokens, getLineProps, getTokenProps }) => ( +
+            {tokens.map((line, i) => (
+              
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+ )} +
+ + ) +} + +export default CodeBlock diff --git a/packages/react-common-docs/src/components/Header/Header.tsx b/packages/react-common-docs/src/components/Header/Header.tsx index a690f89..674c32b 100644 --- a/packages/react-common-docs/src/components/Header/Header.tsx +++ b/packages/react-common-docs/src/components/Header/Header.tsx @@ -5,6 +5,7 @@ import unified from 'unified' import parse from 'remark-parse' import remark2react from 'remark-react' import docgen from '../../docgen.json' +import pkg from '../../../../../package.json' const propTypes = { of: PropTypes.string, @@ -23,8 +24,9 @@ const Header: React.FC = ({ of: ofAttr }) => { - {docs.displayName} | React Common + {docs.displayName} | {pkg['title'] || pkg.name} +

{docs.displayName}

diff --git a/packages/react-common-docs/src/components/Nav/Nav.tsx b/packages/react-common-docs/src/components/Nav/Nav.tsx index 8fd4ffe..65e31ad 100644 --- a/packages/react-common-docs/src/components/Nav/Nav.tsx +++ b/packages/react-common-docs/src/components/Nav/Nav.tsx @@ -18,6 +18,9 @@ const Container = styled('span')({ const HeaderContainer = styled(Container)({ marginTop: '2rem', + fontSize: '0.75rem', + fontWeight: 'bolder', + textTransform: 'uppercase', }) const propTypes = { diff --git a/packages/react-common-docs/src/components/NavLink/NavLink.tsx b/packages/react-common-docs/src/components/NavLink/NavLink.tsx index e70bbd5..92af171 100644 --- a/packages/react-common-docs/src/components/NavLink/NavLink.tsx +++ b/packages/react-common-docs/src/components/NavLink/NavLink.tsx @@ -3,6 +3,7 @@ import * as PropTypes from 'prop-types' import styled from 'styled-components' import { Icon } from '../../../../react-common/src' import MenuGraphics, { propTypes as menuGraphicsPropTypes } from '../MenuGraphics/MenuGraphics' +import { useRouter } from 'next/router' const Link = styled('a')({ display: 'block', @@ -41,6 +42,15 @@ const Link = styled('a')({ }, }) +const ActiveLink = styled(Link)({ + '::before': { + opacity: 0.25, + }, + '::after': { + opacity: 0.5, + }, +}) + const LinkText = styled('span')({ alignSelf: 'center', display: 'block', @@ -102,55 +112,60 @@ const NavLink = React.forwardRef(( onClick, }, ref -) => ( - any} - > - { - // @ts-ignore - - { - graphics as object - && ( - - { - // @ts-ignore - - } - - ) - } - - - {title} - +) => { + const router = useRouter() + const active = router.basePath + router.route === href + const Component = active ? ActiveLink : Link + return ( + any} + > + { + // @ts-ignore + { - subtitle as string + graphics as object && ( - - {subtitle} - + + { + // @ts-ignore + + } + ) } - - { - indicator as string - && ( - - - - ) - } - - } - -)) + + + {title} + + { + subtitle as string + && ( + + {subtitle} + + ) + } + + { + indicator as string + && ( + + + + ) + } + + } + + ) +}) NavLink.propTypes = propTypes diff --git a/packages/react-common-docs/src/components/Playground/Playground.tsx b/packages/react-common-docs/src/components/Playground/Playground.tsx index 68ca3a9..11d4d79 100644 --- a/packages/react-common-docs/src/components/Playground/Playground.tsx +++ b/packages/react-common-docs/src/components/Playground/Playground.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import * as PropTypes from 'prop-types' +import PrismTheme from '../../utilities/prism-themes/dark' import { LiveProvider, LiveEditor, @@ -8,13 +9,42 @@ import { import styled from 'styled-components' const Figure = styled('figure')({ - margin: 0, + margin: '0 -1rem', + padding: '1rem', + boxSizing: 'border-box', + position: 'relative', + '::before': { + position: 'absolute', + content: "''", + zIndex: -1, + backgroundColor: 'black', + opacity: 0.125, + top: 0, + left: 0, + width: '100%', + height: '100%', + }, +}) + +const Caption = styled('figcaption')({ + fontSize: '0.75rem', + fontWeight: 'bolder', + textTransform: 'uppercase', + marginBottom: '1rem', + marginTop: '-0.5rem', }) const StyledLiveEditor = styled(LiveEditor)({ lineHeight: 1.125, fontFamily: 'var(--font-family-monospace), monospace !important', marginTop: '1rem', + 'textarea': { + padding: '0 !important', + outline: 0, + }, + 'pre': { + padding: '0 !important', + }, }) const propTypes = { @@ -42,9 +72,17 @@ const Playground: React.FC = ({ { label as string && ( -

- {label} -
+ + Playground - {label} + + ) + } + { + !label + && ( + + Playground + ) }
@@ -52,7 +90,9 @@ const Playground: React.FC = ({ ...components, }}> - +
diff --git a/packages/react-common-docs/src/components/Props/Props.tsx b/packages/react-common-docs/src/components/Props/Props.tsx index 784d711..c5c2b4f 100644 --- a/packages/react-common-docs/src/components/Props/Props.tsx +++ b/packages/react-common-docs/src/components/Props/Props.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import * as PropTypes from 'prop-types' import styled from 'styled-components' +import PrismTheme from '../../utilities/prism-themes/dark' import docgen from '../../docgen.json' const Base = styled('table')({ @@ -114,6 +115,150 @@ type Docs = { } } +const getPropName = (name, def) => def.required ? name : name + '?' + +const getPropType = def => { + switch (def.type.name) { + case '(...args: any[]) => any': + return 'Function' + case 'enum': + return def.type.value.map(v => v.value).join(' | ') + default: + break + } + return def.type.name +} + +const getPropDefaultValue = def => { + return def.defaultValue + ? JSON.stringify(def.defaultValue.value) + : undefined +} + +const getPrismStyle = style => PrismTheme.styles.find(s => s.types.includes(style)) || PrismTheme.plain + +const getPropNameHtml = (propName, propType) => { + const variable = getPrismStyle('variable') + const operator = getPrismStyle('operator') + const fn = getPrismStyle('function') + const isFunction = propType === 'Function' + const trueStyle = isFunction ? fn : variable + return ( + propName.endsWith('?') + ? ( + + + {propName.slice(0, -1)} + + + {propName.slice(-1)} + + + ) + : ( + + {propName} + + ) + ) +} + +const getPropTypeHtml = propType => { + if (!propType) { + return undefined + } + const punctuation = getPrismStyle('punctuation') + const string = getPrismStyle('string') + const operator = getPrismStyle('operator') + const keyword = getPrismStyle('keyword') + const boolean = getPrismStyle('boolean') + const number = getPrismStyle('number') + const type = getPrismStyle('type') + const tokens = propType.split('|') + return tokens.reduce( + (tt, t, i) => { + let currentToken + if (t.trim().startsWith('"') && t.trim().endsWith('"')) { + currentToken = ( + + + {t.slice(0, t.indexOf('"') + 1)} + + + {t.slice(t.indexOf('"') + 1, t.lastIndexOf('"'))} + + + {t.slice(t.lastIndexOf('"'))} + + + ) + } else if ('any string boolean number symbol unknown object bigint'.split(' ').includes(t)) { + currentToken = ( + + {t} + + ) + } else if ('null'.split(' ').includes(t)) { + currentToken = ( + + {t} + + ) + } else if ('true false'.split(' ').includes(t)) { + currentToken = ( + + {t} + + ) + } else if (!isNaN(Number(t))) { + currentToken = ( + + {t} + + ) + } else { + currentToken = ( + + {t} + + ) + } + return [ + ...tt, + i > 0 && + | + , + currentToken, + ] + }, + [] + ) +} + const Props: React.FC = ({ of: ofAttr }) => { const docs = (docgen as unknown as Docs[]).find(d => d.displayName === ofAttr) @@ -147,50 +292,37 @@ const Props: React.FC = ({ of: ofAttr }) => { { - Object.entries((docs as Docs).props).map(([name, def]) => ( - - - - {def.required ? name : name + '?'} - - - - { - def.type.name === 'enum' - && ( - - {def.type.value.map(v => v.value).join(' | ')} - - ) - } - { - def.type.name !== 'enum' - && ( - - {def.type.name} - - ) - } - - - { - def.defaultValue - && ( - - {JSON.stringify(def.defaultValue.value)} - - ) - } - - - {def.description} - - - )) + Object.entries((docs as Docs).props).map(([name, prop]) => { + const propName = getPropName(name, prop) + const propDefaultValue = getPropDefaultValue(prop) + const propType = getPropType(prop) + return ( + + + + {getPropNameHtml(propName, propType)} + + + + + {getPropTypeHtml(propType)} + + + + + {getPropTypeHtml(propDefaultValue)} + + + + {prop.description} + + + ) + }) } diff --git a/packages/react-common-docs/src/components/Sidebar/Sidebar.tsx b/packages/react-common-docs/src/components/Sidebar/Sidebar.tsx index daf9e6c..7786482 100644 --- a/packages/react-common-docs/src/components/Sidebar/Sidebar.tsx +++ b/packages/react-common-docs/src/components/Sidebar/Sidebar.tsx @@ -6,6 +6,7 @@ import styled from 'styled-components' import Link from 'next/link' import Nav from '../Nav/Nav' import { MouseEventHandler } from 'react' +import ThemeToggle from '../ThemeToggle/ThemeToggle' const StyledLink = styled('a')({ display: 'block', @@ -82,21 +83,6 @@ const Actions = styled('div')({ alignItems: 'center', }) -const ToggleWrapper = styled('label')({ - cursor: 'pointer', - color: 'var(--color-accent)', - display: 'inline-block', -}) - -const ToggleIcon = styled('span')({ - -}) - -const ToggleInput = styled('input')({ - position: 'absolute', - left: -999999, -}) - const NavWrapper = styled('nav')({ '--size-link': '3rem', marginBottom: '4rem', @@ -122,82 +108,39 @@ type Props = PropTypes.InferProps const Sidebar: React.FC = ({ data, brand: BrandRaw, - initialTheme = 'Dark', + initialTheme, }) => { - const [theme, setTheme] = React.useState(initialTheme) const [sidebar, setSidebar] = React.useState(false) const navRef = React.useRef(null) const Brand = BrandRaw! - const toggleDarkMode = (b: string) => () => { - setTheme(b) - } - const toggleSidebar = (b: boolean): MouseEventHandler => (e) => { e.preventDefault() setSidebar(b) } - React.useEffect(() => { - let storageTheme = window.localStorage.getItem('tesseract-theme') - || ( - window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'Dark' - : 'Light' - ) - window.localStorage.setItem('tesseract-theme', storageTheme) - setTimeout(() => { - setTheme(storageTheme) - }) - }, []) - - React.useEffect(() => { - const eventListener = (e: MouseEvent) => { - let currentElement = e.target as HTMLElement - while (currentElement !== e.currentTarget) { - if (currentElement.tagName === 'A') { - setSidebar(false) - break - } - const { parentElement } = currentElement - if (parentElement as HTMLElement === null) { - break - } - currentElement = parentElement as HTMLElement + const handleSidebarClose = (e: MouseEvent) => { + let currentElement = e.target as HTMLElement + while (currentElement !== e.currentTarget) { + if (currentElement.tagName === 'A') { + setSidebar(false) + break } + const { parentElement } = currentElement + if (parentElement as HTMLElement === null) { + break + } + currentElement = parentElement as HTMLElement } + } - if (navRef.current === null) { - return - } - - navRef.current.addEventListener('click', eventListener, { capture: true }) + React.useEffect(() => { + navRef.current.addEventListener('click', handleSidebarClose, { capture: true }) return () => { - if (navRef.current === null) { - return - } - - navRef.current.removeEventListener('click', eventListener, { capture: true }) + navRef.current.removeEventListener('click', handleSidebarClose, { capture: true }) } }, []) - React.useEffect(() => { - window.localStorage.setItem('tesseract-theme', theme as string) - }, [theme]) - - React.useEffect(() => { - const stylesheets = Array.from(window.document.querySelectorAll('link[title]')) as HTMLLinkElement[] - stylesheets.forEach(s => { - const enabled = s.title === theme - s.setAttribute('rel', enabled ? 'stylesheet' : 'alternate stylesheet') - if (enabled) { - s.removeAttribute('disabled') - } else { - s.setAttribute('disabled', 'disabled') - } - }) - }, [theme]) - return ( = ({ - - - - { - theme === 'Dark' - && ( - - ) - } - { - theme === 'Light' - && ( - - ) - } - - + ( + window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'Dark' + : 'Light' +) + +const getTheme = () => window.localStorage.getItem('tesseract-theme') || getAutoDetectTheme() + +const applyStyles = (theme) => { + const stylesheets = Array.from(window.document.querySelectorAll('link[title]')) as HTMLLinkElement[] + stylesheets.forEach(s => { + const enabled = s.title === theme + if (enabled) { + s.removeAttribute('disabled') + } else { + s.setAttribute('disabled', 'disabled') + } + }) +} + +const handleInitializeTheme = (initialTheme: string) => () => { + applyStyles(getTheme() || initialTheme) +} + +const ThemeToggle = ({ + initialTheme, +}) => { + const [theme, setTheme] = React.useState(initialTheme!) + + const handleSetTheme = (t: string) => () => { + setTheme(t) + } + + React.useEffect(() => { + window.localStorage.setItem('tesseract-theme', theme as string) + }, [theme]) + + React.useEffect(() => { + applyStyles(theme) + }, [theme]) + + React.useEffect(() => { + const handler = handleInitializeTheme(initialTheme) + window.addEventListener('load', handler) + return () => { + window.removeEventListener('load', handler) + } + }, [initialTheme]) + + return ( + + + + { + theme === 'Dark' + && ( + + ) + } + { + theme === 'Light' + && ( + + ) + } + + + ) +} + +export default ThemeToggle diff --git a/packages/react-common-docs/src/docgen.json b/packages/react-common-docs/src/docgen.json index f4799ff..842e6b2 100644 --- a/packages/react-common-docs/src/docgen.json +++ b/packages/react-common-docs/src/docgen.json @@ -251,7 +251,7 @@ "defaultValue": { "value": null }, - "description": "Describe of what the component represents.", + "description": "Description of what the component represents.", "name": "label", "required": false, "type": { diff --git a/packages/react-common-docs/src/pages/_app.tsx b/packages/react-common-docs/src/pages/_app.tsx index d44e01e..1a2e1d7 100644 --- a/packages/react-common-docs/src/pages/_app.tsx +++ b/packages/react-common-docs/src/pages/_app.tsx @@ -1,10 +1,12 @@ import * as React from 'react' +import { MDXProvider } from '@mdx-js/react' +import Head from 'next/head' import styled from 'styled-components' import sidebar from '../sidebar.json' import brand from '../../brand' import Sidebar from '../components/Sidebar/Sidebar' -import '../../public/global.css' -import '../../public/theme/dark.css' +import pkg from '../../../../package.json' +import CodeBlock from '../components/CodeBlock/CodeBlock' const Container = styled('div')({ maxWidth: 720, @@ -27,15 +29,26 @@ const App: React.FC = ({ pageProps, }) => ( + + {pkg['title'] || pkg.name} + +
- + + +
diff --git a/packages/react-common-docs/src/pages/_document.tsx b/packages/react-common-docs/src/pages/_document.tsx index 51d6aff..447a5ce 100644 --- a/packages/react-common-docs/src/pages/_document.tsx +++ b/packages/react-common-docs/src/pages/_document.tsx @@ -1,12 +1,14 @@ -import pkg from '../../../../package.json' import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document' import { ServerStyleSheet } from 'styled-components' +import pkg from '../../../../package.json' +import config from '../../next.config' + +const publicUrl = process.env.NODE_ENV === 'production' ? pkg.homepage : config.basePath export default class MyDocument extends Document { static async getInitialProps(ctx: DocumentContext) { const sheet = new ServerStyleSheet() const originalRenderPage = ctx.renderPage - const publicUrl = process.env.NODE_ENV === 'production' ? pkg.homepage : '' try { ctx.renderPage = () => diff --git a/packages/react-common-docs/src/pages/components/TextInput.mdx b/packages/react-common-docs/src/pages/components/TextInput.mdx index f168bc8..28b9bfa 100644 --- a/packages/react-common-docs/src/pages/components/TextInput.mdx +++ b/packages/react-common-docs/src/pages/components/TextInput.mdx @@ -38,7 +38,7 @@ act as guide to the user on how long the expected input values are. components={{ TextInput }} code={`
- I am and I live in . + I am and I live in .
`} /> @@ -50,6 +50,7 @@ element specified as `grid`. This is to be able to add complementing content to some content that is best displayed outside the component instead of putting in the `hint` prop. ( fontSize: SECONDARY_TEXT_SIZES[size!], }} > - ({stringify(hint)}) + {stringify(hint)} )} {!multiple && ( diff --git a/packages/react-common/src/components/TextInput/TextInput.tsx b/packages/react-common/src/components/TextInput/TextInput.tsx index d0977ff..14bdb50 100644 --- a/packages/react-common/src/components/TextInput/TextInput.tsx +++ b/packages/react-common/src/components/TextInput/TextInput.tsx @@ -351,7 +351,7 @@ const TextInput = React.forwardRef - ({stringify(hint)}) + {stringify(hint)} )} {(indicator as PropTypes.ReactComponentLike) && (