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