colors.stories.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import React, { Fragment } from 'react';
  2. import { staticColors, Hues, Shade } from './colors';
  3. import colorThemes, { ThemeCategory } from './colorThemes';
  4. import {
  5. generateColorVariables,
  6. generateThemeColorVariables,
  7. } from './colorVariables';
  8. const description = `
  9. Wagtail’s typographic styles are made available as separate design tokens, but in most scenarios it’s better to use one of the predefined text styles.
  10. `;
  11. interface PaletteProps {
  12. color: string;
  13. hues: Hues;
  14. }
  15. /**
  16. * Generates a contrast grid URL from our color palette.
  17. */
  18. const getContrastGridLink = () => {
  19. const url = 'https://contrast-grid.eightshapes.com/';
  20. const parameters =
  21. '?version=1.1.0&es-color-form__tile-size=compact&es-color-form__show-contrast=aaa&es-color-form__show-contrast=aa&es-color-form__show-contrast=aa18';
  22. const bg: string[] = [];
  23. const fg: string[] = [];
  24. Object.values(staticColors).forEach((hues: Hues) => {
  25. Object.values(hues).forEach((shade: Shade) => {
  26. const color = `${shade.hex}, ${shade.textUtility.replace('w-text-', '')}`;
  27. bg.push(color);
  28. if (!shade.usage.toLowerCase().includes('background only')) {
  29. fg.push(color);
  30. }
  31. });
  32. });
  33. return `${url}${parameters}&background-colors=${encodeURIComponent(
  34. bg.join('\r\n'),
  35. )}&foreground-colors=${encodeURIComponent(fg.join('\r\n'))}`;
  36. };
  37. const Palette = ({ color, hues }: PaletteProps) => (
  38. <div className="w-mb-4 w-mr-4 w-flex w-flex-row">
  39. {Object.entries(hues).map(([name, shade]) => (
  40. <div key={name}>
  41. <h3 className="w-h3">{`${color} ${name === 'DEFAULT' ? '' : name}`}</h3>
  42. <div
  43. className={`w-p-3 w-pr-0 w-flex w-flex-col w-w-52 w-h-52 ${
  44. shade.bgUtility
  45. } ${
  46. color === 'white' ? 'w-border w-border-solid w-border-grey-520' : ''
  47. } w-text-14 w-text-${shade.contrastText}`}
  48. >
  49. <code>{shade.textUtility}</code>
  50. <code>{shade.bgUtility}</code>
  51. <code>{shade.cssVariable}</code>
  52. <code>{shade.hsl}</code>
  53. <code>{shade.hex}</code>
  54. </div>
  55. <p className="mt-3 w-w-52">{shade.usage}</p>
  56. </div>
  57. ))}
  58. </div>
  59. );
  60. export default {
  61. title: 'Foundation / Colors',
  62. parameters: {
  63. docs: {
  64. extractComponentDescription: () => description,
  65. },
  66. },
  67. };
  68. export const ColorPalette = () => (
  69. <>
  70. <p>
  71. View <a href={getContrastGridLink()}>Contrast Grid</a>. Here is our full
  72. color palette, with contrasting text chosen for readability of this
  73. example only.
  74. </p>
  75. {Object.entries(staticColors).map(([color, hues]) => (
  76. <div key={color}>
  77. <h2 className="w-sr-only">{color}</h2>
  78. <Palette color={color} hues={hues} />
  79. </div>
  80. ))}
  81. </>
  82. );
  83. const TokenSwatch = ({ name, token }: { name: string; token: Token }) => (
  84. <div className="w-shadow w-border w-border-border-furniture w-rounded w-w-36 w-p-2.5">
  85. <div
  86. className={`w-w-12 w-h-10 w-rounded ${token.bgUtility}${
  87. token.value.includes('white') || token.value.includes('grey-600')
  88. ? ' w-border w-border-border-furniture'
  89. : ''
  90. }`}
  91. />
  92. <h4 className="w-label-3">{name}</h4>
  93. <p className="w-help-text">
  94. {token.value.replace('var(--w-color-', '').replace(')', '')}
  95. </p>
  96. </div>
  97. );
  98. const CategorySwatches = ({ category }: { category: ThemeCategory }) => (
  99. <div key={category.label}>
  100. <h3 className="w-h3">{category.label}</h3>
  101. <div className="w-grid w-grid-flow-col w-gap-2.5">
  102. {Object.entries(category.tokens).map(([name, token]) => (
  103. <TokenSwatch key={token} name={name} token={token} />
  104. ))}
  105. </div>
  106. </div>
  107. );
  108. export const ColorThemes = () => (
  109. <>
  110. <section className="w-bg-surface-page w-pt-6 w-mt-6 -w-mx-4 w-px-4">
  111. <h2 className="w-h2">Light</h2>
  112. {colorThemes.light.map((category: ThemeCategory) => (
  113. <CategorySwatches key={category.label} category={category} />
  114. ))}
  115. </section>
  116. <section className="w-bg-surface-page w-pt-6 w-mt-6 -w-mx-4 w-px-4 w-theme-dark">
  117. <h2 className="w-h2">Dark</h2>
  118. {colorThemes.dark.map((category: ThemeCategory) => (
  119. <CategorySwatches key={category.label} category={category} />
  120. ))}
  121. </section>
  122. </>
  123. );
  124. const rootVariablesMap = [
  125. ...Object.entries(generateColorVariables(staticColors)),
  126. ...Object.entries(generateThemeColorVariables(colorThemes.light)),
  127. ]
  128. .map(([cssVar, val]) => `${cssVar}: ${val};`)
  129. .join('');
  130. const darkVariablesMap = Object.entries(
  131. generateThemeColorVariables(colorThemes.dark),
  132. )
  133. .map(([cssVar, val]) => `${cssVar}: ${val};`)
  134. .join('');
  135. const secondaryHSL = staticColors.secondary.DEFAULT.hsl.match(
  136. /\d+(\.\d+)?/g,
  137. ) as string[];
  138. // Make sure this contains no empty lines, otherwise Sphinx docs will treat this as paragraphs.
  139. const liveEditorCustomisations = `:root {
  140. --w-color-primary: ${staticColors.primary.DEFAULT.hex};
  141. /* Any valid CSS format is supported. */
  142. --w-color-primary-200: ${staticColors.primary[200].hsl};
  143. /* Set each HSL component separately to change all hues at once. */
  144. --w-color-secondary-hue: ${secondaryHSL[0]};
  145. --w-color-secondary-saturation: ${secondaryHSL[1]}%;
  146. --w-color-secondary-lightness: ${secondaryHSL[2]}%;
  147. }`;
  148. // Story using inline styles only so it can be copy-pasted into the Wagtail documentation for color customisations.
  149. const demoStyles = `
  150. :root {${rootVariablesMap}}
  151. .w-theme-dark {${darkVariablesMap}}
  152. .wagtail-color-swatch {
  153. border-collapse: separate;
  154. border-spacing: 4px;
  155. }
  156. .wagtail-color-swatch td:first-child,
  157. .wagtail-color-swatch .w-theme-dark {
  158. height: 1.5rem;
  159. width: 1.5rem;
  160. border: 1px solid #333;
  161. forced-color-adjust: none;
  162. }
  163. `;
  164. const warningComment =
  165. '<!-- Auto-generated with Storybook. See https://github.com/wagtail/wagtail/blob/main/client/src/tokens/colors.stories.tsx. Copy this comment’s parent section to update the `custom_user_interface_colors` documentation. -->';
  166. const colorCustomisationsDemo = (
  167. <section>
  168. <div
  169. // eslint-disable-next-line react/no-danger
  170. dangerouslySetInnerHTML={{
  171. __html: warningComment,
  172. }}
  173. />
  174. <p>
  175. Make sure to test any customisations against our{' '}
  176. <a href={getContrastGridLink()}>Contrast Grid</a>. Try out your own
  177. customisations with this interactive style editor:
  178. </p>
  179. {/* Required styles are in a separate tag so they can’t be overridden, compressed to a single line for ease of copy-pasting. */}
  180. <style>{demoStyles.replace(/\s+/gm, ' ')}</style>
  181. <pre>
  182. {/* contentEditable style element so it can be edited directly in the browser. */}
  183. <style
  184. contentEditable
  185. suppressContentEditableWarning={true}
  186. style={{ display: 'block' }}
  187. >
  188. {liveEditorCustomisations}
  189. </style>
  190. </pre>
  191. <h3>Static colours</h3>
  192. <table className="wagtail-color-swatch">
  193. <thead>
  194. <tr>
  195. <th aria-label="Swatch" />
  196. <th>Variable</th>
  197. <th>Usage</th>
  198. </tr>
  199. </thead>
  200. <tbody>
  201. {Object.values(staticColors).map((hues) =>
  202. Object.entries(hues)
  203. // Show DEFAULT shades first, then in numerical order.
  204. .sort(([nameA], [nameB]) =>
  205. nameA === 'DEFAULT' ? -1 : Number(nameB) - Number(nameA),
  206. )
  207. .map(([name, shade]) => (
  208. <tr key={shade.hex}>
  209. <td style={{ backgroundColor: `var(${shade.cssVariable})` }} />
  210. <td>
  211. <code>{shade.cssVariable}</code>
  212. </td>
  213. <td>{shade.usage}</td>
  214. </tr>
  215. )),
  216. )}
  217. </tbody>
  218. </table>
  219. <h3>Light & dark theme colours</h3>
  220. <table className="wagtail-color-swatch">
  221. <thead>
  222. <tr>
  223. <th>Light</th>
  224. <th>Dark</th>
  225. <th>Variable</th>
  226. </tr>
  227. </thead>
  228. {colorThemes.light.map((category) => (
  229. <tbody key={category.label}>
  230. <tr>
  231. <th scope="rowgroup" colSpan={3}>
  232. {category.label}
  233. </th>
  234. </tr>
  235. {Object.values(category.tokens).map((token) => (
  236. <tr key={token.cssVariable}>
  237. <td style={{ backgroundColor: `var(${token.cssVariable})` }} />
  238. <td
  239. className="w-theme-dark"
  240. style={{ backgroundColor: `var(${token.cssVariable})` }}
  241. />
  242. <td>
  243. <code>{token.cssVariable}</code>
  244. </td>
  245. </tr>
  246. ))}
  247. </tbody>
  248. ))}
  249. </table>
  250. </section>
  251. );
  252. export const ColorCustomisations = () => (
  253. <>
  254. <p>
  255. Use this story to test customising colors. The section below is also
  256. copied in the Wagtail docs so implementers know which colors are
  257. customisable in a given release.
  258. </p>
  259. <hr />
  260. {colorCustomisationsDemo}
  261. </>
  262. );