typography.stories.tsx 18 KB

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