commit dbcbb7b754563a373dd04186faf06bf0e2d289c2
parent 5696e84aa1bf6a80d96866b5a3548421630e84f5
Author: dwhisman <dwhisman@mozilla.com>
Date: Thu, 13 Nov 2025 19:31:32 +0000
Bug 1988873 - Allow link colors, black, and white for text-color linting rule r=desktop-theme-reviewers,frontend-codestyle-reviewers,mkennedy,hjones
Differential Revision: https://phabricator.services.mozilla.com/D270394
Diffstat:
5 files changed, 211 insertions(+), 42 deletions(-)
diff --git a/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-text-color-tokens.rst b/docs/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-text-color-tokens.rst
@@ -103,3 +103,93 @@ The rule also allows these values non-token values:
.current-text-color {
color: currentColor;
}
+
+.. code-block:: css
+
+ .current-text-color {
+ color: white;
+ }
+
+.. code-block:: css
+
+ .current-text-color {
+ color: black;
+ }
+
+Autofix functionality
+---------------------
+
+This rule can automatically fix some violations by replacing hex color values with
+appropriate color names. Examples of autofixable violations:
+
+.. code-block:: css
+
+ /* Before */
+ .a {
+ color: #fff;
+ }
+
+ /* After autofix */
+ .a {
+ color: white;
+ }
+
+.. code-block:: css
+
+ /* Before */
+ .a {
+ color: #ffffff;
+ }
+
+ /* After autofix */
+ .a {
+ color: white;
+ }
+
+.. code-block:: css
+
+ /* Before */
+ .a {
+ color: #FFF;
+ }
+
+ /* After autofix */
+ .a {
+ color: white;
+ }
+
+.. code-block:: css
+
+ /* Before */
+ .a {
+ color: #FFFFFF;
+ }
+
+ /* After autofix */
+ .a {
+ color: white;
+ }
+
+.. code-block:: css
+
+ /* Before */
+ .a {
+ color: #000;
+ }
+
+ /* After autofix */
+ .a {
+ color: black;
+ }
+
+.. code-block:: css
+
+ /* Before */
+ .a {
+ color: #000000;
+ }
+
+ /* After autofix */
+ .a {
+ color: black;
+ }
diff --git a/toolkit/themes/shared/design-system/config/tokens-config.js b/toolkit/themes/shared/design-system/config/tokens-config.js
@@ -482,7 +482,6 @@ function formatTokensTableData(tokensData) {
const SINGULAR_TABLE_CATEGORIES = [
"button",
"color",
- "link",
"size",
"space",
"opacity",
@@ -510,6 +509,14 @@ function getTableName(tokenName) {
return "font-weight";
}
+ if (tokenName.includes("link-color")) {
+ return "text-color";
+ }
+
+ if (tokenName.includes("outline-offset")) {
+ return "outline";
+ }
+
let replacePattern =
/^(button-|input-text-|input-|focus-|checkbox-|table-row-|attention-dot-|promo-)/;
if (tokenName.match(replacePattern)) {
diff --git a/toolkit/themes/shared/design-system/dist/tokens-table.mjs b/toolkit/themes/shared/design-system/dist/tokens-table.mjs
@@ -916,6 +916,42 @@ export const tokensTable = {
},
{
value: {
+ forcedColors: "LinkText",
+ brand: { default: "var(--color-accent-primary)" },
+ platform: { default: "LinkText" },
+ },
+ name: "--link-color",
+ },
+ {
+ value: {
+ forcedColors: "LinkText",
+ brand: { default: "var(--color-accent-primary-hover)" },
+ platform: {
+ default: "color-mix(in srgb, black 10%, var(--link-color))",
+ },
+ },
+ name: "--link-color-hover",
+ },
+ {
+ value: {
+ forcedColors: "ActiveText",
+ brand: { default: "var(--color-accent-primary-active)" },
+ platform: {
+ default: "color-mix(in srgb, black 20%, var(--link-color))",
+ },
+ },
+ name: "--link-color-active",
+ },
+ {
+ value: {
+ forcedColors: "var(--link-color)",
+ brand: { default: "var(--link-color)" },
+ platform: { default: "var(--link-color)" },
+ },
+ name: "--link-color-visited",
+ },
+ {
+ value: {
prefersContrast: "CanvasText",
brand: {
light: "var(--color-gray-100)",
@@ -980,6 +1016,7 @@ export const tokensTable = {
},
{ value: "2px", name: "--focus-outline-offset" },
{ value: "2px", name: "--focus-outline-width" },
+ { value: "1px", name: "--link-focus-outline-offset" },
{
value: {
light: "var(--color-red-70)",
@@ -998,45 +1035,6 @@ export const tokensTable = {
{ value: "var(--size-item-medium)", name: "--icon-size-large" },
{ value: "var(--size-item-large)", name: "--icon-size-xlarge" },
],
- link: [
- {
- value: {
- forcedColors: "LinkText",
- brand: { default: "var(--color-accent-primary)" },
- platform: { default: "LinkText" },
- },
- name: "--link-color",
- },
- {
- value: {
- forcedColors: "LinkText",
- brand: { default: "var(--color-accent-primary-hover)" },
- platform: {
- default: "color-mix(in srgb, black 10%, var(--link-color))",
- },
- },
- name: "--link-color-hover",
- },
- {
- value: {
- forcedColors: "ActiveText",
- brand: { default: "var(--color-accent-primary-active)" },
- platform: {
- default: "color-mix(in srgb, black 20%, var(--link-color))",
- },
- },
- name: "--link-color-active",
- },
- {
- value: {
- forcedColors: "var(--link-color)",
- brand: { default: "var(--link-color)" },
- platform: { default: "var(--link-color)" },
- },
- name: "--link-color-visited",
- },
- { value: "1px", name: "--link-focus-outline-offset" },
- ],
"box-shadow": [
{
value:
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-text-color-tokens.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/rules/use-text-color-tokens.mjs
@@ -3,6 +3,7 @@
* 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,
@@ -25,7 +26,7 @@ const messages = ruleMessages(ruleName, {
const meta = {
url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/stylelint-plugin-mozilla/rules/use-text-color-tokens.html",
- fixable: false,
+ fixable: true,
};
const INCLUDE_CATEGORIES = ["text-color"];
@@ -33,10 +34,17 @@ const INCLUDE_CATEGORIES = ["text-color"];
const tokenCSS = createTokenNamesArray(INCLUDE_CATEGORIES);
// Allowed text-color values in CSS
-const ALLOW_LIST = createAllowList(["currentColor"]);
+const ALLOW_LIST = createAllowList(["currentColor", "white", "black"]);
const CSS_PROPERTIES = ["color"];
+const VIOLATION_AUTOFIX_MAP = {
+ "#fff": "white",
+ "#ffffff": "white",
+ "#000": "black",
+ "#000000": "black",
+};
+
const ruleFunction = primaryOption => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
@@ -76,6 +84,23 @@ const ruleFunction = primaryOption => {
node: declarations,
result,
ruleName,
+ fix: () => {
+ const val = valueParser(declarations.value);
+ let hasFixes = false;
+ val.walk(node => {
+ if (node.type == "word") {
+ const token =
+ VIOLATION_AUTOFIX_MAP[node.value.trim().toLowerCase()];
+ if (token) {
+ hasFixes = true;
+ node.value = token;
+ }
+ }
+ });
+ if (hasFixes) {
+ declarations.value = val.toString();
+ }
+ },
});
});
};
diff --git a/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-text-color-tokens.tests.mjs b/tools/lint/stylelint/stylelint-plugin-mozilla/tests/use-text-color-tokens.tests.mjs
@@ -31,6 +31,10 @@ testRule({
description: "Using text color token for color is valid.",
},
{
+ code: ".a { color: var(--link-color); }",
+ description: "Using link text color token for color is valid.",
+ },
+ {
code: ".a { color: var(--text-color, #000); }",
description:
"Using text color token with fallback value for color is valid.",
@@ -106,3 +110,48 @@ testRule({
},
],
});
+
+testRule({
+ plugins: [plugin],
+ ruleName,
+ config: true,
+ fix: true,
+ reject: [
+ {
+ code: ".a { color: #fff; }",
+ fixed: ".a { color: white; }",
+ message: messages.rejected("#fff"),
+ description: "#fff should be fixed to white.",
+ },
+ {
+ code: ".a { color: #ffffff; }",
+ fixed: ".a { color: white; }",
+ message: messages.rejected("#ffffff"),
+ description: "#ffffff should be fixed to white.",
+ },
+ {
+ code: ".a { color: #FFF; }",
+ fixed: ".a { color: white; }",
+ message: messages.rejected("#FFF"),
+ description: "#FFF should be fixed to white.",
+ },
+ {
+ code: ".a { color: #FFFFFF; }",
+ fixed: ".a { color: white; }",
+ message: messages.rejected("#FFFFFF"),
+ description: "#FFFFFF should be fixed to white.",
+ },
+ {
+ code: ".a { color: #000; }",
+ fixed: ".a { color: black; }",
+ message: messages.rejected("#000"),
+ description: "#000 should be fixed to black.",
+ },
+ {
+ code: ".a { color: #000000; }",
+ fixed: ".a { color: black; }",
+ message: messages.rejected("#000000"),
+ description: "#000000 should be fixed to black.",
+ },
+ ],
+});