1 import React from 'react'
2 import {Heading} from '@primer/react'
3 import styled from 'styled-components'
4 import {LinkIcon} from '@primer/octicons-react'
5 import textContent from 'react-addons-text-content'
6 import {SCROLL_MARGIN_TOP} from '../constants'
7 import usePage from '../hooks/use-page'
8 import SiteLink, {LinkNoUnderline} from '../components/link'
9 import Code from './code'
10
11 import * as styles from './components.module.css'
12
13 import {clsx} from 'clsx'
14
15 export {Code}
16 export {default as Index} from './nav-hierarchy'
17
18 const required = (prop, name) => {
19 if (!prop) {
20 throw new Error(`${name} prop is required`)
21 }
22 return prop
23 }
24
25 export const Link = props => <SiteLink showUnderline {...props} />
26
27 const StyledHeading = styled(Heading)`
28 margin-top: 24px;
29 margin-bottom: 16px;
30 scroll-margin-top: ${SCROLL_MARGIN_TOP}px;
31 line-height: 1.25;
32
33 @media (hover: hover) {
34 & .octicon-link {
35 visibility: hidden;
36 }
37
38 &:hover .octicon-link,
39 &:focus-within .octicon-link {
40 visibility: visible;
41 }
42 }
43 `
44
45 const HeaderLink = ({autolink, children, ...props}) =>
46 autolink ? (
47 <LinkNoUnderline {...props} className={styles.LinkNoUnderline}>
48 {children}
49 <LinkIcon className={clsx('octicon-link', styles.Octicon)} />
50 </LinkNoUnderline>
51 ) : (
52 children
53 )
54
55 const Headings = {
56 Markdown: ({children, autolink = true, ...props}) => {
57 const childArray = React.Children.toArray(children)
58 const childLink =
59 React.Children.count(children) > 1 && childArray[0].type?.name === 'Link' ? childArray.shift() : null
60
61 const {slugger} = usePage()
62 const text = children ? textContent(children) : ''
63 const id = text ? slugger.slug(text) : ''
64 const linkProps = {
65 autolink,
66 'aria-label': `${text} permalink`.trim(),
67 ...(id ? {href: `#${id}`} : {}),
68 }
69
70 return (
71 <StyledHeading {...(autolink && id ? {id} : {})} {...props}>
72 {childLink ? (
73 <React.Fragment>
74 {childLink}
75 <HeaderLink {...linkProps}>
76 {childArray.map((child, index) => (
77 <React.Fragment key={index}>{child}</React.Fragment>
78 ))}
79 </HeaderLink>
80 </React.Fragment>
81 ) : (
82 <HeaderLink {...linkProps}>{children}</HeaderLink>
83 )}
84 </StyledHeading>
85 )
86 },
87 h1: styled(StyledHeading).attrs({as: 'h1'})`
88 padding-bottom: 8px;
89 font-size: 32px;
90 border-bottom: 1px solid var(--borderColor-default, #d1d9e0);
91 margin-top: 0;
92 `,
93 h2: styled(StyledHeading).attrs({as: 'h2'})`
94 padding-bottom: 8px;
95 font-size: 20px;
96 border-bottom: 1px solid var(--borderColor-default, #d1d9e0);
97 font-weight: 600;
98 `,
99 h3: styled(StyledHeading).attrs({as: 'h3'})`
100 font-size: 16px;
101 font-weight: 500;
102 `,
103 h4: styled(StyledHeading).attrs({as: 'h4'})`
104 font-size: 14px;
105 font-weight: 500;
106 `,
107 h5: styled(StyledHeading).attrs({as: 'h5'})`
108 font-size: 12px;
109 `,
110 h6: styled(StyledHeading).attrs({as: 'h6'})`
111 font-size: 12px;
112 color: var(--fgColor-muted);
113 `,
114 wrap(as) {
115 return props => <Headings.Markdown as={Headings[as]} {...props} />
116 },
117 }
118
119 export const H1 = Headings.wrap('h1')
120 export const H2 = Headings.wrap('h2')
121 export const H3 = Headings.wrap('h2')
122 export const H4 = Headings.wrap('h3')
123 export const H5 = Headings.wrap('h4')
124 export const H6 = Headings.wrap('h5')
125
126 export const Blockquote = styled.blockquote`
127 margin: 0 0 16px;
128 padding: 0 16px;
129 color: var(--fgColor-muted);
130 border-left: 0.25em solid var(--borderColor-default);
131
132 > :first-child {
133 margin-top: 0;
134 }
135
136 > :last-child {
137 margin-bottom: 0;
138 }
139 `
140
141 export const DescriptionList = styled.dl`
142 padding: 0;
143
144 dt {
145 padding: 0;
146 margin-top: 16px;
147 font-size: 1em;
148 font-style: italic;
149 font-weight: 600;
150 }
151
152 dd {
153 padding: 0 16px;
154 margin: 0 0 16px;
155 }
156 `
157
158 export const HorizontalRule = styled.hr`
159 height: 1px;
160 padding: 0;
161 margin: 24px 0;
162 background-color: var(--borderColor-muted, #d1d9e0);
163 border: 0;
164 `
165
166 export const UnorderedList = styled.ul`
167 padding-left: 2em;
168
169 ul,
170 ol {
171 margin-top: 0;
172 margin-bottom: 0;
173 }
174
175 li {
176 word-break: break-word;
177 }
178
179 li > p {
180 margin-top: 16px;
181 }
182
183 li + li {
184 margin-top: 4px;
185 }
186 `
187
188 export const OrderedList = UnorderedList.withComponent('ol')
189
190 export const Paragraph = styled.p`
191 margin: 0 0 16px;
192 word-break: break-word;
193 `
194
195 export const Table = styled.table`
196 display: block;
197 width: 100%;
198 margin: 0 0 16px;
199 overflow: auto;
200 border-collapse: separate;
201 border-spacing: 0px;
202
203 th {
204 font-weight: 600;
205 background-color: var(--bgColor-neutral-muted, #afb8c133);
206 }
207
208 th,
209 td {
210 padding: 8px 16px;
211 border-color: var(--borderColor-muted, #d0d7de);
212 border-style: solid;
213 border-width: 0;
214 border-left-width: 1px;
215 border-top-width: 1px;
216 }
217
218 td code,
219 th code {
220 white-space: nowrap;
221 }
222
223 td a {
224 color: var(--fgColor-default, #1f2328);
225 text-decoration: underline;
226 }
227
228 td a:hover {
229 text-decoration: none;
230 }
231
232 td code a,
233 td a code {
234 color: var(--fgColor-default, #1f2328);
235 text-decoration: underline;
236 font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);
237 background-color: var(--bgColor-neutral-muted, #afb8c133);
238 padding: 2px 6px;
239 border-radius: 6px;
240 }
241
242 td code a:hover,
243 td a code:hover {
244 text-decoration: none;
245 }
246
247 tr:last-child td {
248 border-bottom-width: 1px;
249 }
250
251 tr td:last-child,
252 tr th:last-child {
253 border-right-width: 1px;
254 }
255
256 thead th:first-child {
257 border-top-left-radius: 6px;
258 }
259
260 thead th:last-child {
261 border-top-right-radius: 6px;
262 }
263
264 tbody tr:last-child td:last-child {
265 border-bottom-right-radius: 6px;
266 }
267
268 tbody tr:last-child td:first-child {
269 border-bottom-left-radius: 6px;
270 }
271
272 img {
273 background-color: transparent;
274 vertical-align: middle;
275 }
276 `
277
278 const StyledNote = styled.div`
279 padding: 16px;
280 margin-bottom: 16px;
281 border-radius: 6px;
282 border-left: 6px solid;
283
284 & *:last-child {
285 margin-bottom: 0;
286 }
287
288 &[data-variant='info'] {
289 border-left-color: var(--borderColor-accent-muted, #54aeff);
290 background-color: var(--bgColor-accent-subtle, #ddf4ff);
291 }
292
293 &[data-variant='warning'] {
294 border-left-color: var(--borderColor-attention-muted, #d4a72c);
295 background-color: var(--bgColor-attention-subtle, #fff8c5);
296 }
297
298 &[data-variant='danger'] {
299 border-left-color: var(--borderColor-danger-muted, #ff8182);
300 background-color: var(--bgColor-danger-subtle, #ffebe9);
301 }
302 `
303
304 export const Note = ({variant = 'info', ...props}) => <StyledNote data-variant={variant} {...props} />
305
306 export const Prompt = props => <Code prompt={true} {...props} />
307
308 const RequiredImage = ({src, alt, ...props}) => <img src={required(src, 'src')} alt={required(alt, 'alt')} {...props} />
309
310 export const Image = styled(RequiredImage)`
311 max-width: 100%;
312 box-sizing: content-box;
313 `
314
315 export const Screenshot = styled(RequiredImage)`
316 margin-top: 16px;
317 margin-bottom: 16px;
318 max-width: min(100%, 525px);
319 max-height: 300px;
320 border: 1px solid var(--borderColor-default);
321 display: block;
322 `
323
324 export const YouTube = ({id}) => (
325 <iframe
326 title="YouTube video"
327 src={`https://www.youtube.com/embed/${id}`}
328 frameBorder="0"
329 allowFullScreen
330 className={styles.Box}
331 />
332 )
333
334 export const InlineCode = styled.code`
335 font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);
336 background-color: var(--bgColor-neutral-muted, #afb8c133);
337 padding: 2px 6px;
338 border-radius: 6px;
339 font-size: 85%;
340 `
341
342 export const Strikethrough = styled.span`
343 text-decoration: line-through;
344 `
345
346 const Thead = styled.thead``
347 const Tbody = styled.tbody``
348 const Tr = styled.tr``
349 const Th = styled.th``
350 const Td = styled.td``
351
352 export const DataTable = ({headers, rows, align}) => {
353 const getAlign = index => {
354 if (!align) return undefined
355 const a = align[index]
356 if (a === 'l') return 'left'
357 if (a === 'r') return 'right'
358 if (a === 'c') return 'center'
359 return a
360 }
361
362 return (
363 <Table>
364 {headers && (
365 <Thead>
366 <Tr>
367 {headers.map((header, i) => (
368 <Th key={i} style={{textAlign: getAlign(i)}}>
369 {header}
370 </Th>
371 ))}
372 </Tr>
373 </Thead>
374 )}
375 <Tbody>
376 {rows.map((row, rowIndex) => (
377 <Tr key={rowIndex}>
378 {row.map((cell, cellIndex) => (
379 <Td key={cellIndex} style={{textAlign: getAlign(cellIndex)}}>
380 {cell}
381 </Td>
382 ))}
383 </Tr>
384 ))}
385 </Tbody>
386 </Table>
387 )
388 }