diff --git a/docs/components/ComponentProps.js b/docs/components/ComponentProps.js
new file mode 100644
index 00000000000..e3a1d551df7
--- /dev/null
+++ b/docs/components/ComponentProps.js
@@ -0,0 +1,273 @@
+import React from 'react'
+import styled from 'styled-components'
+import Link from '../../src/Link'
+import {H1, H2, H3, H4, H5, H6} from '@primer/gatsby-theme-doctocat/src/components/heading'
+import BorderBox from '../../src/BorderBox'
+import Button from '../../src/Button'
+import Text from '../../src/Text'
+import Details from '../../src/Details'
+import InlineCode from '@primer/gatsby-theme-doctocat/src/components/inline-code'
+import Paragraph from '@primer/gatsby-theme-doctocat/src/components/paragraph'
+import Table from './Table'
+
+function getHeadingElement(headingLevel) {
+ switch (headingLevel) {
+ case 1:
+ return H1
+ case 2:
+ return H2
+ case 3:
+ return H3
+ case 4:
+ return H4
+ case 5:
+ return H5
+ case 6:
+ return H6
+ }
+}
+
+const InheritedBox = styled(BorderBox)`
+ > :first-child {
+ margin-top: 0px !important;
+ }
+`
+
+function collect(inherited, acc = {system: [], inherited: []}, seen = new Set()) {
+ for (const Comp of inherited) {
+ if (Comp.propTypes && Comp.propTypes.__doc_spec) {
+ const {system, inherited: nestedInherited} = Comp.propTypes.__doc_spec
+ for (const sys of system) {
+ if (!seen.has(sys)) {
+ acc.system.push(sys)
+ seen.add(sys)
+ }
+ }
+ for (const inh of nestedInherited) {
+ if (!seen.has(inh)) {
+ acc.inherited.push(inh)
+ seen.add(inh)
+ }
+ }
+ if (nestedInherited.length) {
+ collect(nestedInherited, acc, seen)
+ }
+ }
+ }
+
+ return acc
+}
+
+function ComponentProps({Component, name, headingLevel, showInherited, showSystem}) {
+ if (!Component.propTypes || !Component.propTypes.__doc_spec) {
+ return null
+ }
+
+ const Heading = getHeadingElement(headingLevel)
+
+ const {own} = Component.propTypes.__doc_spec
+ const {system, inherited} = collect([Component])
+
+ const output = []
+
+ if (own) {
+ output.push(
+
+ )
+ }
+
+ const inheritedWithDocs = inherited.filter(Comp => Comp.propTypes && Comp.propTypes.__doc_spec)
+ if (inheritedWithDocs.length && showInherited) {
+ output.push()
+ output.push(
+
+ Inherited props
+
+ {name} inherits from the following components and thus receives their props:
+
+
+ )
+ for (const Comp of inherited) {
+ output.push(
+
+ {({open}) => (
+ <>
+
+ {Comp.displayName} {open ? '▼' : '►'}
+
+
+
+
+ >
+ )}
+
+ )
+ }
+ }
+
+ if (showSystem && system && system.length) {
+ output.push( )
+ }
+
+ return <>{output}>
+}
+
+ComponentProps.defaultProps = {
+ headingLevel: 3,
+ showInherited: true,
+ showSystem: true
+}
+
+function getDefault(defaults, prop) {
+ const value = defaults[prop]
+ return getDisplayValue(value)
+}
+
+function getDisplayValue(value, key) {
+ const type = typeof value
+
+ if (type === 'object') {
+ return '(object)'
+ }
+
+ if (type === 'string') {
+ return "{value}"
+ }
+
+ if (type === 'number' || type === 'boolean') {
+ return {String(value)}
+ }
+
+ if (type === 'function') {
+ return (function)
+ }
+
+ return value
+}
+
+const PropValueList = styled.ul`
+ margin-block-start: 0;
+ margin-block-end: 0;
+ margin-left: -20px;
+`
+
+function getType(doc) {
+ switch (doc.name) {
+ case 'any':
+ return 'any'
+ case 'array':
+ return 'array'
+ case 'bool':
+ return 'boolean'
+ case 'func':
+ return 'function'
+ case 'number':
+ return 'number'
+ case 'node':
+ return 'node'
+ case 'object':
+ return 'object'
+ case 'string':
+ return 'string'
+ case 'symbol':
+ return 'symbol'
+ case 'element':
+ return 'element'
+ case 'elementType':
+ return 'element type'
+
+ case 'instanceOf':
+ return `instance of ${doc.args.name}`
+ case 'arrayOf':
+ return `array of ${getType(doc.args)}s`
+ case 'oneOf': {
+ const items = doc.args.map((item, idx) => getDisplayValue(item, idx))
+ return (
+ <>
+ One of:
+
+ {items.map((item, idx) => (
+ // eslint-disable-next-line react/no-array-index-key
+ {item}
+ ))}
+
+ >
+ )
+ }
+ case 'oneOfType': {
+ const items = doc.args.map(item => getType(item.doc))
+ return (
+ <>
+ One of type:
+
+ {items.map((item, idx) => (
+ // eslint-disable-next-line react/no-array-index-key
+ {item}
+ ))}
+
+ >
+ )
+ }
+ case 'objectOf': {
+ return `object with values of type ${getType(doc.args.doc)}`
+ }
+ default:
+ return '(unknown type)'
+ }
+}
+
+function SystemProps({name, systemProps, headingLevel}) {
+ const Heading = getHeadingElement(headingLevel)
+ return (
+ <>
+ System props
+
+ {name} components receive the following categories of system props. See our{' '}
+ System Props page for more information.
+
+
[
+ {s.systemPropsName} ,
+ {Object.keys(s.propTypes).join(', ')}
+ ])}
+ />
+ >
+ )
+}
+
+function OwnProps({props, defaults, headingLevel}) {
+ const Heading = getHeadingElement(headingLevel)
+ const propsToShow = Object.keys(props).filter(key => !props[key].doc.hidden)
+ if (propsToShow.length === 0) {
+ return (
+ <>
+ Component props
+ This component gets no additional component specific props.
+ >
+ )
+ }
+
+ return (
+ <>
+ Component props
+ [
+ `${prop}${props[prop].doc.isRequired ? '*' : ''}`,
+ getType(props[prop].doc),
+ getDefault(defaults, prop),
+ props[prop].doc.desc
+ ])}
+ />
+ >
+ )
+}
+
+export default ComponentProps
diff --git a/docs/components/Table.js b/docs/components/Table.js
new file mode 100644
index 00000000000..07438ce4960
--- /dev/null
+++ b/docs/components/Table.js
@@ -0,0 +1,32 @@
+/* eslint-disable react/no-array-index-key */
+import React from 'react'
+import DoctocatTable from '@primer/gatsby-theme-doctocat/src/components/table'
+
+function Table({columns, rows, ...rest}) {
+ return (
+
+
+
+ {columns.map((col, idx) => (
+
+ {col}
+
+ ))}
+
+
+
+ {rows.map((row, idx) => (
+
+ {row.map((val, iidx) => (
+
+ {val}
+
+ ))}
+
+ ))}
+
+
+ )
+}
+
+export default Table
diff --git a/docs/content/BorderBox.md b/docs/content/BorderBox.md
index 287115e210f..41352e17c24 100644
--- a/docs/content/BorderBox.md
+++ b/docs/content/BorderBox.md
@@ -11,7 +11,14 @@ BorderBox is a Box component with a border. When no `borderColor` is present, th
This is a BorderBox
```
-## System props
+## Props
+
+import {BorderBox} from "@primer/components"
+import ComponentProps from "../components/ComponentProps"
+
+
+
+
diff --git a/docs/content/Box.md b/docs/content/Box.md
index e101a340098..24674edf8a7 100644
--- a/docs/content/Box.md
+++ b/docs/content/Box.md
@@ -16,12 +16,9 @@ The Box component serves as a wrapper component for most layout related needs. U
```
-## System props
+## Props
-Box components get the `COMMON` and `LAYOUT` categories of system props. Read our [System Props](/system-props) doc page for a full list of available props.
+import {Box} from "@primer/components"
+import ComponentProps from "../components/ComponentProps"
-## Component props
-
-| Prop name | Type | Default | Description |
-| :- | :- | :-: | :- |
-| as | String | `div` | sets the HTML tag for the component|
+
diff --git a/docs/content/Link.md b/docs/content/Link.md
index 25b302705f9..dd947f53490 100644
--- a/docs/content/Link.md
+++ b/docs/content/Link.md
@@ -16,15 +16,9 @@ In special cases where you'd like a `` styled like a `Link`, use `
```
-## System props
+## Props
-Link components get `COMMON` and `TYPOGRAPHY` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+import {Link} from "@primer/components"
+import ComponentProps from "../components/ComponentProps"
-## Component props
-
-| Name | Type | Default | Description |
-| :-------- | :------ | :-----: | :------------------------------------------------ |
-| href | String | | URL to be used for the Link |
-| muted | Boolean | false | Uses light gray for Link color, and blue on hover |
-| underline | Boolean | false | Adds underline to the Link |
-| as | String | 'a' | Can be 'a', 'button', 'input', or 'summary' |
+
diff --git a/docs/content/SideNav.md b/docs/content/SideNav.md
index 61cc1994180..a627dc1a1fe 100644
--- a/docs/content/SideNav.md
+++ b/docs/content/SideNav.md
@@ -131,27 +131,13 @@ If using React Router, you can use the `as` prop to render the element as a `Nav
...
```
-## System props
+## SideNav props
-`SideNav` components get `COMMON`, `BORDER`, and `LAYOUT` system props. `SideNav.Link` components get `COMMON` and `TYPOGRAPHY` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+import {SideNav} from "@primer/components"
+import ComponentProps from "../components/ComponentProps"
-## Component props
+
-### SideNav
+## SideNav.Link props
-| Name | Type | Default | Description |
-| :- | :- | :-: | :- |
-| as | String | 'nav' | Sets the HTML tag for the component. |
-| bordered | Boolean | false | Renders the component with a border. |
-| variant | String | 'normal' | Set to `lightweight` to render [in a lightweight style](#lightweight-variant). |
-
-### SideNav.Link
-
-| Name | Type | Default | Description |
-| :- | :- | :-: | :- |
-| as | String | 'a' | Sets the HTML tag for the component. |
-| href | String | | URL to be used for the Link |
-| muted | Boolean | false | Uses light gray for Link color, and blue on hover |
-| selected | Boolean | false | Sets the link as selected, giving it a different style and setting the `aria-current` attribute. |
-| underline | Boolean | false | Adds underline to the Link |
-| variant | String | 'normal' | Set to `full` to render [a full variant](#full-variant), suitable for including icons and labels. |
+
diff --git a/src/BorderBox.js b/src/BorderBox.js
index 2cdde7b595d..6e4bc751b6a 100644
--- a/src/BorderBox.js
+++ b/src/BorderBox.js
@@ -1,5 +1,5 @@
import styled from 'styled-components'
-import PropTypes from 'prop-types'
+import PropTypes from './DocPropTypes'
import Box from './Box'
import theme from './theme'
import {BORDER} from './constants'
@@ -13,10 +13,18 @@ BorderBox.defaultProps = {
borderRadius: 2
}
-BorderBox.propTypes = {
- theme: PropTypes.object,
- ...Box.propTypes,
- ...BORDER.propTypes
-}
+BorderBox.propTypes = PropTypes.doc({
+ system: [BORDER],
+ inherited: [Box],
+ own: {
+ border: PropTypes.string.desc('Sets the border; use theme values or provide your own'),
+ borderColor: PropTypes.string.desc('Sets the border; use theme values or provide your own'),
+ borderRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).desc(
+ 'Sets the border radius, use theme values or provide your own'
+ ),
+ boxShadow: PropTypes.string.desc('Sets box shadow, use theme values or provide your own'),
+ theme: PropTypes.object.hidden
+ }
+})
export default BorderBox
diff --git a/src/Box.js b/src/Box.js
index 82b53baf5eb..89ae3b9d679 100644
--- a/src/Box.js
+++ b/src/Box.js
@@ -1,23 +1,20 @@
import styled from 'styled-components'
-import PropTypes from 'prop-types'
-import {space, color} from 'styled-system'
-import systemPropTypes from '@styled-system/prop-types'
-import {LAYOUT} from './constants'
+import PropTypes from './DocPropTypes'
+import {COMMON, LAYOUT} from './constants'
import theme from './theme'
const Box = styled.div`
${LAYOUT}
- ${space}
- ${color}
+ ${COMMON}
`
Box.defaultProps = {theme}
-Box.propTypes = {
- ...LAYOUT.propTypes,
- ...systemPropTypes.space,
- ...systemPropTypes.color,
- theme: PropTypes.object
-}
+Box.propTypes = PropTypes.doc({
+ system: [COMMON, LAYOUT],
+ own: {
+ theme: PropTypes.object.hidden
+ }
+})
export default Box
diff --git a/src/DocPropTypes/index.js b/src/DocPropTypes/index.js
new file mode 100644
index 00000000000..a6704e37af3
--- /dev/null
+++ b/src/DocPropTypes/index.js
@@ -0,0 +1,4 @@
+import {propTypes, wrapPrimitivePropType, wrapCallablePropType} from './propTypes'
+
+export default propTypes
+export {wrapPrimitivePropType, wrapCallablePropType}
diff --git a/src/DocPropTypes/propTypes.js b/src/DocPropTypes/propTypes.js
new file mode 100644
index 00000000000..60aa1f009cb
--- /dev/null
+++ b/src/DocPropTypes/propTypes.js
@@ -0,0 +1,145 @@
+import PropTypes from 'prop-types'
+
+function proxyPropTypes(target, wrapper, names) {
+ for (const name of names) {
+ Object.defineProperty(target, name, {
+ configurable: false,
+ enumerable: true,
+ get() {
+ return wrapper(PropTypes[name], name)
+ }
+ })
+ }
+}
+
+function invisibleProp(descriptor) {
+ return {
+ configurable: false,
+ enumerable: false,
+ ...descriptor
+ }
+}
+
+function addDocKeys(checker, isRequired, name, args = []) {
+ function newIsRequired(...args) {
+ return isRequired(...args)
+ }
+
+ Object.defineProperties(checker, {
+ doc: invisibleProp({
+ value: {
+ name,
+ hidden: false,
+ isRequired: false,
+ desc: '',
+ args
+ }
+ }),
+ desc: invisibleProp({
+ value: desc => {
+ checker.doc.desc = desc
+ return checker
+ }
+ }),
+ hidden: invisibleProp({
+ get() {
+ checker.doc.hidden = true
+ return checker
+ }
+ }),
+ isRequired: invisibleProp({
+ get() {
+ checker.doc.isRequired = true
+ return newIsRequired
+ }
+ })
+ })
+
+ Object.defineProperties(newIsRequired, {
+ doc: invisibleProp({
+ get() {
+ return checker.doc
+ }
+ }),
+ desc: invisibleProp({
+ value: desc => {
+ checker.doc.desc = desc
+ return newIsRequired
+ }
+ }),
+ hidden: invisibleProp({
+ get() {
+ checker.doc.hidden = true
+ return newIsRequired
+ }
+ })
+ })
+}
+
+export function wrapPrimitivePropType(propType, name) {
+ const checker = (...args) => propType(...args)
+ addDocKeys(checker, propType.isRequired, name)
+ return checker
+}
+
+export function wrapCallablePropType(propType, name) {
+ return function checkerCreator(args) {
+ const checker = propType(args)
+ addDocKeys(checker, checker.isRequired, name, args)
+ return checker
+ }
+}
+
+const propTypes = {}
+
+proxyPropTypes(propTypes, wrapPrimitivePropType, [
+ 'any',
+ 'array',
+ 'bool',
+ 'func',
+ 'number',
+ 'object',
+ 'string',
+ 'symbol',
+ 'node',
+ 'element',
+ 'elementType'
+])
+proxyPropTypes(propTypes, wrapCallablePropType, [
+ 'instanceOf',
+ 'arrayOf',
+ 'oneOf',
+ 'oneOfType',
+ 'objectOf',
+ 'shape',
+ 'exact'
+])
+
+function getPropTypesFromArray(ary, propTypes) {
+ return ary.reduce((acc, item) => {
+ Object.assign(acc, item[propTypes])
+ return acc
+ }, {})
+}
+
+propTypes.doc = function docPropTypes(spec) {
+ const system = spec.system || []
+ const inherited = spec.inherited || []
+ const own = spec.own || {}
+
+ const finalPropTypes = {
+ ...getPropTypesFromArray(system, 'propTypes'),
+ ...getPropTypesFromArray(inherited, 'propTypes'),
+ ...own
+ }
+
+ Object.defineProperty(finalPropTypes, '__doc_spec', {
+ configurable: false,
+ enumerable: false,
+ value: {system, inherited, own}
+ })
+
+ return finalPropTypes
+}
+
+export {propTypes}
diff --git a/src/Link.js b/src/Link.js
index e0915715f73..c7b68d6beab 100644
--- a/src/Link.js
+++ b/src/Link.js
@@ -1,4 +1,4 @@
-import PropTypes from 'prop-types'
+import PropTypes from './DocPropTypes'
import styled from 'styled-components'
import {system} from 'styled-system'
import {COMMON, TYPOGRAPHY, get} from './constants'
@@ -37,17 +37,20 @@ const Link = styled.a.attrs(props => ({
`
Link.defaultProps = {
- theme
+ muted: false,
+ theme,
+ underline: false
}
-Link.propTypes = {
- as: elementType,
- href: PropTypes.string,
- muted: PropTypes.bool,
- theme: PropTypes.object,
- underline: PropTypes.bool,
- ...TYPOGRAPHY.propTypes,
- ...COMMON.propTypes
-}
+Link.propTypes = PropTypes.doc({
+ system: [COMMON, TYPOGRAPHY],
+ own: {
+ as: elementType.desc("Can be 'a', 'button', 'input', or 'summary'"),
+ href: PropTypes.string.desc('URL to be used for the Link'),
+ muted: PropTypes.bool.desc('Uses light gray for Link color, and blue on hover'),
+ theme: PropTypes.object.hidden,
+ underline: PropTypes.bool.desc('Adds underline to the Link')
+ }
+})
export default Link
diff --git a/src/SideNav.js b/src/SideNav.js
index 517954d063f..e3eed6abd81 100644
--- a/src/SideNav.js
+++ b/src/SideNav.js
@@ -1,7 +1,7 @@
import React from 'react'
-import PropTypes from 'prop-types'
import styled, {css} from 'styled-components'
import classnames from 'classnames'
+import PropTypes from './DocPropTypes'
import {COMMON, get} from './constants'
import theme from './theme'
import elementType from './utils/elementType'
@@ -140,30 +140,43 @@ SideNav.Link = styled(Link).attrs(props => {
SideNav.defaultProps = {
theme,
+ bordered: false,
variant: 'normal'
}
-SideNav.propTypes = {
- as: elementType,
- bordered: PropTypes.bool,
- children: PropTypes.node,
- theme: PropTypes.object,
- variant: PropTypes.oneOf(['normal', 'lightweight']),
- ...BorderBox.propTypes,
- ...COMMON.propTypes
-}
+SideNav.propTypes = PropTypes.doc({
+ system: [COMMON],
+ inherited: [BorderBox],
+ own: {
+ as: elementType.desc('Sets the HTML tag for the element'),
+ bordered: PropTypes.bool.desc('Renders the component with a border'),
+ children: PropTypes.node.hidden,
+ theme: PropTypes.object.hidden,
+ variant: PropTypes.oneOf(['normal', 'lightweight']).desc(
+ 'Set to `lightweight` to render [in a lightweight style](#lightweight-variant)'
+ )
+ }
+})
SideNav.Link.defaultProps = {
theme,
+ selected: false,
variant: 'normal'
}
-SideNav.Link.propTypes = {
- as: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
- selected: PropTypes.bool,
- theme: PropTypes.object,
- variant: PropTypes.oneOf(['normal', 'full']),
- ...Link.propTypes
-}
+SideNav.Link.propTypes = PropTypes.doc({
+ system: [],
+ inherited: [Link],
+ own: {
+ as: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).desc('Sets the HTML tag for the element'),
+ selected: PropTypes.bool.desc(
+ 'Sets the link as selected, giving it a different style and setting the `aria-current` attribute'
+ ),
+ theme: PropTypes.object.hidden,
+ variant: PropTypes.oneOf(['normal', 'full']).desc(
+ 'Set to `full` to render a [full variant](#full-variant), suitable for including icons and labels'
+ )
+ }
+})
export default SideNav
diff --git a/src/constants.js b/src/constants.js
index 9f48f7745b2..9106b23829f 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -25,13 +25,16 @@ TYPOGRAPHY.propTypes = {
export const COMMON = compose(styledSystem.space, styledSystem.color, styledSystem.display)
COMMON.propTypes = {
...systemPropTypes.space,
- ...systemPropTypes.color
+ ...systemPropTypes.color,
+ ...systemPropTypes.display
}
+COMMON.systemPropsName = 'COMMON'
export const BORDER = compose(styledSystem.border, styledSystem.shadow)
BORDER.propTypes = {
...systemPropTypes.border,
...systemPropTypes.shadow
}
+BORDER.systemPropsName = 'BORDER'
// these are 1:1 with styled-system's API now,
// so you could consider dropping the abstraction
@@ -41,7 +44,12 @@ export const FLEX = styledSystem.flexbox
export const GRID = styledSystem.grid
TYPOGRAPHY.propTypes = systemPropTypes.typography
+TYPOGRAPHY.systemPropsName = 'TYPOGRAPHY'
LAYOUT.propTypes = systemPropTypes.layout
+LAYOUT.systemPropsName = 'LAYOUT'
POSITION.propTypes = systemPropTypes.position
+POSITION.systemPropsName = 'POSITION'
FLEX.propTypes = systemPropTypes.flexbox
+FLEX.systemPropsName = 'FLEX'
GRID.propTypes = systemPropTypes.grid
+GRID.systemPropsName = 'GRID'
diff --git a/src/utils/elementType.js b/src/utils/elementType.js
index a4e72c3581a..368741af2b3 100644
--- a/src/utils/elementType.js
+++ b/src/utils/elementType.js
@@ -1,14 +1,15 @@
+import {wrapPrimitivePropType} from '../DocPropTypes'
import {isValidElementType} from 'react-is'
// This function is a temporary workaround until we can get
// the official PropTypes.elementType working (https://git.io/fjMLX).
// PropTypes.elementType is currently `undefined` in the browser.
-function elementType(props, propName, componentName) {
+const elementType = wrapPrimitivePropType(function(props, propName, componentName) {
if (props[propName] && !isValidElementType(props[propName])) {
return new Error(
`Invalid prop '${propName}' supplied to '${componentName}': the prop is not a valid React component`
)
}
-}
+}, 'elementType')
export default elementType