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
@@ -138,7 +138,7 @@ export const RestBanner = () => { {' '}
-{error.error}
-
{t('action_type_switch_error')}
{error.toString()}