From 60510ac92a556fa7915c34ff718a5eadb9bd4597 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 9 Jun 2026 12:53:13 -0500 Subject: [PATCH 1/7] feat: add support for CSS Layers to Avatar Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/avatar-css-layer.md | 5 ++ package-lock.json | 9 +++ packages/react/package.json | 2 + packages/react/src/Avatar/Avatar.module.css | 56 ++++++++++--------- .../react/src/__tests__/css-layers.test.ts | 37 ++++++++++++ packages/react/vitest.config.browser.mts | 1 + packages/react/vitest.config.mts | 2 +- 7 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 .changeset/avatar-css-layer.md create mode 100644 packages/react/src/__tests__/css-layers.test.ts diff --git a/.changeset/avatar-css-layer.md b/.changeset/avatar-css-layer.md new file mode 100644 index 00000000000..a07678fd303 --- /dev/null +++ b/.changeset/avatar-css-layer.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Avatar: Improve custom class override behavior for component styles diff --git a/package-lock.json b/package-lock.json index fcb2f82a26f..d6539391f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10110,6 +10110,13 @@ "@types/node": "*" } }, + "node_modules/@types/css-tree": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.11.tgz", + "integrity": "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "dev": true, @@ -28628,6 +28635,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", + "@types/css-tree": "^2.3.11", "@types/lodash.groupby": "4.6.9", "@types/lodash.isempty": "4.4.9", "@types/lodash.isobject": "3.0.9", @@ -28653,6 +28661,7 @@ "concurrently": "9.1.2", "copyfiles": "2.4.1", "cross-env": "7.0.3", + "css-tree": "^2.3.1", "fast-glob": "3.3.2", "filesize": "10.1.6", "front-matter": "4.0.2", diff --git a/packages/react/package.json b/packages/react/package.json index 78df4dae326..7c00e1021b8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -125,6 +125,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", + "@types/css-tree": "^2.3.11", "@types/lodash.groupby": "4.6.9", "@types/lodash.isempty": "4.4.9", "@types/lodash.isobject": "3.0.9", @@ -150,6 +151,7 @@ "concurrently": "9.1.2", "copyfiles": "2.4.1", "cross-env": "7.0.3", + "css-tree": "^2.3.1", "fast-glob": "3.3.2", "filesize": "10.1.6", "front-matter": "4.0.2", diff --git a/packages/react/src/Avatar/Avatar.module.css b/packages/react/src/Avatar/Avatar.module.css index fc67531b9a7..da221791acd 100644 --- a/packages/react/src/Avatar/Avatar.module.css +++ b/packages/react/src/Avatar/Avatar.module.css @@ -1,34 +1,36 @@ -:where(.Avatar) { - display: inline-block; - width: var(--avatarSize-regular); - height: var(--avatarSize-regular); - overflow: hidden; /* Ensure page layout in Firefox should images fail to load */ - /* stylelint-disable-next-line primer/typography */ - line-height: 1; - vertical-align: middle; - border-radius: 50%; - /* stylelint-disable-next-line primer/box-shadow */ - box-shadow: 0 0 0 1px var(--avatar-borderColor); +@layer primer.components.Avatar { + :where(.Avatar) { + display: inline-block; + width: var(--avatarSize-regular); + height: var(--avatarSize-regular); + overflow: hidden; /* Ensure page layout in Firefox should images fail to load */ + /* stylelint-disable-next-line primer/typography */ + line-height: 1; + vertical-align: middle; + border-radius: 50%; + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: 0 0 0 1px var(--avatar-borderColor); - &:where([data-square]) { - /* stylelint-disable-next-line primer/borders */ - border-radius: clamp(4px, calc(var(--avatarSize-regular) - 24px), var(--borderRadius-medium)); - } - - &:where([data-responsive]) { - @media screen and (--viewportRange-narrow) { - width: var(--avatarSize-narrow); - height: var(--avatarSize-narrow); + &:where([data-square]) { + /* stylelint-disable-next-line primer/borders */ + border-radius: clamp(4px, calc(var(--avatarSize-regular) - 24px), var(--borderRadius-medium)); } - @media screen and (--viewportRange-regular) { - width: var(--avatarSize-regular); - height: var(--avatarSize-regular); - } + &:where([data-responsive]) { + @media screen and (--viewportRange-narrow) { + width: var(--avatarSize-narrow); + height: var(--avatarSize-narrow); + } + + @media screen and (--viewportRange-regular) { + width: var(--avatarSize-regular); + height: var(--avatarSize-regular); + } - @media screen and (--viewportRange-wide) { - width: var(--avatarSize-wide); - height: var(--avatarSize-wide); + @media screen and (--viewportRange-wide) { + width: var(--avatarSize-wide); + height: var(--avatarSize-wide); + } } } } diff --git a/packages/react/src/__tests__/css-layers.test.ts b/packages/react/src/__tests__/css-layers.test.ts new file mode 100644 index 00000000000..e3dbdd128d8 --- /dev/null +++ b/packages/react/src/__tests__/css-layers.test.ts @@ -0,0 +1,37 @@ +import fs from 'node:fs' +import path from 'node:path' +import {parse} from 'css-tree' +import type {Atrule, Raw, StyleSheet} from 'css-tree' +import {describe, expect, test} from 'vitest' + +const allowlist = new Set([path.resolve(import.meta.dirname, '../Avatar/Avatar.module.css')]) +const files = Array.from(allowlist).map(file => { + return [path.basename(file), file] +}) + +const CSS_LAYER_REGEX = /^primer\.components\.[A-Z][A-Za-z0-9]+$/ + +describe('CSS Layers', () => { + describe.each(files)('%s', (_name, filename) => { + const contents = fs.readFileSync(filename, 'utf8') + const ast = parse(contents, { + filename, + }) as StyleSheet + + test('uses CSS Layer', () => { + expect(ast.children.first?.type).toBe('Atrule') + const node = ast.children.first as Atrule + + expect(node.name).toBe('layer') + }) + + test('CSS Layer matches naming conventions', () => { + const node = ast.children.first as Atrule + + expect(node.prelude?.type).toBe('Raw') + const prelude = node.prelude as Raw + + expect(prelude.value).toMatch(CSS_LAYER_REGEX) + }) + }) +}) diff --git a/packages/react/vitest.config.browser.mts b/packages/react/vitest.config.browser.mts index 21893362d8e..03bce19b8ae 100644 --- a/packages/react/vitest.config.browser.mts +++ b/packages/react/vitest.config.browser.mts @@ -43,6 +43,7 @@ export default defineConfig({ '**/*.types.test.tsx', 'src/__tests__/exports.test.ts', 'src/__tests__/storybook.test.tsx', + 'src/__tests__/css-layers.test.ts', ], include: ['src/**/*.test.?(c|m)[jt]s?(x)'], setupFiles: ['config/vitest/browser/setup.ts'], diff --git a/packages/react/vitest.config.mts b/packages/react/vitest.config.mts index a9b57f621f0..e7f5b50cf2b 100644 --- a/packages/react/vitest.config.mts +++ b/packages/react/vitest.config.mts @@ -23,7 +23,7 @@ export default defineConfig({ }, test: { name: '@primer/react (node)', - include: ['src/__tests__/exports.test.ts', 'src/__tests__/storybook.test.tsx'], + include: ['src/__tests__/exports.test.ts', 'src/__tests__/storybook.test.tsx', 'src/__tests__/css-layers.test.ts'], environment: 'node', detectAsyncLeaks: true, }, From 50e618c6ef5f003d8c8ef88c88edc0b309bc8e98 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 9 Jun 2026 15:12:20 -0500 Subject: [PATCH 2/7] chore: standardize CSS layer changeset wording Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/avatar-css-layer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/avatar-css-layer.md b/.changeset/avatar-css-layer.md index a07678fd303..b484de59933 100644 --- a/.changeset/avatar-css-layer.md +++ b/.changeset/avatar-css-layer.md @@ -2,4 +2,4 @@ '@primer/react': patch --- -Avatar: Improve custom class override behavior for component styles +Avatar: Add CSS layer support for component styles From b6648825e3d21c57894248d91230299abc280f0d Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 9 Jun 2026 15:23:43 -0500 Subject: [PATCH 3/7] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../react/src/__tests__/css-layers.test.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/react/src/__tests__/css-layers.test.ts b/packages/react/src/__tests__/css-layers.test.ts index e3dbdd128d8..1b21cd41368 100644 --- a/packages/react/src/__tests__/css-layers.test.ts +++ b/packages/react/src/__tests__/css-layers.test.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import path from 'node:path' -import {parse} from 'css-tree' -import type {Atrule, Raw, StyleSheet} from 'css-tree' +import {generate, parse} from 'css-tree' +import type {Atrule, StyleSheet} from 'css-tree' import {describe, expect, test} from 'vitest' const allowlist = new Set([path.resolve(import.meta.dirname, '../Avatar/Avatar.module.css')]) @@ -19,19 +19,22 @@ describe('CSS Layers', () => { }) as StyleSheet test('uses CSS Layer', () => { - expect(ast.children.first?.type).toBe('Atrule') - const node = ast.children.first as Atrule + const first = ast.children.first - expect(node.name).toBe('layer') + expect(first?.type).toBe('Atrule') + if (!first || first.type !== 'Atrule') throw new Error('Expected stylesheet to start with an @layer at-rule') + + expect(first.name).toBe('layer') }) test('CSS Layer matches naming conventions', () => { - const node = ast.children.first as Atrule + const first = ast.children.first - expect(node.prelude?.type).toBe('Raw') - const prelude = node.prelude as Raw + expect(first?.type).toBe('Atrule') + if (!first || first.type !== 'Atrule') throw new Error('Expected stylesheet to start with an @layer at-rule') - expect(prelude.value).toMatch(CSS_LAYER_REGEX) + const layerName = first.prelude ? generate(first.prelude).trim() : '' + expect(layerName).toMatch(CSS_LAYER_REGEX) }) }) }) From e9de7721f3cbedd17c56a70c5a6cca32e7ca2682 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 9 Jun 2026 15:28:37 -0500 Subject: [PATCH 4/7] chore: fix lint warning --- packages/react/src/__tests__/css-layers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/__tests__/css-layers.test.ts b/packages/react/src/__tests__/css-layers.test.ts index 1b21cd41368..b9baf8d2fe4 100644 --- a/packages/react/src/__tests__/css-layers.test.ts +++ b/packages/react/src/__tests__/css-layers.test.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import path from 'node:path' import {generate, parse} from 'css-tree' -import type {Atrule, StyleSheet} from 'css-tree' +import type {StyleSheet} from 'css-tree' import {describe, expect, test} from 'vitest' const allowlist = new Set([path.resolve(import.meta.dirname, '../Avatar/Avatar.module.css')]) From bc82a0665cbdbe7cb0df36d6580bbcbb5de25837 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 9 Jun 2026 13:28:17 -0500 Subject: [PATCH 5/7] feat: add CSS Layers to 01-foundations-a Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/css-layers-foundations-a.md | 5 + .../src/AvatarStack/AvatarStack.module.css | 458 +++++++++--------- packages/react/src/BaseStyles.module.css | 124 ++--- .../src/BranchName/BranchName.module.css | 24 +- .../src/ButtonGroup/ButtonGroup.module.css | 124 ++--- packages/react/src/Card/Card.module.css | 184 +++---- .../react/src/Checkbox/Checkbox.module.css | 154 +++--- packages/react/src/Checkbox/shared.module.css | 38 +- .../src/CircleBadge/CircleBadge.module.css | 30 +- .../react/src/__tests__/css-layers.test.ts | 16 +- .../FilteredSearch/FilteredSearch.module.css | 32 +- .../UnderlineNav/UnderlineNav.module.css | 136 +++--- 12 files changed, 683 insertions(+), 642 deletions(-) create mode 100644 .changeset/css-layers-foundations-a.md diff --git a/.changeset/css-layers-foundations-a.md b/.changeset/css-layers-foundations-a.md new file mode 100644 index 00000000000..06240557617 --- /dev/null +++ b/.changeset/css-layers-foundations-a.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +AvatarStack, BaseStyles, BranchName, ButtonGroup, Card, Checkbox, CircleBadge, FilteredSearch, UnderlineNav (deprecated): Improve custom class override behavior for component styles diff --git a/packages/react/src/AvatarStack/AvatarStack.module.css b/packages/react/src/AvatarStack/AvatarStack.module.css index 3eedffd65da..4caf1ae70b5 100644 --- a/packages/react/src/AvatarStack/AvatarStack.module.css +++ b/packages/react/src/AvatarStack/AvatarStack.module.css @@ -1,273 +1,279 @@ -/* stylelint-disable selector-max-specificity */ -.AvatarStack { - --avatar-border-width: 1px; - --mask-size: calc(100% + (var(--avatar-border-width) * 2)); - --mask-start: -1; - --opacity-step: 15%; - - position: relative; - display: flex; - min-width: var(--avatar-stack-size); - height: var(--avatar-stack-size); - isolation: isolate; - - &:where([data-variant='cascade']) { - --overlap-size: calc(var(--avatar-stack-size) * 0.55); - --overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.85); - } +@layer primer.components.AvatarStack { + /* stylelint-disable selector-max-specificity */ + .AvatarStack { + --avatar-border-width: 1px; + --mask-size: calc(100% + (var(--avatar-border-width) * 2)); + --mask-start: -1; + --opacity-step: 15%; - &:where([data-variant='stack']) { - --overlap-size: calc(var(--avatar-stack-size) * 0.55); - --overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.55); - } + position: relative; + display: flex; + min-width: var(--avatar-stack-size); + height: var(--avatar-stack-size); + isolation: isolate; + + &:where([data-variant='cascade']) { + --overlap-size: calc(var(--avatar-stack-size) * 0.55); + --overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.85); + } - &:where([data-responsive]) { - @media screen and (--viewportRange-narrow) { - --avatar-stack-size: var(--stackSize-narrow); + &:where([data-variant='stack']) { + --overlap-size: calc(var(--avatar-stack-size) * 0.55); + --overlap-size-avatar-three-plus: calc(var(--avatar-stack-size) * 0.55); } - @media screen and (--viewportRange-regular) { - --avatar-stack-size: var(--stackSize-regular); + &:where([data-responsive]) { + @media screen and (--viewportRange-narrow) { + --avatar-stack-size: var(--stackSize-narrow); + } + + @media screen and (--viewportRange-regular) { + --avatar-stack-size: var(--stackSize-regular); + } + + @media screen and (--viewportRange-wide) { + --avatar-stack-size: var(--stackSize-wide); + } } - @media screen and (--viewportRange-wide) { - --avatar-stack-size: var(--stackSize-wide); + &:where([data-avatar-count='1'][data-shape='circle']) { + .AvatarItem:where([data-component='Avatar']) { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: 0 0 0 var(--avatar-border-width) var(--avatar-borderColor); + } } - } - &:where([data-avatar-count='1'][data-shape='circle']) { - .AvatarItem:where([data-component='Avatar']) { + &:where([data-avatar-count='1'][data-shape='square']) .AvatarItem { /* stylelint-disable-next-line primer/box-shadow */ - box-shadow: 0 0 0 var(--avatar-border-width) var(--avatar-borderColor); + box-shadow: 1px 0 rgb(0, 0, 0, 1); } - } - &:where([data-avatar-count='1'][data-shape='square']) .AvatarItem { - /* stylelint-disable-next-line primer/box-shadow */ - box-shadow: 1px 0 rgb(0, 0, 0, 1); - } + &:where([data-avatar-count='1'][data-shape='square'][data-align-right]) .AvatarItem { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: -1px 0 rgb(0, 0, 0, 1); + } - &:where([data-avatar-count='1'][data-shape='square'][data-align-right]) .AvatarItem { - /* stylelint-disable-next-line primer/box-shadow */ - box-shadow: -1px 0 rgb(0, 0, 0, 1); - } + &:where([data-avatar-count='2']) { + /* + MIN-WIDTH CALC FORMULA EXPLAINED: + avatar size ➡️ var(--avatar-stack-size) + plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size) + */ + min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size))); + } - &:where([data-avatar-count='2']) { - /* - MIN-WIDTH CALC FORMULA EXPLAINED: - avatar size ➡️ var(--avatar-stack-size) - plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size) - */ - min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size))); - } + &:where([data-avatar-count='3'][data-variant='cascade']) { + /* + MIN-WIDTH CALC FORMULA EXPLAINED: + avatar size ➡️ var(--avatar-stack-size) + plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size) + plus the visible part of the 3rd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) + */ + min-width: calc( + var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size)) + + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) + ); + } - &:where([data-avatar-count='3'][data-variant='cascade']) { - /* - MIN-WIDTH CALC FORMULA EXPLAINED: - avatar size ➡️ var(--avatar-stack-size) - plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size) - plus the visible part of the 3rd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) - */ - min-width: calc( - var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size)) + - (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) - ); - } + &:where([data-avatar-count='3'][data-variant='stack']) { + /* + MIN-WIDTH CALC FORMULA EXPLAINED: + avatar size ➡️ var(--avatar-stack-size) + plus the visible part of the 2nd avatar & 3rd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) * 2 + */ + min-width: calc( + var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2 + ); + } - &:where([data-avatar-count='3'][data-variant='stack']) { - /* - MIN-WIDTH CALC FORMULA EXPLAINED: - avatar size ➡️ var(--avatar-stack-size) - plus the visible part of the 2nd avatar & 3rd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) * 2 - */ - min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2); - } + &:where([data-avatar-count='3+'][data-variant='cascade']) { + /* + MIN-WIDTH CALC FORMULA EXPLAINED: + avatar size ➡️ var(--avatar-stack-size) + plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size) + plus the visible part of the 3rd AND 4th avatar ➡️ (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2 + */ + min-width: calc( + var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size)) + + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2 + ); + } - &:where([data-avatar-count='3+'][data-variant='cascade']) { - /* - MIN-WIDTH CALC FORMULA EXPLAINED: - avatar size ➡️ var(--avatar-stack-size) - plus the visible part of the 2nd avatar ➡️ var(--avatar-stack-size) - var(--overlap-size) - plus the visible part of the 3rd AND 4th avatar ➡️ (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2 - */ - min-width: calc( - var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size)) + - (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 2 - ); - } + &:where([data-avatar-count='3+'][data-variant='stack']) { + /* + MIN-WIDTH CALC FORMULA EXPLAINED: + avatar size ➡️ var(--avatar-stack-size) + plus the visible part of the 2nd to 4th avatars ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) * 3 + */ + min-width: calc( + var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 3 + ); + + --overlap-size: var(--overlap-size-avatar-three-plus); + } - &:where([data-avatar-count='3+'][data-variant='stack']) { - /* - MIN-WIDTH CALC FORMULA EXPLAINED: - avatar size ➡️ var(--avatar-stack-size) - plus the visible part of the 2nd to 4th avatars ➡️ var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus) * 3 - */ - min-width: calc(var(--avatar-stack-size) + (var(--avatar-stack-size) - var(--overlap-size-avatar-three-plus)) * 3); + &:where([data-align-right]) { + --mask-start: 1; - --overlap-size: var(--overlap-size-avatar-three-plus); + direction: rtl; + } } - &:where([data-align-right]) { - --mask-start: 1; + .AvatarStackBody { + position: absolute; + display: flex; - direction: rtl; + &:where([data-disable-expand]) { + position: relative; + } } -} -.AvatarStackBody { - position: absolute; - display: flex; + .AvatarItem { + --avatarSize-regular: var(--avatar-stack-size); - &:where([data-disable-expand]) { position: relative; - } -} - -.AvatarItem { - --avatarSize-regular: var(--avatar-stack-size); - - position: relative; - display: flex; - width: var(--avatar-stack-size); - height: var(--avatar-stack-size); - overflow: hidden; - flex-shrink: 0; - transition: - margin 0.2s ease-in-out, - opacity 0.2s ease-in-out, - mask-position 0.2s ease-in-out, - mask-size 0.2s ease-in-out; - - /* stylelint-disable-next-line selector-no-qualifying-type */ - .AvatarStack:where([data-shape='circle']) &:is(img) { - /* stylelint-disable-next-line primer/box-shadow */ - box-shadow: 0 0 0 var(--avatar-border-width) transparent; - } + display: flex; + width: var(--avatar-stack-size); + height: var(--avatar-stack-size); + overflow: hidden; + flex-shrink: 0; + transition: + margin 0.2s ease-in-out, + opacity 0.2s ease-in-out, + mask-position 0.2s ease-in-out, + mask-size 0.2s ease-in-out; + + /* stylelint-disable-next-line selector-no-qualifying-type */ + .AvatarStack:where([data-shape='circle']) &:is(img) { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: 0 0 0 var(--avatar-border-width) transparent; + } - /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ - &:not([data-component='Avatar']):not(:has([data-square])) { - border-radius: 50%; - } + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ + &:not([data-component='Avatar']):not(:has([data-square])) { + border-radius: 50%; + } - /* stylelint-disable-next-line selector-no-qualifying-type */ - .AvatarStack:where([data-shape='square']) &:is(img) { - /* stylelint-disable-next-line primer/box-shadow */ - box-shadow: 1px 0 rgb(255, 255, 255, 1); - } + /* stylelint-disable-next-line selector-no-qualifying-type */ + .AvatarStack:where([data-shape='square']) &:is(img) { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: 1px 0 rgb(255, 255, 255, 1); + } - /* stylelint-disable-next-line selector-no-qualifying-type */ - .AvatarStack:where([data-shape='square'][data-align-right]) &:is(img) { - /* stylelint-disable-next-line primer/box-shadow */ - box-shadow: -1px 0 rgb(255, 255, 255, 1); - } + /* stylelint-disable-next-line selector-no-qualifying-type */ + .AvatarStack:where([data-shape='square'][data-align-right]) &:is(img) { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: -1px 0 rgb(255, 255, 255, 1); + } - &:first-child { - margin-inline-start: 0; - } + &:first-child { + margin-inline-start: 0; + } - &:nth-child(n + 2) { - /* stylelint-disable-next-line primer/spacing */ - margin-inline-start: calc(var(--overlap-size) * -1); - mask-repeat: no-repeat, no-repeat; - mask-size: - var(--mask-size) var(--mask-size), - auto; - mask-composite: exclude; - - /* - HORIZONTAL POSITION CALC FORMULA EXPLAINED: - width of the visible part of the avatar ➡️ var(--avatar-stack-size) - var(--overlap-size) - multiply by -1 for left-aligned, 1 for right-aligned ➡️ var(--mask-start) - subtract the avatar border width ➡️ var(--avatar-border-width) - */ - mask-position: - calc((var(--avatar-stack-size) - var(--overlap-size)) * var(--mask-start) - var(--avatar-border-width)) center, - 0 0; - - /* HACK: This padding fixes a weird rendering bug where a tiiiiny outline is visible at the edges of the element */ - /* stylelint-disable-next-line primer/spacing */ - padding: 0.1px; - } + &:nth-child(n + 2) { + /* stylelint-disable-next-line primer/spacing */ + margin-inline-start: calc(var(--overlap-size) * -1); + mask-repeat: no-repeat, no-repeat; + mask-size: + var(--mask-size) var(--mask-size), + auto; + mask-composite: exclude; + + /* + HORIZONTAL POSITION CALC FORMULA EXPLAINED: + width of the visible part of the avatar ➡️ var(--avatar-stack-size) - var(--overlap-size) + multiply by -1 for left-aligned, 1 for right-aligned ➡️ var(--mask-start) + subtract the avatar border width ➡️ var(--avatar-border-width) + */ + mask-position: + calc((var(--avatar-stack-size) - var(--overlap-size)) * var(--mask-start) - var(--avatar-border-width)) center, + 0 0; + + /* HACK: This padding fixes a weird rendering bug where a tiiiiny outline is visible at the edges of the element */ + /* stylelint-disable-next-line primer/spacing */ + padding: 0.1px; + } - /* Circular mask */ - .AvatarStack:where([data-shape='circle']) &:nth-child(n + 2) { - mask-image: radial-gradient(at 50% 50%, rgb(0, 0, 0) 70%, rgb(0, 0, 0, 0) 71%), linear-gradient(rgb(0, 0, 0) 0 0); - } + /* Circular mask */ + .AvatarStack:where([data-shape='circle']) &:nth-child(n + 2) { + mask-image: radial-gradient(at 50% 50%, rgb(0, 0, 0) 70%, rgb(0, 0, 0, 0) 71%), linear-gradient(rgb(0, 0, 0) 0 0); + } - /* Square mask */ - .AvatarStack:where([data-shape='square']) &:nth-child(n + 2) { - /* stylelint-disable-next-line declaration-property-value-no-unknown */ - mask-image: linear-gradient(at 50% 50%, rgb(0, 0, 0) 70%, rgb(0, 0, 0, 0) 71%), linear-gradient(rgb(0, 0, 0) 0 0); - } + /* Square mask */ + .AvatarStack:where([data-shape='square']) &:nth-child(n + 2) { + /* stylelint-disable-next-line declaration-property-value-no-unknown */ + mask-image: linear-gradient(at 50% 50%, rgb(0, 0, 0) 70%, rgb(0, 0, 0, 0) 71%), linear-gradient(rgb(0, 0, 0) 0 0); + } - /* Cascade variant override for nth-child(n + 3) */ - .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 3) { - --overlap-size: var(--overlap-size-avatar-three-plus); + /* Cascade variant override for nth-child(n + 3) */ + .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 3) { + --overlap-size: var(--overlap-size-avatar-three-plus); - /* stylelint-disable-next-line alpha-value-notation */ - opacity: calc(100% - 2 * var(--opacity-step)); - } + /* stylelint-disable-next-line alpha-value-notation */ + opacity: calc(100% - 2 * var(--opacity-step)); + } - /* Cascade variant override for nth-child(n + 4) */ - .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 4) { - /* stylelint-disable-next-line alpha-value-notation */ - opacity: calc(100% - 3 * var(--opacity-step)); - } + /* Cascade variant override for nth-child(n + 4) */ + .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 4) { + /* stylelint-disable-next-line alpha-value-notation */ + opacity: calc(100% - 3 * var(--opacity-step)); + } - /* Cascade variant override for nth-child(n + 5) */ - .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 5) { - /* stylelint-disable-next-line alpha-value-notation */ - opacity: calc(100% - 4 * var(--opacity-step)); - } + /* Cascade variant override for nth-child(n + 5) */ + .AvatarStack:where([data-variant='cascade']) &:nth-child(n + 5) { + /* stylelint-disable-next-line alpha-value-notation */ + opacity: calc(100% - 4 * var(--opacity-step)); + } - .AvatarStack:where([data-shape='square']) &:nth-child(1) { - z-index: 5; - } + .AvatarStack:where([data-shape='square']) &:nth-child(1) { + z-index: 5; + } - .AvatarStack:where([data-shape='square']) &:nth-child(2) { - z-index: 4; - } + .AvatarStack:where([data-shape='square']) &:nth-child(2) { + z-index: 4; + } - .AvatarStack:where([data-shape='square']) &:nth-child(3) { - z-index: 3; - } + .AvatarStack:where([data-shape='square']) &:nth-child(3) { + z-index: 3; + } - .AvatarStack:where([data-shape='square']) &:nth-child(4) { - z-index: 2; - } + .AvatarStack:where([data-shape='square']) &:nth-child(4) { + z-index: 2; + } - .AvatarStack:where([data-shape='square']) &:nth-child(5) { - z-index: 1; - } + .AvatarStack:where([data-shape='square']) &:nth-child(5) { + z-index: 1; + } - &:nth-child(n + 6) { - visibility: hidden; - opacity: 0; + &:nth-child(n + 6) { + visibility: hidden; + opacity: 0; + } } -} - -.AvatarStackBody:not([data-disable-expand]):hover, -.AvatarStackBody:not([data-disable-expand]):focus-within { - width: auto; - - .AvatarItem { - --mask-size: 100%; /* reset size of the mask to prevent unintentially clipping due to the additional size created by the border width */ - margin-inline-start: var(--base-size-4); - visibility: visible; - opacity: 1; - - /* - HORIZONTAL POSITION CALC FORMULA EXPLAINED: - width of the full avatar ➡️ var(--avatar-stack-size) - multiply by -1 for left-aligned, 1 for right-aligned ➡️ var(--mask-start) - */ - mask-position: - calc(var(--avatar-stack-size) * var(--mask-start)) center, - 0 0; - - &:first-child { - margin-inline-start: 0; + .AvatarStackBody:not([data-disable-expand]):hover, + .AvatarStackBody:not([data-disable-expand]):focus-within { + width: auto; + + .AvatarItem { + --mask-size: 100%; /* reset size of the mask to prevent unintentially clipping due to the additional size created by the border width */ + + margin-inline-start: var(--base-size-4); + visibility: visible; + opacity: 1; + + /* + HORIZONTAL POSITION CALC FORMULA EXPLAINED: + width of the full avatar ➡️ var(--avatar-stack-size) + multiply by -1 for left-aligned, 1 for right-aligned ➡️ var(--mask-start) + */ + mask-position: + calc(var(--avatar-stack-size) * var(--mask-start)) center, + 0 0; + + &:first-child { + margin-inline-start: 0; + } } } } diff --git a/packages/react/src/BaseStyles.module.css b/packages/react/src/BaseStyles.module.css index b859961869b..c338f607940 100644 --- a/packages/react/src/BaseStyles.module.css +++ b/packages/react/src/BaseStyles.module.css @@ -1,80 +1,82 @@ -/* stylelint-disable selector-max-specificity */ -/* stylelint-disable selector-type-no-unknown */ +@layer primer.components.BaseStyles { + /* stylelint-disable selector-max-specificity */ + /* stylelint-disable selector-type-no-unknown */ -/* -------------------------------- - * Global Styles - *--------------------------------- */ -* { - box-sizing: border-box; -} - -body { - margin: 0; -} - -table { - /* stylelint-disable-next-line primer/borders */ - border-collapse: collapse; -} + /* -------------------------------- + * Global Styles + *--------------------------------- */ + * { + box-sizing: border-box; + } -[data-color-mode='light'] input { - color-scheme: light; -} + body { + margin: 0; + } -[data-color-mode='dark'] input { - color-scheme: dark; -} + table { + /* stylelint-disable-next-line primer/borders */ + border-collapse: collapse; + } -@media (prefers-color-scheme: light) { - [data-color-mode='auto'][data-light-theme*='light'] { + [data-color-mode='light'] input { color-scheme: light; } -} -@media (prefers-color-scheme: dark) { - [data-color-mode='auto'][data-dark-theme*='dark'] { + [data-color-mode='dark'] input { color-scheme: dark; } -} -[role='button']:focus:not(:focus-visible):not(:global(.focus-visible)), -[role='tabpanel'][tabindex='0']:focus:not(:focus-visible):not(:global(.focus-visible)), -button:focus:not(:focus-visible):not(:global(.focus-visible)), -summary:focus:not(:focus-visible):not(:global(.focus-visible)), -a:focus:not(:focus-visible):not(:global(.focus-visible)) { - outline: none; - box-shadow: none; -} + @media (prefers-color-scheme: light) { + [data-color-mode='auto'][data-light-theme*='light'] { + color-scheme: light; + } + } -[tabindex='0']:focus:not(:focus-visible):not(:global(.focus-visible)), -details-dialog:focus:not(:focus-visible):not(:global(.focus-visible)) { - outline: none; -} + @media (prefers-color-scheme: dark) { + [data-color-mode='auto'][data-dark-theme*='dark'] { + color-scheme: dark; + } + } + + [role='button']:focus:not(:focus-visible):not(:global(.focus-visible)), + [role='tabpanel'][tabindex='0']:focus:not(:focus-visible):not(:global(.focus-visible)), + button:focus:not(:focus-visible):not(:global(.focus-visible)), + summary:focus:not(:focus-visible):not(:global(.focus-visible)), + a:focus:not(:focus-visible):not(:global(.focus-visible)) { + outline: none; + box-shadow: none; + } + + [tabindex='0']:focus:not(:focus-visible):not(:global(.focus-visible)), + details-dialog:focus:not(:focus-visible):not(:global(.focus-visible)) { + outline: none; + } -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ -.BaseStyles { - font-family: var(--BaseStyles-fontFamily, var(--fontStack-system)); - /* stylelint-disable-next-line primer/typography */ - line-height: var(--BaseStyles-lineHeight, 1.5); - /* stylelint-disable-next-line primer/colors */ - color: var(--BaseStyles-fgColor, var(--fgColor-default)); + .BaseStyles { + font-family: var(--BaseStyles-fontFamily, var(--fontStack-system)); + /* stylelint-disable-next-line primer/typography */ + line-height: var(--BaseStyles-lineHeight, 1.5); + /* stylelint-disable-next-line primer/colors */ + color: var(--BaseStyles-fgColor, var(--fgColor-default)); - /* - * PERFORMANCE: Removed :has([data-color-mode]) selectors that scanned entire DOM. - * Input color-scheme is already handled by global selectors above: - * [data-color-mode='light'] input { color-scheme: light; } - * [data-color-mode='dark'] input { color-scheme: dark; } - */ + /* + * PERFORMANCE: Removed :has([data-color-mode]) selectors that scanned entire DOM. + * Input color-scheme is already handled by global selectors above: + * [data-color-mode='light'] input { color-scheme: light; } + * [data-color-mode='dark'] input { color-scheme: dark; } + */ - /* Low-specificity default link styling */ - /* stylelint-disable-next-line selector-no-qualifying-type */ - :where(a:not([class*='prc-']):not([class*='PRC-']):not([class*='Primer_Brand__'])) { - color: var(--fgColor-accent, var(--color-accent-fg)); - text-decoration: none; + /* Low-specificity default link styling */ + /* stylelint-disable-next-line selector-no-qualifying-type */ + :where(a:not([class*='prc-']):not([class*='PRC-']):not([class*='Primer_Brand__'])) { + color: var(--fgColor-accent, var(--color-accent-fg)); + text-decoration: none; - &:hover { - text-decoration: underline; + &:hover { + text-decoration: underline; + } } } } diff --git a/packages/react/src/BranchName/BranchName.module.css b/packages/react/src/BranchName/BranchName.module.css index 66e9abc2d50..7dafb487321 100644 --- a/packages/react/src/BranchName/BranchName.module.css +++ b/packages/react/src/BranchName/BranchName.module.css @@ -1,14 +1,16 @@ -.BranchName { - display: inline-block; - padding: var(--base-size-2) var(--base-size-6); - font-family: var(--fontStack-monospace); - font-size: var(--text-body-size-small); - color: var(--fgColor-link); - text-decoration: none; - background-color: var(--bgColor-accent-muted); - border-radius: var(--borderRadius-medium); +@layer primer.components.BranchName { + .BranchName { + display: inline-block; + padding: var(--base-size-2) var(--base-size-6); + font-family: var(--fontStack-monospace); + font-size: var(--text-body-size-small); + color: var(--fgColor-link); + text-decoration: none; + background-color: var(--bgColor-accent-muted); + border-radius: var(--borderRadius-medium); - &:is(:not(a)) { - color: var(--fgColor-muted); + &:is(:not(a)) { + color: var(--fgColor-muted); + } } } diff --git a/packages/react/src/ButtonGroup/ButtonGroup.module.css b/packages/react/src/ButtonGroup/ButtonGroup.module.css index cb942aef85c..a47d701915f 100644 --- a/packages/react/src/ButtonGroup/ButtonGroup.module.css +++ b/packages/react/src/ButtonGroup/ButtonGroup.module.css @@ -1,82 +1,84 @@ -.ButtonGroup { - display: inline-flex; - vertical-align: middle; - isolation: isolate; +@layer primer.components.ButtonGroup { + .ButtonGroup { + display: inline-flex; + vertical-align: middle; + isolation: isolate; - & > *:not([data-loading-wrapper]) { - /* stylelint-disable-next-line primer/spacing */ - margin-inline-end: -1px; - position: relative; + & > *:not([data-loading-wrapper]) { + /* stylelint-disable-next-line primer/spacing */ + margin-inline-end: -1px; + position: relative; - /* reset border-radius */ - button, - a { - border-radius: 0; - } - - &:first-child { + /* reset border-radius */ button, a { - border-top-left-radius: var(--borderRadius-medium); - border-bottom-left-radius: var(--borderRadius-medium); + border-radius: 0; } - } - &:last-child { - button, - a { - border-top-right-radius: var(--borderRadius-medium); - border-bottom-right-radius: var(--borderRadius-medium); + &:first-child { + button, + a { + border-top-left-radius: var(--borderRadius-medium); + border-bottom-left-radius: var(--borderRadius-medium); + } } - } - - &:focus, - &:active, - &:hover { - z-index: 1; - } - } - - /* this is a workaround until portal based tooltips are fully removed from dotcom */ - /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ - &:has(div:last-child:empty) { - button, - a { - border-radius: var(--borderRadius-medium); - } - } - /* if child is loading button */ - & > *[data-loading-wrapper] { - /* stylelint-disable-next-line primer/spacing */ - margin-inline-end: -1px; - position: relative; - - /* reset border-radius */ - button, - a { - border-radius: 0; - } + &:last-child { + button, + a { + border-top-right-radius: var(--borderRadius-medium); + border-bottom-right-radius: var(--borderRadius-medium); + } + } - &:focus, - &:active, - &:hover { - z-index: 1; + &:focus, + &:active, + &:hover { + z-index: 1; + } } - &:first-child { + /* this is a workaround until portal based tooltips are fully removed from dotcom */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ + &:has(div:last-child:empty) { button, a { - border-top-left-radius: var(--borderRadius-medium); - border-bottom-left-radius: var(--borderRadius-medium); + border-radius: var(--borderRadius-medium); } } - &:last-child { + /* if child is loading button */ + & > *[data-loading-wrapper] { + /* stylelint-disable-next-line primer/spacing */ + margin-inline-end: -1px; + position: relative; + + /* reset border-radius */ button, a { - border-top-right-radius: var(--borderRadius-medium); - border-bottom-right-radius: var(--borderRadius-medium); + border-radius: 0; + } + + &:focus, + &:active, + &:hover { + z-index: 1; + } + + &:first-child { + button, + a { + border-top-left-radius: var(--borderRadius-medium); + border-bottom-left-radius: var(--borderRadius-medium); + } + } + + &:last-child { + button, + a { + border-top-right-radius: var(--borderRadius-medium); + border-bottom-right-radius: var(--borderRadius-medium); + } } } } diff --git a/packages/react/src/Card/Card.module.css b/packages/react/src/Card/Card.module.css index 53360e1e55d..c192abda60d 100644 --- a/packages/react/src/Card/Card.module.css +++ b/packages/react/src/Card/Card.module.css @@ -1,108 +1,110 @@ -.Card { - display: grid; - position: relative; - overflow: hidden; - grid-auto-rows: max-content auto; - border: var(--borderWidth-thin) solid var(--borderColor-default); - box-shadow: var(--shadow-resting-small); - background-color: var(--bgColor-default); - gap: var(--stack-gap-normal); - - &[data-border-radius='large'] { - border-radius: var(--borderRadius-large); +@layer primer.components.Card { + .Card { + display: grid; + position: relative; + overflow: hidden; + grid-auto-rows: max-content auto; + border: var(--borderWidth-thin) solid var(--borderColor-default); + box-shadow: var(--shadow-resting-small); + background-color: var(--bgColor-default); + gap: var(--stack-gap-normal); + + &[data-border-radius='large'] { + border-radius: var(--borderRadius-large); + } + + &[data-border-radius='medium'] { + border-radius: var(--borderRadius-medium); + } + + &[data-padding='normal'] { + /* stylelint-disable-next-line primer/spacing */ + padding: var(--stack-padding-spacious); + } + + &[data-padding='condensed'] { + /* stylelint-disable-next-line primer/spacing */ + padding: var(--stack-padding-condensed); + } + + &[data-padding='none'] { + padding: 0; + } } - &[data-border-radius='medium'] { - border-radius: var(--borderRadius-medium); + .CardHeader { + display: block; + width: 100%; + height: auto; } - &[data-padding='normal'] { - /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-spacious); + .CardHeaderEdgeToEdge { + /* stylelint-disable primer/spacing */ + margin-top: calc(-1 * var(--stack-padding-spacious)); + margin-right: calc(-1 * var(--stack-padding-spacious)); + margin-left: calc(-1 * var(--stack-padding-spacious)); + width: calc(100% + 2 * var(--stack-padding-spacious)); + /* stylelint-enable primer/spacing */ } - &[data-padding='condensed'] { - /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-condensed); + .CardImage { + display: block; + width: 100%; + height: auto; } - &[data-padding='none'] { - padding: 0; + .CardIcon { + display: flex; + align-items: center; + justify-content: center; + width: var(--base-size-32); + height: var(--base-size-32); + border-radius: var(--borderRadius-medium); + background-color: var(--bgColor-muted); + color: var(--fgColor-muted); } -} - -.CardHeader { - display: block; - width: 100%; - height: auto; -} - -.CardHeaderEdgeToEdge { - /* stylelint-disable primer/spacing */ - margin-top: calc(-1 * var(--stack-padding-spacious)); - margin-right: calc(-1 * var(--stack-padding-spacious)); - margin-left: calc(-1 * var(--stack-padding-spacious)); - width: calc(100% + 2 * var(--stack-padding-spacious)); - /* stylelint-enable primer/spacing */ -} - -.CardImage { - display: block; - width: 100%; - height: auto; -} -.CardIcon { - display: flex; - align-items: center; - justify-content: center; - width: var(--base-size-32); - height: var(--base-size-32); - border-radius: var(--borderRadius-medium); - background-color: var(--bgColor-muted); - color: var(--fgColor-muted); -} - -.CardBody { - display: grid; - gap: var(--stack-gap-normal); -} + .CardBody { + display: grid; + gap: var(--stack-gap-normal); + } -.CardContent { - display: grid; - gap: var(--stack-gap-condensed); -} + .CardContent { + display: grid; + gap: var(--stack-gap-condensed); + } -.CardHeading { - font: var(--text-title-shorthand-small); - color: var(--fgColor-default); - margin: 0; -} + .CardHeading { + font: var(--text-title-shorthand-small); + color: var(--fgColor-default); + margin: 0; + } -.CardDescription { - font: var(--text-body-shorthand-medium); - color: var(--fgColor-muted); - margin: 0; -} + .CardDescription { + font: var(--text-body-shorthand-medium); + color: var(--fgColor-muted); + margin: 0; + } -.CardMetadataContainer { - display: flex; - align-items: center; - gap: var(--stack-gap-normal); - font: var(--text-body-shorthand-medium); - color: var(--fgColor-muted); -} + .CardMetadataContainer { + display: flex; + align-items: center; + gap: var(--stack-gap-normal); + font: var(--text-body-shorthand-medium); + color: var(--fgColor-muted); + } -.CardMetadataItem { - display: flex; - align-items: center; - gap: var(--stack-gap-condensed); - font: var(--text-body-shorthand-small); -} + .CardMetadataItem { + display: flex; + align-items: center; + gap: var(--stack-gap-condensed); + font: var(--text-body-shorthand-small); + } -.CardAction { - position: absolute; - top: var(--base-size-16); - right: var(--base-size-16); - z-index: 1; + .CardAction { + position: absolute; + top: var(--base-size-16); + right: var(--base-size-16); + z-index: 1; + } } diff --git a/packages/react/src/Checkbox/Checkbox.module.css b/packages/react/src/Checkbox/Checkbox.module.css index 2be4bc11d95..e8ba49b505c 100644 --- a/packages/react/src/Checkbox/Checkbox.module.css +++ b/packages/react/src/Checkbox/Checkbox.module.css @@ -1,102 +1,104 @@ -.Checkbox { - border-radius: var(--borderRadius-small); - - /* checked -> unchecked - add 120ms delay to fully see animation-out */ - transition: - background-color, - border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); - - &::before { - width: var(--base-size-16); - height: var(--base-size-16); - visibility: hidden; - content: ''; - /* stylelint-disable-next-line primer/colors */ - background-color: var(--fgColor-onEmphasis); - transition: visibility 0s linear 230ms; - clip-path: inset(var(--base-size-16) 0 0 0); - mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDEyIDkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTEuNzgwMyAwLjIxOTYyNUMxMS45MjEgMC4zNjA0MjcgMTIgMC41NTEzMDUgMTIgMC43NTAzMTNDMTIgMC45NDkzMjEgMTEuOTIxIDEuMTQwMTkgMTEuNzgwMyAxLjI4MUw0LjUxODYgOC41NDA0MkM0LjM3Nzc1IDguNjgxIDQuMTg2ODIgOC43NiAzLjk4Nzc0IDguNzZDMy43ODg2NyA4Ljc2IDMuNTk3NzMgOC42ODEgMy40NTY4OSA4LjU0MDQyTDAuMjAxNjIyIDUuMjg2MkMwLjA2ODkyNzcgNS4xNDM4MyAtMC4wMDMzMDkwNSA0Ljk1NTU1IDAuMDAwMTE2NDkzIDQuNzYwOThDMC4wMDM1NTIwNSA0LjU2NjQzIDAuMDgyMzg5NCA0LjM4MDgxIDAuMjIwMDMyIDQuMjQzMjFDMC4zNTc2NjUgNC4xMDU2MiAwLjU0MzM1NSA0LjAyNjgxIDAuNzM3OTcgNC4wMjMzOEMwLjkzMjU4NCA0LjAxOTk0IDEuMTIwOTMgNC4wOTIxNyAxLjI2MzM0IDQuMjI0ODJMMy45ODc3NCA2Ljk0ODM1TDEwLjcxODYgMC4yMTk2MjVDMTAuODU5NSAwLjA3ODk5MjMgMTEuMDUwNCAwIDExLjI0OTUgMEMxMS40NDg1IDAgMTEuNjM5NSAwLjA3ODk5MjMgMTEuNzgwMyAwLjIxOTYyNVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='); - mask-size: 75%; - mask-repeat: no-repeat; - mask-position: center; - animation: checkmarkOut 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards; - } - - &:checked, - &:indeterminate { - background: var(--control-checked-bgColor-rest); +@layer primer.components.Checkbox { + .Checkbox { + border-radius: var(--borderRadius-small); - /* using bgColor here to avoid a border change in dark high contrast */ - /* stylelint-disable-next-line primer/colors */ - border-color: var(--control-checked-bgColor-rest); + /* checked -> unchecked - add 120ms delay to fully see animation-out */ + transition: + background-color, + border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); &::before { - animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms; + width: var(--base-size-16); + height: var(--base-size-16); + visibility: hidden; + content: ''; + /* stylelint-disable-next-line primer/colors */ + background-color: var(--fgColor-onEmphasis); + transition: visibility 0s linear 230ms; + clip-path: inset(var(--base-size-16) 0 0 0); + mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDEyIDkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTEuNzgwMyAwLjIxOTYyNUMxMS45MjEgMC4zNjA0MjcgMTIgMC41NTEzMDUgMTIgMC43NTAzMTNDMTIgMC45NDkzMjEgMTEuOTIxIDEuMTQwMTkgMTEuNzgwMyAxLjI4MUw0LjUxODYgOC41NDA0MkM0LjM3Nzc1IDguNjgxIDQuMTg2ODIgOC43NiAzLjk4Nzc0IDguNzZDMy43ODg2NyA4Ljc2IDMuNTk3NzMgOC42ODEgMy40NTY4OSA4LjU0MDQyTDAuMjAxNjIyIDUuMjg2MkMwLjA2ODkyNzcgNS4xNDM4MyAtMC4wMDMzMDkwNSA0Ljk1NTU1IDAuMDAwMTE2NDkzIDQuNzYwOThDMC4wMDM1NTIwNSA0LjU2NjQzIDAuMDgyMzg5NCA0LjM4MDgxIDAuMjIwMDMyIDQuMjQzMjFDMC4zNTc2NjUgNC4xMDU2MiAwLjU0MzM1NSA0LjAyNjgxIDAuNzM3OTcgNC4wMjMzOEMwLjkzMjU4NCA0LjAxOTk0IDEuMTIwOTMgNC4wOTIxNyAxLjI2MzM0IDQuMjI0ODJMMy45ODc3NCA2Ljk0ODM1TDEwLjcxODYgMC4yMTk2MjVDMTAuODU5NSAwLjA3ODk5MjMgMTEuMDUwNCAwIDExLjI0OTUgMEMxMS40NDg1IDAgMTEuNjM5NSAwLjA3ODk5MjMgMTEuNzgwMyAwLjIxOTYyNVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='); + mask-size: 75%; + mask-repeat: no-repeat; + mask-position: center; + animation: checkmarkOut 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards; } - &:disabled { - background-color: var(--control-checked-bgColor-disabled); - border-color: var(--control-checked-borderColor-disabled); - opacity: 1; + &:checked, + &:indeterminate { + background: var(--control-checked-bgColor-rest); + + /* using bgColor here to avoid a border change in dark high contrast */ + /* stylelint-disable-next-line primer/colors */ + border-color: var(--control-checked-bgColor-rest); &::before { - /* stylelint-disable-next-line primer/colors */ - background-color: var(--control-checked-fgColor-disabled); + animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms; } - } - /* Windows High Contrast mode */ - @media (forced-colors: active) { - background-color: canvastext; - border-color: canvastext; + &:disabled { + background-color: var(--control-checked-bgColor-disabled); + border-color: var(--control-checked-borderColor-disabled); + opacity: 1; + + &::before { + /* stylelint-disable-next-line primer/colors */ + background-color: var(--control-checked-fgColor-disabled); + } + } + + /* Windows High Contrast mode */ + @media (forced-colors: active) { + background-color: canvastext; + border-color: canvastext; + } } - } - &:disabled { - cursor: not-allowed; - } + &:disabled { + cursor: not-allowed; + } - &:checked { - transition: - background-color, - border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; + &:checked { + transition: + background-color, + border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; - &::before { - visibility: visible; - transition: visibility 0s linear 0s; + &::before { + visibility: visible; + transition: visibility 0s linear 0s; + } } - } - &:indeterminate { - background: var(--control-checked-bgColor-rest); + &:indeterminate { + background: var(--control-checked-bgColor-rest); - &::before { - mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMiIgdmlld0JveD0iMCAwIDEwIDIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxQzAgMC40NDc3MTUgMC40NDc3MTUgMCAxIDBIOUM5LjU1MjI5IDAgMTAgMC40NDc3MTUgMTAgMUMxMCAxLjU1MjI4IDkuNTUyMjkgMiA5IDJIMUMwLjQ0NzcxNSAyIDAgMS41NTIyOCAwIDFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K'); - visibility: visible; + &::before { + mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMiIgdmlld0JveD0iMCAwIDEwIDIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxQzAgMC40NDc3MTUgMC40NDc3MTUgMCAxIDBIOUM5LjU1MjI5IDAgMTAgMC40NDc3MTUgMTAgMUMxMCAxLjU1MjI4IDkuNTUyMjkgMiA5IDJIMUMwLjQ0NzcxNSAyIDAgMS41NTIyOCAwIDFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K'); + visibility: visible; + } } - } - &:focus-visible:not(:disabled) { - @mixin focusOutline 2px; + &:focus-visible:not(:disabled) { + @mixin focusOutline 2px; + } } -} -@keyframes checkmarkIn { - from { - clip-path: inset(var(--base-size-16) 0 0 0); - } + @keyframes checkmarkIn { + from { + clip-path: inset(var(--base-size-16) 0 0 0); + } - to { - clip-path: inset(0 0 0 0); + to { + clip-path: inset(0 0 0 0); + } } -} -@keyframes checkmarkOut { - from { - clip-path: inset(0 0 0 0); - } + @keyframes checkmarkOut { + from { + clip-path: inset(0 0 0 0); + } - to { - clip-path: inset(var(--base-size-16) 0 0 0); + to { + clip-path: inset(var(--base-size-16) 0 0 0); + } } } diff --git a/packages/react/src/Checkbox/shared.module.css b/packages/react/src/Checkbox/shared.module.css index 963e4a18ae6..5467ef577b7 100644 --- a/packages/react/src/Checkbox/shared.module.css +++ b/packages/react/src/Checkbox/shared.module.css @@ -1,22 +1,24 @@ -.Input { - position: relative; - display: grid; - width: var(--base-size-16); - height: var(--base-size-16); - margin: 0; +@layer primer.components.Checkbox { + .Input { + position: relative; + display: grid; + width: var(--base-size-16); + height: var(--base-size-16); + margin: 0; - /* 2px to center align with label (20px line-height) */ - margin-top: var(--base-size-2); - cursor: pointer; - background-color: var(--bgColor-default); - border-color: var(--control-borderColor-emphasis); - border-style: solid; - border-width: var(--borderWidth-thin); - appearance: none; - place-content: center; + /* 2px to center align with label (20px line-height) */ + margin-top: var(--base-size-2); + cursor: pointer; + background-color: var(--bgColor-default); + border-color: var(--control-borderColor-emphasis); + border-style: solid; + border-width: var(--borderWidth-thin); + appearance: none; + place-content: center; - &:disabled { - background-color: var(--control-bgColor-disabled); - border-color: var(--control-borderColor-disabled); + &:disabled { + background-color: var(--control-bgColor-disabled); + border-color: var(--control-borderColor-disabled); + } } } diff --git a/packages/react/src/CircleBadge/CircleBadge.module.css b/packages/react/src/CircleBadge/CircleBadge.module.css index ec321f42118..5e962acd60f 100644 --- a/packages/react/src/CircleBadge/CircleBadge.module.css +++ b/packages/react/src/CircleBadge/CircleBadge.module.css @@ -1,18 +1,20 @@ -.CircleBadge { - display: flex; - align-items: center; - justify-content: center; - background-color: var(--bgColor-default); - border-radius: 50%; - box-shadow: var(--shadow-resting-medium); +@layer primer.components.CircleBadge { + .CircleBadge { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--bgColor-default); + border-radius: 50%; + box-shadow: var(--shadow-resting-medium); - &:where([data-inline]) { - display: inline-flex; + &:where([data-inline]) { + display: inline-flex; + } } -} -.CircleBadgeIcon { - height: auto; - max-height: 55%; - max-width: 60%; + .CircleBadgeIcon { + height: auto; + max-height: 55%; + max-width: 60%; + } } diff --git a/packages/react/src/__tests__/css-layers.test.ts b/packages/react/src/__tests__/css-layers.test.ts index b9baf8d2fe4..1dc0bf4f778 100644 --- a/packages/react/src/__tests__/css-layers.test.ts +++ b/packages/react/src/__tests__/css-layers.test.ts @@ -4,9 +4,21 @@ import {generate, parse} from 'css-tree' import type {StyleSheet} from 'css-tree' import {describe, expect, test} from 'vitest' -const allowlist = new Set([path.resolve(import.meta.dirname, '../Avatar/Avatar.module.css')]) +const allowlist = new Set([ + path.resolve(import.meta.dirname, '../Avatar/Avatar.module.css'), + path.resolve(import.meta.dirname, '../AvatarStack/AvatarStack.module.css'), + path.resolve(import.meta.dirname, '../BaseStyles.module.css'), + path.resolve(import.meta.dirname, '../BranchName/BranchName.module.css'), + path.resolve(import.meta.dirname, '../ButtonGroup/ButtonGroup.module.css'), + path.resolve(import.meta.dirname, '../Card/Card.module.css'), + path.resolve(import.meta.dirname, '../Checkbox/Checkbox.module.css'), + path.resolve(import.meta.dirname, '../Checkbox/shared.module.css'), + path.resolve(import.meta.dirname, '../CircleBadge/CircleBadge.module.css'), + path.resolve(import.meta.dirname, '../deprecated/FilteredSearch/FilteredSearch.module.css'), + path.resolve(import.meta.dirname, '../deprecated/UnderlineNav/UnderlineNav.module.css'), +]) const files = Array.from(allowlist).map(file => { - return [path.basename(file), file] + return [path.relative(path.resolve(import.meta.dirname, '..'), file), file] }) const CSS_LAYER_REGEX = /^primer\.components\.[A-Z][A-Za-z0-9]+$/ diff --git a/packages/react/src/deprecated/FilteredSearch/FilteredSearch.module.css b/packages/react/src/deprecated/FilteredSearch/FilteredSearch.module.css index 7b72314f38f..b69f3f71e2a 100644 --- a/packages/react/src/deprecated/FilteredSearch/FilteredSearch.module.css +++ b/packages/react/src/deprecated/FilteredSearch/FilteredSearch.module.css @@ -1,19 +1,21 @@ -.FilteredSearch { - display: flex; - align-items: stretch; +@layer primer.components.DeprecatedFilteredSearch { + .FilteredSearch { + display: flex; + align-items: stretch; - & summary, - & > button { - border-radius: 0; - border-top-left-radius: var(--borderRadius-medium); - border-bottom-left-radius: var(--borderRadius-medium); - border-right: 0; - } + & summary, + & > button { + border-radius: 0; + border-top-left-radius: var(--borderRadius-medium); + border-bottom-left-radius: var(--borderRadius-medium); + border-right: 0; + } - /* stylelint-disable-next-line selector-class-pattern */ - & :global(.TextInput-wrapper) { - border-radius: 0; - border-top-right-radius: var(--borderRadius-medium); - border-bottom-right-radius: var(--borderRadius-medium); + /* stylelint-disable-next-line selector-class-pattern */ + & :global(.TextInput-wrapper) { + border-radius: 0; + border-top-right-radius: var(--borderRadius-medium); + border-bottom-right-radius: var(--borderRadius-medium); + } } } diff --git a/packages/react/src/deprecated/UnderlineNav/UnderlineNav.module.css b/packages/react/src/deprecated/UnderlineNav/UnderlineNav.module.css index f4825c09be2..1a8361b5bff 100644 --- a/packages/react/src/deprecated/UnderlineNav/UnderlineNav.module.css +++ b/packages/react/src/deprecated/UnderlineNav/UnderlineNav.module.css @@ -1,85 +1,87 @@ -.UnderlineNav { - display: flex; - justify-content: space-between; - border-bottom: var(--borderWidth-thin) solid var(--borderColor-muted); +@layer primer.components.DeprecatedUnderlineNav { + .UnderlineNav { + display: flex; + justify-content: space-between; + border-bottom: var(--borderWidth-thin) solid var(--borderColor-muted); - &.UnderlineNav--right { - justify-content: flex-end; - } + &.UnderlineNav--right { + justify-content: flex-end; + } - &.UnderlineNav--right .UnderlineNavItem { - margin-right: 0; - margin-left: var(--base-size-16); - } + &.UnderlineNav--right .UnderlineNavItem { + margin-right: 0; + margin-left: var(--base-size-16); + } - &.UnderlineNav--right .UnderlineNavActions { - flex: 1 1 auto; - } + &.UnderlineNav--right .UnderlineNavActions { + flex: 1 1 auto; + } - &.UnderlineNav--full { - display: block; - } + &.UnderlineNav--full { + display: block; + } - .UnderlineNavBody { - display: flex; - /* stylelint-disable-next-line primer/spacing */ - margin-bottom: -1px; - } + .UnderlineNavBody { + display: flex; + /* stylelint-disable-next-line primer/spacing */ + margin-bottom: -1px; + } - .UnderlineNavActions { - align-self: center; + .UnderlineNavActions { + align-self: center; + } } -} -.UnderlineNavLink { - padding: var(--base-size-16) var(--base-size-8); - margin-right: var(--base-size-16); - font-size: var(--text-body-size-medium); - line-height: var(--text-title-lineHeight-large); - color: var(--fgColor-default); - text-align: center; - /* stylelint-disable-next-line primer/borders */ - border-bottom: 2px solid transparent; - text-decoration: none; + .UnderlineNavLink { + padding: var(--base-size-16) var(--base-size-8); + margin-right: var(--base-size-16); + font-size: var(--text-body-size-medium); + line-height: var(--text-title-lineHeight-large); + color: var(--fgColor-default); + text-align: center; + /* stylelint-disable-next-line primer/borders */ + border-bottom: 2px solid transparent; + text-decoration: none; - /* fallback :focus state */ - &:focus:not(:disabled) { - box-shadow: none; - outline: 2px solid var(--fgColor-accent); - outline-offset: -8px; + /* fallback :focus state */ + &:focus:not(:disabled) { + box-shadow: none; + outline: 2px solid var(--fgColor-accent); + outline-offset: -8px; - /* remove fallback :focus if :focus-visible is supported */ - &:not(:focus-visible) { - outline: solid 1px transparent; + /* remove fallback :focus if :focus-visible is supported */ + &:not(:focus-visible) { + outline: solid 1px transparent; + } } - } - /* default focus state */ - &:focus-visible:not(:disabled) { - box-shadow: none; - outline: 2px solid var(--fgColor-accent); - outline-offset: -8px; + /* default focus state */ + &:focus-visible:not(:disabled) { + box-shadow: none; + outline: 2px solid var(--fgColor-accent); + outline-offset: -8px; + } } -} -.UnderlineNavLink:hover, -.UnderlineNavLink:focus { - color: var(--fgColor-default); - text-decoration: none; - border-bottom-color: var(--borderColor-muted); - transition: border-bottom-color 0.2s ease; -} + .UnderlineNavLink:hover, + .UnderlineNavLink:focus { + color: var(--fgColor-default); + text-decoration: none; + border-bottom-color: var(--borderColor-muted); + transition: border-bottom-color 0.2s ease; + } -.UnderlineNavLink:hover .UnderlineNavOcticon, -.UnderlineNavLink:focus .UnderlineNavOcticon { - color: var(--fgColor-muted); -} + .UnderlineNavLink:hover .UnderlineNavOcticon, + .UnderlineNavLink:focus .UnderlineNavOcticon { + color: var(--fgColor-muted); + } -.UnderlineNavLink:where([data-selected]) { - color: var(--fgColor-default); - border-bottom-color: var(--underlineNav-borderColor-active); -} + .UnderlineNavLink:where([data-selected]) { + color: var(--fgColor-default); + border-bottom-color: var(--underlineNav-borderColor-active); + } -.UnderlineNavLink:where([data-selected]) .UnderlineNavOcticon { - color: var(--fgColor-default); + .UnderlineNavLink:where([data-selected]) .UnderlineNavOcticon { + color: var(--fgColor-default); + } } From c20854eb8aa77ef6ef3c5d916a75635ae7347c9e Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 9 Jun 2026 13:31:37 -0500 Subject: [PATCH 6/7] chore: remove BaseStyles CSS whitespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/react/src/BaseStyles.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/BaseStyles.module.css b/packages/react/src/BaseStyles.module.css index c338f607940..2e37c028f4b 100644 --- a/packages/react/src/BaseStyles.module.css +++ b/packages/react/src/BaseStyles.module.css @@ -2,8 +2,8 @@ /* stylelint-disable selector-max-specificity */ /* stylelint-disable selector-type-no-unknown */ - /* -------------------------------- - * Global Styles + /* -------------------------------- + * Global Styles *--------------------------------- */ * { box-sizing: border-box; From 59c0e6166ed5499223969b4366b9e261013aba60 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 9 Jun 2026 15:12:31 -0500 Subject: [PATCH 7/7] chore: standardize CSS layer changeset wording Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/css-layers-foundations-a.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/css-layers-foundations-a.md b/.changeset/css-layers-foundations-a.md index 06240557617..e7ba60e9a08 100644 --- a/.changeset/css-layers-foundations-a.md +++ b/.changeset/css-layers-foundations-a.md @@ -2,4 +2,4 @@ '@primer/react': patch --- -AvatarStack, BaseStyles, BranchName, ButtonGroup, Card, Checkbox, CircleBadge, FilteredSearch, UnderlineNav (deprecated): Improve custom class override behavior for component styles +AvatarStack, BaseStyles, BranchName, ButtonGroup, Card, Checkbox, CircleBadge, FilteredSearch, UnderlineNav (deprecated): Add CSS layer support for component styles