diff --git a/src/frame/components/article/ArticleGridLayout.module.scss b/src/frame/components/article/ArticleGridLayout.module.scss index eedf104a13bf..7e78b0b8ac78 100644 --- a/src/frame/components/article/ArticleGridLayout.module.scss +++ b/src/frame/components/article/ArticleGridLayout.module.scss @@ -122,6 +122,17 @@ pointer-events: none; } +// ...unless the header box ends in a tool/platform picker. Primer's UnderlineNav +// draws its own closing rule as an inset box-shadow, so drop ours to avoid a +// double rule; the tab rule must stay under the selected-tab indicator. Match +// both pickers by name: `$="-picker"` would also match the version picker. +.heroIntro:has( + [data-testid="tool-picker"], + [data-testid="platform-picker"] + )::after { + content: none; +} + // With xxl and a collapsed left rail, the freed 326px allows the drawer layout // at about 1074px. Use one column below 1074px and two columns at or above it. .containerBoxXxlCollapsed { diff --git a/src/frame/components/ui/MarkdownContent/MarkdownContent.module.scss b/src/frame/components/ui/MarkdownContent/MarkdownContent.module.scss index ab35b376610f..92eddab8feb2 100644 --- a/src/frame/components/ui/MarkdownContent/MarkdownContent.module.scss +++ b/src/frame/components/ui/MarkdownContent/MarkdownContent.module.scss @@ -3,6 +3,7 @@ @import "./stylesheets/lists.scss"; @import "./stylesheets/table.scss"; @import "./stylesheets/images.scss"; +@import "src/frame/stylesheets/underline-nav-brand.scss"; .markdownBody { // Article body prose uses Mona Sans from @primer/react-brand's body font var. @@ -51,13 +52,31 @@ } :global(.ghd-codetabs-nav) { + // Docs 2026: the code-language tabs are a second @primer/react UnderlineNav. + // Without this they kept primitives' raw defaults — a #fd8c73 active bar, + // 2.29:1 in light mode, under the WCAG 1.4.11 3:1 floor — and sat next to + // the brand-green tool picker on the same article. Same mixin, same tokens; + // see underline-nav-brand.scss. + @include underline-nav-brand-tokens; + margin-bottom: 0; :global(ul) { padding-left: 0 !important; } + // These are anchors inside the article body, so article-link-overrides.scss + // paints them brand link-blue at (1,4,1) — which beats both PRC's + // `.prc-components-UnderlineItem` (0,1,0) and anything we can write here + // without an id. Tab labels are controls, not prose links: left alone they + // read blue while the tool picker's identical tabs read text-default, and + // the mixin's --fgColor-default never lands. `!important` for the same + // cascade reason as the text-decoration rule beside it. :global(a) { + color: var( + --brand-color-text-default, + var(--color-fg-default) + ) !important; text-decoration: none !important; } } diff --git a/src/frame/stylesheets/index.scss b/src/frame/stylesheets/index.scss index 50bc17542340..38721618aeb0 100644 --- a/src/frame/stylesheets/index.scss +++ b/src/frame/stylesheets/index.scss @@ -90,6 +90,11 @@ html[data-color-mode] body { // light fill (#e4ebe6) while the digit is already patched to white by the // line above — 1.2:1, i.e. invisible. --brand-color-border-muted: #191f1b; + + // Used by both UnderlineNav tab strips (underline-nav-brand.scss) for the + // hover fill and selected-tab bar. Literal hex for the same reason as above. + --brand-color-canvas-subtle: #0f1511; + --brand-NavList-activeIndicator-color: #5fed83; } } diff --git a/src/frame/stylesheets/underline-nav-brand.scss b/src/frame/stylesheets/underline-nav-brand.scss new file mode 100644 index 000000000000..46727a770ad4 --- /dev/null +++ b/src/frame/stylesheets/underline-nav-brand.scss @@ -0,0 +1,112 @@ +// Docs 2026: Primer Brand tokens for @primer/react's UnderlineNav. +// +// Brand ships no UnderlineNav, so the two places we render one are RETOKENED +// rather than replaced — the in-article tool/platform picker +// (InArticlePicker.module.scss) and the code-language tabs +// (MarkdownContent.module.scss, .ghd-codetabs-nav). Include this mixin on the +// wrapper of any new one so we never reintroduce a Primer-coloured tab strip. +// +// UnderlineNav paints entirely from plain custom properties read off the +// element, so everything here is a declaration that inherits into PRC's hashed +// subtree — we never name a `prc-*` class, which carries a build hash. Source +// of truth for every property: +// node_modules/@primer/react/dist/internal/components/UnderlineTabbedInterface-*.css +// +// These override the MODERN Primer names, not the legacy `--color-*` ones, and +// that distinction is load-bearing: this app imports @primer/primitives' +// functional themes (index.scss:19-22), so `--borderColor-*` / `--fgColor-*` / +// `--overlay-bgColor` all resolve on PRC's FIRST leg and its `var(--color-*)` +// fallbacks never fire. Overriding only the legacy spelling would silently do +// nothing. The legacy leg is kept as a real safety net if brand CSS fails to load. +@mixin underline-nav-brand-tokens { + // Active indicator — the 2px bar under the selected tab. + // + // WCAG 1.4.11 non-text contrast (3:1) makes this load-bearing: primitives' + // own value is #fd8c73 light / #f78166 dark, and the light one is 2.29:1 on + // the article canvas. It replaces a --color-severe-emphasis orange with the + // token brand's own nav surfaces use, so selected tab, current article + // (doc-tree sidebar) and current section (mini-TOC) all agree. + // + // vs the article canvas (--brand-color-canvas-default): 4.65:1 light + // (#08872b on #ffffff) and 13.93:1 dark (#5fed83 on #000000). The dark value + // only lands because index.scss patches this token for `auto` — measured, a + // visitor with no colour-mode cookie keeps `auto` on even with JS on, + // and brand's palette then resolves light. Without that patch it would be + // brand's light green on a black canvas, which is still 4.51:1. All three + // clear 3:1, so the 1.4.11 protection holds in every state. + --underlineNav-borderColor-active: var( + --brand-NavList-activeIndicator-color, + var(--color-severe-emphasis) + ); + + // The strip's closing rule. PRC draws it as `box-shadow: inset 0 -1px`, not a + // border, so a border scan won't find it. On the picker this is also the + // article header box's closing rule — ArticleGridLayout.module.scss drops + // `.heroIntro::after` in its favour, and that ::after painted + // --brand-color-border-muted, so the box keeps closing on the same value. + // Decorative separator, not a state indicator: 3:1 does not apply. + --borderColor-muted: var( + --brand-color-border-muted, + var(--color-border-muted) + ); + + // Label colour, rest AND selected — PRC never recolours the selected label, it + // only bumps its weight and paints the bar. 21:1 in both modes. + --fgColor-default: var(--brand-color-text-default, var(--color-fg-default)); + + // Leading icons and counters — but not dead where they are unused: it also + // cascades into the overflow menu's secondary text. 6.27:1 / 9.18:1. + --fgColor-muted: var(--brand-color-text-muted, var(--color-fg-muted)); + + // Tab hover fill. Primer's is a blue-tinted alpha wash, which would leave the + // strip the one grey in the article with no green in it. Label stays >18:1. + --bgColor-neutral-muted: var( + --brand-color-canvas-subtle, + var(--color-neutral-subtle) + ); + + // Focus ring only — in UnderlineNav this property appears solely in + // `:focus-visible { box-shadow: inset 0 0 0 2px … }`. On the picker it is on + // screen for every keyboard selection, because InArticlePicker.tsx moves focus + // to the newly selected tab (WCAG 2.4.3). Brand's focus blue keeps "focused" + // visually distinct from "selected" green. 4.13:1 light / 6.81:1 dark. + --fgColor-accent: var(--brand-color-focus, var(--color-accent-fg)); + + // Overflow menu surface. UnderlineNav applies `background: var(--overlay-bgColor)` + // as an INLINE style, so this property is the only way in. Also fixes a real + // defect: primitives' dark overlay (#010409) was tuned to lift off Primer's + // #0d1117 canvas and sits at ~1:1 on the brand true-black one. + --overlay-bgColor: var( + --brand-color-canvas-overlay, + var(--color-canvas-overlay) + ); + // Inert today: PRC sets the LoadingCounter's border-color but never its + // border-width or border-style, so nothing renders. Declared so no + // Primer-resolved colour survives in the component's computed tree if a + // consumer ever passes counters. + --borderColor-default: var( + --brand-color-border-default, + var(--color-border-default) + ); + + // Type and geometry. Two are pure renames — brand's text-size-100 (0.875rem) + // and stack-padding-condensed (1rem) are byte-identical to PRC's literals, so + // they only matter if brand retunes its scale. Three are real (small) changes: + // lineHeight 1.4285 -> 1.5, semibold 600 -> 525 (brand tunes Mona Sans' + // variable weight axis there), and the hover fill's corner radius + // 0.1875rem -> 0.25rem, brand's nearest step. The visible selected label and + // the hidden `[data-content]::before` that reserves its width read the SAME + // weight property, so the no-reflow trick still holds. + --text-body-size-medium: var(--brand-text-size-100, 0.875rem); + --text-body-lineHeight-medium: var(--brand-text-lineHeight-100, 1.4285); + --base-text-weight-semibold: var(--brand-text-weight-semibold, 600); + --borderRadius-medium: var(--brand-borderRadius-small, 0.1875rem); + --stack-padding-normal: var(--brand-stack-padding-condensed, 1rem); + + // PRC reads this one twice with mismatched fallbacks: `min-height: var(…, 48px)` + // (absolute) but the indicator's `bottom: calc(… var(…, var(--base-size-48, 3rem)) …)` + // (root-relative, and --base-size-48 IS defined, by brand). They agree only at a + // 16px root, so a reader who enlarges browser text (WCAG 1.4.4) detaches the bar + // from the rule. Defining the first link keeps both chains together at any root. + --control-xlarge-size: var(--base-size-48, 3rem); +} diff --git a/src/tools/components/InArticlePicker.module.scss b/src/tools/components/InArticlePicker.module.scss index 072817233a7b..d909609a6d2e 100644 --- a/src/tools/components/InArticlePicker.module.scss +++ b/src/tools/components/InArticlePicker.module.scss @@ -1,13 +1,15 @@ +@import "src/frame/stylesheets/underline-nav-brand.scss"; + +// Shares the Brand token block with the code-language tabs; see +// underline-nav-brand.scss for each property and its contrast numbers. .container { - // Primer's --color-primer-border-active has 2.3:1 contrast and fails WCAG 1.4.11. - // --color-severe-emphasis meets the 3:1 minimum in light and dark modes. - --underlineNav-borderColor-active: var(--color-severe-emphasis); + @include underline-nav-brand-tokens; // Primer does not expose a stable class for the UnderlineNav overflow ActionList. ul[class*="prc-ActionList-ActionList"] { background-color: var( --overlay-bgColor, - var(--color-canvas-overlay, #ffffff) + var(--color-canvas-overlay) ) !important; } }