Bug 1850611 - Create a JSON file source of truth for our design tokens. r=reusable-components-reviewers,desktop-theme-reviewers,hjones,dao

* Add light-dark transformer for generating web CSS
* Use value object in design-tokens.json
* Add HCM media queries to built CSS
* Add MPL license and how to edit file header
* Strip '-default' from token names and values
* Refactor generated media query placement within file.
* generate multiple CSS files from a single JSON file.
* add the :host(.anonymous-content-host) selector to the built CSS
* Output tokens in pre-defined order
* Generate CSS layer declarations and relevant selectors
* Sort tokens by t-shirt size and state semantically not alphabetically
* Add remaining tokens to design-tokens.json
* Add design tokens JSON docs

---------

Co-authored-by: Jules Simplicio <jsimplicio@mozilla.com>
Co-authored-by: Hanna Jones <hjones@mozilla.com>
Co-authored-by: Mark Striemer <mstriemer@mozilla.com>
Co-authored-by: Tim Giles <tgiles@mozilla.com>

Differential Revision: https://phabricator.services.mozilla.com/D204108
This commit is contained in:
Mark Striemer 2024-03-26 16:56:06 +00:00
parent 99ff3ccc0d
commit 68b2a33f07
11 changed files with 2875 additions and 16 deletions

3
.gitignore vendored
View file

@ -341,6 +341,9 @@ browser/components/storybook/storybook-static/
browser/components/storybook/.storybook/chrome-map.js
browser/components/storybook/custom-elements.json
# Ignore design-system node_modules
toolkit/themes/shared/design-system/node_modules/
# Ignore jscodeshift installed by mach esmify on windows
tools/esmify/jscodeshift
tools/esmify/jscodeshift.cmd

View file

@ -333,6 +333,9 @@ tps_result\.json$
^browser/components/storybook/\.storybook/chrome-map\.js
^browser/components/storybook/custom-elements\.json
# Ignore design-system node_modules
toolkit/themes/shared/design-system/node_modules/
# Ignore jscodeshift installed by mach esmify on windows
^tools/esmify/jscodeshift
^tools/esmify/jscodeshift\.cmd

View file

