Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/add-persona-labelled-to-review-board.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Add persona-labeled issues and PRs to Docs Content Review Board

# When the builder persona or driver persona label is added to an issue or PR
# in github/docs, add it to the Docs Content Review Board project.

on:
issues:
types: [labeled]
pull_request_target:
types: [labeled]

permissions:
contents: read

jobs:
add-to-review-board:
name: Add to Docs Content Review Board
runs-on: ubuntu-latest
if: >-
github.repository == 'github/docs' &&
(github.event.label.name == 'builder persona' || github.event.label.name == 'driver persona')
steps:
- name: Add item to project
env:
GITHUB_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }}
ITEM_URL: ${{ github.event.issue.html_url || github.event.pull_request.html_url }}
PROJECT_NUMBER: 2936
run: |
echo "Adding $ITEM_URL to project $PROJECT_NUMBER..."
gh project item-add "$PROJECT_NUMBER" --owner github --url "$ITEM_URL"
3 changes: 3 additions & 0 deletions .github/workflows/moda-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ permissions: {}

jobs:
set-vault-keys:
# Merge queue already built and checked this SHA. Repo-sync and admin merges bypass the queue and still build.
# The actor check only skips a duplicate build and grants no access.
if: ${{ !(github.event_name == 'push' && github.ref == 'refs/heads/main' && github.actor == 'github-merge-queue[bot]') }} # zizmor: ignore[bot-conditions]
permissions: {}
runs-on: ubuntu-latest
outputs:
Expand Down
36 changes: 18 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
"ajv": "^8.18.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^3.0.1",
"chalk": "^5.0.1",
"chalk": "^6.0.1",
"cheerio": "^1.2.0",
"clsx": "^2.1.1",
"cookie-parser": "^1.4.7",
Expand Down
21 changes: 21 additions & 0 deletions src/color-schemes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ The resolved mode comes from the **effective theme**, not the raw `color_mode`,

This is a workaround for a gap in Brand and belongs upstream; until it lands, every consumer has to hand Brand a concrete mode.

### Color contrast requirements

