├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release.yml │ ├── release_canary.yml │ ├── release_candidate.yml │ └── release_tracking.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .stylelintrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── .eslintrc.json ├── __fixtures__ │ ├── color-vars.scss │ ├── defines-new-color-vars.scss │ ├── deprecations.json │ ├── design-tokens.json │ ├── good │ │ ├── example.module.css │ │ ├── example.pcss │ │ ├── example.scss │ │ └── example.tsx │ ├── has-unused-vars.scss │ ├── spacing-vars.scss │ └── uses-vars.scss ├── borders.js ├── box-shadow.js ├── colors.js ├── index.js ├── no-display-colors.js ├── responsive-widths.js ├── spacing.js ├── typography.js └── utils │ ├── index.js │ └── setup.js ├── index.js ├── package-lock.json ├── package.json ├── plugins ├── README.md ├── borders.js ├── box-shadow.js ├── colors.js ├── lib │ └── utils.js ├── no-display-colors.js ├── responsive-widths.js ├── spacing.js └── typography.js ├── prettier.config.js └── rollup.config.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", {"repo": "primer/stylelint-config"}], 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | dist/ 4 | __tests__/__fixtures__/ 5 | rollup.config.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "root": true, 4 | "plugins": ["github"], 5 | "extends": [ 6 | "plugin:github/recommended" 7 | ], 8 | "env": { 9 | "node": true 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2023, 13 | "requireConfigFile": false 14 | }, 15 | "rules": { 16 | "import/no-commonjs": "off", 17 | "github/no-then": "off", 18 | "i18n-text/no-en": "off", 19 | "import/extensions": ["error", "ignorePackages"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @primer/css-reviewers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for GitHub Actions 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | groups: 15 | all-actions: 16 | patterns: 17 | - "*" 18 | 19 | # Maintain dependencies for npm 20 | - package-ecosystem: "npm" 21 | directory: "/" 22 | schedule: 23 | interval: "daily" 24 | groups: 25 | production-dependencies: 26 | dependency-type: "production" 27 | development-dependencies: 28 | dependency-type: "development" 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | - run: npm ci 14 | - run: npm run test 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | - run: npm ci 21 | - run: npm run lint 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | jobs: 7 | release: 8 | name: Final 9 | if: ${{ github.repository == 'primer/stylelint-config' }} 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | persist-credentials: false 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v4 22 | 23 | - name: Install dependencies 24 | run: npm ci && npm run build 25 | 26 | - name: Create release pull request or publish to npm 27 | id: changesets 28 | uses: changesets/action@master 29 | with: 30 | title: Release Tracking 31 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 32 | publish: npm run release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GPR_AUTH_TOKEN_SHARED }} 35 | NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN_SHARED }} 36 | -------------------------------------------------------------------------------- /.github/workflows/release_canary.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'main' 6 | - 'changeset-release/**' 7 | - 'dependabot/**' 8 | 9 | jobs: 10 | release-canary: 11 | name: npm 12 | if: ${{ github.repository == 'primer/stylelint-config' }} 13 | uses: primer/.github/.github/workflows/release_canary.yml@v2.2.0 14 | with: 15 | install: npm i 16 | secrets: 17 | gh_token: ${{ secrets.GITHUB_TOKEN }} 18 | npm_token: ${{ secrets.NPM_AUTH_TOKEN_SHARED }} 19 | -------------------------------------------------------------------------------- /.github/workflows/release_candidate.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - 'changeset-release/main' 6 | 7 | jobs: 8 | release-candidate: 9 | name: Candidate 10 | if: ${{ github.repository == 'primer/stylelint-config' }} 11 | 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | with: 17 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 18 | fetch-depth: 0 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v4 22 | 23 | - name: Install dependencies 24 | run: npm ci && npm run build 25 | 26 | - name: Create .npmrc 27 | run: | 28 | cat << EOF > "$HOME/.npmrc" 29 | //registry.npmjs.org/:_authToken=$NPM_TOKEN 30 | EOF 31 | env: 32 | NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN_SHARED }} 33 | 34 | - name: Publish release candidate 35 | run: | 36 | version=$(jq -r .version package.json) 37 | echo "$( jq ".version = \"$(echo $version)-rc.$(git rev-parse --short HEAD)\"" package.json )" > package.json 38 | npm publish --tag next 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Output candidate version number 43 | uses: actions/github-script@v7 44 | with: 45 | script: | 46 | const package = require(`${process.env.GITHUB_WORKSPACE}/package.json`) 47 | github.rest.repos.createCommitStatus({ 48 | owner: context.repo.owner, 49 | repo: context.repo.repo, 50 | sha: context.sha, 51 | state: 'success', 52 | context: `Published ${package.name}`, 53 | description: package.version, 54 | target_url: `https://unpkg.com/${package.name}@${package.version}/` 55 | }) 56 | -------------------------------------------------------------------------------- /.github/workflows/release_tracking.yml: -------------------------------------------------------------------------------- 1 | name: Release Event Tracking 2 | # Measure a datadog event every time a release occurs 3 | 4 | on: 5 | pull_request: 6 | types: 7 | - closed 8 | - opened 9 | - reopened 10 | 11 | release: 12 | types: [published] 13 | 14 | jobs: 15 | release-tracking: 16 | name: Release Tracking 17 | uses: primer/.github/.github/workflows/release_tracking.yml@v2.2.0 18 | secrets: 19 | datadog_api_key: ${{ secrets.DATADOG_API_KEY }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | coverage/ 3 | dist/ 4 | node_modules/ 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | *.yml 3 | .github 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | git-tag-version=false 2 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": ["./dist/index.cjs"], "ignoreFiles": ["**/*.js", "**/*.cjs", "**/*.ts", "**/*.mjs"], "cache": false } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 13.3.0 4 | 5 | ### Minor Changes 6 | 7 | - [#610](https://github.com/primer/stylelint-config/pull/610) [`baf4786`](https://github.com/primer/stylelint-config/commit/baf4786ad26112003edddb4f8856b3d9428c8a89) Thanks [@langermank](https://github.com/langermank)! - - Remove unused motion token file 8 | - Bump `primer/primitives` 10.4.0 9 | 10 | ### Patch Changes 11 | 12 | - [#600](https://github.com/primer/stylelint-config/pull/600) [`c172b55`](https://github.com/primer/stylelint-config/commit/c172b55d518cd723ff700c4381a149150fc4e833) Thanks [@dependabot](https://github.com/apps/dependabot)! - Bump the production-dependencies group across 1 directory with 2 updates 13 | 14 | ## 13.2.3 15 | 16 | ### Patch Changes 17 | 18 | - [#580](https://github.com/primer/stylelint-config/pull/580) [`56c028f`](https://github.com/primer/stylelint-config/commit/56c028f6d86a7305aa4be81fde7ce50735114838) Thanks [@dependabot](https://github.com/apps/dependabot)! - Bump the production-dependencies group across 1 directory with 4 updates 19 | 20 | - [#583](https://github.com/primer/stylelint-config/pull/583) [`2cab731`](https://github.com/primer/stylelint-config/commit/2cab731c2bf3a74617648ae02afcc96c3130de7f) Thanks [@jonrohan](https://github.com/jonrohan)! - Upgrade to stylelint@16.11.0 to avoid import change errors 21 | 22 | ## 13.2.2 23 | 24 | ### Patch Changes 25 | 26 | - [#539](https://github.com/primer/stylelint-config/pull/539) [`2067c41`](https://github.com/primer/stylelint-config/commit/2067c4166d799ea2dbc6e90c5a6cef27ac67817a) Thanks [@jonrohan](https://github.com/jonrohan)! - Making fixes for primitives v10 rc 27 | 28 | ## 13.2.1 29 | 30 | ### Patch Changes 31 | 32 | - [#526](https://github.com/primer/stylelint-config/pull/526) [`8672c88`](https://github.com/primer/stylelint-config/commit/8672c884c830caa93cbd59cf3d96baf30347b3a0) Thanks [@dependabot](https://github.com/apps/dependabot)! - Bump the production-dependencies group across 1 directory with 2 updates 33 | 34 | ## 13.2.0 35 | 36 | ### Minor Changes 37 | 38 | - [#510](https://github.com/primer/stylelint-config/pull/510) [`79f2330`](https://github.com/primer/stylelint-config/commit/79f233081b8a4af3065199395ee3297b73bcaf8b) Thanks [@langermank](https://github.com/langermank)! - Update `primer/primitives` dependency 39 | 40 | ### Patch Changes 41 | 42 | - [#520](https://github.com/primer/stylelint-config/pull/520) [`ad6fafd`](https://github.com/primer/stylelint-config/commit/ad6fafd7072dd05ab47ce9aab949ec0653ffe9fa) Thanks [@iansan5653](https://github.com/iansan5653)! - Remove comment-empty-line-before and order/properties-order rules 43 | 44 | ## 13.1.1 45 | 46 | ### Patch Changes 47 | 48 | - [#500](https://github.com/primer/stylelint-config/pull/500) [`e0d5847`](https://github.com/primer/stylelint-config/commit/e0d5847e84a364140815a80fa22c59852c966d79) Thanks [@jonrohan](https://github.com/jonrohan)! - Removing no-unsupported-browser-features and replacing with browser-compat 49 | 50 | ## 13.1.0 51 | 52 | ### Minor Changes 53 | 54 | - [#491](https://github.com/primer/stylelint-config/pull/491) [`a615645`](https://github.com/primer/stylelint-config/commit/a615645054778a596e918909ddb0931d018585be) Thanks [@jonrohan](https://github.com/jonrohan)! - Refactor the primer/colors variable to use primitives values match up props 55 | 56 | ### Patch Changes 57 | 58 | - [#482](https://github.com/primer/stylelint-config/pull/482) [`b4c3fb0`](https://github.com/primer/stylelint-config/commit/b4c3fb010bf8cb84be54cdcaa73964249ab23053) Thanks [@dependabot](https://github.com/apps/dependabot)! - Bump stylelint-scss from 6.6.0 to 6.7.0 in the production-dependencies group 59 | 60 | ## 13.0.1 61 | 62 | ### Patch Changes 63 | 64 | - [#479](https://github.com/primer/stylelint-config/pull/479) [`930e5f2`](https://github.com/primer/stylelint-config/commit/930e5f24bc01b7eebc07b085689314eea5f8e1c5) Thanks [@jonrohan](https://github.com/jonrohan)! - Stylelint fixes based on feedback: 65 | 66 | - `font-style` should allow keywords, `italic, normal` 67 | - border should allow `none` https://stylelint.io/user-guide/rules/declaration-property-value-disallowed-list 68 | - Update autofix in typography to always replace with the first suggestion 69 | 70 | ## 13.0.0 71 | 72 | ### Major Changes 73 | 74 | - [#397](https://github.com/primer/stylelint-config/pull/397) [`255a3c4`](https://github.com/primer/stylelint-config/commit/255a3c4cebb453243978b24f35516ed55443c81e) Thanks [@jonrohan](https://github.com/jonrohan)! - Removing `primer/no-experimental-vars` plugin from config. 75 | 76 | - [#415](https://github.com/primer/stylelint-config/pull/415) [`86cf24f`](https://github.com/primer/stylelint-config/commit/86cf24f5c1f3f51b5085a5808a5406bc04e47b68) Thanks [@jonrohan](https://github.com/jonrohan)! - Deleting primer/utilities plugin 77 | 78 | - [#401](https://github.com/primer/stylelint-config/pull/401) [`0a7bc7e`](https://github.com/primer/stylelint-config/commit/0a7bc7eeaec4b6ea63cbc7bda150ea61a3b5d346) Thanks [@jonrohan](https://github.com/jonrohan)! - **BREAKING CHANGE:** Removing plugins from the config. 79 | 80 | - primer/new-color-vars-have-fallback 81 | - primer/no-deprecated-colors 82 | - primer/no-override 83 | - primer/no-scale-colors 84 | - primer/no-undefined-vars 85 | - primer/no-unused-vars 86 | 87 | - [#397](https://github.com/primer/stylelint-config/pull/397) [`255a3c4`](https://github.com/primer/stylelint-config/commit/255a3c4cebb453243978b24f35516ed55443c81e) Thanks [@jonrohan](https://github.com/jonrohan)! - Upgrade to latest stylelint and make esm the default module format 88 | 89 | ### Minor Changes 90 | 91 | - [#429](https://github.com/primer/stylelint-config/pull/429) [`6d80a4d`](https://github.com/primer/stylelint-config/commit/6d80a4def84a803d078e7c425ece4761c0dcbce6) Thanks [@mperrotti](https://github.com/mperrotti)! - Rewrite box-shadow lint plugin for css vars. 92 | 93 | - [#403](https://github.com/primer/stylelint-config/pull/403) [`2c9e2de`](https://github.com/primer/stylelint-config/commit/2c9e2de5ea64754587109098352fef80718b9b30) Thanks [@mattcosta7](https://github.com/mattcosta7)! - Update config to alloy nesting in css modules 94 | 95 | - [#417](https://github.com/primer/stylelint-config/pull/417) [`3318d25`](https://github.com/primer/stylelint-config/commit/3318d25f78f5a2a25dbe9aec077145c2a33db3af) Thanks [@jonrohan](https://github.com/jonrohan)! - Upgrade to @primer/primitives@8.2.0 96 | 97 | - [#368](https://github.com/primer/stylelint-config/pull/368) [`0ed9a47`](https://github.com/primer/stylelint-config/commit/0ed9a4723cc1c5a1d7bbd2a36da064e76634d1b9) Thanks [@jonrohan](https://github.com/jonrohan)! - Change config to accept multiple file types `.css, .scss, .modules.css, .tsx, .pcss` 98 | 99 | - [#400](https://github.com/primer/stylelint-config/pull/400) [`e708ed2`](https://github.com/primer/stylelint-config/commit/e708ed2ad2789e2acabcb8cb64eb99735ea87d17) Thanks [@jonrohan](https://github.com/jonrohan)! - Update primer/spacing for CSS properties 100 | 101 | - [`4c22d43`](https://github.com/primer/stylelint-config/commit/4c22d43342857e282802f242445e25f562001a79) Thanks [@jonrohan](https://github.com/jonrohan)! - Updating `primer/borders` plugin to work for new primitives and CSS vars 102 | 103 | ## 12.9.2 104 | 105 | ### Patch Changes 106 | 107 | - [#388](https://github.com/primer/stylelint-config/pull/388) [`43b1066`](https://github.com/primer/stylelint-config/commit/43b10662c9f48837069690751f42eed1359c7372) Thanks [@langermank](https://github.com/langermank)! - New rule: safegaurd alpha `display` color tokens 108 | 109 | ## 12.9.1 110 | 111 | ### Patch Changes 112 | 113 | - [#382](https://github.com/primer/stylelint-config/pull/382) [`2cbe3be`](https://github.com/primer/stylelint-config/commit/2cbe3be3cecb4d7e0a9d0ad2be32ebcaceb063a5) Thanks [@langermank](https://github.com/langermank)! - Update `new-color-css-vars` to exclude scale colors 114 | 115 | ## 12.9.0 116 | 117 | ### Minor Changes 118 | 119 | - [#376](https://github.com/primer/stylelint-config/pull/376) [`a31e0d3`](https://github.com/primer/stylelint-config/commit/a31e0d392cf73c623ae8a8cf957796ede4386e00) Thanks [@langermank](https://github.com/langermank)! - Adds new plugin: `new-color-vars-have-fallback` to check that if new Primitive v8 colors are used, they have a fallback value. 120 | 121 | ## 12.8.0 122 | 123 | ### Minor Changes 124 | 125 | - [#356](https://github.com/primer/stylelint-config/pull/356) [`fdf5660`](https://github.com/primer/stylelint-config/commit/fdf566018fea555b6b6b0d2cfe1c6e88d8746a07) Thanks [@jonrohan](https://github.com/jonrohan)! - Upgrading to stylelint 15.10.2 126 | 127 | ### Patch Changes 128 | 129 | - [#353](https://github.com/primer/stylelint-config/pull/353) [`cdb7ca9`](https://github.com/primer/stylelint-config/commit/cdb7ca90d4c38e429f24db92bf07578ad44d4032) Thanks [@langermank](https://github.com/langermank)! - Add `bgColor-inset` fallback for Primitives v8 130 | 131 | ## 12.7.2 132 | 133 | ### Patch Changes 134 | 135 | - [#343](https://github.com/primer/stylelint-config/pull/343) [`5b975fc`](https://github.com/primer/stylelint-config/commit/5b975fcd45383ecd1dd9145d868a227e4fe3e27a) Thanks [@langermank](https://github.com/langermank)! - Add missing counter btn tokens to no-deprecated-colors 136 | 137 | ## 12.7.1 138 | 139 | ### Patch Changes 140 | 141 | - [#338](https://github.com/primer/stylelint-config/pull/338) [`7cc4c08`](https://github.com/primer/stylelint-config/commit/7cc4c08f6465f495df89fc0609d3cdf012480dec) Thanks [@langermank](https://github.com/langermank)! - Add more tests to `no-deprecated-colors` 142 | 143 | - [#328](https://github.com/primer/stylelint-config/pull/328) [`5ae7400`](https://github.com/primer/stylelint-config/commit/5ae7400340afc2cd21006093ce7b33206a00372e) Thanks [@langermank](https://github.com/langermank)! - Fix failing tests in no-deprecated-colors 144 | 145 | - [#337](https://github.com/primer/stylelint-config/pull/337) [`6bf0fd6`](https://github.com/primer/stylelint-config/commit/6bf0fd624a69b21e48803ba62a5b2b9dc21b8d8c) Thanks [@langermank](https://github.com/langermank)! - Remove inline fallback var for no-deprecated-colors 146 | 147 | - [#332](https://github.com/primer/stylelint-config/pull/332) [`6485cf0`](https://github.com/primer/stylelint-config/commit/6485cf053f502d71a8ce8c407ad01a939038959c) Thanks [@langermank](https://github.com/langermank)! - Updated deprecated json color file 148 | 149 | - [#333](https://github.com/primer/stylelint-config/pull/333) [`485ce04`](https://github.com/primer/stylelint-config/commit/485ce047d75a635134919678a776ea808604cf4a) Thanks [@langermank](https://github.com/langermank)! - Adding more color replacements for deprecated colors 150 | 151 | - [#340](https://github.com/primer/stylelint-config/pull/340) [`4688bb4`](https://github.com/primer/stylelint-config/commit/4688bb4c0ea7975672b76af8706b80278f00f1a4) Thanks [@langermank](https://github.com/langermank)! - add inlineFallback prop to no-deprecated-colors 152 | 153 | - [#322](https://github.com/primer/stylelint-config/pull/322) [`726d7d1`](https://github.com/primer/stylelint-config/commit/726d7d1bf4eac82a032c448cb0be32d5bf66b29a) Thanks [@jonrohan](https://github.com/jonrohan)! - Updating no-deprecated-colors for primitives v8 154 | 155 | - [#334](https://github.com/primer/stylelint-config/pull/334) [`b14c154`](https://github.com/primer/stylelint-config/commit/b14c154174ddd7190e62fe1d26698fc9cfe75c17) Thanks [@langermank](https://github.com/langermank)! - More tests for `no-deprecated-colors` 156 | 157 | - [#339](https://github.com/primer/stylelint-config/pull/339) [`36fade4`](https://github.com/primer/stylelint-config/commit/36fade45bdc431d223165f5d7226c10cf6591d83) Thanks [@langermank](https://github.com/langermank)! - Update plugins to support Primitives v8 158 | 159 | ## 12.7.0 160 | 161 | ### Minor Changes 162 | 163 | - [#294](https://github.com/primer/stylelint-config/pull/294) [`8bdb1d0`](https://github.com/primer/stylelint-config/commit/8bdb1d0a679c32a1782e33feb74bd8993aba5d80) Thanks [@keithamus](https://github.com/keithamus)! - allow for vars defined in scope, or within :root/:host selectors in file 164 | 165 | ## 12.6.1 166 | 167 | ### Patch Changes 168 | 169 | - [#274](https://github.com/primer/stylelint-config/pull/274) [`4ba7018`](https://github.com/primer/stylelint-config/commit/4ba701887351664d8b937483d3d761fa5022f16c) Thanks [@jonrohan](https://github.com/jonrohan)! - Fixing issue in utilities plugin that missed certain classes 170 | 171 | ## 12.6.0 172 | 173 | ### Minor Changes 174 | 175 | - [#272](https://github.com/primer/stylelint-config/pull/272) [`9104062`](https://github.com/primer/stylelint-config/commit/91040626d2195cbb63f1e302ae53acdd4ba5b361) Thanks [@langermank](https://github.com/langermank)! - Add no-experimental-vars plugin 176 | 177 | ## 12.5.0 178 | 179 | ### Minor Changes 180 | 181 | - [#262](https://github.com/primer/stylelint-config/pull/262) [`28a4086`](https://github.com/primer/stylelint-config/commit/28a4086e8c781f76494c7e77b9437046a6f686a6) Thanks [@jonrohan](https://github.com/jonrohan)! - Writing a primer/utilities plugin to look for code that duplicates utilities 182 | 183 | ## 12.4.2 184 | 185 | ### Patch Changes 186 | 187 | - [#258](https://github.com/primer/stylelint-config/pull/258) [`fa48eed`](https://github.com/primer/stylelint-config/commit/fa48eed1af84474fa49bdb7ec861d2c6a3210239) Thanks [@jonrohan](https://github.com/jonrohan)! - Fixing dependencies 188 | 189 | * [#260](https://github.com/primer/stylelint-config/pull/260) [`4f42328`](https://github.com/primer/stylelint-config/commit/4f4232826cd3055e0e9dc49ff16925c47db21863) Thanks [@jonrohan](https://github.com/jonrohan)! - Turning off 'function-no-unknown': null, 190 | 191 | ## 12.4.1 192 | 193 | ### Patch Changes 194 | 195 | - [#256](https://github.com/primer/stylelint-config/pull/256) [`37eb1cb`](https://github.com/primer/stylelint-config/commit/37eb1cbd342590f4c43e37779f657a4b19594eca) Thanks [@jonrohan](https://github.com/jonrohan)! - Remove stylelint peer 196 | 197 | ## 12.4.0 198 | 199 | ### Minor Changes 200 | 201 | - [#232](https://github.com/primer/stylelint-config/pull/232) [`27ddfc9`](https://github.com/primer/stylelint-config/commit/27ddfc98f93ed898552bb62aa0926d35497dda72) Thanks [@jonrohan](https://github.com/jonrohan)! - Creating a responsive-widths plugin to keep fixed widths smaller than the minimum viewport size 202 | 203 | * [#253](https://github.com/primer/stylelint-config/pull/253) [`0edeee0`](https://github.com/primer/stylelint-config/commit/0edeee07b1e7ef51bcd0942c65d98131ac384887) Thanks [@jonrohan](https://github.com/jonrohan)! - Changing this peerDependency to be any 204 | 205 | ## 12.3.3 206 | 207 | ### Patch Changes 208 | 209 | - [#218](https://github.com/primer/stylelint-config/pull/218) [`c03be7d`](https://github.com/primer/stylelint-config/commit/c03be7da1126123c079d86e00a2158a913e015f8) Thanks [@jonrohan](https://github.com/jonrohan)! - [Bug fix] Catching values with dots in them 210 | 211 | * [#217](https://github.com/primer/stylelint-config/pull/217) [`5bb2834`](https://github.com/primer/stylelint-config/commit/5bb28342a6194dfdd4fbf5197682367ea54792b7) Thanks [@jsoref](https://github.com/jsoref)! - Spelling fixes 212 | 213 | ## 12.3.2 214 | 215 | ### Patch Changes 216 | 217 | - [#215](https://github.com/primer/stylelint-config/pull/215) [`66b16ae`](https://github.com/primer/stylelint-config/commit/66b16ae2edd81f8c8949f83c96d7011e5d395cc0) Thanks [@jonrohan](https://github.com/jonrohan)! - Making linter pick up separate function groups 218 | 219 | ## 12.3.1 220 | 221 | ### Patch Changes 222 | 223 | - [#213](https://github.com/primer/stylelint-config/pull/213) [`2a27f86`](https://github.com/primer/stylelint-config/commit/2a27f86868b1f4717100a1f0897cdaefb1dd6be7) Thanks [@jonrohan](https://github.com/jonrohan)! - Fixing an issue where the new spacing plugin isn't traversing child sectors. 224 | 225 | ## 12.3.0 226 | 227 | ### Minor Changes 228 | 229 | - [#191](https://github.com/primer/stylelint-config/pull/191) [`71c7985`](https://github.com/primer/stylelint-config/commit/71c79853b679b674c1d27686f8d2168660b24a45) Thanks [@jonrohan](https://github.com/jonrohan)! - Refactoring the primer/spacing plugin to better match results 230 | 231 | ## 12.2.0 232 | 233 | ### Minor Changes 234 | 235 | - [#170](https://github.com/primer/stylelint-config/pull/170) [`b56fcd1`](https://github.com/primer/stylelint-config/commit/b56fcd1bce90d2e3e1621ef7af7545c52c935579) Thanks [@jonrohan](https://github.com/jonrohan)! - Moving config from primer/css's [stylelint.config.cjs](https://github.com/primer/css/blob/c65be7f0c8b0fb6e1ba406b5d35c6073df161a33/stylelint.config.cjs) file to this package. 236 | 237 | * [#168](https://github.com/primer/stylelint-config/pull/168) [`d6ff2b9`](https://github.com/primer/stylelint-config/commit/d6ff2b94ff0d309c1b79e783e6ee1b2f87a375ff) Thanks [@jonrohan](https://github.com/jonrohan)! - Extending stylelint-config-standard and removing defaults 238 | 239 | - [#170](https://github.com/primer/stylelint-config/pull/170) [`b56fcd1`](https://github.com/primer/stylelint-config/commit/b56fcd1bce90d2e3e1621ef7af7545c52c935579) Thanks [@jonrohan](https://github.com/jonrohan)! - Adding config from the [stylelint-scss recommended config](https://github.com/stylelint-scss/stylelint-config-recommended-scss/blob/82d51c399ddaa2f9d282e419399dd2028f47830c/index.js). 240 | 241 | ### Patch Changes 242 | 243 | - [#181](https://github.com/primer/stylelint-config/pull/181) [`23e438a`](https://github.com/primer/stylelint-config/commit/23e438a7a9062550baa696cafbb186dc78b723f5) Thanks [@jonrohan](https://github.com/jonrohan)! - Turning off scss/dollar-variable-default 244 | 245 | ## 12.1.1 246 | 247 | ### Patch Changes 248 | 249 | - [#161](https://github.com/primer/stylelint-config/pull/161) [`48c4afc`](https://github.com/primer/stylelint-config/commit/48c4afc1913863136d62967653697323f8da57b7) Thanks [@dependabot](https://github.com/apps/dependabot)! - Bump @primer/primitives from 6.1.0 to 7.0.1 250 | 251 | ## 12.1.0 252 | 253 | ### Minor Changes 254 | 255 | - [#150](https://github.com/primer/stylelint-config/pull/150) [`4af1647`](https://github.com/primer/stylelint-config/commit/4af16474148d96fba5567068280a9ffe6e7a80ba) Thanks [@jonrohan](https://github.com/jonrohan)! - Making all be first in property order 256 | 257 | * [#151](https://github.com/primer/stylelint-config/pull/151) [`d7c8b2b`](https://github.com/primer/stylelint-config/commit/d7c8b2b908b113fa14c7637dfced34610a3bcfac) Thanks [@jonrohan](https://github.com/jonrohan)! - Adding [string-quotes](https://stylelint.io/user-guide/rules/list/string-quotes) rule to config 258 | 259 | ### Patch Changes 260 | 261 | - [#146](https://github.com/primer/stylelint-config/pull/146) [`214362c`](https://github.com/primer/stylelint-config/commit/214362c0e3c9449a5ff7d3bd047018493043d3c0) Thanks [@dependabot](https://github.com/apps/dependabot)! - Bump @primer/css from 13.2.0 to 16.3.0 262 | 263 | ## 12.0.1 264 | 265 | ### Patch Changes 266 | 267 | - [#132](https://github.com/primer/stylelint-config/pull/132) [`b672367`](https://github.com/primer/stylelint-config/commit/b6723679606bb8d39e75025ae17ace9f1c3e2631) Thanks [@jonrohan](https://github.com/jonrohan)! - Updating no-deprecated-colors plugin for edge cases 268 | 269 | ## 12.0.0 270 | 271 | ### Major Changes 272 | 273 | - [#129](https://github.com/primer/stylelint-config/pull/129) [`653d596`](https://github.com/primer/stylelint-config/commit/653d596072b897b265b093aac4cd5c143e61410e) Thanks [@jonrohan](https://github.com/jonrohan)! - Renaming the package to use org scope. This is a breaking change, you'll need to uninstall `stylelint-config-primer` and reinstall `@primer/stylelint-config`. 274 | 275 | ### Patch Changes 276 | 277 | - [#130](https://github.com/primer/stylelint-config/pull/130) [`f495a56`](https://github.com/primer/stylelint-config/commit/f495a563a9e809252630466088eb94177e6c0be4) Thanks [@jonrohan](https://github.com/jonrohan)! - Updating @primer/primitives to 5.0 release candidate 278 | 279 | ## 11.1.1 280 | 281 | ### Patch Changes 282 | 283 | - [`3a4654b`](https://github.com/primer/stylelint-config/commit/3a4654b1b7920d71e1284ff78a3bedff932e66a3) [#111](https://github.com/primer/stylelint-config/pull/111) Thanks [@jonrohan](https://github.com/jonrohan)! - Fixing the primer/colors and primer/borders rules 284 | 285 | ## 11.1.0 286 | 287 | ### Minor Changes 288 | 289 | - [`e83f61c`](https://github.com/primer/stylelint-config/commit/e83f61cef3bf1df1d9420662594040efdcb86c89) [#99](https://github.com/primer/stylelint-config/pull/99) Thanks [@jonrohan](https://github.com/jonrohan)! - Create a `no-deprecated-colors` rule that looks for deprecated css color variables from primer/primitives. 290 | 291 | ### Patch Changes 292 | 293 | - [`581f40a`](https://github.com/primer/stylelint-config/commit/581f40a4aacb45db5426b82d4a2434e81eb032e2) [#105](https://github.com/primer/stylelint-config/pull/105) Thanks [@jonrohan](https://github.com/jonrohan)! - Adding reporting to the linter to know how many variables are replaced 294 | 295 | ## 10.0.1 296 | 297 | ### Patch Changes 298 | 299 | - [`aa76171`](https://github.com/primer/stylelint-config/commit/aa76171fc5c9c308fcd9d7f7285c8fbdb2c18a7b) [#90](https://github.com/primer/stylelint-config/pull/90) Thanks [@jonrohan](https://github.com/jonrohan)! - Updating the no-undefined-variables lint for the new color-variables mixin. 300 | 301 | ## 10.0.0 302 | 303 | ### Major Changes 304 | 305 | - [`23a1f15`](https://github.com/primer/stylelint-config/commit/23a1f1599673f2a4f9f28c39da61f42871c05697) [#85](https://github.com/primer/stylelint-config/pull/85) Thanks [@koddsson](https://github.com/koddsson)! - Replace deprecated "blacklist" rules for "disallow list" rules. 306 | 307 | See https://stylelint.io/user-guide/rules/at-rule-blacklist and http://stylelint.io/user-guide/rules/declaration-property-value-disallowed-list/ 308 | 309 | ### Patch Changes 310 | 311 | - [`40d9bb8`](https://github.com/primer/stylelint-config/commit/40d9bb867194ae4335846953b5d8706dc7dc7d79) [#86](https://github.com/primer/stylelint-config/pull/86) Thanks [@koddsson](https://github.com/koddsson)! - Allow rules to optionally display a URL with their message. 312 | 313 | ## 9.3.3 314 | 315 | ### Patch Changes 316 | 317 | - [`a339c69`](https://github.com/primer/stylelint-config/commit/a339c698b9ba7ccd01b8cb773dad7a3a14dd13a1) [#81](https://github.com/primer/stylelint-config/pull/81) Thanks [@BinaryMuse](https://github.com/BinaryMuse)! - Update globby to v11 318 | 319 | ## 9.3.2 320 | 321 | ### Patch Changes 322 | 323 | - [`d18cfbf`](https://github.com/primer/stylelint-config/commit/d18cfbfefc25be6ae38f73132552d2f3c62c4d02) [#79](https://github.com/primer/stylelint-config/pull/79) Thanks [@BinaryMuse](https://github.com/BinaryMuse)! - Add additional verbose logging to `no-undefined-vars` 324 | 325 | * [`d18cfbf`](https://github.com/primer/stylelint-config/commit/d18cfbfefc25be6ae38f73132552d2f3c62c4d02) [#79](https://github.com/primer/stylelint-config/pull/79) Thanks [@BinaryMuse](https://github.com/BinaryMuse)! - Fix handling of edge-cases in `no-undefined-vars` 326 | 327 | - [`bb07673`](https://github.com/primer/stylelint-config/commit/bb076732aa216fcb56e411b8dd7477efc89f7f8a) [#76](https://github.com/primer/stylelint-config/pull/76) Thanks [@BinaryMuse](https://github.com/BinaryMuse)! - Set the default verbose option for `no-scale-colors` to false 328 | 329 | ## 9.3.1 330 | 331 | ### Patch Changes 332 | 333 | - [`df11e2d`](https://github.com/primer/stylelint-config/commit/df11e2d912913346e0499f7eac901cdfcb83f38c) [#74](https://github.com/primer/stylelint-config/pull/74) Thanks [@BinaryMuse](https://github.com/BinaryMuse)! - Add primer/no-scale-colors to the list of exported plugins 334 | 335 | ## 9.2.1 336 | 337 | ### :bug: Bug fixes 338 | 339 | - Fix slow runtime by caching variable definitions in `primer/no-undefined-vars` rule 340 | - Fix duplicate errors in `primer/no-undefined-vars` rule 341 | 342 | ## 9.2.0 343 | 344 | ### :rocket: Enhancements 345 | 346 | - New `primer/no-undefined-vars` to prohibit usages of undefined CSS variables 347 | 348 | ## 9.1.0 349 | 350 | ### :rocket: Enhancements 351 | 352 | - The `primer/colors`, `primer/borders`, and `primer/box-shadow` rules now allow CSS color variables with the correct functional names (e.g. `var(--color-text-primary)`). #62 353 | 354 | ## 9.0.0 355 | 356 | ### :boom: Breaking Change 357 | 358 | - `primer/variables` is no longer supported; please use the `primer/colors`, `primer/borders`, `primer/box-shadow`, `primer/spacing`, and `primer/typography` rules instead. #50 359 | 360 | ### :rocket: Enhancements 361 | 362 | - The new `primer/colors` rule enforces color variable usage in `color`, `background-color`, and `fill` properties 363 | - The new `primer/borders` rule enforces border variable usage in border CSS props 364 | - The new `primer/box-shadow` rule enforces `$box-shadow*` variables 365 | - The new `primer/spacing` rule enforces `$spacer-*` variables in margin and padding props 366 | - The new `primer/typography` rule enforces typography variable use in `font-family`, `line-height`, and `font-weight` props 367 | - Variable replacements for autofixing are automatically detected in variable data from Primer CSS (see: https://github.com/primer/css/pull/949) #52 368 | - It is now possible to define variable rules using functions that take the variables, as in: 369 | ```js 370 | module.exports = createVariableRule('primer/whatever', ({variables}) => { 371 | /* do something with variables here */ 372 | }) 373 | ``` 374 | - It's also now possible to provide rule _overrides_ in local stylelint configs as functions: 375 | ```js 376 | module.exports = { 377 | extends: '@primer/stylelint-config', 378 | rules: { 379 | 'primer/colors': [true, { 380 | rules: ({variables, rules}) => { 381 | /* do something with variables and/or rules here */ 382 | return rules 383 | }] 384 | } 385 | }) 386 | ``` 387 | - This release adds support for an optional `singular: true` flag to rule configs, which skips the parsing of individual values in the matched properties. We use this in `primer/box-shadow` to prevent multiple warnings for a single value like `box-shadow: inset 0 1px $blue` (before there would be 4 separate ones!). 388 | 389 | ### :bug: Bug fixes 390 | 391 | - Use `requirePrimerFile()` when loading `dist/variables.json` so that we can access the right file when running _within_ the `@primer/css` repo. 392 | - Walk only declarations (`prop: value`) in rules (blocks with selectors, _not_ `@rules`), and skip linting for declarations nested in `@each`, `@for`, `@function`, and `@mixin` blocks, since those can define their own variables and we can't reliably assert their values. 393 | - Allow `$*-shadow` variable patterns in `primer/box-shadow` to match `$btn-active-shadow` and `$form-control-shadow` 394 | - Allow `color: inherit` in `primer/colors` 395 | - Allow `$em-spacer-*` in `padding` and `margin` properties 396 | - Allow (and auto-fix!) negative spacer variables in `margin` properties 397 | - Make `primer/colors` smarter re: `background` property shorthand values (allowing positions and image `url(*)` values) 398 | - Remove `100%` from allowed values for `border-radius`, and suggest `50%` instead 399 | - Prohibit negative spacer values in `padding` properties 400 | - Allow `$h000-size` for marketing 😬 401 | 402 | ## 2.0.0 403 | 404 | :boom: **The following updates are breaking changes**, since code that disables the deprecated rule will now produce linting errors. Please update your `stylelint-disable` statements accordingly. 405 | 406 | - Replaced `selector-no-id: true` with `selector-max-id: 0` 407 | - Replaced `selector-no-type: true` with `selector-max-type: 0` 408 | 409 | The rest of the changes should not introduce new linting errors: 410 | 411 | - Updated: moved [browserslist](https://github.com/ai/browserslist) spec to `package.json` 412 | - Updated: using the [`no-unsupported-browser-features` plugin](https://github.com/ismay/stylelint-no-unsupported-browser-features) instead of the deprecated `no-unsupported-browser-features` rule 413 | - Removed: `media-feature-no-missing-punctuation` 414 | - Updated: replaced `rule-nested-empty-line-before` and `rule-non-nested-empty-line-before` with `rule-empty-line-before` 415 | 416 | ## 1.4.0 417 | 418 | - Updated: Development dependencies are any version `*` 419 | - Removed: `selector-class-pattern` from config. https://github.com/primer/stylelint-config/pull/11 420 | 421 | ## 1.3.0 422 | 423 | - Added: `length-zero-no-unit` to disallow zero values with units eg `0px` 424 | 425 | ## 1.2.0 426 | 427 | - Removed: We don't need `scss/at-extend-no-missing-placeholder` anymore taken care of by `at-rule-blacklist` 428 | - Added: Adding `selector-no-type` to the rules 429 | 430 | ## 1.0.0 431 | 432 | - Creating a sharable config object 433 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 GitHub Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Primer Stylelint Config 2 | 3 | [![npm version](https://img.shields.io/npm/v/@primer/stylelint-config.svg)](https://www.npmjs.org/package/@primer/stylelint-config) 4 | 5 | > A sharable stylelint config object that enforces GitHub's CSS rules 6 | 7 | ## Install 8 | 9 | ``` 10 | $ npm install --save --dev @primer/stylelint-config 11 | ``` 12 | 13 | ## Usage 14 | 15 | Within your [stylelint config object](http://stylelint.io/user-guide/configuration/#extends) You can extend this configuration. This will serve as a base for your config, then you can make overrides in your own config object: 16 | 17 | ```json 18 | { 19 | "extends": ["@primer/stylelint-config"], 20 | "rules": { } 21 | } 22 | ``` 23 | 24 | ## Documentation 25 | 26 | Primer Stylelint Config extends the [stylelint-config-standard](https://github.com/stylelint/stylelint-config-standard) configuration supplied by Stylelint, and makes modifications in `/index.js`. 27 | 28 | ### Plugins 29 | 30 | - [stylelint-order](https://github.com/hudochenkov/stylelint-order): Order-related linting rules for stylelint. Properties must be [sorted according to this list](https://github.com/primer/stylelint-config/blob/main/property-order.js). 31 | - [stylelint-scss](https://github.com/kristerkari/stylelint-scss): A collection of SCSS specific linting rules for stylelint 32 | - [scss/selector-no-redundant-nesting-selector](https://github.com/kristerkari/stylelint-scss/blob/master/src/rules/selector-no-redundant-nesting-selector/README.md): Disallow redundant nesting selectors (`&`). 33 | - [primer/colors](./plugins/#primercolors): Enforces the use of certain color variables. 34 | - [primer/spacing](./plugins/#primerspacing): Enforces the use of spacing variables for margin and padding. 35 | - [primer/typography](./plugins/#primertypography): Enforces the use of typography variables for certain CSS properties. 36 | - [primer/borders](./plugins/#primerborders): Enforces the use of certain variables for border properties. 37 | - [primer/box-shadow](./plugins/#primerbox-shadow): Enforces the use of certain variables for `box-shadow`. 38 | - [primer/responsive-widths](./plugins/#primerresponsive-widths): Errors on `width` and `min-width` that is larger than the minimum browser size supported. `320px` 39 | 40 | ## License 41 | 42 | [MIT](./LICENSE) © [GitHub](https://github.com/) 43 | -------------------------------------------------------------------------------- /__tests__/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "rules": { 6 | "no-undef": "off", 7 | "import/no-named-as-default-member": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/color-vars.scss: -------------------------------------------------------------------------------- 1 | @mixin primer-colors-light($sel) { 2 | #{$sel} { 3 | --color-scale-black: #1b1f23; 4 | --color-scale-white: #ffffff; 5 | --color-scale-gray-0: #fafbfc; 6 | --color-scale-gray-1: #f6f8fa; 7 | --color-scale-gray-2: #e1e4e8; 8 | --color-scale-gray-3: #d1d5da; 9 | --color-scale-gray-4: #959da5; 10 | --color-scale-gray-5: #6a737d; 11 | --color-scale-gray-6: #586069; 12 | --color-scale-gray-7: #444d56; 13 | --color-scale-gray-8: #2f363d; 14 | --color-scale-gray-9: #24292e; 15 | --color-scale-blue-0: #f1f8ff; 16 | --color-scale-blue-1: #dbedff; 17 | --color-scale-blue-2: #c8e1ff; 18 | --color-scale-blue-3: #79b8ff; 19 | --color-scale-blue-4: #2188ff; 20 | --color-scale-blue-5: #0366d6; 21 | --color-scale-blue-6: #005cc5; 22 | --color-scale-blue-7: #044289; 23 | --color-scale-blue-8: #032f62; 24 | --color-scale-blue-9: #05264c; 25 | --color-scale-green-0: #f0fff4; 26 | --color-scale-green-1: #dcffe4; 27 | --color-scale-green-2: #bef5cb; 28 | --color-scale-green-3: #85e89d; 29 | --color-scale-green-4: #34d058; 30 | --color-scale-green-5: #28a745; 31 | --color-scale-green-6: #22863a; 32 | --color-scale-green-7: #176f2c; 33 | --color-scale-green-8: #165c26; 34 | --color-scale-green-9: #144620; 35 | --color-scale-yellow-0: #fffdef; 36 | --color-scale-yellow-1: #fffbdd; 37 | --color-scale-yellow-2: #fff5b1; 38 | --color-scale-yellow-3: #ffea7f; 39 | --color-scale-yellow-4: #ffdf5d; 40 | --color-scale-yellow-5: #ffd33d; 41 | --color-scale-yellow-6: #f9c513; 42 | --color-scale-yellow-7: #dbab09; 43 | --color-scale-yellow-8: #b08800; 44 | --color-scale-yellow-9: #735c0f; 45 | --color-scale-orange-0: #fff8f2; 46 | --color-scale-orange-1: #ffebda; 47 | --color-scale-orange-2: #ffd1ac; 48 | --color-scale-orange-3: #ffab70; 49 | --color-scale-orange-4: #fb8532; 50 | --color-scale-orange-5: #f66a0a; 51 | --color-scale-orange-6: #e36209; 52 | --color-scale-orange-7: #d15704; 53 | --color-scale-orange-8: #c24e00; 54 | --color-scale-orange-9: #a04100; 55 | --color-scale-red-0: #ffeef0; 56 | --color-scale-red-1: #ffdce0; 57 | --color-scale-red-2: #fdaeb7; 58 | --color-scale-red-3: #f97583; 59 | --color-scale-red-4: #ea4a5a; 60 | --color-scale-red-5: #d73a49; 61 | --color-scale-red-6: #cb2431; 62 | --color-scale-red-7: #b31d28; 63 | --color-scale-red-8: #9e1c23; 64 | --color-scale-red-9: #86181d; 65 | --color-scale-purple-0: #f5f0ff; 66 | --color-scale-purple-1: #e6dcfd; 67 | --color-scale-purple-2: #d1bcf9; 68 | --color-scale-purple-3: #b392f0; 69 | --color-scale-purple-4: #8a63d2; 70 | --color-scale-purple-5: #6f42c1; 71 | --color-scale-purple-6: #5a32a3; 72 | --color-scale-purple-7: #4c2889; 73 | --color-scale-purple-8: #3a1d6e; 74 | --color-scale-purple-9: #29134e; 75 | --color-scale-pink-0: #ffeef8; 76 | --color-scale-pink-1: #fedbf0; 77 | --color-scale-pink-2: #f9b3dd; 78 | --color-scale-pink-3: #f692ce; 79 | --color-scale-pink-4: #ec6cb9; 80 | --color-scale-pink-5: #ea4aaa; 81 | --color-scale-pink-6: #d03592; 82 | --color-scale-pink-7: #b93a86; 83 | --color-scale-pink-8: #99306f; 84 | --color-scale-pink-9: #6d224f; 85 | --color-text-primary: #24292e; 86 | --color-text-secondary: #586069; 87 | --color-text-tertiary: #6a737d; 88 | --color-text-placeholder: #d1d5da; 89 | --color-text-disabled: #d1d5da; 90 | --color-text-inverse: #ffffff; 91 | --color-text-link-primary: #0366d6; 92 | --color-text-link-secondary: #24292e; 93 | --color-text-link-tertiary: #586069; 94 | --color-text-danger: #d73a49; 95 | --color-text-success: #22863a; 96 | --color-text-warning: #b08800; 97 | --color-icon-primary: #24292e; 98 | --color-icon-secondary: #586069; 99 | --color-icon-tertiary: #959da5; 100 | --color-icon-info: #0366d6; 101 | --color-icon-danger: #d73a49; 102 | --color-icon-success: #22863a; 103 | --color-icon-warning: #b08800; 104 | --color-hl-hover-primary-bg: #0366d6; 105 | --color-hl-hover-primary-border: #0366d6; 106 | --color-hl-hover-secondary-bg: #f6f8fa; 107 | --color-hl-hover-secondary-border: #f6f8fa; 108 | --color-hl-selected-primary-bg: #0366d6; 109 | --color-hl-selected-primary-border: #0366d6; 110 | --color-border-primary: #e1e4e8; 111 | --color-border-primary-light: #eaecef; 112 | --color-border-secondary: #d1d5da; 113 | --color-border-tertiary: #fafbfc; 114 | --color-border-inverse: #ffffff; 115 | --color-border-info: #0366d6; 116 | --color-border-danger: #d73a49; 117 | --color-border-success: #28a745; 118 | --color-border-warning: #dbab09; 119 | --color-bg-canvas: #ffffff; 120 | --color-bg-canvas-inverse: #24292e; 121 | --color-bg-primary: #ffffff; 122 | --color-bg-secondary: #f6f8fa; 123 | --color-bg-tertiary: #fafbfc; 124 | --color-bg-overlay: #ffffff; 125 | --color-bg-selected: #0366d6; 126 | --color-bg-info: #dbedff; 127 | --color-bg-info-inverse: #0366d6; 128 | --color-bg-danger: #ffdce0; 129 | --color-bg-danger-inverse: #d73a49; 130 | --color-bg-success: #dcffe4; 131 | --color-bg-success-inverse: #28a745; 132 | --color-bg-warning: #fff5b1; 133 | --color-bg-warning-inverse: #dbab09; 134 | --color-shadow-small: 0 1px 0 rgba(27, 31, 35, 0.04); 135 | --color-shadow-medium: 0 3px 6px rgba(149, 157, 165, 0.15); 136 | --color-shadow-large: 0 8px 24px rgba(149, 157, 165, 0.2); 137 | --color-shadow-extra-large: 0 12px 48px rgba(149, 157, 165, 0.3); 138 | --color-shadow-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.25); 139 | --color-shadow-inset: inset 0 1px 0 rgba(225, 228, 232, 0.2); 140 | --color-shadow-focus: 0 0 0 3px rgba(3, 102, 214, 0.3); 141 | --color-fade-black-10: rgba(27, 31, 35, 0.1); 142 | --color-fade-black-15: rgba(27, 31, 35, 0.15); 143 | --color-fade-black-30: rgba(27, 31, 35, 0.3); 144 | --color-fade-black-50: rgba(27, 31, 35, 0.5); 145 | --color-fade-black-70: rgba(27, 31, 35, 0.7); 146 | --color-fade-black-85: rgba(27, 31, 35, 0.85); 147 | --color-fade-white-10: rgba(255, 255, 255, 0.1); 148 | --color-fade-white-15: rgba(255, 255, 255, 0.15); 149 | --color-fade-white-30: rgba(255, 255, 255, 0.3); 150 | --color-fade-white-50: rgba(255, 255, 255, 0.5); 151 | --color-fade-white-70: rgba(255, 255, 255, 0.7); 152 | --color-fade-white-85: rgba(255, 255, 255, 0.85); 153 | --color-alert-bg: #dbedff; 154 | --color-alert-border: rgba(4, 66, 137, 0.2); 155 | --color-alert-icon: rgba(4, 66, 137, 0.6); 156 | --color-alert-warn-bg: #fffbdd; 157 | --color-alert-warn-border: rgba(176, 136, 0, 0.2); 158 | --color-alert-warn-icon: #b08800; 159 | --color-alert-error-bg: #ffe3e6; 160 | --color-alert-error-border: rgba(158, 28, 35, 0.2); 161 | --color-alert-error-icon: rgba(158, 28, 35, 0.6); 162 | --color-alert-success-bg: #dcffe4; 163 | --color-alert-success-border: rgba(23, 111, 44, 0.2); 164 | --color-alert-success-icon: rgba(23, 111, 44, 0.8); 165 | --color-blankslate-icon: #a3aab1; 166 | --color-btn-bg: #fafbfc; 167 | --color-btn-border: rgba(27, 31, 35, 0.15); 168 | --color-btn-text: #24292e; 169 | --color-btn-ic: #6a737d; 170 | --color-btn-bg-hover: #f3f4f6; 171 | --color-btn-bg-active: #edeff2; 172 | --color-btn-primary-bg: #2ea44f; 173 | --color-btn-primary-border: rgba(27, 31, 35, 0.15); 174 | --color-btn-primary-border-disabled: rgba(27, 31, 35, 0.1); 175 | --color-btn-primary-text: #ffffff; 176 | --color-btn-primary-ic: rgba(255, 255, 255, 0.8); 177 | --color-btn-primary-shadow: 0 1px 0 rgba(27, 31, 35, 0.1); 178 | --color-btn-primary-shadow-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.03); 179 | --color-btn-primary-shadow-selected: inset 0 1px 0 rgba(20, 70, 32, 0.2); 180 | --color-btn-primary-bg-hover: #2c974b; 181 | --color-btn-primary-bg-active: #2a8f47; 182 | --color-btn-primary-bg-disabled: #94d3a2; 183 | --color-btn-primary-disabled-text: rgba(255, 255, 255, 0.8); 184 | --color-btn-primary-disabled-shadow: 0 0 0 3px rgba(46, 164, 79, 0.4); 185 | --color-btn-primary-counter-bg: rgba(255, 255, 255, 0.2); 186 | --color-btn-inverse-on-hover-border: rgba(27, 31, 35, 0.15); 187 | --color-btn-inverse-on-hover-shadow: 0 1px 0 rgba(27, 31, 35, 0.1); 188 | --color-btn-inverse-on-hover-shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.03); 189 | --color-btn-inverse-on-hover-counter-bg: rgba(255, 255, 255, 0.2); 190 | --color-btn-danger-text: #d73a49; 191 | --color-btn-danger-text-disabled: rgba(215, 58, 73, 0.5); 192 | --color-btn-danger-bg-hover: #cb2431; 193 | --color-btn-danger-bg-active: #be222e; 194 | --color-btn-danger-shadow: inset 0 1px 0 rgba(134, 24, 29, 0.2); 195 | --color-btn-danger-shadow-focus: 0 0 0 3px rgba(203, 36, 49, 0.4); 196 | --color-btn-danger-counter-bg: rgba(215, 58, 73, 0.1); 197 | --color-btn-danger-counter-bg-disabled: rgba(215, 58, 73, 0.05); 198 | --color-btn-outline-text: #0366d6; 199 | --color-btn-outline-text-disabled: rgba(3, 102, 214, 0.5); 200 | --color-btn-outline-bg-hover: #0366d6; 201 | --color-btn-outline-bg-active: #035fc7; 202 | --color-btn-outline-shadow: inset 0 1px 0 rgba(5, 38, 76, 0.2); 203 | --color-btn-outline-shadow-focus: 0 0 0 3px rgba(0, 92, 197, 0.4); 204 | --color-btn-outline-counter-bg: rgba(3, 102, 214, 0.1); 205 | --color-btn-outline-counter-bg-disabled: rgba(3, 102, 214, 0.05); 206 | --color-btn-counter-bg: rgba(27, 31, 35, 0.08); 207 | --color-counter-text: #24292e; 208 | --color-counter-bg: rgba(209, 213, 218, 0.5); 209 | --color-counter-border: rgba(209, 213, 218, 0.5); 210 | --color-topic-tag-bg: #f1f8ff; 211 | --color-topic-tag-border: #f1f8ff; 212 | --color-topic-tag-text: #0366d6; 213 | --color-input-bg: #ffffff; 214 | --color-input-contrast-bg: #fafbfc; 215 | --color-input-shadow: inset 0 1px 2px rgba(27, 31, 35, 0.075); 216 | --color-input-shadow-focus: 0 0 0 0.2em rgba(3, 102, 214, 0.3); 217 | --color-avatar-border: rgba(27, 31, 35, 0.1); 218 | --color-avatar-stack-fade: #d1d5da; 219 | --color-avatar-stack-fade-more: #e1e4e8; 220 | --color-avatar-child-shadow: -2px -2px 0 rgba(255, 255, 255, 0.8); 221 | --color-toast-ic-bg-loading: #586069; 222 | --color-timeline-text: #444d56; 223 | --color-timeline-badge-bg: #e1e4e8; 224 | --color-timeline-target-badge-border: #2188ff; 225 | --color-timeline-target-badge-shadow: #c8e1ff; 226 | --color-select-menu-backdrop-bg: rgba(27, 31, 35, 0.5); 227 | --color-select-menu-shadow: 0 0 18px rgba(27, 31, 35, 0.4); 228 | --color-select-menu-tap-highlight: rgba(209, 213, 218, 0.5); 229 | --color-select-menu-tap-focus-bg: #dbedff; 230 | --color-box-blue-border: #c8e1ff; 231 | --color-dropdown-border: rgba(27, 31, 35, 0.15); 232 | --color-popover-border: rgba(27, 31, 35, 0.15); 233 | --color-branch-name-text: rgba(27, 31, 35, 0.6); 234 | --color-branch-name-bg: #eaf5ff; 235 | --color-branch-name-icon: #a8bbd0; 236 | --color-markdown-code-bg: rgba(27, 31, 35, 0.05); 237 | --color-markdown-frame-border: #dfe2e5; 238 | --color-markdown-blockquote-border: #dfe2e5; 239 | --color-markdown-table-border: #dfe2e5; 240 | --color-markdown-table-tr-border: #c6cbd1; 241 | --color-header-text: rgba(255, 255, 255, 0.7); 242 | --color-filter-item-bar-bg: #eff3f6; 243 | --color-hidden-text-expander-bg: #dfe2e5; 244 | --color-hidden-text-expander-bg-hover: #c6cbd1; 245 | --color-drag-and-drop-border: #c3c8cf; 246 | --color-upload-enabled-border: #dfe2e5; 247 | --color-upload-enabled-border-focused: #4a9eff; 248 | --color-previewable-comment-form-border: #c3c8cf; 249 | --color-ic-folder: #79b8ff; 250 | --color-hl-author-bg: #f1f8ff; 251 | --color-hl-author-border: #c8e1ff; 252 | --color-logo-subdued: #d1d5da; 253 | --color-discussion-border: #a2cbac; 254 | --color-diff-neutral-bg: #d1d5da; 255 | --color-diff-deletion-bg: #cb2431; 256 | --color-diff-deletion-bg-strong: #1b1f23; 257 | --color-diff-deletion-border: #1b1f23; 258 | --color-diff-deletion-border-strong: #1b1f23; 259 | --color-diff-addition-bg: #2cbe4e; 260 | --color-diff-addition-bg-strong: #1b1f23; 261 | --color-diff-addition-border: #1b1f23; 262 | --color-diff-addition-border-strong: #1b1f23; 263 | --color-global-nav-logo: #ffffff; 264 | --color-global-nav-bg: #24292e; 265 | --color-global-nav-text: #ffffff; 266 | --color-global-nav-input-bg: #fafbfc; 267 | --color-global-nav-input-border: #fafbfc; 268 | --color-global-nav-input-ic: #1b1f23; 269 | --color-global-nav-input-placeholder: #1b1f23; 270 | --color-calendar-graph-day-bg: #ebedf0; 271 | --color-calendar-graph-day-border: rgba(27, 31, 35, 0.06); 272 | --color-calendar-graph-day-L1-bg: #9be9a8; 273 | --color-calendar-graph-day-L2-bg: #40c463; 274 | --color-calendar-graph-day-L3-bg: #30a14e; 275 | --color-calendar-graph-day-L4-bg: #216e39; 276 | --color-calendar-graph-day-L4-border: rgba(27, 31, 35, 0.06); 277 | --color-calendar-graph-day-L3-border: rgba(27, 31, 35, 0.06); 278 | --color-calendar-graph-day-L2-border: rgba(27, 31, 35, 0.06); 279 | --color-calendar-graph-day-L1-border: rgba(27, 31, 35, 0.06); 280 | --display-blue-fgColor: blue; 281 | --display-blue-bgColor-muted: blue; 282 | --display-yellow-bgColor-emphasis: yellow; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/defines-new-color-vars.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // @include color-variables((('my-commented-color', (light: var(--color-scale-blue-5), dark: var(--color-scale-blue-3))))); 3 | // --color-my-other-commented-color: #1b1f23; 4 | @include color-variables( 5 | ( 6 | ('my-first-feature', (light: var(--color-scale-blue-5), dark: var(--color-scale-blue-3))), 7 | (my-second-feature, (light: var(--color-scale-green-5), dark: var(--color-scale-red-3))) 8 | ) 9 | ); 10 | 11 | $readme-blue--light: #4f80f9; 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/deprecations.json: -------------------------------------------------------------------------------- 1 | { 2 | "text.primary": "fg.default", 3 | "text.secondary": ["fg.one", "fg.two"], 4 | "text.white": null, 5 | "border.primary": "border.default", 6 | "border.secondary": "border.subtle" 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/design-tokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens/base/size/size.json": [ 3 | { 4 | "value": "0.25rem", 5 | "filePath": "tokens/base/size/size.json", 6 | "isSource": true, 7 | "original": { 8 | "value": "4px" 9 | }, 10 | "name": "base-size-4", 11 | "attributes": {}, 12 | "path": [ 13 | "base", 14 | "size", 15 | "4" 16 | ] 17 | }, 18 | { 19 | "value": "0.25rem", 20 | "filePath": "tokens/base/size/size.json", 21 | "isSource": true, 22 | "original": { 23 | "value": "8px" 24 | }, 25 | "name": "base-size-8", 26 | "attributes": {}, 27 | "path": [ 28 | "base", 29 | "size", 30 | "8" 31 | ] 32 | } 33 | ], 34 | "tokens/base/size/spacing.json": [ 35 | { 36 | "value": "0.25rem", 37 | "filePath": "tokens/base/size/size.json", 38 | "isSource": true, 39 | "original": { 40 | "value": "4px" 41 | }, 42 | "name": "base-size-4", 43 | "attributes": {}, 44 | "path": [ 45 | "base", 46 | "size", 47 | "4" 48 | ] 49 | }, 50 | { 51 | "value": "0.25rem", 52 | "filePath": "tokens/base/size/size.json", 53 | "isSource": true, 54 | "original": { 55 | "value": "8px" 56 | }, 57 | "name": "base-size-8", 58 | "attributes": {}, 59 | "path": [ 60 | "base", 61 | "size", 62 | "8" 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/good/example.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | height: 264px; 3 | overflow: hidden; 4 | } 5 | 6 | :root { 7 | --offset: -500px; 8 | } 9 | 10 | .gradient { 11 | position: absolute; 12 | top: 0; 13 | /* stylelint-disable-next-line primer/responsive-widths */ 14 | width: 1000px; 15 | height: 800px; 16 | } 17 | 18 | .octicon { 19 | display: inline-block; 20 | } 21 | 22 | .gradient-left { 23 | /* stylelint-disable-next-line primer/spacing */ 24 | top: var(--offset); 25 | /* stylelint-disable-next-line primer/spacing */ 26 | left: var(--offset); 27 | /* stylelint-disable-next-line primer/colors */ 28 | background: radial-gradient(30% 70% at 50% 50%, rgb(130 80 223 / 0.2) 0%, rgb(130 80 223 / 0) 100%); 29 | } 30 | 31 | .gradient-right { 32 | /* stylelint-disable-next-line primer/spacing */ 33 | right: var(--offset); 34 | /* stylelint-disable-next-line primer/colors */ 35 | background: radial-gradient(30% 70% at 50% 50%, rgb(9 107 222 / 0.3) 0%, rgb(9 107 222 / 0) 100%); 36 | } 37 | 38 | .header-background, 39 | .header-copilot { 40 | user-select: none; 41 | } 42 | 43 | .header-background { 44 | /* stylelint-disable-next-line primer/spacing */ 45 | top: -220px; 46 | /* stylelint-disable-next-line primer/spacing */ 47 | right: -110px; 48 | height: 580px; 49 | } 50 | 51 | .header-copilot { 52 | top: -35%; 53 | /* stylelint-disable-next-line primer/spacing */ 54 | right: 100px; 55 | height: 146px; 56 | } 57 | 58 | .header-content { 59 | position: relative; 60 | z-index: 1; 61 | } 62 | 63 | .negative-margin { 64 | /* stylelint-disable-next-line primer/spacing */ 65 | margin: -0.5rem; 66 | } 67 | 68 | .search-input { 69 | max-width: 600px; 70 | } 71 | 72 | .marketplace-featured-grid, 73 | .marketplace-list-grid { 74 | display: grid; 75 | gap: 1rem; 76 | } 77 | 78 | .marketplace-featured-grid { 79 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 80 | max-width: 100%; 81 | } 82 | 83 | .marketplace-list-grid { 84 | grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); 85 | max-width: 100%; 86 | } 87 | 88 | .marketplace-item { 89 | box-shadow: var(--shadow-resting-small); 90 | } 91 | 92 | .marketplace-item:hover, 93 | .marketplace-item:focus-within { 94 | background-color: var(--bgColor-muted); 95 | } 96 | 97 | .marketplace-item:focus-within { 98 | outline: 2px solid var(--fgColor-accent); 99 | } 100 | 101 | .marketplace-item-link { 102 | color: var(--fgColor-default); 103 | } 104 | 105 | .marketplace-item-link:hover { 106 | color: inherit; 107 | text-decoration: none; 108 | } 109 | 110 | .marketplace-item-link:focus { 111 | outline: none; 112 | } 113 | 114 | .marketplace-item-link::before { 115 | position: absolute; 116 | cursor: pointer; 117 | content: ""; 118 | inset: 0; 119 | } 120 | 121 | .marketplace-logo { 122 | --container-size: var(--base-size-40); 123 | --logo-size: var(--base-size-32); 124 | 125 | display: grid; 126 | place-items: center; 127 | width: var(--container-size); 128 | height: var(--container-size); 129 | /* stylelint-disable-next-line primer/spacing */ 130 | padding: var(--mySpace-xsmall); 131 | } 132 | 133 | .marketplace-logo--large { 134 | --container-size: var(--base-size-96); 135 | --logo-size: var(--base-size-80); 136 | } 137 | 138 | .marketplace-logo-img { 139 | width: var(--logo-size); 140 | height: var(--logo-size); 141 | } 142 | 143 | .details { 144 | &[open] .down-icon { 145 | display: none !important; 146 | } 147 | 148 | &:not([open]) .up-icon { 149 | display: none !important; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/good/example.pcss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable primer/spacing */ 2 | 3 | /* CSS for Button */ 4 | 5 | /* temporary, pre primitives release */ 6 | :root { 7 | --duration-fast: 80ms; 8 | --easing-easeInOut: cubic-bezier(0.65, 0, 0.35, 1); 9 | } 10 | 11 | /* base button */ 12 | .Button { 13 | position: relative; 14 | display: inline-flex; 15 | min-width: max-content; 16 | height: var(--control-medium-size); 17 | padding: 0 var(--control-medium-paddingInline-normal); 18 | font-size: var(--text-body-size-medium); 19 | font-weight: var(--base-text-weight-medium); 20 | color: var(--button-default-fgColor-rest); 21 | text-align: center; 22 | cursor: pointer; 23 | flex-direction: row; 24 | user-select: none; 25 | background-color: transparent; 26 | border: var(--borderWidth-thin) solid; 27 | border-color: transparent; 28 | border-radius: var(--borderRadius-medium); 29 | transition: var(--duration-fast) var(--easing-easeInOut); 30 | transition-property: color, fill, background-color, border-color; 31 | justify-content: space-between; 32 | align-items: center; 33 | gap: var(--base-size-4); 34 | 35 | /* mobile friendly sizing */ 36 | @media (pointer: coarse) { 37 | &::before { 38 | @mixin minTouchTarget 48px, 48px; 39 | } 40 | } 41 | 42 | /* base states */ 43 | 44 | &:hover { 45 | transition-duration: var(--duration-fast); 46 | } 47 | 48 | &:active { 49 | transition: none; 50 | } 51 | 52 | &:disabled, 53 | &[aria-disabled='true'] { 54 | cursor: not-allowed; 55 | box-shadow: none; 56 | } 57 | 58 | &.Button--iconOnly { 59 | color: var(--fgColor-muted); 60 | } 61 | } 62 | 63 | /* wrap grid content to allow trailingAction to lock-right */ 64 | .Button-content { 65 | flex: 1 0 auto; 66 | display: grid; 67 | grid-template-areas: 'leadingVisual text trailingVisual'; 68 | grid-template-columns: min-content minmax(0, auto) min-content; 69 | align-items: center; 70 | place-content: center; 71 | 72 | /* padding-bottom: 1px; optical alignment for firefox */ 73 | 74 | & > :not(:last-child) { 75 | margin-right: var(--control-medium-gap); 76 | } 77 | } 78 | 79 | /* center child elements for fullWidth */ 80 | .Button-content--alignStart { 81 | justify-content: start; 82 | } 83 | 84 | /* button child elements */ 85 | 86 | /* align svg */ 87 | .Button-visual { 88 | display: flex; 89 | pointer-events: none; /* allow click handler to work, avoiding visuals */ 90 | 91 | & .counta { 92 | color: inherit; 93 | background-color: var(--buttonCounter-default-bgColor-rest); 94 | } 95 | } 96 | 97 | .Button-label { 98 | line-height: var(--text-body-lineHeight-medium); 99 | white-space: nowrap; 100 | grid-area: text; 101 | } 102 | 103 | .Button-leadingVisual { 104 | grid-area: leadingVisual; 105 | } 106 | 107 | .Button-leadingVisual .svg { 108 | fill: currentcolor; 109 | } 110 | 111 | .Button-trailingVisual { 112 | grid-area: trailingVisual; 113 | } 114 | 115 | .Button-trailingAction { 116 | margin-right: calc(var(--base-size-4) * -1); 117 | } 118 | 119 | /* sizes */ 120 | 121 | .Button--small { 122 | height: var(--control-small-size); 123 | padding: 0 var(--control-small-paddingInline-condensed); 124 | font-size: var(--text-body-size-small); 125 | gap: var(--control-small-gap); 126 | 127 | & .Button-label { 128 | line-height: var(--text-body-lineHeight-small); 129 | } 130 | 131 | & .Button-content { 132 | & > :not(:last-child) { 133 | margin-right: var(--control-small-gap); 134 | } 135 | } 136 | } 137 | 138 | .Button--large { 139 | height: var(--control-large-size); 140 | padding: 0 var(--control-large-paddingInline-spacious); 141 | gap: var(--control-large-gap); 142 | 143 | & .Button-label { 144 | line-height: var(--text-body-lineHeight-large); 145 | } 146 | 147 | & .Button-content { 148 | & > :not(:last-child) { 149 | margin-right: var(--control-large-gap); 150 | } 151 | } 152 | } 153 | 154 | .Button--fullWidth { 155 | width: 100%; 156 | } 157 | 158 | /* allow button label text to wrap */ 159 | 160 | .Button--labelWrap { 161 | min-width: fit-content; 162 | height: unset; 163 | min-height: var(--control-medium-size); 164 | 165 | & .Button-content { 166 | flex: 1 1 auto; 167 | align-self: stretch; 168 | padding-block: calc(var(--control-medium-paddingBlock) - 2px); 169 | } 170 | 171 | & .Button-label { 172 | white-space: unset; 173 | } 174 | 175 | &.Button--small { 176 | height: unset; 177 | min-height: var(--control-small-size); 178 | 179 | & .Button-content { 180 | padding-block: calc(var(--control-small-paddingBlock) - 2px); 181 | } 182 | } 183 | 184 | &.Button--large { 185 | height: unset; 186 | min-height: var(--control-large-size); 187 | padding-inline: var(--control-large-paddingInline-spacious); 188 | 189 | & .Button-content { 190 | padding-block: calc(var(--control-large-paddingBlock) - 2px); 191 | } 192 | } 193 | } 194 | 195 | /* variants */ 196 | 197 | /* primary */ 198 | .Button--primary { 199 | color: var(--button-primary-fgColor-rest); 200 | fill: var(--button-primary-iconColor-rest); 201 | background-color: var(--button-primary-bgColor-rest); 202 | border-color: var(--button-primary-borderColor-rest); 203 | box-shadow: var(--shadow-resting-small); 204 | 205 | &.Button--iconOnly { 206 | color: var(--button-primary-iconColor-rest); 207 | } 208 | 209 | &:hover:not(:disabled, .Button--inactive) { 210 | background-color: var(--button-primary-bgColor-hover); 211 | border-color: var(--button-primary-borderColor-hover); 212 | } 213 | 214 | /* fallback :focus state */ 215 | &:focus { 216 | @mixin focusOutlineOnEmphasis; 217 | 218 | /* remove fallback :focus if :focus-visible is supported */ 219 | &:not(:focus-visible) { 220 | outline: solid 1px transparent; 221 | box-shadow: none; 222 | } 223 | } 224 | 225 | /* default focus state */ 226 | &:focus-visible { 227 | @mixin focusOutlineOnEmphasis; 228 | } 229 | 230 | &:active:not(:disabled), 231 | &[aria-pressed='true'] { 232 | background-color: var(--button-primary-bgColor-active); 233 | box-shadow: var(--button-primary-shadow-selected); 234 | } 235 | 236 | &:disabled, 237 | &[aria-disabled='true'] { 238 | color: var(--button-primary-fgColor-disabled); 239 | fill: var(--button-primary-fgColor-disabled); 240 | background-color: var(--button-primary-bgColor-disabled); 241 | border-color: var(--button-primary-borderColor-disabled); 242 | } 243 | 244 | & .counta { 245 | color: inherit; 246 | background-color: var(--buttonCounter-primary-bgColor-rest); 247 | } 248 | } 249 | 250 | /* default (secondary) */ 251 | .Button--secondary { 252 | color: var(--button-default-fgColor-rest); 253 | fill: var(--fgColor-muted); /* help this */ 254 | background-color: var(--button-default-bgColor-rest); 255 | border-color: var(--button-default-borderColor-rest); 256 | box-shadow: var(--button-default-shadow-resting); 257 | 258 | &:hover:not(:disabled, .Button--inactive) { 259 | background-color: var(--button-default-bgColor-hover); 260 | border-color: var(--button-default-borderColor-hover); 261 | } 262 | 263 | &:active:not(:disabled) { 264 | background-color: var(--button-default-bgColor-active); 265 | border-color: var(--button-default-borderColor-active); 266 | } 267 | 268 | &[aria-pressed='true'] { 269 | background-color: var(--button-default-bgColor-selected); 270 | box-shadow: var(--shadow-inset); 271 | } 272 | 273 | &:disabled, 274 | &[aria-disabled='true'] { 275 | color: var(--control-fgColor-disabled); 276 | fill: var(--control-fgColor-disabled); 277 | background-color: var(--button-default-bgColor-disabled); 278 | border-color: var(--button-default-borderColor-disabled); 279 | } 280 | } 281 | 282 | .Button--invisible { 283 | color: var(--button-default-fgColor-rest); 284 | 285 | &.Button--iconOnly { 286 | color: var(--button-invisible-iconColor-rest); 287 | } 288 | 289 | &:hover:not(:disabled, .Button--inactive) { 290 | background-color: var(--control-transparent-bgColor-hover); 291 | } 292 | 293 | &[aria-pressed='true'], 294 | &:active:not(:disabled) { 295 | background-color: var(--button-invisible-bgColor-active); 296 | } 297 | 298 | &:disabled, 299 | &[aria-disabled='true'] { 300 | color: var(--button-invisible-fgColor-disabled); 301 | fill: var(--button-invisible-fgColor-disabled); 302 | background-color: var(--button-invisible-bgColor-disabled); 303 | border-color: var(--button-invisible-borderColor-disabled); 304 | } 305 | 306 | /* if button has no visuals, use link blue for text */ 307 | &.Button--invisible-noVisuals .Button-label { 308 | color: var(--button-invisible-fgColor-rest); 309 | } 310 | 311 | & .Button-visual { 312 | color: var(--button-invisible-iconColor-rest); 313 | 314 | & .counta { 315 | color: var(--fgColor-default); 316 | } 317 | } 318 | } 319 | 320 | .Button--link { 321 | display: inline-block; 322 | min-width: fit-content; 323 | height: unset; 324 | padding: 0; 325 | font-size: inherit; 326 | color: var(--fgColor-link); 327 | fill: var(--fgColor-link); 328 | 329 | &:hover:not(:disabled, .Button--inactive) { 330 | text-decoration: underline; 331 | } 332 | 333 | &:focus-visible, 334 | &:focus { 335 | outline-offset: 2px; 336 | } 337 | 338 | &:disabled, 339 | &[aria-disabled='true'] { 340 | color: var(--control-fgColor-disabled); 341 | fill: var(--control-fgColor-disabled); 342 | background-color: transparent; 343 | border-color: transparent; 344 | } 345 | 346 | & .Button-label { 347 | white-space: unset; 348 | } 349 | } 350 | 351 | /* danger */ 352 | .Button--danger { 353 | color: var(--button-danger-fgColor-rest); 354 | fill: var(--button-danger-iconColor-rest); 355 | background-color: var(--button-danger-bgColor-rest); 356 | border-color: var(--button-danger-borderColor-rest); 357 | box-shadow: var(--button-default-shadow-resting); 358 | 359 | &.Button--iconOnly { 360 | color: var(--button-danger-iconColor-rest); 361 | } 362 | 363 | &:hover:not(:disabled, .Button--inactive) { 364 | color: var(--button-danger-fgColor-hover); 365 | fill: var(--button-danger-fgColor-hover); 366 | background-color: var(--button-danger-bgColor-hover); 367 | border-color: var(--button-danger-borderColor-hover); 368 | box-shadow: var(--shadow-resting-small); 369 | 370 | & .counta { 371 | color: var(--buttonCounter-danger-fgColor-hover); 372 | background-color: var(--buttonCounter-danger-bgColor-hover); 373 | } 374 | } 375 | 376 | &:active:not(:disabled), 377 | &[aria-pressed='true'] { 378 | color: var(--button-danger-fgColor-active); 379 | fill: var(--button-danger-fgColor-active); 380 | background-color: var(--button-danger-bgColor-active); 381 | border-color: var(--button-danger-borderColor-active); 382 | box-shadow: var(--button-danger-shadow-selected); 383 | } 384 | 385 | &:disabled, 386 | &[aria-disabled='true'] { 387 | color: var(--button-danger-fgColor-disabled); 388 | fill: var(--button-danger-fgColor-disabled); 389 | background-color: var(--button-danger-bgColor-disabled); 390 | border-color: var(--button-default-borderColor-disabled); 391 | 392 | & .counta { 393 | color: var(--buttonCounter-danger-fgColor-disabled); 394 | background-color: var(--buttonCounter-danger-bgColor-disabled); 395 | } 396 | } 397 | 398 | & .counta { 399 | color: var(--buttonCounter-danger-fgColor-rest); 400 | background-color: var(--buttonCounter-danger-bgColor-rest); 401 | } 402 | } 403 | 404 | .Button--iconOnly { 405 | display: inline-grid; 406 | width: var(--control-medium-size); 407 | padding: unset; 408 | place-content: center; 409 | 410 | &.Button--small { 411 | width: var(--control-small-size); 412 | } 413 | 414 | &.Button--large { 415 | width: var(--control-large-size); 416 | } 417 | } 418 | 419 | /* `disabled` takes precedence over `inactive` */ 420 | .Button--inactive:not([aria-disabled='true'], :disabled) { 421 | color: var(--button-inactive-fgColor); 422 | cursor: default; 423 | background-color: var(--button-inactive-bgColor); 424 | border: 0; 425 | } 426 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/good/example.scss: -------------------------------------------------------------------------------- 1 | .sugest { 2 | position: relative; 3 | top: 0; 4 | left: 0; 5 | min-width: 180px; 6 | padding: 0; 7 | margin: 0; 8 | margin-top: var(--base-size-16); 9 | list-style: none; 10 | cursor: pointer; 11 | background: var(--overlay-bgColor); 12 | border: var(--borderWidth-thin) solid var(--borderColor-default); 13 | border-radius: var(--borderRadius-small); 14 | box-shadow: var(--shadow-resting-medium); 15 | 16 | .li { 17 | display: block; 18 | padding: var(--base-size-4) var(--base-size-8); 19 | font-weight: var(--base-text-weight-semibold); 20 | border-bottom: var(--borderWidth-thin) solid var(--borderColor-muted); 21 | 22 | .foo { 23 | font-weight: var(--base-text-weight-normal); 24 | color: var(--fgColor-muted); 25 | } 26 | 27 | &:last-child { 28 | border-bottom: 0; 29 | border-bottom-right-radius: var(--borderRadius-small); 30 | border-bottom-left-radius: var(--borderRadius-small); 31 | } 32 | 33 | &:first-child { 34 | border-top-left-radius: var(--borderRadius-small); 35 | border-top-right-radius: var(--borderRadius-small); 36 | } 37 | 38 | &:hover { 39 | color: var(--fgColor-onEmphasis); 40 | text-decoration: none; 41 | background: var(--bgColor-accent-emphasis); 42 | 43 | .foo { 44 | color: var(--fgColor-onEmphasis); 45 | } 46 | 47 | .svgicon { 48 | color: inherit !important; 49 | } 50 | } 51 | 52 | &[aria-selected='true'], 53 | &.nav-focus { 54 | color: var(--fgColor-onEmphasis); 55 | text-decoration: none; 56 | background: var(--bgColor-accent-emphasis); 57 | 58 | .foo { 59 | color: var(--fgColor-onEmphasis); 60 | } 61 | 62 | .svgicon { 63 | color: inherit !important; 64 | } 65 | } 66 | } 67 | } 68 | 69 | .sugest-container { 70 | position: absolute; 71 | top: 0; 72 | left: 0; 73 | z-index: 30; 74 | } 75 | 76 | // Responsive 77 | 78 | .responsive-page { 79 | @media (max-width: $width-sm) { 80 | .sugest-container { 81 | right: var(--base-size-8) !important; 82 | left: var(--base-size-8) !important; 83 | } 84 | 85 | .sugest .li { 86 | padding: var(--base-size-8) var(--base-size-12); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /__tests__/__fixtures__/good/example.tsx: -------------------------------------------------------------------------------- 1 | import {testIdProps} from '@github-ui/test-id-props' 2 | import {AlertIcon, CheckCircleIcon, InfoIcon, StopIcon, XIcon} from '@primer/octicons-react' 3 | import {Box, Octicon, sx, type SxProp, Text, themeGet} from '@primer/react' 4 | import {useCallback, useEffect, useRef} from 'react' 5 | import styled, {keyframes} from 'styled-components' 6 | 7 | import {TOAST_ANIMATION_LENGTH, type ToastItem, ToastType} from './types' 8 | 9 | type IToastCloseButtonProps = Omit, 'color'> 10 | 11 | const StyledButton = styled.button` 12 | padding: var(--base-size-8); 13 | /* stylelint-disable-next-line primer/spacing */ 14 | margin: var(--base-size-8) 10px var(--base-size-8) 0px; 15 | border-radius: var(--borderRadius-small); 16 | color: ${themeGet('colors.fg.onEmphasis')}; 17 | border: 0; 18 | background: transparent; 19 | outline: none; 20 | cursor: pointer; 21 | 22 | /* can be removed if using Primer IconButton component */ 23 | &:focus { 24 | /* stylelint-disable-next-line primer/box-shadow */ 25 | box-shadow: 0 0 0 2px ${themeGet('colors.accent.fg')}; 26 | } 27 | ` 28 | 29 | const ToastCloseButton = (props: IToastCloseButtonProps) => ( 30 | 31 | 32 | 33 | ) 34 | 35 | /** 36 | * Lookup for converting toast types into Primer colors 37 | */ 38 | export const stateColorMap = { 39 | [ToastType.default]: 'accent.emphasis', 40 | [ToastType.success]: 'success.emphasis', 41 | [ToastType.warning]: 'attention.emphasis', 42 | [ToastType.error]: 'danger.emphasis', 43 | } 44 | 45 | const DefaultIcon = 46 | const SuccessIcon = 47 | export const WarningIcon = 48 | const ErrorIcon = 49 | 50 | const stateMap = { 51 | [ToastType.default]: DefaultIcon, 52 | [ToastType.success]: SuccessIcon, 53 | [ToastType.warning]: WarningIcon, 54 | [ToastType.error]: ErrorIcon, 55 | } 56 | 57 | const toastEnter = keyframes` 58 | from { 59 | transform: translateX(-400px); 60 | } 61 | to { 62 | transform: translateX(0); 63 | } 64 | ` 65 | 66 | const toastLeave = keyframes` 67 | from { 68 | transform: translateX(0); 69 | } 70 | to { 71 | transform: translateX(-460px); 72 | } 73 | 74 | ` 75 | 76 | export const IconContainer = styled(Box)` 77 | flex-shrink: 0; 78 | padding: ${themeGet('space.3')}; 79 | ` 80 | 81 | export const StyledToast = styled.div` 82 | position: fixed; 83 | display: flex; 84 | gap: ${themeGet('space.2')}; 85 | /* stylelint-disable-next-line primer/box-shadow */ 86 | box-shadow: ${themeGet('shadows.shadow.large')}; 87 | background-color: ${themeGet('colors.neutral.emphasisPlus')}; 88 | border-radius: ${themeGet('radii.2')}; 89 | align-items: stretch; 90 | box-sizing: border-box; 91 | overflow: hidden; 92 | max-width: 400px; 93 | /* stylelint-disable-next-line primer/spacing */ 94 | bottom: 66px; 95 | left: var(--base-size-12); 96 | z-index:99; 97 | 98 | animation: ${toastEnter} ${TOAST_ANIMATION_LENGTH}ms cubic-bezier(0.25, 1, 0.5, 1); 99 | 100 | &.toast-leave { 101 | animation: ${toastLeave} ${TOAST_ANIMATION_LENGTH}ms cubic-bezier(0.5, 0, 0.75, 0) forwards; 102 | } 103 | ` 104 | 105 | export const ToastAction = styled.button` 106 | background-color: transparent; 107 | border: 0; 108 | /* stylelint-disable-next-line primer/typography */ 109 | font-weight: ${themeGet('fontWeights.bold')}; 110 | margin-left: ${themeGet('space.2')}; 111 | 112 | margin-top: ${themeGet('space.3')}; 113 | margin-bottom: ${themeGet('space.3')}; 114 | color: ${themeGet('colors.fg.onEmphasis')}; 115 | /* stylelint-disable-next-line primer/typography */ 116 | font-size: ${themeGet('fontSizes.1')}; 117 | font-family: inherit; 118 | outline: none; 119 | padding: 0; 120 | white-space: nowrap; 121 | 122 | &:hover { 123 | text-decoration: underline; 124 | } 125 | 126 | &:focus { 127 | border-color: transparent; 128 | /* stylelint-disable-next-line primer/box-shadow */ 129 | box-shadow: 0 0 0 3px ${themeGet('colors.border.default')}; 130 | } 131 | ${sx} 132 | ` 133 | 134 | interface ToastProps { 135 | removeToast: (id: number) => void 136 | startRemovingToast: (id: number) => void 137 | toast: ToastItem 138 | } 139 | 140 | /** 141 | * Component for rendering a toast within the application 142 | */ 143 | const Toast = (props: ToastProps) => { 144 | const {toast, startRemovingToast} = props 145 | const callToActionRef = useRef(null) 146 | 147 | useEffect(() => { 148 | const handleKeyDown = (event: KeyboardEvent) => { 149 | // eslint-disable-next-line @github-ui/ui-commands/no-manual-shortcut-logic 150 | if (callToActionRef.current && event.ctrlKey && event.key === 't') { 151 | callToActionRef.current.focus() 152 | toast.timeout?.cancel() 153 | } 154 | } 155 | document.addEventListener('keydown', handleKeyDown) 156 | return () => { 157 | document.removeEventListener('keydown', handleKeyDown) 158 | } 159 | }, [toast]) 160 | 161 | const toastIcon = toast.icon ? ( 162 | 163 | ) : ( 164 | stateMap[toast.type] 165 | ) 166 | 167 | const handleActionClick = useCallback(() => { 168 | toast.action?.handleClick() 169 | startRemovingToast(toast.id) 170 | }, [toast, startRemovingToast]) 171 | 172 | return ( 173 | 174 | 175 | {toastIcon} 176 | 177 | {toast.message} 178 | {toast.action && ( 179 | 180 | {toast.action.text} 181 | 182 | )} 183 | startRemovingToast(toast.id)} /> 184 | 185 | ) 186 | } 187 | 188 | export default Toast 189 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/has-unused-vars.scss: -------------------------------------------------------------------------------- 1 | $used-elsewhere: 640px; 2 | $unused: 9999; 3 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/spacing-vars.scss: -------------------------------------------------------------------------------- 1 | @mixin primer-spacing-normal($sel) { 2 | #{$sel} { 3 | --spacing-spacer-0: 0; 4 | --spacing-spacer-1: 4px; 5 | --spacing-spacer-2: 8px; 6 | --spacing-spacer-3: 16px; 7 | --spacing-spacer-4: 24px; 8 | --spacing-spacer-5: 32px; 9 | --spacing-spacer-6: 40px; 10 | } 11 | } -------------------------------------------------------------------------------- /__tests__/__fixtures__/uses-vars.scss: -------------------------------------------------------------------------------- 1 | @import "./has-unused-vars.scss"; 2 | 3 | .card { 4 | max-width: $used-elsewhere; 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/borders.js: -------------------------------------------------------------------------------- 1 | import plugin from '../plugins/borders.js' 2 | import dedent from 'dedent' 3 | 4 | const plugins = [plugin] 5 | const { 6 | ruleName, 7 | rule: {messages}, 8 | } = plugin 9 | 10 | // General Tests 11 | testRule({ 12 | plugins, 13 | ruleName, 14 | config: [true, {}], 15 | fix: true, 16 | cache: false, 17 | accept: [ 18 | // Border widths 19 | { 20 | code: '.x { border: var(--borderWidth-thin) solid var(--borderColor-default); }', 21 | description: 'CSS > Accepts border shorthand with variables', 22 | }, 23 | { 24 | code: '.x { border-width: var(--borderWidth-thin); }', 25 | description: 'CSS > Accepts border shorthand with variables', 26 | }, 27 | { 28 | code: '.x { border-left-width: var(--borderWidth-thin); }', 29 | description: 'CSS > Accepts directional border longhand with variables', 30 | }, 31 | { 32 | code: '.x { border-inline-start-width: var(--borderWidth-thin); }', 33 | description: 'CSS > Accepts logical properties directional border longhand with variables', 34 | }, 35 | { 36 | code: '.x { border: 0; }', 37 | description: 'CSS > Allows zero values', 38 | }, 39 | { 40 | code: '.x { border: inherit; border: initial; border: revert; border: revert-layer; border: unset; }', 41 | description: 'CSS > Allows global values', 42 | }, 43 | // Border radii 44 | { 45 | code: '.x { border-radius: var(--borderRadius-medium); }', 46 | description: 'CSS > Accepts border-radius with variables', 47 | }, 48 | { 49 | code: '.x { border-radius: var(--borderRadius-large) var(--borderRadius-small); }', 50 | description: 'CSS > Accepts border-radius shorthand with variables', 51 | }, 52 | { 53 | code: '.x { border-bottom: var(--borderWidth-thin) solid var(--borderColor-muted); }', 54 | description: 'CSS > Accepts directional border-bottom', 55 | }, 56 | { 57 | code: '.x { border: var(--borderWidth-thin) solid transparent; }', 58 | description: 'CSS > Accepts transparent colors', 59 | }, 60 | { 61 | code: '.x { border-color: red; }', 62 | description: 'CSS > Ignores border-color prop', 63 | }, 64 | // Figure out how to allow `calc()` values 65 | ], 66 | reject: [ 67 | // Border widths 68 | { 69 | code: '.x { border: 20px; }', 70 | unfixable: true, 71 | message: messages.rejected('20px'), 72 | line: 1, 73 | column: 14, 74 | endColumn: 18, 75 | description: 'CSS > Errors on value not in border width list', 76 | }, 77 | { 78 | code: '.x { border: $border-width $border-style var(--borderColor-attention-emphasis, var(--color-attention-emphasis)); }', 79 | unfixable: true, 80 | description: 'CSS > Errors on value not in border width list', 81 | warnings: [ 82 | { 83 | message: messages.rejected('$border-width'), 84 | line: 1, 85 | column: 14, 86 | endColumn: 27, 87 | rule: 'primer/spacing', 88 | severity: 'error', 89 | }, 90 | { 91 | message: messages.rejected('$border-style'), 92 | line: 1, 93 | column: 28, 94 | endColumn: 41, 95 | rule: 'primer/spacing', 96 | severity: 'error', 97 | }, 98 | ], 99 | }, 100 | { 101 | code: '.x { border: 1px; }', 102 | fixed: '.x { border: var(--borderWidth-thin); }', 103 | message: messages.rejected('1px', {name: '--borderWidth-thin'}), 104 | line: 1, 105 | column: 14, 106 | endColumn: 17, 107 | description: "CSS > Replaces '1px' with 'var(--borderWidth-thin)'.", 108 | }, 109 | { 110 | code: '.x { border-width: var(--borderRadius-small); }', 111 | unfixable: true, 112 | message: messages.rejected('var(--borderRadius-small)', undefined, 'border-width'), 113 | line: 1, 114 | column: 24, 115 | endColumn: 44, 116 | description: 'CSS > Does not accept a border radius variable for border width.', 117 | }, 118 | // Border radii 119 | { 120 | code: '.x { border-radius: 3px; }', 121 | fixed: '.x { border-radius: var(--borderRadius-small); }', 122 | message: messages.rejected('3px', {name: '--borderRadius-small'}), 123 | line: 1, 124 | column: 21, 125 | endColumn: 24, 126 | description: "CSS > Replaces '3px' with 'var(--borderRadius-small)'.", 127 | }, 128 | { 129 | code: '.x { border-radius: 0.1875rem; }', 130 | fixed: '.x { border-radius: var(--borderRadius-small); }', 131 | message: messages.rejected('0.1875rem', {name: '--borderRadius-small'}), 132 | line: 1, 133 | column: 21, 134 | endColumn: 30, 135 | description: "CSS > Replaces '0.1875rem' with 'var(--borderRadius-small)'.", 136 | }, 137 | { 138 | code: '.x { border-radius: var(--borderWidth-thin); }', 139 | unfixable: true, 140 | message: messages.rejected('var(--borderWidth-thin)', undefined, 'border-radius'), 141 | line: 1, 142 | column: 25, 143 | endColumn: 43, 144 | description: 'CSS > Does not accept a border width variable for border radius.', 145 | }, 146 | { 147 | code: '.x { border-radius: 1px; }', 148 | unfixable: true, 149 | message: messages.rejected('1px', undefined, 'border-radius'), 150 | line: 1, 151 | column: 21, 152 | endColumn: 24, 153 | description: 'CSS > Does not autofix 1px to borderWidth-thin variable.', 154 | }, 155 | { 156 | code: '.x { border: 1px solid var(--borderColor-default); }', 157 | fixed: '.x { border: var(--borderWidth-thin) solid var(--borderColor-default); }', 158 | message: messages.rejected('1px', {name: '--borderWidth-thin'}), 159 | line: 1, 160 | column: 14, 161 | endColumn: 17, 162 | description: 'CSS > Places error border on 1px for shorthand border.', 163 | }, 164 | ], 165 | }) 166 | 167 | // Styled Syntax Specific Tests 168 | testRule({ 169 | plugins, 170 | ruleName, 171 | customSyntax: 'postcss-styled-syntax', 172 | codeFilename: 'example.tsx', 173 | config: [true, {}], 174 | fix: true, 175 | cache: false, 176 | accept: [ 177 | { 178 | code: dedent` 179 | export const IconContainer = styled(Box)\` 180 | border-radius: \${themeGet('radii.2')}; 181 | \` 182 | `, 183 | description: 'TSX > Ignores themeGet.', 184 | }, 185 | ], 186 | }) 187 | -------------------------------------------------------------------------------- /__tests__/box-shadow.js: -------------------------------------------------------------------------------- 1 | import plugin from '../plugins/box-shadow.js' 2 | 3 | const plugins = [plugin] 4 | const { 5 | ruleName, 6 | rule: {messages}, 7 | } = plugin 8 | 9 | // General Tests 10 | testRule({ 11 | plugins, 12 | ruleName, 13 | config: [true, {}], 14 | fix: true, 15 | cache: false, 16 | accept: [ 17 | { 18 | code: '.x { box-shadow: var(--shadow-resting-medium); }', 19 | description: 'CSS > Accepts box shadow variables', 20 | }, 21 | { 22 | code: '.x { box-shadow: var(--boxShadow-thin); }', 23 | description: 'CSS > Accepts box shadow variables that are used to "fake" borders', 24 | }, 25 | ], 26 | reject: [ 27 | { 28 | code: '.x { box-shadow: 1px 2px 3px 4px #000000; }', 29 | unfixable: true, 30 | message: messages.rejected('1px 2px 3px 4px #000000'), 31 | line: 1, 32 | column: 18, 33 | endColumn: 41, 34 | description: 'CSS > Errors on value not in box-shadow list', 35 | }, 36 | { 37 | code: '.x { box-shadow: 0px 1px 1px 0px #25292e1a, 0px 3px 6px 0px #25292e1f; }', 38 | fixed: '.x { box-shadow: var(--shadow-resting-medium); }', 39 | message: messages.rejected('0px 1px 1px 0px #25292e1a, 0px 3px 6px 0px #25292e1f', { 40 | name: '--shadow-resting-medium', 41 | }), 42 | line: 1, 43 | column: 18, 44 | endColumn: 70, 45 | description: 46 | "CSS > Replaces '0px 1px 1px 0px #25292e1a, 0px 3px 6px 0px #25292e1f' with 'var(--shadow-resting-medium)'.", 47 | }, 48 | { 49 | code: '.x { box-shadow: var(--borderWidth-thin); }', 50 | unfixable: true, 51 | message: messages.rejected('var(--borderWidth-thin)'), 52 | line: 1, 53 | column: 18, 54 | endColumn: 41, 55 | description: 'CSS > Does not allow border variables besides the ones used to mimic a box-shadow', 56 | }, 57 | ], 58 | }) 59 | -------------------------------------------------------------------------------- /__tests__/colors.js: -------------------------------------------------------------------------------- 1 | import plugin from '../plugins/colors.js' 2 | 3 | const plugins = [plugin] 4 | const { 5 | ruleName, 6 | rule: {messages}, 7 | } = plugin 8 | 9 | // General Tests 10 | testRule({ 11 | plugins, 12 | ruleName, 13 | config: [true, {}], 14 | fix: true, 15 | cache: false, 16 | accept: [ 17 | { 18 | code: '.x { color: var(--fgColor-default); }', 19 | description: 'CSS > Accepts foreground color variables', 20 | }, 21 | { 22 | code: '.x { color: var(--fgColor-default, var(--color-fg-default)); }', 23 | description: 'CSS > Ignores fallback old colors', 24 | }, 25 | { 26 | code: '.x { background-color: var(--bgColor-default); }', 27 | description: 'CSS > Accepts background color variables', 28 | }, 29 | { 30 | code: '.x { border-color: var(--borderColor-default); }', 31 | description: 'CSS > Accepts border color variables', 32 | }, 33 | { 34 | code: '.x { fill: var(--fgColor-default); }', 35 | description: 'CSS > Accepts fill fg color variables', 36 | }, 37 | { 38 | code: '.x { fill: var(--bgColor-default); }', 39 | description: 'CSS > Accepts fill bg color variables', 40 | }, 41 | { 42 | code: '.x { stroke: var(--borderColor-default); }', 43 | description: 'CSS > Accepts border color variables', 44 | }, 45 | { 46 | code: '.x { stroke: var(--fgColor-default); }', 47 | description: 'CSS > Accepts fg color variables', 48 | }, 49 | { 50 | code: '.x { background: var(--bgColor-default); }', 51 | description: 'CSS > Accepts bg color variables', 52 | }, 53 | { 54 | code: '.x { border: 1px solid var(--borderColor-default); }', 55 | description: 'CSS > Accepts border color variables', 56 | }, 57 | { 58 | code: '.x { border-top-left: 1px solid var(--borderColor-default); }', 59 | description: 'CSS > Accepts border color variables', 60 | }, 61 | { 62 | code: '.x { border-top-left-color: var(--borderColor-default); }', 63 | description: 'CSS > Accepts border color variables', 64 | }, 65 | { 66 | code: '.x { border-top-left-width: 1px; }', 67 | description: 'CSS > Ignores border width variables', 68 | }, 69 | { 70 | code: '.x { border: none; }', 71 | description: 'CSS > Ignores border none', 72 | }, 73 | { 74 | code: '.x { background-color: currentcolor; }', 75 | description: 'CSS > Ignores background-color currentcolor', 76 | }, 77 | { 78 | code: '.x { background-color: inherit; }', 79 | description: 'CSS > Ignores background-color inherit', 80 | }, 81 | { 82 | code: '.x { background-color: initial; }', 83 | description: 'CSS > Ignores background-color initial', 84 | }, 85 | { 86 | code: '.x { background-color: revert; }', 87 | description: 'CSS > Ignores background-color revert', 88 | }, 89 | { 90 | code: '.x { background-color: revert-layer; }', 91 | description: 'CSS > Ignores background-color revert-layer', 92 | }, 93 | { 94 | code: '.x { background-color: unset; }', 95 | description: 'CSS > Ignores background-color unset', 96 | }, 97 | { 98 | code: '.x { background-color: transparent; }', 99 | description: 'CSS > Ignores background-color transparent', 100 | }, 101 | { 102 | code: '.x { border: var(--borderWidth-thin) solid; }', 103 | description: 'CSS > Ignores border without color value', 104 | }, 105 | { 106 | code: '.x { border: var(--borderWidth-thin) solid transparent; }', 107 | description: 'CSS > Ignores border with ignored color value', 108 | }, 109 | ], 110 | reject: [ 111 | { 112 | code: '.x { color: #123; }', 113 | unfixable: true, 114 | message: messages.rejected('#123', 'fg'), 115 | line: 1, 116 | column: 13, 117 | endColumn: 17, 118 | description: 'CSS > Errors when using a hex color variable for a fg prop', 119 | }, 120 | { 121 | code: '.x { color: rgba(0,0,0,0); }', 122 | unfixable: true, 123 | message: messages.rejected('rgba(0,0,0,0)', 'fg'), 124 | line: 1, 125 | column: 13, 126 | endColumn: 26, 127 | description: 'CSS > Errors when using a hex color variable for a fg prop', 128 | }, 129 | { 130 | code: '.x { color: var(--bgColor-default); }', 131 | unfixable: true, 132 | message: messages.rejected('var(--bgColor-default)', 'fg'), 133 | line: 1, 134 | column: 17, 135 | endColumn: 34, 136 | description: 'CSS > Errors when using a bg color variable for a fg prop', 137 | }, 138 | { 139 | code: '.x { color: var(--fgColor-blue); }', 140 | unfixable: true, 141 | message: messages.rejected('var(--fgColor-blue)', 'fg'), 142 | line: 1, 143 | column: 17, 144 | endColumn: 31, 145 | description: 'CSS > Errors when variable does not exist but includes "Color"', 146 | }, 147 | { 148 | code: '.x { background-color: var(--fgColor-default); }', 149 | unfixable: true, 150 | message: messages.rejected('var(--fgColor-default)', 'bg'), 151 | line: 1, 152 | column: 28, 153 | endColumn: 45, 154 | description: 'CSS > Errors when using a fg color variable for a bg prop', 155 | }, 156 | { 157 | code: '.x { background: var(--fgColor-default); }', 158 | unfixable: true, 159 | message: messages.rejected('var(--fgColor-default)', 'bg'), 160 | line: 1, 161 | column: 22, 162 | endColumn: 39, 163 | description: 'CSS > Errors when using a fg color variable for a bg prop', 164 | }, 165 | { 166 | code: '.x { border: var(--borderWidth-thin) solid var(--fgColor-default); }', 167 | unfixable: true, 168 | message: messages.rejected('var(--fgColor-default)', 'border'), 169 | line: 1, 170 | column: 48, 171 | endColumn: 65, 172 | description: 'CSS > Errors when using a fg color variable for a border prop', 173 | }, 174 | { 175 | code: '.x { border: var(--borderWidth-thin) solid #123; }', 176 | unfixable: true, 177 | message: messages.rejected('#123', 'border'), 178 | line: 1, 179 | column: 44, 180 | endColumn: 48, 181 | description: 'CSS > Errors when using a hex color variable for a border prop', 182 | }, 183 | { 184 | code: '.x { background: center / contain no-repeat url("../../media/examples/firefox-logo.svg"), #eee 35% url("../../media/examples/lizard.png"); }', 185 | unfixable: true, 186 | message: messages.rejected('#eee', 'bg'), 187 | line: 1, 188 | column: 91, 189 | endColumn: 95, 190 | description: 'CSS > Errors when using a hex color variable for a background prop', 191 | }, 192 | { 193 | code: '.x { color: var(--base-color-scale-purple-5); }', 194 | unfixable: true, 195 | message: messages.rejected('var(--base-color-scale-purple-5)', 'fg'), 196 | line: 1, 197 | column: 17, 198 | endColumn: 44, 199 | description: 'CSS > Errors when using a variable not in primitives', 200 | }, 201 | ], 202 | }) 203 | 204 | // SCSS Specific Tests 205 | testRule({ 206 | plugins, 207 | ruleName, 208 | customSyntax: 'postcss-scss', 209 | codeFilename: 'example.scss', 210 | config: [true, {}], 211 | fix: true, 212 | cache: false, 213 | accept: [], 214 | reject: [ 215 | { 216 | code: '.x { color: $static-color-white; }', 217 | unfixable: true, 218 | message: messages.rejected('$static-color-white', 'fg'), 219 | line: 1, 220 | column: 13, 221 | endColumn: 32, 222 | description: 'SCSS > Errors when using a sass variable', 223 | }, 224 | { 225 | code: '.x { background-color: darken($static-color-blue-000, 4%); }', 226 | unfixable: true, 227 | message: messages.rejected('$static-color-blue-000', 'bg'), 228 | line: 1, 229 | column: 31, 230 | endColumn: 53, 231 | description: 'SCSS > Errors when using a sass variable', 232 | }, 233 | ], 234 | }) 235 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | import {lint} from './utils/index.js' 2 | import stylelint from 'stylelint' 3 | 4 | const SAFE_SCSS_EXAMPLE = ` 5 | .Component { float: left; } 6 | ` 7 | 8 | describe('stylelint-config', () => { 9 | it('stylelint runs with our config', () => { 10 | return lint('.uppercase { text-transform: uppercase; }').then(data => { 11 | expect(data).not.toHaveErrored() 12 | expect(data).toHaveResultsLength(1) 13 | }) 14 | }) 15 | 16 | it('produces zero warnings with valid css', () => { 17 | return lint(` 18 | .selector-x { width: 10%; } 19 | .selector-y { width: 20%; } 20 | .selector-z { width: 30%; } 21 | `).then(data => { 22 | expect(data).not.toHaveErrored() 23 | expect(data).toHaveResultsLength(1) 24 | expect(data).toHaveWarningsLength(0) 25 | }) 26 | }) 27 | 28 | it('generates two warnings with invalid css', () => { 29 | return lint(` 30 | .foo { 31 | width: 10px; 32 | top: .2em; 33 | max-width: initial; 34 | } 35 | `).then(data => { 36 | expect(data).toHaveErrored() 37 | expect(data).toHaveWarningsLength(2) 38 | expect(data).toHaveWarnings([ 39 | "Please use a primer size variable instead of '.2em'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/size (primer/spacing)", 40 | 'Unexpected value "initial" for property "max-width" (declaration-property-value-disallowed-list)', 41 | ]) 42 | }) 43 | }) 44 | 45 | it('does not report any deprecations', () => { 46 | return lint(SAFE_SCSS_EXAMPLE).then(data => { 47 | expect(data).not.toHaveErrored() 48 | expect(data).not.toHaveResultsLength(0) 49 | expect(data).toHaveDeprecationsLength(0) 50 | }) 51 | }) 52 | 53 | it('resolves css files correctly', async () => { 54 | const config = await stylelint.resolveConfig('./__fixtures__/good/example.css') 55 | expect(config).not.toHaveProperty('customSyntax') 56 | }) 57 | 58 | it('resolves tsx files correctly', async () => { 59 | const config = await stylelint.resolveConfig('./__fixtures__/good/example.tsx') 60 | expect(config).toHaveProperty('customSyntax', 'postcss-styled-syntax') 61 | }) 62 | 63 | it('resolves scss files correctly', async () => { 64 | const config = await stylelint.resolveConfig('./__fixtures__/good/example.scss') 65 | expect(config).toHaveProperty('customSyntax', 'postcss-scss') 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /__tests__/no-display-colors.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import {messages, ruleName} from '../plugins/no-display-colors.js' 3 | import {fileURLToPath} from 'url' 4 | 5 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 6 | 7 | testRule({ 8 | plugins: ['./plugins/no-display-colors'], 9 | ruleName, 10 | config: [ 11 | true, 12 | { 13 | files: [path.join(__dirname, '__fixtures__/color-vars.scss')], 14 | }, 15 | ], 16 | 17 | accept: [ 18 | {code: '.x { color: var(--fgColor-accent); }'}, 19 | {code: '.x { line-height: var(--text-display-lineHeight); }'}, 20 | ], 21 | 22 | reject: [ 23 | { 24 | code: '.x { color: var(--display-blue-fgColor); }', 25 | message: messages.rejected('--display-blue-fgColor'), 26 | line: 1, 27 | column: 6, 28 | }, 29 | { 30 | code: '.x { color: var(--display-yellow-bgColor-emphasis); }', 31 | message: messages.rejected('--display-yellow-bgColor-emphasis'), 32 | line: 1, 33 | column: 6, 34 | }, 35 | ], 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/responsive-widths.js: -------------------------------------------------------------------------------- 1 | import {ruleName} from '../plugins/responsive-widths.js' 2 | 3 | testRule({ 4 | plugins: ['./plugins/responsive-widths'], 5 | customSyntax: 'postcss-scss', 6 | ruleName, 7 | config: [true], 8 | accept: [ 9 | { 10 | code: '.x { width: 319px; }', 11 | description: 'Width is less than small viewport.', 12 | }, 13 | { 14 | code: '.x { width: 100vw; }', 15 | description: 'Width is 100vw or less.', 16 | }, 17 | { 18 | code: '.x { min-width: 319px; }', 19 | description: 'Min-width is less than small viewport.', 20 | }, 21 | { 22 | code: '.x { max-width: 1000px; }', 23 | description: 'Max-width larger than small viewport.', 24 | }, 25 | { 26 | code: '.x { width: calc(10px + 10px); }', 27 | description: 'Max-width larger than small viewport.', 28 | }, 29 | { 30 | code: '.x { @include breakpoint(md) { width: 500px; } }', 31 | description: 'Ignore widths inside breakpoint.', 32 | }, 33 | ], 34 | reject: [ 35 | { 36 | code: '.x { width: 325px; }', 37 | message: 38 | 'A value larger than the smallest viewport could break responsive pages. Use a width value smaller than 325px. https://primer.style/css/support/breakpoints (primer/responsive-widths)', 39 | line: 1, 40 | column: 13, 41 | description: 'Errors on width greater than minimum size.', 42 | }, 43 | { 44 | code: '.x { min-width: 325px; }', 45 | message: 46 | 'A value larger than the smallest viewport could break responsive pages. Use a width value smaller than 325px. https://primer.style/css/support/breakpoints (primer/responsive-widths)', 47 | line: 1, 48 | column: 17, 49 | description: 'Errors on min-width greater than minimum size.', 50 | }, 51 | { 52 | code: '.x { width: 300vw; }', 53 | message: 54 | 'A value larger than the smallest viewport could break responsive pages. Use a width value smaller than 300vw. https://primer.style/css/support/breakpoints (primer/responsive-widths)', 55 | line: 1, 56 | column: 13, 57 | description: 'Errors on viewport width greater than 100.', 58 | }, 59 | ], 60 | }) 61 | -------------------------------------------------------------------------------- /__tests__/spacing.js: -------------------------------------------------------------------------------- 1 | import plugin from '../plugins/spacing.js' 2 | import dedent from 'dedent' 3 | 4 | const plugins = [plugin] 5 | const { 6 | ruleName, 7 | rule: {messages}, 8 | } = plugin 9 | 10 | // General Tests 11 | testRule({ 12 | plugins, 13 | ruleName, 14 | config: [true, {}], 15 | fix: true, 16 | cache: false, 17 | accept: [ 18 | { 19 | code: '.x { padding: var(--base-size-4); }', 20 | description: 'CSS > One variable is valid.', 21 | }, 22 | { 23 | code: '.x { padding-bottom: var(--base-size-4); }', 24 | description: 'CSS > Works on property partial match.', 25 | }, 26 | { 27 | code: '.x { padding: var(--base-size-4) var(--base-size-8); }', 28 | description: 'CSS > Two variables are valid.', 29 | }, 30 | { 31 | code: '.x { padding: 0 0; }', 32 | description: 'CSS > Ignore zero values.', 33 | }, 34 | { 35 | code: '.x { margin: auto; }', 36 | description: 'CSS > Ignore auto values.', 37 | }, 38 | { 39 | code: '.x { top: 100%; bottom: 100vh; }', 40 | description: 'CSS > Ignore top with non-spacer units.', 41 | }, 42 | { 43 | code: '.x { border-top-width: 4px; }', 44 | description: 'CSS > Ignores values with top in name.', 45 | }, 46 | { 47 | code: '.x { padding: calc(var(--base-size-4) * 2); }', 48 | description: 'CSS > Finds variable calc values.', 49 | }, 50 | ], 51 | reject: [ 52 | { 53 | code: '.x { padding-bottom: 14px; }', 54 | unfixable: true, 55 | message: messages.rejected('14px'), 56 | line: 1, 57 | column: 22, 58 | endColumn: 26, 59 | description: 'CSS > Errors on value not in spacer list', 60 | }, 61 | { 62 | code: '.x { padding-bottom: 0.25rem; }', 63 | fixed: '.x { padding-bottom: var(--base-size-4); }', 64 | message: messages.rejected('0.25rem', {name: '--base-size-4'}), 65 | line: 1, 66 | column: 22, 67 | endColumn: 29, 68 | description: "CSS > Replaces '0.25rem' with 'var(--base-size-4)'.", 69 | }, 70 | { 71 | code: '.x { padding: 4px; }', 72 | fixed: '.x { padding: var(--base-size-4); }', 73 | message: messages.rejected('4px', {name: '--base-size-4'}), 74 | line: 1, 75 | column: 15, 76 | endColumn: 18, 77 | description: "CSS > Replaces '4px' with '--base-size-4'.", 78 | }, 79 | { 80 | code: '.x { padding: 3px; margin: 5px 7px; }', 81 | fixed: '.x { padding: var(--base-size-4); margin: var(--base-size-4) var(--base-size-8); }', 82 | description: "CSS > Replaces +- pixel values with closest variable '--base-size-4'.", 83 | warnings: [ 84 | { 85 | message: messages.rejected('3px', {name: '--base-size-4'}), 86 | line: 1, 87 | column: 15, 88 | endColumn: 18, 89 | rule: 'primer/spacing', 90 | severity: 'error', 91 | }, 92 | { 93 | message: messages.rejected('5px', {name: '--base-size-4'}), 94 | line: 1, 95 | column: 28, 96 | endColumn: 31, 97 | rule: 'primer/spacing', 98 | severity: 'error', 99 | }, 100 | { 101 | message: messages.rejected('7px', {name: '--base-size-8'}), 102 | line: 1, 103 | column: 32, 104 | endColumn: 35, 105 | rule: 'primer/spacing', 106 | severity: 'error', 107 | }, 108 | ], 109 | }, 110 | { 111 | code: '.x { padding: 4px; margin: 4px; top: 4px; right: 4px; bottom: 4px; left: 4px; }', 112 | fixed: 113 | '.x { padding: var(--base-size-4); margin: var(--base-size-4); top: var(--base-size-4); right: var(--base-size-4); bottom: var(--base-size-4); left: var(--base-size-4); }', 114 | description: "CSS > Replaces '4px' with '--base-size-4' for all properties supported.", 115 | warnings: [ 116 | { 117 | endColumn: 18, 118 | column: 15, 119 | line: 1, 120 | rule: 'primer/spacing', 121 | severity: 'error', 122 | message: messages.rejected('4px', {name: '--base-size-4'}), 123 | }, 124 | { 125 | endColumn: 31, 126 | column: 28, 127 | line: 1, 128 | rule: 'primer/spacing', 129 | severity: 'error', 130 | message: messages.rejected('4px', {name: '--base-size-4'}), 131 | }, 132 | { 133 | endColumn: 41, 134 | column: 38, 135 | line: 1, 136 | rule: 'primer/spacing', 137 | severity: 'error', 138 | message: messages.rejected('4px', {name: '--base-size-4'}), 139 | }, 140 | { 141 | endColumn: 53, 142 | column: 50, 143 | line: 1, 144 | rule: 'primer/spacing', 145 | severity: 'error', 146 | message: messages.rejected('4px', {name: '--base-size-4'}), 147 | }, 148 | { 149 | endColumn: 66, 150 | column: 63, 151 | line: 1, 152 | rule: 'primer/spacing', 153 | severity: 'error', 154 | message: messages.rejected('4px', {name: '--base-size-4'}), 155 | }, 156 | { 157 | endColumn: 77, 158 | column: 74, 159 | line: 1, 160 | rule: 'primer/spacing', 161 | severity: 'error', 162 | message: messages.rejected('4px', {name: '--base-size-4'}), 163 | }, 164 | ], 165 | }, 166 | { 167 | code: '.x { padding: -4px; }', 168 | unfixable: true, 169 | message: messages.rejected('-4px', {name: '--base-size-4'}), 170 | line: 1, 171 | column: 15, 172 | endColumn: 19, 173 | description: "CSS > Replaces '-4px' with '-$spacer-1'.", 174 | }, 175 | { 176 | code: '.x { padding: calc(8px * 2); }', 177 | fixed: '.x { padding: calc(var(--base-size-8) * 2); }', 178 | description: 'CSS > Replaces "8px" with "var(--base-size-8)" inside calc.', 179 | message: messages.rejected('8px', {name: '--base-size-8'}), 180 | line: 1, 181 | endColumn: 23, 182 | column: 20, 183 | }, 184 | { 185 | code: '.x { padding: 4px calc(var(--base-size-8) + 12px + var(--base-size-8)); }', 186 | fixed: '.x { padding: var(--base-size-4) calc(var(--base-size-8) + var(--base-size-12) + var(--base-size-8)); }', 187 | description: 'CSS > Complex calc expression.', 188 | warnings: [ 189 | { 190 | endColumn: 18, 191 | column: 15, 192 | line: 1, 193 | rule: 'primer/spacing', 194 | severity: 'error', 195 | message: messages.rejected('4px', {name: '--base-size-4'}), 196 | }, 197 | { 198 | endColumn: 49, 199 | column: 45, 200 | line: 1, 201 | rule: 'primer/spacing', 202 | severity: 'error', 203 | message: messages.rejected('12px', {name: '--base-size-12'}), 204 | }, 205 | ], 206 | }, 207 | { 208 | code: '.x { padding: 14px 4px; }', 209 | fixed: '.x { padding: 14px var(--base-size-4); }', 210 | description: "CSS > Replaces '4px' with 'var(--base-size-4)' and errors on '14px'.", 211 | warnings: [ 212 | { 213 | endColumn: 19, 214 | column: 15, 215 | line: 1, 216 | rule: 'primer/spacing', 217 | severity: 'error', 218 | message: messages.rejected('14px'), 219 | }, 220 | { 221 | endColumn: 23, 222 | column: 20, 223 | line: 1, 224 | rule: 'primer/spacing', 225 | severity: 'error', 226 | message: messages.rejected('4px', {name: '--base-size-4'}), 227 | }, 228 | ], 229 | }, 230 | { 231 | code: '.x { padding: var(--my-space); }', 232 | unfixable: true, 233 | message: messages.rejected('--my-space'), 234 | line: 1, 235 | column: 19, 236 | endColumn: 29, 237 | description: 'CSS > Errors on non-primer spacer.', 238 | }, 239 | { 240 | code: '.x { margin-right: calc(var(--my-space) * -1); }', 241 | unfixable: true, 242 | message: messages.rejected('--my-space'), 243 | line: 1, 244 | column: 29, 245 | endColumn: 39, 246 | description: 'CSS > Errors on non-primer spacer in parens.', 247 | }, 248 | ], 249 | }) 250 | 251 | // SCSS Specific Tests 252 | testRule({ 253 | plugins, 254 | ruleName, 255 | customSyntax: 'postcss-scss', 256 | codeFilename: 'example.scss', 257 | config: [true, {}], 258 | fix: true, 259 | cache: false, 260 | accept: [ 261 | { 262 | code: '.x { padding: var(--base-size-4); .y { padding: var(--base-size-8); } }', 263 | description: 'SCSS > Nested css works.', 264 | }, 265 | ], 266 | reject: [ 267 | { 268 | code: '.x { padding: -$spacer-1; }', 269 | unfixable: true, 270 | message: messages.rejected('-$spacer-1'), 271 | line: 1, 272 | column: 15, 273 | endColumn: 25, 274 | description: 'SCSS > Fails on negative SCSS variable.', 275 | }, 276 | { 277 | code: '.x { padding: 14px; .y { padding: 14px; .z { padding: 14px; } } }', 278 | unfixable: true, 279 | description: 'SCSS > Rejects nested CSS.', 280 | warnings: [ 281 | { 282 | column: 15, 283 | endColumn: 19, 284 | line: 1, 285 | rule: 'primer/spacing', 286 | severity: 'error', 287 | message: messages.rejected('14px'), 288 | }, 289 | { 290 | column: 35, 291 | endColumn: 39, 292 | line: 1, 293 | rule: 'primer/spacing', 294 | severity: 'error', 295 | message: messages.rejected('14px'), 296 | }, 297 | { 298 | column: 55, 299 | endColumn: 59, 300 | line: 1, 301 | rule: 'primer/spacing', 302 | severity: 'error', 303 | message: messages.rejected('14px'), 304 | }, 305 | ], 306 | }, 307 | ], 308 | }) 309 | 310 | // Styled Syntax Specific Tests 311 | testRule({ 312 | plugins, 313 | ruleName, 314 | customSyntax: 'postcss-styled-syntax', 315 | codeFilename: 'example.tsx', 316 | config: [true, {}], 317 | fix: true, 318 | cache: false, 319 | accept: [ 320 | { 321 | code: dedent` 322 | const X = styled.div\` 323 | padding: var(--base-size-4); 324 | \`; 325 | `, 326 | description: 'TSX > Styled components work.', 327 | }, 328 | { 329 | code: dedent` 330 | export const IconContainer = styled(Box)\` 331 | flex-shrink: 0; 332 | padding: \${themeGet('space.3')}; 333 | \` 334 | `, 335 | description: 'TSX > Ignores themeGet.', 336 | }, 337 | ], 338 | reject: [ 339 | { 340 | code: dedent` 341 | const X = styled.div\` 342 | padding: 4px; 343 | \`; 344 | `, 345 | fixed: dedent` 346 | const X = styled.div\` 347 | padding: var(--base-size-4); 348 | \`; 349 | `, 350 | message: messages.rejected('4px', {name: '--base-size-4'}), 351 | line: 2, 352 | column: 12, 353 | endColumn: 15, 354 | description: 'TSX > Fails on pixel value.', 355 | }, 356 | ], 357 | }) 358 | -------------------------------------------------------------------------------- /__tests__/typography.js: -------------------------------------------------------------------------------- 1 | import plugin from '../plugins/typography.js' 2 | 3 | const plugins = [plugin] 4 | const { 5 | ruleName, 6 | rule: {messages}, 7 | } = plugin 8 | 9 | // General Tests 10 | testRule({ 11 | plugins, 12 | ruleName, 13 | config: [true, {}], 14 | fix: true, 15 | cache: false, 16 | accept: [ 17 | // Font sizes 18 | { 19 | code: '.x { font-size: var(--text-title-size-medium); }', 20 | description: 'CSS > Accepts font size variables', 21 | }, 22 | // Font weights 23 | { 24 | code: '.x { font-weight: var(--base-text-weight-semibold); }', 25 | description: 'CSS > Accepts base font weight variables', 26 | }, 27 | { 28 | code: '.x { font-weight: var(--text-title-weight-medium); }', 29 | description: 'CSS > Accepts functional font weight variables', 30 | }, 31 | // Line heights 32 | { 33 | code: '.x { line-height: var(--text-title-lineHeight-medium); }', 34 | description: 'CSS > Accepts line height variables', 35 | }, 36 | // Font family 37 | { 38 | code: '.x { font-family: var(--fontStack-system); }', 39 | description: 'CSS > Accepts font stack variables', 40 | }, 41 | // Font shorthand 42 | { 43 | code: '.x { font: var(--text-display-shorthand); }', 44 | description: 'CSS > Accepts font shorthand variables', 45 | }, 46 | { 47 | code: '.x { font-style: italic; }', 48 | description: 'CSS > Ignores font-style property', 49 | }, 50 | ], 51 | reject: [ 52 | // Font sizes 53 | { 54 | code: '.x { font-size: 42px; }', 55 | unfixable: true, 56 | message: messages.rejected('42px'), 57 | line: 1, 58 | column: 17, 59 | endColumn: 21, 60 | description: 'CSS > Errors on value not in font size list', 61 | }, 62 | { 63 | code: '.x { font-size: 40px; }', 64 | fixed: '.x { font-size: var(--text-display-size); }', 65 | message: messages.rejected('40px', {name: '--text-display-size'}), 66 | line: 1, 67 | column: 17, 68 | endColumn: 21, 69 | description: "CSS > Replaces '40px' with 'var(--text-display-size)'.", 70 | }, 71 | { 72 | code: '.x { font-size: 2.5rem; }', 73 | fixed: '.x { font-size: var(--text-display-size); }', 74 | message: messages.rejected('2.5rem', {name: '--text-display-size'}), 75 | line: 1, 76 | column: 17, 77 | endColumn: 23, 78 | description: "CSS > Replaces '2.5rem' with 'var(--text-display-size)'.", 79 | }, 80 | // Font weights 81 | { 82 | code: '.x { font-weight: 500; }', 83 | fixed: '.x { font-weight: var(--base-text-weight-medium); }', 84 | message: messages.rejected('500', {name: '--base-text-weight-medium'}), 85 | line: 1, 86 | column: 19, 87 | endColumn: 22, 88 | description: "CSS > Errors on font-weight of 500 and suggests '--base-text-weight-medium'.", 89 | }, 90 | { 91 | code: '.x { font-weight: 100; }', 92 | fixed: '.x { font-weight: var(--base-text-weight-light); }', 93 | message: messages.rejected('100', {name: '--base-text-weight-light'}), 94 | line: 1, 95 | column: 19, 96 | endColumn: 22, 97 | description: "CSS > Replaces font-weight less than 300 with 'var(--base-text-weight-light)'.", 98 | }, 99 | { 100 | code: '.x { font-weight: 800; }', 101 | fixed: '.x { font-weight: var(--base-text-weight-semibold); }', 102 | message: messages.rejected('800', {name: '--base-text-weight-semibold'}), 103 | line: 1, 104 | column: 19, 105 | endColumn: 22, 106 | description: "CSS > Errors on font-weight greater than 600 and suggests '--base-text-weight-semibold'.", 107 | }, 108 | { 109 | code: '.x { font-weight: lighter; }', 110 | fixed: '.x { font-weight: var(--base-text-weight-light); }', 111 | message: messages.rejected('lighter', {name: '--base-text-weight-light'}), 112 | line: 1, 113 | column: 19, 114 | endColumn: 26, 115 | description: "CSS > Replaces 'lighter' font-weight keyword with 'var(--base-text-weight-light)'.", 116 | }, 117 | { 118 | code: '.x { font-weight: bold; }', 119 | fixed: '.x { font-weight: var(--base-text-weight-semibold); }', 120 | message: messages.rejected('bold', {name: '--base-text-weight-semibold'}), 121 | line: 1, 122 | column: 19, 123 | endColumn: 23, 124 | description: "CSS > Errors on 'bold' font-weight keyword and suggests '--base-text-weight-semibold'.", 125 | }, 126 | { 127 | code: '.x { font-weight: bolder; }', 128 | fixed: '.x { font-weight: var(--base-text-weight-semibold); }', 129 | message: messages.rejected('bolder', {name: '--base-text-weight-semibold'}), 130 | line: 1, 131 | column: 19, 132 | endColumn: 25, 133 | description: "CSS > Errors on 'bolder' font-weight keyword and suggests '--base-text-weight-semibold'.", 134 | }, 135 | { 136 | code: '.x { font-weight: normal; }', 137 | fixed: '.x { font-weight: var(--base-text-weight-normal); }', 138 | message: messages.rejected('normal', {name: '--base-text-weight-normal'}), 139 | line: 1, 140 | column: 19, 141 | endColumn: 25, 142 | description: "CSS > Errors on 'normal' font-weight keyword and suggests '--base-text-weight-normal'.", 143 | }, 144 | // Line heights 145 | { 146 | code: '.x { line-height: 42px; }', 147 | unfixable: true, 148 | message: messages.rejected('42px'), 149 | line: 1, 150 | column: 19, 151 | endColumn: 23, 152 | description: 'CSS > Errors on value not in line height list', 153 | }, 154 | { 155 | code: '.x { line-height: 1.4; }', 156 | fixed: '.x { line-height: var(--text-display-lineHeight); }', 157 | message: messages.rejected('1.4', {name: '--text-display-lineHeight'}), 158 | line: 1, 159 | column: 19, 160 | endColumn: 22, 161 | description: "CSS > Replaces '1.4' line-height with 'var(--text-display-lineHeight)'.", 162 | }, 163 | { 164 | code: '.x { line-height: 1.5; }', 165 | fixed: '.x { line-height: var(--text-body-lineHeight-large); }', 166 | message: messages.rejected('1.5', {name: '--text-body-lineHeight-large'}), 167 | line: 1, 168 | column: 19, 169 | endColumn: 22, 170 | description: "CSS > Errors on '1.5' line-height and suggests '--text-body-lineHeight-large'.", 171 | }, 172 | // Font family 173 | { 174 | code: '.x { font-family: Comic Sans; }', 175 | unfixable: true, 176 | message: messages.rejected('Comic Sans'), 177 | line: 1, 178 | column: 19, 179 | endColumn: 29, 180 | description: 'CSS > Errors on value not in font family list', 181 | }, 182 | // Font shorthand 183 | { 184 | code: '.x { font: bold 24px/1 sans-serif; }', 185 | unfixable: true, 186 | message: messages.rejected('bold 24px/1 sans-serif'), 187 | line: 1, 188 | column: 12, 189 | endColumn: 34, 190 | description: 'CSS > Errors on hard-coded value not in font shorthand list', 191 | }, 192 | ], 193 | }) 194 | -------------------------------------------------------------------------------- /__tests__/utils/index.js: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent' 2 | import stylelint from 'stylelint' 3 | import defaultConfig from '../../index.js' 4 | 5 | export default { 6 | defaultConfig, 7 | } 8 | 9 | export function lint(code, config = defaultConfig, options = {}) { 10 | return stylelint.lint( 11 | Object.assign(options, { 12 | code: `${dedent(code).trim()}\n`, 13 | config, 14 | }), 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/utils/setup.js: -------------------------------------------------------------------------------- 1 | expect.extend({ 2 | toHaveErrored(data) { 3 | return { 4 | pass: data.errored, 5 | message: () => `Expected "errored" === ${!data.errored}, but got ${data.errored}: 6 | - ${data.results[0].warnings.map(warning => warning.text).join('\n- ')}`, 7 | } 8 | }, 9 | 10 | toHaveResultsLength(data, length) { 11 | return { 12 | pass: data.results.length === length, 13 | message: () => `Expected results.length === ${length}, but got ${data.results.length}`, 14 | } 15 | }, 16 | 17 | toHaveWarningsLength(data, length) { 18 | const {warnings} = data.results[0] 19 | return { 20 | pass: warnings.length === length, 21 | message: () => `Expected results[0].warnings.length === ${length}, but got ${warnings.length}: 22 | - ${warnings.map(warning => warning.text).join('\n- ')}`, 23 | } 24 | }, 25 | 26 | toHaveWarnings(data, warningTexts) { 27 | const trimmedWarnings = new Set(data.results[0].warnings.map(({text}) => text.trim())) 28 | return { 29 | pass: warningTexts.every(text => trimmedWarnings.has(text)), 30 | message: () => `Expected to find the following warnings: 31 | - ${warningTexts.filter(text => !trimmedWarnings.has(text)).join('\n- ')} 32 | 33 | But got instead: 34 | - ${data.results[0].warnings.map(warning => warning.text).join('\n- ')} 35 | `, 36 | } 37 | }, 38 | 39 | toHaveDeprecationsLength(data, length) { 40 | const {deprecations} = data.results[0] 41 | return { 42 | pass: deprecations.length === length, 43 | message: () => `Expected ${length} deprecations, but got ${deprecations.length}`, 44 | } 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import browsers from '@github/browserslist-config' 2 | 3 | import borders from './plugins/borders.js' 4 | import boxShadow from './plugins/box-shadow.js' 5 | import colors from './plugins/colors.js' 6 | import responsiveWidths from './plugins/responsive-widths.js' 7 | import spacing from './plugins/spacing.js' 8 | import typography from './plugins/typography.js' 9 | import noDisplayColors from './plugins/no-display-colors.js' 10 | 11 | import {createRequire} from 'node:module' 12 | 13 | const require = createRequire(import.meta.url) 14 | 15 | /** @type {import('stylelint').Config} */ 16 | export default { 17 | extends: ['stylelint-config-standard'], 18 | ignoreFiles: ['**/*.js', '**/*.cjs', '**/*.ts', '**/*.mjs'], 19 | reportNeedlessDisables: true, 20 | plugins: [ 21 | 'stylelint-value-no-unknown-custom-properties', 22 | 'stylelint-browser-compat', 23 | borders, 24 | boxShadow, 25 | colors, 26 | responsiveWidths, 27 | spacing, 28 | typography, 29 | noDisplayColors, 30 | ], 31 | rules: { 32 | 'alpha-value-notation': 'number', 33 | 'at-rule-disallowed-list': ['extend'], 34 | 'at-rule-no-unknown': null, 35 | 'block-no-empty': true, 36 | 'color-function-notation': null, 37 | 'color-named': 'never', 38 | 'color-no-invalid-hex': true, 39 | 'comment-no-empty': null, 40 | 'csstools/value-no-unknown-custom-properties': [ 41 | true, 42 | { 43 | severity: 'warning', 44 | importFrom: [ 45 | '@primer/primitives/dist/css/functional/size/size-coarse.css', 46 | '@primer/primitives/dist/css/functional/size/border.css', 47 | '@primer/primitives/dist/css/functional/size/size.css', 48 | '@primer/primitives/dist/css/functional/size/size-fine.css', 49 | '@primer/primitives/dist/css/functional/size/breakpoints.css', 50 | '@primer/primitives/dist/css/functional/size/viewport.css', 51 | '@primer/primitives/dist/css/functional/themes/light.css', 52 | '@primer/primitives/dist/css/functional/typography/typography.css', 53 | '@primer/primitives/dist/css/base/size/size.css', 54 | '@primer/primitives/dist/css/base/typography/typography.css', 55 | ].map(path => require.resolve(path)), 56 | }, 57 | ], 58 | 'custom-property-pattern': null, 59 | 'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates']}], 60 | 'declaration-block-no-redundant-longhand-properties': null, 61 | 'declaration-block-no-shorthand-property-overrides': true, 62 | 'declaration-property-value-disallowed-list': { 63 | '/^transition/': ['/all/'], 64 | '/^background/': ['http:', 'https:'], 65 | '/.+/': ['initial'], 66 | }, 67 | 'function-calc-no-unspaced-operator': true, 68 | 'function-linear-gradient-no-nonstandard-direction': true, 69 | 'function-no-unknown': null, 70 | 'keyframes-name-pattern': null, 71 | 'max-nesting-depth': 3, 72 | 'media-feature-name-no-unknown': null, 73 | 'media-feature-name-no-vendor-prefix': null, 74 | 'no-descending-specificity': null, 75 | 'no-duplicate-selectors': true, 76 | 'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['use']}], 77 | 'number-max-precision': null, 78 | 'plugin/browser-compat': [ 79 | true, 80 | { 81 | severity: 'warning', 82 | allow: { 83 | features: [ 84 | 'properties.scrollbar-width', 85 | 'properties.scrollbar-gutter', 86 | 'properties.scrollbar-color', 87 | 'selectors.highlight', 88 | ], 89 | flagged: false, 90 | partialImplementation: true, 91 | prefix: true, 92 | }, 93 | browserslist: browsers, 94 | }, 95 | ], 96 | 'primer/borders': true, 97 | 'primer/box-shadow': true, 98 | 'primer/colors': true, 99 | 'primer/responsive-widths': true, 100 | 'primer/spacing': true, 101 | 'primer/typography': true, 102 | 'primer/no-display-colors': true, 103 | 'property-no-unknown': [ 104 | true, 105 | { 106 | ignoreProperties: ['@container', 'container-type'], 107 | }, 108 | ], 109 | 'selector-class-pattern': null, 110 | 'selector-max-compound-selectors': 3, 111 | 'selector-max-id': 0, 112 | 'selector-max-specificity': '0,4,0', 113 | 'selector-max-type': 0, 114 | 'selector-no-qualifying-type': true, 115 | 'selector-pseudo-element-no-unknown': true, 116 | 'string-no-newline': true, 117 | 'unit-no-unknown': true, 118 | 'value-keyword-case': null, 119 | 'selector-not-notation': null, 120 | 'import-notation': ['string'], 121 | 'annotation-no-unknown': null, 122 | 'keyframe-selector-notation': ['percentage-unless-within-keyword-only-block'], 123 | 'media-query-no-invalid': null, 124 | 'media-feature-range-notation': ['prefix'], 125 | 'comment-empty-line-before': null, 126 | }, 127 | overrides: [ 128 | { 129 | files: ['**/*.scss'], 130 | customSyntax: 'postcss-scss', 131 | plugins: ['stylelint-scss'], 132 | rules: { 133 | 'scss/at-extend-no-missing-placeholder': true, 134 | 'scss/at-rule-no-unknown': true, 135 | 'scss/declaration-nested-properties-no-divided-groups': true, 136 | 'scss/dollar-variable-no-missing-interpolation': true, 137 | 'scss/function-quote-no-quoted-strings-inside': true, 138 | 'scss/function-unquote-no-unquoted-strings-inside': true, 139 | 'scss/no-duplicate-mixins': true, 140 | 'scss/selector-no-redundant-nesting-selector': true, 141 | }, 142 | }, 143 | { 144 | files: ['**/*.tsx'], 145 | customSyntax: 'postcss-styled-syntax', 146 | rules: { 147 | 'rule-empty-line-before': null, 148 | 'declaration-empty-line-before': null, 149 | 'length-zero-no-unit': null, 150 | 'selector-max-type': null, 151 | 'primer/colors': null, 152 | }, 153 | }, 154 | { 155 | files: ['**/*.pcss'], 156 | rules: { 157 | 'media-feature-range-notation': null, 158 | 'import-notation': null, 159 | 'custom-property-pattern': null, 160 | 'selector-class-pattern': null, 161 | 'keyframes-name-pattern': null, 162 | 'no-descending-specificity': null, 163 | 'declaration-block-no-redundant-longhand-properties': null, 164 | 'color-function-notation': 'legacy', 165 | 'selector-nested-pattern': '^&\\s?\\W', 166 | 'at-rule-no-unknown': [ 167 | true, 168 | { 169 | ignoreAtRules: ['mixin', 'define-mixin'], 170 | }, 171 | ], 172 | }, 173 | }, 174 | { 175 | files: ['**/*.module.css'], 176 | rules: { 177 | 'property-no-unknown': [ 178 | true, 179 | { 180 | ignoreProperties: ['composes', 'compose-with'], 181 | ignoreSelectors: [':export', /^:import/], 182 | }, 183 | ], 184 | 'selector-pseudo-class-no-unknown': [ 185 | true, 186 | {ignorePseudoClasses: ['export', 'import', 'global', 'local', 'external']}, 187 | ], 188 | 'selector-type-no-unknown': [ 189 | true, 190 | { 191 | ignoreTypes: ['from'], 192 | }, 193 | ], 194 | 'function-no-unknown': [ 195 | true, 196 | { 197 | ignoreFunctions: ['global'], 198 | }, 199 | ], 200 | }, 201 | }, 202 | ], 203 | } 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@primer/stylelint-config", 3 | "version": "13.3.0", 4 | "description": "Sharable stylelint config used by GitHub's CSS", 5 | "author": "GitHub, Inc.", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "dist/index.cjs", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.cjs" 13 | } 14 | }, 15 | "browserslist": "extends @github/browserslist-config", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/primer/stylelint-config.git" 19 | }, 20 | "keywords": [ 21 | "github", 22 | "primer", 23 | "css", 24 | "stylelint-config", 25 | "stylelint" 26 | ], 27 | "files": [ 28 | "dist/", 29 | "plugins/" 30 | ], 31 | "engines": { 32 | "node": ">16.0.0" 33 | }, 34 | "scripts": { 35 | "pretest": "npm run build", 36 | "build": "rollup -c", 37 | "clean": "rimraf dist", 38 | "test": "npm run test:jest && npm run test:stylelint", 39 | "test:jest": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --coverage false", 40 | "test:stylelint": "stylelint __tests__/__fixtures__/good/", 41 | "lint": "eslint .", 42 | "release": "changeset publish" 43 | }, 44 | "dependencies": { 45 | "@github/browserslist-config": "^1.0.0", 46 | "postcss-scss": "^4.0.2", 47 | "postcss-styled-syntax": "^0.7.0", 48 | "postcss-value-parser": "^4.0.2", 49 | "string.prototype.matchall": "^4.0.2", 50 | "stylelint": "^16.11.0", 51 | "stylelint-browser-compat": "^1.0.0-beta.140", 52 | "stylelint-config-standard": "^38.0.0", 53 | "stylelint-scss": "^6.2.0", 54 | "stylelint-value-no-unknown-custom-properties": "^6.0.1" 55 | }, 56 | "prettier": "@github/prettier-config", 57 | "devDependencies": { 58 | "@changesets/changelog-github": "^0.5.0", 59 | "@changesets/cli": "2.27.10", 60 | "@github/prettier-config": "^0.0.6", 61 | "@primer/primitives": "^10.4.0", 62 | "@rollup/plugin-commonjs": "^28.0.0", 63 | "@rollup/plugin-json": "^6.1.0", 64 | "@rollup/plugin-node-resolve": "^15.2.3", 65 | "@typescript-eslint/parser": "^8.0.1", 66 | "dedent": "^1.5.3", 67 | "eslint": "^8.57.1", 68 | "eslint-plugin-github": "^5.0.1", 69 | "eslint-plugin-import": "^2.29.1", 70 | "eslint-plugin-jest": "^28.2.0", 71 | "eslint-plugin-prettier": "^5.1.3", 72 | "jest": "^29.7.0", 73 | "jest-preset-stylelint": "^7.0.0", 74 | "prettier": "^3.2.5", 75 | "rimraf": "^6.0.1", 76 | "rollup": "^4.21.1" 77 | }, 78 | "peerDependencies": { 79 | "@primer/primitives": "9.x || 10.x" 80 | }, 81 | "jest": { 82 | "transform": {}, 83 | "preset": "jest-preset-stylelint", 84 | "collectCoverage": true, 85 | "collectCoverageFrom": [ 86 | "src/**/*.js", 87 | "plugins/**/*.js" 88 | ], 89 | "setupFilesAfterEnv": [ 90 | "/__tests__/utils/setup.js" 91 | ], 92 | "testPathIgnorePatterns": [ 93 | "/node_modules/", 94 | "__tests__/utils", 95 | "__tests__/__fixtures__" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Primer stylelint plugins 2 | 3 | This directory contains all of our custom stylelint plugins, each of which provides a single stylelint rule. 4 | 5 | ### Rules 6 | 7 | - [Primer stylelint plugins](#primer-stylelint-plugins) 8 | - [Rules](#rules) 9 | - [Usage](#usage) 10 | - [`primer/colors`](#primercolors) 11 | - [`primer/spacing`](#primerspacing) 12 | - [`primer/typography`](#primertypography) 13 | - [`primer/borders`](#primerborders) 14 | - [`primer/box-shadow`](#primerbox-shadow) 15 | - [`primer/responsive-widths`](#primerresponsive-widths) 16 | - [Variable rules](#variable-rules) 17 | - [Variable rule options](#variable-rule-options) 18 | 19 | These were intended for use with [Primer CSS] and GitHub projects, but you may find them useful elsewhere. 20 | 21 | ## Usage 22 | 23 | If you're using or extending `@primer/stylelint-config` already, then you're using all of these plugins by default. See [`index.js`](../index.js) for the config defaults. 24 | 25 | If you're _not_ using or extending `@primer/stylelint-config`, you can still reference the plugins by referencing their module paths like so: 26 | 27 | ```js 28 | // stylelint.config.js 29 | module.exports = { 30 | plugins: ['@primer/stylelint-config/plugins/colors'] 31 | } 32 | ``` 33 | 34 | ## `primer/colors` 35 | 36 | This [variable rule](#variable-rules) enforces the use of Primer [color system](https://primer.style/css/support/color-system) variables for `color` and `background-color` CSS properties. Generally speaking, variables matching the pattern `$text-*` are acceptable for `color` (and `fill`), and `$bg-*` are acceptable for `background-color`. See [the configuration](./colors.js) for more info. 37 | 38 | ```scss 39 | body { 40 | color: black; 41 | } 42 | /** ↑ 43 | * FAIL: Use a variable. */ 44 | 45 | body { 46 | color: $gray-900; 47 | } 48 | /** ↑ 49 | * FAIL: Use $text-gray-dark instead. */ 50 | ``` 51 | 52 | ## `primer/spacing` 53 | 54 | This [variable rule](#variable-rules) enforces the use of Primer [spacing variables](https://primer.style/css/support/spacing) in margin and padding CSS properties. See [the configuration](./spacing.js) for more info. 55 | 56 | ```scss 57 | ul { 58 | margin: 0 0 $spacer-3; 59 | } 60 | /** ↑ 61 | * OK: "0" and "$spacer-*" are allowed values */ 62 | 63 | ul { 64 | margin: 0 0 16px; 65 | } 66 | /** ↑ 67 | * FAIL: Use "$spacer-3" (auto-fixable!) */ 68 | ``` 69 | 70 | ## `primer/typography` 71 | 72 | This [variable rule](#variable-rules) enforces the use of [typography variables](https://primer.style/css/support/typography#typography-variables) for `font-size`, `font-weight`, and `line-height` CSS properties. See [the configuration](./typography.js) for more info. 73 | 74 | ## `primer/borders` 75 | 76 | This [variable rule](#variable-rules) enforces the use of border-specific variables (`$border-width`, `$border-style`, and `$border-color*`, and the `$border` shorthand) for all border CSS properties (including the `border` shorthand). The values `0` and `none` are also allowed; see [the configuration](./borders.js) for more info. 77 | 78 | ## `primer/box-shadow` 79 | 80 | This [variable rule](#variable-rules) enforces the use of `$box-shadow*` variables for the `box-shadow` CSS property. See [the configuration](./box-shadow.js) for more info. 81 | 82 | ## `primer/responsive-widths` 83 | 84 | This plugin checks for `width` and `min-width` declarations that use a value less than the minimum browser size. `320px` 85 | 86 | ## Variable rules 87 | 88 | Variable rules are created using a general-purpose helper that can validate constraints for matching CSS properties and values. In general, the Primer CSS variable rules enforce two basic principles for custom CSS: 89 | 90 | - Use Primer variables whenever possible. 91 | - Use _functional_ variables (`$text-gray` vs. `$gray-700`) whenever possible. 92 | 93 | Validations take the form of an object literal in which each key/value pair defines a set of named constraints that apply to one or more CSS properties: 94 | 95 | ```js 96 | { 97 | 'background color': { 98 | props: 'background-color', 99 | // ... 100 | }, 101 | 'foreground color': { 102 | props: ['color', 'fill'], 103 | // ... 104 | } 105 | } 106 | ``` 107 | 108 | The objects in each named rule may have the following keys: 109 | 110 | - `props` is an array or string of [glob patterns] that match CSS properties. For individual properties like `color`, a string without any special glob characters works just fine. You can use brace expansion to match directional properties: 111 | 112 | ```js 113 | 'border width': { 114 | props: 'border{,-top,-right,-bottom,-left}-width', 115 | // which expands to: 116 | props: [ 117 | 'border-width', 118 | 'border-top-width', 119 | 'border-right-width', 120 | 'border-bottom-width', 121 | 'border-left-width' 122 | ] 123 | } 124 | ``` 125 | 126 | **Note:** if no `props` are listed, the name of the rule is assumed to be the CSS property. 127 | 128 | - `values` is, similar to `props`, an array or string of [glob patterns] that match _individual CSS values_ in a single property. Property values are parsed with [postcss-value-parser](https://www.npmjs.com/package/postcss-value-parser), so they respect parentheses, functions, and values within them. If a property has more than one value (e.g. `margin: 0 auto`), each one is compared against the `values` list to determine its validity, and a warning is generated for each invalid value. 129 | 130 | For example, if we fleshed out the `border width` rule defined above with `values`: 131 | 132 | ```js 133 | 'border width': { 134 | props: 'border{,-top,-right,-bottom,-left}-width', 135 | values: ['$border-*', '0'] 136 | } 137 | ``` 138 | 139 | Then the following SCSS checks out: 140 | 141 | ```scss 142 | .Box { 143 | border-width: 0 0 $border-width; 144 | /** ↑ ↑ ↑ 145 | * ↑ ↑ OK! 146 | * ↑ OK! 147 | * OK! */ 148 | } 149 | ``` 150 | 151 | - `components` tells the rule that multiple values resolve to a list of ordered properties with their own, separately defined rules. This makes it possible for shorthand CSS properties like `border`, `background`, or `font` to "delegate" validation to a rule with more specific constraints. For example, you could enforce different types of border variables for most of the CSS border properties with: 152 | 153 | ```js 154 | 'border': { 155 | props: 'border{,-top,-right,-bottom,-left}', 156 | components: ['border-width', 'border-style', 'border-color'] 157 | }, 158 | 'border width': { 159 | props: 'border{,-top,-right,-bottom,-left}-width', 160 | values: ['$border-width', '0'] 161 | }, 162 | 'border style': { 163 | props: 'border{,-top,-right,-bottom,-left}-style', 164 | values: ['$border-style', 'none'] 165 | }, 166 | 'border color': { 167 | props: 'border{,-top,-right,-bottom,-left}-color', 168 | values: ['$border-*', 'transparent'] 169 | } 170 | ``` 171 | 172 | - `replacements` is an object listing property values that can safely be replaced via `stylelint --fix` with other variable or static values, as in the Primer CSS `font-size` rule: 173 | 174 | ```js 175 | 'font-size': { 176 | props: 'font-size', 177 | values: ['$h{0,1,2,3,4,5,6}-size', '$font-size-small'], 178 | replacements: { 179 | '40px': '$h0-size', 180 | '32px': '$h1-size', 181 | '24px': '$h2-size', 182 | '20px': '$h3-size', 183 | '16px': '$h4-size', 184 | '14px': '$h5-size', 185 | '12px': '$h6-size' 186 | } 187 | } 188 | ``` 189 | 190 | ### Variable rule options 191 | 192 | All variable rules respect the following rule options, as in: 193 | 194 | ```js 195 | // stylelint.config.js 196 | module.exports = { 197 | extends: '@primer/stylelint-config', 198 | rules: { 199 | 'primer/colors': [true /* options here */] 200 | /* ↑ 201 | * false disables the rule */ 202 | } 203 | } 204 | ``` 205 | 206 | - `rules` extends the validations for the rule, and can be used to disable specific validations, as e.g. 207 | 208 | ```js 209 | rules: { 210 | 'primer/colors': [true, { 211 | rules: { 212 | 'background color': false, // disabled 213 | 'text color': { 214 | // override the text color validation rules here 215 | } 216 | }] 217 | } 218 | ``` 219 | 220 | - `verbose` is a boolean that enables chatty `console.warn()` messages that may help you debug more complicated configurations. 221 | 222 | - `disableFix` is a boolean that can disable auto-fixing of this rule when running `stylelint --fix` to auto-fix other rules. 223 | 224 | [primer css]: https://primer.style/css 225 | [glob patterns]: http://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm 226 | -------------------------------------------------------------------------------- /plugins/borders.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint' 2 | import {declarationValueIndex} from 'stylelint/lib/utils/nodeFieldIndices.cjs' 3 | import valueParser from 'postcss-value-parser' 4 | import {walkGroups, primitivesVariables} from './lib/utils.js' 5 | 6 | const { 7 | createPlugin, 8 | utils: {report, ruleMessages, validateOptions}, 9 | } = stylelint 10 | 11 | export const ruleName = 'primer/borders' 12 | export const messages = ruleMessages(ruleName, { 13 | rejected: (value, replacement, propName) => { 14 | if (propName && propName.includes('radius') && value.includes('borderWidth')) { 15 | return `Border radius variables can not be used for border widths` 16 | } 17 | 18 | if ((propName && propName.includes('width')) || (borderShorthand(propName) && value.includes('borderRadius'))) { 19 | return `Border width variables can not be used for border radii` 20 | } 21 | 22 | if (!replacement) { 23 | return `Please use a Primer border variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/size#border` 24 | } 25 | 26 | return `Please replace '${value}' with a Primer border variable '${replacement['name']}'. https://primer.style/foundations/primitives/size#border` 27 | }, 28 | }) 29 | 30 | const variables = primitivesVariables('border') 31 | const sizes = [] 32 | const radii = [] 33 | 34 | // Props that we want to check 35 | const propList = ['border', 'border-width', 'border-radius'] 36 | // Values that we want to ignore 37 | const valueList = ['${'] 38 | 39 | const borderShorthand = prop => 40 | /^border(-(top|right|bottom|left|block-start|block-end|inline-start|inline-end))?$/.test(prop) 41 | 42 | for (const variable of variables) { 43 | const name = variable['name'] 44 | 45 | if (name.includes('borderWidth')) { 46 | const value = variable['values'] 47 | .pop() 48 | .replace(/max|\(|\)/g, '') 49 | .split(',')[0] 50 | sizes.push({ 51 | name, 52 | values: [value], 53 | }) 54 | } 55 | 56 | if (name.includes('borderRadius')) { 57 | radii.push(variable) 58 | } 59 | } 60 | 61 | /** @type {import('stylelint').Rule} */ 62 | const ruleFunction = primary => { 63 | return (root, result) => { 64 | const validOptions = validateOptions(result, ruleName, { 65 | actual: primary, 66 | possible: [true], 67 | }) 68 | 69 | if (!validOptions) return 70 | 71 | root.walkDecls(declNode => { 72 | const {prop, value} = declNode 73 | 74 | if (!propList.some(borderProp => prop.startsWith(borderProp))) return 75 | if (/^border(-(top|right|bottom|left|block-start|block-end|inline-start|inline-end))?-color$/.test(prop)) return 76 | if (valueList.some(valueToIgnore => value.includes(valueToIgnore))) return 77 | 78 | const parsedValue = walkGroups(valueParser(value), node => { 79 | const checkForVariable = (vars, nodeValue) => 80 | vars.some(variable => 81 | new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue), 82 | ) 83 | 84 | // Only check word types. https://github.com/TrySound/postcss-value-parser#word 85 | if (node.type !== 'word') { 86 | return 87 | } 88 | 89 | // Exact values to ignore. 90 | if ( 91 | [ 92 | '*', 93 | '+', 94 | '-', 95 | '/', 96 | '0', 97 | 'none', 98 | 'inherit', 99 | 'initial', 100 | 'revert', 101 | 'revert-layer', 102 | 'unset', 103 | 'solid', 104 | 'dashed', 105 | 'dotted', 106 | 'transparent', 107 | ].includes(node.value) 108 | ) { 109 | return 110 | } 111 | 112 | const valueUnit = valueParser.unit(node.value) 113 | 114 | if (valueUnit && (valueUnit.unit === '' || !/^-?[0-9.]+$/.test(valueUnit.number))) { 115 | return 116 | } 117 | 118 | // Skip if the value unit isn't a supported unit. 119 | if (valueUnit && !['px', 'rem', 'em'].includes(valueUnit.unit)) { 120 | return 121 | } 122 | 123 | // if we're looking at the border property that sets color in shorthand, don't bother checking the color 124 | if ( 125 | // using border shorthand 126 | borderShorthand(prop) && 127 | // includes a color as a third space-separated value 128 | value.split(' ').length > 2 && 129 | // the color in the third space-separated value includes `node.value` 130 | value 131 | .split(' ') 132 | .slice(2) 133 | .some(color => color.includes(node.value)) 134 | ) { 135 | return 136 | } 137 | 138 | // If the variable is found in the value, skip it. 139 | if (prop.includes('width') || borderShorthand(prop)) { 140 | if (checkForVariable(sizes, node.value)) { 141 | return 142 | } 143 | } 144 | 145 | if (prop.includes('radius')) { 146 | if (checkForVariable(radii, node.value)) { 147 | return 148 | } 149 | } 150 | 151 | const replacement = (prop.includes('radius') ? radii : sizes).find(variable => 152 | variable.values.includes(node.value.replace('-', '')), 153 | ) 154 | const fixable = replacement && valueUnit && !valueUnit.number.includes('-') 155 | let fix = undefined 156 | if (fixable) { 157 | fix = () => { 158 | node.value = node.value.replace(node.value, `var(${replacement['name']})`) 159 | } 160 | } 161 | report({ 162 | index: declarationValueIndex(declNode) + node.sourceIndex, 163 | endIndex: declarationValueIndex(declNode) + node.sourceIndex + node.value.length, 164 | message: messages.rejected(node.value, replacement, prop), 165 | node: declNode, 166 | result, 167 | ruleName, 168 | fix, 169 | }) 170 | 171 | return 172 | }) 173 | 174 | declNode.value = parsedValue.toString() 175 | }) 176 | } 177 | } 178 | 179 | ruleFunction.ruleName = ruleName 180 | ruleFunction.messages = messages 181 | ruleFunction.meta = { 182 | fixable: true, 183 | } 184 | 185 | export default createPlugin(ruleName, ruleFunction) 186 | -------------------------------------------------------------------------------- /plugins/box-shadow.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint' 2 | import {declarationValueIndex} from 'stylelint/lib/utils/nodeFieldIndices.cjs' 3 | import {primitivesVariables} from './lib/utils.js' 4 | 5 | const { 6 | createPlugin, 7 | utils: {report, ruleMessages, validateOptions}, 8 | } = stylelint 9 | 10 | export const ruleName = 'primer/box-shadow' 11 | export const messages = ruleMessages(ruleName, { 12 | rejected: (value, replacement) => { 13 | if (!replacement) { 14 | return `Please use a Primer box-shadow variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/color#shadow or https://primer.style/foundations/primitives/size#border-size` 15 | } 16 | 17 | return `Please replace '${value}' with a Primer box-shadow variable '${replacement['name']}'. https://primer.style/foundations/primitives/color#shadow or https://primer.style/foundations/primitives/size#border-size` 18 | }, 19 | }) 20 | 21 | const variables = primitivesVariables('box-shadow') 22 | const shadows = [] 23 | 24 | for (const variable of variables) { 25 | const name = variable['name'] 26 | 27 | // TODO: Decide if this is safe. Someday we might have variables that 28 | // have 'shadow' in the name but aren't full box-shadows. 29 | if (name.includes('shadow') || name.includes('boxShadow')) { 30 | shadows.push(variable) 31 | } 32 | } 33 | 34 | /** @type {import('stylelint').Rule} */ 35 | const ruleFunction = primary => { 36 | return (root, result) => { 37 | const validOptions = validateOptions(result, ruleName, { 38 | actual: primary, 39 | possible: [true], 40 | }) 41 | const validValues = shadows 42 | 43 | if (!validOptions) return 44 | 45 | root.walkDecls(declNode => { 46 | const {prop, value} = declNode 47 | 48 | if (prop !== 'box-shadow') return 49 | 50 | if (value === 'none') return 51 | 52 | const checkForVariable = (vars, nodeValue) => { 53 | return vars.some(variable => 54 | new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue), 55 | ) 56 | } 57 | 58 | if (checkForVariable(validValues, value)) { 59 | return 60 | } 61 | 62 | const replacement = validValues.find(variable => variable.values.includes(value)) 63 | let fix = undefined 64 | if (replacement) { 65 | fix = () => { 66 | declNode.value = value.replace(value, `var(${replacement['name']})`) 67 | } 68 | } 69 | 70 | report({ 71 | index: declarationValueIndex(declNode), 72 | endIndex: declarationValueIndex(declNode) + value.length, 73 | message: messages.rejected(value, replacement), 74 | node: declNode, 75 | result, 76 | ruleName, 77 | fix, 78 | }) 79 | }) 80 | } 81 | } 82 | 83 | ruleFunction.ruleName = ruleName 84 | ruleFunction.messages = messages 85 | ruleFunction.meta = { 86 | fixable: true, 87 | } 88 | 89 | export default createPlugin(ruleName, ruleFunction) 90 | -------------------------------------------------------------------------------- /plugins/colors.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint' 2 | import {declarationValueIndex} from 'stylelint/lib/utils/nodeFieldIndices.cjs' 3 | import {primitivesVariables, hasValidColor} from './lib/utils.js' 4 | import valueParser from 'postcss-value-parser' 5 | 6 | const { 7 | createPlugin, 8 | utils: {report, ruleMessages, validateOptions}, 9 | } = stylelint 10 | 11 | export const ruleName = 'primer/colors' 12 | export const messages = ruleMessages(ruleName, { 13 | rejected: (value, type) => { 14 | if (type === 'fg') { 15 | return `Please use a Primer foreground color variable instead of '${value}'. https://primer.style/foundations/primitives/color#foreground` 16 | } else if (type === 'bg') { 17 | return `Please use a Primer background color variable instead of '${value}'. https://primer.style/foundations/primitives/color#background` 18 | } else if (type === 'border') { 19 | return `Please use a Primer border color variable instead of '${value}'. https://primer.style/foundations/primitives/color#border` 20 | } 21 | return `Please use with a Primer color variable instead of '${value}'. https://primer.style/foundations/primitives/color` 22 | }, 23 | }) 24 | 25 | let variables = primitivesVariables('colors') 26 | const validProps = { 27 | '^color$': ['fgColor', 'iconColor'], 28 | '^background(-color)?$': ['bgColor'], 29 | '^border(-top|-right|-bottom|-left|-inline|-block)*(-color)?$': ['borderColor'], 30 | '^fill$': ['fgColor', 'iconColor', 'bgColor'], 31 | '^stroke$': ['fgColor', 'iconColor', 'bgColor', 'borderColor'], 32 | } 33 | 34 | const validValues = [ 35 | 'none', 36 | 'currentcolor', 37 | 'inherit', 38 | 'initial', 39 | 'unset', 40 | 'revert', 41 | 'revert-layer', 42 | 'transparent', 43 | '0', 44 | ] 45 | 46 | const propType = prop => { 47 | if (/^color/.test(prop)) { 48 | return 'fg' 49 | } else if (/^background(-color)?$/.test(prop)) { 50 | return 'bg' 51 | } else if (/^border(-top|-right|-bottom|-left|-inline|-block)*(-color)?$/.test(prop)) { 52 | return 'border' 53 | } else if (/^fill$/.test(prop)) { 54 | return 'fg' 55 | } else if (/^stroke$/.test(prop)) { 56 | return 'fg' 57 | } 58 | return undefined 59 | } 60 | 61 | variables = variables.filter(variable => { 62 | const name = variable['name'] 63 | // remove shadow and boxShadow variables 64 | return !(name.includes('shadow') || name.includes('boxShadow')) 65 | }) 66 | 67 | /** @type {import('stylelint').Rule} */ 68 | const ruleFunction = primary => { 69 | return (root, result) => { 70 | const validOptions = validateOptions(result, ruleName, { 71 | actual: primary, 72 | possible: [true], 73 | }) 74 | if (!validOptions) return 75 | 76 | const valueIsCorrectType = (value, types) => types.some(type => value.includes(type)) 77 | 78 | root.walkDecls(declNode => { 79 | const {prop, value} = declNode 80 | 81 | // Skip if prop is not a valid color prop 82 | if (!Object.keys(validProps).some(validProp => new RegExp(validProp).test(prop))) return 83 | 84 | // Get the valid types for the prop 85 | const types = validProps[Object.keys(validProps).find(re => new RegExp(re).test(prop))] 86 | 87 | // Walk the value split 88 | valueParser(value).walk(valueNode => { 89 | // Skip if value is not a word or function 90 | if (valueNode.type !== 'word' && valueNode.type !== 'function') return 91 | 92 | // Skip if value is a valid value 93 | if (validValues.includes(valueNode.value)) return 94 | 95 | if (hasValidColor(valueNode.value) || /^\$/.test(valueNode.value)) { 96 | const rejectedValue = 97 | valueNode.type === 'function' 98 | ? `${valueNode.value}(${valueParser.stringify(valueNode.nodes)})` 99 | : valueNode.value 100 | 101 | report({ 102 | index: declarationValueIndex(declNode) + valueNode.sourceIndex, 103 | endIndex: declarationValueIndex(declNode) + valueNode.sourceEndIndex, 104 | message: messages.rejected(rejectedValue, propType(prop)), 105 | node: declNode, 106 | result, 107 | ruleName, 108 | }) 109 | return 110 | } 111 | 112 | // Skip functions 113 | if (valueNode.type === 'function') { 114 | return 115 | } 116 | 117 | // Variable exists and is the correct type (fg, bg, border) 118 | if ( 119 | variables.some(variable => new RegExp(variable['name']).test(valueNode.value)) && 120 | valueIsCorrectType(valueNode.value, types) 121 | ) { 122 | return 123 | } 124 | 125 | // Value doesn't start with variable -- 126 | if (!valueNode.value.startsWith('--')) { 127 | return 128 | } 129 | 130 | // Ignore old system colors --color-* 131 | if ( 132 | [ 133 | /^--color-(?:[a-zA-Z0-9-]+-)*text(?:-[a-zA-Z0-9-]+)*$/, 134 | /^--color-(?:[a-zA-Z0-9-](?!-))*-fg(?:-[a-zA-Z0-9-]+)*$/, 135 | /^--color-[^)]+$/, 136 | ].some(oldSysRe => oldSysRe.test(valueNode.value)) 137 | ) { 138 | return 139 | } 140 | 141 | // Property is shortand and value doesn't include color 142 | if ( 143 | (/^border(-top|-right|-bottom|-left|-inline|-block)*$/.test(prop) || /^background$/.test(prop)) && 144 | !valueNode.value.toLowerCase().includes('color') 145 | ) { 146 | return 147 | } 148 | 149 | report({ 150 | index: declarationValueIndex(declNode) + valueNode.sourceIndex, 151 | endIndex: declarationValueIndex(declNode) + valueNode.sourceEndIndex, 152 | message: messages.rejected(`var(${valueNode.value})`, propType(prop)), 153 | node: declNode, 154 | result, 155 | ruleName, 156 | }) 157 | }) 158 | }) 159 | } 160 | } 161 | 162 | ruleFunction.ruleName = ruleName 163 | ruleFunction.messages = messages 164 | ruleFunction.meta = { 165 | fixable: false, 166 | } 167 | 168 | export default createPlugin(ruleName, ruleFunction) 169 | -------------------------------------------------------------------------------- /plugins/lib/utils.js: -------------------------------------------------------------------------------- 1 | import {createRequire} from 'node:module' 2 | 3 | const require = createRequire(import.meta.url) 4 | 5 | export function primitivesVariables(type) { 6 | const variables = [] 7 | 8 | const files = [] 9 | switch (type) { 10 | case 'spacing': 11 | files.push('base/size/size.json') 12 | break 13 | case 'border': 14 | files.push('functional/size/border.json') 15 | break 16 | case 'typography': 17 | files.push('base/typography/typography.json') 18 | files.push('functional/typography/typography.json') 19 | break 20 | case 'box-shadow': 21 | files.push('functional/themes/light.json') 22 | files.push('functional/size/border.json') 23 | break 24 | case 'colors': 25 | files.push('functional/themes/light.json') 26 | break 27 | } 28 | 29 | for (const file of files) { 30 | // eslint-disable-next-line import/no-dynamic-require 31 | const data = require(`@primer/primitives/dist/styleLint/${file}`) 32 | 33 | for (const key of Object.keys(data)) { 34 | const token = data[key] 35 | const valueProp = '$value' in token ? '$value' : 'value' 36 | const values = typeof token[valueProp] === 'string' ? [token[valueProp]] : token[valueProp] 37 | 38 | variables.push({ 39 | name: `--${token['name']}`, 40 | values, 41 | }) 42 | } 43 | } 44 | 45 | return variables 46 | } 47 | 48 | const HAS_VALID_HEX = /#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})(?:$|[^\da-f])/i 49 | const COLOR_FUNCTION_NAMES = ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch'] 50 | 51 | /** 52 | * Check if a value contains a valid 3, 4, 6 or 8 digit hex 53 | * 54 | * @param {string} value 55 | * @returns {boolean} 56 | */ 57 | export function hasValidColor(value) { 58 | return HAS_VALID_HEX.test(value) || COLOR_FUNCTION_NAMES.includes(value) 59 | } 60 | 61 | export function walkGroups(root, validate) { 62 | for (const node of root.nodes) { 63 | if (node.type === 'function') { 64 | walkGroups(node, validate) 65 | } else { 66 | validate(node) 67 | } 68 | } 69 | return root 70 | } 71 | -------------------------------------------------------------------------------- /plugins/no-display-colors.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint' 2 | import matchAll from 'string.prototype.matchall' 3 | 4 | export const ruleName = 'primer/no-display-colors' 5 | export const messages = stylelint.utils.ruleMessages(ruleName, { 6 | rejected: varName => `${varName} is in alpha and should be used with caution with approval from the Primer team`, 7 | }) 8 | 9 | // Match CSS variable references (e.g var(--display-blue-fgColor)) 10 | // eslint-disable-next-line no-useless-escape 11 | const variableReferenceRegex = /var\(([^\),]+)(,.*)?\)/g 12 | 13 | export default stylelint.createPlugin(ruleName, (enabled, options = {}) => { 14 | if (!enabled) { 15 | return noop 16 | } 17 | 18 | const {verbose = false} = options 19 | // eslint-disable-next-line no-console 20 | const log = verbose ? (...args) => console.warn(...args) : noop 21 | 22 | // Keep track of declarations we've already seen 23 | const seen = new WeakMap() 24 | 25 | return (root, result) => { 26 | root.walkRules(rule => { 27 | rule.walkDecls(decl => { 28 | if (seen.has(decl)) { 29 | return 30 | } else { 31 | seen.set(decl, true) 32 | } 33 | 34 | for (const [, variableName] of matchAll(decl.value, variableReferenceRegex)) { 35 | log(`Found variable reference ${variableName}`) 36 | if (variableName.match(/^--display-.*/)) { 37 | stylelint.utils.report({ 38 | message: messages.rejected(variableName), 39 | node: decl, 40 | result, 41 | ruleName, 42 | }) 43 | } 44 | } 45 | }) 46 | }) 47 | } 48 | }) 49 | 50 | function noop() {} 51 | -------------------------------------------------------------------------------- /plugins/responsive-widths.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint' 2 | import {declarationValueIndex} from 'stylelint/lib/utils/nodeFieldIndices.cjs' 3 | import valueParser from 'postcss-value-parser' 4 | 5 | export const ruleName = 'primer/responsive-widths' 6 | 7 | export const messages = stylelint.utils.ruleMessages(ruleName, { 8 | rejected: value => { 9 | return `A value larger than the smallest viewport could break responsive pages. Use a width value smaller than ${value}. https://primer.style/css/support/breakpoints` 10 | }, 11 | }) 12 | 13 | // 320px is the smallest viewport size that we support 14 | 15 | const walkGroups = (root, validate) => { 16 | for (const node of root.nodes) { 17 | if (node.type === 'function') { 18 | walkGroups(node, validate) 19 | } else { 20 | validate(node) 21 | } 22 | } 23 | return root 24 | } 25 | 26 | // eslint-disable-next-line no-unused-vars 27 | export default stylelint.createPlugin(ruleName, (enabled, options = {}, context) => { 28 | if (!enabled) { 29 | return noop 30 | } 31 | 32 | const lintResult = (root, result) => { 33 | root.walk(decl => { 34 | // Ignore things inside of breakpoints 35 | if (decl.type === 'atrule' && decl.name === 'include' && decl.params.includes('breakpoint')) { 36 | return false 37 | } 38 | 39 | if (decl.type !== 'decl' || !decl.prop.match(/^(min-width|width)/)) { 40 | return noop 41 | } 42 | 43 | const problems = [] 44 | 45 | walkGroups(valueParser(decl.value), node => { 46 | // Only check word types. https://github.com/TrySound/postcss-value-parser#word 47 | if (node.type !== 'word') { 48 | return 49 | } 50 | 51 | // Exact values to ignore. 52 | if (['*', '+', '-', '/', '0', 'auto', 'inherit', 'initial'].includes(node.value)) { 53 | return 54 | } 55 | 56 | const valueUnit = valueParser.unit(node.value) 57 | 58 | switch (valueUnit.unit) { 59 | case 'px': 60 | if (parseInt(valueUnit.number) > 320) { 61 | problems.push({ 62 | index: declarationValueIndex(decl) + node.sourceIndex, 63 | message: messages.rejected(node.value), 64 | }) 65 | } 66 | break 67 | case 'vw': 68 | if (parseInt(valueUnit.number) > 100) { 69 | problems.push({ 70 | index: declarationValueIndex(decl) + node.sourceIndex, 71 | message: messages.rejected(node.value), 72 | }) 73 | } 74 | break 75 | } 76 | }) 77 | 78 | if (problems.length) { 79 | for (const err of problems) { 80 | stylelint.utils.report({ 81 | index: err.index, 82 | message: err.message, 83 | node: decl, 84 | result, 85 | ruleName, 86 | }) 87 | } 88 | } 89 | }) 90 | } 91 | 92 | return lintResult 93 | }) 94 | 95 | function noop() {} 96 | -------------------------------------------------------------------------------- /plugins/spacing.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint' 2 | import {declarationValueIndex} from 'stylelint/lib/utils/nodeFieldIndices.cjs' 3 | import valueParser from 'postcss-value-parser' 4 | import {primitivesVariables, walkGroups} from './lib/utils.js' 5 | 6 | const { 7 | createPlugin, 8 | utils: {report, ruleMessages, validateOptions}, 9 | } = stylelint 10 | 11 | export const ruleName = 'primer/spacing' 12 | export const messages = ruleMessages(ruleName, { 13 | rejected: (value, replacement) => { 14 | if (!replacement) { 15 | return `Please use a primer size variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/size` 16 | } 17 | 18 | return `Please replace '${value}' with size variable '${replacement['name']}'. https://primer.style/foundations/primitives/size` 19 | }, 20 | }) 21 | 22 | // Props that we want to check 23 | const propList = ['padding', 'margin', 'top', 'right', 'bottom', 'left'] 24 | // Values that we want to ignore 25 | const valueList = ['${'] 26 | 27 | const sizes = primitivesVariables('spacing') 28 | 29 | // Add +-1px to each value 30 | for (const size of sizes) { 31 | const values = size['values'] 32 | const px = parseInt(values.find(value => value.includes('px'))) 33 | if (![2, 6].includes(px)) { 34 | values.push(`${px + 1}px`) 35 | values.push(`${px - 1}px`) 36 | } 37 | } 38 | 39 | /** @type {import('stylelint').Rule} */ 40 | const ruleFunction = primary => { 41 | return (root, result) => { 42 | const validOptions = validateOptions(result, ruleName, { 43 | actual: primary, 44 | possible: [true], 45 | }) 46 | 47 | if (!validOptions) return 48 | 49 | root.walkDecls(declNode => { 50 | const {prop, value} = declNode 51 | 52 | if (!propList.some(spacingProp => prop.startsWith(spacingProp))) return 53 | if (valueList.some(valueToIgnore => value.includes(valueToIgnore))) return 54 | 55 | const parsedValue = walkGroups(valueParser(value), node => { 56 | // Only check word types. https://github.com/TrySound/postcss-value-parser#word 57 | if (node.type !== 'word') { 58 | return 59 | } 60 | 61 | // Exact values to ignore. 62 | if (['*', '+', '-', '/', '0', 'auto', 'inherit', 'initial'].includes(node.value)) { 63 | return 64 | } 65 | 66 | const valueUnit = valueParser.unit(node.value) 67 | 68 | if (valueUnit && (valueUnit.unit === '' || !/^-?[0-9.]+$/.test(valueUnit.number))) { 69 | return 70 | } 71 | 72 | // Skip if the value unit isn't a supported unit. 73 | if (valueUnit && !['px', 'rem', 'em'].includes(valueUnit.unit)) { 74 | return 75 | } 76 | 77 | // If the variable is found in the value, skip it. 78 | if ( 79 | sizes.some(variable => 80 | new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(node.value), 81 | ) 82 | ) { 83 | return 84 | } 85 | 86 | const replacement = sizes.find(variable => variable.values.includes(node.value.replace('-', ''))) 87 | const fixable = replacement && valueUnit && !valueUnit.number.includes('-') 88 | let fix = undefined 89 | if (fixable) { 90 | fix = () => { 91 | node.value = node.value.replace(node.value, `var(${replacement['name']})`) 92 | } 93 | } 94 | report({ 95 | index: declarationValueIndex(declNode) + node.sourceIndex, 96 | endIndex: declarationValueIndex(declNode) + node.sourceIndex + node.value.length, 97 | message: messages.rejected(node.value, replacement), 98 | node: declNode, 99 | result, 100 | ruleName, 101 | fix, 102 | }) 103 | }) 104 | 105 | declNode.value = parsedValue.toString() 106 | }) 107 | } 108 | } 109 | 110 | ruleFunction.ruleName = ruleName 111 | ruleFunction.messages = messages 112 | ruleFunction.meta = { 113 | fixable: true, 114 | } 115 | 116 | export default createPlugin(ruleName, ruleFunction) 117 | -------------------------------------------------------------------------------- /plugins/typography.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint' 2 | import {declarationValueIndex} from 'stylelint/lib/utils/nodeFieldIndices.cjs' 3 | import {primitivesVariables} from './lib/utils.js' 4 | 5 | const { 6 | createPlugin, 7 | utils: {report, ruleMessages, validateOptions}, 8 | } = stylelint 9 | 10 | export const ruleName = 'primer/typography' 11 | export const messages = ruleMessages(ruleName, { 12 | rejected: (value, replacement) => { 13 | // no possible replacement 14 | if (!replacement) { 15 | return `Please use a Primer typography variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/typography` 16 | } 17 | 18 | // multiple possible replacements 19 | if (replacement.length) { 20 | return `Please use one of the following Primer typography variables instead of '${value}': ${replacement.map(replacementObj => `'${replacementObj.name}'`).join(', ')}. https://primer.style/foundations/primitives/typography` 21 | } 22 | 23 | // one possible replacement 24 | return `Please replace '${value}' with Primer typography variable '${replacement['name']}'. https://primer.style/foundations/primitives/typography` 25 | }, 26 | }) 27 | 28 | const fontWeightKeywordMap = { 29 | normal: 400, 30 | bold: 700, 31 | bolder: 600, 32 | lighter: 300, 33 | } 34 | const getClosestFontWeight = (goalWeightNumber, fontWeightsTokens) => { 35 | return fontWeightsTokens.reduce((prev, curr) => 36 | Math.abs(curr.values - goalWeightNumber) < Math.abs(prev.values - goalWeightNumber) ? curr : prev, 37 | ).values 38 | } 39 | 40 | const variables = primitivesVariables('typography') 41 | const fontSizes = [] 42 | const fontWeights = [] 43 | const lineHeights = [] 44 | const fontStacks = [] 45 | const fontShorthands = [] 46 | 47 | // Props that we want to check for typography variables 48 | const propList = ['font-size', 'font-weight', 'line-height', 'font-family', 'font'] 49 | 50 | for (const variable of variables) { 51 | const name = variable['name'] 52 | 53 | if (name.includes('size')) { 54 | fontSizes.push(variable) 55 | } 56 | 57 | if (name.includes('weight')) { 58 | fontWeights.push(variable) 59 | } 60 | 61 | if (name.includes('lineHeight')) { 62 | lineHeights.push(variable) 63 | } 64 | 65 | if (name.includes('fontStack')) { 66 | fontStacks.push(variable) 67 | } 68 | 69 | if (name.includes('shorthand')) { 70 | fontShorthands.push(variable) 71 | } 72 | } 73 | 74 | /** @type {import('stylelint').Rule} */ 75 | const ruleFunction = primary => { 76 | return (root, result) => { 77 | const validOptions = validateOptions(result, ruleName, { 78 | actual: primary, 79 | possible: [true], 80 | }) 81 | let validValues = [] 82 | 83 | if (!validOptions) return 84 | 85 | root.walkDecls(declNode => { 86 | const {prop, value} = declNode 87 | 88 | if (!propList.some(typographyProp => prop === typographyProp)) return 89 | 90 | const checkForVariable = (vars, nodeValue) => 91 | vars.some(variable => 92 | new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue), 93 | ) 94 | 95 | // Exact values to ignore. 96 | if (value === 'inherit') { 97 | return 98 | } 99 | 100 | switch (prop) { 101 | case 'font-size': 102 | validValues = fontSizes 103 | break 104 | case 'font-weight': 105 | validValues = fontWeights 106 | break 107 | case 'line-height': 108 | validValues = lineHeights 109 | break 110 | case 'font-family': 111 | validValues = fontStacks 112 | break 113 | case 'font': 114 | validValues = fontShorthands 115 | break 116 | default: 117 | validValues = [] 118 | } 119 | 120 | if (checkForVariable(validValues, value)) { 121 | return 122 | } 123 | 124 | const getReplacements = () => { 125 | const replacementTokens = validValues.filter(variable => { 126 | if (!(variable.values instanceof Array)) { 127 | let nodeValue = value 128 | 129 | if (prop === 'font-weight') { 130 | nodeValue = getClosestFontWeight(fontWeightKeywordMap[value] || value, fontWeights) 131 | } 132 | 133 | return variable.values.toString() === nodeValue.toString() 134 | } 135 | 136 | return variable.values.includes(value.replace('-', '')) 137 | }) 138 | 139 | if (!replacementTokens.length) { 140 | return 141 | } 142 | 143 | return replacementTokens[0] 144 | } 145 | const replacement = getReplacements() 146 | const fixable = replacement && !replacement.length 147 | let fix = undefined 148 | if (fixable) { 149 | fix = () => { 150 | declNode.value = value.replace(value, `var(${replacement['name']})`) 151 | } 152 | } 153 | 154 | report({ 155 | index: declarationValueIndex(declNode), 156 | endIndex: declarationValueIndex(declNode) + value.length, 157 | message: messages.rejected(value, replacement, prop), 158 | node: declNode, 159 | result, 160 | ruleName, 161 | fix, 162 | }) 163 | }) 164 | } 165 | } 166 | 167 | ruleFunction.ruleName = ruleName 168 | ruleFunction.messages = messages 169 | ruleFunction.meta = { 170 | fixable: true, 171 | } 172 | 173 | export default createPlugin(ruleName, ruleFunction) 174 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | import prettierConfig from '@github/prettier-config' 2 | 3 | export default prettierConfig 4 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import pluginJson from '@rollup/plugin-json' 4 | import packageJson from './package.json' with {type: 'json'} 5 | 6 | const external = ['dependencies', 'devDependencies', 'peerDependencies'].flatMap(type => { 7 | if (packageJson[type]) { 8 | return Object.keys(packageJson[type]).map(name => { 9 | return new RegExp(`^${name}(/.*)?`) 10 | }) 11 | } 12 | return [] 13 | }) 14 | 15 | const baseConfig = { 16 | input: 'index.js', 17 | external: [...external, new RegExp('^node:')], 18 | plugins: [nodeResolve(), pluginJson(), commonjs()], 19 | } 20 | 21 | export default [ 22 | { 23 | ...baseConfig, 24 | output: { 25 | format: 'esm', 26 | file: 'dist/index.mjs', 27 | importAttributesKey: 'with', 28 | }, 29 | }, 30 | { 31 | ...baseConfig, 32 | output: { 33 | format: 'cjs', 34 | file: 'dist/index.cjs', 35 | }, 36 | }, 37 | ] 38 | --------------------------------------------------------------------------------