diff --git a/.changeset/css-layers-navigation-composites.md b/.changeset/css-layers-navigation-composites.md
new file mode 100644
index 00000000000..ebcea2df8e9
--- /dev/null
+++ b/.changeset/css-layers-navigation-composites.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': patch
+---
+
+LabelGroup, NavList, SegmentedControl, TreeView, UnderlineNav: Add CSS layer support for component styles
diff --git a/packages/react/src/LabelGroup/LabelGroup.module.css b/packages/react/src/LabelGroup/LabelGroup.module.css
index 08cdc11fe77..2b104b8bb54 100644
--- a/packages/react/src/LabelGroup/LabelGroup.module.css
+++ b/packages/react/src/LabelGroup/LabelGroup.module.css
@@ -1,52 +1,54 @@
-.Container {
- display: flex;
- flex-wrap: nowrap;
- gap: var(--base-size-4);
- /* stylelint-disable-next-line primer/typography */
- line-height: 1;
- max-width: 100%;
- overflow: hidden;
+@layer primer.components.LabelGroup {
+ .Container {
+ display: flex;
+ flex-wrap: nowrap;
+ gap: var(--base-size-4);
+ /* stylelint-disable-next-line primer/typography */
+ line-height: 1;
+ max-width: 100%;
+ overflow: hidden;
- &:where([data-overflow='inline']) {
- flex-wrap: wrap;
- }
+ &:where([data-overflow='inline']) {
+ flex-wrap: wrap;
+ }
- &:where([data-list]) {
- padding-inline-start: 0;
- margin-block-start: 0;
- margin-block-end: 0;
- list-style-type: none;
+ &:where([data-list]) {
+ padding-inline-start: 0;
+ margin-block-start: 0;
+ margin-block-end: 0;
+ list-style-type: none;
+ }
}
-}
-.ItemWrapper {
- display: flex;
- align-items: center;
- /* This min-height matches the height of the expand/collapse button.
- Without it, the labels/tokens will do a slight layout shift when expanded.
- This is because the height of the first row will match the token sizes,
- but the height of the second row will be the height of the collapse button.
- */
- min-height: 28px;
-}
+ .ItemWrapper {
+ display: flex;
+ align-items: center;
+ /* This min-height matches the height of the expand/collapse button.
+ Without it, the labels/tokens will do a slight layout shift when expanded.
+ This is because the height of the first row will match the token sizes,
+ but the height of the second row will be the height of the collapse button.
+ */
+ min-height: 28px;
+ }
-.ItemWrapper--hidden {
- order: 9999;
- pointer-events: none;
- visibility: hidden;
-}
+ .ItemWrapper--hidden {
+ order: 9999;
+ pointer-events: none;
+ visibility: hidden;
+ }
-.OverlayContainer {
- align-items: flex-start;
- display: flex;
-}
+ .OverlayContainer {
+ align-items: flex-start;
+ display: flex;
+ }
-.OverlayInner {
- display: flex;
- flex-wrap: wrap;
- gap: var(--base-size-4);
-}
+ .OverlayInner {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--base-size-4);
+ }
-.CloseButton {
- flex-shrink: 0;
+ .CloseButton {
+ flex-shrink: 0;
+ }
}
diff --git a/packages/react/src/NavList/NavList.module.css b/packages/react/src/NavList/NavList.module.css
index 9908361e112..e071dec505c 100644
--- a/packages/react/src/NavList/NavList.module.css
+++ b/packages/react/src/NavList/NavList.module.css
@@ -1,8 +1,10 @@
-.GroupHeading > a {
- color: var(--fgColor-default);
- text-decoration: inherit;
-}
+@layer primer.components.NavList {
+ .GroupHeading > a {
+ color: var(--fgColor-default);
+ text-decoration: inherit;
+ }
-.GroupHeading > a:hover {
- text-decoration: underline;
+ .GroupHeading > a:hover {
+ text-decoration: underline;
+ }
}
diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css
index 05bc0feed8f..1dc0ec540b1 100644
--- a/packages/react/src/SegmentedControl/SegmentedControl.module.css
+++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css
@@ -1,352 +1,354 @@
-.SegmentedControl {
- /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */
- --segmented-control-icon-width: 32px;
-
- display: inline-flex;
-
- /* TODO: use primitive `control.{small|medium}.size` when it is available */
- height: 32px;
- padding: 0;
- margin: 0;
- font-size: var(--text-body-size-medium);
- background-color: var(--controlTrack-bgColor-rest);
- border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent);
- border-radius: var(--borderRadius-medium);
-
- /* Responsive full-width */
- &[data-full-width='true'] {
- display: flex;
- width: 100%;
-
- --segmented-control-icon-width: 100%;
- }
+@layer primer.components.SegmentedControl {
+ .SegmentedControl {
+ /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */
+ --segmented-control-icon-width: 32px;
- &[data-full-width='false'] {
display: inline-flex;
- width: auto;
-
- --segmented-control-icon-width: 32px;
- }
- @media (--viewportRange-narrow) {
- &[data-full-width-narrow='true'] {
+ /* TODO: use primitive `control.{small|medium}.size` when it is available */
+ height: 32px;
+ padding: 0;
+ margin: 0;
+ font-size: var(--text-body-size-medium);
+ background-color: var(--controlTrack-bgColor-rest);
+ border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent);
+ border-radius: var(--borderRadius-medium);
+
+ /* Responsive full-width */
+ &[data-full-width='true'] {
display: flex;
width: 100%;
--segmented-control-icon-width: 100%;
}
- &[data-full-width-narrow='false'] {
+ &[data-full-width='false'] {
display: inline-flex;
width: auto;
--segmented-control-icon-width: 32px;
}
- }
- @media (--viewportRange-regular) {
- &[data-full-width-regular='true'] {
- display: flex;
- width: 100%;
+ @media (--viewportRange-narrow) {
+ &[data-full-width-narrow='true'] {
+ display: flex;
+ width: 100%;
- --segmented-control-icon-width: 100%;
- }
+ --segmented-control-icon-width: 100%;
+ }
- &[data-full-width-regular='false'] {
- display: inline-flex;
- width: auto;
+ &[data-full-width-narrow='false'] {
+ display: inline-flex;
+ width: auto;
- --segmented-control-icon-width: 32px;
+ --segmented-control-icon-width: 32px;
+ }
}
- }
- @media (--viewportRange-wide) {
- &[data-full-width-wide='true'] {
- display: flex;
- width: 100%;
+ @media (--viewportRange-regular) {
+ &[data-full-width-regular='true'] {
+ display: flex;
+ width: 100%;
- --segmented-control-icon-width: 100%;
- }
+ --segmented-control-icon-width: 100%;
+ }
- &[data-full-width-wide='false'] {
- display: inline-flex;
- width: auto;
+ &[data-full-width-regular='false'] {
+ display: inline-flex;
+ width: auto;
- --segmented-control-icon-width: 32px;
+ --segmented-control-icon-width: 32px;
+ }
}
- &[data-full-width-regular='true']:not([data-full-width-wide='true']) {
- display: inline-flex;
- width: auto;
+ @media (--viewportRange-wide) {
+ &[data-full-width-wide='true'] {
+ display: flex;
+ width: 100%;
- --segmented-control-icon-width: 32px;
- }
- }
+ --segmented-control-icon-width: 100%;
+ }
- /* Hide when dropdown variant is active */
- &[data-variant='dropdown'] {
- display: none;
- }
+ &[data-full-width-wide='false'] {
+ display: inline-flex;
+ width: auto;
- /* Handle hideLabels variant - hide button text */
- &[data-variant='hideLabels'] .Text {
- display: none;
- }
+ --segmented-control-icon-width: 32px;
+ }
- @media (--viewportRange-narrow) {
- &[data-variant-narrow='dropdown'] {
- display: none;
- }
+ &[data-full-width-regular='true']:not([data-full-width-wide='true']) {
+ display: inline-flex;
+ width: auto;
- &[data-variant-narrow='hideLabels'] .Text {
- display: none;
+ --segmented-control-icon-width: 32px;
+ }
}
- }
- @media (--viewportRange-regular) {
- &[data-variant-regular='dropdown'] {
+ /* Hide when dropdown variant is active */
+ &[data-variant='dropdown'] {
display: none;
}
- &[data-variant-regular='hideLabels'] .Text {
+ /* Handle hideLabels variant - hide button text */
+ &[data-variant='hideLabels'] .Text {
display: none;
}
- }
- @media (--viewportRange-wide) {
- &[data-variant-wide='dropdown'] {
- display: none;
+ @media (--viewportRange-narrow) {
+ &[data-variant-narrow='dropdown'] {
+ display: none;
+ }
+
+ &[data-variant-narrow='hideLabels'] .Text {
+ display: none;
+ }
}
- &[data-variant-wide='hideLabels'] .Text {
- display: none;
+ @media (--viewportRange-regular) {
+ &[data-variant-regular='dropdown'] {
+ display: none;
+ }
+
+ &[data-variant-regular='hideLabels'] .Text {
+ display: none;
+ }
}
- }
- &:where([data-size='small']) {
- /* TODO: use primitive `control.{small|medium}.size` when it is available */
- height: 28px;
- font-size: var(--text-body-size-small);
- }
-}
+ @media (--viewportRange-wide) {
+ &[data-variant-wide='dropdown'] {
+ display: none;
+ }
-.DropdownContainer {
- display: none;
+ &[data-variant-wide='hideLabels'] .Text {
+ display: none;
+ }
+ }
- /* Show when dropdown variant is active */
- &[data-variant='dropdown'] {
- display: block;
+ &:where([data-size='small']) {
+ /* TODO: use primitive `control.{small|medium}.size` when it is available */
+ height: 28px;
+ font-size: var(--text-body-size-small);
+ }
}
- @media (--viewportRange-narrow) {
- &[data-variant-narrow='dropdown'] {
+ .DropdownContainer {
+ display: none;
+
+ /* Show when dropdown variant is active */
+ &[data-variant='dropdown'] {
display: block;
}
- }
- @media (--viewportRange-regular) {
- &[data-variant-regular='dropdown'] {
- display: block;
+ @media (--viewportRange-narrow) {
+ &[data-variant-narrow='dropdown'] {
+ display: block;
+ }
}
- }
- @media (--viewportRange-wide) {
- &[data-variant-wide='dropdown'] {
- display: block;
+ @media (--viewportRange-regular) {
+ &[data-variant-regular='dropdown'] {
+ display: block;
+ }
}
- }
-}
-.Item {
- position: relative;
- display: block;
- /* stylelint-disable-next-line primer/spacing */
- margin-top: -1px;
- /* stylelint-disable-next-line primer/spacing */
- margin-bottom: -1px;
- flex-grow: 1;
+ @media (--viewportRange-wide) {
+ &[data-variant-wide='dropdown'] {
+ display: block;
+ }
+ }
+ }
- &:not(:last-child) {
+ .Item {
+ position: relative;
+ display: block;
+ /* stylelint-disable-next-line primer/spacing */
+ margin-top: -1px;
/* stylelint-disable-next-line primer/spacing */
- margin-right: 1px;
-
- &::after {
- position: absolute;
- top: var(--base-size-8);
- right: calc(-1 * var(--base-size-2));
- bottom: var(--base-size-8);
- width: 1px;
- content: '';
- /* stylelint-disable-next-line primer/colors */
- background-color: var(--borderColor-default);
+ margin-bottom: -1px;
+ flex-grow: 1;
+
+ &:not(:last-child) {
+ /* stylelint-disable-next-line primer/spacing */
+ margin-right: 1px;
+
+ &::after {
+ position: absolute;
+ top: var(--base-size-8);
+ right: calc(-1 * var(--base-size-2));
+ bottom: var(--base-size-8);
+ width: 1px;
+ content: '';
+ /* stylelint-disable-next-line primer/colors */
+ background-color: var(--borderColor-default);
+ }
+
+ /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */
+ &:has(+ [data-selected])::after,
+ &:where([data-selected])::after {
+ background-color: transparent;
+ }
}
/* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */
- &:has(+ [data-selected])::after,
- &:where([data-selected])::after {
+ &:focus-within:has(:focus-visible) {
background-color: transparent;
}
- }
-
- /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */
- &:focus-within:has(:focus-visible) {
- background-color: transparent;
- }
- &:first-child {
- /* stylelint-disable-next-line primer/spacing */
- margin-left: -1px;
- }
+ &:first-child {
+ /* stylelint-disable-next-line primer/spacing */
+ margin-left: -1px;
+ }
- &:last-child {
- /* stylelint-disable-next-line primer/spacing */
- margin-right: -1px;
- }
+ &:last-child {
+ /* stylelint-disable-next-line primer/spacing */
+ margin-right: -1px;
+ }
- .Counter {
- margin-inline-start: var(--base-size-8);
- display: flex;
- align-items: center;
+ .Counter {
+ margin-inline-start: var(--base-size-8);
+ display: flex;
+ align-items: center;
+ }
}
-}
-.Button {
- /* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */
- --segmented-control-button-inner-padding: 12px;
- --segmented-control-button-bg-inset: 4px;
- --segmented-control-outer-radius: var(--borderRadius-medium);
-
- width: 100%;
- height: 100%;
- /* stylelint-disable-next-line primer/spacing */
- padding: var(--segmented-control-button-bg-inset);
- font-family: inherit;
- font-size: inherit;
- font-weight: var(--base-text-weight-normal);
- color: currentColor;
- cursor: pointer;
- background-color: transparent;
- border-color: transparent;
- border-width: 0;
- /* stylelint-disable-next-line primer/borders */
- border-radius: var(--segmented-control-outer-radius);
-
- & svg {
- fill: var(--fgColor-muted);
- color: var(--fgColor-muted);
- }
+ .Button {
+ /* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */
+ --segmented-control-button-inner-padding: 12px;
+ --segmented-control-button-bg-inset: 4px;
+ --segmented-control-outer-radius: var(--borderRadius-medium);
- /* fallback :focus state */
- &:focus:not(:disabled) {
- outline: var(--base-size-2) solid var(--fgColor-accent);
- outline-offset: -1px;
- box-shadow: none;
+ width: 100%;
+ height: 100%;
+ /* stylelint-disable-next-line primer/spacing */
+ padding: var(--segmented-control-button-bg-inset);
+ font-family: inherit;
+ font-size: inherit;
+ font-weight: var(--base-text-weight-normal);
+ color: currentColor;
+ cursor: pointer;
+ background-color: transparent;
+ border-color: transparent;
+ border-width: 0;
+ /* stylelint-disable-next-line primer/borders */
+ border-radius: var(--segmented-control-outer-radius);
- /* remove fallback :focus if :focus-visible is supported */
- &:not(:focus-visible) {
- outline: solid 1px transparent;
+ & svg {
+ fill: var(--fgColor-muted);
+ color: var(--fgColor-muted);
}
- }
- /* default focus state */
- &:focus-visible:not(:disabled) {
- outline: var(--base-size-2) solid var(--fgColor-accent);
- outline-offset: -1px;
- box-shadow: none;
- }
+ /* fallback :focus state */
+ &:focus:not(:disabled) {
+ outline: var(--base-size-2) solid var(--fgColor-accent);
+ outline-offset: -1px;
+ box-shadow: none;
- /* stylelint-disable-next-line selector-max-specificity */
- &:focus:focus-visible:not(:last-child)::after {
- /* fixes an issue where the focus outline shows over the pseudo-element */
- width: 0;
- }
+ /* remove fallback :focus if :focus-visible is supported */
+ &:not(:focus-visible) {
+ outline: solid 1px transparent;
+ }
+ }
- &[aria-disabled='true']:not([aria-current='true']) {
- cursor: not-allowed;
- color: var(--fgColor-disabled);
- background-color: transparent;
+ /* default focus state */
+ &:focus-visible:not(:disabled) {
+ outline: var(--base-size-2) solid var(--fgColor-accent);
+ outline-offset: -1px;
+ box-shadow: none;
+ }
- & svg {
- fill: var(--fgColor-disabled);
+ /* stylelint-disable-next-line selector-max-specificity */
+ &:focus:focus-visible:not(:last-child)::after {
+ /* fixes an issue where the focus outline shows over the pseudo-element */
+ width: 0;
+ }
+
+ &[aria-disabled='true']:not([aria-current='true']) {
+ cursor: not-allowed;
color: var(--fgColor-disabled);
+ background-color: transparent;
+
+ & svg {
+ fill: var(--fgColor-disabled);
+ color: var(--fgColor-disabled);
+ }
}
- }
- @media (pointer: coarse) {
- &::before {
- position: absolute;
- top: 50%;
- right: 0;
- left: 0;
- min-height: 44px;
- content: '';
- transform: translateY(-50%);
+ @media (pointer: coarse) {
+ &::before {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ left: 0;
+ min-height: 44px;
+ content: '';
+ transform: translateY(-50%);
+ }
}
}
-}
-
-.IconButton {
- width: var(--segmented-control-icon-width, 32px);
-}
-
-.Content {
- display: flex;
- height: 100%;
- /* stylelint-disable-next-line primer/spacing */
- padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset));
- /* stylelint-disable-next-line primer/spacing */
- padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset));
- border-color: transparent;
- border-style: solid;
- border-width: var(--borderWidth-thin);
-
- /*
- innerRadius = outerRadius - distance/2
- https://stackoverflow.com/questions/2932146/math-problem-determine-the-corner-radius-of-an-inner-border-based-on-outer-corn
- */
- /* stylelint-disable-next-line primer/borders */
- border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2);
- align-items: center;
- justify-content: center;
-}
-.Button[aria-current='true'] {
- padding: 0;
- font-weight: var(--base-text-weight-semibold);
+ .IconButton {
+ width: var(--segmented-control-icon-width, 32px);
+ }
.Content {
+ display: flex;
+ height: 100%;
/* stylelint-disable-next-line primer/spacing */
- padding-right: var(--segmented-control-button-inner-padding);
+ padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset));
/* stylelint-disable-next-line primer/spacing */
- padding-left: var(--segmented-control-button-inner-padding);
- background-color: var(--controlKnob-bgColor-rest);
- border-color: var(--controlKnob-borderColor-rest);
+ padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset));
+ border-color: transparent;
+ border-style: solid;
+ border-width: var(--borderWidth-thin);
+
+ /*
+ innerRadius = outerRadius - distance/2
+ https://stackoverflow.com/questions/2932146/math-problem-determine-the-corner-radius-of-an-inner-border-based-on-outer-corn
+ */
/* stylelint-disable-next-line primer/borders */
- border-radius: var(--segmented-control-outer-radius);
+ border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2);
+ align-items: center;
+ justify-content: center;
}
-}
-.Button:not([aria-current='true'], [aria-disabled='true']) {
- &:hover .Content {
- background-color: var(--controlTrack-bgColor-hover);
+ .Button[aria-current='true'] {
+ padding: 0;
+ font-weight: var(--base-text-weight-semibold);
+
+ .Content {
+ /* stylelint-disable-next-line primer/spacing */
+ padding-right: var(--segmented-control-button-inner-padding);
+ /* stylelint-disable-next-line primer/spacing */
+ padding-left: var(--segmented-control-button-inner-padding);
+ background-color: var(--controlKnob-bgColor-rest);
+ border-color: var(--controlKnob-borderColor-rest);
+ /* stylelint-disable-next-line primer/borders */
+ border-radius: var(--segmented-control-outer-radius);
+ }
}
- &:active .Content {
- background-color: var(--controlTrack-bgColor-active);
+ .Button:not([aria-current='true'], [aria-disabled='true']) {
+ &:hover .Content {
+ background-color: var(--controlTrack-bgColor-hover);
+ }
+
+ &:active .Content {
+ background-color: var(--controlTrack-bgColor-active);
+ }
}
-}
-.Text::after {
- display: block;
- height: 0;
- overflow: hidden;
- font-weight: var(--base-text-weight-semibold);
- pointer-events: none;
- visibility: hidden;
- content: attr(data-text);
- user-select: none;
-}
+ .Text::after {
+ display: block;
+ height: 0;
+ overflow: hidden;
+ font-weight: var(--base-text-weight-semibold);
+ pointer-events: none;
+ visibility: hidden;
+ content: attr(data-text);
+ user-select: none;
+ }
-.LeadingIcon {
- margin-right: var(--base-size-4);
+ .LeadingIcon {
+ margin-right: var(--base-size-4);
+ }
}
diff --git a/packages/react/src/TreeView/TreeView.module.css b/packages/react/src/TreeView/TreeView.module.css
index 2c3ea4ee472..df567d87ffd 100644
--- a/packages/react/src/TreeView/TreeView.module.css
+++ b/packages/react/src/TreeView/TreeView.module.css
@@ -1,291 +1,293 @@
-.TreeViewRootUlStyles {
- padding: 0;
- margin: 0;
- list-style: none;
-
- /*
- * WARNING: This is a performance optimization.
- *
- * We define styles for the tree items at the root level of the tree
- * to avoid recomputing the styles for each item when the tree updates.
- * We're sacrificing maintainability for performance because TreeView
- * needs to be performant enough to handle large trees (thousands of items).
- *
- * This is intended to be a temporary solution until we can improve the
- * performance of our styling patterns.
- *
- * Do NOT copy this pattern without understanding the tradeoffs.
- */
- .TreeViewItem {
+@layer primer.components.TreeView {
+ .TreeViewRootUlStyles {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+
/*
- * `overflow-clip-margin` extends the paint clip edge by 8px so the current-item indicator
- * (positioned at `left: -8px` of the row container) remains visible when a consumer applies
- * `contain: paint` (or `contain: strict`, or `content-visibility: auto`) to this `
`. Has
- * no effect when no paint containment is active, so default rendering is unchanged.
+ * WARNING: This is a performance optimization.
+ *
+ * We define styles for the tree items at the root level of the tree
+ * to avoid recomputing the styles for each item when the tree updates.
+ * We're sacrificing maintainability for performance because TreeView
+ * needs to be performant enough to handle large trees (thousands of items).
+ *
+ * This is intended to be a temporary solution until we can improve the
+ * performance of our styling patterns.
+ *
+ * Do NOT copy this pattern without understanding the tradeoffs.
*/
- overflow-clip-margin: var(--base-size-8);
- outline: none;
+ .TreeViewItem {
+ /*
+ * `overflow-clip-margin` extends the paint clip edge by 8px so the current-item indicator
+ * (positioned at `left: -8px` of the row container) remains visible when a consumer applies
+ * `contain: paint` (or `contain: strict`, or `content-visibility: auto`) to this ``. Has
+ * no effect when no paint containment is active, so default rendering is unchanged.
+ */
+ overflow-clip-margin: var(--base-size-8);
+ outline: none;
- &:focus-visible > div,
- &:global(.focus-visible) > div {
- box-shadow: var(--boxShadow-thick) var(--fgColor-accent);
+ &:focus-visible > div,
+ &:global(.focus-visible) > div {
+ box-shadow: var(--boxShadow-thick) var(--fgColor-accent);
- @media (forced-colors: active) {
- outline: 2px solid HighlightText;
- outline-offset: -2px;
+ @media (forced-colors: active) {
+ outline: 2px solid HighlightText;
+ outline-offset: -2px;
+ }
+ }
+
+ &[data-has-leading-action] {
+ --has-leading-action: 1;
}
}
- &[data-has-leading-action] {
- --has-leading-action: 1;
+ .TreeViewItemContainer {
+ --level: 1;
+ --toggle-width: 1rem;
+ --min-item-height: 2rem;
+
+ position: relative;
+ display: grid;
+ width: 100%;
+ /*
+ * Mirrors the `overflow-clip-margin` on `.TreeViewItem` so the indicator also stays
+ * visible when `containIntrinsicSize` is set on this row (which sets
+ * `content-visibility: auto` on this container and implies paint containment).
+ */
+ overflow-clip-margin: var(--base-size-8);
+ font-size: var(--text-body-size-medium);
+ color: var(--fgColor-default);
+ cursor: pointer;
+ border-radius: var(--borderRadius-medium);
+ grid-template-columns: var(--spacer-width) var(--leading-action-width) var(--toggle-width) 1fr auto;
+ grid-template-areas: 'spacer leadingAction toggle content trailingAction';
+
+ --leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem);
+ --spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2));
+
+ &:hover {
+ background-color: var(--control-transparent-bgColor-hover);
+
+ @media (forced-colors: active) {
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ }
+ }
+
+ @media (pointer: coarse) {
+ --toggle-width: 1.5rem;
+ --min-item-height: 2.75rem;
+ }
}
- }
- .TreeViewItemContainer {
- --level: 1;
- --toggle-width: 1rem;
- --min-item-height: 2rem;
+ &:where([data-omit-spacer='true']) .TreeViewItemContainer {
+ grid-template-columns: 0 0 0 1fr auto;
+ }
- position: relative;
- display: grid;
- width: 100%;
/*
- * Mirrors the `overflow-clip-margin` on `.TreeViewItem` so the indicator also stays
- * visible when `containIntrinsicSize` is set on this row (which sets
- * `content-visibility: auto` on this container and implies paint containment).
+ * Suppress hover affordances on rows being used as skeleton loading placeholders.
+ * Marked positively via `data-loading` from `LoadingItem` so we avoid the broad
+ * invalidation cost of `:has(.TreeViewItemSkeleton)` across every row in large trees.
*/
- overflow-clip-margin: var(--base-size-8);
- font-size: var(--text-body-size-medium);
- color: var(--fgColor-default);
- cursor: pointer;
- border-radius: var(--borderRadius-medium);
- grid-template-columns: var(--spacer-width) var(--leading-action-width) var(--toggle-width) 1fr auto;
- grid-template-areas: 'spacer leadingAction toggle content trailingAction';
-
- --leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem);
- --spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2));
-
- &:hover {
- background-color: var(--control-transparent-bgColor-hover);
+ .TreeViewItem:where([data-loading]) > .TreeViewItemContainer:hover {
+ cursor: default;
+ background-color: transparent;
@media (forced-colors: active) {
- outline: 2px solid transparent;
- outline-offset: -2px;
+ outline: none;
}
}
- @media (pointer: coarse) {
- --toggle-width: 1.5rem;
- --min-item-height: 2.75rem;
+ .TreeViewItem[aria-current='true'] > .TreeViewItemContainer {
+ background-color: var(--control-transparent-bgColor-selected);
+
+ /* Current item indicator */
+ /* stylelint-disable-next-line selector-max-specificity */
+ &::after {
+ position: absolute;
+ top: calc(50% - var(--base-size-12));
+ left: calc(-1 * var(--base-size-8));
+ width: 0.25rem;
+ height: 1.5rem;
+ content: '';
+
+ /*
+ * Use fgColor accent for consistency across all themes. Using the "correct" variable,
+ * --bgColor-accent-emphasis, causes vrt failures for dark high contrast mode
+ */
+ /* stylelint-disable-next-line primer/colors */
+ background-color: var(--fgColor-accent);
+ border-radius: var(--borderRadius-medium);
+
+ @media (forced-colors: active) {
+ background-color: HighlightText;
+ }
+ }
}
- }
- &:where([data-omit-spacer='true']) .TreeViewItemContainer {
- grid-template-columns: 0 0 0 1fr auto;
- }
-
- /*
- * Suppress hover affordances on rows being used as skeleton loading placeholders.
- * Marked positively via `data-loading` from `LoadingItem` so we avoid the broad
- * invalidation cost of `:has(.TreeViewItemSkeleton)` across every row in large trees.
- */
- .TreeViewItem:where([data-loading]) > .TreeViewItemContainer:hover {
- cursor: default;
- background-color: transparent;
-
- @media (forced-colors: active) {
- outline: none;
+ .TreeViewItemToggle {
+ display: flex;
+ height: 100%;
+
+ /* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap
+ across more lines. */
+ /* stylelint-disable-next-line primer/spacing */
+ padding-top: calc(var(--min-item-height) / 2 - var(--base-size-12) / 2);
+ color: var(--fgColor-muted);
+ grid-area: toggle;
+ justify-content: center;
+ align-items: flex-start;
}
- }
-
- .TreeViewItem[aria-current='true'] > .TreeViewItemContainer {
- background-color: var(--control-transparent-bgColor-selected);
-
- /* Current item indicator */
- /* stylelint-disable-next-line selector-max-specificity */
- &::after {
- position: absolute;
- top: calc(50% - var(--base-size-12));
- left: calc(-1 * var(--base-size-8));
- width: 0.25rem;
- height: 1.5rem;
- content: '';
-
- /*
- * Use fgColor accent for consistency across all themes. Using the "correct" variable,
- * --bgColor-accent-emphasis, causes vrt failures for dark high contrast mode
- */
- /* stylelint-disable-next-line primer/colors */
- background-color: var(--fgColor-accent);
- border-radius: var(--borderRadius-medium);
- @media (forced-colors: active) {
- background-color: HighlightText;
- }
+ .TreeViewItemToggleHover:hover {
+ background-color: var(--control-transparent-bgColor-hover);
}
- }
- .TreeViewItemToggle {
- display: flex;
- height: 100%;
-
- /* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap
- across more lines. */
- /* stylelint-disable-next-line primer/spacing */
- padding-top: calc(var(--min-item-height) / 2 - var(--base-size-12) / 2);
- color: var(--fgColor-muted);
- grid-area: toggle;
- justify-content: center;
- align-items: flex-start;
- }
+ .TreeViewItemToggleEnd {
+ border-top-left-radius: var(--borderRadius-medium);
+ border-bottom-left-radius: var(--borderRadius-medium);
+ }
- .TreeViewItemToggleHover:hover {
- background-color: var(--control-transparent-bgColor-hover);
- }
+ .TreeViewItemContent {
+ display: flex;
+ height: 100%;
+ padding: 0 var(--base-size-8);
+
+ /* The dynamic top and bottom padding to maintain the minimum item height for single line items */
+ /* stylelint-disable-next-line primer/spacing */
+ padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
+ /* stylelint-disable-next-line primer/spacing */
+ padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
+ line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285));
+ grid-area: content;
+ gap: var(--stack-gap-condensed);
+ }
- .TreeViewItemToggleEnd {
- border-top-left-radius: var(--borderRadius-medium);
- border-bottom-left-radius: var(--borderRadius-medium);
- }
+ .TreeViewItemContentText {
+ flex: 1 1 auto;
+ width: 0;
+ }
- .TreeViewItemContent {
- display: flex;
- height: 100%;
- padding: 0 var(--base-size-8);
-
- /* The dynamic top and bottom padding to maintain the minimum item height for single line items */
- /* stylelint-disable-next-line primer/spacing */
- padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
- /* stylelint-disable-next-line primer/spacing */
- padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
- line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285));
- grid-area: content;
- gap: var(--stack-gap-condensed);
- }
+ &:where([data-truncate-text='true']) .TreeViewItemContentText {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
- .TreeViewItemContentText {
- flex: 1 1 auto;
- width: 0;
- }
+ &:where([data-truncate-text='false']) .TreeViewItemContentText {
+ /* stylelint-disable-next-line declaration-property-value-keyword-no-deprecated */
+ word-break: break-word;
+ }
- &:where([data-truncate-text='true']) .TreeViewItemContentText {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
+ .TreeViewItemVisual {
+ display: flex;
- &:where([data-truncate-text='false']) .TreeViewItemContentText {
- /* stylelint-disable-next-line declaration-property-value-keyword-no-deprecated */
- word-break: break-word;
- }
+ /* The visual icons should appear vertically centered for single-line items, but remain at the top for items that wrap
+ across more lines. */
+ height: var(--custom-line-height, 1.3rem);
+ color: var(--fgColor-muted);
+ align-items: center;
+ }
- .TreeViewItemVisual {
- display: flex;
+ .TreeViewItemLeadingAction {
+ display: flex;
+ color: var(--fgColor-muted);
+ grid-area: leadingAction;
- /* The visual icons should appear vertically centered for single-line items, but remain at the top for items that wrap
- across more lines. */
- height: var(--custom-line-height, 1.3rem);
- color: var(--fgColor-muted);
- align-items: center;
- }
+ & > button {
+ flex-shrink: 1;
+ }
+ }
- .TreeViewItemLeadingAction {
- display: flex;
- color: var(--fgColor-muted);
- grid-area: leadingAction;
+ .TreeViewItemTrailingAction {
+ display: flex;
+ color: var(--fgColor-muted);
+ grid-area: trailingAction;
+ }
- & > button {
+ .TreeViewItemTrailingActionButton {
flex-shrink: 1;
}
- }
-
- .TreeViewItemTrailingAction {
- display: flex;
- color: var(--fgColor-muted);
- grid-area: trailingAction;
- }
- .TreeViewItemTrailingActionButton {
- flex-shrink: 1;
- }
+ .TreeViewItemLevelLine {
+ width: 100%;
+ height: 100%;
+ border-right: var(--borderWidth-thin) solid;
- .TreeViewItemLevelLine {
- width: 100%;
- height: 100%;
- border-right: var(--borderWidth-thin) solid;
+ /*
+ * `--tree-line-color` is set on the root `` and inherited down. On coarse pointers it
+ * stays unset and falls back to `muted` (lines always visible). On hover-capable devices it
+ * is initialized to `transparent` on the root and flipped to `muted` while the tree is
+ * hovered or focused, so the browser only has to propagate a single inherited custom
+ * property instead of re-matching `.TreeViewItemLevelLine` descendant selectors on every
+ * hover/focus change inside large trees.
+ */
+ /* stylelint-disable-next-line primer/colors -- private custom property, defaults to a Primer token */
+ border-color: var(--tree-line-color, var(--borderColor-muted));
+ }
- /*
- * `--tree-line-color` is set on the root `` and inherited down. On coarse pointers it
- * stays unset and falls back to `muted` (lines always visible). On hover-capable devices it
- * is initialized to `transparent` on the root and flipped to `muted` while the tree is
- * hovered or focused, so the browser only has to propagate a single inherited custom
- * property instead of re-matching `.TreeViewItemLevelLine` descendant selectors on every
- * hover/focus change inside large trees.
- */
- /* stylelint-disable-next-line primer/colors -- private custom property, defaults to a Primer token */
- border-color: var(--tree-line-color, var(--borderColor-muted));
- }
+ @media (hover: hover) {
+ --tree-line-color: transparent;
- @media (hover: hover) {
- --tree-line-color: transparent;
+ &:hover,
+ &:focus-within {
+ --tree-line-color: var(--borderColor-muted);
+ }
+ }
- &:hover,
- &:focus-within {
- --tree-line-color: var(--borderColor-muted);
+ .TreeViewDirectoryIcon {
+ display: grid;
+ color: var(--treeViewItem-leadingVisual-iconColor-rest);
}
- }
- .TreeViewDirectoryIcon {
- display: grid;
- color: var(--treeViewItem-leadingVisual-iconColor-rest);
+ .TreeViewVisuallyHidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ /* stylelint-disable-next-line primer/spacing */
+ margin: -1px;
+ overflow: hidden;
+ /* stylelint-disable-next-line property-no-deprecated */
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+ }
}
- .TreeViewVisuallyHidden {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- /* stylelint-disable-next-line primer/spacing */
- margin: -1px;
- overflow: hidden;
- /* stylelint-disable-next-line property-no-deprecated */
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border-width: 0;
- }
-}
+ .TreeViewSkeletonItemContainerStyle {
+ display: flex;
+ align-items: center;
+ column-gap: 0.5rem;
+ height: 2rem;
-.TreeViewSkeletonItemContainerStyle {
- display: flex;
- align-items: center;
- column-gap: 0.5rem;
- height: 2rem;
+ @media (pointer: coarse) {
+ height: 2.75rem;
+ }
- @media (pointer: coarse) {
- height: 2.75rem;
- }
+ &:nth-of-type(5n + 1) {
+ --tree-item-loading-width: 67%;
+ }
- &:nth-of-type(5n + 1) {
- --tree-item-loading-width: 67%;
- }
+ &:nth-of-type(5n + 2) {
+ --tree-item-loading-width: 47%;
+ }
- &:nth-of-type(5n + 2) {
- --tree-item-loading-width: 47%;
- }
+ &:nth-of-type(5n + 3) {
+ --tree-item-loading-width: 73%;
+ }
- &:nth-of-type(5n + 3) {
- --tree-item-loading-width: 73%;
- }
+ &:nth-of-type(5n + 4) {
+ --tree-item-loading-width: 64%;
+ }
- &:nth-of-type(5n + 4) {
- --tree-item-loading-width: 64%;
+ &:nth-of-type(5n + 5) {
+ --tree-item-loading-width: 50%;
+ }
}
- &:nth-of-type(5n + 5) {
- --tree-item-loading-width: 50%;
+ .TreeItemSkeletonTextStyles {
+ width: var(--tree-item-loading-width, 67%);
}
}
-
-.TreeItemSkeletonTextStyles {
- width: var(--tree-item-loading-width, 67%);
-}
diff --git a/packages/react/src/UnderlineNav/UnderlineNav.module.css b/packages/react/src/UnderlineNav/UnderlineNav.module.css
index d905aff3fcf..4136a0fdf14 100644
--- a/packages/react/src/UnderlineNav/UnderlineNav.module.css
+++ b/packages/react/src/UnderlineNav/UnderlineNav.module.css
@@ -1,22 +1,24 @@
-.MenuItemContent {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
+@layer primer.components.UnderlineNav {
+ .MenuItemContent {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
-/* More button styles migrated from styles.ts (was moreBtnStyles) */
-.MoreButton {
- margin: 0; /* reset Safari extra margin */
- border: 0;
- background: transparent;
- font-weight: var(--base-text-weight-normal);
- box-shadow: none;
- padding-top: var(--base-size-4);
- padding-bottom: var(--base-size-4);
- padding-left: var(--base-size-8);
- padding-right: var(--base-size-8);
+ /* More button styles migrated from styles.ts (was moreBtnStyles) */
+ .MoreButton {
+ margin: 0; /* reset Safari extra margin */
+ border: 0;
+ background: transparent;
+ font-weight: var(--base-text-weight-normal);
+ box-shadow: none;
+ padding-top: var(--base-size-4);
+ padding-bottom: var(--base-size-4);
+ padding-left: var(--base-size-8);
+ padding-right: var(--base-size-8);
- & > [data-component='trailingVisual'] {
- margin-left: 0;
+ & > [data-component='trailingVisual'] {
+ margin-left: 0;
+ }
}
}
diff --git a/packages/react/src/UnderlineNav/UnderlineNavItem.module.css b/packages/react/src/UnderlineNav/UnderlineNavItem.module.css
index c1e75b06204..10d3b641b05 100644
--- a/packages/react/src/UnderlineNav/UnderlineNavItem.module.css
+++ b/packages/react/src/UnderlineNav/UnderlineNavItem.module.css
@@ -1,5 +1,7 @@
-.UnderlineNavItem {
- display: flex;
- flex-direction: column;
- align-items: center;
+@layer primer.components.UnderlineNav {
+ .UnderlineNavItem {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
}
diff --git a/packages/react/src/__tests__/css-layers.test.ts b/packages/react/src/__tests__/css-layers.test.ts
index 3a8b243291a..3350bf34650 100644
--- a/packages/react/src/__tests__/css-layers.test.ts
+++ b/packages/react/src/__tests__/css-layers.test.ts
@@ -1,118 +1,37 @@
import fs from 'node:fs'
import path from 'node:path'
+import {sync as glob} from 'fast-glob'
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'),
- 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'),
- path.resolve(import.meta.dirname, '../Details/Details.module.css'),
- path.resolve(import.meta.dirname, '../experimental/CSSComponent/component.module.css'),
- path.resolve(import.meta.dirname, '../experimental/UnderlinePanels/UnderlinePanels.module.css'),
- path.resolve(import.meta.dirname, '../Flash/Flash.module.css'),
- path.resolve(import.meta.dirname, '../Header/Header.module.css'),
- path.resolve(import.meta.dirname, '../Heading/Heading.module.css'),
- path.resolve(import.meta.dirname, '../Hidden/Hidden.module.css'),
- path.resolve(import.meta.dirname, '../InlineMessage/InlineMessage.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/ButtonReset.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/Caret.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/InputLabel.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/InputValidation.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/TextInputInnerAction.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/TextInputInnerVisualSlot.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/TextInputWrapper.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/UnderlineTabbedInterface.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/UnstyledTextInput.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/ValidationAnimationContainer.module.css'),
- path.resolve(import.meta.dirname, '../Label/Label.module.css'),
- path.resolve(import.meta.dirname, '../Link/Link.module.css'),
- path.resolve(import.meta.dirname, '../Overlay/Overlay.module.css'),
- path.resolve(import.meta.dirname, '../Pagehead/Pagehead.module.css'),
- path.resolve(import.meta.dirname, '../PageHeader/PageHeader.module.css'),
- path.resolve(import.meta.dirname, '../Pagination/Pagination.module.css'),
- path.resolve(import.meta.dirname, '../Placeholder.module.css'),
- path.resolve(import.meta.dirname, '../Popover/Popover.module.css'),
- path.resolve(import.meta.dirname, '../ProgressBar/ProgressBar.module.css'),
- path.resolve(import.meta.dirname, '../Radio/Radio.module.css'),
- path.resolve(import.meta.dirname, '../PageLayout/PageLayout.module.css'),
- path.resolve(import.meta.dirname, '../ScrollableRegion/ScrollableRegion.module.css'),
- path.resolve(import.meta.dirname, '../Select/Select.module.css'),
- path.resolve(import.meta.dirname, '../SideNav.module.css'),
- path.resolve(import.meta.dirname, '../Skeleton/SkeletonBox.module.css'),
- path.resolve(import.meta.dirname, '../SkeletonAvatar/SkeletonAvatar.module.css'),
- path.resolve(import.meta.dirname, '../SkeletonText/SkeletonText.module.css'),
- path.resolve(import.meta.dirname, '../Stack/Stack.module.css'),
- path.resolve(import.meta.dirname, '../StateLabel/StateLabel.module.css'),
- path.resolve(import.meta.dirname, '../SubNav/SubNav.module.css'),
- path.resolve(import.meta.dirname, '../TabNav/TabNav.module.css'),
- path.resolve(import.meta.dirname, '../Text/Text.module.css'),
- path.resolve(import.meta.dirname, '../Timeline/Timeline.module.css'),
- path.resolve(import.meta.dirname, '../Tooltip/Tooltip.module.css'),
- path.resolve(import.meta.dirname, '../TopicTag/TopicTag.module.css'),
- path.resolve(import.meta.dirname, '../TopicTag/TopicTagGroup.module.css'),
- path.resolve(import.meta.dirname, '../Truncate/Truncate.module.css'),
- path.resolve(import.meta.dirname, '../deprecated/ActionList/Divider.module.css'),
- path.resolve(import.meta.dirname, '../deprecated/ActionList/Header.module.css'),
- path.resolve(import.meta.dirname, '../deprecated/ActionList/Item.module.css'),
- path.resolve(import.meta.dirname, '../deprecated/ActionList/List.module.css'),
- path.resolve(import.meta.dirname, '../VisuallyHidden/VisuallyHidden.module.css'),
- path.resolve(import.meta.dirname, '../_VisuallyHidden.module.css'),
- path.resolve(import.meta.dirname, '../CounterLabel/CounterLabel.module.css'),
- path.resolve(import.meta.dirname, '../DataTable/Pagination.module.css'),
- path.resolve(import.meta.dirname, '../DataTable/Table.module.css'),
- path.resolve(import.meta.dirname, '../internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.module.css'),
- path.resolve(import.meta.dirname, '../KeybindingHint/KeybindingHint.module.css'),
- path.resolve(import.meta.dirname, '../KeybindingHint/components/Chord.module.css'),
- path.resolve(import.meta.dirname, '../Spinner/Spinner.module.css'),
- path.resolve(import.meta.dirname, '../Textarea/TextArea.module.css'),
- path.resolve(import.meta.dirname, '../TextInput/TextInput.module.css'),
- path.resolve(import.meta.dirname, '../ToggleSwitch/ToggleSwitch.module.css'),
- path.resolve(import.meta.dirname, '../Token/IssueLabelToken.module.css'),
- path.resolve(import.meta.dirname, '../Token/Token.module.css'),
- path.resolve(import.meta.dirname, '../Token/TokenBase.module.css'),
- path.resolve(import.meta.dirname, '../Token/_RemoveTokenButton.module.css'),
- path.resolve(import.meta.dirname, '../Token/_TokenTextContainer.module.css'),
- path.resolve(import.meta.dirname, '../TextInputWithTokens/TextInputWithTokens.module.css'),
- path.resolve(import.meta.dirname, '../TooltipV2/Tooltip.module.css'),
- path.resolve(import.meta.dirname, '../Button/ButtonBase.module.css'),
- path.resolve(import.meta.dirname, '../ActionList/ActionList.module.css'),
- path.resolve(import.meta.dirname, '../ActionList/Group.module.css'),
- path.resolve(import.meta.dirname, '../ActionList/Heading.module.css'),
- path.resolve(import.meta.dirname, '../AnchoredOverlay/AnchoredOverlay.module.css'),
- path.resolve(import.meta.dirname, '../Autocomplete/AutocompleteMenu.module.css'),
- path.resolve(import.meta.dirname, '../Autocomplete/AutocompleteOverlay.module.css'),
- path.resolve(import.meta.dirname, '../Banner/Banner.module.css'),
- path.resolve(import.meta.dirname, '../Blankslate/Blankslate.module.css'),
- path.resolve(import.meta.dirname, '../Breadcrumbs/Breadcrumbs.module.css'),
- path.resolve(import.meta.dirname, '../deprecated/DialogV1/Dialog.module.css'),
- path.resolve(import.meta.dirname, '../Dialog/Dialog.module.css'),
- path.resolve(import.meta.dirname, '../ActionMenu/ActionMenu.module.css'),
- path.resolve(import.meta.dirname, '../ActionBar/ActionBar.module.css'),
- path.resolve(import.meta.dirname, '../experimental/SelectPanel2/SelectPanel.module.css'),
- path.resolve(import.meta.dirname, '../FilteredActionList/FilteredActionList.module.css'),
- path.resolve(import.meta.dirname, '../FilteredActionList/FilteredActionListLoaders.module.css'),
- path.resolve(import.meta.dirname, '../FormControl/FormControl.module.css'),
- path.resolve(import.meta.dirname, '../FormControl/FormControlCaption.module.css'),
- path.resolve(import.meta.dirname, '../FormControl/FormControlLeadingVisual.module.css'),
- path.resolve(import.meta.dirname, '../SelectPanel/SelectPanel.module.css'),
-])
-const files = Array.from(allowlist).map(file => {
- return [path.relative(path.resolve(import.meta.dirname, '..'), file), file]
-})
-
const CSS_LAYER_REGEX = /^primer\.components\.[A-Z][A-Za-z0-9]+$/
+const SRC_DIR = path.resolve(import.meta.dirname, '..')
+
+const CSS_MODULE_IGNORE_PATTERNS = [
+ '**/*.dev.module.css',
+ '**/*.examples.stories.module.css',
+ '**/*.features.module.css',
+ '**/*.figma.module.css',
+ '**/*.stories.module.css',
+ '**/*.test.module.css',
+ '**/*StoryWrapper.module.css',
+ 'utils/StressTest.module.css',
+]
+
+const files = glob('**/*.module.css', {
+ absolute: true,
+ cwd: SRC_DIR,
+ ignore: CSS_MODULE_IGNORE_PATTERNS,
+}).map(file => {
+ return [path.relative(SRC_DIR, file), file]
+})
describe('CSS Layers', () => {
+ test('discovers migrated CSS Modules', () => {
+ expect(files.length).toBeGreaterThan(0)
+ })
+
describe.each(files)('%s', (_name, filename) => {
const contents = fs.readFileSync(filename, 'utf8')
const ast = parse(contents, {