1import { css } from '@emotion/react'; 2import { borderRadius, breakpoints, shadows, spacing, theme, typography } from '@expo/styleguide'; 3import type { ComponentProps, ComponentType } from 'react'; 4import { Fragment } from 'react'; 5import ReactMarkdown from 'react-markdown'; 6import remarkGfm from 'remark-gfm'; 7 8import { APIDataType } from './APIDataType'; 9 10import { HeadingType } from '~/common/headingManager'; 11import { Code as PrismCodeBlock } from '~/components/base/code'; 12import { 13 CommentData, 14 MethodDefinitionData, 15 MethodParamData, 16 MethodSignatureData, 17 PropData, 18 TypeDefinitionData, 19 TypePropertyDataFlags, 20} from '~/components/plugins/api/APIDataTypes'; 21import { APISectionPlatformTags } from '~/components/plugins/api/APISectionPlatformTags'; 22import { Callout } from '~/ui/components/Callout'; 23import { Cell, HeaderCell, Row, Table, TableHead } from '~/ui/components/Table'; 24import { tableWrapperStyle } from '~/ui/components/Table/Table'; 25import { Tag } from '~/ui/components/Tag'; 26import { 27 A, 28 BOLD, 29 CODE, 30 H4, 31 LI, 32 OL, 33 P, 34 RawH3, 35 RawH4, 36 UL, 37 createPermalinkedComponent, 38} from '~/ui/components/Text'; 39 40const isDev = process.env.NODE_ENV === 'development'; 41 42export enum TypeDocKind { 43 Namespace = 4, 44 Enum = 8, 45 Variable = 32, 46 Function = 64, 47 Class = 128, 48 Interface = 256, 49 Property = 1024, 50 Method = 2048, 51 Parameter = 32768, 52 Accessor = 262144, 53 TypeAlias = 4194304, 54} 55 56export type MDComponents = ComponentProps<typeof ReactMarkdown>['components']; 57 58const getInvalidLinkMessage = (href: string) => 59 `Using "../" when linking other packages in doc comments produce a broken link! Please use "./" instead. Problematic link:\n\t${href}`; 60 61export const mdComponents: MDComponents = { 62 blockquote: ({ children }) => <Callout>{children}</Callout>, 63 code: ({ children, className }) => 64 className ? ( 65 <PrismCodeBlock className={className}>{children}</PrismCodeBlock> 66 ) : ( 67 <CODE css={css({ display: 'inline' })}>{children}</CODE> 68 ), 69 h1: ({ children }) => <H4>{children}</H4>, 70 ul: ({ children }) => <UL css={STYLES_ELEMENT_SPACING}>{children}</UL>, 71 ol: ({ children }) => <OL css={STYLES_ELEMENT_SPACING}>{children}</OL>, 72 li: ({ children }) => <LI>{children}</LI>, 73 a: ({ href, children }) => { 74 if ( 75 href?.startsWith('../') && 76 !href?.startsWith('../..') && 77 !href?.startsWith('../react-native') 78 ) { 79 if (isDev) { 80 throw new Error(getInvalidLinkMessage(href)); 81 } else { 82 console.warn(getInvalidLinkMessage(href)); 83 } 84 } 85 return <A href={href}>{children}</A>; 86 }, 87 p: ({ children }) => (children ? <P css={STYLES_ELEMENT_SPACING}>{children}</P> : null), 88 strong: ({ children }) => <BOLD>{children}</BOLD>, 89 span: ({ children }) => (children ? <span>{children}</span> : null), 90 table: ({ children }) => <Table>{children}</Table>, 91 thead: ({ children }) => <TableHead>{children}</TableHead>, 92 tr: ({ children }) => <Row>{children}</Row>, 93 th: ({ children }) => <HeaderCell>{children}</HeaderCell>, 94 td: ({ children }) => <Cell>{children}</Cell>, 95}; 96 97export const mdInlineComponents: MDComponents = { 98 ...mdComponents, 99 p: ({ children }) => (children ? <span>{children}</span> : null), 100}; 101 102export const mdInlineComponentsNoValidation: MDComponents = { 103 ...mdInlineComponents, 104 a: ({ href, children }) => <A href={href}>{children}</A>, 105}; 106 107const nonLinkableTypes = [ 108 'ColorValue', 109 'Component', 110 'ComponentClass', 111 'PureComponent', 112 'E', 113 'EventSubscription', 114 'Listener', 115 'NativeSyntheticEvent', 116 'ParsedQs', 117 'ServiceActionResult', 118 'T', 119 'TaskOptions', 120 'Uint8Array', 121 // React & React Native 122 'React.FC', 123 'ForwardRefExoticComponent', 124 'StyleProp', 125 // Cross-package permissions management 126 'RequestPermissionMethod', 127 'GetPermissionMethod', 128 'Options', 129 'PermissionHookBehavior', 130]; 131 132/** 133 * List of type names that should not be visible in the docs. 134 */ 135const omittableTypes = [ 136 // Internal React type that adds `ref` prop to the component 137 'RefAttributes', 138]; 139 140/** 141 * Map of internal names/type names that should be replaced with something more developer-friendly. 142 */ 143const replaceableTypes: Partial<Record<string, string>> = { 144 ForwardRefExoticComponent: 'Component', 145}; 146 147const hardcodedTypeLinks: Record<string, string> = { 148 AVPlaybackSource: '/versions/latest/sdk/av/#avplaybacksource', 149 AVPlaybackStatus: '/versions/latest/sdk/av/#avplaybackstatus', 150 AVPlaybackStatusToSet: '/versions/latest/sdk/av/#avplaybackstatustoset', 151 Blob: 'https://developer.mozilla.org/en-US/docs/Web/API/Blob', 152 Date: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date', 153 DeviceSensor: '/versions/latest/sdk/sensors', 154 Element: 'https://www.typescriptlang.org/docs/handbook/jsx.html#function-component', 155 Error: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error', 156 ExpoConfig: 157 'https://github.com/expo/expo/blob/main/packages/%40expo/config-types/src/ExpoConfig.ts', 158 File: 'https://developer.mozilla.org/en-US/docs/Web/API/File', 159 FileList: 'https://developer.mozilla.org/en-US/docs/Web/API/FileList', 160 MessageEvent: 'https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent', 161 Omit: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys', 162 Pick: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys', 163 Partial: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype', 164 Promise: 165 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise', 166 View: '/versions/latest/react-native/view', 167 ViewProps: '/versions/latest/react-native/view#props', 168 ViewStyle: '/versions/latest/react-native/view-style-props', 169 WebGL2RenderingContext: 'https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext', 170 WebGLFramebuffer: 'https://developer.mozilla.org/en-US/docs/Web/API/WebGLFramebuffer', 171}; 172 173const renderWithLink = (name: string, type?: string) => { 174 const replacedName = replaceableTypes[name] ?? name; 175 176 if (name.includes('.')) return name; 177 178 return nonLinkableTypes.includes(replacedName) ? ( 179 replacedName + (type === 'array' ? '[]' : '') 180 ) : ( 181 <A 182 href={hardcodedTypeLinks[replacedName] || `#${replacedName.toLowerCase()}`} 183 key={`type-link-${replacedName}`}> 184 {replacedName} 185 {type === 'array' && '[]'} 186 </A> 187 ); 188}; 189 190const renderUnion = (types: TypeDefinitionData[]) => 191 types 192 .map(type => resolveTypeName(type)) 193 .map((valueToRender, index) => ( 194 <span key={`union-type-${index}`}> 195 {valueToRender} 196 {index + 1 !== types.length && ' | '} 197 </span> 198 )); 199 200export const resolveTypeName = ( 201 typeDefinition: TypeDefinitionData 202): string | JSX.Element | (string | JSX.Element)[] => { 203 if (!typeDefinition) { 204 return 'undefined'; 205 } 206 207 const { 208 elements, 209 elementType, 210 name, 211 type, 212 types, 213 typeArguments, 214 declaration, 215 value, 216 queryType, 217 operator, 218 objectType, 219 indexType, 220 } = typeDefinition; 221 222 try { 223 if (name) { 224 if (type === 'reference') { 225 if (typeArguments) { 226 if (name === 'Record' || name === 'React.ComponentProps') { 227 return ( 228 <> 229 {name}< 230 {typeArguments.map((type, index) => ( 231 <span key={`record-type-${index}`}> 232 {resolveTypeName(type)} 233 {index !== typeArguments.length - 1 ? ', ' : null} 234 </span> 235 ))} 236 > 237 </> 238 ); 239 } else { 240 return ( 241 <> 242 {renderWithLink(name)} 243 < 244 {typeArguments.map((type, index) => ( 245 <span key={`${name}-nested-type-${index}`}> 246 {resolveTypeName(type)} 247 {index !== typeArguments.length - 1 ? ', ' : null} 248 </span> 249 ))} 250 > 251 </> 252 ); 253 } 254 } else { 255 return renderWithLink(name); 256 } 257 } else { 258 return name; 259 } 260 } else if (elementType?.name) { 261 if (elementType.type === 'reference') { 262 return renderWithLink(elementType.name, type); 263 } else if (type === 'array') { 264 return elementType.name + '[]'; 265 } 266 return elementType.name + type; 267 } else if (elementType?.declaration) { 268 if (type === 'array') { 269 const { parameters, type: paramType } = elementType.declaration.indexSignature || {}; 270 if (parameters && paramType) { 271 return `{ [${listParams(parameters)}]: ${resolveTypeName(paramType)} }`; 272 } 273 } 274 return elementType.name + type; 275 } else if (type === 'union' && types?.length) { 276 return renderUnion(types); 277 } else if (elementType && elementType.type === 'union' && elementType?.types?.length) { 278 const unionTypes = elementType?.types || []; 279 return ( 280 <> 281 ({renderUnion(unionTypes)}){type === 'array' && '[]'} 282 </> 283 ); 284 } else if (declaration?.signatures) { 285 const baseSignature = declaration.signatures[0]; 286 if (baseSignature?.parameters?.length) { 287 return ( 288 <> 289 ( 290 {baseSignature.parameters?.map((param, index) => ( 291 <span key={`param-${index}-${param.name}`}> 292 {param.name}: {resolveTypeName(param.type)} 293 {index + 1 !== baseSignature.parameters?.length && ', '} 294 </span> 295 ))} 296 ) {'=>'} {resolveTypeName(baseSignature.type)} 297 </> 298 ); 299 } else { 300 return ( 301 <> 302 {'() =>'} {resolveTypeName(baseSignature.type)} 303 </> 304 ); 305 } 306 } else if (type === 'reflection' && declaration?.children) { 307 return ( 308 <> 309 {'{\n'} 310 {declaration?.children.map((child: PropData, i) => ( 311 <span key={`reflection-${name}-${i}`}> 312 {' '} 313 {child.name + ': '} 314 {resolveTypeName(child.type)} 315 {i + 1 !== declaration?.children?.length ? ', ' : null} 316 {'\n'} 317 </span> 318 ))} 319 {'}'} 320 </> 321 ); 322 } else if (type === 'tuple' && elements) { 323 return ( 324 <> 325 [ 326 {elements.map((elem, i) => ( 327 <span key={`tuple-${name}-${i}`}> 328 {resolveTypeName(elem)} 329 {i + 1 !== elements.length ? ', ' : null} 330 </span> 331 ))} 332 ] 333 </> 334 ); 335 } else if (type === 'query' && queryType) { 336 return queryType.name; 337 } else if (type === 'literal' && typeof value === 'boolean') { 338 return `${value}`; 339 } else if (type === 'literal' && (value || (typeof value === 'number' && value === 0))) { 340 return `'${value}'`; 341 } else if (type === 'intersection' && types) { 342 return types 343 .filter(({ name }) => !omittableTypes.includes(name ?? '')) 344 .map((value, index, array) => ( 345 <span key={`intersection-${name}-${index}`}> 346 {resolveTypeName(value)} 347 {index + 1 !== array.length && ' & '} 348 </span> 349 )); 350 } else if (type === 'indexedAccess') { 351 return `${objectType?.name}['${indexType?.value}']`; 352 } else if (type === 'typeOperator') { 353 return operator || 'undefined'; 354 } else if (type === 'intrinsic') { 355 return name || 'undefined'; 356 } else if (value === null) { 357 return 'null'; 358 } 359 return 'undefined'; 360 } catch (e) { 361 console.warn('Type resolve has failed!', e); 362 return 'undefined'; 363 } 364}; 365 366export const parseParamName = (name: string) => (name.startsWith('__') ? name.substr(2) : name); 367 368export const renderParamRow = ({ 369 comment, 370 name, 371 type, 372 flags, 373 defaultValue, 374}: MethodParamData): JSX.Element => { 375 const initValue = parseCommentContent(defaultValue || getTagData('default', comment)?.text); 376 return ( 377 <Row key={`param-${name}`}> 378 <Cell> 379 <BOLD>{parseParamName(name)}</BOLD> 380 {renderFlags(flags, initValue)} 381 </Cell> 382 <Cell> 383 <APIDataType typeDefinition={type} /> 384 </Cell> 385 <Cell> 386 <CommentTextBlock 387 comment={comment} 388 afterContent={renderDefaultValue(initValue)} 389 emptyCommentFallback="-" 390 /> 391 </Cell> 392 </Row> 393 ); 394}; 395 396export const ParamsTableHeadRow = () => ( 397 <TableHead> 398 <Row> 399 <HeaderCell>Name</HeaderCell> 400 <HeaderCell>Type</HeaderCell> 401 <HeaderCell>Description</HeaderCell> 402 </Row> 403 </TableHead> 404); 405 406export const renderParams = (parameters: MethodParamData[]) => ( 407 <Table> 408 <ParamsTableHeadRow /> 409 <tbody>{parameters?.map(renderParamRow)}</tbody> 410 </Table> 411); 412 413export const listParams = (parameters: MethodParamData[]) => 414 parameters ? parameters?.map(param => parseParamName(param.name)).join(', ') : ''; 415 416export const renderDefaultValue = (defaultValue?: string) => 417 defaultValue && defaultValue !== '...' ? ( 418 <div css={defaultValueContainerStyle}> 419 <BOLD>Default:</BOLD> <CODE>{defaultValue}</CODE> 420 </div> 421 ) : undefined; 422 423export const renderTypeOrSignatureType = ( 424 type?: TypeDefinitionData, 425 signatures?: MethodSignatureData[], 426 allowBlock: boolean = false 427) => { 428 if (signatures && signatures.length) { 429 return ( 430 <CODE key={`signature-type-${signatures[0].name}`}> 431 ( 432 {signatures?.map(({ parameters }) => 433 parameters?.map(param => ( 434 <span key={`signature-param-${param.name}`}> 435 {param.name} 436 {param.flags?.isOptional && '?'}: {resolveTypeName(param.type)} 437 </span> 438 )) 439 )} 440 ) =>{' '} 441 {type ? <CODE key={`signature-type-${type.name}`}>{resolveTypeName(type)}</CODE> : 'void'} 442 </CODE> 443 ); 444 } else if (type) { 445 if (allowBlock) { 446 return <APIDataType typeDefinition={type} />; 447 } 448 return <CODE key={`signature-type-${type.name}`}>{resolveTypeName(type)}</CODE>; 449 } 450 return undefined; 451}; 452 453export const renderFlags = (flags?: TypePropertyDataFlags, defaultValue?: string) => 454 (flags?.isOptional || defaultValue) && ( 455 <> 456 <br /> 457 <span css={STYLES_OPTIONAL}>(optional)</span> 458 </> 459 ); 460 461export const renderIndexSignature = (kind: TypeDocKind) => 462 kind === TypeDocKind.Parameter && ( 463 <> 464 <br /> 465 <A 466 css={STYLES_OPTIONAL} 467 href="https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures" 468 openInNewTab 469 isStyled> 470 (index signature) 471 </A> 472 </> 473 ); 474 475export type CommentTextBlockProps = { 476 comment?: CommentData; 477 components?: MDComponents; 478 withDash?: boolean; 479 beforeContent?: JSX.Element; 480 afterContent?: JSX.Element; 481 includePlatforms?: boolean; 482 emptyCommentFallback?: string; 483}; 484 485export const parseCommentContent = (content?: string): string => 486 content && content.length ? content.replace(/*/g, '*').replace(/\t/g, '') : ''; 487 488export const getCommentOrSignatureComment = ( 489 comment?: CommentData, 490 signatures?: MethodSignatureData[] 491) => comment || (signatures && signatures[0]?.comment); 492 493export const getTagData = (tagName: string, comment?: CommentData) => 494 getAllTagData(tagName, comment)?.[0]; 495 496export const getAllTagData = (tagName: string, comment?: CommentData) => 497 comment?.tags?.filter(tag => tag.tag === tagName); 498 499export const getTagNamesList = (comment?: CommentData) => 500 comment && [ 501 ...(getAllTagData('platform', comment)?.map(platformData => platformData.text) || []), 502 ...(getTagData('deprecated', comment) ? ['deprecated'] : []), 503 ...(getTagData('experimental', comment) ? ['experimental'] : []), 504 ]; 505 506export const getMethodName = ( 507 method: MethodDefinitionData, 508 apiName?: string, 509 name?: string, 510 parameters?: MethodParamData[] 511) => { 512 const isProperty = method.kind === TypeDocKind.Property && !parameters?.length; 513 const methodName = ((apiName && `${apiName}.`) ?? '') + (method.name || name); 514 if (!isProperty) { 515 return `${methodName}(${parameters ? listParams(parameters) : ''})`; 516 } 517 518 return methodName; 519}; 520 521export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); 522 523const PARAM_TAGS_REGEX = /@tag-\S*/g; 524 525const getParamTags = (shortText?: string) => { 526 if (!shortText || !shortText.includes('@tag-')) { 527 return undefined; 528 } 529 return Array.from(shortText.matchAll(PARAM_TAGS_REGEX), match => match[0]); 530}; 531 532export const CommentTextBlock = ({ 533 comment, 534 components = mdComponents, 535 withDash, 536 beforeContent, 537 afterContent, 538 includePlatforms = true, 539 emptyCommentFallback, 540}: CommentTextBlockProps) => { 541 const paramTags = getParamTags(comment?.shortText?.trim()); 542 543 const shortText = comment?.shortText?.trim().length ? ( 544 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 545 {parseCommentContent( 546 paramTags ? comment.shortText.replaceAll(PARAM_TAGS_REGEX, '') : comment.shortText 547 )} 548 </ReactMarkdown> 549 ) : null; 550 const text = comment?.text?.trim().length ? ( 551 <ReactMarkdown components={components} remarkPlugins={[remarkGfm]}> 552 {parseCommentContent(comment.text)} 553 </ReactMarkdown> 554 ) : null; 555 556 if (emptyCommentFallback && (!comment || (!shortText && !text))) { 557 return <>{emptyCommentFallback}</>; 558 } 559 560 const examples = getAllTagData('example', comment); 561 const exampleText = examples?.map((example, index) => ( 562 <Fragment key={'example-' + index}> 563 {components !== mdComponents ? ( 564 <div css={STYLES_EXAMPLE_IN_TABLE}> 565 <BOLD>Example</BOLD> 566 </div> 567 ) : ( 568 <div css={STYLES_NESTED_SECTION_HEADER}> 569 <RawH4>Example</RawH4> 570 </div> 571 )} 572 <ReactMarkdown components={components}>{example.text}</ReactMarkdown> 573 </Fragment> 574 )); 575 576 const see = getTagData('see', comment); 577 const seeText = see && ( 578 <Callout> 579 <BOLD>See: </BOLD> 580 <ReactMarkdown components={mdInlineComponents}>{see.text}</ReactMarkdown> 581 </Callout> 582 ); 583 584 const hasPlatforms = (getAllTagData('platform', comment)?.length || 0) > 0; 585 586 return ( 587 <> 588 {!withDash && includePlatforms && hasPlatforms && ( 589 <APISectionPlatformTags comment={comment} prefix="Only for:" /> 590 )} 591 {paramTags && ( 592 <> 593 <BOLD>Only for: </BOLD> 594 {paramTags.map(tag => ( 595 <Tag key={tag} name={tag.split('-')[1]} /> 596 ))} 597 </> 598 )} 599 {beforeContent} 600 {withDash && (shortText || text) && ' - '} 601 {withDash && includePlatforms && <APISectionPlatformTags comment={comment} />} 602 {shortText} 603 {text} 604 {afterContent} 605 {seeText} 606 {exampleText} 607 </> 608 ); 609}; 610 611export const getAPISectionHeader = (exposeInSidebar?: boolean) => 612 exposeInSidebar ? createPermalinkedComponent(RawH4, { baseNestingLevel: 2 }) : H4; 613 614const getMonospaceHeader = (element: ComponentType<any>) => { 615 const level = parseInt(element?.displayName?.replace(/\D/g, '') ?? '0', 10); 616 return createPermalinkedComponent(element, { 617 baseNestingLevel: level !== 0 ? level : undefined, 618 sidebarType: HeadingType.InlineCode, 619 }); 620}; 621 622export const H3Code = getMonospaceHeader(RawH3); 623export const H4Code = getMonospaceHeader(RawH4); 624 625export const getComponentName = (name?: string, children: PropData[] = []) => { 626 if (name && name !== 'default') return name; 627 const ctor = children.filter((child: PropData) => child.name === 'constructor')[0]; 628 return ctor?.signatures?.[0]?.type?.name ?? 'default'; 629}; 630 631export const STYLES_APIBOX = css({ 632 borderRadius: borderRadius.medium, 633 borderWidth: 1, 634 borderStyle: 'solid', 635 borderColor: theme.border.default, 636 padding: spacing[5], 637 boxShadow: shadows.micro, 638 marginBottom: spacing[6], 639 overflowX: 'hidden', 640 641 h3: { 642 marginBottom: spacing[2], 643 }, 644 645 'h3, h4': { 646 marginTop: 0, 647 }, 648 649 th: { 650 color: theme.text.secondary, 651 padding: `${spacing[3]}px ${spacing[4]}px`, 652 }, 653 654 li: { 655 marginBottom: 0, 656 }, 657 658 [`.css-${tableWrapperStyle.name}`]: { 659 boxShadow: 'none', 660 marginBottom: 0, 661 }, 662 663 [`@media screen and (max-width: ${breakpoints.medium + 124}px)`]: { 664 padding: `0 ${spacing[4]}px`, 665 }, 666}); 667 668export const STYLES_APIBOX_NESTED = css({ 669 boxShadow: 'none', 670 marginBottom: spacing[4], 671 padding: `${spacing[4]}px ${spacing[5]}px 0`, 672 673 h4: { 674 marginTop: 0, 675 }, 676}); 677 678export const STYLE_APIBOX_NO_SPACING = css({ marginBottom: -spacing[5] }); 679 680export const STYLES_NESTED_SECTION_HEADER = css({ 681 display: 'flex', 682 borderTop: `1px solid ${theme.border.default}`, 683 borderBottom: `1px solid ${theme.border.default}`, 684 margin: `${spacing[4]}px -${spacing[5]}px ${spacing[4]}px`, 685 padding: `${spacing[2.5]}px ${spacing[5]}px`, 686 backgroundColor: theme.background.secondary, 687 688 h4: { 689 ...typography.fontSizes[16], 690 fontFamily: typography.fontFaces.medium, 691 marginBottom: 0, 692 marginTop: 0, 693 color: theme.text.secondary, 694 }, 695}); 696 697export const STYLES_NOT_EXPOSED_HEADER = css({ 698 marginBottom: spacing[1], 699 display: 'inline-block', 700 701 code: { 702 marginBottom: 0, 703 }, 704}); 705 706export const STYLES_OPTIONAL = css({ 707 color: theme.text.secondary, 708 fontSize: '90%', 709 paddingTop: 22, 710}); 711 712export const STYLES_SECONDARY = css({ 713 color: theme.text.secondary, 714 fontSize: '90%', 715 fontWeight: 600, 716}); 717 718const defaultValueContainerStyle = css({ 719 marginTop: spacing[2], 720 marginBottom: spacing[2], 721 722 '&:last-child': { 723 marginBottom: 0, 724 }, 725}); 726 727const STYLES_EXAMPLE_IN_TABLE = css({ 728 margin: `${spacing[2]}px 0`, 729}); 730 731export const STYLES_ELEMENT_SPACING = css({ 732 marginBottom: spacing[4], 733}); 734