commit b7da164f64ec9d92c357f51d28ef8f52906c195b
parent 453c9ef630ee2124ef3665f430fff0f524bd538f
Author: Osmond Arnesto <oarnesto@mozilla.com>
Date: Tue, 4 Nov 2025 16:59:07 +0000
Bug 1988860 - stylelint rule to enforce using size design tokens r=frontend-codestyle-reviewers,akulyk,Gijs
Differential Revision: https://phabricator.services.mozilla.com/D269537
Diffstat:
7 files changed, 878 insertions(+), 4 deletions(-)
diff --git a/.stylelintrc.js b/.stylelintrc.js
@@ -283,6 +283,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-text-color-tokens": true,
"stylelint-plugin-mozilla/use-box-shadow-tokens": true,
"stylelint-plugin-mozilla/no-non-semantic-token-usage": true,
+ "stylelint-plugin-mozilla/use-size-tokens": true,
},
overrides: [
@@ -431,6 +432,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-text-color-tokens": null,
"stylelint-plugin-mozilla/use-box-shadow-tokens": null,
"stylelint-plugin-mozilla/no-non-semantic-token-usage": null,
+ "stylelint-plugin-mozilla/use-size-tokens": null,
},
},
{
@@ -446,6 +448,7 @@ module.exports = {
"stylelint-plugin-mozilla/use-space-tokens": true,
"stylelint-plugin-mozilla/use-text-color-tokens": true,
"stylelint-plugin-mozilla/no-non-semantic-token-usage": true,
+ "stylelint-plugin-mozilla/use-size-tokens": true,
},
},
{
diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-size-tokens.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-size-tokens.rst
@@ -0,0 +1,111 @@
+===============
+use-size-tokens
+===============
+
+This rule checks that CSS declarations use size design token variables
+instead of hardcoded values.
+
+Examples of incorrect token usage for this rule:
+------------------------------------------------
+
+.. code-block:: css
+
+ .card {
+ min-width: 48px;
+ }
+
+.. code-block:: css
+
+ .button {
+ height: 0.75rem;
+ }
+
+.. code-block:: css
+
+ .icon {
+ width: 20px;
+ }
+
+
+.. code-block:: css
+
+ .icon {
+ width: calc(2 * 16px);
+ }
+
+.. code-block:: css
+
+ :root {
+ --local-size: 24px;
+ }
+
+ .button {
+ min-height: var(--local-size);
+ }
+
+Examples of correct code for this rule:
+---------------------------------------
+
+.. code-block:: css
+
+ .card {
+ min-width: var(--size-item-large);
+ }
+
+.. code-block:: css
+
+ .button {
+ height: var(--button-min-height);
+ }
+
+.. code-block:: css
+
+ .icon {
+ width: var(--icon-size-medium);
+ }
+
+.. code-block:: css
+
+ .icon {
+ width: var(--icon-size-medium, 28px);
+ }
+
+.. code-block:: css
+
+ .icon {
+ width: calc(2 * var(--icon-size-medium));
+ }
+
+.. code-block:: css
+
+ .icon {
+ width: calc(2 * var(--icon-size-medium, 28px));
+ }
+
+.. code-block:: css
+
+ :root {
+ --local-size: var(--size-item-small);
+ }
+
+ .button {
+ min-height: var(--local-size);
+ }
+
+.. code-block:: css
+
+ .button {
+ width: 100%;
+ }
+
+.. code-block:: css
+
+ .button {
+ width: auto;
+ }
+
+.. code-block:: css
+
+ .icon {
+ max-height: 2em;
+ }
diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js
@@ -1507,7 +1507,7 @@ module.exports = [
// stylelint fixes for this rule will be addressed in Bug 1992749
name: "rollout-no-non-semantic-token-usage",
rules: {
- "stylelint-plugin-mozilla/no-non-semantic-token-usage": "null",
+ "stylelint-plugin-mozilla/no-non-semantic-token-usage": null,
},
files: [
"browser/components/aboutlogins/content/components/input-field/input-field.css",
@@ -1587,4 +1587,309 @@ module.exports = [
"toolkit/themes/shared/menulist.css",
],
},
+ {
+ // stylelint fixes for this rule will be addressed in Bug 1995685
+ name: "rollout-use-size-tokens",
+ rules: {
+ "stylelint-plugin-mozilla/use-size-tokens": null,
+ },
+ files: [
+ "browser/base/content/aboutDialog.css",
+ "browser/base/content/pageinfo/pageInfo.css",
+ "browser/branding/aurora/stubinstaller/installing_page.css",
+ "browser/branding/aurora/stubinstaller/profile_cleanup_page.css",
+ "browser/branding/nightly/stubinstaller/installing_page.css",
+ "browser/branding/nightly/stubinstaller/profile_cleanup_page.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/branding/unofficial/stubinstaller/profile_cleanup_page.css",
+ "browser/components/aboutlogins/content/aboutLogins.css",
+ "browser/components/aboutlogins/content/aboutLoginsImportReport.css",
+ "browser/components/aboutlogins/content/components/confirmation-dialog.css",
+ "browser/components/aboutlogins/content/components/fxaccounts-button.css",
+ "browser/components/aboutlogins/content/components/generic-dialog.css",
+ "browser/components/aboutlogins/content/components/login-alert.css",
+ "browser/components/aboutlogins/content/components/login-filter.css",
+ "browser/components/aboutlogins/content/components/login-intro.css",
+ "browser/components/aboutlogins/content/components/login-item.css",
+ "browser/components/aboutlogins/content/components/login-list-lit-item.css",
+ "browser/components/aboutlogins/content/components/login-list.css",
+ "browser/components/aboutlogins/content/components/login-message-popup.css",
+ "browser/components/aboutlogins/content/components/login-timeline.css",
+ "browser/components/aboutlogins/content/components/menu-button.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/backup/content/backup-settings.css",
+ "browser/components/backup/content/password-rules-tooltip.css",
+ "browser/components/backup/content/password-validation-inputs.css",
+ "browser/components/backup/content/restore-from-backup.css",
+ "browser/components/backup/content/turn-on-scheduled-backups.css",
+ "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-empty-state.css",
+ "browser/components/firefoxview/fxview-tab-row.css",
+ "browser/components/firefoxview/opentabs-tab-row.css",
+ "browser/components/firefoxview/view-syncedtabs.css",
+ "browser/components/genai/chat.css",
+ "browser/components/genai/content/link-preview-card.css",
+ "browser/components/genai/content/model-optin.css",
+ "browser/components/messagepreview/messagepreview.css",
+ "browser/components/places/content/clearDataForSite.css",
+ "browser/components/places/metadataViewer/interactionsViewer.css",
+ "browser/components/preferences/dialogs/sitePermissions.css",
+ "browser/components/preferences/widgets/placeholder-message/placeholder-message.css",
+ "browser/components/profiles/content/avatar.css",
+ "browser/components/profiles/content/edit-profile-card.css",
+ "browser/components/profiles/content/profile-avatar-selector.css",
+ "browser/components/profiles/content/profile-card.css",
+ "browser/components/profiles/content/profile-selector.css",
+ "browser/components/profiles/content/profiles-pages.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-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/components/webrtc/content/webrtc-preview/webrtc-preview.css",
+ "browser/extensions/formautofill/content/formautofill.css",
+ "browser/extensions/formautofill/content/manageDialog.css",
+ "browser/extensions/formautofill/skin/shared/editAddress.css",
+ "browser/extensions/formautofill/skin/shared/editDialog-shared.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/AdBannerContextMenu/AdBannerContextMenu.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/CardSections/_CardSections.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.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/DSMessage/_DSMessage.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_DownloadMobilePromoHighlight.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FeatureHighlight.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_FollowSectionButtonHighlight.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_ShortcutFeatureHighlight.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/_WallpaperFeatureHighlight.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/Navigation/_Navigation.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/PersonalizedCard/_PersonalizedCard.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/PromoCard/_PromoCard.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/ReportContent/ReportContent.scss",
+ "browser/extensions/newtab/content-src/components/DiscoveryStreamComponents/SectionContextMenu/_SectionContextMenu.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/TopSites/_TopSites.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/Logo/_Logo.scss",
+ "browser/extensions/newtab/content-src/components/ModalOverlay/_ModalOverlay.scss",
+ "browser/extensions/newtab/content-src/components/MoreRecommendations/_MoreRecommendations.scss",
+ "browser/extensions/newtab/content-src/components/Notifications/_Notifications.scss",
+ "browser/extensions/newtab/content-src/components/Search/_Search.scss",
+ "browser/extensions/newtab/content-src/components/Sections/_Sections.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/_Widgets.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/_icons.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/themes/linux/browser.css",
+ "browser/themes/linux/places/organizer.css",
+ "browser/themes/osx/browser.css",
+ "browser/themes/osx/places/organizer.css",
+ "browser/themes/shared/aboutSessionRestore.css",
+ "browser/themes/shared/addon-notification.css",
+ "browser/themes/shared/addons/extension-controlled.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/download-blockedStates.css",
+ "browser/themes/shared/downloads/downloads.inc.css",
+ "browser/themes/shared/downloads/indicator.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/migration/migration-wizard.css",
+ "browser/themes/shared/notification-icons.css",
+ "browser/themes/shared/pageInfo.css",
+ "browser/themes/shared/places/editBookmark.css",
+ "browser/themes/shared/places/editBookmarkPanel.css",
+ "browser/themes/shared/places/organizer-shared.css",
+ "browser/themes/shared/places/sidebar.css",
+ "browser/themes/shared/places/tree-icons.css",
+ "browser/themes/shared/preferences/applications.css",
+ "browser/themes/shared/preferences/containers-dialog.css",
+ "browser/themes/shared/preferences/containers.css",
+ "browser/themes/shared/preferences/fxaPairDevice.css",
+ "browser/themes/shared/preferences/preferences.css",
+ "browser/themes/shared/preferences/privacy.css",
+ "browser/themes/shared/preferences/search.css",
+ "browser/themes/shared/preferences/siteDataSettings.css",
+ "browser/themes/shared/preferences/translations.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/tab-list-tree.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/tab-hover-preview.css",
+ "browser/themes/shared/tabbrowser/tabs.css",
+ "browser/themes/shared/toolbarbuttons.css",
+ "browser/themes/shared/translations/panel.css",
+ "browser/themes/shared/UITour.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/places/organizer.css",
+ "devtools/client/aboutdebugging/src/base.css",
+ "devtools/client/aboutdebugging/src/components/App.css",
+ "devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css",
+ "devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css",
+ "devtools/client/aboutdebugging/src/components/ProfilerDialog.css",
+ "devtools/client/aboutdebugging/src/components/shared/IconLabel.css",
+ "devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css",
+ "devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css",
+ "devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css",
+ "devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css",
+ "dom/events/test/pointerevents/wpt/pointerevent_styles.css",
+ "dom/xml/test/old/books/classic.css",
+ "dom/xml/test/old/books/common.css",
+ "gfx/layers/apz/test/mochitest/helper_subframe_style.css",
+ "layout/generic/test/frame_selection_underline.css",
+ "layout/style/res/forms.css",
+ "layout/style/res/html.css",
+ "layout/style/res/scrollbars.css",
+ "security/manager/pki/resources/content/certManager.css",
+ "testing/marionette/harness/marionette_harness/tests/unit/assets/chrome/style.css",
+ "testing/mochitest/static/harness.css",
+ "testing/mozbase/mozlog/mozlog/formatters/html/style.css",
+ "testing/talos/talos/tests/scroll/reader.css",
+ "testing/talos/talos/tests/tart/addon/content/tab-min-width-1px.css",
+ "toolkit/components/aboutcheckerboard/content/aboutCheckerboard.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/aboutthirdparty/content/aboutThirdParty.css",
+ "toolkit/components/certviewer/content/certviewer.css",
+ "toolkit/components/certviewer/content/components/error-section.css",
+ "toolkit/components/certviewer/content/components/info-group.css",
+ "toolkit/components/normandy/content/about-studies/about-studies.css",
+ "toolkit/components/printing/content/print.css",
+ "toolkit/components/printing/content/printPagination.css",
+ "toolkit/components/printing/content/printPreview.css",
+ "toolkit/components/printing/content/toggle-group.css",
+ "toolkit/components/reader/moz-slider.css",
+ "toolkit/components/resistfingerprinting/content/letterboxing.css",
+ "toolkit/components/satchel/megalist/content/components/login-line/login-line.css",
+ "toolkit/components/satchel/megalist/content/components/password-card/password-card.css",
+ "toolkit/components/satchel/megalist/content/megalist.css",
+ "toolkit/components/translations/content/about-translations.css",
+ "toolkit/content/aboutGlean.css",
+ "toolkit/content/aboutLogging/aboutLogging.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-color/moz-input-color.css",
+ "toolkit/content/widgets/moz-input-folder/moz-input-folder.css",
+ "toolkit/content/widgets/moz-input-text/moz-input-text.css",
+ "toolkit/content/widgets/moz-message-bar/moz-message-bar.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-promo/moz-promo.css",
+ "toolkit/content/widgets/moz-reorderable-list/moz-reorderable-list.css",
+ "toolkit/content/widgets/moz-select/moz-select.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/widgets/moz-box-group/moz-box-group.css",
+ "toolkit/content/xul.css",
+ "toolkit/crashreporter/content/crashes.css",
+ "toolkit/mozapps/extensions/content/aboutaddons.css",
+ "toolkit/mozapps/extensions/content/components/mlmodel-card-header-additions.css",
+ "toolkit/mozapps/extensions/content/shortcuts.css",
+ "toolkit/themes/linux/global/autocomplete.css",
+ "toolkit/themes/linux/mozapps/update/updates.css",
+ "toolkit/themes/osx/global/autocomplete.css",
+ "toolkit/themes/osx/global/button.css",
+ "toolkit/themes/osx/global/global.css",
+ "toolkit/themes/osx/global/in-content/common.css",
+ "toolkit/themes/osx/global/wizard.css",
+ "toolkit/themes/osx/mozapps/handling/handling.css",
+ "toolkit/themes/osx/mozapps/update/updates.css",
+ "toolkit/themes/shared/aboutHttpsOnlyError.css",
+ "toolkit/themes/shared/aboutLicense.css",
+ "toolkit/themes/shared/aboutNetError.css",
+ "toolkit/themes/shared/aboutReader.css",
+ "toolkit/themes/shared/aboutServiceWorkers.css",
+ "toolkit/themes/shared/aboutSupport.css",
+ "toolkit/themes/shared/alert.css",
+ "toolkit/themes/shared/appPicker.css",
+ "toolkit/themes/shared/checkbox.css",
+ "toolkit/themes/shared/close-icon.css",
+ "toolkit/themes/shared/commonDialog.css",
+ "toolkit/themes/shared/datetimeinputpickers.css",
+ "toolkit/themes/shared/design-system/tokens-table.css",
+ "toolkit/themes/shared/dirListing/dirListing.css",
+ "toolkit/themes/shared/downloads/unknownContentType.css",
+ "toolkit/themes/shared/error-pages.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/menulist.css",
+ "toolkit/themes/shared/narrate.css",
+ "toolkit/themes/shared/offlineSupportPages.css",
+ "toolkit/themes/shared/pictureinpicture/player.css",
+ "toolkit/themes/shared/pictureinpicture/texttracks.css",
+ "toolkit/themes/shared/popup.css",
+ "toolkit/themes/shared/popupnotification.css",
+ "toolkit/themes/shared/profileDowngrade.css",
+ "toolkit/themes/shared/radio.css",
+ "toolkit/themes/shared/splitter.css",
+ "toolkit/themes/shared/toolbar.css",
+ "toolkit/themes/shared/toolbarbutton.css",
+ "toolkit/themes/shared/tree/tree.css",
+ "toolkit/themes/windows/global/autocomplete.css",
+ "toolkit/themes/windows/global/global.css",
+ "toolkit/themes/windows/global/wizard.css",
+ "toolkit/themes/windows/mozapps/handling/handling.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
@@ -47,6 +47,11 @@ export const ALLOW_LIST = [
];
/**
+ * Regex capturing numeric values, em values, ch values, and percentage values
+ */
+export const FIXED_UNITS = /^\d*\.?\d*(em|ch|%)?$/;
+
+/**
* Extends our base ALLOW_LIST with additional allowed values.
*
* @param {string[]} additionalAllows to be appended to our list
@@ -166,6 +171,9 @@ export const isFunction = node => node.type === "function";
export const isVariableFunction = node =>
isFunction(node) && node.value === "var";
+// checks if a node is a `calc()` function
+export const isCalcFunction = node => isFunction(node) && node.value === "calc";
+
// checks if a node is a url() function
export const isUrlFunction = node => isFunction(node) && node.value === "url";
@@ -226,6 +234,19 @@ export const containsColorFunction = value => {
};
/**
+ * Checks if a node contains a value using vw/vh units
+ * e.g., `100vh`.
+ *
+ * @param {string} value some CSS declaration to match
+ * @returns {boolean}
+ */
+export const containsViewportUnit = value => {
+ return valueParser(String(value)).nodes.some(
+ node => node.type === "word" && /^(0|[\d.]+)(vh|vw)$/.test(node.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.
*
@@ -262,15 +283,20 @@ export const getColorProperties = value => {
export const isToken = (value, tokenCSS) => tokenCSS.includes(value);
/**
- * Checks if a CSS value is allowed.
+ * Checks if a CSS value is allowed, given exact strings or a
+ * regex pattern in an allowList.
*
* @param {string} value some CSS declaration to match
* @param {string[]} allowList
* @returns {boolean}
*/
export const isAllowed = (value, allowList) => {
+ const allowListPattern = pattern => {
+ pattern instanceof RegExp ? pattern.test(value) : pattern === value;
+ };
+
// If the value is in the allowList
- if (allowList.includes(value)) {
+ if (allowList.some(allowListPattern)) {
return true;
}
@@ -281,7 +307,13 @@ export const isAllowed = (value, allowList) => {
// If the value is in the allowList but the string is CSS shorthand, e.g. `border` properties
return valueParser(value).nodes.some(
- node => isWord(node) && allowList.includes(node.value)
+ node =>
+ isWord(node) &&
+ allowList.some(pattern =>
+ pattern instanceof RegExp
+ ? pattern.test(node.value)
+ : pattern === node.value
+ )
);
};
@@ -432,6 +464,63 @@ export const isValidTokenUsage = (
};
/**
+ * Checks if a calc() function contains valid token usage.
+ *
+ * @param {string} value - CSS declaration to match
+ * @param {string[]} tokenCSS
+ * @param {object} cssCustomProperties
+ * @param {string[]} allowList
+ * @returns {boolean}
+ */
+export const isValidTokenUsageInCalc = (
+ value,
+ tokenCSS,
+ cssCustomProperties,
+ allowList = ALLOW_LIST
+) => {
+ const parsed = valueParser(String(value));
+ let isEveryChildValid = true;
+
+ parsed.walk(node => {
+ if (!isEveryChildValid || !isCalcFunction(node)) {
+ return;
+ }
+
+ isEveryChildValid = node.nodes.every(child => {
+ if (
+ child.type === "space" ||
+ (child.type === "word" && /^[+\-*/]$/.test(child.value))
+ ) {
+ return true;
+ }
+
+ if (child.type === "word") {
+ if (
+ isAllowed(child.value, allowList) ||
+ /^\d*\.?\d*(vh|vw|em|%)?$/.test(child.value)
+ ) {
+ return true;
+ }
+ }
+
+ if (isVariableFunction(child)) {
+ const variableNode = `var(${child.nodes[0].value})`;
+ if (
+ isToken(variableNode, tokenCSS) ||
+ isValidLocalProperty(variableNode, cssCustomProperties, tokenCSS)
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+ });
+
+ return isEveryChildValid;
+};
+
+/**
* Checks if CSS value uses color tokens correctly.
*
* @param {string} value some CSS declaration to match
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/index.mjs
@@ -15,6 +15,7 @@ import useBackgroundColorTokens from "./use-background-color-tokens.mjs";
import useTextColorTokens from "./use-text-color-tokens.mjs";
import useBoxShadowTokens from "./use-box-shadow-tokens.mjs";
import noNonSemanticTokenUsage from "./no-non-semantic-token-usage.mjs";
+import useSizeTokens from "./use-size-tokens.mjs";
export default {
"no-base-design-tokens": noBaseDesignTokens,
@@ -28,4 +29,5 @@ export default {
"use-text-color-tokens": useTextColorTokens,
"use-box-shadow-tokens": useBoxShadowTokens,
"no-non-semantic-token-usage": noNonSemanticTokenUsage,
+ "use-size-tokens": useSizeTokens,
};
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-size-tokens.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-size-tokens.mjs
@@ -0,0 +1,172 @@
+/* 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 valueParser from "postcss-value-parser";
+import {
+ namespace,
+ createTokenNamesArray,
+ createRawValuesObject,
+ isValidTokenUsage,
+ isValidTokenUsageInCalc,
+ containsViewportUnit,
+ getLocalCustomProperties,
+ usesRawFallbackValues,
+ usesRawShorthandValues,
+ createAllowList,
+ FIXED_UNITS,
+} from "../helpers.mjs";
+
+const {
+ utils: { report, ruleMessages, validateOptions },
+} = stylelint;
+
+const ruleName = namespace("use-size-tokens");
+
+const messages = ruleMessages(ruleName, {
+ rejected: value =>
+ `Consider using a size design token instead of ${value}. This may be fixable by running the same command again with --fix.`,
+});
+
+const meta = {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-size-tokens.html",
+ fixable: true,
+};
+
+// Bug 1979978 moves this object to ../data.mjs
+const SIZE = {
+ CATEGORIES: ["size", "icon-size"],
+ PROPERTIES: [
+ "width",
+ "min-width",
+ "max-width",
+ "height",
+ "min-height",
+ "max-height",
+ "inline-size",
+ "min-inline-size",
+ "max-inline-size",
+ "block-size",
+ "min-block-size",
+ "max-block-size",
+ "inset",
+ "inset-block",
+ "inset-block-end",
+ "inset-block-start",
+ "inset-inline",
+ "inset-inline-end",
+ "inset-inline-start",
+ "left",
+ "right",
+ "top",
+ "bottom",
+ "background-size",
+ ],
+};
+
+const tokenCSS = createTokenNamesArray(SIZE.CATEGORIES);
+
+// Allowed size values in CSS
+const SIZE_TOKENS_ALLOW_LIST = createAllowList([
+ FIXED_UNITS,
+ "0",
+ "auto",
+ "none",
+ "fit-content",
+ "min-content",
+ "max-content",
+]);
+
+const RAW_VALUE_TO_TOKEN_VALUE = {
+ ...createRawValuesObject(SIZE.CATEGORIES),
+ "0.75rem": "var(--size-item-xsmall)",
+ "1rem": "var(--size-item-small)",
+ "1.5rem": "var(--size-item-medium)",
+ "2rem": "var(--size-item-large)",
+ "3rem": "var(--size-item-xlarge)",
+};
+
+const ruleFunction = primaryOption => {
+ return (root, result) => {
+ const validOptions = validateOptions(result, ruleName, {
+ actual: primaryOption,
+ possible: [true],
+ });
+
+ if (!validOptions) {
+ return;
+ }
+
+ // Walk declarations once to generate a lookup table of variables.
+ const cssCustomProperties = getLocalCustomProperties(root);
+
+ // Walk declarations again to detect non-token values.
+ root.walkDecls(declarations => {
+ if (!SIZE.PROPERTIES.includes(declarations.prop)) {
+ return;
+ }
+
+ // Allows values using `vh` or `vw` units
+ if (containsViewportUnit(declarations.value)) {
+ return;
+ }
+
+ // Otherwise, see if we are using the tokens correctly
+ if (
+ isValidTokenUsage(
+ declarations.value,
+ tokenCSS,
+ cssCustomProperties,
+ SIZE_TOKENS_ALLOW_LIST
+ ) &&
+ isValidTokenUsageInCalc(
+ declarations.value,
+ tokenCSS,
+ cssCustomProperties,
+ SIZE_TOKENS_ALLOW_LIST
+ ) &&
+ !usesRawFallbackValues(declarations.value, RAW_VALUE_TO_TOKEN_VALUE) &&
+ !usesRawShorthandValues(
+ declarations.value,
+ tokenCSS,
+ cssCustomProperties,
+ SIZE_TOKENS_ALLOW_LIST
+ )
+ ) {
+ return;
+ }
+
+ report({
+ message: messages.rejected(declarations.value),
+ node: declarations,
+ result,
+ ruleName,
+ fix: () => {
+ const val = valueParser(declarations.value);
+ let hasFixes = false;
+
+ val.walk(node => {
+ if (node.type === "word") {
+ const token = RAW_VALUE_TO_TOKEN_VALUE[node.value.trim()];
+ if (token) {
+ hasFixes = true;
+ node.value = token;
+ }
+ }
+ });
+
+ if (hasFixes) {
+ declarations.value = val.toString();
+ }
+ },
+ });
+ });
+ };
+};
+
+ruleFunction.ruleName = ruleName;
+ruleFunction.messages = messages;
+ruleFunction.meta = meta;
+
+export default ruleFunction;
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-size-tokens.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-size-tokens.tests.mjs
@@ -0,0 +1,192 @@
+/**
+ * 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-disable-next-line import/no-unresolved
+import { testRule } from "stylelint-test-rule-node";
+import stylelint from "stylelint";
+import useSizeTokens from "../rules/use-size-tokens.mjs";
+
+let plugin = stylelint.createPlugin(useSizeTokens.ruleName, useSizeTokens);
+let {
+ ruleName,
+ rule: { messages },
+} = plugin;
+
+testRule({
+ plugins: [plugin],
+ ruleName,
+ config: true,
+ fix: true,
+ accept: [
+ {
+ code: ".a { max-height: var(--size-item-medium); }",
+ description:
+ "Using the medium item size token with the max-height property is valid.",
+ },
+ {
+ code: ".a { min-height: var(--size-item-large); }",
+ description:
+ "Using the large item size token with the min-height property is valid.",
+ },
+ {
+ code: ".a { max-width: var(--size-item-xlarge); }",
+ description:
+ "Using the xlarge item size token with the max-width property is valid.",
+ },
+ {
+ code: ".a { min-width: var(--size-item-medium); }",
+ description:
+ "Using the medium item size token with the min-width property is valid.",
+ },
+ {
+ code: ".a { min-width: var(--size-item-medium); }",
+ description:
+ "Using the medium item size token with the min-width property is valid.",
+ },
+ {
+ code: ".a { inline-size: 50%; }",
+ description:
+ "Using a percentage value with the inline-size property is valid.",
+ },
+ {
+ code: ".a { inline-size: var(--size-item-xlarge); }",
+ description:
+ "Using the xlarge item size token with the inline-size property is valid.",
+ },
+ {
+ code: `
+ @media (min-width: 768px) {
+ a. { color: var(--text-color); }
+ }
+ `,
+ description:
+ "Using a pixel value with a size property within an atrule is valid.",
+ },
+ {
+ code: ".a { width: calc(var(--size-item-small) * 2); }",
+ description:
+ "Using the small item size token in a `calc()` declaration with the width property is valid.",
+ },
+ {
+ code: ".a { width: calc(2 * var(--size-item-small)); }",
+ description:
+ "Using the small item size token in a `calc()` declaration with the width property is valid.",
+ },
+ {
+ code: ".a { width: calc(2 * var(--size-item-small, 14px)); }",
+ description:
+ "Using the small item size token with a fallback value in a `calc()` declaration with the width property is valid.",
+ },
+ {
+ code: ".a { width: calc(var(--size-item-small, 14px) * 2); }",
+ description:
+ "Using the small item size token with a fallback value in a `calc()` declaration with the width property is valid.",
+ },
+ {
+ code: ".a { height: 100vh; }",
+ description:
+ "Using a value using the `vh` unit in the height property is valid.",
+ },
+ {
+ code: ".a { height: calc(100vh - 2em); }",
+ description:
+ "Using a value using the `vh` unit and a percentage value in a `calc()` function in the height property is valid.",
+ },
+ {
+ code: ".a { height: calc(100% - 2em); }",
+ description:
+ "Using a percentage value and a value using the `em` unit in a `calc()` function in the height property is valid.",
+ },
+ {
+ code: ".a { width: calc(100vw - 10%); }",
+ description:
+ "Using a percentage value and a value using the `em` unit in a `calc()` function in the height property is valid.",
+ },
+ {
+ code: ".a { min-block-size: 8em; }",
+ description:
+ "Using a value using the `em` unit in the min-block-size property is valid.",
+ },
+ {
+ code: ".a { max-block-size: 75%; }",
+ description:
+ "Using a percentage value in the min-block-size property is valid.",
+ },
+ {
+ code: ".a { max-height: var(--size-item-medium, 50%); }",
+ description:
+ "Using a size design token with a fallback value in the max-height property is valid.",
+ },
+ {
+ code: ".a { min-block-size: fit-content; }",
+ description:
+ "Using the fit-content value in the min-block-size property is valid.",
+ },
+ {
+ code: ".a { background-size: var(--icon-size-medium) auto; }",
+ description:
+ "Using the medium icon size token and the auto value in the background-size property is valid.",
+ },
+ {
+ code: ".a { background-size: calc(100vh * 0.8); }",
+ description:
+ "Using the medium icon size token and the auto value in the background-size property is valid.",
+ },
+ ],
+ reject: [
+ {
+ code: ".a { max-height: 500px; }",
+ unfixable: true,
+ message: messages.rejected("500px"),
+ description:
+ "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.",
+ },
+ {
+ code: ".a { height: 0.75rem; }",
+ fixed: ".a { height: var(--size-item-xsmall); }",
+ message: messages.rejected("0.75rem"),
+ description:
+ "Consider using a size design token instead of using a rem value. This may be fixable by running the same command again with --fix.",
+ },
+ {
+ code: `
+ :root { --local-size: 24px; }
+ .a { min-height: var(--local-size); }
+ `,
+ unfixable: true,
+ message: messages.rejected("var(--local-size)"),
+ description:
+ "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.",
+ },
+ {
+ code: `.a { max-inline-size: calc(16px + 32px); }`,
+ fixed: `.a { max-inline-size: calc(var(--size-item-small) + var(--size-item-large)); }`,
+ message: messages.rejected("calc(16px + 32px)"),
+ description:
+ "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.",
+ },
+ {
+ code: `.a { max-inline-size: calc(16px + var(--size-item-xlarge)); }`,
+ fixed: `.a { max-inline-size: calc(var(--size-item-small) + var(--size-item-xlarge)); }`,
+ message: messages.rejected("calc(16px + var(--size-item-xlarge))"),
+ description:
+ "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.",
+ },
+ {
+ code: `.a { max-inline-size: calc(var(--size-item-small) + 32px); }`,
+ fixed: `.a { max-inline-size: calc(var(--size-item-small) + var(--size-item-large)); }`,
+ message: messages.rejected("calc(var(--size-item-small) + 32px)"),
+ description:
+ "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.",
+ },
+ {
+ code: `.a { max-block-size: calc(100vh + 32px); }`,
+ fixed: `.a { max-block-size: calc(100vh + var(--size-item-large)); }`,
+ message: messages.rejected("calc(100vh + 32px)"),
+ description:
+ "Consider using a size design token instead of using a pixel value. This may be fixable by running the same command again with --fix.",
+ },
+ ],
+});