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