@ -0,0 +1,56 @@
# JSON design tokens
## Background
The benefit of storing design tokens with a platform-agnostic format such as JSON is that it can be converted or translated into other languages or tools (e.g CSS, Swift, Kotlin, Figma).
## Quick start
`design-tokens.json` holds our source of truth for design tokens in `mozilla-central` under the [design-system](https://searchfox.org/mozilla-central/source/toolkit/themes/shared/design-system) folder in `toolkit/themes/shared`. The CSS design token files in that folder come from the JSON file. If you need to modify a design token file, you should be editing the JSON.
In order for us to be able to define design tokens in one place (the JSON file) and allow all platforms to consume design tokens in their specific format, we use a build system called [Style Dictionary](https://amzn.github.io/style-dictionary/#/).
Here's how to build design tokens for desktop:
```sh
$ ./mach npm run build --prefix=toolkit/themes/shared/design-system
```
If successful, you should see Style Dictionary building all of our tokens files within the `design-system` folder. Otherwise, Style Dictionary can also generate helpful errors to help you debug.
At the end, we're capable of transforming JSON notation into CSS:
```json
{
"color": {
"blue": {
"05": {
"value": "#deeafc"
},
"30": {
"value": "#73a7f3"
},
"50": {
"value": "#0060df"
},
"60": {
"value": "#0250bb"
},
"70": {
"value": "#054096"
},
"80": {
"value": "#003070"
}
},
},
}
```
```css
--color-blue-05: #deeafc;
--color-blue-30: #73a7f3;
--color-blue-50: #0060df;
--color-blue-60: #0250bb;
--color-blue-70: #054096;
--color-blue-80: #003070;
```
Neat!

View file

@ -0,0 +1,944 @@
{
"attention": {
"dot": {
"color": {
"value": {
"platform": {
"default": "AccentColor"
},
"brand": {
"light": "#2ac3a2",
"dark": "#54ffbd"
},
"prefersContrast": "AccentColor"
}
}
}
},
"background": {
"color": {
"box": {
"value": {
"light": "{color.white}",
"dark": "{color.gray.80}",
"prefersContrast": "{background.color.canvas}"
}
},
"canvas": {
"value": {
"prefersContrast": "Canvas",
"brand": {
"light": "{color.white}",
"dark": "{color.gray.90}"
},
"platform": {
"default": "Canvas"
}
}
},
"critical": {
"value": {
"light": "{color.red.05}",
"dark": "{color.red.80}",
"prefersContrast": "{background.color.canvas}"
}
},
"information": {
"value": {
"light": "{color.blue.05}",
"dark": "{color.blue.80}",
"prefersContrast": "{background.color.canvas}"
}
},
"success": {
"value": {
"light": "{color.green.05}",
"dark": "{color.green.80}",
"prefersContrast": "{background.color.canvas}"
}
},
"warning": {
"value": {
"light": "{color.yellow.05}",
"dark": "{color.yellow.80}",
"prefersContrast": "{background.color.canvas}"
}
}
}
},
"border": {
"color": {
"@base": {
"value": {
"prefersContrast": "{text.color.@base}"
}
},
"interactive": {
"@base": {
"value": {
"prefersContrast": "{text.color.@base}",
"forcedColors": "ButtonText",
"brand": {
"light": "{color.gray.60}",
"dark": "{color.gray.50}"
},
"platform": {
"default": "color-mix(in srgb, currentColor 15%, {color.gray.60})"
}
}
},
"hover": {
"value": {
"default": "{border.color.interactive.@base}",
"forcedColors": "SelectedItem"
}
},
"active": {
"value": {
"default": "{border.color.interactive.@base}",
"forcedColors": "ButtonText"
}
},
"disabled": {
"value": {
"default": "{border.color.interactive.@base}",
"forcedColors": "GrayText"
}
}
}
},
"radius": {
"circle": {
"value": "9999px"
},
"small": {
"value": "4px"
},
"medium": {
"value": "8px"
}
},
"width": {
"value": "1px"
}
},
"box": {
"shadow": {
"10": {
"value": "0 1px 4px {color.black.a10}"
}
}
},
"button": {
"background": {
"color": {
"@base": {
"comment": "TODO Bug 1821203 - Gray use needs to be consolidated",
"value": {
"forcedColors": "ButtonFace",
"brand": {
"default": "color-mix(in srgb, currentColor 7%, transparent)"
},
"platform": {
"default": "var(--button-bgcolor)"
}
}
},
"hover": {
"value": {
"forcedColors": "SelectedItemText",
"brand": {
"default": "color-mix(in srgb, currentColor 14%, transparent)"
},
"platform": {
"default": "var(--button-hover-bgcolor)"
}
}
},
"active": {
"value": {
"forcedColors": "SelectedItemText",
"brand": {
"default": "color-mix(in srgb, currentColor 21%, transparent)"
},
"platform": {
"default": "var(--button-active-bgcolor)"
}
}
},
"disabled": {
"value": {
"default": "{button.background.color.@base}",
"forcedColors": "ButtonFace"
}
},
"primary": {
"@base": {
"value": "{color.accent.primary.@base}"
},
"hover": {
"value": "{color.accent.primary.hover}"
},
"active": {
"value": "{color.accent.primary.active}"
},
"disabled": {
"value": {
"default": "{button.background.color.primary.@base}",
"forcedColors": "{button.text.color.disabled}"
}
}
},
"destructive": {
"@base": {
"value": {
"light": "{color.red.50}",
"dark": "{color.red.30}",
"forcedColors": "{button.background.color.primary.@base}"
}
},
"active": {
"value": {
"light": "{color.red.70}",
"dark": "{color.red.05}",
"forcedColors": "{button.background.color.primary.active}"
}
},
"disabled": {
"value": {
"default": "{button.background.color.destructive.@base}",
"forcedColors": "{button.background.color.primary.disabled}"
}
},
"hover": {
"value": {
"light": "{color.red.60}",
"dark": "{color.red.10}",
"forcedColors": "{button.background.color.primary.hover}"
}
}
},
"ghost": {
"@base": {
"value": {
"default": "transparent",
"prefersContrast": "{button.background.color.@base}",
"forcedColors": "{button.background.color.@base}"
}
},
"active": {
"value": "{button.background.color.active}"
},
"disabled": {
"value": {
"default": "{button.background.color.ghost.@base}",
"forcedColors": "{button.background.color.disabled}"
}
},
"hover": {
"value": "{button.background.color.hover}"
}
}
}
},
"border": {
"@base": {
"value": "{border.width} solid {button.border.color.@base}"
},
"color": {
"@base": {
"value": {
"default": "transparent",
"prefersContrast": "{button.text.color.@base}",
"forcedColors": "{border.color.interactive.@base}"
}
},
"active": {
"value": {
"default": "{button.border.color.@base}",
"forcedColors": "{border.color.interactive.active}"
}
},
"destructive": {
"@base": {
"value": {
"default": "transparent",
"forcedColors": "{button.border.color.primary.@base}"
}
},
"active": {
"value": {
"default": "{button.border.color.destructive.@base}",
"forcedColors": "{button.border.color.primary.active}"
}
},
"disabled": {
"value": {
"default": "{button.border.color.destructive.@base}",
"forcedColors": "{button.border.color.primary.disabled}"
}
},
"hover": {
"value": {
"default": "{button.border.color.destructive.@base}",
"forcedColors": "{button.border.color.primary.hover}"
}
}
},
"disabled": {
"value": {
"default": "{button.border.color.@base}",
"forcedColors": "{border.color.interactive.disabled}"
}
},
"ghost": {
"@base": {
"value": {
"default": "{button.border.color.@base}"
}
},
"active": {
"value": {
"default": "{button.border.color.active}"
}
},
"disabled": {
"value": {
"default": "{button.border.color.disabled}"
}
},
"hover": {
"value": {
"default": "{button.border.color.hover}"
}
}
},
"hover": {
"value": {
"default": "{button.border.color.@base}",
"forcedColors": "{border.color.interactive.hover}"
}
},
"primary": {
"@base": {
"value": {
"default": "transparent",
"forcedColors": "ButtonFace"
}
},
"active": {
"value": {
"default": "{button.border.color.primary.@base}",
"forcedColors": "ButtonText"
}
},
"disabled": {
"value": "{button.border.color.primary.@base}"
},
"hover": {
"value": {
"default": "{button.border.color.primary.@base}",
"forcedColors": "SelectedItemText"
}
}
}
},
"radius": {
"value": "{border.radius.small}"
}
},
"font": {
"size": {
"@base": {
"value": "{font.size.root}"
},
"small": {
"value": "{font.size.small}"
}
},
"weight": {
"value": "{font.weight.bold}"
}
},
"min": {
"height": {
"@base": {
"value": "{size.item.large}"
},
"small": {
"value": "{size.item.medium}"
}
}
},
"opacity": {
"disabled": {
"value": {
"default": 0.5,
"forcedColors": 1
}
}
},
"padding": {
"@base": {
"value": "{space.xsmall} {space.large}"
},
"icon": {
"value": 0
}
},
"size": {
"icon": {
"@base": {
"value": "{button.min.height.@base}"
},
"small": {
"value": "{button.min.height.small}"
}
}
},
"text": {
"color": {
"@base": {
"value": {
"forcedColors": "ButtonText",
"brand": {
"light": "{color.gray.100}",
"dark": "{color.gray.05}"
},
"platform": {
"default": "var(--button-color)"
}
}
},
"active": {
"value": {
"default": "{button.text.color.@base}",
"forcedColors": "SelectedItem"
}
},
"destructive": {
"@base": {
"value": {
"light": "{color.gray.05}",
"dark": "{color.gray.100}",
"forcedColors": "{button.text.color.primary.@base}"
}
},
"active": {
"value": {
"default": "{button.text.color.destructive.@base}",
"forcedColors": "{button.text.color.primary.active}"
}
},
"disabled": {
"value": {
"default": "{button.text.color.destructive.@base}",
"forcedColors": "{button.text.color.primary.disabled}"
}
},
"hover": {
"value": {
"default": "{button.text.color.destructive.@base}",
"forcedColors": "{button.text.color.primary.hover}"
}
}
},
"disabled": {
"value": {
"default": "{button.text.color.@base}",
"forcedColors": "GrayText"
}
},
"ghost": {
"@base": {
"value": {
"default": "{button.text.color.@base}"
}
},
"active": {
"value": {
"default": "{button.text.color.active}"
}
},
"disabled": {
"value": {
"default": "{button.text.color.disabled}"
}
},
"hover": {
"value": {
"default": "{button.text.color.hover}"
}
}
},
"hover": {
"value": {
"default": "{button.text.color.@base}",
"forcedColors": "SelectedItem"
}
},
"primary": {
"@base": {
"value": {
"forcedColors": "ButtonFace",
"brand": {
"light": "{color.gray.05}",
"dark": "{color.gray.100}"
},
"platform": {
"default": "var(--button-primary-color)"
}
}
},
"active": {
"value": "{button.text.color.primary.hover}"
},
"disabled": {
"value": "{button.text.color.primary.@base}"
},
"hover": {
"value": {
"default": "{button.text.color.primary.@base}",
"forcedColors": "SelectedItemText"
}
}
}
}
}
},
"checkbox": {
"margin": {
"inline": {
"value": "{space.small}"
}
},
"size": {
"comment": "TODO Bug 1876537: Make this em-based, probably?",
"value": "{size.item.small}"
}
},
"color": {
"black": {
"a10": {
"value": "rgba(0, 0, 0, 0.1)"
}
},
"blue": {
"05": {
"value": "#deeafc"
},
"30": {
"value": "#73a7f3"
},
"50": {
"value": "#0060df"
},
"60": {
"value": "#0250bb"
},
"70": {
"value": "#054096"
},
"80": {
"value": "#003070"
}
},
"cyan": {
"20": {
"value": "#aaf2ff"
},
"30": {
"value": "#80ebff"
},
"50": {
"value": "#00ddff"
}
},
"gray": {
"05": {
"value": "#fbfbfe"
},
"50": {
"value": "#bfbfc9"
},
"60": {
"value": "#8f8f9d"
},
"70": {
"value": "#5b5b66"
},
"80": {
"value": "#23222b"
},
"90": {
"value": "#1c1b22"
},
"100": {
"value": "#15141a"
}
},
"green": {
"05": {
"value": "#d8eedc"
},
"30": {
"value": "#4dbc87"
},
"50": {
"value": "#017a40"
},
"80": {
"value": "#004725"
}
},
"red": {
"05": {
"value": "#ffe8e8"
},
"10": {
"value": "#ffbdc5"
},
"20": {
"value": "#ff9aa2"
},
"30": {
"value": "#f37f98"
},
"50": {
"value": "#d7264c"
},
"60": {
"value": "#ac1e3d"
},
"70": {
"value": "#8a1831"
},
"80": {
"value": "#690f22"
}
},
"yellow": {
"05": {
"value": "#ffebcd"
},
"30": {
"value": "#e49c49"
},
"50": {
"value": "#cd411e"
},
"80": {
"value": "#5a3100"
}
},
"white": {
"value": "#ffffff"
},
"accent": {
"primary": {
"@base": {
"value": {
"forcedColors": "ButtonText",
"brand": {
"light": "{color.blue.50}",
"dark": "{color.cyan.50}"
},
"platform": {
"default": "var(--button-primary-bgcolor, AccentColor)"
}
}
},
"hover": {
"value": {
"forcedColors": "SelectedItem",
"brand": {
"light": "{color.blue.60}",
"dark": "{color.cyan.30}"
},
"platform": {
"default": "var(--button-primary-hover-bgcolor)"
}
}
},
"active": {
"value": {
"forcedColors": "{color.accent.primary.hover}",
"brand": {
"light": "{color.blue.70}",
"dark": "{color.cyan.20}"
},
"platform": {
"default": "var(--button-primary-active-bgcolor)"
}
}
}
}
}
},
"focus": {
"outline": {
"@base": {
"value": "{focus.outline.width} solid {focus.outline.color}"
},
"color": {
"value": "{color.accent.primary.@base}"
},
"inset": {
"value": "calc(-1 * {focus.outline.width})"
},
"offset": {
"value": "2px"
},
"width": {
"value": "2px"
}
}
},
"font": {
"size": {
"root": {
"value": {
"brand": {
"default": "15px"
},
"platform": {
"default": "unset"
}
}
},
"small": {
"value": {
"brand": {
"default": "0.867rem"
},
"platform": {
"default": "unset"
}
}
},
"large": {
"value": {
"brand": {
"default": "1.133rem"
},
"platform": {
"default": "unset"
}
}
},
"xlarge": {
"value": {
"brand": {
"default": "1.467rem"
},
"platform": {
"default": "unset"
}
}
},
"xxlarge": {
"value": {
"brand": {
"default": "1.6rem"
},
"platform": {
"default": "unset"
}
}
}
},
"weight": {
"@base": {
"value": "normal"
},
"bold": {
"value": 600
}
}
},
"icon": {
"color": {
"@base": {
"value": {
"light": "{color.gray.70}",
"dark": "{color.gray.05}",
"prefersContrast": "{text.color.@base}"
}
},
"information": {
"value": {
"light": "{color.blue.50}",
"dark": "{color.blue.30}",
"prefersContrast": "{icon.color.@base}"
}
},
"success": {
"value": {
"light": "{color.green.50}",
"dark": "{color.green.30}",
"prefersContrast": "{icon.color.@base}"
}
},
"warning": {
"value": {
"light": "{color.yellow.50}",
"dark": "{color.yellow.30}",
"prefersContrast": "{icon.color.@base}"
}
},
"critical": {
"value": {
"light": "{color.red.50}",
"dark": "{color.red.30}",
"prefersContrast": "{icon.color.@base}"
}
}
},
"size": {
"default": {
"value": "{size.item.small}"
}
}
},
"input": {
"text": {
"min": {
"height": {
"value": "{button.min.height.@base}"
}
}
}
},
"link": {
"color": {
"@base": {
"value": {
"prefersContrast": "LinkText",
"brand": {
"default": "{color.accent.primary.@base}"
},
"platform": {
"default": "LinkText"
}
}
},
"hover": {
"value": {
"prefersContrast": "LinkText",
"brand": {
"default": "{color.accent.primary.hover}"
},
"platform": {
"default": "LinkText"
}
}
},
"active": {
"value": {
"prefersContrast": "ActiveText",
"brand": {
"default": "{color.accent.primary.active}"
},
"platform": {
"default": "ActiveText"
}
}
},
"visited": {
"value": {
"prefersContrast": "{link.color.@base}",
"brand": {
"default": "{link.color.@base}"
},
"platform": {
"default": "{link.color.@base}"
}
}
}
},
"focus": {
"outline": {
"offset": {
"comment": "Not using --force-outline-offset for links because that's intended for\nelements with a background, and we only want a slight offset here while\nnot overlapping adjacent text",
"value": "1px"
}
}
}
},
"outline": {
"color": {
"error": {
"value": {
"light": "{color.red.50}",
"dark": "{color.red.20}",
"prefersContrast": "{border.color.@base}"
}
}
}
},
"size": {
"item": {
"small": {
"value": "16px"
},
"medium": {
"value": "28px"
},
"large": {
"value": "32px"
}
}
},
"space": {
"xxsmall": {
"value": "calc(0.5 * {space.xsmall})"
},
"xsmall": {
"value": "0.267rem"
},
"small": {
"value": "calc(2 * {space.xsmall})"
},
"medium": {
"value": "calc(3 * {space.xsmall})"
},
"large": {
"value": "calc(4 * {space.xsmall})"
},
"xlarge": {
"value": "calc(6 * {space.xsmall})"
},
"xxlarge": {
"value": "calc(8 * {space.xsmall})"
}
},
"text": {
"color": {
"@base": {
"value": {
"prefersContrast": "CanvasText",
"brand": {
"light": "{color.gray.100}",
"dark": "{color.gray.05}"
},
"platform": {
"default": "currentColor"
}
}
},
"deemphasized": {
"value": {
"default": "color-mix(in srgb, currentColor 69%, transparent)",
"prefersContrast": "inherit"
}
},
"error": {
"value": {
"light": "{color.red.50}",
"dark": "{color.red.20}",
"prefersContrast": "inherit"
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
{
"name": "design-system",
"version": "1.0.0",
"description": "Package file for node modules used to create our CSS tokens from a JSON source of truth.",
"license": "MPL-2.0",
"scripts": {
"build": "(npm ls || npm ci) && style-dictionary build --config ./tokens-config.js"
},
"devDependencies": {
"style-dictionary": "^3.9.2"
}
}

View file

@ -2,6 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* DO NOT EDIT this file directly, instead modify design-tokens.json
* and run `npm run build` to see your changes. */
@import url("chrome://global/skin/design-system/tokens-shared.css");
@layer tokens-foundation {
@ -17,8 +20,7 @@
--border-color-interactive: light-dark(var(--color-gray-60), var(--color-gray-50));
/** Button **/
/* TODO Bug 1821203 - Gray use needs to be consolidated */
--button-background-color: color-mix(in srgb, currentColor 7%, transparent);
--button-background-color: color-mix(in srgb, currentColor 7%, transparent); /* TODO Bug 1821203 - Gray use needs to be consolidated */
--button-background-color-hover: color-mix(in srgb, currentColor 14%, transparent);
--button-background-color-active: color-mix(in srgb, currentColor 21%, transparent);
--button-text-color: light-dark(var(--color-gray-100), var(--color-gray-05));
@ -30,11 +32,11 @@
--color-accent-primary-active: light-dark(var(--color-blue-70), var(--color-cyan-20));
/** Font Size **/
--font-size-root: 15px; /* Set at the `:root`. Do not use */
--font-size-small: 0.867rem; /* 13px */
--font-size-large: 1.133rem; /* 17px */
--font-size-xlarge: 1.467rem; /* 22px */
--font-size-xxlarge: 1.6rem; /* 24px */
--font-size-root: 15px;
--font-size-small: 0.867rem;
--font-size-large: 1.133rem;
--font-size-xlarge: 1.467rem;
--font-size-xxlarge: 1.6rem;
/** Link **/
--link-color: var(--color-accent-primary);

View file

@ -0,0 +1,428 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-env node */
const StyleDictionary = require("style-dictionary");
const { createPropertyFormatter } = StyleDictionary.formatHelpers;
const TOKEN_SECTIONS = {
"Attention Dot": "attention-dot",
"Background Color": "background-color",
Border: "border",
"Box Shadow": "box-shadow",
Button: "button",
Checkbox: "checkbox",
Color: ["brand-color", "color", "platform-color"],
"Focus Outline": "focus-outline",
"Font Size": "font-size",
"Font Weight": "font-weight",
Icon: "icon",
"Input - Text": "input-text",
Link: "link",
"Outline Color": "outline-color",
Size: "size",
Space: "space",
Text: "text",
Unspecified: "",
};
const TSHIRT_ORDER = [
"circle",
"xxxsmall",
"xxsmall",
"xsmall",
"small",
"medium",
"large",
"xlarge",
"xxlarge",
"xxxlarge",
];
const STATE_ORDER = [
"base",
"default",
"root",
"hover",
"active",
"focus",
"disabled",
];
/**
* Adds the Mozilla Public License header in one comment and
* how to make changes in the generated output files via the
* design-tokens.json file in another comment. Also imports
* tokens-shared.css when applicable.
*
* @param {string} surface
* Desktop surface, either "brand" or "platform". Determines
* whether or not we need to import tokens-shared.css.
* @returns {string} Formatted comment header string
*/
let customFileHeader = surface => {
let licenseString = [
"/* This Source Code Form is subject to the terms of the Mozilla Public",
" * License, v. 2.0. If a copy of the MPL was not distributed with this",
" * file, You can obtain one at http://mozilla.org/MPL/2.0/. */",
].join("\n");
let commentString = [
"/* DO NOT EDIT this file directly, instead modify design-tokens.json",
" * and run `npm run build` to see your changes. */",
].join("\n");
let cssImport = surface
? `@import url("chrome://global/skin/design-system/tokens-shared.css");\n\n`
: "";
let layerString = !surface
? `@layer tokens-foundation, tokens-prefers-contrast, tokens-forced-colors;\n\n`
: "";
return [
licenseString + "\n\n" + commentString + "\n\n" + cssImport + layerString,
];
};
const NEST_MEDIA_QUERIES_COMMENT = `/* Bug 1879900: Can't nest media queries inside of :host, :root selector
until Bug 1879349 lands */`;
const MEDIA_QUERY_PROPERTY_MAP = {
"forced-colors": "forcedColors",
"prefers-contrast": "prefersContrast",
};
function formatBaseTokenNames(str) {
return str.replaceAll(/(?<tokenName>\w+)-base(?=\b)/g, "$<tokenName>");
}
/**
* Creates a surface-specific formatter. The formatter is used to build
* our different CSS files, including "prefers-contrast" and "forced-colors"
* media queries. See more at
* https://amzn.github.io/style-dictionary/#/formats?id=formatter
*
* @param {string} surface
* Which desktop area we are generating CSS for.
* Either "brand" (i.e. in-content) or "platform" (i.e. chrome).
* @returns {Function} - Formatter function that returns a CSS string.
*/
const createDesktopFormat = surface => args => {
return formatBaseTokenNames(
customFileHeader(surface) +
formatTokens({
surface,
args,
}) +
formatTokens({
mediaQuery: "prefers-contrast",
surface,
args,
}) +
formatTokens({
mediaQuery: "forced-colors",
surface,
args,
})
);
};
/**
* Formats a subset of tokens into CSS. Wraps token CSS in a media query when
* applicable.
*
* @param {object} tokenArgs
* @param {string} [tokenArgs.mediaQuery]
* Media query formatted CSS should be wrapped in. This is used
* to determine what property we are parsing from the token values.
* @param {string} [tokenArgs.surface]
* Specifies a desktop surface, either "brand" or "platform".
* @param {object} tokenArgs.args
* Formatter arguments provided by style-dictionary. See more at
* https://amzn.github.io/style-dictionary/#/formats?id=formatter
* @returns {string} Tokens formatted into a CSS string.
*/
function formatTokens({ mediaQuery, surface, args }) {
let prop = MEDIA_QUERY_PROPERTY_MAP[mediaQuery] ?? "default";
let dictionary = Object.assign({}, args.dictionary);
let tokens = [];
dictionary.allTokens.forEach(token => {
let originalVal = getOriginalTokenValue(token, prop, surface);
if (originalVal != undefined) {
let formattedToken = transformTokenValue(token, originalVal, dictionary);
tokens.push(formattedToken);
}
});
if (!tokens.length) {
return "";
}
dictionary.allTokens = dictionary.allProperties = tokens;
let formattedVars = formatVariables({
format: "css",
dictionary,
outputReferences: args.options.outputReferences,
formatting: {
indentation: mediaQuery ? " " : " ",
},
});
let layer = `tokens-${mediaQuery ?? "foundation"}`;
// Weird spacing below is unfortunately necessary for formatting the built CSS.
if (mediaQuery) {
return `
${NEST_MEDIA_QUERIES_COMMENT}
@layer ${layer} {
@media (${mediaQuery}) {
:root,
:host(.anonymous-content-host) {
${formattedVars}
}
}
}
`;
}
return `@layer ${layer} {
:root,
:host(.anonymous-content-host) {
${formattedVars}
}
}
`;
}
/**
* Finds the original value of a token for a given media query and surface.
*
* @param {object} token - Token object parsed by style-dictionary.
* @param {string} prop - Name of the property we're querying for.
* @param {string} surface
* The desktop surface we're generating CSS for, either "brand" or "platform".
* @returns {string} The original token value based on our parameters.
*/
function getOriginalTokenValue(token, prop, surface) {
if (surface) {
return token.original.value[surface]?.[prop];
} else if (prop == "default" && typeof token.original.value != "object") {
return token.original.value;
}
return token.original.value?.[prop];
}
/**
* Updates a token's value to the relevant original value after resolving
* variable references.
*
* @param {object} token - Token object parsed from JSON by style-dictionary.
* @param {string} originalVal
* Original value of the token for the combination of surface and media query.
* @param {object} dictionary
* Object of transformed tokens and helper fns provided by style-dictionary.
* @returns {object} Token object with an updated value.
*/
function transformTokenValue(token, originalVal, dictionary) {
let value = originalVal;
if (dictionary.usesReference(value)) {
dictionary.getReferences(value).forEach(ref => {
value = value.replace(`{${ref.path.join(".")}}`, `var(--${ref.name})`);
});
}
return { ...token, value };
}
/**
* Creates a light-dark transform that works for a given surface. Registers
* the transform with style-dictionary and returns the transform's name.
*
* @param {string} surface
* The desktop surface we're generating CSS for, either "brand", "platform",
* or "shared".
* @returns {string} Name of the transform that was registered.
*/
const createLightDarkTransform = surface => {
let name = `lightDarkTransform/${surface}`;
// Matcher function for determining if a token's value needs to undergo
// a light-dark transform.
let matcher = token => {
if (surface != "shared") {
return (
token.original.value[surface]?.light &&
token.original.value[surface]?.dark
);
}
return token.original.value.light && token.original.value.dark;
};
// Function that uses the token's original value to create a new "default"
// light-dark value and updates the original value object.
let transformer = token => {
if (surface != "shared") {
let lightDarkVal = `light-dark(${token.original.value[surface].light}, ${token.original.value[surface].dark})`;
token.original.value[surface].default = lightDarkVal;
return token.value;
}
let value = `light-dark(${token.original.value.light}, ${token.original.value.dark})`;
token.original.value.default = value;
return value;
};
StyleDictionary.registerTransform({
type: "value",
transitive: true,
name,
matcher,
transformer,
});
return name;
};
/**
* Format the tokens dictionary to a string. This mostly defers to
* StyleDictionary.createPropertyFormatter but first it sorts the tokens based
* on the groupings in TOKEN_SECTIONS and adds comment headers to CSS output.
*
* @param {object} options
* Options for tokens to format.
* @param {string} options.format
* The format to output. Supported: "css"
* @param {object} options.dictionary
* The tokens dictionary.
* @param {string} options.outputReferences
* Whether to output variable references.
* @param {object} options.formatting
* The formatting settings to be passed to createPropertyFormatter.
* @returns {string} The formatted tokens.
*/
function formatVariables({ format, dictionary, outputReferences, formatting }) {
let lastSection = [];
let propertyFormatter = createPropertyFormatter({
outputReferences,
dictionary,
format,
formatting,
});
let outputParts = [];
let remainingTokens = [...dictionary.allTokens];
let isFirst = true;
function tokenParts(name) {
let lastDash = name.lastIndexOf("-");
let suffix = name.substring(lastDash + 1);
if (TSHIRT_ORDER.includes(suffix) || STATE_ORDER.includes(suffix)) {
return [name.substring(0, lastDash), suffix];
}
return [name, ""];
}
for (let [label, selector] of Object.entries(TOKEN_SECTIONS)) {
let sectionMatchers = Array.isArray(selector) ? selector : [selector];
let sectionParts = [];
remainingTokens = remainingTokens.filter(token => {
if (
sectionMatchers.some(m =>
m.test ? m.test(token.name) : token.name.startsWith(m)
)
) {
sectionParts.push(token);
return false;
}
return true;
});
if (sectionParts.length) {
sectionParts.sort((a, b) => {
let aName = formatBaseTokenNames(a.name);
let bName = formatBaseTokenNames(b.name);
let [aToken, aSuffix] = tokenParts(aName);
let [bToken, bSuffix] = tokenParts(bName);
if (aSuffix || bSuffix) {
if (aToken == bToken) {
let aSize = TSHIRT_ORDER.indexOf(aSuffix);
let bSize = TSHIRT_ORDER.indexOf(bSuffix);
if (aSize != -1 && bSize != -1) {
return aSize - bSize;
}
let aState = STATE_ORDER.indexOf(aSuffix);
let bState = STATE_ORDER.indexOf(bSuffix);
if (aState != -1 && bState != -1) {
return aState - bState;
}
}
}
return aToken.localeCompare(bToken, undefined, { numeric: true });
});
let headingParts = [];
if (!isFirst) {
headingParts.push("");
}
isFirst = false;
let sectionLevel = "**";
let labelParts = label.split("/");
for (let i = 0; i < labelParts.length; i++) {
if (labelParts[i] != lastSection[i]) {
headingParts.push(
`${formatting.indentation}/${sectionLevel} ${labelParts[i]} ${sectionLevel}/`
);
}
sectionLevel += "*";
}
lastSection = labelParts;
outputParts = outputParts.concat(
headingParts.concat(sectionParts.map(propertyFormatter))
);
}
}
return outputParts.join("\n");
}
module.exports = {
source: ["design-tokens.json"],
format: {
"css/variables/shared": createDesktopFormat(),
"css/variables/brand": createDesktopFormat("brand"),
"css/variables/platform": createDesktopFormat("platform"),
},
platforms: {
css: {
options: {
outputReferences: true,
showFileHeader: false,
},
transforms: [
...StyleDictionary.transformGroup.css,
...["shared", "platform", "brand"].map(createLightDarkTransform),
],
files: [
{
destination: "tokens-shared.css",
format: "css/variables/shared",
},
{
destination: "tokens-brand.css",
format: "css/variables/brand",
filter: token =>
typeof token.original.value == "object" &&
token.original.value.brand,
},
{
destination: "tokens-platform.css",
format: "css/variables/platform",
filter: token =>
typeof token.original.value == "object" &&
token.original.value.platform,
},
],
},
},
};

View file

@ -2,6 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* DO NOT EDIT this file directly, instead modify design-tokens.json
* and run `npm run build` to see your changes. */
@import url("chrome://global/skin/design-system/tokens-shared.css");
@layer tokens-foundation {
@ -17,7 +20,7 @@
--border-color-interactive: color-mix(in srgb, currentColor 15%, var(--color-gray-60));
/** Button **/
--button-background-color: var(--button-bgcolor);
--button-background-color: var(--button-bgcolor); /* TODO Bug 1821203 - Gray use needs to be consolidated */
--button-background-color-hover: var(--button-hover-bgcolor);
--button-background-color-active: var(--button-active-bgcolor);
--button-text-color: var(--button-color);

View file

@ -2,6 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* DO NOT EDIT this file directly, instead modify design-tokens.json
* and run `npm run build` to see your changes. */
@layer tokens-foundation, tokens-prefers-contrast, tokens-forced-colors;
@layer tokens-foundation {
@ -85,8 +88,7 @@
/** Checkbox **/
--checkbox-margin-inline: var(--space-small);
/* TODO Bug 1876537: Make this em-based, probably? */
--checkbox-size: var(--size-item-small);
--checkbox-size: var(--size-item-small); /* TODO Bug 1876537: Make this em-based, probably? */
/** Color **/
--color-black-a10: rgba(0, 0, 0, 0.1);
@ -147,9 +149,11 @@
--input-text-min-height: var(--button-min-height);
/** Link **/
/* Not using --focus-outline-offset for links because that's intended for
elements with a background, and we only want a slight offset here while not
overlapping adjacent text. */
/**
* Not using --force-outline-offset for links because that's intended for
* elements with a background, and we only want a slight offset here while
* not overlapping adjacent text
*/
--link-focus-outline-offset: 1px;
/** Outline Color **/
@ -226,7 +230,6 @@
/* Bug 1879900: Can't nest media queries inside of :host, :root selector
until Bug 1879349 lands */
/* NOTE: These do not apply in the browser chrome (bug 1878919). */
@layer tokens-forced-colors {
@media (forced-colors) {
:root,
@ -238,7 +241,7 @@
--border-color-interactive-disabled: GrayText;
/** Button **/
--button-background-color: ButtonFace;
--button-background-color: ButtonFace; /* TODO Bug 1821203 - Gray use needs to be consolidated */
--button-background-color-hover: SelectedItemText;
--button-background-color-active: SelectedItemText;
--button-background-color-disabled: ButtonFace;

View file

@ -32,8 +32,8 @@
skin/classic/global/datetimeinputpickers.css (../../shared/datetimeinputpickers.css)
skin/classic/global/design-system/text-and-typography.css(../../shared/design-system/text-and-typography.css)
skin/classic/global/design-system/tokens-brand.css (../../shared/design-system/tokens-brand.css)
skin/classic/global/design-system/tokens-shared.css (../../shared/design-system/tokens-shared.css)
skin/classic/global/design-system/tokens-platform.css (../../shared/design-system/tokens-platform.css)
skin/classic/global/design-system/tokens-shared.css (../../shared/design-system/tokens-shared.css)
skin/classic/global/error-pages.css (../../shared/error-pages.css)
skin/classic/global/findbar.css (../../shared/findbar.css)
skin/classic/global/global-shared.css (../../shared/global-shared.css)