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 a {
219 color: var(--fgColor-default, #1f2328);
220 text-decoration: underline;
221 }
222
223 td a:hover {
224 text-decoration: none;
225 }
226
227 td code a,
228 td a code {
229 color: var(--fgColor-default, #1f2328);
230 text-decoration: underline;
231 font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);
232 background-color: var(--bgColor-neutral-muted, #afb8c133);
233 padding: 2px 6px;
234 border-radius: 6px;
235 }
236
237 td code a:hover,
238 td a code:hover {
239 text-decoration: none;
240 }
241
242 tr:last-child td {
243 border-bottom-width: 1px;
244 }
245
246 tr td:last-child,
247 tr th:last-child {
248 border-right-width: 1px;
249 }
250
251 thead th:first-child {
252 border-top-left-radius: 6px;
253 }
254
255 thead th:last-child {
256 border-top-right-radius: 6px;
257 }
258
259 tbody tr:last-child td:last-child {
260 border-bottom-right-radius: 6px;
261 }
262
263 tbody tr:last-child td:first-child {
264 border-bottom-left-radius: 6px;
265 }
266
267 img {
268 background-color: transparent;
269 vertical-align: middle;
270 }
271 `
272
273 const StyledNote = styled.div`
274 padding: 16px;
275 margin-bottom: 16px;
276 border-radius: 6px;
277 border-left: 6px solid;
278
279 & *:last-child {
280 margin-bottom: 0;
281 }
282
283 &[data-variant='info'] {
284 border-left-color: var(--borderColor-accent-muted, #54aeff);
285 background-color: var(--bgColor-accent-subtle, #ddf4ff);
286 }
287
288 &[data-variant='warning'] {
289 border-left-color: var(--borderColor-attention-muted, #d4a72c);
290 background-color: var(--bgColor-attention-subtle, #fff8c5);
291 }
292
293 &[data-variant='danger'] {
294 border-left-color: var(--borderColor-danger-muted, #ff8182);
295 background-color: var(--bgColor-danger-subtle, #ffebe9);
296 }
297 `
298
299 export const Note = ({variant = 'info', ...props}) => <StyledNote data-variant={variant} {...props} />
300
301 export const Prompt = props => <Code prompt={true} {...props} />
302
303 const RequiredImage = ({src, alt, ...props}) => <img src={required(src, 'src')} alt={required(alt, 'alt')} {...props} />
304
305 export const Image = styled(RequiredImage)`
306 max-width: 100%;
307 box-sizing: content-box;
308 `
309
310 export const Screenshot = styled(RequiredImage)`
311 margin-top: 16px;
312 margin-bottom: 16px;
313 max-width: min(100%, 525px);
314 max-height: 300px;
315 border: 1px solid var(--borderColor-default);
316 display: block;
317 `
318
319 export const YouTube = ({id}) => (
320 <iframe
321 title="YouTube video"
322 src={`https://www.youtube.com/embed/${id}`}
323 frameBorder="0"
324 allowFullScreen
325 className={styles.Box}
326 />
327 )
328
329 export const InlineCode = styled.code`
330 font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);
331 background-color: var(--bgColor-neutral-muted, #afb8c133);
332 padding: 2px 6px;
333 border-radius: 6px;
334 font-size: 85%;
335 `
336
337 export const Strikethrough = styled.span`
338 text-decoration: line-through;
339 `
340
341 const Thead = styled.thead``
342 const Tbody = styled.tbody``
343 const Tr = styled.tr``
344 const Th = styled.th``
345 const Td = styled.td``
346
347 export const DataTable = ({headers, rows, align}) => {
348 const getAlign = index => {
349 if (!align) return undefined
350 const a = align[index]
351 if (a === 'l') return 'left'
352 if (a === 'r') return 'right'
353 if (a === 'c') return 'center'
354 return a
355 }
356
357 return (
358 <Table>
359 {headers && (
360 <Thead>
361 <Tr>
362 {headers.map((header, i) => (
363 <Th key={i} style={{textAlign: getAlign(i)}}>
364 {header}
365 </Th>
366 ))}
367 </Tr>
368 </Thead>
369 )}
370 <Tbody>
371 {rows.map((row, rowIndex) => (
372 <Tr key={rowIndex}>
373 {row.map((cell, cellIndex) => (
374 <Td key={cellIndex} style={{textAlign: getAlign(cellIndex)}}>
375 {cell}
376 </Td>
377 ))}
378 </Tr>
379 ))}
380 </Tbody>
381 </Table>
382 )
383 }