@@ -1,5 +1,6 @@ | |||||
{ | { | ||||
"name": "@tesseract-design/react-common", | "name": "@tesseract-design/react-common", | ||||
"title": "React Common", | |||||
"version": "0.2.3", | "version": "0.2.3", | ||||
"description": "Common front-end components for Web using the Tesseract design system, written in React.", | "description": "Common front-end components for Web using the Tesseract design system, written in React.", | ||||
"directories": { | "directories": { | ||||
@@ -16,6 +16,8 @@ | |||||
"next": "10.0.1", | "next": "10.0.1", | ||||
"next-mdx-frontmatter": "^0.0.3", | "next-mdx-frontmatter": "^0.0.3", | ||||
"pascal-case": "^3.1.1", | "pascal-case": "^3.1.1", | ||||
"prism-react-renderer": "^1.1.1", | |||||
"prismjs": "^1.22.0", | |||||
"react-docgen-typescript": "^1.20.5", | "react-docgen-typescript": "^1.20.5", | ||||
"react-live": "^2.2.3", | "react-live": "^2.2.3", | ||||
"remark-parse": "^9.0.0", | "remark-parse": "^9.0.0", | ||||
@@ -55,4 +55,5 @@ pre { | |||||
overflow: auto; | overflow: auto; | ||||
margin: 0 -1rem; | margin: 0 -1rem; | ||||
padding: 0 1rem; | padding: 0 1rem; | ||||
line-height: 1.2; | |||||
} | } |
@@ -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 ( | |||||
<Base> | |||||
{ | |||||
meta['title'] && ( | |||||
<Title> | |||||
{meta['title']} | |||||
</Title> | |||||
) | |||||
} | |||||
<Highlight | |||||
{...defaultProps} | |||||
language={language} | |||||
code={(code as string).trim()} | |||||
theme={PrismTheme} | |||||
> | |||||
{({ style, tokens, getLineProps, getTokenProps }) => ( | |||||
<pre style={style}> | |||||
{tokens.map((line, i) => ( | |||||
<div {...getLineProps({ line, key: i })}> | |||||
{line.map((token, key) => ( | |||||
<span {...getTokenProps({ token, key })} /> | |||||
))} | |||||
</div> | |||||
))} | |||||
</pre> | |||||
)} | |||||
</Highlight> | |||||
</Base> | |||||
) | |||||
} | |||||
export default CodeBlock |
@@ -5,6 +5,7 @@ import unified from 'unified' | |||||
import parse from 'remark-parse' | import parse from 'remark-parse' | ||||
import remark2react from 'remark-react' | import remark2react from 'remark-react' | ||||
import docgen from '../../docgen.json' | import docgen from '../../docgen.json' | ||||
import pkg from '../../../../../package.json' | |||||
const propTypes = { | const propTypes = { | ||||
of: PropTypes.string, | of: PropTypes.string, | ||||
@@ -23,8 +24,9 @@ const Header: React.FC<Props> = ({ of: ofAttr }) => { | |||||
<React.Fragment> | <React.Fragment> | ||||
<Head> | <Head> | ||||
<title> | <title> | ||||
{docs.displayName} | React Common | |||||
{docs.displayName} | {pkg['title'] || pkg.name} | |||||
</title> | </title> | ||||
<meta name="description" content={docs.description} key="description" /> | |||||
</Head> | </Head> | ||||
<h1>{docs.displayName}</h1> | <h1>{docs.displayName}</h1> | ||||
<p> | <p> | ||||
@@ -18,6 +18,9 @@ const Container = styled('span')({ | |||||
const HeaderContainer = styled(Container)({ | const HeaderContainer = styled(Container)({ | ||||
marginTop: '2rem', | marginTop: '2rem', | ||||
fontSize: '0.75rem', | |||||
fontWeight: 'bolder', | |||||
textTransform: 'uppercase', | |||||
}) | }) | ||||
const propTypes = { | const propTypes = { | ||||
@@ -3,6 +3,7 @@ import * as PropTypes from 'prop-types' | |||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import { Icon } from '../../../../react-common/src' | import { Icon } from '../../../../react-common/src' | ||||
import MenuGraphics, { propTypes as menuGraphicsPropTypes } from '../MenuGraphics/MenuGraphics' | import MenuGraphics, { propTypes as menuGraphicsPropTypes } from '../MenuGraphics/MenuGraphics' | ||||
import { useRouter } from 'next/router' | |||||
const Link = styled('a')({ | const Link = styled('a')({ | ||||
display: 'block', | 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')({ | const LinkText = styled('span')({ | ||||
alignSelf: 'center', | alignSelf: 'center', | ||||
display: 'block', | display: 'block', | ||||
@@ -102,55 +112,60 @@ const NavLink = React.forwardRef<HTMLAnchorElement, Props>(( | |||||
onClick, | onClick, | ||||
}, | }, | ||||
ref | ref | ||||
) => ( | |||||
<Link | |||||
ref={ref} | |||||
href={href} | |||||
onClick={onClick as (...args: any[]) => any} | |||||
> | |||||
{ | |||||
// @ts-ignore | |||||
<EnclosingComponent> | |||||
{ | |||||
graphics as object | |||||
&& ( | |||||
<MenuGraphicsContainer> | |||||
{ | |||||
// @ts-ignore | |||||
<MenuGraphics | |||||
{...graphics} | |||||
/> | |||||
} | |||||
</MenuGraphicsContainer> | |||||
) | |||||
} | |||||
<LinkText> | |||||
<LinkTitle> | |||||
{title} | |||||
</LinkTitle> | |||||
) => { | |||||
const router = useRouter() | |||||
const active = router.basePath + router.route === href | |||||
const Component = active ? ActiveLink : Link | |||||
return ( | |||||
<Component | |||||
ref={ref} | |||||
href={href} | |||||
onClick={onClick as (...args: any[]) => any} | |||||
> | |||||
{ | |||||
// @ts-ignore | |||||
<EnclosingComponent> | |||||
{ | { | ||||
subtitle as string | |||||
graphics as object | |||||
&& ( | && ( | ||||
<LinkSubtitle> | |||||
{subtitle} | |||||
</LinkSubtitle> | |||||
<MenuGraphicsContainer> | |||||
{ | |||||
// @ts-ignore | |||||
<MenuGraphics | |||||
{...graphics} | |||||
/> | |||||
} | |||||
</MenuGraphicsContainer> | |||||
) | ) | ||||
} | } | ||||
</LinkText> | |||||
{ | |||||
indicator as string | |||||
&& ( | |||||
<IndicatorContainer> | |||||
<Icon | |||||
name={indicator!} | |||||
/> | |||||
</IndicatorContainer> | |||||
) | |||||
} | |||||
</EnclosingComponent> | |||||
} | |||||
</Link> | |||||
)) | |||||
<LinkText> | |||||
<LinkTitle> | |||||
{title} | |||||
</LinkTitle> | |||||
{ | |||||
subtitle as string | |||||
&& ( | |||||
<LinkSubtitle> | |||||
{subtitle} | |||||
</LinkSubtitle> | |||||
) | |||||
} | |||||
</LinkText> | |||||
{ | |||||
indicator as string | |||||
&& ( | |||||
<IndicatorContainer> | |||||
<Icon | |||||
name={indicator!} | |||||
/> | |||||
</IndicatorContainer> | |||||
) | |||||
} | |||||
</EnclosingComponent> | |||||
} | |||||
</Component> | |||||
) | |||||
}) | |||||
NavLink.propTypes = propTypes | NavLink.propTypes = propTypes | ||||
@@ -1,5 +1,6 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as PropTypes from 'prop-types' | import * as PropTypes from 'prop-types' | ||||
import PrismTheme from '../../utilities/prism-themes/dark' | |||||
import { | import { | ||||
LiveProvider, | LiveProvider, | ||||
LiveEditor, | LiveEditor, | ||||
@@ -8,13 +9,42 @@ import { | |||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
const Figure = styled('figure')({ | 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)({ | const StyledLiveEditor = styled(LiveEditor)({ | ||||
lineHeight: 1.125, | lineHeight: 1.125, | ||||
fontFamily: 'var(--font-family-monospace), monospace !important', | fontFamily: 'var(--font-family-monospace), monospace !important', | ||||
marginTop: '1rem', | marginTop: '1rem', | ||||
'textarea': { | |||||
padding: '0 !important', | |||||
outline: 0, | |||||
}, | |||||
'pre': { | |||||
padding: '0 !important', | |||||
}, | |||||
}) | }) | ||||
const propTypes = { | const propTypes = { | ||||
@@ -42,9 +72,17 @@ const Playground: React.FC<Props> = ({ | |||||
{ | { | ||||
label as string | label as string | ||||
&& ( | && ( | ||||
<figcaption> | |||||
{label} | |||||
</figcaption> | |||||
<Caption> | |||||
Playground - {label} | |||||
</Caption> | |||||
) | |||||
} | |||||
{ | |||||
!label | |||||
&& ( | |||||
<Caption> | |||||
Playground | |||||
</Caption> | |||||
) | ) | ||||
} | } | ||||
<div> | <div> | ||||
@@ -52,7 +90,9 @@ const Playground: React.FC<Props> = ({ | |||||
...components, | ...components, | ||||
}}> | }}> | ||||
<LivePreview /> | <LivePreview /> | ||||
<StyledLiveEditor /> | |||||
<StyledLiveEditor | |||||
theme={PrismTheme} | |||||
/> | |||||
</LiveProvider> | </LiveProvider> | ||||
</div> | </div> | ||||
</Figure> | </Figure> | ||||
@@ -1,6 +1,7 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import * as PropTypes from 'prop-types' | import * as PropTypes from 'prop-types' | ||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import PrismTheme from '../../utilities/prism-themes/dark' | |||||
import docgen from '../../docgen.json' | import docgen from '../../docgen.json' | ||||
const Base = styled('table')({ | 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('?') | |||||
? ( | |||||
<React.Fragment> | |||||
<span | |||||
style={trueStyle.style} | |||||
> | |||||
{propName.slice(0, -1)} | |||||
</span> | |||||
<span | |||||
style={operator.style} | |||||
> | |||||
{propName.slice(-1)} | |||||
</span> | |||||
</React.Fragment> | |||||
) | |||||
: ( | |||||
<span | |||||
style={trueStyle.style} | |||||
> | |||||
{propName} | |||||
</span> | |||||
) | |||||
) | |||||
} | |||||
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 = ( | |||||
<React.Fragment> | |||||
<span | |||||
style={punctuation.style} | |||||
> | |||||
{t.slice(0, t.indexOf('"') + 1)} | |||||
</span> | |||||
<span | |||||
style={string.style} | |||||
> | |||||
{t.slice(t.indexOf('"') + 1, t.lastIndexOf('"'))} | |||||
</span> | |||||
<span | |||||
style={punctuation.style} | |||||
> | |||||
{t.slice(t.lastIndexOf('"'))} | |||||
</span> | |||||
</React.Fragment> | |||||
) | |||||
} else if ('any string boolean number symbol unknown object bigint'.split(' ').includes(t)) { | |||||
currentToken = ( | |||||
<span | |||||
style={keyword.style} | |||||
> | |||||
{t} | |||||
</span> | |||||
) | |||||
} else if ('null'.split(' ').includes(t)) { | |||||
currentToken = ( | |||||
<span | |||||
style={keyword.style} | |||||
> | |||||
{t} | |||||
</span> | |||||
) | |||||
} else if ('true false'.split(' ').includes(t)) { | |||||
currentToken = ( | |||||
<span | |||||
style={boolean.style} | |||||
> | |||||
{t} | |||||
</span> | |||||
) | |||||
} else if (!isNaN(Number(t))) { | |||||
currentToken = ( | |||||
<span | |||||
style={number.style} | |||||
> | |||||
{t} | |||||
</span> | |||||
) | |||||
} else { | |||||
currentToken = ( | |||||
<span | |||||
style={type.style} | |||||
> | |||||
{t} | |||||
</span> | |||||
) | |||||
} | |||||
return [ | |||||
...tt, | |||||
i > 0 && <span | |||||
style={operator.style} | |||||
> | |||||
| | |||||
</span>, | |||||
currentToken, | |||||
] | |||||
}, | |||||
[] | |||||
) | |||||
} | |||||
const Props: React.FC<Props> = ({ of: ofAttr }) => { | const Props: React.FC<Props> = ({ of: ofAttr }) => { | ||||
const docs = (docgen as unknown as Docs[]).find(d => d.displayName === ofAttr) | const docs = (docgen as unknown as Docs[]).find(d => d.displayName === ofAttr) | ||||
@@ -147,50 +292,37 @@ const Props: React.FC<Props> = ({ of: ofAttr }) => { | |||||
</HeaderCellGroup> | </HeaderCellGroup> | ||||
<BodyCellGroup> | <BodyCellGroup> | ||||
{ | { | ||||
Object.entries((docs as Docs).props).map(([name, def]) => ( | |||||
<HeaderRow> | |||||
<MainBodyCell> | |||||
<Code> | |||||
{def.required ? name : name + '?'} | |||||
</Code> | |||||
</MainBodyCell> | |||||
<BodyCell | |||||
data-column-name="Type" | |||||
> | |||||
{ | |||||
def.type.name === 'enum' | |||||
&& ( | |||||
<Code> | |||||
{def.type.value.map(v => v.value).join(' | ')} | |||||
</Code> | |||||
) | |||||
} | |||||
{ | |||||
def.type.name !== 'enum' | |||||
&& ( | |||||
<Code> | |||||
{def.type.name} | |||||
</Code> | |||||
) | |||||
} | |||||
</BodyCell> | |||||
<BodyCell | |||||
data-column-name={def.defaultValue ? 'Default' : undefined } | |||||
> | |||||
{ | |||||
def.defaultValue | |||||
&& ( | |||||
<Code> | |||||
{JSON.stringify(def.defaultValue.value)} | |||||
</Code> | |||||
) | |||||
} | |||||
</BodyCell> | |||||
<BodyCell> | |||||
{def.description} | |||||
</BodyCell> | |||||
</HeaderRow> | |||||
)) | |||||
Object.entries((docs as Docs).props).map(([name, prop]) => { | |||||
const propName = getPropName(name, prop) | |||||
const propDefaultValue = getPropDefaultValue(prop) | |||||
const propType = getPropType(prop) | |||||
return ( | |||||
<HeaderRow> | |||||
<MainBodyCell> | |||||
<Code> | |||||
{getPropNameHtml(propName, propType)} | |||||
</Code> | |||||
</MainBodyCell> | |||||
<BodyCell | |||||
data-column-name="Type" | |||||
> | |||||
<Code> | |||||
{getPropTypeHtml(propType)} | |||||
</Code> | |||||
</BodyCell> | |||||
<BodyCell | |||||
data-column-name={propDefaultValue ? 'Default' : undefined} | |||||
> | |||||
<Code> | |||||
{getPropTypeHtml(propDefaultValue)} | |||||
</Code> | |||||
</BodyCell> | |||||
<BodyCell> | |||||
{prop.description} | |||||
</BodyCell> | |||||
</HeaderRow> | |||||
) | |||||
}) | |||||
} | } | ||||
</BodyCellGroup> | </BodyCellGroup> | ||||
</Base> | </Base> | ||||
@@ -6,6 +6,7 @@ import styled from 'styled-components' | |||||
import Link from 'next/link' | import Link from 'next/link' | ||||
import Nav from '../Nav/Nav' | import Nav from '../Nav/Nav' | ||||
import { MouseEventHandler } from 'react' | import { MouseEventHandler } from 'react' | ||||
import ThemeToggle from '../ThemeToggle/ThemeToggle' | |||||
const StyledLink = styled('a')({ | const StyledLink = styled('a')({ | ||||
display: 'block', | display: 'block', | ||||
@@ -82,21 +83,6 @@ const Actions = styled('div')({ | |||||
alignItems: 'center', | 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')({ | const NavWrapper = styled('nav')({ | ||||
'--size-link': '3rem', | '--size-link': '3rem', | ||||
marginBottom: '4rem', | marginBottom: '4rem', | ||||
@@ -122,82 +108,39 @@ type Props = PropTypes.InferProps<typeof propTypes> | |||||
const Sidebar: React.FC<Props> = ({ | const Sidebar: React.FC<Props> = ({ | ||||
data, | data, | ||||
brand: BrandRaw, | brand: BrandRaw, | ||||
initialTheme = 'Dark', | |||||
initialTheme, | |||||
}) => { | }) => { | ||||
const [theme, setTheme] = React.useState(initialTheme) | |||||
const [sidebar, setSidebar] = React.useState(false) | const [sidebar, setSidebar] = React.useState(false) | ||||
const navRef = React.useRef<HTMLDivElement>(null) | const navRef = React.useRef<HTMLDivElement>(null) | ||||
const Brand = BrandRaw! | const Brand = BrandRaw! | ||||
const toggleDarkMode = (b: string) => () => { | |||||
setTheme(b) | |||||
} | |||||
const toggleSidebar = (b: boolean): MouseEventHandler => (e) => { | const toggleSidebar = (b: boolean): MouseEventHandler => (e) => { | ||||
e.preventDefault() | e.preventDefault() | ||||
setSidebar(b) | 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 () => { | 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 ( | return ( | ||||
<React.Fragment> | <React.Fragment> | ||||
<SidebarToggle | <SidebarToggle | ||||
@@ -237,33 +180,9 @@ const Sidebar: React.FC<Props> = ({ | |||||
</Link> | </Link> | ||||
<Container> | <Container> | ||||
<Actions> | <Actions> | ||||
<ToggleWrapper> | |||||
<ToggleInput | |||||
type="checkbox" | |||||
defaultChecked={theme === 'Dark'} | |||||
onChange={toggleDarkMode(theme === 'Dark' ? 'Light' : 'Dark')} | |||||
/> | |||||
<ToggleIcon> | |||||
{ | |||||
theme === 'Dark' | |||||
&& ( | |||||
<Icon | |||||
label="Set Light Mode" | |||||
name="moon" | |||||
/> | |||||
) | |||||
} | |||||
{ | |||||
theme === 'Light' | |||||
&& ( | |||||
<Icon | |||||
label="Set Dark Mode" | |||||
name="sun" | |||||
/> | |||||
) | |||||
} | |||||
</ToggleIcon> | |||||
</ToggleWrapper> | |||||
<ThemeToggle | |||||
initialTheme={initialTheme} | |||||
/> | |||||
<RepoLink | <RepoLink | ||||
href={pkg.repository} | href={pkg.repository} | ||||
target="_blank" | target="_blank" | ||||
@@ -0,0 +1,96 @@ | |||||
import * as React from 'react' | |||||
import styled from 'styled-components' | |||||
import { Icon } from '../../../../react-common/src' | |||||
const ToggleWrapper = styled('label')({ | |||||
cursor: 'pointer', | |||||
color: 'var(--color-accent)', | |||||
display: 'inline-block', | |||||
}) | |||||
const ToggleInput = styled('input')({ | |||||
position: 'absolute', | |||||
left: -999999, | |||||
}) | |||||
const getAutoDetectTheme = () => ( | |||||
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 ( | |||||
<ToggleWrapper> | |||||
<ToggleInput | |||||
type="checkbox" | |||||
defaultChecked={theme === 'Dark'} | |||||
onChange={handleSetTheme(theme === 'Dark' ? 'Light' : 'Dark')} | |||||
/> | |||||
<span> | |||||
{ | |||||
theme === 'Dark' | |||||
&& ( | |||||
<Icon | |||||
label="Set Light Mode" | |||||
name="moon" | |||||
/> | |||||
) | |||||
} | |||||
{ | |||||
theme === 'Light' | |||||
&& ( | |||||
<Icon | |||||
label="Set Dark Mode" | |||||
name="sun" | |||||
/> | |||||
) | |||||
} | |||||
</span> | |||||
</ToggleWrapper> | |||||
) | |||||
} | |||||
export default ThemeToggle |
@@ -251,7 +251,7 @@ | |||||
"defaultValue": { | "defaultValue": { | ||||
"value": null | "value": null | ||||
}, | }, | ||||
"description": "Describe of what the component represents.", | |||||
"description": "Description of what the component represents.", | |||||
"name": "label", | "name": "label", | ||||
"required": false, | "required": false, | ||||
"type": { | "type": { | ||||
@@ -1,10 +1,12 @@ | |||||
import * as React from 'react' | import * as React from 'react' | ||||
import { MDXProvider } from '@mdx-js/react' | |||||
import Head from 'next/head' | |||||
import styled from 'styled-components' | import styled from 'styled-components' | ||||
import sidebar from '../sidebar.json' | import sidebar from '../sidebar.json' | ||||
import brand from '../../brand' | import brand from '../../brand' | ||||
import Sidebar from '../components/Sidebar/Sidebar' | 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')({ | const Container = styled('div')({ | ||||
maxWidth: 720, | maxWidth: 720, | ||||
@@ -27,15 +29,26 @@ const App: React.FC<AppProps> = ({ | |||||
pageProps, | pageProps, | ||||
}) => ( | }) => ( | ||||
<React.Fragment> | <React.Fragment> | ||||
<Head> | |||||
<title>{pkg['title'] || pkg.name}</title> | |||||
<meta name="description" content={pkg.description} key="description" /> | |||||
</Head> | |||||
<Sidebar | <Sidebar | ||||
brand={brand} | brand={brand} | ||||
data={sidebar} | data={sidebar} | ||||
initialTheme="Dark" | |||||
/> | /> | ||||
<Main> | <Main> | ||||
<Container> | <Container> | ||||
<Component | |||||
{...pageProps} | |||||
/> | |||||
<MDXProvider | |||||
components={{ | |||||
pre: CodeBlock, | |||||
}} | |||||
> | |||||
<Component | |||||
{...pageProps} | |||||
/> | |||||
</MDXProvider> | |||||
</Container> | </Container> | ||||
</Main> | </Main> | ||||
</React.Fragment> | </React.Fragment> | ||||
@@ -1,12 +1,14 @@ | |||||
import pkg from '../../../../package.json' | |||||
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document' | import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document' | ||||
import { ServerStyleSheet } from 'styled-components' | 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 { | export default class MyDocument extends Document { | ||||
static async getInitialProps(ctx: DocumentContext) { | static async getInitialProps(ctx: DocumentContext) { | ||||
const sheet = new ServerStyleSheet() | const sheet = new ServerStyleSheet() | ||||
const originalRenderPage = ctx.renderPage | const originalRenderPage = ctx.renderPage | ||||
const publicUrl = process.env.NODE_ENV === 'production' ? pkg.homepage : '' | |||||
try { | try { | ||||
ctx.renderPage = () => | ctx.renderPage = () => | ||||
@@ -38,7 +38,7 @@ act as guide to the user on how long the expected input values are. | |||||
components={{ TextInput }} | components={{ TextInput }} | ||||
code={` | code={` | ||||
<form> | <form> | ||||
I am <span style={{ display: 'inline-block', width: '16rem', verticalAlign: 'bottom', }}><TextInput label="Full name" hint="given and family name" /></span> and I live in <span style={{ display: 'inline-block', width: '24rem', verticalAlign: 'bottom', }}><TextInput label="Address" hint="city, state and country" /></span>. | |||||
I am <span style={{ display: 'inline-block', width: '12rem', verticalAlign: 'bottom', }}><TextInput label="Full name" hint="given and family name" /></span> and I live in <span style={{ display: 'inline-block', width: '16rem', verticalAlign: 'bottom', }}><TextInput label="Address" hint="city, state and country" /></span>. | |||||
</form> | </form> | ||||
`} | `} | ||||
/> | /> | ||||
@@ -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. | some content that is best displayed outside the component instead of putting in the `hint` prop. | ||||
<Playground | <Playground | ||||
label="Example shipping address form" | |||||
components={{ TextInput }} | components={{ TextInput }} | ||||
code={` | code={` | ||||
<form | <form | ||||
@@ -51,8 +51,7 @@ The base font family. Used for body text. | |||||
You should have a global theme rule applied to `:root`. Below is an example of using a global theme to apply styles: | You should have a global theme rule applied to `:root`. Below is an example of using a global theme to apply styles: | ||||
`theme.css` | |||||
```css | |||||
```css title=theme.css | |||||
:root { | :root { | ||||
/* This will be our reference background color */ | /* This will be our reference background color */ | ||||
--color-negative: #eee; | --color-negative: #eee; | ||||
@@ -0,0 +1,141 @@ | |||||
import { PrismTheme } from 'prism-react-renderer' | |||||
const theme: PrismTheme = { | |||||
plain: { | |||||
backgroundColor: "transparent", | |||||
color: "#ffffff" | |||||
}, | |||||
styles: [ | |||||
{ | |||||
types: ["comment", "prolog", "doctype", "cdata"], | |||||
style: { | |||||
// color: "#6c6783" | |||||
opacity: 0.3, | |||||
fontStyle: 'italic', | |||||
} | |||||
}, | |||||
{ | |||||
types: ["namespace"], | |||||
style: { | |||||
opacity: 0.7 | |||||
} | |||||
}, | |||||
{ | |||||
types: ["number", "boolean", "hexcode"], | |||||
style: { | |||||
color: "#74f95e", | |||||
fontWeight: 'bold', | |||||
} | |||||
}, | |||||
{ | |||||
types: ["tag", "operator", "keyword", "atrule", "selector"], | |||||
style: { | |||||
color: "#ff4389", | |||||
fontWeight: 'bold', | |||||
} | |||||
}, | |||||
{ | |||||
types: ["type", "class"], | |||||
style: { | |||||
color: "#5097D2", | |||||
fontWeight: 'bold', | |||||
} | |||||
}, | |||||
{ | |||||
types: ["punctuation"], | |||||
style: { | |||||
color: "#ffffff" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["function"], | |||||
style: { | |||||
color: "#67c252", | |||||
fontStyle: 'italic', | |||||
} | |||||
}, | |||||
{ | |||||
types: ["parameter"], | |||||
style: { | |||||
color: "#915ec2" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["property"], | |||||
style: { | |||||
color: "#ffa1c9" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["tag-id", "atrule-id"], | |||||
style: { | |||||
color: "#eeebff" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["attr-name"], | |||||
style: { | |||||
color: "#ffa1c9" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["string", "attr-value", 'unit'], | |||||
style: { | |||||
color: "#eed371" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["variable"], | |||||
style: { | |||||
color: "#8bc275" | |||||
} | |||||
}, | |||||
{ | |||||
types: [ | |||||
"entity", | |||||
"url", | |||||
"control", | |||||
"directive", | |||||
"statement", | |||||
"regex", | |||||
"placeholder", | |||||
], | |||||
style: { | |||||
color: "#ffcc99" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["deleted"], | |||||
style: { | |||||
textDecorationLine: "line-through" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["inserted"], | |||||
style: { | |||||
textDecorationLine: "underline" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["italic"], | |||||
style: { | |||||
fontStyle: "italic" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["important", "bold"], | |||||
style: { | |||||
fontWeight: "bold" | |||||
} | |||||
}, | |||||
{ | |||||
types: ["important"], | |||||
style: { | |||||
color: "#c4b9fe" | |||||
} | |||||
} | |||||
] | |||||
} | |||||
export default theme |
@@ -2175,6 +2175,15 @@ clean-stack@^2.0.0: | |||||
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" | resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" | ||||
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== | integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== | ||||
clipboard@^2.0.0: | |||||
version "2.0.6" | |||||
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" | |||||
integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg== | |||||
dependencies: | |||||
good-listener "^1.2.2" | |||||
select "^1.1.2" | |||||
tiny-emitter "^2.0.0" | |||||
code-point-at@^1.0.0: | code-point-at@^1.0.0: | ||||
version "1.1.0" | version "1.1.0" | ||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" | ||||
@@ -2590,6 +2599,11 @@ define-property@^2.0.2: | |||||
is-descriptor "^1.0.2" | is-descriptor "^1.0.2" | ||||
isobject "^3.0.1" | isobject "^3.0.1" | ||||
delegate@^3.1.2: | |||||
version "3.2.0" | |||||
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" | |||||
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== | |||||
delegates@^1.0.0: | delegates@^1.0.0: | ||||
version "1.0.0" | version "1.0.0" | ||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" | ||||
@@ -3173,6 +3187,13 @@ globals@^11.1.0: | |||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" | resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" | ||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== | integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== | ||||
good-listener@^1.2.2: | |||||
version "1.2.2" | |||||
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" | |||||
integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= | |||||
dependencies: | |||||
delegate "^3.1.2" | |||||
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: | graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: | ||||
version "4.2.4" | version "4.2.4" | ||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" | ||||
@@ -4897,11 +4918,18 @@ prebuild-install@^5.3.5: | |||||
tunnel-agent "^0.6.0" | tunnel-agent "^0.6.0" | ||||
which-pm-runs "^1.0.0" | which-pm-runs "^1.0.0" | ||||
prism-react-renderer@^1.0.1: | |||||
prism-react-renderer@^1.0.1, prism-react-renderer@^1.1.1: | |||||
version "1.1.1" | version "1.1.1" | ||||
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.1.1.tgz#1c1be61b1eb9446a146ca7a50b7bcf36f2a70a44" | resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.1.1.tgz#1c1be61b1eb9446a146ca7a50b7bcf36f2a70a44" | ||||
integrity sha512-MgMhSdHuHymNRqD6KM3eGS0PNqgK9q4QF5P0yoQQvpB6jNjeSAi3jcSAz0Sua/t9fa4xDOMar9HJbLa08gl9ug== | integrity sha512-MgMhSdHuHymNRqD6KM3eGS0PNqgK9q4QF5P0yoQQvpB6jNjeSAi3jcSAz0Sua/t9fa4xDOMar9HJbLa08gl9ug== | ||||
prismjs@^1.22.0: | |||||
version "1.22.0" | |||||
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.22.0.tgz#73c3400afc58a823dd7eed023f8e1ce9fd8977fa" | |||||
integrity sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w== | |||||
optionalDependencies: | |||||
clipboard "^2.0.0" | |||||
process-nextick-args@~2.0.0: | process-nextick-args@~2.0.0: | ||||
version "2.0.1" | version "2.0.1" | ||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" | ||||
@@ -5411,6 +5439,11 @@ schema-utils@^1.0.0: | |||||
ajv-errors "^1.0.0" | ajv-errors "^1.0.0" | ||||
ajv-keywords "^3.1.0" | ajv-keywords "^3.1.0" | ||||
select@^1.1.2: | |||||
version "1.1.2" | |||||
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" | |||||
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= | |||||
semver@7.0.0: | semver@7.0.0: | ||||
version "7.0.0" | version "7.0.0" | ||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" | resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" | ||||
@@ -5952,6 +5985,11 @@ timers-browserify@^2.0.4: | |||||
dependencies: | dependencies: | ||||
setimmediate "^1.0.4" | setimmediate "^1.0.4" | ||||
tiny-emitter@^2.0.0: | |||||
version "2.1.0" | |||||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" | |||||
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== | |||||
title-case@^2.1.0: | title-case@^2.1.0: | ||||
version "2.1.1" | version "2.1.1" | ||||
resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" | resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" | ||||
@@ -36,7 +36,7 @@ const propTypes = { | |||||
*/ | */ | ||||
size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | ||||
/** | /** | ||||
* Describe of what the component represents. | |||||
* Description of what the component represents. | |||||
*/ | */ | ||||
label: PropTypes.string, | label: PropTypes.string, | ||||
} | } | ||||
@@ -291,7 +291,7 @@ const Select = React.forwardRef<HTMLSelectElement, Props>( | |||||
fontSize: SECONDARY_TEXT_SIZES[size!], | fontSize: SECONDARY_TEXT_SIZES[size!], | ||||
}} | }} | ||||
> | > | ||||
({stringify(hint)}) | |||||
{stringify(hint)} | |||||
</HintWrapper> | </HintWrapper> | ||||
)} | )} | ||||
{!multiple && ( | {!multiple && ( | ||||
@@ -351,7 +351,7 @@ const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, Props | |||||
fontSize: SECONDARY_TEXT_SIZES[size!], | fontSize: SECONDARY_TEXT_SIZES[size!], | ||||
}} | }} | ||||
> | > | ||||
({stringify(hint)}) | |||||
{stringify(hint)} | |||||
</HintWrapper> | </HintWrapper> | ||||
)} | )} | ||||
{(indicator as PropTypes.ReactComponentLike) && ( | {(indicator as PropTypes.ReactComponentLike) && ( | ||||