Colors must meet WCAG 2.1 AA in every supported mode. Primer documents the requirements in [ADR-010: Color contrast requirements](https://github.com/primer/primitives/blob/main/contributor-docs/adrs/adr-010-color-contrast.md). The ones that come up most often are:

| Pair | Minimum ratio |
|---|---|
| Text vs. any background it can sit on | 4.5:1 |
| Large text, icons, and other non-text UI vs. background | 3:1 |
| Link vs. surrounding text | 3:1, unless the link is underlined. Links also need 4.5:1 against the background. |
| Focus indicator vs. adjacent colors | 3:1 |
| Border of a control that contains text, or a decorative border | None |

Check both light and dark mode. Brand's base color scales (`--base-color-scale-*`) don't invert in dark mode, so a step that passes in light mode can fail in dark mode.

Also check the no-JavaScript fallback, where `<html>` keeps `data-color-mode="auto"`. Brand stays on its light palette there, so a component that darkens its own background still gets light-mode link blue. Scope fallback selectors to `html`, because the `@primer/react` `ThemeProvider` wrapper also starts as `auto`.

On colored surfaces, such as the `Banner` component in `src/frame/components/ui/Banner/`, we enforce these rules:

* Always underline links. Don't rely on link color to separate links from surrounding text.
* If the link color can't be guaranteed to reach 4.5:1 against the surface, set links to the surface's text color.

## Setup & Usage

To access the current theme in a component:
Expand Down
65 changes: 65 additions & 0 deletions src/frame/components/ui/Banner/Banner.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Colors follow the Docs 2026 Figma warning banner: Brand yellow-9 text with a yellow-7 accent.
// Default and danger reuse the same scale steps in blue and red.
// Brand's base scales do not invert in dark mode, so dark mode mirrors the steps explicitly,
// the same way Brand's own component tokens pair a light step with a dark step.
@mixin banner-colors($hue, $mode) {
@if $mode == light {
--banner-bg: var(--base-color-scale-#{$hue}-0);
--banner-border: var(--base-color-scale-#{$hue}-7);
--banner-fg: var(--base-color-scale-#{$hue}-9);
} @else {
--banner-bg: var(--base-color-scale-#{$hue}-9);
--banner-border: var(--base-color-scale-#{$hue}-2);
--banner-fg: var(--base-color-scale-#{$hue}-0);
}
}

@mixin banner-variant($hue) {
@include banner-colors($hue, light);

:global([data-color-mode="dark"]) & {
@include banner-colors($hue, dark);
}

// No-JS fallback; see the note in src/frame/stylesheets/index.scss. Scoped to html because
// the @primer/react ThemeProvider wrapper also starts as auto, even when html is light.
// Brand keeps its light link blue here, so links use the banner text color for contrast.
@media (prefers-color-scheme: dark) {
:global(html[data-color-mode="auto"][data-dark-theme*="dark"]) & {
@include banner-colors($hue, dark);

a {
color: var(--banner-fg);
}
}
}
}

.banner {
padding: var(--base-size-16, 1rem);
color: var(--banner-fg);
background-color: var(--banner-bg);
border: var(--brand-borderWidth-thin, 1px) solid var(--banner-border);
border-radius: var(--brand-borderRadius-medium, 0.5rem);

p:last-child {
margin-bottom: 0;
}

// Link color alone is not distinct enough from banner text, especially in dark mode.
a {
text-decoration: underline;
}
}

.default {
@include banner-variant(blue);
}

.warning {
@include banner-variant(yellow);
}

.danger {
@include banner-variant(red);
}
15 changes: 15 additions & 0 deletions src/frame/components/ui/Banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ReactNode } from 'react'
import cx from 'clsx'

import styles from './Banner.module.scss'

export type BannerPropsT = {
variant?: 'default' | 'warning' | 'danger'
className?: string
children: ReactNode
}

// No live region role: every banner renders with the page and never updates in place.
export function Banner({ variant = 'default', className, children }: BannerPropsT) {
return <div className={cx(styles.banner, styles[variant], className)}>{children}</div>
}
2 changes: 2 additions & 0 deletions src/frame/components/ui/Banner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Banner } from './Banner'
export type { BannerPropsT } from './Banner'
10 changes: 5 additions & 5 deletions src/rest/components/RestBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react'
import { Flash } from '@primer/react'
import { useRouter } from 'next/router'

import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion'
import { Link } from '@/frame/components/Link'
import { Banner } from '@/frame/components/ui/Banner'
import { useMainContext } from '@/frame/components/context/MainContext'
import { useTranslation } from '@/languages/components/useTranslation'
import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
Expand Down Expand Up @@ -76,7 +76,7 @@ export const RestBanner = () => {
data-testid="rest-api-versioning-temporary-banner"
className="container-xl mt-3 mx-auto p-responsive"
>
<Flash>
<Banner>
<RenderedHTML as="span" html={bannerText} />{' '}
<RenderedHTML
as="span"
Expand All @@ -85,7 +85,7 @@ export const RestBanner = () => {
versionWithApiVersion === DEFAULT_VERSION ? '' : `/${versionWithApiVersion}`,
)}
/>
</Flash>
</Banner>
</div>
)
}
Expand Down Expand Up @@ -130,15 +130,15 @@ export const RestBanner = () => {

return (
<div data-testid="rest-api-repos-banner" className="container-xl mt-3 mx-auto p-responsive">
<Flash variant="warning">
<Banner variant="warning">
<p>
<b className="text-bold">
<span>
{t('rest.banner.redirect_notice')} {noticeString}
</span>
</b>{' '}
</p>
</Flash>
</Banner>
</div>
)
}
2 changes: 1 addition & 1 deletion src/search/components/results/NoQuery.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
line-height: 1.2;
}

.flash {
.banner {
margin: 2rem;
}
8 changes: 3 additions & 5 deletions src/search/components/results/NoQuery.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Flash } from '@primer/react'
import { Heading } from '@primer/react-brand'

import { useMainContext } from '@/frame/components/context/MainContext'
import { Banner } from '@/frame/components/ui/Banner'
import { useTranslation } from '@/languages/components/useTranslation'

import styles from './NoQuery.module.scss'

// NoQuery keeps the callout on Primer React because Brand lacks Flash, Banner, or Alert.
// The Docs 2026 callout system is the planned replacement.
export function NoQuery() {
const { t } = useTranslation('old_search')
const mainContext = useMainContext()
Expand All @@ -20,9 +18,9 @@ export function NoQuery() {
{page.title}
</Heading>

<Flash variant="danger" className={styles.flash}>
<Banner variant="danger" className={styles.banner}>
{t('description')}
</Flash>
</Banner>
</>
)
}
2 changes: 1 addition & 1 deletion src/search/components/results/ValidationErrors.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.flash {
.banner {
margin: 3rem;
}
7 changes: 3 additions & 4 deletions src/search/components/results/ValidationErrors.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Flash } from '@primer/react'

import { Banner } from '@/frame/components/ui/Banner'
import { useTranslation } from '@/languages/components/useTranslation'
import type { SearchValidationErrorEntry } from '../../types'

Expand All @@ -16,11 +15,11 @@ export function ValidationErrors({ errors }: Props) {
<div>
{errors.map((error) => {
return (
<Flash key={error.error} variant="warning" className={styles.flash}>
<Banner key={error.error} variant="warning" className={styles.banner}>
{t('search_validation_error')}
<br />
<code>{error.error}</code>
</Flash>
</Banner>
)
})}
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/versions/components/DeprecationBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { EnterpriseDeprecation } from '@/frame/components/context/MainContext'
import { useMainContext } from '@/frame/components/context/MainContext'
import { useVersion } from '@/versions/components/useVersion'
import { Flash } from '@primer/react'
import cx from 'clsx'

import styles from './DeprecationBanner.module.scss'
import { Banner } from '@/frame/components/ui/Banner'
import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'

export const DeprecationBanner = () => {
Expand All @@ -27,7 +27,7 @@ export const DeprecationBanner = () => {
data-testid="deprecation-banner"
className={cx('container-xl mt-3 mx-auto p-responsive', styles.DeprecationBanner)}
>
<Flash variant="warning">
<Banner variant="warning">
<p>
<b className="text-bold">
<RenderedHTML as="span" html={message} />{' '}
Expand All @@ -38,7 +38,7 @@ export const DeprecationBanner = () => {
</b>{' '}
<RenderedHTML as="span" html={enterpriseDeprecation.deprecation_details} />
</p>
</Flash>
</Banner>
</div>
)
}
7 changes: 3 additions & 4 deletions src/webhooks/components/Webhook.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { ActionMenu } from '@primer/react-brand'
import { onActionMenuItemKeyDownCapture } from '@/frame/components/lib/action-menu'
// Webhook keeps the error callout on Primer React because Brand lacks Flash, Banner, or Alert.
import { Flash } from '@primer/react'
import { useState, useEffect, useCallback } from 'react'
import useSWR from 'swr'
import { slug } from 'github-slugger'
Expand All @@ -10,6 +8,7 @@ import { announce } from '@primer/live-region-element'

import { useVersion } from '@/versions/components/useVersion'
import { HeadingLink } from '@/frame/components/article/HeadingLink'
import { Banner } from '@/frame/components/ui/Banner'
import { useTranslation } from '@/languages/components/useTranslation'
import type { WebhookAction, WebhookData } from './types'
import { ParameterTable } from '@/automated-pipelines/components/parameter-table/ParameterTable'
Expand Down Expand Up @@ -156,12 +155,12 @@ export function Webhook({ webhook }: Props) {
)}
/>
{error && (
<Flash className="mb-5" variant="danger">
<Banner className="mb-5" variant="danger">
<p>{t('action_type_switch_error')}</p>
<p>
<code className={`f6 ${styles.errorCode}`}>{error.toString()}</code>
</p>
</Flash>
</Banner>
)}
{webhook.actionTypes.length > 1 && (
<div className="mb-4">
Expand Down
Loading