|
| 1 | +// Docs 2026: Primer Brand tokens for @primer/react's UnderlineNav. |
| 2 | +// |
| 3 | +// Brand ships no UnderlineNav, so the two places we render one are RETOKENED |
| 4 | +// rather than replaced — the in-article tool/platform picker |
| 5 | +// (InArticlePicker.module.scss) and the code-language tabs |
| 6 | +// (MarkdownContent.module.scss, .ghd-codetabs-nav). Include this mixin on the |
| 7 | +// wrapper of any new one so we never reintroduce a Primer-coloured tab strip. |
| 8 | +// |
| 9 | +// UnderlineNav paints entirely from plain custom properties read off the |
| 10 | +// element, so everything here is a declaration that inherits into PRC's hashed |
| 11 | +// subtree — we never name a `prc-*` class, which carries a build hash. Source |
| 12 | +// of truth for every property: |
| 13 | +// node_modules/@primer/react/dist/internal/components/UnderlineTabbedInterface-*.css |
| 14 | +// |
| 15 | +// These override the MODERN Primer names, not the legacy `--color-*` ones, and |
| 16 | +// that distinction is load-bearing: this app imports @primer/primitives' |
| 17 | +// functional themes (index.scss:19-22), so `--borderColor-*` / `--fgColor-*` / |
| 18 | +// `--overlay-bgColor` all resolve on PRC's FIRST leg and its `var(--color-*)` |
| 19 | +// fallbacks never fire. Overriding only the legacy spelling would silently do |
| 20 | +// nothing. The legacy leg is kept as a real safety net if brand CSS fails to load. |
| 21 | +@mixin underline-nav-brand-tokens { |
| 22 | + // Active indicator — the 2px bar under the selected tab. |
| 23 | + // |
| 24 | + // WCAG 1.4.11 non-text contrast (3:1) makes this load-bearing: primitives' |
| 25 | + // own value is #fd8c73 light / #f78166 dark, and the light one is 2.29:1 on |
| 26 | + // the article canvas. It replaces a --color-severe-emphasis orange with the |
| 27 | + // token brand's own nav surfaces use, so selected tab, current article |
| 28 | + // (doc-tree sidebar) and current section (mini-TOC) all agree. |
| 29 | + // |
| 30 | + // vs the article canvas (--brand-color-canvas-default): 4.65:1 light |
| 31 | + // (#08872b on #ffffff) and 13.93:1 dark (#5fed83 on #000000). The dark value |
| 32 | + // only lands because index.scss patches this token for `auto` — measured, a |
| 33 | + // visitor with no colour-mode cookie keeps `auto` on <html> even with JS on, |
| 34 | + // and brand's palette then resolves light. Without that patch it would be |
| 35 | + // brand's light green on a black canvas, which is still 4.51:1. All three |
| 36 | + // clear 3:1, so the 1.4.11 protection holds in every state. |
| 37 | + --underlineNav-borderColor-active: var( |
| 38 | + --brand-NavList-activeIndicator-color, |
| 39 | + var(--color-severe-emphasis) |
| 40 | + ); |
| 41 | + |
| 42 | + // The strip's closing rule. PRC draws it as `box-shadow: inset 0 -1px`, not a |
| 43 | + // border, so a border scan won't find it. On the picker this is also the |
| 44 | + // article header box's closing rule — ArticleGridLayout.module.scss drops |
| 45 | + // `.heroIntro::after` in its favour, and that ::after painted |
| 46 | + // --brand-color-border-muted, so the box keeps closing on the same value. |
| 47 | + // Decorative separator, not a state indicator: 3:1 does not apply. |
| 48 | + --borderColor-muted: var( |
| 49 | + --brand-color-border-muted, |
| 50 | + var(--color-border-muted) |
| 51 | + ); |
| 52 | + |
| 53 | + // Label colour, rest AND selected — PRC never recolours the selected label, it |
| 54 | + // only bumps its weight and paints the bar. 21:1 in both modes. |
| 55 | + --fgColor-default: var(--brand-color-text-default, var(--color-fg-default)); |
| 56 | + |
| 57 | + // Leading icons and counters — but not dead where they are unused: it also |
| 58 | + // cascades into the overflow menu's secondary text. 6.27:1 / 9.18:1. |
| 59 | + --fgColor-muted: var(--brand-color-text-muted, var(--color-fg-muted)); |
| 60 | + |
| 61 | + // Tab hover fill. Primer's is a blue-tinted alpha wash, which would leave the |
| 62 | + // strip the one grey in the article with no green in it. Label stays >18:1. |
| 63 | + --bgColor-neutral-muted: var( |
| 64 | + --brand-color-canvas-subtle, |
| 65 | + var(--color-neutral-subtle) |
| 66 | + ); |
| 67 | + |
| 68 | + // Focus ring only — in UnderlineNav this property appears solely in |
| 69 | + // `:focus-visible { box-shadow: inset 0 0 0 2px … }`. On the picker it is on |
| 70 | + // screen for every keyboard selection, because InArticlePicker.tsx moves focus |
| 71 | + // to the newly selected tab (WCAG 2.4.3). Brand's focus blue keeps "focused" |
| 72 | + // visually distinct from "selected" green. 4.13:1 light / 6.81:1 dark. |
| 73 | + --fgColor-accent: var(--brand-color-focus, var(--color-accent-fg)); |
| 74 | + |
| 75 | + // Overflow menu surface. UnderlineNav applies `background: var(--overlay-bgColor)` |
| 76 | + // as an INLINE style, so this property is the only way in. Also fixes a real |
| 77 | + // defect: primitives' dark overlay (#010409) was tuned to lift off Primer's |
| 78 | + // #0d1117 canvas and sits at ~1:1 on the brand true-black one. |
| 79 | + --overlay-bgColor: var( |
| 80 | + --brand-color-canvas-overlay, |
| 81 | + var(--color-canvas-overlay) |
| 82 | + ); |
| 83 | + // Inert today: PRC sets the LoadingCounter's border-color but never its |
| 84 | + // border-width or border-style, so nothing renders. Declared so no |
| 85 | + // Primer-resolved colour survives in the component's computed tree if a |
| 86 | + // consumer ever passes counters. |
| 87 | + --borderColor-default: var( |
| 88 | + --brand-color-border-default, |
| 89 | + var(--color-border-default) |
| 90 | + ); |
| 91 | + |
| 92 | + // Type and geometry. Two are pure renames — brand's text-size-100 (0.875rem) |
| 93 | + // and stack-padding-condensed (1rem) are byte-identical to PRC's literals, so |
| 94 | + // they only matter if brand retunes its scale. Three are real (small) changes: |
| 95 | + // lineHeight 1.4285 -> 1.5, semibold 600 -> 525 (brand tunes Mona Sans' |
| 96 | + // variable weight axis there), and the hover fill's corner radius |
| 97 | + // 0.1875rem -> 0.25rem, brand's nearest step. The visible selected label and |
| 98 | + // the hidden `[data-content]::before` that reserves its width read the SAME |
| 99 | + // weight property, so the no-reflow trick still holds. |
| 100 | + --text-body-size-medium: var(--brand-text-size-100, 0.875rem); |
| 101 | + --text-body-lineHeight-medium: var(--brand-text-lineHeight-100, 1.4285); |
| 102 | + --base-text-weight-semibold: var(--brand-text-weight-semibold, 600); |
| 103 | + --borderRadius-medium: var(--brand-borderRadius-small, 0.1875rem); |
| 104 | + --stack-padding-normal: var(--brand-stack-padding-condensed, 1rem); |
| 105 | + |
| 106 | + // PRC reads this one twice with mismatched fallbacks: `min-height: var(…, 48px)` |
| 107 | + // (absolute) but the indicator's `bottom: calc(… var(…, var(--base-size-48, 3rem)) …)` |
| 108 | + // (root-relative, and --base-size-48 IS defined, by brand). They agree only at a |
| 109 | + // 16px root, so a reader who enlarges browser text (WCAG 1.4.4) detaches the bar |
| 110 | + // from the rule. Defining the first link keeps both chains together at any root. |
| 111 | + --control-xlarge-size: var(--base-size-48, 3rem); |
| 112 | +} |
0 commit comments