tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d754e748bbe1327276fcd42a09d630067303907f
parent e7d971e5ce8dde935f0e19a761747c652c998286
Author: dustin-jw <dustin@heysparkbox.com>
Date:   Thu, 16 Oct 2025 16:19:02 +0000

Bug 1988856 - Add a Stylelint rule to enforce using background-color tokens r=frontend-codestyle-reviewers,hjones

Differential Revision: https://phabricator.services.mozilla.com/D267619

Diffstat:
M.stylelintrc.js | 3+++
Adocs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-background-color-tokens.rst | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mstylelint-rollouts.config.js | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/lint/stylelint/stylelint-plugin-mozilla/referenceColors.mjs | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs | 2++
Atools/lint/stylelint/stylelint-plugin-mozilla/rules/use-background-color-tokens.mjs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/lint/stylelint/stylelint-plugin-mozilla/tests/use-background-color-tokens.tests.mjs | 328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 1086 insertions(+), 0 deletions(-)

diff --git a/.stylelintrc.js b/.stylelintrc.js @@ -275,6 +275,7 @@ module.exports = { "stylelint-plugin-mozilla/use-font-size-tokens": true, "stylelint-plugin-mozilla/use-font-weight-tokens": true, "stylelint-plugin-mozilla/use-space-tokens": true, + "stylelint-plugin-mozilla/use-background-color-tokens": true, }, overrides: [ @@ -420,6 +421,7 @@ module.exports = { "stylelint-plugin-mozilla/use-font-size-tokens": false, "stylelint-plugin-mozilla/use-font-weight-tokens": false, "stylelint-plugin-mozilla/use-space-tokens": false, + "stylelint-plugin-mozilla/use-background-color-tokens": false, }, }, { @@ -432,6 +434,7 @@ module.exports = { "stylelint-plugin-mozilla/use-border-radius-tokens": true, "stylelint-plugin-mozilla/use-border-color-tokens": false, "stylelint-plugin-mozilla/use-space-tokens": false, + "stylelint-plugin-mozilla/use-background-color-tokens": false, }, }, ], diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-background-color-tokens.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-background-color-tokens.rst @@ -0,0 +1,117 @@ +=========================== +use-background-color-tokens +=========================== + +This rule checks that CSS declarations use background-color design token variables +instead of hard-coded values. This ensures consistent background-color across +the application and makes it easier to maintain design system adoption. + +Examples of incorrect code for this rule: +----------------------------------------- + +.. code-block:: css + + .card { + background-color: #191919; + } + +.. code-block:: css + + .custom-button { + background: url('image.png') rgba(42 42 42 / 0.15); + } + +.. code-block:: css + + button:hover { + background: rgba(0 0 0 / 0.25); + } + +.. code-block:: css + + :root { + --my-token: blue; + } + + .my-button { + background: url('image.png') no-repeat center center / auto var(--my-token, oklch(55% 0.21 15)); + } + +.. code-block:: css + + .accent-background-color { + background-color: AccentColor; + } + +Examples of correct token usage for this rule: +---------------------------------------------- + +.. code-block:: css + + .card { + background-color: var(--background-color-box); + } + +.. code-block:: css + + .custom-button { + background: url('image.png') var(--background-color-box); + } + +.. code-block:: css + + button:hover { + background: var(--background-color-box); + } + +.. code-block:: css + + /* You may set a fallback for a token. */ + + .my-button { + background: var(--background-color-box, oklch(55% 0.21 15)); + } + +.. code-block:: css + + /* Local CSS variables that reference valid border-radius tokens are allowed */ + + :root { + --my-token: var(--background-color-box); + } + + .my-button { + background-color: var(--my-token, oklch(55% 0.21 15)); + } + +The rule also allows these non-token values: + +.. code-block:: css + + .transparent-background-color { + background-color: transparent; + } + +.. code-block:: css + + .inherited-background-color{ + background-color: inherit; + } + +.. code-block:: css + + .unset-background-color { + background-color: unset; + } + +.. code-block:: css + + .initial-background-color { + background-color: initial; + } + +.. code-block:: css + + .current-background-color { + background-color: currentColor; + } diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js @@ -932,4 +932,226 @@ module.exports = [ "tools/tryselect/selectors/chooser/static/style.css", ], }, + { + // stylelint fixes for this rule will be addressed in Bug 1993105 + name: "rollout-use-background-color-tokens", + rules: { + "stylelint-plugin-mozilla/use-background-color-tokens": null, + }, + files: [ + "browser/branding/aurora/content/aboutDialog.css", + "browser/branding/aurora/stubinstaller/installing_page.css", + "browser/branding/nightly/content/aboutDialog.css", + "browser/branding/nightly/stubinstaller/installing_page.css", + "browser/branding/official/content/aboutDialog.css", + "browser/branding/official/stubinstaller/installing_page.css", + "browser/branding/official/stubinstaller/profile_cleanup_page.css", + "browser/branding/unofficial/content/aboutDialog.css", + "browser/branding/unofficial/stubinstaller/installing_page.css", + "browser/components/aboutlogins/content/aboutLogins.css", + "browser/components/aboutlogins/content/components/confirmation-dialog.css", + "browser/components/aboutlogins/content/components/generic-dialog.css", + "browser/components/aboutlogins/content/components/login-alert.css", + "browser/components/aboutlogins/content/components/login-item.css", + "browser/components/aboutlogins/content/components/login-timeline.css", + "browser/components/aboutlogins/content/components/remove-logins-dialog.css", + "browser/components/aboutwelcome/content-src/aboutwelcome.scss", + "browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.scss", + "browser/components/asrouter/content-src/styles/_feature-callout.scss", + "browser/components/contextualidentity/content/usercontext.css", + "browser/components/enterprisepolicies/content/aboutPolicies.css", + "browser/components/firefoxview/card-container.css", + "browser/components/firefoxview/firefoxview.css", + "browser/components/firefoxview/fxview-tab-row.css", + "browser/components/firefoxview/opentabs-tab-row.css", + "browser/components/genai/chat.css", + "browser/components/genai/content/link-preview-card.css", + "browser/components/ipprotection/content/ipprotection-content.css", + "browser/components/preferences/widgets/nav-notice/nav-notice.css", + "browser/components/profiles/content/avatar.css", + "browser/components/profiles/content/profile-avatar-selector.css", + "browser/components/profiles/content/profiles-theme-card.css", + "browser/components/protections/content/protections.css", + "browser/components/screenshots/overlay/overlay.css", + "browser/components/screenshots/screenshots-buttons.css", + "browser/components/search/content/contentSearchUI.css", + "browser/components/search/test/browser/telemetry/serp.css", + "browser/components/sidebar/sidebar-customize.css", + "browser/components/sidebar/sidebar-main.css", + "browser/components/sidebar/sidebar-pins-promo.css", + "browser/components/sidebar/sidebar.css", + "browser/components/textrecognition/textrecognition.css", + "browser/components/urlbar/tests/browser/dynamicResult0.css", + "browser/components/urlbar/tests/browser/dynamicResult1.css", + "browser/extensions/newtab/content-src/components/Base/_Base.scss", + "browser/extensions/newtab/content-src/components/Card/_Card.scss", + "browser/extensions/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss", + "browser/extensions/newtab/content-src/components/ConfirmDialog/_ConfirmDialog.scss", + "browser/extensions/newtab/content-src/components/ContextMenu/_ContextMenu.scss", + "browser/extensions/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/AdBanner/_AdBanner.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSDismiss/_DSDismiss.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/_DSEmptyState.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FeatureHighlight.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/InterestPicker/_InterestPicker.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/ListFeed/_ListFeed.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/PromoCard/_PromoCard.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TopicSelection/_TopicSelection.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/_TopicsWidget.scss", + "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/TrendingSearches/_TrendingSearches.scss", + "browser/extensions/newtab/content-src/components/DownloadModalToggle/_DownloadModalToggle.scss", + "browser/extensions/newtab/content-src/components/ModalOverlay/_ModalOverlay.scss", + "browser/extensions/newtab/content-src/components/Search/_Search.scss", + "browser/extensions/newtab/content-src/components/TopSites/_TopSites.scss", + "browser/extensions/newtab/content-src/components/WallpaperCategories/_WallpaperCategories.scss", + "browser/extensions/newtab/content-src/components/Weather/_Weather.scss", + "browser/extensions/newtab/content-src/components/Widgets/FocusTimer/_FocusTimer.scss", + "browser/extensions/newtab/content-src/components/Widgets/Lists/_Lists.scss", + "browser/extensions/newtab/content-src/styles/_mixins.scss", + "browser/extensions/newtab/content-src/styles/_variables.scss", + "browser/extensions/newtab/content-src/styles/activity-stream.scss", + "browser/extensions/webcompat/about-compat/aboutCompat.css", + "browser/themes/linux/browser.css", + "browser/themes/linux/places/organizer.css", + "browser/themes/osx/browser.css", + "browser/themes/osx/downloads/allDownloadsView.css", + "browser/themes/shared/UITour.css", + "browser/themes/shared/addons/unified-extensions.css", + "browser/themes/shared/autocomplete.css", + "browser/themes/shared/blockedSite.css", + "browser/themes/shared/browser-shared.css", + "browser/themes/shared/controlcenter/panel.css", + "browser/themes/shared/customizableui/customizeMode.css", + "browser/themes/shared/customizableui/panelUI-shared.css", + "browser/themes/shared/downloads/allDownloadsView.inc.css", + "browser/themes/shared/downloads/downloads.inc.css", + "browser/themes/shared/downloads/progressmeter.css", + "browser/themes/shared/formautofill-notification.css", + "browser/themes/shared/identity-block/identity-block.css", + "browser/themes/shared/identity-credential-notification.css", + "browser/themes/shared/notification-icons.css", + "browser/themes/shared/pageInfo.css", + "browser/themes/shared/places/editBookmark.css", + "browser/themes/shared/places/sidebar.css", + "browser/themes/shared/preferences/preferences.css", + "browser/themes/shared/preferences/privacy.css", + "browser/themes/shared/preferences/search.css", + "browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css", + "browser/themes/shared/search/searchbar.css", + "browser/themes/shared/sidebar.css", + "browser/themes/shared/syncedtabs/sidebar.css", + "browser/themes/shared/tabbrowser/content-area.css", + "browser/themes/shared/tabbrowser/ctrlTab.css", + "browser/themes/shared/tabbrowser/fullscreen-and-pointerlock.css", + "browser/themes/shared/tabbrowser/tabs.css", + "browser/themes/shared/toolbarbutton-icons.css", + "browser/themes/shared/toolbarbuttons.css", + "browser/themes/shared/translations/panel.css", + "browser/themes/shared/urlbar-dynamic-results.css", + "browser/themes/shared/urlbar-searchbar.css", + "browser/themes/shared/urlbarView.css", + "browser/themes/shared/webRTC-indicator.css", + "browser/themes/windows/browser.css", + "browser/themes/windows/downloads/allDownloadsView.css", + "browser/themes/windows/downloads/downloads.css", + "browser/themes/windows/places/organizer.css", + "browser/tools/mozscreenshots/mozscreenshots/extension/lib/mozscreenshots-style.css", + "dom/base/test/file_bug498897.css", + "dom/crypto/test/test_WebCrypto.css", + "dom/events/test/pointerevents/wpt/pointerevent_styles.css", + "dom/security/test/csp/file_docwrite_meta.css", + "dom/xml/resources/XMLPrettyPrint.css", + "dom/xml/test/old/books/classic.css", + "gfx/layers/layerviewer/tree.css", + "layout/generic/test/frame_selection_underline.css", + "layout/inspector/tests/bug1202095.css", + "layout/mathml/mathml.css", + "layout/style/TopLevelImageDocument.css", + "layout/style/TopLevelVideoDocument.css", + "layout/style/res/forms.css", + "layout/style/res/html.css", + "layout/style/res/ua.css", + "layout/style/res/viewsource.css", + "layout/style/test/file_shared_sheet_caching.css", + "remote/marionette/reftest-chrome/reftest.css", + "security/manager/ssl/tests/mochitest/mixedcontent/somestyle.css", + "testing/mochitest/static/harness.css", + "testing/mochitest/tests/SimpleTest/test.css", + "testing/mozbase/mozlog/mozlog/formatters/html/style.css", + "testing/talos/talos/tests/scroll/reader.css", + "toolkit/components/aboutconfig/content/aboutconfig.css", + "toolkit/components/aboutinference/content/aboutInference.css", + "toolkit/components/aboutinference/content/model-files-view.css", + "toolkit/components/aboutprocesses/content/aboutProcesses.css", + "toolkit/components/aboutwebauthn/content/aboutWebauthn.css", + "toolkit/components/certviewer/content/components/info-item.css", + "toolkit/components/certviewer/content/components/list-item.css", + "toolkit/components/cleardata/tests/browser/file_css_cache.css", + "toolkit/components/normandy/content/about-studies/about-studies.css", + "toolkit/components/printing/content/printPagination.css", + "toolkit/components/resistfingerprinting/content/letterboxing.css", + "toolkit/components/satchel/megalist/content/components/login-form/login-form.css", + "toolkit/components/satchel/megalist/content/components/password-card/password-card.css", + "toolkit/components/satchel/megalist/content/megalist.css", + "toolkit/content/aboutLogging/aboutLogging.css", + "toolkit/content/aboutMozilla.css", + "toolkit/content/aboutTelemetry.css", + "toolkit/content/aboutwebrtc/aboutWebrtc.css", + "toolkit/content/tests/widgets/videomask.css", + "toolkit/content/widgets/infobar.css", + "toolkit/content/widgets/moz-button/moz-button.css", + "toolkit/content/widgets/moz-input-text/moz-input-text.css", + "toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css", + "toolkit/content/widgets/moz-page-nav/moz-page-nav.css", + "toolkit/content/widgets/moz-reorderable-list/moz-reorderable-list.css", + "toolkit/content/widgets/moz-toggle/moz-toggle.css", + "toolkit/content/widgets/panel-list/panel-item.css", + "toolkit/content/widgets/panel-list/panel-list.css", + "toolkit/content/xul.css", + "toolkit/mozapps/extensions/content/aboutaddons.css", + "toolkit/mozapps/extensions/content/shortcuts.css", + "toolkit/themes/linux/global/autocomplete.css", + "toolkit/themes/linux/global/richlistbox.css", + "toolkit/themes/linux/mozapps/update/updates.css", + "toolkit/themes/mobile/global/aboutMemory.css", + "toolkit/themes/mobile/global/aboutSupport.css", + "toolkit/themes/osx/global/richlistbox.css", + "toolkit/themes/shared/aboutCache.css", + "toolkit/themes/shared/aboutNetError.css", + "toolkit/themes/shared/aboutReader.css", + "toolkit/themes/shared/aboutSupport.css", + "toolkit/themes/shared/alert.css", + "toolkit/themes/shared/arrowscrollbox.css", + "toolkit/themes/shared/close-icon.css", + "toolkit/themes/shared/datetimeinputpickers.css", + "toolkit/themes/shared/design-system/tokens-table.css", + "toolkit/themes/shared/dirListing/dirListing.css", + "toolkit/themes/shared/findbar.css", + "toolkit/themes/shared/global-shared.css", + "toolkit/themes/shared/in-content/common-shared.css", + "toolkit/themes/shared/in-content/info-pages.css", + "toolkit/themes/shared/menu.css", + "toolkit/themes/shared/narrate.css", + "toolkit/themes/shared/pictureinpicture/player.css", + "toolkit/themes/shared/pictureinpicture/texttracks.css", + "toolkit/themes/shared/popup.css", + "toolkit/themes/shared/splitter.css", + "toolkit/themes/shared/tabbox.css", + "toolkit/themes/shared/toolbarbutton.css", + "toolkit/themes/shared/tree/tree.css", + "toolkit/themes/windows/global/autocomplete.css", + "toolkit/themes/windows/global/button.css", + "toolkit/themes/windows/global/richlistbox.css", + "toolkit/themes/windows/global/wizard.css", + "toolkit/themes/windows/mozapps/update/updates.css", + "tools/tryselect/selectors/chooser/static/style.css", + ], + }, ]; diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/helpers.mjs @@ -6,6 +6,12 @@ import valueParser from "postcss-value-parser"; import { tokensTable } from "../../../../toolkit/themes/shared/design-system/tokens-table.mjs"; +import { + DEPRECATED_SYSTEM_COLORS, + NAMED_COLORS, + PREFIXED_SYSTEM_COLORS, + SYSTEM_COLORS, +} from "./referenceColors.mjs"; /** * Allows rules to access the tokens table without hard-coding the import path in multiple files. @@ -162,6 +168,35 @@ export const isVariableFunction = node => isFunction(node) && node.value === "var"; /** + * Checks if CSS includes a named color, e.g. 'white' or 'rebeccapurple' + * + * @param {string} value some CSS declaration to match + * @returns {boolean} + */ +export const containsNamedColor = value => + valueParser(String(value)).nodes.some( + node => + node.type === "word" && NAMED_COLORS.includes(node.value.toLowerCase()) + ); + +/** + * Checks if CSS includes a named color, e.g. 'white' or 'rebeccapurple' + * + * @param {string} value some CSS declaration to match + * @returns {boolean} + */ +export const containsSystemColor = value => + valueParser(String(value)).nodes.some( + node => + node.type === "word" && + [ + ...PREFIXED_SYSTEM_COLORS, + ...DEPRECATED_SYSTEM_COLORS, + ...SYSTEM_COLORS, + ].includes(node.value.toLowerCase()) + ); + +/** * Checks if CSS includes a hex value, e.g. `#00000`. * * @param {string} value some CSS declaration to match @@ -189,6 +224,33 @@ export const containsColorFunction = value => { }; /** + * Returns only the properties in the declaration that are colors, or at least likely to be colors. + * This allows for ignoring properties in shorthand that are not relevant to color rules. + * + * @param {string} value some CSS declaration to match + * @returns {string[]} + */ +export const getColorProperties = value => { + const relevantProperties = []; + const parsed = valueParser(value); + parsed.nodes.forEach(node => { + const property = value.substring(node.sourceIndex, node.sourceEndIndex); + if ( + ALLOW_LIST.includes(property) || + containsHexColor(property) || + containsNamedColor(property) || + containsSystemColor(property) || + containsColorFunction(property) || + isVariableFunction(node) + ) { + relevantProperties.push(property); + } + }); + + return relevantProperties; +}; + +/** * Looks to see if a value is included in our token var() array. * * @param {string} value some CSS declaration to match diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/referenceColors.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/referenceColors.mjs @@ -0,0 +1,258 @@ +/** + * 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/. + */ + +/** + * The list of named colors in CSS. + */ +export const NAMED_COLORS = [ + "aliceblue", + "antiquewhite", + "aqua", + "aquamarine", + "azure", + "beige", + "bisque", + "black", + "blanchedalmond", + "blue", + "blueviolet", + "brown", + "burlywood", + "cadetblue", + "chartreuse", + "chocolate", + "coral", + "cornflowerblue", + "cornsilk", + "crimson", + "cyan", + "darkblue", + "darkcyan", + "darkgoldenrod", + "darkgray", + "darkgreen", + "darkgrey", + "darkkhaki", + "darkmagenta", + "darkolivegreen", + "darkorange", + "darkorchid", + "darkred", + "darksalmon", + "darkseagreen", + "darkslateblue", + "darkslategray", + "darkslategrey", + "darkturquoise", + "darkviolet", + "deeppink", + "deepskyblue", + "dimgray", + "dimgrey", + "dodgerblue", + "firebrick", + "floralwhite", + "forestgreen", + "fuchsia", + "gainsboro", + "ghostwhite", + "gold", + "goldenrod", + "gray", + "green", + "greenyellow", + "grey", + "honeydew", + "hotpink", + "indianred", + "indigo", + "ivory", + "khaki", + "lavender", + "lavenderblush", + "lawngreen", + "lemonchiffon", + "lightblue", + "lightcoral", + "lightcyan", + "lightgoldenrodyellow", + "lightgray", + "lightgreen", + "lightgrey", + "lightpink", + "lightsalmon", + "lightseagreen", + "lightskyblue", + "lightslategray", + "lightslategrey", + "lightsteelblue", + "lightyellow", + "lime", + "limegreen", + "linen", + "magenta", + "maroon", + "mediumaquamarine", + "mediumblue", + "mediumorchid", + "mediumpurple", + "mediumseagreen", + "mediumslateblue", + "mediumspringgreen", + "mediumturquoise", + "mediumvioletred", + "midnightblue", + "mintcream", + "mistyrose", + "moccasin", + "navajowhite", + "navy", + "oldlace", + "olive", + "olivedrab", + "orange", + "orangered", + "orchid", + "palegoldenrod", + "palegreen", + "paleturquoise", + "palevioletred", + "papayawhip", + "peachpuff", + "peru", + "pink", + "plum", + "powderblue", + "purple", + "rebeccapurple", + "red", + "rosybrown", + "royalblue", + "saddlebrown", + "salmon", + "sandybrown", + "seagreen", + "seashell", + "sienna", + "silver", + "skyblue", + "slateblue", + "slategray", + "slategrey", + "snow", + "springgreen", + "steelblue", + "tan", + "teal", + "thistle", + "tomato", + "turquoise", + "violet", + "wheat", + "white", + "whitesmoke", + "yellow", + "yellowgreen", +]; + +/** + * The list of browser prefixed system colors. + */ +export const PREFIXED_SYSTEM_COLORS = [ + "-moz-buttondefault", + "-moz-buttonhoverface", + "-moz-buttonhovertext", + "-moz-cellhighlight", + "-moz-cellhighlighttext", + "-moz-combobox", + "-moz-comboboxtext", + "-moz-dialog", + "-moz-dialogtext", + "-moz-dragtargetzone", + "-moz-eventreerow", + "-moz-field", + "-moz-fieldtext", + "-moz-html-cellhighlight", + "-moz-html-cellhighlighttext", + "-moz-mac-accentdarkestshadow", + "-moz-mac-accentdarkshadow", + "-moz-mac-accentface", + "-moz-mac-accentlightesthighlight", + "-moz-mac-accentlightshadow", + "-moz-mac-accentregularhighlight", + "-moz-mac-accentregularshadow", + "-moz-mac-chrome-active", + "-moz-mac-chrome-inactive", + "-moz-mac-focusring", + "-moz-mac-menuselect", + "-moz-mac-menushadow", + "-moz-mac-menutextselect", + "-moz-menubarhovertext", + "-moz-menubartext", + "-moz-menuhover", + "-moz-menuhovertext", + "-moz-nativehyperlinktext", + "-moz-oddtreerow", + "-moz-win-accentcolor", + "-moz-win-accentcolortext", + "-moz-win-communicationstext", + "-moz-win-mediatext", + "-ms-hotlight", +]; + +/** + * The list of system colors that are deprecated, but may still be in use. + */ +export const DEPRECATED_SYSTEM_COLORS = [ + "activeborder", + "activecaption", + "appworkspace", + "background", + "buttonhighlight", + "buttonshadow", + "captiontext", + "inactiveborder", + "inactivecaption", + "inactivecaptiontext", + "infobackground", + "infotext", + "menu", + "menutext", + "scrollbar", + "threeddarkshadow", + "threedface", + "threedhighlight", + "threedlightshadow", + "threedshadow", + "window", + "windowframe", + "windowtext", +]; + +/** + * The list of system colors that are valid and intended to be used for high contrast/forced colors mode situations. + */ +export const SYSTEM_COLORS = [ + "accentcolor", + "accentcolortext", + "activetext", + "buttonborder", + "buttonface", + "buttontext", + "canvas", + "canvastext", + "field", + "fieldtext", + "graytext", + "highlight", + "highlighttext", + "linktext", + "mark", + "marktext", + "selecteditem", + "selecteditemtext", + "visitedtext", +]; diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs @@ -10,6 +10,7 @@ import useBorderColorTokens from "./use-border-color-tokens.mjs"; import useFontSizeTokens from "./use-font-size-tokens.mjs"; import useFontWeightTokens from "./use-font-weight-tokens.mjs"; import useSpaceTokens from "./use-space-tokens.mjs"; +import useBackgroundColorTokens from "./use-background-color-tokens.mjs"; export default { "no-base-design-tokens": noBaseDesignTokens, @@ -18,4 +19,5 @@ export default { "use-font-size-tokens": useFontSizeTokens, "use-font-weight-tokens": useFontWeightTokens, "use-space-tokens": useSpaceTokens, + "use-background-color-tokens": useBackgroundColorTokens, }; diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-background-color-tokens.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-background-color-tokens.mjs @@ -0,0 +1,94 @@ +/* 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/. */ + +import stylelint from "stylelint"; +import { + namespace, + createTokenNamesArray, + createAllowList, + getLocalCustomProperties, + getColorProperties, + isValidValue, +} from "../helpers.mjs"; + +const { + utils: { report, ruleMessages, validateOptions }, +} = stylelint; + +// Name our rule, set the error message, and link to meta +const ruleName = namespace("use-background-color-tokens"); + +const messages = ruleMessages(ruleName, { + rejected: value => `${value} should use a background-color design token.`, +}); + +const meta = { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-background-color-tokens.html", + fixable: false, +}; + +// Gather an array of the ready css `['var(--token-name)']` +const INCLUDE_CATEGORIES = ["background-color"]; + +const tokenCSS = createTokenNamesArray(INCLUDE_CATEGORIES); + +// Allowed border-color values in CSS +const ALLOW_LIST = createAllowList([ + "transparent", + "currentColor", + "auto", + "normal", + "none", +]); + +const CSS_PROPERTIES = ["background", "background-color"]; + +const ruleFunction = primaryOption => { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primaryOption, + possible: [true], + }); + + if (!validOptions) { + return; + } + + // The first time through gathers our custom properties + const cssCustomProperties = getLocalCustomProperties(root); + + // And then we validate our properties + root.walkDecls(declarations => { + const { prop, value } = declarations; + + // If the property is not in our list to check, skip it + if (!CSS_PROPERTIES.includes(prop)) { + return; + } + + // This rule only cares about colors, so all other shorthand properties are ignored + const colorProperties = getColorProperties(value); + const allColorsAreValid = colorProperties.every(property => + isValidValue(property, tokenCSS, cssCustomProperties, ALLOW_LIST) + ); + + if (allColorsAreValid) { + return; + } + + report({ + message: messages.rejected(declarations.value), + node: declarations, + result, + ruleName, + }); + }); + }; +}; + +ruleFunction.ruleName = ruleName; +ruleFunction.messages = messages; +ruleFunction.meta = meta; + +export default ruleFunction; diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-background-color-tokens.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-background-color-tokens.tests.mjs @@ -0,0 +1,328 @@ +/** + * 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/PL/2.0/. + */ + +// Bug 1948378: remove this exception when the eslint import plugin fully +// supports exports in package.json files +// eslint-disable-next-line import/no-unresolved +import { testRule } from "stylelint-test-rule-node"; +import stylelint from "stylelint"; +import useBackgroundColorTokens from "../rules/use-background-color-tokens.mjs"; + +let plugin = stylelint.createPlugin( + useBackgroundColorTokens.ruleName, + useBackgroundColorTokens +); +let { + ruleName, + rule: { messages }, +} = plugin; + +testRule({ + plugins: [plugin], + ruleName, + config: true, + fix: false, + accept: [ + { + code: ".bg { background-color: var(--background-color-box); }", + description: "Using background-color token is valid.", + }, + { + code: ".bg { background-color: var(--background-color-box, #666); }", + description: + "Using background-color token with a raw color fallback is valid.", + }, + { + code: ".bg { background-color: var(--background-color-box, var(--another-token)); }", + description: + "Using background-color token with a variable fallback is valid.", + }, + { + code: ` + :root { --custom-token: var(--background-color-box); } + .bg { background-color: var(--custom-token); } + `, + description: + "Using a custom token that resolves to a background-color token is valid.", + }, + { + code: ".bg { background-color: inherit; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: initial; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: revert; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: revert-layer; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: unset; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: transparent; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: currentColor; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: auto; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: normal; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background-color: none; }", + description: "Using a keyword is valid.", + }, + { + code: ".bg { background: var(--background-color-box); }", + description: "Using background-color token is valid in the shorthand.", + }, + { + code: ".bg { background: var(--background-color-box, #666); }", + description: + "Using background-color token with a raw color fallback is valid in the shorthand.", + }, + { + code: ".bg { background: var(--background-color-box, var(--another-token)); }", + description: + "Using background-color token with a token fallback is valid in the shorthand.", + }, + { + code: ` + :root { --custom-token: var(--background-color-box); } + .bg { background: var(--custom-token); } + `, + description: + "Using a custom token that resolves to a background-color token is valid in the shorthand.", + }, + { + code: ".bg { background: url('image.png'); }", + description: + "Using the background shorthand without any color declarations is valid.", + }, + { + code: ".bg { background: linear-gradient(to bottom, #fff, #000) var(--background-color-box); }", + description: + "Using the background shorthand, other properties plus a background-color token is valid.", + }, + { + code: ".bg { background: url('image.png') no-repeat center center / auto var(--background-color-box, oklch(69% 0.19 15)); }", + description: + "Using a background-color token with a raw color value fallback is valid in the shorthand.", + }, + { + code: ".bg { background: url('image.png') fixed content-box var(--background-color-box, var(--another-token)); }", + description: + "Using a background-color token with another token fallback is valid in the shorthand.", + }, + { + code: ` + :root { --custom-token: var(--background-color-box); } + .bg { background: url('image.png') var(--custom-token) repeat-y fixed; } + `, + description: + "Using a custom token that resolves to a background-color token is valid in the shorthand.", + }, + { + code: ".bg { background: inherit; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: initial; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: revert; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: revert-layer; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: unset; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: transparent; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: currentColor; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: auto; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: normal; }", + description: "Using a keyword is valid in the shorthand.", + }, + { + code: ".bg { background: none; }", + description: "Using a keyword is valid in the shorthand.", + }, + ], + + reject: [ + { + code: ".bg { background-color: #666; }", + message: messages.rejected("#666"), + description: "#666 should use a background-color design token.", + }, + { + code: ".bg { background-color: #fff0; }", + message: messages.rejected("#fff0"), + description: "#fff0 should use a background-color design token.", + }, + { + code: ".bg { background-color: #666666; }", + message: messages.rejected("#666666"), + description: "#666666 should use a background-color design token.", + }, + { + code: ".bg { background-color: #ffffff00; }", + message: messages.rejected("#ffffff00"), + description: "#ffffff00 should use a background-color design token.", + }, + { + code: ".bg { background-color: oklch(69% 0.19 15); }", + message: messages.rejected("oklch(69% 0.19 15)"), + description: + "oklch(69% 0.19 15) should use a background-color design token.", + }, + { + code: ".bg { background-color: rgba(42 42 42 / 0.15); }", + message: messages.rejected("rgba(42 42 42 / 0.15)"), + description: + "rgba(42 42 42 / 0.15) should use a background-color design token.", + }, + { + code: ".bg { background-color: ButtonFace; }", + message: messages.rejected("ButtonFace"), + description: "ButtonFace should use a background-color design token.", + }, + { + code: ".bg { background-color: var(--random-token, oklch(69% 0.19 15)); }", + message: messages.rejected("var(--random-token, oklch(69% 0.19 15))"), + description: + "var(--random-token, oklch(69% 0.19 15)) should use a background-color design token.", + }, + { + code: ` + :root { --custom-token: #666; } + .bg { background-color: var(--custom-token); } + `, + message: messages.rejected("var(--custom-token)"), + description: + "var(--custom-token) should use a background-color design token.", + }, + { + code: ".bg { background: #666; }", + message: messages.rejected("#666"), + description: "#666 should use a background-color design token.", + }, + { + code: ".bg { background: #fff0; }", + message: messages.rejected("#fff0"), + description: "#fff0 should use a background-color design token.", + }, + { + code: ".bg { background: #666666; }", + message: messages.rejected("#666666"), + description: "#666666 should use a background-color design token.", + }, + { + code: ".bg { background: #ffffff00; }", + message: messages.rejected("#ffffff00"), + description: "#ffffff00 should use a background-color design token.", + }, + { + code: ".bg { background: oklch(69% 0.19 15); }", + message: messages.rejected("oklch(69% 0.19 15)"), + description: + "oklch(69% 0.19 15) should use a background-color design token.", + }, + { + code: ".bg { background: rgba(42 42 42 / 0.15); }", + message: messages.rejected("rgba(42 42 42 / 0.15)"), + description: + "rgba(42 42 42 / 0.15) should use a background-color design token.", + }, + { + code: ".bg { background: border-box #666; }", + message: messages.rejected("border-box #666"), + description: + "border-box #666 should use a background-color design token.", + }, + { + code: ".bg { background: url('image.png') #fff0, #666; }", + message: messages.rejected("url('image.png') #fff0, #666"), + description: + "url('image.png') #fff0, #666 should use a background-color design token.", + }, + { + code: ".bg { background: url('image.png') oklch(69% 0.19 15) repeat-y; }", + message: messages.rejected( + "url('image.png') oklch(69% 0.19 15) repeat-y" + ), + description: + "url('image.png') oklch(69% 0.19 15) repeat-y should use a background-color design token.", + }, + { + code: ".bg { background: url('image.png') top center fixed #ffffff00; }", + message: messages.rejected("url('image.png') top center fixed #ffffff00"), + description: + "url('image.png') top center fixed #ffffff00 should use a background-color design token.", + }, + { + code: ".bg { background: url('image.png') center left / auto no-repeat scroll content-box padding-box red, rgba(42 42 42 / 0.15); }", + message: messages.rejected( + "url('image.png') center left / auto no-repeat scroll content-box padding-box red, rgba(42 42 42 / 0.15)" + ), + description: + "url('image.png') center left / auto no-repeat scroll content-box padding-box red, rgba(42 42 42 / 0.15) should use a background-color design token.", + }, + { + code: ".bg { background: url('image.png') var(--random-token, rgba(42 42 42 / 0.15)); }", + message: messages.rejected( + "url('image.png') var(--random-token, rgba(42 42 42 / 0.15))" + ), + description: + "url('image.png') var(--random-token, rgba(42 42 42 / 0.15)) should use a background-color design token.", + }, + { + code: ".bg { background: url('image.png') Canvas; }", + message: messages.rejected("url('image.png') Canvas"), + description: + "url('image.png') Canvas should use a background-color design token.", + }, + { + code: ` + :root { --custom-token: #666666; } + .bg { background: url('image.png') no-repeat center / auto var(--custom-token); } + `, + message: messages.rejected( + "url('image.png') no-repeat center / auto var(--custom-token)" + ), + description: + "url('image.png') no-repeat center / auto var(--custom-token) should use a background-color design token.", + }, + ], +});