typography.stories.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. import type {CSSProperties} from 'react';
  2. import {Fragment} from 'react';
  3. import styled from '@emotion/styled';
  4. import LetterSpacingGraphic from 'sentry-images/stories/typography/letter-spacing.svg';
  5. import LineHeightGraphic from 'sentry-images/stories/typography/line-height.svg';
  6. import WeightGraphic from 'sentry-images/stories/typography/weight.svg';
  7. import {CodeSnippet} from 'sentry/components/codeSnippet';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import Link from 'sentry/components/links/link';
  10. import Panel from 'sentry/components/panels/panel';
  11. import {PanelTable} from 'sentry/components/panels/panelTable';
  12. import {Flex} from 'sentry/components/profiling/flex';
  13. import {IconCheckmark, IconCircleFill, IconClose} from 'sentry/icons';
  14. import {space} from 'sentry/styles/space';
  15. import type {ColorOrAlias} from 'sentry/utils/theme';
  16. const FixedWidth = styled('div')`
  17. max-width: 800px;
  18. `;
  19. interface TypeScaleItem {
  20. fontSize: CSSProperties['fontSize'];
  21. fontWeight: CSSProperties['fontWeight'];
  22. letterSpacing: CSSProperties['letterSpacing'];
  23. lineHeight: CSSProperties['lineHeight'];
  24. name: string;
  25. }
  26. const TYPE_SCALE: TypeScaleItem[] = [
  27. {
  28. name: 'Heading 1',
  29. fontWeight: 600,
  30. fontSize: '2.25rem',
  31. lineHeight: 1.2,
  32. letterSpacing: '-0.02rem',
  33. },
  34. {
  35. name: 'Heading 2',
  36. fontWeight: 600,
  37. fontSize: '1.875rem',
  38. lineHeight: 1.2,
  39. letterSpacing: '-0.016em',
  40. },
  41. {
  42. name: 'Heading 3',
  43. fontWeight: 600,
  44. fontSize: '1.625rem',
  45. lineHeight: 1.2,
  46. letterSpacing: '-0.012em',
  47. },
  48. {
  49. name: 'Heading 4',
  50. fontWeight: 600,
  51. fontSize: '1.375rem',
  52. lineHeight: 1.2,
  53. letterSpacing: '-0.008em',
  54. },
  55. {
  56. name: 'Heading 5',
  57. fontWeight: 600,
  58. fontSize: '1.25rem',
  59. lineHeight: 1.2,
  60. letterSpacing: '-0.004em',
  61. },
  62. {
  63. name: 'Heading 6',
  64. fontWeight: 600,
  65. fontSize: '1.125rem',
  66. lineHeight: 1.2,
  67. letterSpacing: 'normal',
  68. },
  69. {
  70. name: 'Paragraph',
  71. fontWeight: 400,
  72. fontSize: '1rem',
  73. lineHeight: 1.4,
  74. letterSpacing: 'normal',
  75. },
  76. {
  77. name: 'Button/Label',
  78. fontWeight: 600,
  79. fontSize: '1rem',
  80. lineHeight: 1.2,
  81. letterSpacing: 'normal',
  82. },
  83. {
  84. name: 'Small',
  85. fontWeight: 400,
  86. fontSize: '0.875rem',
  87. lineHeight: 1.4,
  88. letterSpacing: '+0.01rem',
  89. },
  90. ];
  91. const InlineLinkExampleStyles = `styled('a')\`
  92. color: \${p => p.theme.blue300};
  93. text-decoration: underline;
  94. text-decoration-color: ${p => p.theme.blue100};
  95. cursor: pointer;
  96. &:hover {
  97. text-decoration-color: ${p => p.theme.blue200};
  98. }
  99. \`;
  100. `;
  101. const StandaloneLinkExampleStyles = `/* Link color is flexible, choose between Gray 500, 400, and 300. */
  102. styled('a')\`
  103. color: \${p => p.theme.gray500};
  104. text-decoration: none;
  105. cursor: pointer;
  106. &:hover {
  107. text-decoration: underline;
  108. text-decoration-color: \${p => p.theme.gray200};
  109. }
  110. \`;
  111. `;
  112. const UnorderedListExampleStyles = `/* First-level items */
  113. ul > li {
  114. list-style-type: disc;
  115. }
  116. /* Second-level items */
  117. ul > ul > li {
  118. list-style-type: circle;
  119. }
  120. `;
  121. const OrderedListExampleStyles = `/* First-level items */
  122. ul > li {
  123. list-style-type: decimal;
  124. }
  125. /* Second-level items */
  126. ul > ul > li {
  127. list-style-type: lower-alpha;
  128. }
  129. `;
  130. const TabularNumsExampleStyles = `/* Add this to numeric columns */
  131. font-variant-numeric: tabular-nums;
  132. `;
  133. const FontLigatureExampleStyles = `/* Add this to the root element */
  134. font-feature-settings: 'liga';
  135. `;
  136. const FontFractionExampleStyles = `/* Be careful: this changes the appearance of normal,
  137. non-fractional numbers, so only apply it to specific
  138. text elements with fractions inside. */
  139. font-feature-settings: 'frac';
  140. `;
  141. export default function TypographyStories() {
  142. return (
  143. <FixedWidth>
  144. <h3>Typography</h3>
  145. <p>
  146. We've built Sentry's type system around Rubik - a playful open-source typeface.
  147. For code and code-like elements, we use <code>Roboto Mono</code>.
  148. </p>
  149. <hr />
  150. <h4>Type scale</h4>
  151. <p>
  152. Type scales are hierarchical type systems consisting of style definitions for
  153. common elements, such as Heading 1, Heading 2, Paragraph, and Button/Label.
  154. </p>
  155. <p>
  156. Sentry's type scale is based on the Rubik typeface. The root font size is 16px
  157. (1rem = 16px).
  158. </p>
  159. <PanelTable headers={['Scale', 'Weight', 'Size', 'Line Height', 'Letter Spacing']}>
  160. {TYPE_SCALE.map(({name, ...props}) => {
  161. return (
  162. <Fragment key={name}>
  163. <div style={props}>{name}</div>
  164. <div>{props.fontWeight}</div>
  165. <div>{props.fontSize}</div>
  166. <div>{props.lineHeight}</div>
  167. <div>{props.letterSpacing}</div>
  168. </Fragment>
  169. );
  170. })}
  171. </PanelTable>
  172. <hr />
  173. <h4>Styling</h4>
  174. <p>
  175. The type scale above should cover a large majority of use cases. However, if an
  176. element requires a custom style outside of the type scale, make sure to follow the
  177. rules below.
  178. </p>
  179. <h5>Size</h5>
  180. <p>
  181. <strong>Use values from the type scale above.</strong> Be mindful of the type
  182. hierarchy. If the element has low importance, use a smaller size.
  183. </p>
  184. <p>
  185. <Flex gap={space(1)} align="flex-start">
  186. <PositiveLabel />
  187. Always define font sizes with the <code>rem</code> unit.
  188. </Flex>
  189. </p>
  190. <h5>Weight</h5>
  191. <p>
  192. <ExampleImg src={WeightGraphic} />
  193. </p>
  194. <ul>
  195. <li>
  196. <strong>Use Medium (600)</strong> for headings, button labels, and elements that
  197. need to stand out from the rest of the user interface, like table headers
  198. </li>
  199. <li>
  200. <strong>Use Regular (400)</strong> for all other elements
  201. </li>
  202. </ul>
  203. <h5>Line height</h5>
  204. <p>
  205. <ExampleImg src={LineHeightGraphic} />
  206. </p>
  207. <ul>
  208. <li>
  209. <strong>Use 1.4</strong> for body text (content that can wrap to multiple lines)
  210. </li>
  211. <li>
  212. <strong>Use 1.2</strong> for headings and short, single-line text like table
  213. headers and input fields
  214. </li>
  215. <li>
  216. <strong>Use 1</strong> for text labels with immediate bounding boxes, like
  217. buttons, pills, and badges
  218. </li>
  219. </ul>
  220. <h5>Letter spacing</h5>
  221. <p>
  222. <ExampleImg src={LetterSpacingGraphic} />
  223. </p>
  224. <ul>
  225. <li>
  226. <strong>Reduce letter spacing for headings.</strong> This makes them look more
  227. condensed, thereby reinforcing their high order in the type hierarchy. Refer to
  228. the type scale above for how much to reduce.
  229. </li>
  230. <li>
  231. <strong>
  232. Increase letter spacing (+0.02rem) in text elements that are smaller than
  233. 16px,
  234. </strong>
  235. with the exception of code and code-like elements. This makes them easier to
  236. read.
  237. </li>
  238. </ul>
  239. <hr />
  240. <h4>Code</h4>
  241. <p>
  242. Use Roboto Mono in Regular (400) for code and code-like elements, like search
  243. tokens.
  244. </p>
  245. <p>Set the line height based on the context:</p>
  246. <ul>
  247. <li>
  248. <strong>For multi-line code</strong>, use 1.6
  249. </li>
  250. <li>
  251. <strong>For single-line code elements</strong>, like search tokens, use the same
  252. line height as that of the text surrounding the token
  253. </li>
  254. </ul>
  255. <hr />
  256. <h4>External Links</h4>
  257. <p>
  258. External links lead users to pages outside the application. Examples include links
  259. to Sentry's blog/marketing pages, terms of service, third-party documentation,…
  260. </p>
  261. <p>
  262. The following styling rules apply to external links only. Internal links, on the
  263. other hand, can have more flexible styles, based on their behavior and context.
  264. </p>
  265. <h5>In a sentence</h5>
  266. <p>When a link appears inside a longer sentence…</p>
  267. <ExamplePanel>
  268. ... like this{' '}
  269. <FixedExternalLink onClick={() => {}}>little link</FixedExternalLink>.
  270. </ExamplePanel>
  271. <ul>
  272. <li>
  273. Use <ColorSwatch color="blue300" /> as the text color
  274. </li>
  275. <li>
  276. Add a solid underline in <ColorSwatch color="blue100" />
  277. </li>
  278. <li>
  279. Don't include any preceding articles (a, the, this, our) in the linked text, for
  280. example:
  281. <ul>
  282. <li>
  283. <Flex gap={space(1)} align="baseline">
  284. <PositiveLabel style={{alignSelf: 'flex-end'}} /> the{' '}
  285. <FixedExternalLink onClick={() => {}}>
  286. Church of the Flying Spaghetti Monster
  287. </FixedExternalLink>
  288. </Flex>
  289. </li>
  290. <li>
  291. <Flex gap={space(1)} align="baseline">
  292. <NegativeLabel style={{alignSelf: 'flex-end'}} />{' '}
  293. <FixedExternalLink onClick={() => {}}>
  294. the Church of the Flying Spaghetti Monster
  295. </FixedExternalLink>
  296. </Flex>
  297. </li>
  298. </ul>
  299. </li>
  300. <li>
  301. On hover:
  302. <ul>
  303. <li>
  304. Use a pointer cursor - <code>cursor: pointer</code>
  305. </li>
  306. <li>
  307. Change the underline color to <ColorSwatch color="blue200" />
  308. </li>
  309. </ul>
  310. </li>
  311. </ul>
  312. <p>
  313. <CodeSnippet filename="Styled Components" language="typescript">
  314. {InlineLinkExampleStyles}
  315. </CodeSnippet>
  316. </p>
  317. <h5>Standalone</h5>
  318. <p>
  319. When a link appears on its own and the user likely knows that it's a link given
  320. the context, like in a footer:
  321. </p>
  322. <ExamplePanel>
  323. <Flex column>
  324. <FooterLink to="">Privacy Policy</FooterLink>
  325. <FooterLink to="">Terms of Use</FooterLink>
  326. </Flex>
  327. </ExamplePanel>
  328. <ul>
  329. <li>
  330. Use <ColorSwatch color="gray500" />, <ColorSwatch color="gray400" />, or{' '}
  331. <ColorSwatch color="gray300" />, depending on the context
  332. </li>
  333. <li>Don't add any underline</li>
  334. <li>
  335. On hover:
  336. <ul>
  337. <li>
  338. Use a pointer cursor - <code>cursor: pointer</code>
  339. </li>
  340. <li>
  341. Add a solid underline in <ColorSwatch color="gray200" />
  342. </li>
  343. </ul>
  344. </li>
  345. </ul>
  346. <p>
  347. <CodeSnippet filename="Styled Components" language="typescript">
  348. {StandaloneLinkExampleStyles}
  349. </CodeSnippet>
  350. </p>
  351. <hr />
  352. <h4>Lists</h4>
  353. <h3>Unordered</h3>
  354. <p>Use filled and hollow circles as bullets points:</p>
  355. <ExamplePanel>
  356. <ul>
  357. <li>
  358. Camelus
  359. <ul>
  360. <li>Bactrian camel</li>
  361. <li>Dromedary</li>
  362. </ul>
  363. </li>
  364. <li>
  365. Lama
  366. <ul>
  367. <li>Llama</li>
  368. <li>Alpaca</li>
  369. </ul>
  370. </li>
  371. </ul>
  372. </ExamplePanel>
  373. <p>
  374. <CodeSnippet filename="CSS" language="css">
  375. {UnorderedListExampleStyles}
  376. </CodeSnippet>
  377. </p>
  378. <p>
  379. <Flex gap={space(1)} align="flex-start">
  380. <PositiveLabel />
  381. Don't add full stops (.) to the end of each item, unless the item contains
  382. multiple sentences.
  383. </Flex>
  384. </p>
  385. <p>
  386. <Flex gap={space(1)} align="flex-start">
  387. <PositiveLabel /> Avoid using custom symbols and icons as bullet characters, as
  388. they usually look out of place and distract from the main text content.
  389. </Flex>
  390. </p>
  391. <h5>Ordered</h5>
  392. <p>Use Arabic numerals and lowercase letters as counters:</p>
  393. <ExamplePanel>
  394. <ol>
  395. <li>
  396. Camelus
  397. <ol>
  398. <li>Bactrian camel</li>
  399. <li>Dromedary</li>
  400. </ol>
  401. </li>
  402. <li>
  403. Lama
  404. <ol>
  405. <li>Llama</li>
  406. <li>Alpaca</li>
  407. </ol>
  408. </li>
  409. </ol>
  410. </ExamplePanel>
  411. <p>
  412. <CodeSnippet filename="CSS" language="css">
  413. {OrderedListExampleStyles}
  414. </CodeSnippet>
  415. </p>
  416. <p>
  417. <Flex gap={space(1)} align="flex-start">
  418. <PositiveLabel />
  419. Avoid using custom symbols and icons as counters.
  420. </Flex>
  421. </p>
  422. <hr />
  423. <h4>OpenType features</h4>
  424. <p>
  425. Rubik supports a few useful{' '}
  426. <ExternalLink href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/OpenType_fonts_guide">
  427. OpenType font features
  428. </ExternalLink>
  429. . These features, or variants, are alternative characters that, when used in the
  430. right places, can help improve the reading experience.
  431. </p>
  432. <h5>Tabular figures</h5>
  433. <p>
  434. By default, Rubik uses proportional figures. This works well in most cases.
  435. However, for large tables with a lot of numbers, tabular figures would be a better
  436. choice, thanks to their consistent width and more legible design.
  437. </p>
  438. <PanelTable headers={['Proportional Figures', 'Tabular Figures']}>
  439. <div>999,999</div>
  440. <TabularNum>999,999</TabularNum>
  441. <div>111,111</div>
  442. <TabularNum>111,111</TabularNum>
  443. <div>9.99999</div>
  444. <TabularNum>9.99999</TabularNum>
  445. <div>1.11111</div>
  446. <TabularNum>1.11111</TabularNum>
  447. </PanelTable>
  448. <p>
  449. <CodeSnippet filename="CSS" language="css">
  450. {TabularNumsExampleStyles}
  451. </CodeSnippet>
  452. </p>
  453. <h5>Ligatures</h5>
  454. <p>
  455. Ligatures are special glyphs that replace two or more glyphs in order to better
  456. connect them. Common ligature replacements include ff, fi, fl, and ffi.
  457. </p>
  458. <SideBySideList>
  459. <li>
  460. <ExamplePanel fontSize="large">
  461. <FontNoLiga>ff, fi, fl</FontNoLiga>
  462. </ExamplePanel>
  463. <p>Without ligatures, the characters are all separate.</p>
  464. </li>
  465. <li>
  466. <ExamplePanel fontSize="large">
  467. <FontLiga>ff, fi, fl</FontLiga>
  468. </ExamplePanel>
  469. <p>With ligatures, the characters are connected into a single glyph.</p>
  470. </li>
  471. </SideBySideList>
  472. <p>
  473. <Flex gap={space(1)} align="flex-start">
  474. <PositiveLabel />
  475. Use ligatures across the whole user interface.
  476. </Flex>
  477. </p>
  478. <p>
  479. <CodeSnippet filename="CSS" language="css">
  480. {FontLigatureExampleStyles}
  481. </CodeSnippet>
  482. </p>
  483. <h5>Fractions</h5>
  484. <SideBySideList>
  485. <li>
  486. <ExamplePanel fontSize="large">1/12</ExamplePanel>
  487. <p>
  488. Rubik also contains special formatting for fractions. Without this formatting,
  489. numbers in fractions are just rendered as separate characters.
  490. </p>
  491. </li>
  492. <li>
  493. <ExamplePanel fontSize="large">
  494. <FontFractional>1/12</FontFractional>
  495. </ExamplePanel>
  496. <p>
  497. Fractional formatting shrinks the numbers and connects them with a diagonal
  498. slash, forming a proportional, condensed visual block.
  499. </p>
  500. </li>
  501. </SideBySideList>
  502. <p>
  503. <Flex gap={space(1)} align="flex-start">
  504. <PositiveLabel />
  505. Use fractional formatting whenever possible.
  506. </Flex>
  507. </p>
  508. <p>
  509. <CodeSnippet filename="CSS" language="css">
  510. {FontFractionExampleStyles}
  511. </CodeSnippet>
  512. </p>
  513. </FixedWidth>
  514. );
  515. }
  516. const FixedExternalLink = styled(ExternalLink)`
  517. color: ${p => p.theme.blue300};
  518. text-decoration: underline ${p => p.theme.blue100};
  519. :hover {
  520. color: ${p => p.theme.blue300};
  521. text-decoration: underline ${p => p.theme.blue200};
  522. }
  523. `;
  524. const FooterLink = styled(Link)`
  525. color: ${p => p.theme.gray300};
  526. :hover {
  527. color: ${p => p.theme.gray300};
  528. text-decoration: underline ${p => p.theme.gray200};
  529. }
  530. `;
  531. const SideBySideList = styled('ul')`
  532. /* Reset */
  533. list-style-type: none;
  534. margin: 0;
  535. padding: 0;
  536. & > li {
  537. margin: 0;
  538. }
  539. & > li > div {
  540. margin-bottom: 0;
  541. }
  542. /* Side-by-side display */
  543. display: grid;
  544. grid-template-columns: 1fr 1fr;
  545. gap: ${space(2)};
  546. `;
  547. const ColorSwatch = styled(
  548. ({
  549. color,
  550. className,
  551. style,
  552. }: {
  553. color: ColorOrAlias;
  554. className?: string;
  555. style?: CSSProperties;
  556. }) => (
  557. <span className={className} style={style}>
  558. <IconCircleFill color={color} />
  559. {color}
  560. </span>
  561. )
  562. )`
  563. display: inline-flex;
  564. gap: ${space(0.5)};
  565. align-items: center;
  566. border: 1px solid ${p => p.theme.border};
  567. border-radius: ${p => p.theme.borderRadius};
  568. padding: ${space(0.25)} ${space(0.5)};
  569. vertical-align: sub;
  570. `;
  571. const ExampleImg = styled('img')`
  572. border: 1px solid ${p => p.theme.border};
  573. border-radius: ${p => p.theme.borderRadius};
  574. `;
  575. const PositiveLabel = styled(
  576. ({className, style}: {className?: string; style?: CSSProperties}) => (
  577. <div className={className} style={style}>
  578. <IconCheckmark />
  579. DO
  580. </div>
  581. )
  582. )`
  583. color: ${p => p.theme.green400};
  584. align-items: center;
  585. display: flex;
  586. font-weight: bold;
  587. gap: ${space(0.5)};
  588. `;
  589. const NegativeLabel = styled(
  590. ({className, style}: {className?: string; style?: CSSProperties}) => (
  591. <div className={className} style={style}>
  592. <IconClose color="red400" />
  593. DON'T
  594. </div>
  595. )
  596. )`
  597. color: ${p => p.theme.red400};
  598. align-items: center;
  599. display: flex;
  600. font-weight: bold;
  601. gap: ${space(0.5)};
  602. `;
  603. const ExamplePanel = styled(Panel)<{fontSize?: 'large'}>`
  604. padding: ${space(2)};
  605. ${p =>
  606. p.fontSize === 'large'
  607. ? `
  608. font-weight: 600;
  609. font-size: 1.875rem;
  610. line-height: 1.2;
  611. letter-spacing: -0.016em;
  612. `
  613. : ''}
  614. `;
  615. const TabularNum = styled('div')`
  616. font-variant-numeric: tabular-nums;
  617. `;
  618. const FontLiga = styled('div')`
  619. /**
  620. * TODO: This should be applied to the root node of the side, why is that not the case?
  621. */
  622. font-feature-settings: 'liga';
  623. `;
  624. const FontNoLiga = styled('div')`
  625. /**
  626. * Using 'liga' is the default
  627. * We want to turn it off for the example.
  628. *
  629. * Don't copy+paste this!
  630. */
  631. font-feature-settings: 'liga' 0;
  632. `;
  633. const FontFractional = styled('div')`
  634. font-feature-settings: 'frac';
  635. `;