typography.stories.tsx 18 KB

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