diff --git a/.github/workflows/add-persona-labelled-to-review-board.yml b/.github/workflows/add-persona-labelled-to-review-board.yml new file mode 100644 index 000000000000..27bc845f4ec6 --- /dev/null +++ b/.github/workflows/add-persona-labelled-to-review-board.yml @@ -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" diff --git a/.github/workflows/moda-ci.yaml b/.github/workflows/moda-ci.yaml index c21b934fea03..21cee497a072 100644 --- a/.github/workflows/moda-ci.yaml +++ b/.github/workflows/moda-ci.yaml @@ -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: diff --git a/package-lock.json b/package-lock.json index 3e88d33ba6ec..19c42b66fa87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,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", @@ -5983,19 +5983,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/boxen/node_modules/chalk": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.0.tgz", - "integrity": "sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/boxen/node_modules/is-fullwidth-code-point": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", @@ -6350,12 +6337,12 @@ } }, "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.1.tgz", + "integrity": "sha512-/Ce6KNm3vIbWdMlNna6RVIZ/ICQxnJxCicet5LBKK9ZffBkqzDw0xh9EiKSljdRtiIQ1S1z4YgcscUUGzNCWrA==", "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=22" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -12580,6 +12567,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/ora/node_modules/string-width": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", diff --git a/package.json b/package.json index 0b8c05944f70..8fdd4b54d0f7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/color-schemes/README.md b/src/color-schemes/README.md index a59079125500..6ecab00451a7 100644 --- a/src/color-schemes/README.md +++ b/src/color-schemes/README.md @@ -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 `` 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: diff --git a/src/frame/components/ui/Banner/Banner.module.scss b/src/frame/components/ui/Banner/Banner.module.scss new file mode 100644 index 000000000000..ee8617c5dbbd --- /dev/null +++ b/src/frame/components/ui/Banner/Banner.module.scss @@ -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); +} diff --git a/src/frame/components/ui/Banner/Banner.tsx b/src/frame/components/ui/Banner/Banner.tsx new file mode 100644 index 000000000000..c886e9304e96 --- /dev/null +++ b/src/frame/components/ui/Banner/Banner.tsx @@ -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
{children}
+} diff --git a/src/frame/components/ui/Banner/index.ts b/src/frame/components/ui/Banner/index.ts new file mode 100644 index 000000000000..efb8197e7e31 --- /dev/null +++ b/src/frame/components/ui/Banner/index.ts @@ -0,0 +1,2 @@ +export { Banner } from './Banner' +export type { BannerPropsT } from './Banner' diff --git a/src/rest/components/RestBanner.tsx b/src/rest/components/RestBanner.tsx index 5033925b58b6..868be014cbb2 100644 --- a/src/rest/components/RestBanner.tsx +++ b/src/rest/components/RestBanner.tsx @@ -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' @@ -76,7 +76,7 @@ export const RestBanner = () => { data-testid="rest-api-versioning-temporary-banner" className="container-xl mt-3 mx-auto p-responsive" > - + {' '} { versionWithApiVersion === DEFAULT_VERSION ? '' : `/${versionWithApiVersion}`, )} /> - + ) } @@ -130,7 +130,7 @@ export const RestBanner = () => { return (
- +

@@ -138,7 +138,7 @@ export const RestBanner = () => { {' '}

-
+
) } diff --git a/src/search/components/results/NoQuery.module.scss b/src/search/components/results/NoQuery.module.scss index ad52f6406bb4..7faf99c0e394 100644 --- a/src/search/components/results/NoQuery.module.scss +++ b/src/search/components/results/NoQuery.module.scss @@ -5,6 +5,6 @@ line-height: 1.2; } -.flash { +.banner { margin: 2rem; } diff --git a/src/search/components/results/NoQuery.tsx b/src/search/components/results/NoQuery.tsx index 14189f89b467..e68d7c850111 100644 --- a/src/search/components/results/NoQuery.tsx +++ b/src/search/components/results/NoQuery.tsx @@ -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() @@ -20,9 +18,9 @@ export function NoQuery() { {page.title} - + {t('description')} - + ) } diff --git a/src/search/components/results/ValidationErrors.module.scss b/src/search/components/results/ValidationErrors.module.scss index 81e38518f870..a1edc1bb66d7 100644 --- a/src/search/components/results/ValidationErrors.module.scss +++ b/src/search/components/results/ValidationErrors.module.scss @@ -1,3 +1,3 @@ -.flash { +.banner { margin: 3rem; } diff --git a/src/search/components/results/ValidationErrors.tsx b/src/search/components/results/ValidationErrors.tsx index a9d0e54ceea0..733a493d71f1 100644 --- a/src/search/components/results/ValidationErrors.tsx +++ b/src/search/components/results/ValidationErrors.tsx @@ -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' @@ -16,11 +15,11 @@ export function ValidationErrors({ errors }: Props) {
{errors.map((error) => { return ( - + {t('search_validation_error')}
{error.error} -
+ ) })}
diff --git a/src/versions/components/DeprecationBanner.tsx b/src/versions/components/DeprecationBanner.tsx index 6062966c591d..ad55b1276092 100644 --- a/src/versions/components/DeprecationBanner.tsx +++ b/src/versions/components/DeprecationBanner.tsx @@ -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 = () => { @@ -27,7 +27,7 @@ export const DeprecationBanner = () => { data-testid="deprecation-banner" className={cx('container-xl mt-3 mx-auto p-responsive', styles.DeprecationBanner)} > - +

{' '} @@ -38,7 +38,7 @@ export const DeprecationBanner = () => { {' '}

-
+ ) } diff --git a/src/webhooks/components/Webhook.tsx b/src/webhooks/components/Webhook.tsx index 267e5d32d67f..67ad299b1832 100644 --- a/src/webhooks/components/Webhook.tsx +++ b/src/webhooks/components/Webhook.tsx @@ -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' @@ -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' @@ -156,12 +155,12 @@ export function Webhook({ webhook }: Props) { )} /> {error && ( - +

{t('action_type_switch_error')}

{error.toString()}

-
+ )} {webhook.actionTypes.length > 1 && (