diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 50571f1549..6ea117e044 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -82,6 +82,12 @@ apps/
npm run test # Unit tests
```
+### Docs Figma images
+
+Files in `apps/next-docs/public/images/figma` are generated artifacts. Never edit, optimize, recompress, rename, replace, or delete these images or their manifest directly.
+
+Update Figma references in the docs source, then regenerate the artifacts with `npm run figma-images:generate --workspace=apps/next-docs` or use the `update figma images` pull request label workflow. Commit only the output produced by the generator or workflow.
+
### Component development
- **Location**: `packages/react/src/ComponentName/`
diff --git a/.github/workflows/update_figma_images.yml b/.github/workflows/update_figma_images.yml
new file mode 100644
index 0000000000..01f13dfeec
--- /dev/null
+++ b/.github/workflows/update_figma_images.yml
@@ -0,0 +1,140 @@
+name: Update docs Figma images
+
+on:
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-main
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ generate:
+ if: ${{ github.repository == 'primer/brand' }}
+ name: Generate image updates
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ has_changes: ${{ steps.capture.outputs.has_changes }}
+ source_sha: ${{ steps.source-sha.outputs.sha }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ ref: main
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Capture source SHA
+ id: source-sha
+ run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+
+ - name: Set up Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24.14.1
+
+ - name: Cache dependencies
+ uses: actions/cache@v5
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Generate Figma images
+ run: npm run figma-images:generate --workspace=apps/next-docs
+ env:
+ FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }}
+
+ - name: Capture generated image updates
+ id: capture
+ run: |
+ if [ -z "$(git status --short -- apps/next-docs/public/images/figma)" ]; then
+ echo 'has_changes=false' >> "$GITHUB_OUTPUT"
+ echo 'No docs Figma image changes were generated.'
+ exit 0
+ fi
+
+ artifact_directory='workflow-artifacts/next-docs-figma-images'
+ mkdir -p "$artifact_directory"
+ tar -cf "$artifact_directory/generated-images.tar" apps/next-docs/public/images/figma
+
+ echo 'has_changes=true' >> "$GITHUB_OUTPUT"
+ git status --short -- apps/next-docs/public/images/figma
+
+ - name: Upload generated image updates
+ if: steps.capture.outputs.has_changes == 'true'
+ uses: actions/upload-artifact@v7
+ with:
+ name: next-docs-figma-image-updates
+ path: workflow-artifacts/next-docs-figma-images/generated-images.tar
+ retention-days: 7
+
+ create-pr:
+ if: ${{ github.repository == 'primer/brand' && needs.generate.outputs.has_changes == 'true' }}
+ name: Create draft update PR
+ needs: generate
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ needs.generate.outputs.source_sha }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Download generated image updates
+ uses: actions/download-artifact@v5
+ with:
+ name: next-docs-figma-image-updates
+ path: workflow-artifacts/next-docs-figma-images
+
+ - name: Apply generated image updates
+ run: |
+ rm -rf apps/next-docs/public/images/figma
+ tar -xf workflow-artifacts/next-docs-figma-images/generated-images.tar
+ git status --short -- apps/next-docs/public/images/figma
+
+ - name: Create GitHub App token
+ id: app-token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
+ with:
+ app-id: ${{ vars.PRIMER_APP_ID_SHARED }}
+ private-key: ${{ secrets.PRIMER_APP_PRIVATE_KEY_SHARED }}
+
+ - name: Create draft pull request
+ id: create-pull-request
+ # Uses SHA for security hardening
+ uses: peter-evans/create-pull-request@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ token: ${{ steps.app-token.outputs.token }}
+ add-paths: |
+ apps/next-docs/public/images/figma
+ author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
+ committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
+ branch: github-actions/update-next-docs-figma-images
+ base: main
+ delete-branch: true
+ commit-message: Update docs Figma images
+ title: Update docs Figma images
+ body: |
+ ## Summary
+ - refresh generated Figma images for `apps/next-docs`
+ - apply additions, modifications, and deletions captured by the read-only generation job
+
+ ## Testing
+ - `npm run figma-images:generate --workspace=apps/next-docs`
+ draft: always-true
+
+ - name: Report pull request
+ if: steps.create-pull-request.outputs.pull-request-url != ''
+ run: echo "Created or updated ${{ steps.create-pull-request.outputs.pull-request-url }}"
diff --git a/.github/workflows/update_figma_images_on_label.yml b/.github/workflows/update_figma_images_on_label.yml
new file mode 100644
index 0000000000..26f16e53e3
--- /dev/null
+++ b/.github/workflows/update_figma_images_on_label.yml
@@ -0,0 +1,211 @@
+name: Update pull request Figma images
+
+on:
+ pull_request_target:
+ types: [labeled]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: read
+
+jobs:
+ update-figma-images:
+ if: ${{ github.event.label.name == 'update figma images' }}
+ name: Update Figma images
+ runs-on: ubuntu-latest
+ steps:
+ - name: Verify update request is still valid
+ id: request-gate
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const labelName = 'update figma images'
+ const {owner, repo} = context.repo
+ const pull_number = context.payload.pull_request.number
+
+ const {data: pullRequest} = await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number,
+ })
+
+ const hasLabel = pullRequest.labels.some((label) => label.name === labelName)
+ const isRepositoryBranch = pullRequest.head.repo.full_name === `${owner}/${repo}`
+ const shouldProcess = pullRequest.state === 'open' && hasLabel && isRepositoryBranch
+ const reason = !hasLabel ? 'label-removed' : pullRequest.state !== 'open' ? 'closed' : !isRepositoryBranch ? 'fork' : 'authorized'
+
+ core.setOutput('base_sha', pullRequest.base.sha)
+ core.setOutput('head_ref', pullRequest.head.ref)
+ core.setOutput('head_sha', pullRequest.head.sha)
+ core.setOutput('head_repo_full_name', pullRequest.head.repo.full_name)
+ core.setOutput('reason', reason)
+ core.setOutput('should_cleanup', hasLabel ? 'true' : 'false')
+ core.setOutput('should_process', shouldProcess ? 'true' : 'false')
+
+ if (shouldProcess) {
+ core.info(`Figma image update is authorized for PR #${pull_number}`)
+ return
+ }
+
+ if (reason === 'fork') {
+ core.info('Skipping because pull requests from forks cannot use repository secrets or push commits back to the branch.')
+ return
+ }
+
+ core.info(`Skipping because PR #${pull_number} is ${pullRequest.state} or missing the \"${labelName}\" label.`)
+
+ - name: Checkout trusted base revision
+ if: steps.request-gate.outputs.should_process == 'true'
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ steps.request-gate.outputs.base_sha }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Fetch pull request head revision
+ if: steps.request-gate.outputs.should_process == 'true'
+ env:
+ HEAD_SHA: ${{ steps.request-gate.outputs.head_sha }}
+ run: git fetch --no-tags origin "$HEAD_SHA" --depth=1
+
+ - name: Overlay pull request content as inert input
+ if: steps.request-gate.outputs.should_process == 'true'
+ env:
+ HEAD_SHA: ${{ steps.request-gate.outputs.head_sha }}
+ run: |
+ rm -rf apps/next-docs/content
+ mkdir -p apps/next-docs
+ git archive "$HEAD_SHA" apps/next-docs/content | tar -xf -
+
+ - name: Set up Node
+ if: steps.request-gate.outputs.should_process == 'true'
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24.14.1
+
+ - name: Cache dependencies
+ if: steps.request-gate.outputs.should_process == 'true'
+ uses: actions/cache@v5
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+
+ - name: Install trusted dependencies
+ if: steps.request-gate.outputs.should_process == 'true'
+ run: npm ci
+
+ - name: Generate Figma images with trusted code
+ if: steps.request-gate.outputs.should_process == 'true'
+ run: npm run figma-images:generate --workspace=apps/next-docs
+ env:
+ FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }}
+
+ - name: Archive generated Figma images
+ if: steps.request-gate.outputs.should_process == 'true'
+ run: |
+ artifact_directory='workflow-artifacts/update-figma-images'
+ mkdir -p "$artifact_directory"
+ tar -cf "$artifact_directory/generated-figma-images.tar" apps/next-docs/public/images/figma
+
+ - name: Checkout pull request head for commit only
+ if: steps.request-gate.outputs.should_process == 'true'
+ uses: actions/checkout@v6
+ with:
+ repository: ${{ steps.request-gate.outputs.head_repo_full_name }}
+ ref: ${{ steps.request-gate.outputs.head_sha }}
+ fetch-depth: 0
+ persist-credentials: false
+ path: pr-head
+
+ - name: Apply generated images to pull request head
+ if: steps.request-gate.outputs.should_process == 'true'
+ run: |
+ rm -rf pr-head/apps/next-docs/public/images/figma
+ mkdir -p pr-head/apps/next-docs/public/images
+ tar -xf workflow-artifacts/update-figma-images/generated-figma-images.tar -C pr-head
+
+ - name: Commit generated images
+ if: steps.request-gate.outputs.should_process == 'true'
+ id: commit-generated-images
+ run: |
+ git -C pr-head add -A apps/next-docs/public/images/figma
+
+ if git -C pr-head diff --cached --quiet; then
+ echo 'created_commit=false' >> "$GITHUB_OUTPUT"
+ echo 'No generated Figma image changes to commit.'
+ exit 0
+ fi
+
+ git -C pr-head config user.name 'github-actions[bot]'
+ git -C pr-head config user.email '41898282+github-actions[bot]@users.noreply.github.com'
+ git -C pr-head commit -m 'github-actions[bot] Update Figma images'
+ echo 'created_commit=true' >> "$GITHUB_OUTPUT"
+
+ - name: Create GitHub App token
+ if: steps.commit-generated-images.outputs.created_commit == 'true'
+ id: app-token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
+ with:
+ app-id: ${{ vars.PRIMER_APP_ID_SHARED }}
+ private-key: ${{ secrets.PRIMER_APP_PRIVATE_KEY_SHARED }}
+
+ - name: Push generated images
+ if: steps.commit-generated-images.outputs.created_commit == 'true'
+ env:
+ APP_TOKEN: ${{ steps.app-token.outputs.token }}
+ HEAD_REF: ${{ steps.request-gate.outputs.head_ref }}
+ HEAD_REPOSITORY: ${{ steps.request-gate.outputs.head_repo_full_name }}
+ run: |
+ git -C pr-head remote set-url origin "https://x-access-token:${APP_TOKEN}@github.com/${HEAD_REPOSITORY}.git"
+ git -C pr-head push origin "HEAD:${HEAD_REF}"
+
+ - name: Skip unsupported request
+ if: steps.request-gate.outputs.should_process != 'true'
+ run: |
+ case '${{ steps.request-gate.outputs.reason }}' in
+ fork)
+ echo 'Skipping Figma image update because pull requests from forks cannot use FIGMA_ACCESS_TOKEN or receive generated commits. Recreate the branch in this repository and re-apply the label.'
+ ;;
+ closed)
+ echo 'Skipping Figma image update because the pull request is closed.'
+ ;;
+ label-removed)
+ echo 'Skipping Figma image update because the request label is no longer present.'
+ ;;
+ *)
+ echo 'Skipping Figma image update.'
+ ;;
+ esac
+
+ - name: Remove request label
+ if: always() && steps.request-gate.outputs.should_cleanup == 'true' && (steps.request-gate.outputs.should_process != 'true' || success())
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const labelName = 'update figma images'
+ const {owner, repo} = context.repo
+ const issue_number = context.payload.pull_request.number
+
+ try {
+ await github.rest.issues.removeLabel({
+ owner,
+ repo,
+ issue_number,
+ name: labelName,
+ })
+ core.info(`Removed label \"${labelName}\" from PR #${issue_number}`)
+ } catch (error) {
+ if (error.status === 404) {
+ core.info(`Label \"${labelName}\" was already removed from PR #${issue_number}`)
+ return
+ }
+
+ throw error
+ }
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 87e02d2be6..b4f58701f2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,6 +49,31 @@ Here are a few things you can do that will increase the likelihood of your pull
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
+### Updating docs Figma images
+
+Docs can use frames from the [Brand Interface Guidelines Figma file](https://www.figma.com/design/kc69gOteR1MsL0aQtLdxLW/-Brand--Interface-guidelines) with the `FigmaImage` component:
+
+```mdx
+
+```
+
+Figma URLs can also be used for the `thumbnail` and `thumbnail_darkMode` frontmatter fields.
+
+To update the generated images locally:
+
+1. Add `FIGMA_ACCESS_TOKEN=...` to `apps/next-docs/.env.local`.
+1. Run `npm run figma-images:generate --workspace=apps/next-docs`.
+
+Files in `apps/next-docs/public/images/figma` are generated artifacts. Do not edit, optimize, recompress, rename, replace, or delete the generated images or their manifest directly. Update the Figma references in the docs source and regenerate the files instead.
+
+Commit only the changes produced by the generator. To update the images through GitHub Actions instead, add the `update figma images` label to your pull request.
+
+CI runs `npm run figma-images:validate --workspace=apps/next-docs` to check the Figma URLs and generated files. This command does not require a Figma access token.
+
## Releasing a new Primer Brand version
See [RELEASING.md](RELEASING.md) for our release process.
diff --git a/apps/next-docs/app/layout.tsx b/apps/next-docs/app/layout.tsx
index 398471b7c1..5630d08458 100644
--- a/apps/next-docs/app/layout.tsx
+++ b/apps/next-docs/app/layout.tsx
@@ -2,6 +2,8 @@ import type {Metadata} from 'next'
import Theme, {getPageMap} from '@primer/doctocat-nextjs'
import type {FC, ReactNode} from 'react'
+// eslint-disable-next-line import/extensions
+import {resolveFigmaPageMapThumbnails} from '../src/components/FigmaImage/FigmaImage.server.mjs'
import '@primer/doctocat-nextjs/css/global.css'
import '../../../packages/react/lib/css/main.css'
import '../src/global.css'
@@ -52,7 +54,7 @@ const sidebarLinks: ThemeProps['sidebarLinks'] = [
]
const RootLayout: FC<{children: ReactNode}> = async ({children}) => {
- const pageMap = await getPageMap()
+ const pageMap = resolveFigmaPageMapThumbnails(await getPageMap())
return (
- Heading: The main title of the CTA banner. It should be short and concise.
- Description: Short text that extends the information provided by the heading.
diff --git a/apps/next-docs/content/components/Card/index.mdx b/apps/next-docs/content/components/Card/index.mdx
index 8b685ad77c..0bf20c95cb 100644
--- a/apps/next-docs/content/components/Card/index.mdx
+++ b/apps/next-docs/content/components/Card/index.mdx
@@ -4,8 +4,8 @@ description: Use the card component to display information in a compact way and
keywords: ['card', 'link', 'summary', 'content', 'information']
show-tabs: true
tab-label: Guidelines
-thumbnail: '/images/thumbnails/card-thumbnail.png'
-thumbnail_darkMode: '/images/thumbnails/card-thumbnail-dark.png'
+thumbnail: 'https://www.figma.com/design/kc69gOteR1MsL0aQtLdxLW/-Brand--Interface-guidelines?node-id=3084-6062&t=vMUGjQTfz0ZJ2Qfa-11'
+thumbnail_darkMode: 'https://www.figma.com/design/kc69gOteR1MsL0aQtLdxLW/-Brand--Interface-guidelines?node-id=3084-6143&t=vMUGjQTfz0ZJ2Qfa-11'
---
import centerAlignDo from './images/cardstackedmaxdo.png'
diff --git a/apps/next-docs/mdx-components.js b/apps/next-docs/mdx-components.js
index 3d662267e7..0d6a3aeda2 100644
--- a/apps/next-docs/mdx-components.js
+++ b/apps/next-docs/mdx-components.js
@@ -19,6 +19,8 @@ import {
} from '@primer/doctocat-nextjs/components'
import NextLink from 'next/link'
+// eslint-disable-next-line import/extensions
+import {FigmaImage} from './src/components/FigmaImage/FigmaImage.tsx'
// eslint-disable-next-line import/extensions
import {Pre} from './src/components/Pre/Pre.tsx'
@@ -56,6 +58,7 @@ export function useMDXComponents(customComponents) {
CodeBlock,
PropTableValues,
TableWrapper,
+ FigmaImage,
Link,
a: Link,
h2: props => ,
diff --git a/apps/next-docs/package.json b/apps/next-docs/package.json
index a9c34458ba..6483eb3b72 100644
--- a/apps/next-docs/package.json
+++ b/apps/next-docs/package.json
@@ -24,11 +24,14 @@
"scripts": {
"clean": "rm -rf out .next",
"dev": "next dev",
- "check": "tsc --noEmit",
+ "check": "tsc --noEmit && npm run figma-images:validate",
"build": "rm -rf out .next && next build --webpack",
"build:prod": "scripts/build",
+ "figma-images:generate": "node --env-file-if-exists=.env.local scripts/figma-images.mjs generate",
+ "figma-images:validate": "node scripts/figma-images.mjs validate",
"lint": "eslint '**/*.{js,ts,tsx,md,mdx}' --max-warnings=0",
- "start": "next start"
+ "start": "next start",
+ "test": "node --test scripts/*.test.mjs"
},
"dependencies": {
"@primer/doctocat-nextjs": "0.10.0",
@@ -40,6 +43,7 @@
"devDependencies": {
"@github/prettier-config": "^0.0.6",
"@primer/brand-primitives": "^0.71.0",
+ "@primer/figma-images": "^0.2.0",
"@primer/react": "38.18.0",
"@primer/react-brand": "0.71.0",
"@swc/core": "^1.15.21",
@@ -47,7 +51,13 @@
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"eslint-config-next": "16.2.2",
- "typescript": "5.9.3"
+ "remark-frontmatter": "^5.0.0",
+ "remark-mdx": "^3.1.1",
+ "remark-parse": "^11.0.0",
+ "typescript": "5.9.3",
+ "unified": "^11.0.5",
+ "unist-util-visit": "^5.1.0",
+ "yaml": "^2.8.3"
},
"peerDependencies": {
"react": "^19.2.0",
diff --git a/apps/next-docs/public/images/figma/anatomy-1804-8382.png b/apps/next-docs/public/images/figma/anatomy-1804-8382.png
new file mode 100644
index 0000000000..f141544a8b
Binary files /dev/null and b/apps/next-docs/public/images/figma/anatomy-1804-8382.png differ
diff --git a/apps/next-docs/public/images/figma/anatomy-dark-5176-1043.png b/apps/next-docs/public/images/figma/anatomy-dark-5176-1043.png
new file mode 100644
index 0000000000..df8443402f
Binary files /dev/null and b/apps/next-docs/public/images/figma/anatomy-dark-5176-1043.png differ
diff --git a/apps/next-docs/public/images/figma/card-thumbnail-3084-6062.png b/apps/next-docs/public/images/figma/card-thumbnail-3084-6062.png
new file mode 100644
index 0000000000..bc63048809
Binary files /dev/null and b/apps/next-docs/public/images/figma/card-thumbnail-3084-6062.png differ
diff --git a/apps/next-docs/public/images/figma/card-thumbnail-dark-3084-6143.png b/apps/next-docs/public/images/figma/card-thumbnail-dark-3084-6143.png
new file mode 100644
index 0000000000..3afa2f1ff2
Binary files /dev/null and b/apps/next-docs/public/images/figma/card-thumbnail-dark-3084-6143.png differ
diff --git a/apps/next-docs/public/images/figma/images.json b/apps/next-docs/public/images/figma/images.json
new file mode 100644
index 0000000000..4ca29ed677
--- /dev/null
+++ b/apps/next-docs/public/images/figma/images.json
@@ -0,0 +1,26 @@
+{
+ "kc69gOteR1MsL0aQtLdxLW-1804-8382": {
+ "width": 1452,
+ "height": 765,
+ "basename": "anatomy-1804-8382",
+ "filename": "anatomy-1804-8382.png"
+ },
+ "kc69gOteR1MsL0aQtLdxLW-5176-1043": {
+ "width": 1452,
+ "height": 765,
+ "basename": "anatomy-dark-5176-1043",
+ "filename": "anatomy-dark-5176-1043.png"
+ },
+ "kc69gOteR1MsL0aQtLdxLW-3084-6062": {
+ "width": 296,
+ "height": 222,
+ "basename": "card-thumbnail-3084-6062",
+ "filename": "card-thumbnail-3084-6062.png"
+ },
+ "kc69gOteR1MsL0aQtLdxLW-3084-6143": {
+ "width": 296,
+ "height": 222,
+ "basename": "card-thumbnail-dark-3084-6143",
+ "filename": "card-thumbnail-dark-3084-6143.png"
+ }
+}
\ No newline at end of file
diff --git a/apps/next-docs/scripts/figma-images.mjs b/apps/next-docs/scripts/figma-images.mjs
new file mode 100644
index 0000000000..fa04246a11
--- /dev/null
+++ b/apps/next-docs/scripts/figma-images.mjs
@@ -0,0 +1,302 @@
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import {fileURLToPath} from 'node:url'
+import figmaImages from '@primer/figma-images'
+import {parseFigmaNodeUrl} from '@primer/figma-images/parseFigmaNodeUrl'
+import {unified} from 'unified'
+import remarkParse from 'remark-parse'
+import remarkFrontmatter from 'remark-frontmatter'
+import remarkMdx from 'remark-mdx'
+import {visit} from 'unist-util-visit'
+import {parse as parseYaml} from 'yaml'
+
+export const FIGMA_FILE_KEY = 'kc69gOteR1MsL0aQtLdxLW'
+
+const APP_ROOT = fileURLToPath(new URL('..', import.meta.url))
+const CONTENT_DIRECTORY = path.join(APP_ROOT, 'content')
+const OUTPUT_DIRECTORY = path.join(APP_ROOT, 'public/images/figma')
+const FILENAME_FORMAT = '{nodeName}-{nodeId}'
+const FIGMA_IMAGE_PROPS = new Set(['src', 'darkModeSrc'])
+const FIGMA_THUMBNAIL_FIELDS = new Set(['thumbnail', 'thumbnail_darkMode'])
+const mdxParser = unified().use(remarkParse).use(remarkFrontmatter, ['yaml']).use(remarkMdx)
+
+export function extractFigmaUrls(content, source = 'MDX content') {
+ const urls = []
+ let tree
+
+ try {
+ tree = mdxParser.parse({value: content, path: source})
+ } catch (error) {
+ throw new Error(`${source}: unable to parse MDX: ${error.message}`)
+ }
+
+ visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], node => {
+ if (node.name !== 'FigmaImage') return
+
+ for (const attribute of node.attributes) {
+ if (attribute.type !== 'mdxJsxAttribute' || !FIGMA_IMAGE_PROPS.has(attribute.name)) continue
+
+ if (typeof attribute.value !== 'string') {
+ throw new Error(
+ `${formatNodeLocation(source, attribute)}: FigmaImage ${attribute.name} must be a static quoted URL.`,
+ )
+ }
+
+ urls.push(attribute.value)
+ }
+ })
+
+ visit(tree, 'yaml', node => {
+ let frontmatter
+
+ try {
+ frontmatter = parseYaml(node.value)
+ } catch (error) {
+ throw new Error(`${formatNodeLocation(source, node)}: unable to parse frontmatter: ${error.message}`)
+ }
+
+ if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) return
+
+ for (const [field, value] of Object.entries(frontmatter)) {
+ if (FIGMA_THUMBNAIL_FIELDS.has(field) && typeof value === 'string' && isFigmaHostedUrl(value)) {
+ urls.push(value)
+ }
+ }
+ })
+
+ return [...new Set(urls)]
+}
+
+export async function discoverFigmaUrls(contentDirectory = CONTENT_DIRECTORY) {
+ const files = await findMdxFiles(contentDirectory)
+ const discovered = []
+ const errors = []
+
+ for (const filePath of files) {
+ try {
+ const content = await fs.readFile(filePath, 'utf8')
+ for (const url of extractFigmaUrls(content, filePath)) {
+ discovered.push({url, filePath})
+ }
+ } catch (error) {
+ errors.push(error.message)
+ }
+ }
+
+ if (errors.length > 0) {
+ throw new Error(`Unable to discover Figma images:\n- ${errors.join('\n- ')}`)
+ }
+
+ return discovered
+}
+
+export function validateFigmaUrl(url, source = 'Figma image') {
+ let parsedUrl
+
+ try {
+ parsedUrl = new URL(url, 'https://www.figma.com')
+ } catch {
+ throw new Error(`${source}: "${url}" is not a valid URL.`)
+ }
+
+ if (
+ parsedUrl.protocol !== 'https:' ||
+ parsedUrl.hostname !== 'www.figma.com' ||
+ !['design', 'file', 'board'].includes(parsedUrl.pathname.split('/')[1])
+ ) {
+ throw new Error(`${source}: "${url}" must use an HTTPS design, file, or board node URL from www.figma.com.`)
+ }
+
+ const nodeId = parsedUrl.searchParams.get('node-id')
+ if (!nodeId) {
+ throw new Error(`${source}: "${url}" must include a node-id query parameter.`)
+ }
+
+ parsedUrl.searchParams.set('node-id', nodeId.replaceAll(':', '-'))
+ const canonicalUrl = parsedUrl.toString()
+ const parsedNode = parseFigmaNodeUrl(canonicalUrl)
+
+ if (!parsedNode) {
+ throw new Error(`${source}: "${url}" is not a valid Figma node URL.`)
+ }
+
+ if (parsedNode.fileId !== FIGMA_FILE_KEY) {
+ throw new Error(
+ `${source}: Figma file key "${parsedNode.fileId}" is not approved. Use a frame from the Brand Interface Guidelines file (${FIGMA_FILE_KEY}).`,
+ )
+ }
+
+ return {
+ originalUrl: url,
+ canonicalUrl,
+ ...parsedNode,
+ }
+}
+
+export function validateFigmaUrls(discovered) {
+ const validated = []
+ const errors = []
+
+ for (const {url, filePath} of discovered) {
+ try {
+ validated.push(validateFigmaUrl(url, filePath))
+ } catch (error) {
+ errors.push(error.message)
+ }
+ }
+
+ if (errors.length > 0) {
+ throw new Error(`Figma image validation failed:\n- ${errors.join('\n- ')}`)
+ }
+
+ return [...new Map(validated.map(image => [image.basename, image])).values()]
+}
+
+export async function generateFigmaImages(
+ validated,
+ token,
+ outputDirectory = OUTPUT_DIRECTORY,
+ filenameFormat = FILENAME_FORMAT,
+) {
+ if (!token) {
+ throw new Error(
+ 'FIGMA_ACCESS_TOKEN is required to generate Figma images. Add it to apps/next-docs/.env.local or your shell environment.',
+ )
+ }
+
+ await figmaImages(token, {
+ nodeURLs: validated.map(image => image.canonicalUrl),
+ outputDir: outputDirectory,
+ missingImagesLogLevel: 'fail',
+ filenameFormat,
+ clean: true,
+ })
+
+ await verifyFigmaImageAssets(validated, outputDirectory)
+}
+
+async function findMdxFiles(directory) {
+ const entries = await fs.readdir(directory, {withFileTypes: true})
+ const files = []
+
+ for (const entry of entries) {
+ const entryPath = path.join(directory, entry.name)
+ if (entry.isDirectory()) {
+ files.push(...(await findMdxFiles(entryPath)))
+ } else if (entry.isFile() && entry.name.endsWith('.mdx')) {
+ files.push(entryPath)
+ }
+ }
+
+ return files.sort()
+}
+
+function isFigmaHostedUrl(value) {
+ try {
+ return ['figma.com', 'www.figma.com'].includes(new URL(value).hostname)
+ } catch {
+ return false
+ }
+}
+
+function formatNodeLocation(source, node) {
+ return node.position?.start.line ? `${source}:${node.position.start.line}:${node.position.start.column}` : source
+}
+
+export async function verifyFigmaImageAssets(validated, outputDirectory = OUTPUT_DIRECTORY) {
+ const manifestPath = path.join(outputDirectory, 'images.json')
+ let manifest
+
+ try {
+ manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'))
+ } catch (error) {
+ throw new Error(`Unable to read the generated Figma image manifest at ${manifestPath}: ${error.message}`)
+ }
+
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
+ throw new Error(`The generated Figma image manifest at ${manifestPath} must contain an object.`)
+ }
+
+ const expectedBasenames = new Set(validated.map(image => image.basename))
+ const staleManifestEntries = Object.keys(manifest).filter(basename => !expectedBasenames.has(basename))
+ if (staleManifestEntries.length > 0) {
+ throw new Error(`Manifest entries remain for unreferenced Figma images: ${staleManifestEntries.join(', ')}`)
+ }
+
+ const expectedFiles = new Set()
+
+ for (const image of validated) {
+ const manifestEntry = manifest[image.basename]
+ if (
+ !manifestEntry ||
+ !Number.isFinite(manifestEntry.width) ||
+ !Number.isFinite(manifestEntry.height) ||
+ typeof manifestEntry.filename !== 'string'
+ ) {
+ throw new Error(`The manifest entry for ${image.originalUrl} must include width, height, and filename.`)
+ }
+
+ if (
+ !manifestEntry.filename ||
+ manifestEntry.filename === '.' ||
+ manifestEntry.filename === '..' ||
+ path.basename(manifestEntry.filename) !== manifestEntry.filename ||
+ path.win32.basename(manifestEntry.filename) !== manifestEntry.filename
+ ) {
+ throw new Error(`The manifest filename for ${image.originalUrl} must not contain path segments.`)
+ }
+
+ expectedFiles.add(manifestEntry.filename)
+ }
+
+ const generatedFiles = (await fs.readdir(outputDirectory, {withFileTypes: true}))
+ .filter(entry => entry.name !== 'images.json')
+ .map(entry => entry.name)
+ const staleGeneratedFiles = generatedFiles.filter(filename => !expectedFiles.has(filename))
+ if (staleGeneratedFiles.length > 0) {
+ throw new Error(`Generated files remain for unreferenced Figma images: ${staleGeneratedFiles.join(', ')}`)
+ }
+
+ for (const image of validated) {
+ const manifestEntry = manifest[image.basename]
+ try {
+ await fs.access(path.join(outputDirectory, manifestEntry.filename))
+ } catch {
+ throw new Error(`Generated image is missing for ${image.originalUrl}: ${manifestEntry.filename}`)
+ }
+ }
+}
+
+async function run() {
+ const command = process.argv[2]
+ const discovered = await discoverFigmaUrls()
+ const validated = validateFigmaUrls(discovered)
+
+ if (command === 'validate') {
+ await verifyFigmaImageAssets(validated)
+ // eslint-disable-next-line no-console
+ console.log(
+ `Validated ${validated.length} Figma image URL${validated.length === 1 ? '' : 's'} and committed asset${
+ validated.length === 1 ? '' : 's'
+ }.`,
+ )
+ return
+ }
+
+ if (command === 'generate') {
+ await generateFigmaImages(validated, process.env.FIGMA_ACCESS_TOKEN)
+ return
+ }
+
+ throw new Error('Usage: node scripts/figma-images.mjs ')
+}
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ try {
+ await run()
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error.message)
+ process.exitCode = 1
+ }
+}
diff --git a/apps/next-docs/scripts/figma-images.test.mjs b/apps/next-docs/scripts/figma-images.test.mjs
new file mode 100644
index 0000000000..222156c21b
--- /dev/null
+++ b/apps/next-docs/scripts/figma-images.test.mjs
@@ -0,0 +1,114 @@
+/* eslint-disable github/unescaped-html-literal, import/extensions */
+import assert from 'node:assert/strict'
+import fs from 'node:fs/promises'
+import os from 'node:os'
+import path from 'node:path'
+import {afterEach, describe, it} from 'node:test'
+
+import {
+ resolveFigmaImageSource,
+ resolveFigmaPageMapThumbnails,
+} from '../src/components/FigmaImage/FigmaImage.server.mjs'
+import {
+ FIGMA_FILE_KEY,
+ discoverFigmaUrls,
+ extractFigmaUrls,
+ validateFigmaUrl,
+ validateFigmaUrls,
+ verifyFigmaImageAssets,
+} from './figma-images.mjs'
+
+const temporaryDirectories = []
+const approvedUrl = `https://www.figma.com/design/${FIGMA_FILE_KEY}/Brand?node-id=1804-8382`
+
+afterEach(async () => {
+ await Promise.all(temporaryDirectories.splice(0).map(directory => fs.rm(directory, {recursive: true, force: true})))
+})
+
+describe('Figma image workflow', () => {
+ it('discovers and validates supported references', async () => {
+ const contentDirectory = await createTemporaryDirectory()
+ await fs.writeFile(
+ path.join(contentDirectory, 'example.mdx'),
+ `---
+thumbnail: https://www.figma.com/file/${FIGMA_FILE_KEY}/Brand?node-id=1804-8383
+---
+
+
+`,
+ )
+
+ const discovered = await discoverFigmaUrls(contentDirectory)
+ const validated = validateFigmaUrls(discovered)
+
+ assert.equal(discovered.length, 3)
+ assert.deepEqual(
+ validated.map(image => image.basename),
+ [`${FIGMA_FILE_KEY}-1804-8382`, `${FIGMA_FILE_KEY}-1804-8383`],
+ )
+ })
+
+ it('rejects dynamic and unapproved references', () => {
+ assert.throws(
+ () => extractFigmaUrls('', 'example.mdx'),
+ /example\.mdx.*must be a static quoted URL/,
+ )
+ assert.throws(
+ () => validateFigmaUrl('https://www.figma.com/design/foreign-file/Other?node-id=1-2'),
+ /file key "foreign-file" is not approved/,
+ )
+ })
+
+ it('verifies generated assets without allowing manifest paths to escape the output directory', async () => {
+ const outputDirectory = await createTemporaryDirectory()
+ const image = validateFigmaUrl(approvedUrl)
+ const manifestPath = path.join(outputDirectory, 'images.json')
+ const filename = 'preview.png'
+
+ await fs.writeFile(manifestPath, JSON.stringify({[image.basename]: {width: 100, height: 100, filename}}))
+ await fs.writeFile(path.join(outputDirectory, filename), 'image')
+
+ await verifyFigmaImageAssets([image], outputDirectory)
+
+ await fs.writeFile(
+ manifestPath,
+ JSON.stringify({[image.basename]: {width: 100, height: 100, filename: '../outside.png'}}),
+ )
+
+ await assert.rejects(verifyFigmaImageAssets([image], outputDirectory), /must not contain path segments/)
+ })
+
+ it('only exposes canonical Figma edit links', () => {
+ const source = resolveFigmaImageSource(`/design/${FIGMA_FILE_KEY}/Brand?node-id=1804%3A8382`)
+
+ assert.equal(source.editUrl, approvedUrl)
+ assert.equal(resolveFigmaImageSource('javascript:alert(1)').editUrl, undefined)
+ assert.equal(
+ resolveFigmaImageSource(`https://example.com/design/${FIGMA_FILE_KEY}/Brand?node-id=1804-8382`).editUrl,
+ undefined,
+ )
+ })
+
+ it('maps Figma thumbnails to committed assets', () => {
+ const pageMap = [
+ {
+ route: '/example',
+ frontMatter: {
+ thumbnail: approvedUrl,
+ thumbnail_darkMode: '/images/example-dark.png',
+ },
+ },
+ ]
+
+ const resolved = resolveFigmaPageMapThumbnails(pageMap)
+
+ assert.equal(resolved[0].frontMatter.thumbnail, '/images/figma/anatomy-1804-8382.png')
+ assert.equal(resolved[0].frontMatter.thumbnail_darkMode, '/images/example-dark.png')
+ })
+})
+
+async function createTemporaryDirectory() {
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'brand-figma-images-'))
+ temporaryDirectories.push(directory)
+ return directory
+}
diff --git a/apps/next-docs/src/components/FigmaImage/FigmaImage.module.css b/apps/next-docs/src/components/FigmaImage/FigmaImage.module.css
new file mode 100644
index 0000000000..1c7081d863
--- /dev/null
+++ b/apps/next-docs/src/components/FigmaImage/FigmaImage.module.css
@@ -0,0 +1,107 @@
+.FigmaImage {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: var(--base-size-8);
+ inline-size: fit-content;
+ max-inline-size: 100%;
+}
+
+.FigmaImage--full-width {
+ inline-size: 100%;
+}
+
+.FigmaImage__preview {
+ position: relative;
+ overflow: hidden;
+ inline-size: 100%;
+ border-radius: var(--brand-borderRadius-medium);
+}
+
+.FigmaImage__preview::after {
+ position: absolute;
+ z-index: 1;
+ background-image: linear-gradient(to bottom left, var(--brand-color-canvas-default) 10%, transparent 80%);
+ content: '';
+ inset: 0;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--brand-animation-duration-faster) var(--brand-animation-easing-default);
+}
+
+.FigmaImage:hover .FigmaImage__preview::after,
+.FigmaImage:focus-within .FigmaImage__preview::after {
+ opacity: 1;
+}
+
+.FigmaImage__image {
+ display: block;
+ max-inline-size: 100%;
+ block-size: auto;
+ border: var(--brand-borderWidth-thin) solid var(--borderColor-default);
+ border-radius: var(--brand-borderRadius-medium);
+ background-color: var(--brand-color-canvas-default);
+ transition: opacity var(--brand-animation-duration-faster) var(--brand-animation-easing-default);
+}
+
+.FigmaImage:hover .FigmaImage__image,
+.FigmaImage:focus-within .FigmaImage__image {
+ opacity: 0.8;
+}
+
+.FigmaImage__image--full-width {
+ inline-size: 100%;
+}
+
+.FigmaImage__missing-preview {
+ display: grid;
+ gap: var(--base-size-8);
+ padding: var(--base-size-16);
+ padding-block-start: var(--base-size-64);
+ border: var(--brand-borderWidth-thin) solid var(--borderColor-default);
+ border-radius: var(--brand-borderRadius-medium);
+ background-color: var(--brand-color-canvas-subtle);
+}
+
+.FigmaImage__missing-preview-title {
+ margin: 0;
+ color: var(--brand-color-text-default);
+ font-size: var(--brand-text-size-200);
+ font-weight: var(--brand-text-weight-600);
+ line-height: var(--brand-text-lineHeight-200);
+}
+
+.FigmaImage__missing-preview-body {
+ margin: 0;
+ color: var(--brand-color-text-muted);
+}
+
+.FigmaImage__caption {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--base-size-8) var(--base-size-12);
+ align-items: baseline;
+ color: var(--brand-color-text-muted);
+ font-size: var(--brand-text-size-100);
+ line-height: var(--brand-text-lineHeight-100);
+}
+
+.FigmaImage__caption-text {
+ min-inline-size: 0;
+}
+
+.FigmaImage__edit-link {
+ position: absolute;
+ z-index: 1;
+ inset-block-start: var(--base-size-16);
+ inset-inline-end: var(--base-size-16);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--brand-animation-duration-faster) var(--brand-animation-easing-default);
+}
+
+.FigmaImage:hover .FigmaImage__edit-link,
+.FigmaImage__edit-link:focus {
+ opacity: 1;
+ pointer-events: auto;
+}
diff --git a/apps/next-docs/src/components/FigmaImage/FigmaImage.server.mjs b/apps/next-docs/src/components/FigmaImage/FigmaImage.server.mjs
new file mode 100644
index 0000000000..f069dde5ba
--- /dev/null
+++ b/apps/next-docs/src/components/FigmaImage/FigmaImage.server.mjs
@@ -0,0 +1,107 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import {parseFigmaNodeUrl} from '@primer/figma-images/parseFigmaNodeUrl'
+
+const FIGMA_IMAGE_DIRECTORY = path.join(process.cwd(), 'public/images/figma')
+const FIGMA_IMAGE_MANIFEST_PATH = path.join(FIGMA_IMAGE_DIRECTORY, 'images.json')
+const DOCTOCAT_BASE_PATH = process.env.GITHUB_ACTIONS === 'true' ? '/brand' : ''
+const FIGMA_IMAGE_MANIFEST = readFigmaImageManifest()
+
+function readFigmaImageManifest() {
+ if (!fs.existsSync(FIGMA_IMAGE_MANIFEST_PATH)) {
+ return {}
+ }
+
+ return JSON.parse(fs.readFileSync(FIGMA_IMAGE_MANIFEST_PATH, 'utf8'))
+}
+
+function normalizeFigmaNodeUrl(url) {
+ const parsedUrl = new URL(url, 'https://www.figma.com')
+
+ if (
+ parsedUrl.protocol !== 'https:' ||
+ parsedUrl.hostname !== 'www.figma.com' ||
+ !['design', 'file', 'board'].includes(parsedUrl.pathname.split('/')[1])
+ ) {
+ throw new Error('Invalid Figma node URL')
+ }
+
+ const nodeId = parsedUrl.searchParams.get('node-id')
+
+ if (nodeId) {
+ parsedUrl.searchParams.set('node-id', nodeId.replaceAll(':', '-'))
+ }
+
+ return parsedUrl.toString()
+}
+
+export function resolveFigmaImageSource(url, basePath = DOCTOCAT_BASE_PATH) {
+ try {
+ const editUrl = normalizeFigmaNodeUrl(url)
+ const parsedNode = parseFigmaNodeUrl(editUrl)
+
+ if (!parsedNode) {
+ return {
+ missingReason: 'The selected Figma frame URL could not be parsed.',
+ }
+ }
+
+ const manifestEntry = FIGMA_IMAGE_MANIFEST[parsedNode.basename]
+ const filename = manifestEntry?.filename ?? parsedNode.filename
+ const imagePath = path.join(FIGMA_IMAGE_DIRECTORY, filename)
+
+ if (!fs.existsSync(imagePath)) {
+ return {
+ editUrl,
+ width: manifestEntry?.width,
+ height: manifestEntry?.height,
+ missingReason: 'The generated preview image is missing for this Figma frame.',
+ }
+ }
+
+ return {
+ assetUrl: `${basePath}/images/figma/${filename}`,
+ editUrl,
+ width: manifestEntry?.width,
+ height: manifestEntry?.height,
+ }
+ } catch {
+ return {
+ missingReason: 'The selected Figma frame URL could not be parsed.',
+ }
+ }
+}
+
+export function resolveFigmaPageMapThumbnails(pageMap) {
+ return pageMap.map(item => {
+ const resolvedItem = {...item}
+
+ if ('children' in item) {
+ resolvedItem.children = resolveFigmaPageMapThumbnails(item.children)
+ }
+
+ if ('frontMatter' in item && item.frontMatter) {
+ resolvedItem.frontMatter = {
+ ...item.frontMatter,
+ thumbnail: resolveFigmaThumbnail(item.frontMatter.thumbnail, item.route),
+ thumbnail_darkMode: resolveFigmaThumbnail(item.frontMatter.thumbnail_darkMode, item.route),
+ }
+ }
+
+ return resolvedItem
+ })
+}
+
+function resolveFigmaThumbnail(value, route) {
+ if (typeof value !== 'string' || !value.startsWith('https://www.figma.com/')) {
+ return value
+ }
+
+ const resolved = resolveFigmaImageSource(value, '')
+
+ if (!resolved.assetUrl) {
+ throw new Error(`${route}: ${resolved.missingReason}`)
+ }
+
+ return resolved.assetUrl
+}
diff --git a/apps/next-docs/src/components/FigmaImage/FigmaImage.tsx b/apps/next-docs/src/components/FigmaImage/FigmaImage.tsx
new file mode 100644
index 0000000000..60e1ec4552
--- /dev/null
+++ b/apps/next-docs/src/components/FigmaImage/FigmaImage.tsx
@@ -0,0 +1,107 @@
+import {clsx} from 'clsx'
+import type {ReactNode} from 'react'
+import {FigmaImageEditLink} from './FigmaImageEditLink'
+import {FigmaImagePreview} from './FigmaImagePreview'
+import type {ResolvedFigmaImageSource} from './FigmaImage.types'
+import styles from './FigmaImage.module.css'
+// eslint-disable-next-line import/extensions
+import {resolveFigmaImageSource} from './FigmaImage.server.mjs'
+
+type FigmaImageProps = {
+ src: string
+ darkModeSrc?: string
+ alt?: string
+ role?: 'presentation'
+ caption?: ReactNode
+ children?: ReactNode
+ width?: number
+ height?: number
+ fullWidth?: boolean
+ className?: string
+}
+
+function getAccessibleAlt(alt?: string, role?: 'presentation') {
+ if (role === 'presentation') {
+ return {alt: '', presentation: true}
+ }
+
+ const trimmedAlt = alt?.trim()
+
+ if (trimmedAlt) {
+ return {alt: trimmedAlt, presentation: false}
+ }
+
+ // eslint-disable-next-line i18n-text/no-en
+ const missingReason = 'Add descriptive alt text or set role="presentation" for decorative Figma images.'
+
+ return {
+ alt: '',
+ presentation: false,
+ missingReason,
+ }
+}
+
+export function FigmaImage({
+ src,
+ darkModeSrc,
+ alt,
+ role,
+ caption,
+ children,
+ width,
+ height,
+ fullWidth = false,
+ className,
+}: FigmaImageProps) {
+ const lightSource: ResolvedFigmaImageSource = resolveFigmaImageSource(src)
+ const darkSource: ResolvedFigmaImageSource | undefined = darkModeSrc
+ ? resolveFigmaImageSource(darkModeSrc)
+ : undefined
+ const accessibleImage = getAccessibleAlt(alt, role)
+ const captionContent = caption ?? children
+ const figureClassName = clsx(
+ 'custom-component',
+ styles.FigmaImage,
+ fullWidth && styles['FigmaImage--full-width'],
+ className,
+ )
+
+ const previewLightSource = accessibleImage.missingReason
+ ? {
+ ...lightSource,
+ assetUrl: undefined,
+ missingReason: accessibleImage.missingReason,
+ }
+ : lightSource
+
+ const previewDarkSource =
+ accessibleImage.missingReason && darkSource
+ ? {
+ ...darkSource,
+ assetUrl: undefined,
+ missingReason: accessibleImage.missingReason,
+ }
+ : darkSource
+
+ return (
+
+
+
+
+
+ {captionContent ? (
+
+ {captionContent}
+
+ ) : null}
+
+ )
+}
diff --git a/apps/next-docs/src/components/FigmaImage/FigmaImage.types.ts b/apps/next-docs/src/components/FigmaImage/FigmaImage.types.ts
new file mode 100644
index 0000000000..a23da5364a
--- /dev/null
+++ b/apps/next-docs/src/components/FigmaImage/FigmaImage.types.ts
@@ -0,0 +1,7 @@
+export type ResolvedFigmaImageSource = {
+ assetUrl?: string
+ editUrl?: string
+ width?: number
+ height?: number
+ missingReason?: string
+}
diff --git a/apps/next-docs/src/components/FigmaImage/FigmaImage.utils.ts b/apps/next-docs/src/components/FigmaImage/FigmaImage.utils.ts
new file mode 100644
index 0000000000..b8abf424e6
--- /dev/null
+++ b/apps/next-docs/src/components/FigmaImage/FigmaImage.utils.ts
@@ -0,0 +1,62 @@
+import type {ResolvedFigmaImageSource} from './FigmaImage.types'
+
+export function getActiveFigmaSource(
+ colorMode: 'light' | 'dark',
+ lightSource: ResolvedFigmaImageSource,
+ darkSource?: ResolvedFigmaImageSource,
+) {
+ if (colorMode === 'dark' && darkSource?.assetUrl) {
+ return darkSource
+ }
+
+ if (lightSource.assetUrl) {
+ return lightSource
+ }
+
+ if (darkSource?.assetUrl) {
+ return darkSource
+ }
+
+ return colorMode === 'dark' ? darkSource ?? lightSource : lightSource
+}
+
+export function getActiveFigmaEditUrl(
+ colorMode: 'light' | 'dark',
+ lightSource: ResolvedFigmaImageSource,
+ darkSource?: ResolvedFigmaImageSource,
+) {
+ if (colorMode === 'dark' && darkSource?.editUrl) {
+ return darkSource.editUrl
+ }
+
+ return lightSource.editUrl
+}
+
+export function resolveImageDimensions(
+ source: ResolvedFigmaImageSource,
+ explicitWidth?: number,
+ explicitHeight?: number,
+) {
+ if (explicitWidth && explicitHeight) {
+ return {width: explicitWidth, height: explicitHeight}
+ }
+
+ if (explicitWidth && source.width && source.height) {
+ return {
+ width: explicitWidth,
+ height: Math.round((explicitWidth * source.height) / source.width),
+ }
+ }
+
+ if (explicitHeight && source.width && source.height) {
+ return {
+ width: Math.round((explicitHeight * source.width) / source.height),
+ height: explicitHeight,
+ }
+ }
+
+ return {
+ width: explicitWidth ?? source.width,
+ height: explicitHeight ?? source.height,
+ }
+}
diff --git a/apps/next-docs/src/components/FigmaImage/FigmaImageEditLink.tsx b/apps/next-docs/src/components/FigmaImage/FigmaImageEditLink.tsx
new file mode 100644
index 0000000000..2541a75a3d
--- /dev/null
+++ b/apps/next-docs/src/components/FigmaImage/FigmaImageEditLink.tsx
@@ -0,0 +1,40 @@
+'use client'
+
+import {useColorMode} from '@primer/doctocat-nextjs/components/context/color-modes/useColorMode'
+import {PencilIcon} from '@primer/octicons-react'
+import {Button} from '@primer/react-brand'
+import type {ResolvedFigmaImageSource} from './FigmaImage.types'
+import {getActiveFigmaEditUrl} from './FigmaImage.utils'
+import styles from './FigmaImage.module.css'
+
+type FigmaImageEditLinkProps = {
+ lightSource: ResolvedFigmaImageSource
+ darkSource?: ResolvedFigmaImageSource
+}
+
+export function FigmaImageEditLink({lightSource, darkSource}: FigmaImageEditLinkProps) {
+ const {colorMode} = useColorMode()
+ const editUrl = getActiveFigmaEditUrl(colorMode, lightSource, darkSource)
+
+ if (!editUrl) {
+ return null
+ }
+
+ // eslint-disable-next-line i18n-text/no-en
+ const editLinkLabel = 'Edit in Figma'
+
+ return (
+
+ )
+}
diff --git a/apps/next-docs/src/components/FigmaImage/FigmaImagePreview.tsx b/apps/next-docs/src/components/FigmaImage/FigmaImagePreview.tsx
new file mode 100644
index 0000000000..da2657a845
--- /dev/null
+++ b/apps/next-docs/src/components/FigmaImage/FigmaImagePreview.tsx
@@ -0,0 +1,58 @@
+'use client'
+
+import {useColorMode} from '@primer/doctocat-nextjs/components/context/color-modes/useColorMode'
+import {clsx} from 'clsx'
+import type {ResolvedFigmaImageSource} from './FigmaImage.types'
+import {getActiveFigmaSource, resolveImageDimensions} from './FigmaImage.utils'
+import styles from './FigmaImage.module.css'
+
+type FigmaImagePreviewProps = {
+ lightSource: ResolvedFigmaImageSource
+ darkSource?: ResolvedFigmaImageSource
+ alt: string
+ presentation: boolean
+ width?: number
+ height?: number
+ fullWidth: boolean
+}
+
+export function FigmaImagePreview({
+ lightSource,
+ darkSource,
+ alt,
+ presentation,
+ width,
+ height,
+ fullWidth,
+}: FigmaImagePreviewProps) {
+ const {colorMode} = useColorMode()
+ const activeSource = getActiveFigmaSource(colorMode, lightSource, darkSource)
+ // eslint-disable-next-line i18n-text/no-en
+ const previewUnavailableText = 'Figma preview unavailable.'
+ // eslint-disable-next-line i18n-text/no-en
+ const defaultPreviewFallbackText = 'This page build does not include a generated preview for the selected frame.'
+ const previewFallbackText = activeSource.missingReason ?? defaultPreviewFallbackText
+
+ if (!activeSource.assetUrl) {
+ return (
+
+
{previewUnavailableText}
+
{previewFallbackText}
+
+ )
+ }
+
+ const dimensions = resolveImageDimensions(activeSource, width, height)
+
+ return (
+
+ )
+}
diff --git a/package-lock.json b/package-lock.json
index da2812c60f..06e740eaf2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,7 +51,7 @@
},
"apps/next-docs": {
"name": "@primer/brand-docs",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"dependencies": {
"@primer/doctocat-nextjs": "0.10.0",
@@ -62,15 +62,22 @@
},
"devDependencies": {
"@github/prettier-config": "^0.0.6",
- "@primer/brand-primitives": "^0.69.0",
+ "@primer/brand-primitives": "^0.71.0",
+ "@primer/figma-images": "^0.2.0",
"@primer/react": "38.18.0",
- "@primer/react-brand": "0.69.0",
+ "@primer/react-brand": "0.71.0",
"@swc/core": "^1.15.21",
"@types/node": "24.12.0",
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"eslint-config-next": "16.2.2",
- "typescript": "5.9.3"
+ "remark-frontmatter": "^5.0.0",
+ "remark-mdx": "^3.1.1",
+ "remark-parse": "^11.0.0",
+ "typescript": "5.9.3",
+ "unified": "^11.0.5",
+ "unist-util-visit": "^5.1.0",
+ "yaml": "^2.8.3"
},
"engines": {
"node": ">=24.0.0",
@@ -155,7 +162,7 @@
},
"apps/storybook": {
"name": "@primer/brand-storybook",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"dependencies": {
"@storybook/addon-links": "10.3.4",
@@ -359,9 +366,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -379,9 +383,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -399,9 +400,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -419,9 +417,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -510,6 +505,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -2817,6 +2813,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -2840,6 +2837,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -2962,6 +2960,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -2985,6 +2984,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -3164,6 +3164,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -3187,6 +3188,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -3279,6 +3281,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -3302,6 +3305,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -3424,6 +3428,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -3447,6 +3452,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -3569,6 +3575,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -3592,6 +3599,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -3664,6 +3672,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -3786,6 +3795,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -3809,6 +3819,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -3881,6 +3892,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -3904,6 +3916,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -4077,6 +4090,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -4100,6 +4114,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -4222,6 +4237,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -4245,6 +4261,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -4367,6 +4384,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -4390,6 +4408,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -4540,6 +4559,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -4755,6 +4775,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -4778,6 +4799,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -4850,6 +4872,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -4873,6 +4896,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -4967,6 +4991,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -5142,6 +5167,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -5165,6 +5191,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -5284,6 +5311,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -5356,6 +5384,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -5379,6 +5408,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -5501,6 +5531,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -5524,6 +5555,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -5622,6 +5654,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -5645,6 +5678,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -5717,6 +5751,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -5740,6 +5775,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -5856,6 +5892,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -5975,6 +6012,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -5998,6 +6036,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -6105,20 +6144,21 @@
}
},
"node_modules/@emnapi/core": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
- "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
+ "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
+ "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/wasi-threads": "1.2.1",
+ "@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
- "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
+ "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6126,9 +6166,10 @@
}
},
"node_modules/@emnapi/wasi-threads": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
- "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6738,9 +6779,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -6757,9 +6795,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -6776,9 +6811,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -6795,9 +6827,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -6814,9 +6843,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -6833,9 +6859,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -6852,9 +6875,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -6871,9 +6891,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -6890,9 +6907,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -6915,9 +6929,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -6940,9 +6951,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -6965,9 +6973,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -6990,9 +6995,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -7015,9 +7017,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -7040,9 +7039,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -7065,9 +7061,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -8372,6 +8365,7 @@
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -8688,9 +8682,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -8707,9 +8698,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -8726,9 +8714,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -8745,9 +8730,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -8764,9 +8746,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -8783,9 +8762,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -8971,9 +8947,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -8990,9 +8963,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9009,9 +8979,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9028,9 +8995,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9824,9 +9788,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9847,9 +9808,6 @@
"cpu": [
"arm"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9870,9 +9828,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9893,9 +9848,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9916,9 +9868,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9939,9 +9888,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10209,7 +10155,218 @@
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz",
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@primer/figma-images": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@primer/figma-images/-/figma-images-0.2.0.tgz",
+ "integrity": "sha512-2bqMPtZ6Dxc6dmVm9jLp8qdsA60TPxKCOmyBrdrRC23SPIbP7B0uMJzOtDCTFY1nwdc4s5xCrmwrfHFxitxhPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.3.4",
+ "dotenv": "^16.0.3",
+ "figma-js": "^1.16.1-0",
+ "ora": "^6.1.2",
+ "p-limit": "^4.0.0",
+ "p-retry": "^5.1.2",
+ "yargs": "^17.7.1"
+ },
+ "bin": {
+ "figma-images": "bin/cli.js"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@primer/figma-images/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/@primer/figma-images/node_modules/cli-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+ "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/is-interactive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+ "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/log-symbols": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz",
+ "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.0.0",
+ "is-unicode-supported": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/ora": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz",
+ "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.0.0",
+ "cli-cursor": "^4.0.0",
+ "cli-spinners": "^2.6.1",
+ "is-interactive": "^2.0.0",
+ "is-unicode-supported": "^1.1.0",
+ "log-symbols": "^5.1.0",
+ "stdin-discarder": "^0.1.0",
+ "strip-ansi": "^7.0.1",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/p-limit": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
+ "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/restore-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+ "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@primer/figma-images/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@primer/figma-images/node_modules/yocto-queue": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
+ "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
},
"node_modules/@primer/live-region-element": {
"version": "0.7.2",
@@ -11248,9 +11405,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -11267,9 +11421,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -11286,9 +11437,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -11305,9 +11453,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -11324,9 +11469,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -11343,9 +11485,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -11389,6 +11528,16 @@
"node": "^20.19.0 || >=22.12.0"
}
},
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
@@ -12508,6 +12657,7 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.26"
@@ -12601,9 +12751,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -12621,9 +12768,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -12641,9 +12785,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -12661,9 +12802,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -12681,9 +12819,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -12701,9 +12836,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -12840,6 +12972,7 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -13651,6 +13784,7 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -13661,6 +13795,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -13671,6 +13806,13 @@
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
"license": "MIT"
},
+ "node_modules/@types/retry": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz",
+ "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -13789,6 +13931,7 @@
"integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.59.0",
"@typescript-eslint/types": "8.59.0",
@@ -14150,9 +14293,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -14167,9 +14307,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -14184,9 +14321,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -14201,9 +14335,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -14218,9 +14349,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -14235,9 +14363,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -14252,9 +14377,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -14269,9 +14391,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -14699,6 +14818,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -15274,6 +15394,7 @@
"integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==",
"dev": true,
"license": "MPL-2.0",
+ "peer": true,
"engines": {
"node": ">=4"
}
@@ -15686,6 +15807,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -16099,6 +16221,7 @@
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz",
"integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "12.0.0",
"@chevrotain/gast": "12.0.0",
@@ -17024,6 +17147,7 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz",
"integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10"
}
@@ -17445,6 +17569,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
+ "peer": true,
"engines": {
"node": ">=12"
}
@@ -18464,6 +18589,7 @@
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -18901,6 +19027,7 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -19056,6 +19183,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -19383,6 +19511,7 @@
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -19409,6 +19538,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -19705,6 +19835,7 @@
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -20474,6 +20605,19 @@
"node": "^12.20 || >= 14.13"
}
},
+ "node_modules/figma-js": {
+ "version": "1.16.1-0",
+ "resolved": "https://registry.npmjs.org/figma-js/-/figma-js-1.16.1-0.tgz",
+ "integrity": "sha512-4yA1PJOAnFBp8V26nEikzsBz4E6KZtdIGg6EMiqnFr2x5x2ehZy70MadFQOc/wnyPDvorqvN6eH8b7IfmUqIRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=8.9"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -21795,6 +21939,7 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz",
"integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -21968,6 +22113,7 @@
}
],
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"typescript": "^5 || ^6"
},
@@ -23098,6 +23244,7 @@
"integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/core": "30.3.0",
"@jest/types": "30.3.0",
@@ -24715,6 +24862,7 @@
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -24774,6 +24922,7 @@
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssstyle": "^4.0.1",
"data-urls": "^5.0.0",
@@ -25255,9 +25404,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -25278,9 +25424,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -25301,9 +25444,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -25324,9 +25464,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -27243,6 +27380,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz",
"integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@next/env": "16.2.2",
"@swc/helpers": "0.5.15",
@@ -28327,6 +28465,33 @@
"node": ">=8"
}
},
+ "node_modules/p-retry": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.2.tgz",
+ "integrity": "sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.1",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-retry/node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -28821,6 +28986,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -28876,6 +29042,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -29045,6 +29212,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -29068,6 +29236,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -29195,6 +29364,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -29218,6 +29388,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -29316,6 +29487,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -29339,6 +29511,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -29412,6 +29585,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -29435,6 +29609,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -29723,6 +29898,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -29746,6 +29922,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -30200,6 +30377,7 @@
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -30249,6 +30427,7 @@
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
@@ -30565,6 +30744,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -30658,6 +30838,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -31967,6 +32148,7 @@
"integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.1.5",
@@ -32065,6 +32247,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -32429,6 +32612,7 @@
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz",
"integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@shikijs/core": "3.23.0",
"@shikijs/engine-javascript": "3.23.0",
@@ -32537,6 +32721,7 @@
"integrity": "sha512-VnDS2fycANrJFVPQwjaD+h+hkISY7EB3LsPsYWje4lBCjQwwsZLxjwwRwVJKHrcj2ZqyG+DdXykWm9mbZklZrw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"bytes-iec": "^3.1.1",
"lilconfig": "^3.1.3",
@@ -32841,6 +33026,59 @@
"node": ">= 0.8"
}
},
+ "node_modules/stdin-discarder": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
+ "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stdin-discarder/node_modules/bl": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
+ "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^6.0.3",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/stdin-discarder/node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -32860,6 +33098,7 @@
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.3.5.tgz",
"integrity": "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^2.0.1",
@@ -33748,6 +33987,7 @@
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz",
"integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==",
"license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -35879,6 +36119,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -36236,6 +36477,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz",
"integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -36810,6 +37052,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.58.tgz",
"integrity": "sha512-DVLmMQzSZwNYzQoMaM3MQWnxr2eq+AtM9Hx3w1/Yl0pH8sLTSjN4jGP7w6f7uand6Hw44tsnSu1hz1AOA6qI2Q==",
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -36848,7 +37091,7 @@
},
"packages/css": {
"name": "@primer/brand-css",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "UNLICENSED",
"devDependencies": {
"@types/node": "24.12.0",
@@ -36864,7 +37107,7 @@
},
"packages/design-tokens": {
"name": "@primer/brand-primitives",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"devDependencies": {
"@primer/primitives": "9.1.1",
@@ -36878,7 +37121,7 @@
},
"packages/e2e": {
"name": "@primer/brand-e2e",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"devDependencies": {
"@github/axe-github": "^0.8.1",
@@ -36894,7 +37137,7 @@
},
"packages/fonts": {
"name": "@primer/brand-fonts",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"engines": {
"node": ">=24.0.0",
@@ -36903,7 +37146,7 @@
},
"packages/mcp": {
"name": "@primer/brand-mcp",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.24.0",
@@ -36922,7 +37165,7 @@
},
"packages/react": {
"name": "@primer/react-brand",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"dependencies": {
"@oddbird/popover-polyfill": "0.5.2",
@@ -37069,7 +37312,7 @@
},
"packages/repo-configs": {
"name": "@primer/brand-config",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT"
}
}