Compare commits

..

1 Commits

Author SHA1 Message Date
Nolann Biron 010b838d84 test 2026-03-11 14:46:16 +01:00
191 changed files with 1593 additions and 4967 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/embed": minor
---
Support `assistantName` property to override Assistant name
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add backward compatibility for OpenAPI spec content URL
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/openapi-parser": patch
"@gitbook/react-openapi": patch
---
Support x-enable-proxy at operation level
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Improve Assistant form controls
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Redirect sitemap.md and .well-known/sitemap.md to llms.txt.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Improve AI agent detection and readability by outputting 200 for page not found.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Refactor embeddable context to merge local & site properties in one unified way
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Move ~scalar/proxy route to site route group
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Add token verification on OpenAPI proxy
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add support for localized site section titles
+5
View File
@@ -136,6 +136,7 @@ jobs:
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/preview/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-vercel
visual-testing-v2-cloudflare:
@@ -160,6 +161,7 @@ jobs:
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/preview/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-cloudflare
visual-testing-customers-v2:
@@ -184,6 +186,7 @@ jobs:
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/preview/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-vercel
visual-testing-customers-v2-cloudflare:
@@ -208,6 +211,7 @@ jobs:
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/preview/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-cloudflare
browserless-testing-v2-vercel:
@@ -231,6 +235,7 @@ jobs:
env:
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/preview/
# browserless-testing-v2-cloudflare:
# runs-on: ubuntu-latest
# name: Browserless Testing v2 (Cloudflare)
+1 -75
View File
@@ -1,77 +1,3 @@
# AGENTS Instructions
## Commands
```bash
bun install # Install dependencies
bun dev # Start dev server (all packages)
bun run build # Build all packages
bun run format # Format with Biome (run after every change)
bun run typecheck # Type-check all packages
bun run unit # Run unit tests
```
## Development
The dev server proxies published GitBook sites locally. After `bun dev`, access any published site at:
```
http://localhost:3000/url/<published-gitbook-url>
```
Examples:
- `http://localhost:3000/url/gitbook.com/docs`
- `http://localhost:3000/url/open-source.gitbook.io/midjourney`
## Architecture
```
packages/
gitbook/ # Main Next.js app
src/
app/ # Next.js App Router (sites/)
components/ # React components
lib/ # Server utilities, data fetching
intl/ # Internationalization (translations/)
openapi-parser/ # OpenAPI 3.0/3.1/Swagger parser
react-openapi/ # OpenAPI rendering components
react-contentkit/ # ContentKit component rendering
embed/ # Embeddable GitBook components
shared/ # Shared utilities
icons/ # Icon assets
fonts/ # Font assets
colors/ # Color tokens
expr/ # GitBook expression evaluator
cache-do/ # Cloudflare DO cache
cache-tags/ # Cache tag utilities
```
## Testing
```bash
bun run unit # Unit tests via bun test (not vitest)
bun run e2e # Playwright e2e tests (requires built app)
```
Run a specific test file:
```bash
cd packages/gitbook && bun test src/lib/cache.test.ts
```
## Changesets
After committing code changes, create a changeset for the affected package:
```md
---
"gitbook": patch
---
Provide a short description of the change.
```
Save as `.changeset/<name>.md`, then commit it separately with message: `changeset`
## Formatting
Uses Biome (not ESLint/Prettier). Always run `bun run format` before committing.
- After making code changes, run `bun format`.
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+126 -74
View File
@@ -7,8 +7,8 @@
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.30.0",
"turbo": "^2.9.2",
"vercel": "50.37.3",
"turbo": "^2.8.13",
"vercel": "^50.26.1",
},
},
"packages/browser-types": {
@@ -48,7 +48,7 @@
},
"packages/embed": {
"name": "@gitbook/embed",
"version": "0.4.0",
"version": "0.3.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
@@ -109,7 +109,7 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.23.2",
"version": "0.22.2",
"dependencies": {
"@cloudflare/workers-types": "^4.20251011.0",
"@gitbook/api": "catalog:",
@@ -129,7 +129,6 @@
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "^3.8.5",
"@opennextjs/cloudflare": "^1.14.4",
"@panzoom/panzoom": "^4.6.1",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15",
@@ -139,7 +138,6 @@
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tusbar/cache-control": "^1.0.2",
"@vercel/agent-readability": "^0.2.1",
"ai": "^4.2.2",
"assert-never": "catalog:",
"bidc": "catalog:",
@@ -151,7 +149,6 @@
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"jwt-decode": "^4.0.0",
"leven": "^4.1.0",
"mcp-handler": "^1.0.2",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-frontmatter": "^2.0.1",
@@ -162,6 +159,7 @@
"micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24",
"negotiator": "^1.0.0",
"next": "15.4.11",
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
@@ -173,7 +171,6 @@
"react": "catalog:",
"react-dom": "catalog:",
"react-hotkeys-hook": "^4.4.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
@@ -220,19 +217,19 @@
"tailwindcss": "^4.1.11",
"ts-essentials": "^10.0.1",
"typescript": "catalog:",
"vercel": "50.37.3",
"vercel": "^50.15.1",
"wrangler": "^4.43.0",
},
},
"packages/icons": {
"name": "@gitbook/icons",
"version": "0.4.6",
"version": "0.4.3",
"bin": {
"gitbook-icons": "./bin/gitbook-icons.js",
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@gitbook/fontawesome-pro": "1.0.23",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@gitbook/fontawesome-pro": "1.0.16",
},
"devDependencies": {
"@types/react": "catalog:",
@@ -247,7 +244,7 @@
},
"packages/openapi-parser": {
"name": "@gitbook/openapi-parser",
"version": "3.0.11",
"version": "3.0.9",
"dependencies": {
"@scalar/json-magic": "^0.9.4",
"@scalar/openapi-parser": "^0.24.5",
@@ -264,7 +261,7 @@
},
"packages/react-contentkit": {
"name": "@gitbook/react-contentkit",
"version": "0.7.15",
"version": "0.7.12",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:*",
@@ -306,7 +303,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.5.11",
"version": "1.5.9",
"dependencies": {
"@gitbook/expr": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
@@ -346,13 +343,12 @@
"@codemirror/state": "6.4.1",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"axios": "1.8.4",
"esbuild": "0.24.2",
"react": "catalog:",
"react-dom": "catalog:",
},
"catalog": {
"@gitbook/api": "0.175.0",
"@gitbook/api": "0.167.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
@@ -745,11 +741,11 @@
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.5.10", "", { "dependencies": { "tslib": "2" } }, "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q=="],
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.2.0", "", {}, "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw=="],
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.1.0", "", {}, "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA=="],
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
"@gitbook/api": ["@gitbook/api@0.175.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-2xIPSsXV7ensBbPNs8K/pxt8xIbmsbwBx8E1fn3Do5WhubcRcGy7UduqIsyzIAYeA24K32VoNN1i4GnoUAa3HQ=="],
"@gitbook/api": ["@gitbook/api@0.167.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9A9Bsgi2aRbGf3DPejK4YQh9M/+grtNymFwUkK3B3N4m7VOxcUMQ2HaV7s51mtrBdWKcEdeFm4LEnEaAGHcM1A=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
@@ -763,7 +759,7 @@
"@gitbook/expr": ["@gitbook/expr@workspace:packages/expr"],
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.23", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^7.2.0" } }, "sha512-igxgeuZvZIDghZhju8vAV3GjznwOUU2RHMGxGLxwf6QhL4soTJIveD+84C6EGQRieX8O1H6b4TOac3Hhom+Jcg=="],
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.16", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^7.1.0" } }, "sha512-jypE3FpC5U7lealzpGVq1Jie5Ay5jYv/UkOLxPhOpffjCd98A+Ex08M6GyINMDO7KCBaF5p8pbdl693hf0DwNg=="],
"@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
@@ -1013,8 +1009,6 @@
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.111.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QddKW4kBH0Wof6Y65eYCNHM4iOGmCTWLLcNYY1FGswhzmTYOUVXajNROR+iCXAOFnOF0ldtsR79SyqgyHH1Bgg=="],
"@panzoom/panzoom": ["@panzoom/panzoom@4.6.1", "", {}, "sha512-ogf/KhHHjj+DYAvHfaf3TXMQ8OE36pJtKpabLlx1OmpjcgtpCvkUoCiNONA8kuVRPGJdLiqMf0n8LRFXj1OyuA=="],
"@phosphor-icons/core": ["@phosphor-icons/core@2.1.1", "", {}, "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -1581,18 +1575,6 @@
"@tsconfig/strictest": ["@tsconfig/strictest@2.0.6", "", {}, "sha512-tPOhmDhIUcDjvpDDYyiUdssP84Eqm7n5KxJe5J3/g+s6xoDIPAf+SIn06dhw7VkhxIvLOnhDDrX7tsqMHNEhDg=="],
"@turbo/darwin-64": ["@turbo/darwin-64@2.9.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yg0qdthTUuV/K0oJB9t8YGweW1hOefQ+NTiSA6gw/mwAqqifCvAXH/kDkvVj/fFP9+wzoQ4WjyAF5/syDbCIHA=="],
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-x4H9U8cXZob3oqTleKOEdY89OCWwLX3qVRpW0NeZg+CHDXemYD+PhACjwjvh7orIlhmmq2Lk+7iO7b34kphFig=="],
"@turbo/linux-64": ["@turbo/linux-64@2.9.2", "", { "os": "linux", "cpu": "x64" }, "sha512-fxbpor8zvRtaf75sfloNTaBkl487JECSVDrXXMGlwR3+Qmq7yStUE+ccQddPu+nKqYD46QxDspk8KZvT78SBSg=="],
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-ufMQcCQMi8kzHbEA+FihPaRdrPQC1vUO0LQ9oU/LGWlnc+RVyDlvH9l9x0BWoS7HLyHK8hbqLf3O2ml/ZLMEIw=="],
"@turbo/windows-64": ["@turbo/windows-64@2.9.2", "", { "os": "win32", "cpu": "x64" }, "sha512-SOELKum/pjdJWKQb+RU/OEAlNtWJKfjjYUA9QhYxBvIT26SyfWgY+ecZ7QlVWt+xnEdQyC6BkYlWR546KxPO5w=="],
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPFjErR4MrKo71lNfqgUNaajftZN2nyaOsSxMSb2NI+5r/zlB26yjXUKYvADGg4EWvaLTJPIzgZDT9ZX+WN9bw=="],
"@tusbar/cache-control": ["@tusbar/cache-control@1.0.2", "", {}, "sha512-PXfjYTYBVvMPYCLDWj+xIOA9ITFbbhWCHzLcqUCJ5TPGm4JO4cxpGb7x3K8Q1K1ADgNgfBxLsDcTMVRydtZB9A=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
@@ -1719,67 +1701,63 @@
"@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="],
"@vercel/agent-readability": ["@vercel/agent-readability@0.2.1", "", { "peerDependencies": { "next": ">=14" }, "optionalPeers": ["next"], "bin": { "agent-readability": "dist/cli/index.cjs" } }, "sha512-ShT7BzIS/dwKompii8tm5do+NR1g4xL5M3wM7S01xsH6yuYQ7wiTPZEcmHMFLHCsAQg45/mD0hgrufpS3NVunw=="],
"@vercel/backends": ["@vercel/backends@0.0.53", "", { "dependencies": { "@vercel/build-utils": "13.12.0", "@vercel/nft": "1.5.0", "execa": "3.2.0", "fs-extra": "11.1.0", "oxc-transform": "0.111.0", "path-to-regexp": "8.3.0", "resolve.exports": "2.0.3", "rolldown": "1.0.0-rc.1", "srvx": "0.8.9", "tsx": "4.21.0", "zod": "3.22.4" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" } }, "sha512-f8JK9Jatlco6kT/NsLYVR88iqS7oZxUXzXW8TofLmyTT5o+OEXSY2hzAQSjul6r0qz/9yAhtQkMhcHG8Ne01ZQ=="],
"@vercel/backends": ["@vercel/backends@0.0.40", "", { "dependencies": { "@vercel/build-utils": "13.6.2", "@vercel/nft": "1.3.0", "execa": "3.2.0", "fs-extra": "11.1.0", "oxc-transform": "0.111.0", "path-to-regexp": "8.3.0", "resolve.exports": "2.0.3", "rolldown": "1.0.0-rc.1", "srvx": "0.8.9", "tsx": "4.21.0", "zod": "3.22.4" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" } }, "sha512-cYyfPeGrpqyxBgcgKoaZJ+9M7NRKf1eevzKkrX9QRmiBBlGduR34hV859uK4ouBJByHE7n9Tkga2dRLOH8pMcQ=="],
"@vercel/blob": ["@vercel/blob@2.3.0", "", { "dependencies": { "async-retry": "^1.3.3", "is-buffer": "^2.0.5", "is-node-process": "^1.2.0", "throttleit": "^2.1.0", "undici": "^6.23.0" } }, "sha512-oYWiJbWRQ7gz9Mj0X/NHFJ3OcLMOBzq/2b3j6zeNrQmtFo6dHwU8FAwNpxVIYddVMd+g8eqEi7iRueYx8FtM0Q=="],
"@vercel/build-utils": ["@vercel/build-utils@13.12.0", "", { "dependencies": { "@vercel/python-analysis": "0.11.0", "cjs-module-lexer": "1.2.3", "es-module-lexer": "1.5.0" } }, "sha512-GnavaJQPt1tm9qxFdghMsN1Sf2P+Bo3fIG3iEKJBuYTLahWm17AZrH+/Hh1A+R3nRgS3De5BJ2/fDgh0VV2QZA=="],
"@vercel/build-utils": ["@vercel/build-utils@13.6.2", "", { "dependencies": { "@vercel/python-analysis": "0.8.1" } }, "sha512-KiwRUd2x1ZHm73p+/hfdVWZHWXT8r+oN/5ZN1lJyb41AGK4Zhrug6X60jNyVhnw6GjpwBuPTBcys29cCsPjkWQ=="],
"@vercel/cervel": ["@vercel/cervel@0.0.40", "", { "dependencies": { "@vercel/backends": "0.0.53" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" }, "bin": { "cervel": "bin/cervel.mjs" } }, "sha512-5eY17uPXU6Mhn6lh8HIKyOLkhVr826D7G0CeVIujzsMfiGZjWIYc9udbXLGqikgBucvH9PwkxRQuCXI10jBzIw=="],
"@vercel/cervel": ["@vercel/cervel@0.0.27", "", { "dependencies": { "@vercel/backends": "0.0.40" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" }, "bin": { "cervel": "bin/cervel.mjs" } }, "sha512-w6FPgnnZD2nr43pkhXSY0tGMwCJT6kh8f8VoEL97ISSkxywZYX+Bw5FxrNavAEMC0wYm7esEosOdAEAh8Twbag=="],
"@vercel/detect-agent": ["@vercel/detect-agent@1.2.1", "", {}, "sha512-U/BJCltQSTFTHwaiCQQTQG3GonTbRoEewjV+OU2mMjcHLAoPOh6CP1SXA2XNmqiqI3c82nkRNJ7piZ14RqmTXw=="],
"@vercel/detect-agent": ["@vercel/detect-agent@1.1.0", "", {}, "sha512-Zfq6FbIcYl9gaAmVu6ROsqUiCNwpEj3Ljz/tMX5fl12Z95OFOxzf7vlO03WE5JBU/ri1tBDFHnW41dihMINOPQ=="],
"@vercel/elysia": ["@vercel/elysia@0.1.55", "", { "dependencies": { "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "sha512-LKXOglYMdihN4CKMFZsuf9Oa7ymO1YEWQUdey61fe4qcJzywrEJD70ZFO0bNuRtXMn7Gf0iiGXKmPIPBUklH1g=="],
"@vercel/elysia": ["@vercel/elysia@0.1.43", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-Q5XCGVMuO0XF2n/XEEP43d+AIk8Yl09NwJmJ0m9w5Mx0Pyacb1Yo6dcbn6vnsj4j50dpYGeejdaqu77xOKi8mQ=="],
"@vercel/error-utils": ["@vercel/error-utils@2.0.3", "", {}, "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ=="],
"@vercel/express": ["@vercel/express@0.1.65", "", { "dependencies": { "@vercel/cervel": "0.0.40", "@vercel/nft": "1.5.0", "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", "zod": "3.22.4" } }, "sha512-hTCTKv7XZJ+SDOpyPBrneX+Qq7tV1WiWmA2sr2/foMb36M4JYexn4OxcGyXQrLEntAVMPg+tp4dHHL/cW/XUeg=="],
"@vercel/express": ["@vercel/express@0.1.52", "", { "dependencies": { "@vercel/cervel": "0.0.27", "@vercel/nft": "1.1.1", "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", "zod": "3.22.4" } }, "sha512-lvZyw/7IOVMx6YqniijlMrU0ys+bcbqkY+5slGQMoXrtW7Mo2RhF9f27y+1Bl1ELv8JzyrXhOLHu2f5gn4CsgQ=="],
"@vercel/fastify": ["@vercel/fastify@0.1.58", "", { "dependencies": { "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "sha512-QRuhhcPneqOvHEUT5He3gGSEkH7JTqNhSIQG298mLEx2fL8OfVJHLHq/50GHVBhRLzIkFA2lfLQbRuwqRSyDvA=="],
"@vercel/fastify": ["@vercel/fastify@0.1.46", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-R8uSV3SSbFJ39o3VAfsKwlWOPGDfQyNXxJ2dT8Z05GQ4O3qgGTcRd/oEothQZgnJR9fjGzDQ3q7SwWBd12150A=="],
"@vercel/fun": ["@vercel/fun@1.3.0", "", { "dependencies": { "@tootallnate/once": "2.0.0", "async-listen": "1.2.0", "debug": "4.3.4", "generic-pool": "3.4.2", "micro": "9.3.5-canary.3", "ms": "2.1.1", "node-fetch": "2.6.7", "path-to-regexp": "8.2.0", "promisepipe": "3.0.0", "semver": "7.5.4", "stat-mode": "0.3.0", "stream-to-promise": "2.2.0", "tar": "7.5.7", "tinyexec": "0.3.2", "tree-kill": "1.2.2", "uid-promise": "1.0.0", "xdg-app-paths": "5.1.0", "yauzl-promise": "2.1.3" } }, "sha512-8erw9uPe0dFg45THkNxmjtvMX143SkZebmjgSVbcM3XCkXu3RIiBaJMcMNG8aaS+rnTuw8+d4De9HVT0M/r3wg=="],
"@vercel/gatsby-plugin-vercel-analytics": ["@vercel/gatsby-plugin-vercel-analytics@1.0.11", "", { "dependencies": { "web-vitals": "0.2.4" } }, "sha512-iTEA0vY6RBPuEzkwUTVzSHDATo1aF6bdLLspI68mQ/BTbi5UQEGjpjyzdKOVcSYApDtFU6M6vypZ1t4vIEnHvw=="],
"@vercel/gatsby-plugin-vercel-builder": ["@vercel/gatsby-plugin-vercel-builder@2.1.6", "", { "dependencies": { "@sinclair/typebox": "0.25.24", "@vercel/build-utils": "13.12.0", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "sha512-Lh0wbhs7WR/h05iBPqaoxVDY5Ut4/GgLlO15H8TUIonZZcAfHD1gm5owdyLIPwOTGvtz3qoxxsgc0hSMo/zCkw=="],
"@vercel/gatsby-plugin-vercel-builder": ["@vercel/gatsby-plugin-vercel-builder@2.0.142", "", { "dependencies": { "@sinclair/typebox": "0.25.24", "@vercel/build-utils": "13.6.2", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "sha512-F6Mo5fROP57wX/vAngXHA+xBZVwi98vU5YT1J+n/PYxIwbOVCa6SyPOI98o6a+uLQnchqbWbcXcV2CaYmU85VA=="],
"@vercel/go": ["@vercel/go@3.4.6", "", {}, "sha512-ig91qRY+f4lLXWoRvnhEvu5DcT7LXtALBo/jJqxvlyOsHRBP4mdAvUgg9sygu2NOeMSV/cy249yJqQniP32HWw=="],
"@vercel/go": ["@vercel/go@3.4.3", "", {}, "sha512-LDT4wpx7SW2UJHd3rOL4+2m2V4w7a5zjyuU0pu0yDBiuxp9bEh7QuAH5qZC7pINGLC3vGftiFVzynd5N454sVA=="],
"@vercel/h3": ["@vercel/h3@0.1.64", "", { "dependencies": { "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "sha512-zH57M8boAs+YKbT0gtnnq4aBYNKjpXwd1p7h8PlnH3MrQmAAapwb9SCAndRLu8STJSUCsYbT/JLk66rWLTMXNQ=="],
"@vercel/h3": ["@vercel/h3@0.1.52", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-bk8id7w9evxD+JeOUJt6qfDohGwJFBVh7N1qh1OG/nngzzb/Ojg+poH5EGpYwYiKymPK7NMgQl0fb0ZjhIU4bg=="],
"@vercel/hono": ["@vercel/hono@0.2.58", "", { "dependencies": { "@vercel/nft": "1.5.0", "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", "zod": "3.22.4" } }, "sha512-yOxq0aGAi5SxFagN/uKgksSv1eNj3NqosZdbhzwd3fRVttGFBloaGuDjNFG1hq/J5O6gm/tRYrLt99amIkINjQ=="],
"@vercel/hono": ["@vercel/hono@0.2.46", "", { "dependencies": { "@vercel/nft": "1.1.1", "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", "zod": "3.22.4" } }, "sha512-rg+6PnvIV20lUM3r2fcjJ/1g1z3ND6h3EqlOQytwCuwpP/jB88TUy+guutB4rDpylPvHzleZqjgi2rE9yH4URg=="],
"@vercel/hydrogen": ["@vercel/hydrogen@1.3.6", "", { "dependencies": { "@vercel/static-config": "3.2.0", "ts-morph": "12.0.0" } }, "sha512-Ec8dKEjGIM4BfThcRLtQs5zaJ4+iJbgLZwkytwi7Blk8VrK6W2F1dtLDmVQYZdVnQcnmHmTx8mxUuMkfP06Mnw=="],
"@vercel/hydrogen": ["@vercel/hydrogen@1.3.5", "", { "dependencies": { "@vercel/static-config": "3.1.2", "ts-morph": "12.0.0" } }, "sha512-7EE6yVKcCnjMb1io9y069GkLyGyIzRbW3Krm3Q7EEfJ3P46h9xe9v/O5UhBoPrwtqDUHxmDngZp9YyfgY8IITA=="],
"@vercel/koa": ["@vercel/koa@0.1.38", "", { "dependencies": { "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "sha512-Xtnw41TPf84l/tL7eg4ktciBdAJoNZBDZ4lVCZL4/RCVxL4cTr0moC+yRiJ6nC7/eT3leml6ni1eOvG2479kBw=="],
"@vercel/koa": ["@vercel/koa@0.1.26", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-TzH4xovhGbr2kmZyvMraaJ3A28qRdo8SxkBO+LIb3ahM739VyTAkH3EYR5CPaemkRv00+nTyBxJJxfnhP1n2MA=="],
"@vercel/nestjs": ["@vercel/nestjs@0.2.59", "", { "dependencies": { "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "sha512-0LbEeFqMSUPU/2nd1alZYkEpDNq+4WU1G00ROhKAXbfap/s6ChcbBKG9Xi86jUTRJ4ntoIf6PGzlhY03bjlwcA=="],
"@vercel/nestjs": ["@vercel/nestjs@0.2.47", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-/yIbLc5rv8GQ+lsJ45JkObcNiMe5yUgXx2d8wryW1SajPV1/iP+wFRK28/Q+6zdvapcgqhFRwInZh+61um/ZWg=="],
"@vercel/next": ["@vercel/next@4.16.3", "", { "dependencies": { "@vercel/nft": "1.5.0" } }, "sha512-yVLrMxMI+Taq47C94lWVUf981WerJ+COrJbgltHcMY8HDdXdgthYe06+na3dni31AL6BYShS8VLpE54QsAdM9Q=="],
"@vercel/next": ["@vercel/next@4.15.39", "", { "dependencies": { "@vercel/nft": "1.1.1" } }, "sha512-E3Wnn5Wfxa7e2RvguE4YNO8OBoOVZhnRXDRfn5jJ9ct+GVRZDgPS9VdXK3eQazMJUCLmQCKdyTca/I57ImPNpQ=="],
"@vercel/nft": ["@vercel/nft@1.5.0", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA=="],
"@vercel/nft": ["@vercel/nft@1.3.0", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-i4EYGkCsIjzu4vorDUbqglZc5eFtQI2syHb++9ZUDm6TU4edVywGpVnYDein35x9sevONOn9/UabfQXuNXtuzQ=="],
"@vercel/node": ["@vercel/node@5.6.22", "", { "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", "@vercel/build-utils": "13.12.0", "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.5.0", "@vercel/static-config": "3.2.0", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", "es-module-lexer": "1.4.1", "esbuild": "0.27.0", "etag": "1.8.1", "mime-types": "2.1.35", "node-fetch": "2.6.9", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0", "tsx": "4.21.0", "typescript": "npm:typescript@5.9.3", "undici": "5.28.4" } }, "sha512-Lu6ToUYz8oMdYwN+6OR9KCh2MbkAloBu5YFKlEmIc51Je9Q21W/kqBneA3ZuWlMMpYHstz1E19GL+tqHb7yFdw=="],
"@vercel/node": ["@vercel/node@5.6.10", "", { "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", "@vercel/build-utils": "13.6.2", "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@vercel/static-config": "3.1.2", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", "es-module-lexer": "1.4.1", "esbuild": "0.27.0", "etag": "1.8.1", "mime-types": "2.1.35", "node-fetch": "2.6.9", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0", "tsx": "4.21.0", "typescript": "npm:typescript@5.9.3", "undici": "5.28.4" } }, "sha512-HEydd6y0KPjCyI9sM3n2/XCjS/fZV/7sO7jFjia9I5XbEJ6LCwkI6rR+GQB751NL0He0vFusy+mm394hJC/Rqg=="],
"@vercel/prepare-flags-definitions": ["@vercel/prepare-flags-definitions@0.2.1", "", {}, "sha512-ouXTsqn7I9xZ1KKezgvn/w3tZeQHL/tc52j9GHiOYi6kT8xgdbT8s2x8C9BQr44iceX0hfhtZwk9q7NuI2Tqbw=="],
"@vercel/python": ["@vercel/python@6.20.1", "", { "dependencies": { "@vercel/python-analysis": "0.8.1" } }, "sha512-a/VgUJ/N7SfKgE2fen6iaNow/nWEGEQj57SNyPwo4emvPfO1dPxUlZWyAEdctfXYKjnJR2WAU2kYcCDw+zenZg=="],
"@vercel/python": ["@vercel/python@6.28.0", "", { "dependencies": { "@vercel/python-analysis": "0.11.0" } }, "sha512-/Ley7HPz/AXhxO7unK5+4hEtsMUxXa02SJ8Tiaz//nEtRQMUcVREIyAUkOOfecjiCpg2hMHSVyCtABFjhzAbgA=="],
"@vercel/python-analysis": ["@vercel/python-analysis@0.8.1", "", { "dependencies": { "@bytecodealliance/preview2-shim": "0.17.6", "@renovatebot/pep440": "4.2.1", "fs-extra": "11.1.1", "js-yaml": "4.1.1", "minimatch": "10.1.1", "pip-requirements-js": "1.0.2", "smol-toml": "1.5.2", "zod": "3.22.4" } }, "sha512-gW1pZDqJaTcjZYPvNhXXLOPgLu6vJW9PKweJoX2f8EKAoW+JIiYncl8AddcSlngNhQRG7SqUl2u3qosZM4kUBA=="],
"@vercel/python-analysis": ["@vercel/python-analysis@0.11.0", "", { "dependencies": { "@bytecodealliance/preview2-shim": "0.17.6", "@renovatebot/pep440": "4.2.1", "fs-extra": "11.1.1", "js-yaml": "4.1.1", "minimatch": "10.1.1", "smol-toml": "1.5.2", "zod": "3.22.4" } }, "sha512-gsoj+nscmNm0xDh+tRhECRhit2VlAVaD7jc9h93sN6rDEBDxPo7eLEgIJFzVDaAItxERZ9Od2IK/04fB9vFy+g=="],
"@vercel/redwood": ["@vercel/redwood@2.4.9", "", { "dependencies": { "@vercel/nft": "1.1.1", "@vercel/static-config": "3.1.2", "semver": "6.3.1", "ts-morph": "12.0.0" } }, "sha512-U7bYIuWfMEFMIcKKbX7lTT8pFNjig9Q3vLeCYRYQUrKVP8xLoUBXSEfW3ijtWJBUV8GmbZCDI30A16uUfNhN+g=="],
"@vercel/redwood": ["@vercel/redwood@2.4.12", "", { "dependencies": { "@vercel/nft": "1.5.0", "@vercel/static-config": "3.2.0", "semver": "6.3.1", "ts-morph": "12.0.0" } }, "sha512-8kJ7eEerI4iMpKVRxQCsnxiIwRVsWtyirEbYb4erCqqsJymTu/xrjhsfAUuzeP8qkuYGP82MJVK+hpKlFtsjGw=="],
"@vercel/remix-builder": ["@vercel/remix-builder@5.7.2", "", { "dependencies": { "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.5.0", "@vercel/static-config": "3.2.0", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0" } }, "sha512-bfStsDBQramYbWugelfyp1szTuDOLQ+ZELvgA9cpYc4FucgCrDy/bpvMMvsjiDACB9PoDzLrG//ZBgsic9yAhg=="],
"@vercel/remix-builder": ["@vercel/remix-builder@5.6.0", "", { "dependencies": { "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@vercel/static-config": "3.1.2", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0" } }, "sha512-neTpO4aGksYcPJjTbAEUhWmsOdFqgx02H47RUsXBKHdMTW4ZKrL9oAKT3pD+Bv9kUgj7uRzqAr8JeiPILPWybg=="],
"@vercel/ruby": ["@vercel/ruby@2.3.2", "", {}, "sha512-okIgMmPEePyDR9TZYaKM4oftcxVHM5Dbdl7V/tIdh3lq8MGLi7HR5vvQglmZUwZOeovE6MVtezxl960EOzeIiQ=="],
"@vercel/rust": ["@vercel/rust@1.0.5", "", { "dependencies": { "@iarna/toml": "^2.2.5", "execa": "5" } }, "sha512-Y03g59nv1uT6Da+PvB/50WqJSHlaFZ9MSkG00R82dUcTySslMbQdOeaXymZtabrmU8zQYhWDb1/CwBki8sWnaQ=="],
"@vercel/static-build": ["@vercel/static-build@2.9.6", "", { "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", "@vercel/gatsby-plugin-vercel-builder": "2.1.6", "@vercel/static-config": "3.2.0", "ts-morph": "12.0.0" } }, "sha512-XBUHUuYNfRi2JVPJF1lx+etTQhfVFVzAW3e8/5Aw/kKtfT1fDLc+uR/fadWlCC+ezXUcyV/h25O5nAk3g3vBaA=="],
"@vercel/static-build": ["@vercel/static-build@2.8.44", "", { "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", "@vercel/gatsby-plugin-vercel-builder": "2.0.142", "@vercel/static-config": "3.1.2", "ts-morph": "12.0.0" } }, "sha512-xzRBFm+tgVoyE/qSF+BeKXKckLVYXR2dGHsp+fNRttLXi7fq2AUCTM7DNNT4TkkTxcPX5FaJ1tRHxlqLizv91A=="],
"@vercel/static-config": ["@vercel/static-config@3.2.0", "", { "dependencies": { "ajv": "8.6.3", "json-schema-to-ts": "1.6.4", "ts-morph": "12.0.0" } }, "sha512-UpOEIgWxWx0M+mDe1IMdHS6JuWM/L5nNIJ4ixX8v9JgBAejymo88OkgnmfLCNMem0Wd+b5vcQPWLdZybCndlsA=="],
"@vercel/static-config": ["@vercel/static-config@3.1.2", "", { "dependencies": { "ajv": "8.6.3", "json-schema-to-ts": "1.6.4", "ts-morph": "12.0.0" } }, "sha512-2d+TXr6K30w86a+WbMbGm2W91O0UzO5VeemZYBBUJbCjk/5FLLGIi8aV6RS2+WmaRvtcqNTn2pUA7nCOK3bGcQ=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.21", "", { "dependencies": { "@babel/parser": "^7.28.3", "@vue/shared": "3.5.21", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw=="],
@@ -1881,6 +1859,8 @@
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
"axios": ["axios@1.8.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@2.0.0", "", {}, "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA=="],
@@ -2221,7 +2201,7 @@
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-module-lexer": ["es-module-lexer@1.5.0", "", {}, "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw=="],
"es-module-lexer": ["es-module-lexer@1.4.1", "", {}, "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
@@ -2329,6 +2309,8 @@
"focus-trap": ["focus-trap@7.6.1", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-nB8y4nQl8PshahLpGKZOq1sb0xrMVFSn6at7u/qOsBZTlZRzaapISGENcB6mOkoezbClZyiMwEF/dGY8AZ00rA=="],
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
@@ -2615,7 +2597,7 @@
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
"leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="],
"leven": ["leven@4.0.0", "", {}, "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
@@ -2885,6 +2867,8 @@
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"ohm-js": ["ohm-js@17.4.0", "", {}, "sha512-WC3eMImUWrXd95iZQ84odjxEBbXgm4yFs1louQRrRbOpCWtG+RDmSQ8hu3sDucflwTnc1jBma99DrP7BegnAng=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
@@ -2975,6 +2959,8 @@
"pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="],
"pip-requirements-js": ["pip-requirements-js@1.0.2", "", { "dependencies": { "ohm-js": "^17.1.0" } }, "sha512-awqoNOSOl4Blu4E4Hzp7jL0g8WKEhCwO+s7C2ibtIW3CAJMwspgoTXd4vnHd21UmhdrsI44Pn8FFSuA8QKrzvg=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="],
@@ -3341,7 +3327,19 @@
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"turbo": ["turbo@2.9.2", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.2", "@turbo/darwin-arm64": "2.9.2", "@turbo/linux-64": "2.9.2", "@turbo/linux-arm64": "2.9.2", "@turbo/windows-64": "2.9.2", "@turbo/windows-arm64": "2.9.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-DUUWysctg/vax8W05UJcyrHm4LvxtX0umAT4bZB5lab4inNXTbwoYSFpdgqzBxON23j8ls3xGxvx0kwQz6Q8wQ=="],
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="],
"turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="],
"turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="],
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
@@ -3409,7 +3407,7 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vercel": ["vercel@50.37.3", "", { "dependencies": { "@vercel/backends": "0.0.53", "@vercel/blob": "2.3.0", "@vercel/build-utils": "13.12.0", "@vercel/detect-agent": "1.2.1", "@vercel/elysia": "0.1.55", "@vercel/express": "0.1.65", "@vercel/fastify": "0.1.58", "@vercel/fun": "1.3.0", "@vercel/go": "3.4.6", "@vercel/h3": "0.1.64", "@vercel/hono": "0.2.58", "@vercel/hydrogen": "1.3.6", "@vercel/koa": "0.1.38", "@vercel/nestjs": "0.2.59", "@vercel/next": "4.16.3", "@vercel/node": "5.6.22", "@vercel/prepare-flags-definitions": "0.2.1", "@vercel/python": "6.28.0", "@vercel/redwood": "2.4.12", "@vercel/remix-builder": "5.7.2", "@vercel/ruby": "2.3.2", "@vercel/rust": "1.0.5", "@vercel/static-build": "2.9.6", "chokidar": "4.0.0", "esbuild": "0.27.0", "form-data": "^4.0.0", "jose": "5.9.6", "luxon": "^3.4.0", "proxy-agent": "6.4.0" }, "bin": { "vc": "dist/vc.js", "vercel": "dist/vc.js" } }, "sha512-ARHoIjLrCsdrXx3uJ3m7CifCE7NizI2mLyQOxH4e5KM4+FruZuTmhzhiMx2rPtJkffkpeH64YimciibwX0YcOg=="],
"vercel": ["vercel@50.26.1", "", { "dependencies": { "@vercel/backends": "0.0.40", "@vercel/blob": "2.3.0", "@vercel/build-utils": "13.6.2", "@vercel/detect-agent": "1.1.0", "@vercel/elysia": "0.1.43", "@vercel/express": "0.1.52", "@vercel/fastify": "0.1.46", "@vercel/fun": "1.3.0", "@vercel/go": "3.4.3", "@vercel/h3": "0.1.52", "@vercel/hono": "0.2.46", "@vercel/hydrogen": "1.3.5", "@vercel/koa": "0.1.26", "@vercel/nestjs": "0.2.47", "@vercel/next": "4.15.39", "@vercel/node": "5.6.10", "@vercel/python": "6.20.1", "@vercel/redwood": "2.4.9", "@vercel/remix-builder": "5.6.0", "@vercel/ruby": "2.3.2", "@vercel/rust": "1.0.5", "@vercel/static-build": "2.8.44", "chokidar": "4.0.0", "esbuild": "0.27.0", "form-data": "^4.0.0", "jose": "5.9.6", "luxon": "^3.4.0", "proxy-agent": "6.4.0" }, "bin": { "vc": "dist/vc.js", "vercel": "dist/vc.js" } }, "sha512-dFIsEkqTZYBNIZwe5cprBDe1a1AoWQXtBsk+/otAjuVk9WJ3x6kA9fE4rvEWAsnpk2jYj2ZbY04xjBt2cxGt5w=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@@ -4363,8 +4361,6 @@
"@scalar/object-utils/@scalar/helpers": ["@scalar/helpers@0.1.1", "", {}, "sha512-eJjuCI5djqU0adTwrHvpDf0xuwNRpwZinCfJ03QjnmIFBM9Ch0u3tn/0EZOhcVNbdEyJ+3yvSVy1dCmmvgtpvQ=="],
"@scalar/openapi-parser/leven": ["leven@4.0.0", "", {}, "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw=="],
"@scalar/openapi-types/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
"@scalar/postman-to-openapi/@scalar/helpers": ["@scalar/helpers@0.0.12", "", {}, "sha512-4NDmHShyi1hrVRsJCdRZT/FIpy+/5PFbVbQLRYX/pjpu5cYqHBj9s6n5RI6gGDXEBHAIFi63g9FC6Isgr66l1Q=="],
@@ -4499,6 +4495,8 @@
"@vercel/backends/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="],
"@vercel/express/@vercel/nft": ["@vercel/nft@1.1.1", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA=="],
"@vercel/express/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="],
"@vercel/express/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
@@ -4517,19 +4515,25 @@
"@vercel/gatsby-plugin-vercel-builder/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="],
"@vercel/hono/@vercel/nft": ["@vercel/nft@1.1.1", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA=="],
"@vercel/hono/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="],
"@vercel/hono/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"@vercel/hono/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="],
"@vercel/next/@vercel/nft": ["@vercel/nft@1.1.1", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA=="],
"@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"@vercel/nft/glob": ["glob@13.0.2", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ=="],
"@vercel/node/@types/node": ["@types/node@20.11.0", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ=="],
"@vercel/node/async-listen": ["async-listen@3.0.0", "", {}, "sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg=="],
"@vercel/node/@vercel/nft": ["@vercel/nft@1.1.1", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA=="],
"@vercel/node/es-module-lexer": ["es-module-lexer@1.4.1", "", {}, "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="],
"@vercel/node/async-listen": ["async-listen@3.0.0", "", {}, "sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg=="],
"@vercel/node/node-fetch": ["node-fetch@2.6.9", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg=="],
@@ -4541,8 +4545,12 @@
"@vercel/python-analysis/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="],
"@vercel/redwood/@vercel/nft": ["@vercel/nft@1.1.1", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA=="],
"@vercel/redwood/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@vercel/remix-builder/@vercel/nft": ["@vercel/nft@1.1.1", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA=="],
"@vercel/remix-builder/path-to-regexp": ["path-to-regexp@6.1.0", "", {}, "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="],
"@vercel/rust/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
@@ -5423,8 +5431,6 @@
"@scalar/api-client/@scalar/openapi-parser/@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.1.3", "", { "dependencies": { "@scalar/openapi-types": "0.5.0" } }, "sha512-iROhcgy3vge6zsviPtoTLHale0nYt1PLhuMmJweQwJ0U23ZYyYhV5xgHtAd0OBEXuqT6rjYbJFvKOJZmJOwpNQ=="],
"@scalar/api-client/@scalar/openapi-parser/leven": ["leven@4.0.0", "", {}, "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw=="],
"@scalar/api-client/@scalar/workspace-store/@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.1.3", "", { "dependencies": { "@scalar/openapi-types": "0.5.0" } }, "sha512-iROhcgy3vge6zsviPtoTLHale0nYt1PLhuMmJweQwJ0U23ZYyYhV5xgHtAd0OBEXuqT6rjYbJFvKOJZmJOwpNQ=="],
"@scalar/components/@scalar/oas-utils/@scalar/json-magic": ["@scalar/json-magic@0.6.1", "", { "dependencies": { "@scalar/helpers": "0.0.12", "yaml": "2.8.0" } }, "sha512-HJMPY5dUU3EXVS4EkjAFXo+uCrby/YFu/gljKDQnhYWRy5zQ0sJWrOEDcHS8nLoJRCIRD5tiVpCxnUnSb6OoAQ=="],
@@ -5459,8 +5465,6 @@
"@scalar/import/@scalar/openapi-parser/@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.1.3", "", { "dependencies": { "@scalar/openapi-types": "0.5.0" } }, "sha512-iROhcgy3vge6zsviPtoTLHale0nYt1PLhuMmJweQwJ0U23ZYyYhV5xgHtAd0OBEXuqT6rjYbJFvKOJZmJOwpNQ=="],
"@scalar/import/@scalar/openapi-parser/leven": ["leven@4.0.0", "", {}, "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw=="],
"@scalar/postman-to-openapi/@scalar/oas-utils/@scalar/json-magic": ["@scalar/json-magic@0.6.1", "", { "dependencies": { "@scalar/helpers": "0.0.12", "yaml": "2.8.0" } }, "sha512-HJMPY5dUU3EXVS4EkjAFXo+uCrby/YFu/gljKDQnhYWRy5zQ0sJWrOEDcHS8nLoJRCIRD5tiVpCxnUnSb6OoAQ=="],
"@scalar/postman-to-openapi/@scalar/oas-utils/@scalar/object-utils": ["@scalar/object-utils@1.2.8", "", { "dependencies": { "@scalar/helpers": "0.0.12", "flatted": "^3.3.3", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1", "type-fest": "5.0.0" } }, "sha512-FjPxEg7Hw0tz2iTKvi+gYt+luZK0TqhX50hUIBuaYa/Ja/OMuKLp9QHhB5U68F1L55CZNP4vwoNNfXeYWnVEZg=="],
@@ -5571,6 +5575,10 @@
"@vercel/backends/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.1", "", {}, "sha512-UTBjtTxVOhodhzFVp/ayITaTETRHPUPYZPXQe0WU0wOgxghMojXxYjOiPOauKIYNWJAWS2fd7gJgGQK8GU8vDA=="],
"@vercel/express/@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"@vercel/express/@vercel/nft/glob": ["glob@13.0.2", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ=="],
"@vercel/express/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"@vercel/express/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
@@ -5583,18 +5591,38 @@
"@vercel/gatsby-plugin-vercel-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"@vercel/hono/@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"@vercel/hono/@vercel/nft/glob": ["glob@13.0.2", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ=="],
"@vercel/hono/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"@vercel/hono/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"@vercel/next/@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"@vercel/next/@vercel/nft/glob": ["glob@13.0.2", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ=="],
"@vercel/nft/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
"@vercel/node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@vercel/node/@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"@vercel/node/@vercel/nft/glob": ["glob@13.0.2", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ=="],
"@vercel/python-analysis/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"@vercel/python-analysis/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"@vercel/redwood/@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"@vercel/redwood/@vercel/nft/glob": ["glob@13.0.2", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ=="],
"@vercel/remix-builder/@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"@vercel/remix-builder/@vercel/nft/glob": ["glob@13.0.2", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ=="],
"@vercel/rust/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"@vercel/rust/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
@@ -5947,10 +5975,22 @@
"@vercel/backends/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@vercel/express/@vercel/nft/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
"@vercel/fun/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"@vercel/hono/@vercel/nft/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
"@vercel/next/@vercel/nft/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
"@vercel/nft/glob/minimatch/@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
"@vercel/node/@vercel/nft/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
"@vercel/redwood/@vercel/nft/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
"@vercel/remix-builder/@vercel/nft/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
"@zenuml/core/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@zenuml/core/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
@@ -6029,6 +6069,18 @@
"@vercel/backends/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@vercel/express/@vercel/nft/glob/minimatch/@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
"@vercel/hono/@vercel/nft/glob/minimatch/@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
"@vercel/next/@vercel/nft/glob/minimatch/@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
"@vercel/node/@vercel/nft/glob/minimatch/@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
"@vercel/redwood/@vercel/nft/glob/minimatch/@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
"@vercel/remix-builder/@vercel/nft/glob/minimatch/@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
"@zenuml/core/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="],
+4 -5
View File
@@ -7,8 +7,8 @@
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.30.0",
"turbo": "^2.9.2",
"vercel": "50.37.3"
"turbo": "^2.8.13",
"vercel": "^50.26.1"
},
"packageManager": "bun@1.3.7",
"overrides": {
@@ -17,8 +17,7 @@
"@types/react-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"esbuild": "0.24.2",
"axios": "1.8.4"
"esbuild": "0.24.2"
},
"private": true,
"scripts": {
@@ -42,7 +41,7 @@
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.175.0",
"@gitbook/api": "0.167.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/embed
## 0.4.0
### Minor Changes
- b3875a1: Support `assistantName` property to override Assistant name
## 0.3.0
### Minor Changes
-38
View File
@@ -285,44 +285,6 @@ Display GitBook branding in the embed. Defaults to true.
trademark: true
```
### Theming and `color-scheme` (CSS-first)
The embed supports both site-controlled theming and CSS-driven theming.
Precedence (highest → lowest):
- **Site mode / forced theme**: if the GitBook site does not support multiple themes, the embed is forced to the site’s default theme.
- **Visitor preference**: if the site supports multiple themes and the visitor has previously selected a theme on the site, that preference is remembered.
- **Browser/OS default**: otherwise the embed follows the browser/OS preference (`prefers-color-scheme`).
You can also drive the embed theme from CSS by setting `color-scheme` on the iframe element (or a parent it inherits from). When the iframe resolves to an explicit `color-scheme: light` or `color-scheme: dark`, that value is propagated into the embedded content so it renders consistently.
Standalone widget example:
```css
/* Force the GitBook widget iframe to render in dark mode */
#gitbook-widget-iframe {
color-scheme: dark;
}
```
### `colorScheme`
Available in: Standalone script, NPM package, React components
Force the embed to render in a specific color scheme.
- **Type**: `'light' | 'dark'`
- **Default**: `undefined` (follow site/visitor preference/system)
```javascript
GitBook('configure', {
colorScheme: 'dark'
});
```
To clear an override, omit `colorScheme` (or set it to `undefined` in JS) in a subsequent `configure` call.
### `actions`
Available in: Standalone script, NPM package, React components
+2 -2
View File
@@ -13,7 +13,7 @@
}
},
"sideEffects": false,
"version": "0.4.0",
"version": "0.3.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
@@ -32,7 +32,7 @@
"scripts": {
"build": "bun run build-lib && bun run build-standalone",
"build-lib": "tsdown",
"build-standalone": "bun run ./scripts/build-standalone.ts",
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
"clean": "rm -rf ./dist",
"unit": "bun test",
"typecheck": "tsc --noEmit",
@@ -1,36 +0,0 @@
import { spawn } from 'node:child_process';
import { readFile, writeFile } from 'node:fs/promises';
import { Features, transform } from 'lightningcss';
/**
* Build the standalone embed script.
* Bun's default CSS transpiler (which is a port of LightningCSS) strips out the native light-dark() function in favour of a polyfill.
* Light-dark() is widely supported now, and the polyfill requires you to set a data attribute on the element instead of relying on plain CSS.
* This script's purpose is to pass a feature flag to the CSS transpiler to keep the native light-dark() behavior.
*/
await new Promise<void>((resolve, reject) => {
const child = spawn(
'bun',
['build', 'src/standalone/index.ts', '--bundle', '--minify', '--outdir=standalone'],
{ stdio: 'inherit' }
);
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`bun build failed with exit code ${code ?? 'unknown'}`));
});
});
const sourceCSS = await readFile('src/standalone/style.css');
const transformedCSS = transform({
filename: 'src/standalone/style.css',
code: sourceCSS,
minify: true,
// Keep native light-dark() behavior scoped to element color-scheme.
exclude: Features.LightDark,
});
await writeFile('standalone/index.css', transformedCSS.code);
-5
View File
@@ -83,11 +83,6 @@ export type GitBookEmbeddableConfiguration = {
* Display a close button inside the assistant.
*/
closeButton?: boolean;
/**
* Force the embed to render in a specific color-scheme.
*/
colorScheme?: 'light' | 'dark';
};
/**
+1 -45
View File
@@ -55,7 +55,6 @@ let widgetIframe: HTMLIFrameElement | undefined;
let _client: GitBookClient | undefined;
let _frame: GitBookFrameClient | undefined;
let frameOptions: GetFrameURLOptions | undefined;
let lastPushedColorScheme: 'light' | 'dark' | undefined;
let frameConfiguration: GitBookEmbeddableConfiguration & StandaloneConfiguration = {
button: {
label: 'Ask',
@@ -86,43 +85,6 @@ widgetWindow.classList.add('hidden');
document.body.appendChild(widgetButton);
document.body.appendChild(widgetWindow);
/** Resolved `color-scheme` from the iframe element (incl. inheritance from `#gitbook-widget-window`). */
function colorSchemeFromIframe(): 'light' | 'dark' | undefined {
if (!widgetIframe) return undefined;
const v = getComputedStyle(widgetIframe).colorScheme.trim().toLowerCase();
return v === 'dark' || v === 'light' ? v : undefined;
}
function pushColorSchemeToFrame() {
if (!_frame) return;
const desired = frameConfiguration.colorScheme ?? colorSchemeFromIframe();
if (desired === lastPushedColorScheme) return;
lastPushedColorScheme = desired;
_frame.configure({
...frameConfiguration,
colorScheme: desired,
});
}
/** Re-push when the host page or widget chrome changes theme (class/style) or OS preference changes. */
function installHostThemeBridge() {
const onChange = () => pushColorSchemeToFrame();
window.matchMedia?.('(prefers-color-scheme: dark)')?.addEventListener?.('change', onChange);
if (typeof MutationObserver === 'undefined') return;
const observer = new MutationObserver(onChange);
const opts: MutationObserverInit = {
attributes: true,
attributeFilter: ['class', 'style'],
};
observer.observe(document.documentElement, opts);
// Safari: `color-scheme` on `#gitbook-widget-window` does not always surface on `<html>`.
observer.observe(widgetWindow, opts);
}
installHostThemeBridge();
function getClient() {
if (!_client) {
throw new Error(
@@ -137,7 +99,6 @@ function getIframe() {
const client = getClient();
widgetIframe?.remove();
lastPushedColorScheme = undefined;
widgetIframe = document.createElement('iframe');
widgetIframe.id = 'gitbook-widget-iframe';
widgetIframe.src = client.getFrameURL({
@@ -150,8 +111,6 @@ function getIframe() {
widgetWindow.classList.add('hidden');
widgetButton.classList.remove('open');
});
pushColorSchemeToFrame();
}
return { iframe: widgetIframe, frame: _frame };
}
@@ -229,12 +188,9 @@ const GitBook = (...args: StandaloneCalls) => {
}
}
const { frame } = getIframe();
// Always propagate configuration updates, even when color-scheme doesn't change.
frame.configure({
getIframe().frame.configure({
...frameConfiguration,
});
pushColorSchemeToFrame();
break;
}
case 'clearChat':
+15 -14
View File
@@ -8,6 +8,14 @@
--gitbook-widget-radius: .5rem;
--gitbook-widget-text-size: 1rem;
--gitbook-widget-text-color: #656973;
--gitbook-widget-border-color: #e5e5e5;
--gitbook-widget-background-translucent: rgba(255, 255, 255, 0.9);
--gitbook-widget-background-translucent-hover: rgba(250, 250, 250, 0.9);
--gitbook-widget-background-solid: #FFFFFF;
--gitbook-widget-background-solid-hover: #FBFBFB;
--gitbook-widget-icon-size: 1.25rem;
--gitbook-widget-window-width: 28rem; /* 448px */
@@ -21,20 +29,13 @@
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
#gitbook-widget-button,
#gitbook-widget-window {
--gitbook-widget-text-color: light-dark(#656973, #FFFFFF);
--gitbook-widget-border-color: light-dark(#e5e5e5, #202020);
--gitbook-widget-background-translucent: light-dark(rgba(255, 255, 255, 0.9), rgba(15, 15, 15, 0.9));
--gitbook-widget-background-translucent-hover: light-dark(rgba(250, 250, 250, 0.9), rgba(20, 20, 20, 0.9));
--gitbook-widget-background-solid: light-dark(#FFFFFF, #f0f0f0);
--gitbook-widget-background-solid-hover: light-dark(#FBFBFB, #f0f0f0);
&[data-color-scheme=light] {
color-scheme: light;
}
&[data-color-scheme=dark] {
color-scheme: dark;
@media (prefers-color-scheme: dark) {
:root {
--gitbook-widget-text-color: #FFFFFF;
--gitbook-widget-border-color: #202020;
--gitbook-widget-background-translucent: rgba(15, 15, 15, 0.9);
--gitbook-widget-background-translucent-hover: rgba(20, 20, 20, 0.9);
--gitbook-widget-background-solid: #f0f0f0;
}
}
-70
View File
@@ -1,75 +1,5 @@
# gitbook
## 0.23.2
### Patch Changes
- Updated dependencies [ab17ff4]
- @gitbook/icons@0.4.6
- @gitbook/react-contentkit@0.7.15
## 0.23.1
### Patch Changes
- Updated dependencies [3c141f0]
- @gitbook/icons@0.4.5
- @gitbook/react-contentkit@0.7.14
## 0.23.0
### Minor Changes
- 8e5a2aa: Pan and zoom controls for mermaid diagrams
### Patch Changes
- b77c4fc: Add getPage MCP tool
- 31e77d4: Track new events for site analytics.
- f884477: Associate questions asked in the embeddable assistant to the proper channel.
- deabce9: Sort results by score to mix records and pages.
- 4909044: Add Claude Code and Codex connect actions
- da648ca: Fix regressions in Assistant UX
- 3ce7e52: Fix Variant dropdown not showing in GitBook embed
- 7f4e644: Small OpenAPI Improvements
- 4787b7e: Use localized titles/descriptions for site entities
- 7ab6351: Import site redirect patterns from @gitbook/api
- b40465e: Fix normalization of URL causing invalid redirections.
- b5d6f2e: Fix translation quality across 11 locales
- 3151864: Track events from the proper server URL.
- 16bfafe: Add option to sticky header row in tables
- 7c2823c: Fix full width header alignment
- c4a2604: Fix PDF export crash when page contains an OpenAPI block
- c56b569: Support expand-all options for OpenAPI
- Updated dependencies [7f4e644]
- Updated dependencies [b5d6f2e]
- Updated dependencies [9dd2db6]
- Updated dependencies [c56b569]
- Updated dependencies [ccd21e4]
- @gitbook/react-openapi@1.5.11
- @gitbook/openapi-parser@3.0.11
- @gitbook/icons@0.4.4
- @gitbook/react-contentkit@0.7.13
## 0.22.3
### Patch Changes
- f182680: Add backward compatibility for OpenAPI spec content URL
- bdc6cd9: Use OpenAPI publicURL as Scalar specUrl
- b3875a1: Refactor embeddable context to merge local & site properties in one unified way
- eceec6f: Move ~scalar/proxy route to site route group
- 887c3f8: Add token verification on OpenAPI proxy
- d666212: Add support for localized site section titles
- Updated dependencies [b3875a1]
- Updated dependencies [bdc6cd9]
- Updated dependencies [8ab419a]
- Updated dependencies [eceec6f]
- Updated dependencies [887c3f8]
- @gitbook/embed@0.4.0
- @gitbook/react-openapi@1.5.10
- @gitbook/openapi-parser@3.0.10
## 0.22.2
### Patch Changes
+1 -18
View File
@@ -1,18 +1,5 @@
import { type TestsCase, runTestCases, waitForCookiesDialog } from './util';
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
const CLOSED_ONETRUST_COOKIE_BANNER = [
{
name: '__gitbook_cookie_granted',
value: 'no',
},
{
name: 'OptanonAlertBoxClosed',
value: new Date(Date.now() - ONE_HOUR_IN_MS).toISOString(),
},
];
/** A list of test cases to run on the customers' docs sites. */
const testCases: TestsCase[] = [
{
@@ -246,11 +233,7 @@ const testCases: TestsCase[] = [
name: 'run-ai-docs.nvidia.com',
contentBaseURL: 'https://run-ai-docs.nvidia.com',
tests: [
{
name: 'Home',
url: '/',
cookies: CLOSED_ONETRUST_COOKIE_BANNER,
},
{ name: 'Home', url: '/' },
{ name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' },
],
},
+26 -130
View File
@@ -3,11 +3,11 @@ import {
CustomizationBackground,
CustomizationCorners,
CustomizationDefaultMonospaceFont,
CustomizationDefaultThemeMode,
CustomizationDepth,
CustomizationHeaderPreset,
CustomizationIconsStyle,
CustomizationSidebarListStyle,
CustomizationThemeMode,
SiteSocialAccountPlatform,
} from '@gitbook/api';
import type { GitBookStandalone } from '@gitbook/embed';
@@ -16,7 +16,7 @@ import jwt from 'jsonwebtoken';
import { VISITOR_TOKEN_COOKIE } from '@/lib/visitors';
import { getGitBookPreviewURL, getSiteAPIToken } from '../tests/utils';
import { getSiteAPIToken } from '../tests/utils';
import {
type Test,
type TestsCase,
@@ -36,7 +36,7 @@ import {
waitForNotFound,
} from './util';
const AI_PROMPT = `You're being invoked by the GitBook CI/CD pipeline. Search for "Lorem ipsum", then return the first sentence of the first page you find.`;
const AI_PROMPT = `You're being invoked by the GitBook CI/CD pipeline. To make screenshot testing of the GitBook Assistant visually consistent, look up the title of the first page you find and respond with only EXACTLY its title. To find the page title, invoke the search tool with the query "GitBook". Before invoking the search tool, respond with the exact text: "I'm going to look up 'GitBook' and then respond with only the page title.". Do not execute any other tools or output any other text.`;
const overrideAIInitialState = () => {
const greeting = document.querySelector('[data-testid="ai-chat-greeting-title"]');
@@ -69,6 +69,7 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.None,
},
}),
screenshot: false,
run: async (page) => {
await waitForCookiesDialog(page);
const searchInput = page.getByTestId('search-input');
@@ -94,6 +95,7 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.None,
},
}),
screenshot: false,
run: async (page) => {
await waitForCookiesDialog(page);
await page.keyboard.press('ControlOrMeta+K');
@@ -147,6 +149,7 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
await waitForCookiesDialog(page);
const searchInput = page.locator('css=[data-testid="search-input"]');
@@ -183,6 +186,7 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
await waitForCookiesDialog(page);
await page.keyboard.press('ControlOrMeta+I');
@@ -199,6 +203,7 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
await waitForCookiesDialog(page);
await page.getByTestId('ai-chat-button').click();
@@ -215,6 +220,7 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Assistant,
},
})}&ask=`,
screenshot: false,
run: async (page) => {
await waitForCookiesDialog(page);
await expect(page.getByTestId('search-input')).not.toBeFocused();
@@ -232,6 +238,7 @@ const searchTestCases: Test[] = [
mode: CustomizationAIMode.Assistant,
},
})}&ask=${encodeURIComponent(AI_PROMPT)}`,
screenshot: false,
run: async (page) => {
await waitForCookiesDialog(page);
await expect(page.getByTestId('search-input')).not.toBeFocused();
@@ -320,6 +327,7 @@ const testCases: TestsCase[] = [
await expect(navigationLink).toBeVisible();
},
},
...searchTestCases,
{
name: 'Not found',
url: 'content-not-found',
@@ -615,6 +623,7 @@ const testCases: TestsCase[] = [
url: '',
run: waitForCookiesDialog,
},
...searchTestCases,
{
name: 'Not found',
url: 'content-not-found',
@@ -700,30 +709,10 @@ const testCases: TestsCase[] = [
await expect(page.locator('[data-testid="print-button"]')).toBeVisible();
},
},
{
name: 'Show error when missing token',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
// Intentionally not setting the token to test error handling when the token is missing
const searchParams = new URLSearchParams();
searchParams.set('limit', '10');
return `~space/${data.space}/~gitbook/pdf?${searchParams.toString()}`;
},
screenshot: false,
run: async (page, response) => {
expect(response).not.toBeNull();
expect(response?.status()).toBe(400);
await expect(page.getByText('Missing API token')).toBeVisible();
},
},
],
},
{
name: 'Site Previews',
name: 'Site Preview',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
tests: [
{
@@ -736,7 +725,7 @@ const testCases: TestsCase[] = [
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`;
return `url/preview/${data.site}/?${searchParams.toString()}`;
},
screenshot: false,
run: async (page) => {
@@ -751,7 +740,7 @@ const testCases: TestsCase[] = [
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`;
return `url/preview/${data.site}/?${searchParams.toString()}`;
},
screenshot: false,
run: async (page) => {
@@ -761,72 +750,10 @@ const testCases: TestsCase[] = [
const sectionTabLinks = sectionTabs.getByRole('link');
for (const link of await sectionTabLinks.all()) {
const href = await link.getAttribute('href');
expect(href?.includes('/preview/site_p4Xo4')).toBeTruthy();
expect(href).toMatch(/^\/url\/preview\/site_p4Xo4\/?/);
}
},
},
{
name: 'With customization cookie',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`;
},
screenshot: false,
run: async (page) => {
await expect(page.locator('[data-testid="table-of-contents"]')).toBeVisible();
// Trademark exists by default
await expect(page.getByTestId('gb-trademark')).toHaveCount(1);
// Go to another page with the customization query to disable the trademark
const pageBlocks = new URL(page.url());
pageBlocks.pathname = `${pageBlocks.pathname.replace(/\/$/, '')}/blocks`;
pageBlocks.search = getCustomizationURL({
trademark: {
enabled: false,
},
}).slice(1);
await page.goto(pageBlocks.toString());
// No trademark because customization is disabled
await expect(page.getByTestId('gb-trademark')).toHaveCount(0);
await expect(
page.getByRole('heading', { level: 1, name: 'Blocks' })
).toBeVisible();
const pageBlocksCode = new URL(page.url());
pageBlocksCode.pathname = `${pageBlocksCode.pathname.replace(/\/$/, '')}/code`;
pageBlocksCode.search = '';
await page.goto(pageBlocksCode.toString());
// The trademark should not be visible because the cookie is still set,
await expect(page.getByTestId('gb-trademark')).toHaveCount(0);
await expect(
page.getByRole('heading', { level: 1, name: 'Code' })
).toBeVisible();
},
},
{
name: 'Redirect to app for authentication when missing token',
url: async () => {
const data = await getSiteAPIToken('https://gitbook.com/docs');
const searchParams = new URLSearchParams();
// Intentionally not setting the token to test redirection for authentication
return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`;
},
screenshot: false,
run: async (page) => {
await page.waitForURL(
(url) =>
url.host === 'app.gitbook.com' && url.pathname.includes('/preview/auth')
);
},
},
],
},
{
@@ -866,11 +793,6 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'Search & AI',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: searchTestCases,
},
{
name: 'Content tests',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
@@ -1097,7 +1019,7 @@ const testCases: TestsCase[] = [
name: 'With cover for dark mode',
url: `page-options/page-with-dark-cover${getCustomizationURL({
themes: {
default: CustomizationDefaultThemeMode.Dark,
default: CustomizationThemeMode.Dark,
toggeable: false,
},
})}`,
@@ -1384,7 +1306,7 @@ const testCases: TestsCase[] = [
).toBeVisible();
const url = page.url();
expect(url.includes('shared-space-uno')).toBeTruthy(); // same uno site
expect(url.endsWith('/shared')).toBeTruthy(); // correct page
expect(url.endsWith('/shared/')).toBeTruthy(); // correct page
},
screenshot: false,
},
@@ -1404,7 +1326,7 @@ const testCases: TestsCase[] = [
).toBeVisible();
const url = page.url();
expect(url.includes('shared-space-dos')).toBeTruthy(); // same dos site
expect(url.endsWith('/shared')).toBeTruthy(); // correct page
expect(url.endsWith('/shared/')).toBeTruthy(); // correct page
},
screenshot: false,
},
@@ -1415,21 +1337,13 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [
{
name: 'Basic redirect',
name: 'Redirect to SSO page',
url: 'a/redirect/to/sso',
run: async (page) => {
await expect(page.locator('h1')).toHaveText('SSO');
},
screenshot: false,
},
{
name: 'Complex wildcard with special characters',
url: 'foo/bar/baz/123456789-welcome-to-gitbook-%22%20target=%22_blank',
run: async (page) => {
await expect(page.locator('h1')).toHaveText('SEO');
},
screenshot: false,
},
],
},
{
@@ -2155,7 +2069,7 @@ const testCases: TestsCase[] = [
},
{
name: 'Docs Embed - Basic',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/~gitbook/embed/demo/',
contentBaseURL: 'https://gitbook.com/docs/~gitbook/embed/demo/',
tests: [
{
name: 'Standalone UX',
@@ -2202,7 +2116,7 @@ const testCases: TestsCase[] = [
},
{
name: 'Docs Embed - Assistant + Docs',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/~gitbook/embed/demo/',
contentBaseURL: 'https://gitbook.com/docs/~gitbook/embed/demo/',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
tests: [
{
@@ -2230,7 +2144,7 @@ const testCases: TestsCase[] = [
run: async (page) => {
await page.evaluate(() => {
const GitBook = window.GitBook as unknown as GitBookStandalone;
GitBook('navigateToPage', '/text-page');
GitBook('navigateToPage', '/getting-started/quickstart');
});
await expect(page.locator('#gitbook-widget-window')).toBeVisible();
const iframe = page.frameLocator('#gitbook-widget-iframe');
@@ -2239,7 +2153,7 @@ const testCases: TestsCase[] = [
});
await expect(iframe.owner()).toHaveAttribute(
'src',
expect.stringContaining('text-page')
expect.stringContaining('getting-started/quickstart')
);
},
},
@@ -2300,7 +2214,7 @@ const testCases: TestsCase[] = [
onClick: () => {
const GitBook =
window.GitBook as unknown as GitBookStandalone;
GitBook('navigateToPage', '/text-page');
GitBook('navigateToPage', '/getting-started/quickstart');
},
},
{
@@ -2341,7 +2255,7 @@ const testCases: TestsCase[] = [
await expect(iframe.getByTestId('embed-docs-page')).toBeVisible();
await expect(iframe.owner()).toHaveAttribute(
'src',
expect.stringContaining('text-page')
expect.stringContaining('getting-started/quickstart')
);
await expect(actions.nth(1)).toHaveAccessibleName('Open external link');
@@ -2425,12 +2339,6 @@ const testCases: TestsCase[] = [
name: 'Docs only',
url: '',
run: async (page) => {
await page.evaluate(() => {
const GitBook = window.GitBook as unknown as GitBookStandalone;
GitBook('configure', {
tabs: ['docs'],
});
});
await expect(page.locator('#gitbook-widget-window')).toBeVisible();
const iframe = page.frameLocator('#gitbook-widget-iframe');
await expect(iframe.getByTestId('embed-docs-page')).toBeVisible({
@@ -2442,12 +2350,6 @@ const testCases: TestsCase[] = [
name: 'Table of contents',
url: '',
run: async (page) => {
await page.evaluate(() => {
const GitBook = window.GitBook as unknown as GitBookStandalone;
GitBook('configure', {
tabs: ['docs'],
});
});
await expect(page.locator('#gitbook-widget-window')).toBeVisible();
const iframe = page.frameLocator('#gitbook-widget-iframe');
await expect(iframe.getByTestId('embed-docs-page')).toBeVisible({
@@ -2463,12 +2365,6 @@ const testCases: TestsCase[] = [
name: 'Open in new tab',
url: '',
run: async (page) => {
await page.evaluate(() => {
const GitBook = window.GitBook as unknown as GitBookStandalone;
GitBook('configure', {
tabs: ['docs'],
});
});
await expect(page.locator('#gitbook-widget-window')).toBeVisible();
const iframe = page.frameLocator('#gitbook-widget-iframe');
await expect(iframe.getByTestId('embed-docs-page')).toBeVisible({
+5 -5
View File
@@ -6,7 +6,6 @@ import {
CustomizationCorners,
CustomizationDefaultFont,
CustomizationDefaultMonospaceFont,
CustomizationDefaultThemeMode,
CustomizationDepth,
type CustomizationHeaderItem,
CustomizationHeaderPreset,
@@ -17,6 +16,7 @@ import {
CustomizationSidebarBackgroundStyle,
CustomizationSidebarListStyle,
CustomizationTheme,
CustomizationThemeMode,
type CustomizationThemedColor,
type SiteCustomizationSettings,
SiteExternalLinksTarget,
@@ -84,9 +84,9 @@ export const allLocales: CustomizationLocale[] = [
CustomizationLocale.Zh,
];
export const allThemeModes: CustomizationDefaultThemeMode[] = [
CustomizationDefaultThemeMode.Light,
CustomizationDefaultThemeMode.Dark,
export const allThemeModes: CustomizationThemeMode[] = [
CustomizationThemeMode.Light,
CustomizationThemeMode.Dark,
];
export const allTintColors: Array<{
@@ -340,7 +340,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
groups: [],
},
themes: {
default: CustomizationDefaultThemeMode.System,
default: CustomizationThemeMode.Light,
toggeable: true,
},
pdf: {
-1
View File
@@ -27,7 +27,6 @@ const nextConfig = {
GITBOOK_API_URL: process.env.GITBOOK_API_URL,
GITBOOK_APP_URL: process.env.GITBOOK_APP_URL,
GITBOOK_OAUTH_SERVER_URL: process.env.GITBOOK_OAUTH_SERVER_URL,
GITBOOK_PREVIEW_BASE_URL: process.env.GITBOOK_PREVIEW_BASE_URL,
GITBOOK_INTEGRATIONS_HOST: process.env.GITBOOK_INTEGRATIONS_HOST,
GITBOOK_IMAGE_RESIZE_URL: process.env.GITBOOK_IMAGE_RESIZE_URL,
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
@@ -8,9 +8,6 @@
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
},
@@ -8,9 +8,6 @@
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
},
"env": {
"preview": {
"vars": {
@@ -8,9 +8,6 @@
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
},
"assets": {
"directory": "../../.open-next/assets",
"binding": "ASSETS"
+3 -6
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.23.2",
"version": "0.22.2",
"private": true,
"dependencies": {
"@cloudflare/workers-types": "^4.20251011.0",
@@ -21,7 +21,6 @@
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "^3.8.5",
"@opennextjs/cloudflare": "^1.14.4",
"@panzoom/panzoom": "^4.6.1",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15",
@@ -31,7 +30,6 @@
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tusbar/cache-control": "^1.0.2",
"@vercel/agent-readability": "^0.2.1",
"ai": "^4.2.2",
"assert-never": "catalog:",
"bidc": "catalog:",
@@ -53,7 +51,7 @@
"micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24",
"leven": "^4.1.0",
"negotiator": "^1.0.0",
"next": "15.4.11",
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
@@ -65,7 +63,6 @@
"react": "catalog:",
"react-dom": "catalog:",
"react-hotkeys-hook": "^4.4.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
@@ -111,7 +108,7 @@
"tailwindcss": "^4.1.11",
"ts-essentials": "^10.0.1",
"typescript": "catalog:",
"vercel": "50.37.3",
"vercel": "^50.15.1",
"wrangler": "^4.43.0",
"rss-parser": "^3.13.0"
},
@@ -1,30 +0,0 @@
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
/**
* Redirect to the upstream auth provider login URL of site, or to the site root when not configured.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getDynamicSiteContext(await params);
const noLoginFallbackURL = context.linker.toAbsoluteURL(context.linker.toPathInSite(''));
if (!context.site.urls.login) {
return NextResponse.redirect(noLoginFallbackURL);
}
try {
const loginURL = new URL(context.site.urls.login);
const location = request.nextUrl.searchParams.get('location');
if (location) {
loginURL.searchParams.set('location', location);
}
return NextResponse.redirect(loginURL);
} catch (_error) {
return NextResponse.redirect(noLoginFallbackURL);
}
}
@@ -1,12 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import type { NextRequest } from 'next/server';
import { handleMcpRequest } from '../handler';
async function handler(
rawRequest: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
return handleMcpRequest(rawRequest, await params, '~gitbook/mcp/auth');
}
export { handler as GET, handler as POST };
@@ -1,223 +0,0 @@
import { SiteInsightsDisplayContext } from '@gitbook/api';
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
import { getExposableError, throwIfDataError } from '@/lib/data';
import { getMarkdownForPageInSpace } from '@/lib/markdownPage';
import { resolvePagePath } from '@/lib/pages';
import { joinPathWithBaseURL } from '@/lib/paths';
import { findSiteSpaceBy, findSiteSpaceByUrl } from '@/lib/sites';
import { trackServerInsightsEvents } from '@/lib/tracking';
import { waitUntil } from '@/lib/waitUntil';
import { createMcpHandler } from 'mcp-handler';
import type { NextRequest } from 'next/server';
import { z } from 'zod';
export async function handleMcpRequest(
rawRequest: NextRequest,
params: RouteLayoutParams,
endpoint: '~gitbook/mcp' | '~gitbook/mcp/auth'
) {
const { context } = await getDynamicSiteContext(params);
const { dataFetcher, linker, site } = context;
// Next.js request.url is the original URL and not the rewritten one from the middleware
const requestURL = new URL(context.linker.toAbsoluteURL(context.linker.toPathInSite(endpoint)));
requestURL.search = rawRequest.nextUrl.search;
const request = new Request(requestURL, rawRequest);
waitUntil(
trackServerInsightsEvents({
organizationId: context.organizationId,
siteId: context.site.id,
events: [
{
type: 'mcp_request',
location: {
displayContext: SiteInsightsDisplayContext.Server,
},
},
],
request,
})
);
const mcpHandler = createMcpHandler(
(server) => {
server.tool(
'searchDocumentation',
`Search across the documentation to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about ${site.title}, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages.`,
{
query: z.string(),
},
async ({ query }) => {
const results = await throwIfDataError(
dataFetcher.searchSiteContent({
organizationId: context.organizationId,
siteId: site.id,
query,
scope: { mode: 'all' },
})
);
waitUntil(
trackServerInsightsEvents({
organizationId: context.organizationId,
siteId: site.id,
events: [
{
type: 'search_type_query',
query,
location: {
displayContext: SiteInsightsDisplayContext.Mcp,
},
},
],
request,
})
);
return {
content: results.flatMap((result) => {
if (result.type === 'record') {
return {
type: 'text',
text: [
`Title: ${result.title}`,
`Link: ${result.url}`,
result.description ? `Content: ${result.description}` : '',
]
.filter(Boolean)
.join('\n'),
};
}
const found = findSiteSpaceBy(
context.structure,
(siteSpace) => siteSpace.space.id === result.id
);
const spaceURL = found?.siteSpace.urls.published;
if (!spaceURL) {
return [];
}
return result.pages.map((pageResult) => {
const pageURL = linker.toAbsoluteURL(
linker.toLinkForContent(
joinPathWithBaseURL(spaceURL, pageResult.path)
)
);
const body = pageResult.sections
?.map((section) => section.body)
.join('\n');
return {
type: 'text',
text: [
`Title: ${pageResult.title}`,
`Link: ${pageURL}`,
body ? `Content: ${body}` : '',
]
.filter(Boolean)
.join('\n'),
};
});
}),
};
}
);
const siteUrl = context.siteSpace.urls.published;
server.tool(
'getPage',
`Fetch the full markdown content of a specific documentation page from ${site.title}. Use this when you have a page URL and want to read its content. Accepts full URLs (e.g. ${siteUrl}/getting-started). Since \`searchDocumentation\` returns partial content, use \`getPage\` to retrieve the complete page when you need more details. The content includes links you can follow to navigate to related pages.`,
{
url: z
.string()
.describe('The URL of the page to fetch')
.transform((value, ctx) => {
if (URL.canParse(value)) {
return value;
}
if (URL.canParse(`https://${value}`)) {
return `https://${value}`;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `"${value}" is not a valid URL. Expected a full URL like ${siteUrl}/getting-started`,
});
return z.NEVER;
}),
},
async ({ url }) => {
try {
const match = findSiteSpaceByUrl(context.structure, url);
if (!match) {
return {
content: [{ type: 'text', text: `Page not found: "${url}"` }],
isError: true,
};
}
const revision = await throwIfDataError(
dataFetcher.getRevision({
spaceId: match.siteSpace.space.id,
revisionId: match.siteSpace.space.revision,
})
);
const resolved = resolvePagePath(revision.pages, match.pagePath ?? '');
if (!resolved) {
return {
content: [{ type: 'text', text: `Page not found: "${url}"` }],
isError: true,
};
}
const markdown = await getMarkdownForPageInSpace(
context,
match.siteSpace,
resolved.page
);
waitUntil(
trackServerInsightsEvents({
organizationId: context.organizationId,
siteId: site.id,
events: [
{
type: 'page_view',
location: {
displayContext: SiteInsightsDisplayContext.Mcp,
page: resolved.page.id,
space: match.siteSpace.space.id,
revision: match.siteSpace.space.revision,
},
},
],
request,
})
);
return { content: [{ type: 'text', text: markdown }] };
} catch (error) {
const exposable = getExposableError(error);
return {
content: [{ type: 'text', text: exposable.message }],
isError: true,
};
}
}
);
},
{},
{
streamableHttpEndpoint: context.linker.toPathInSite(endpoint),
maxDuration: 60,
verboseLogs: true,
disableSse: true,
}
);
return mcpHandler(request);
}
@@ -1,12 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import type { NextRequest } from 'next/server';
import { handleMcpRequest } from './handler';
async function handler(
rawRequest: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
return handleMcpRequest(rawRequest, await params, '~gitbook/mcp');
}
export { handler as GET, handler as POST };
@@ -9,7 +9,7 @@ import { throwIfDataError } from '@/lib/data';
import { getSiteURLDataFromMiddleware } from '@/lib/middleware';
import { joinPathWithBaseURL } from '@/lib/paths';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { findSiteSpaceBy, getLocalizedTitle } from '@/lib/sites';
import { findSiteSpaceBy } from '@/lib/sites';
import type {
SearchPageResult,
SearchSpaceResult,
@@ -21,11 +21,6 @@ import type {
import type { IconName } from '@gitbook/icons';
import { type NextRequest, NextResponse } from 'next/server';
type SearchResultGroup = {
score: number;
items: OrderedComputedResult[];
};
export async function POST(request: NextRequest) {
const [context, { organization, site, shareKey }] = await Promise.all([
getServerActionBaseContext(),
@@ -63,7 +58,7 @@ export async function POST(request: NextRequest) {
]);
const results = searchResults
.flatMap((resultItem): SearchResultGroup[] => {
.map((resultItem) => {
if (resultItem.type === 'record') {
const result: OrderedComputedResult = {
type: 'record',
@@ -71,14 +66,8 @@ export async function POST(request: NextRequest) {
title: resultItem.title,
description: resultItem.description,
href: resultItem.url,
score: resultItem.score,
};
return [
{
score: resultItem.score,
items: [result],
},
];
return result;
}
const found = findSiteSpaceBy(
@@ -88,9 +77,8 @@ export async function POST(request: NextRequest) {
const siteSection = found?.siteSection;
const siteSectionGroup = found?.siteSectionGroup;
return resultItem.pages.map((pageItem) => ({
score: pageItem.score,
items: transformSitePageResult(context, {
return resultItem.pages.map((pageItem) =>
transformSitePageResult(context, {
pageItem,
spaceItem: resultItem,
siteSpace: found?.siteSpace,
@@ -98,11 +86,10 @@ export async function POST(request: NextRequest) {
spaceURL: found?.siteSpace.urls.published,
siteSection: siteSection ?? undefined,
siteSectionGroup: (siteSectionGroup as SiteSectionGroup) ?? undefined,
}),
}));
})
);
})
.sort((a, b) => b.score - a.score)
.flatMap((group) => group.items);
.flat(2);
return NextResponse.json(results);
}
@@ -121,7 +108,6 @@ function transformSitePageResult(
): OrderedComputedResult[] {
const { pageItem, spaceItem, spaceURL, siteSection, siteSectionGroup, siteSpace } = args;
const { linker } = context;
const currentLanguage = siteSpace?.space.language;
const page: ComputedPageResult = {
type: 'page',
@@ -132,15 +118,14 @@ function transformSitePageResult(
: linker.toPathInSpace(pageItem.path),
pageId: pageItem.id,
spaceId: spaceItem.id,
score: pageItem.score,
breadcrumbs: [
siteSectionGroup && {
icon: siteSectionGroup?.icon as IconName,
label: getLocalizedTitle(siteSectionGroup, currentLanguage),
label: siteSectionGroup.title,
},
siteSection && {
icon: siteSection?.icon as IconName,
label: getLocalizedTitle(siteSection, currentLanguage),
label: siteSection.title,
},
(siteSection?.siteSpaces?.filter(
(space) =>
@@ -149,7 +134,7 @@ function transformSitePageResult(
).length > 1
).length ?? 0) > 1 && siteSpace
? {
label: getLocalizedTitle(siteSpace, currentLanguage),
label: siteSpace.title,
}
: undefined,
...pageItem.ancestors.map((ancestor) => ({
@@ -171,7 +156,6 @@ function transformSitePageResult(
body: section.body,
pageId: pageItem.id,
spaceId: spaceItem.id,
score: section.score,
})) ?? [];
return [page, ...pageSections];
@@ -10,6 +10,5 @@ export async function GET(
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getStaticSiteContext(await params);
return serveLLMsTxt(context, { withMarkdownPages: true });
}
@@ -0,0 +1,127 @@
import { SiteInsightsDisplayContext } from '@gitbook/api';
import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils';
import { throwIfDataError } from '@/lib/data';
import { joinPathWithBaseURL } from '@/lib/paths';
import { findSiteSpaceBy } from '@/lib/sites';
import { trackServerInsightsEvents } from '@/lib/tracking';
import { waitUntil } from '@/lib/waitUntil';
import { createMcpHandler } from 'mcp-handler';
import type { NextRequest } from 'next/server';
import { z } from 'zod';
async function handler(
nextRequest: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getStaticSiteContext(await params);
const { dataFetcher, linker, site } = context;
const mcpHandler = createMcpHandler(
(server) => {
server.tool(
'searchDocumentation',
`Search across the documentation to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about ${site.title}, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages.`,
{
query: z.string(),
},
async ({ query }) => {
const results = await throwIfDataError(
dataFetcher.searchSiteContent({
organizationId: context.organizationId,
siteId: site.id,
query,
scope: { mode: 'all' },
})
);
// Track the search event server-side
waitUntil(
trackServerInsightsEvents({
organizationId: context.organizationId,
siteId: site.id,
events: [
{
type: 'search_type_query',
query,
location: {
displayContext: SiteInsightsDisplayContext.Mcp,
},
},
],
request: nextRequest,
})
);
return {
content: results.flatMap((result) => {
if (result.type === 'record') {
return {
type: 'text',
text: [
`Title: ${result.title}`,
`Link: ${result.url}`,
result.description ? `Content: ${result.description}` : '',
]
.filter(Boolean)
.join('\n'),
};
}
const found = findSiteSpaceBy(
context.structure,
(siteSpace) => siteSpace.space.id === result.id
);
const spaceURL = found?.siteSpace.urls.published;
if (!spaceURL) {
return [];
}
return result.pages.map((pageResult) => {
const pageURL = linker.toAbsoluteURL(
linker.toLinkForContent(
joinPathWithBaseURL(spaceURL, pageResult.path)
)
);
const body = pageResult.sections
?.map((section) => section.body)
.join('\n');
return {
type: 'text',
text: [
`Title: ${pageResult.title}`,
`Link: ${pageURL}`,
body ? `Content: ${body}` : '',
]
.filter(Boolean)
.join('\n'),
};
});
}),
};
}
);
},
{},
{
basePath: context.linker.toPathInSite('~gitbook/'),
streamableHttpEndpoint: '/mcp',
maxDuration: 60,
verboseLogs: true,
disableSse: true,
}
);
// Next.js request.url is the original URL and not the rewritten one from the middleware
const requestURL = new URL(
context.linker.toAbsoluteURL(context.linker.toPathInSite('~gitbook/mcp'))
);
requestURL.search = nextRequest.nextUrl.search;
const request = new Request(requestURL, nextRequest);
return mcpHandler(request);
}
export { handler as GET, handler as POST };
@@ -1,7 +1,6 @@
'use client';
import { Button } from '@/components/primitives';
import { tString, useLanguage } from '@/intl/client';
import { z } from 'zod';
import { AIToolContainer } from './common';
import { type GetAIControlProps, createAIControl } from './helpers';
@@ -30,7 +29,6 @@ export const ConfirmControlDef = createAIControl({
function ConfirmControl(props: GetAIControlProps<typeof ConfirmControlDef>) {
const { label, icon, onSubmit } = props;
const language = useLanguage();
return (
<AIToolContainer className="flex w-full flex-col gap-2">
<Button
@@ -38,18 +36,17 @@ function ConfirmControl(props: GetAIControlProps<typeof ConfirmControlDef>) {
onClick={() => {
onSubmit({ result: 'cancelled' });
}}
truncate={false}
variant="blank"
variant="secondary"
icon="xmark"
label={tString(language, 'cancel')}
label="Cancel"
/>
<Button
data-testid="ai-chat-tool-confirm-accept"
onClick={() => {
onSubmit({ result: 'confirmed' });
}}
className="flex-1"
variant="primary"
truncate={false}
icon={icon}
label={label}
/>
@@ -1,229 +0,0 @@
'use client';
import { Button, Input } from '@/components/primitives';
import { ScrollContainer } from '@/components/primitives/ScrollContainer';
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Icon, IconStyle } from '@gitbook/icons';
import * as React from 'react';
import { z } from 'zod';
import { AIToolContainer } from './common';
import { type GetAIControlProps, createAIControl } from './helpers';
const OTHER_OPTION_ID = '$other';
export const MultiChoiceControlOutputSchema = z.object({
ids: z.array(z.string()).describe('The identifiers of the options selected by the user.'),
labels: z.array(z.string()).describe('The labels of the options selected by the user.'),
input: z
.string()
.optional()
.describe(
'The custom text entered by the user when "Other" is enabled and selected among the choices.'
),
});
export const MultiChoiceControlDef = createAIControl({
name: 'multi-choice',
exposeAsTool: true,
description:
'Use this control whenever you need the user to choose one or more options from a predefined list. Never add an "Other" option to the `options` array, use the `allowOther` property instead.',
inputSchema: z.object({
prompt: z
.string()
.describe(
'Provide the question or instruction that tells the user what choices they need to make.'
),
options: z
.array(
z
.object({
id: z
.string()
.describe(
'Provide a unique, stable identifier for this option. This is the value returned to the agent when the user selects it.'
),
label: z
.string()
.describe('Provide the short label the user sees for this option.'),
description: z
.string()
.optional()
.describe(
'Optionally provide supporting details to help the user understand this option.'
),
})
.describe('Define one selectable option the user can pick.')
)
.describe(
'Provide the list of options the user can choose from. The user may select one or more.'
),
allowOther: z
.boolean()
.optional()
.describe(
'Set to true to let the user select an "Other" option and enter a custom text response.'
),
}),
outputSchema: MultiChoiceControlOutputSchema,
render: (props) => {
return <MultiChoiceControl {...props} />;
},
});
function MultiChoiceControl(props: GetAIControlProps<typeof MultiChoiceControlDef>) {
const { prompt, options, allowOther, onSubmit } = props;
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
const [otherInput, setOtherInput] = React.useState('');
const orderedOptionIds = [
...options.map((option) => option.id),
...(allowOther ? [OTHER_OPTION_ID] : []),
];
const language = useLanguage();
const toggleOption = (id: string) => {
setSelectedIds((current) =>
current.includes(id)
? current.filter((currentId) => currentId !== id)
: orderedOptionIds.filter(
(optionId) => optionId === id || current.includes(optionId)
)
);
};
const hasOtherSelected = selectedIds.includes(OTHER_OPTION_ID);
const canSubmit = selectedIds.length > 0 && (!hasOtherSelected || otherInput.trim().length > 0);
return (
<AIToolContainer className="flex w-full flex-col gap-2">
<ScrollContainer orientation="vertical" contentClassName="flex flex-col gap-2">
<p className="px-2 pt-1 font-semibold text-sm">{prompt}</p>
{options.map((option) => {
const isSelected = selectedIds.includes(option.id);
return (
<button
key={option.id}
type="button"
data-testid={`ai-chat-tool-multi-choice-option-${option.id}`}
onClick={() => {
toggleOption(option.id);
}}
className={tcls(
'text-left transition-colors',
'circular-corners:rounded-3xl rounded-corners:rounded-xl px-2 py-1 text-left transition-colors',
isSelected
? 'bg-primary text-tint-strong contrast-more:bg-primary-active'
: 'hover:bg-tint contrast-more:hover:bg-tint-hover'
)}
>
<div className="flex items-center gap-2">
<span
aria-hidden
className={tcls(
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded border bg-tint-base transition-colors',
isSelected
? 'border-primary-original bg-primary-original text-contrast-primary-original'
: 'border-tint-subtle'
)}
>
{isSelected ? (
<Icon
icon="check"
iconStyle={IconStyle.Solid}
className="size-3"
/>
) : null}
</span>
<span className="min-w-0">
<p className="font-medium text-sm">{option.label}</p>
{option.description ? (
<p className="mt-0.5 text-tint-subtle text-xs">
{option.description}
</p>
) : null}
</span>
</div>
</button>
);
})}
{allowOther ? (
<button
type="button"
data-testid="ai-chat-tool-multi-choice-option-other"
tabIndex={-1}
onClick={() => {
toggleOption(OTHER_OPTION_ID);
}}
>
<Input
label={tString(language, 'form_other_prompt')}
value={otherInput}
onValueChange={setOtherInput}
data-testid="ai-chat-tool-multi-choice-other-input"
placeholder={tString(
language,
hasOtherSelected ? 'form_other_prompt' : 'form_other_field'
)}
className={tcls(
'grow gap-2 border-0 px-2 ring-inset **:placeholder:text-tint',
hasOtherSelected
? 'bg-primary text-tint-strong hover:bg-primary contrast-more:bg-primary-active'
: 'hover:not-focus-within:bg-tint contrast-more:hover:bg-tint-hover'
)}
sizing="small"
onClick={(event) => {
event.stopPropagation();
if (!hasOtherSelected) {
toggleOption(OTHER_OPTION_ID);
}
}}
leading={
<span
aria-hidden
className={tcls(
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded border bg-tint-base transition-colors',
hasOtherSelected
? 'border-primary-original bg-primary-original text-contrast-primary-original'
: 'border-tint-subtle'
)}
>
{hasOtherSelected ? (
<Icon
icon="check"
iconStyle={IconStyle.Solid}
className="size-3"
/>
) : null}
</span>
}
/>
</button>
) : null}
</ScrollContainer>
<Button
data-testid="ai-chat-tool-multi-choice-submit"
variant="primary"
label={tString(language, 'submit')}
disabled={!canSubmit}
onClick={() => {
if (!canSubmit) {
return;
}
onSubmit({
ids: selectedIds,
labels: selectedIds.map((id) =>
id === OTHER_OPTION_ID
? 'Other'
: options.find((option) => option.id === id)?.label || ''
),
input: hasOtherSelected ? otherInput.trim() : undefined,
});
}}
/>
</AIToolContainer>
);
}
@@ -1,8 +1,6 @@
'use client';
import { Button, Input } from '@/components/primitives';
import { ScrollContainer } from '@/components/primitives/ScrollContainer';
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import * as React from 'react';
import { z } from 'zod';
@@ -13,7 +11,6 @@ const OTHER_OPTION_ID = '$other';
export const SingleChoiceControlOutputSchema = z.object({
id: z.string().describe('The identifier of the option selected by the user.'),
label: z.string().describe('The label of the option selected by the user.'),
input: z
.string()
.optional()
@@ -24,7 +21,7 @@ export const SingleChoiceControlDef = createAIControl({
name: 'single-choice',
exposeAsTool: true,
description:
'Use this control whenever you need the user to choose exactly one option from a predefined list. Important: NEVER write an "Other" choice yourself to the `options` array, pass the `allowOther` property as `true` to add it automatically instead.',
'Use this control when you need the user to choose exactly one option from a predefined list.',
inputSchema: z.object({
prompt: z
.string()
@@ -47,7 +44,7 @@ export const SingleChoiceControlDef = createAIControl({
.string()
.optional()
.describe(
'Optionally provide supporting details to help the user understand this option. Keep it concise and do not repeat the label.'
'Optionally provide supporting details to help the user understand this option.'
),
})
.describe('Define one selectable option the user can pick.')
@@ -73,16 +70,14 @@ function SingleChoiceControl(props: GetAIControlProps<typeof SingleChoiceControl
const [selectedId, setSelectedId] = React.useState<string | null>(null);
const [otherInput, setOtherInput] = React.useState('');
const language = useLanguage();
const canSubmit =
selectedId !== null &&
(selectedId !== OTHER_OPTION_ID || (allowOther && otherInput.trim().length > 0));
return (
<AIToolContainer className="flex w-full flex-col gap-2">
<p className="px-2 pt-1 font-semibold text-sm">{prompt}</p>
<ScrollContainer orientation="vertical" contentClassName="flex flex-col gap-2">
<div className="no-scrollbar flex flex-1 flex-col gap-1 overflow-auto">
<p className="mb-1 font-semibold text-sm">{prompt}</p>
{options.map((option) => {
const isSelected = selectedId === option.id;
return (
@@ -94,18 +89,17 @@ function SingleChoiceControl(props: GetAIControlProps<typeof SingleChoiceControl
setSelectedId(option.id);
}}
className={tcls(
'text-left transition-colors',
'circular-corners:rounded-3xl rounded-corners:rounded-xl px-2 py-1 text-left transition-colors',
'circular-corners:rounded-3xl rounded-corners:rounded-xl border px-3 py-2 text-left transition-colors',
isSelected
? 'bg-primary text-tint-strong contrast-more:bg-primary-active'
: 'hover:bg-tint contrast-more:hover:bg-tint-hover'
? 'border-primary-original bg-primary-subtle text-tint-strong'
: 'border-tint bg-tint-base hover:bg-tint-subtle'
)}
>
<div className="flex items-center gap-2">
<div className="flex items-center gap-3">
<span
aria-hidden
className={tcls(
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border bg-tint-base transition-colors',
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors',
isSelected
? 'border-primary-original'
: 'border-tint-subtle'
@@ -121,7 +115,7 @@ function SingleChoiceControl(props: GetAIControlProps<typeof SingleChoiceControl
<span className="min-w-0">
<p className="font-medium text-sm">{option.label}</p>
{option.description ? (
<p className="mt-0.5 text-tint-subtle text-xs">
<p className="mt-0.5 text-sm text-tint-subtle">
{option.description}
</p>
) : null}
@@ -135,58 +129,58 @@ function SingleChoiceControl(props: GetAIControlProps<typeof SingleChoiceControl
<button
type="button"
data-testid="ai-chat-tool-single-choice-option-other"
tabIndex={-1} // The input is already focusable, so prevent focus on the wrapper button
onClick={() => {
setSelectedId(OTHER_OPTION_ID);
}}
className={tcls(
'circular-corners:rounded-3xl rounded-corners:rounded-xl border px-3 py-2 text-left transition-colors',
selectedId === OTHER_OPTION_ID
? 'border-primary-original bg-primary-subtle text-tint-strong'
: 'border-tint bg-tint-base hover:bg-tint-subtle'
)}
>
<Input
label={tString(language, 'form_other_prompt')}
value={otherInput}
onValueChange={setOtherInput}
data-testid="ai-chat-tool-single-choice-other-input"
placeholder={tString(
language,
selectedId === OTHER_OPTION_ID
? 'form_other_prompt'
: 'form_other_field'
)}
className={tcls(
'grow gap-2 border-0 px-2 ring-inset **:placeholder:text-tint',
selectedId === OTHER_OPTION_ID
? 'bg-primary text-tint-strong hover:bg-primary contrast-more:bg-primary-active'
: 'hover:not-focus-within:bg-tint contrast-more:hover:bg-tint-hover'
)}
sizing="small"
leading={
<div className="flex items-center gap-3">
<span
aria-hidden
className={tcls(
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors',
selectedId === OTHER_OPTION_ID
? 'border-primary-original'
: 'border-tint-subtle'
)}
>
<span
aria-hidden
className={tcls(
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border bg-tint-base transition-colors',
'size-2.5 rounded-full transition-colors',
selectedId === OTHER_OPTION_ID
? 'border-primary-original'
: 'border-tint-subtle'
? 'bg-primary-original'
: 'bg-transparent'
)}
>
<span
className={tcls(
'size-2.5 rounded-full transition-colors',
selectedId === OTHER_OPTION_ID
? 'bg-primary-original'
: 'bg-transparent'
)}
/>
</span>
<div className="flex flex-1 flex-col gap-1">
<p className="font-medium text-sm">Other</p>
{allowOther && selectedId === OTHER_OPTION_ID ? (
<Input
label="Enter your answer"
value={otherInput}
onValueChange={setOtherInput}
data-testid="ai-chat-tool-single-choice-other-input"
autoFocus
className="w-full"
sizing="small"
/>
</span>
}
/>
) : null}
</div>
</div>
</button>
) : null}
</ScrollContainer>
</div>
<Button
data-testid="ai-chat-tool-single-choice-submit"
variant="primary"
label={tString(language, 'submit')}
label="Submit answer"
disabled={!canSubmit}
onClick={() => {
if (!canSubmit || !selectedId) {
@@ -196,16 +190,12 @@ function SingleChoiceControl(props: GetAIControlProps<typeof SingleChoiceControl
if (selectedId === OTHER_OPTION_ID) {
onSubmit({
id: OTHER_OPTION_ID,
label: 'Other',
input: otherInput.trim(),
});
return;
}
onSubmit({
id: selectedId,
label: options.find((option) => option.id === selectedId)?.label || '',
});
onSubmit({ id: selectedId });
}}
/>
</AIToolContainer>
@@ -6,10 +6,10 @@ export function AIToolContainer(props: ComponentPropsWithRef<'div'>) {
<div
{...props}
className={tcls(
'min-h-0 shrink grow-0 animate-blur-in circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base p-2',
'min-h-0 animate-present-slow circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-subtle p-2',
props.className
)}
style={{ animationDelay: '0.3s', ...props.style }}
style={{ animationDelay: '0.5s', ...props.style }}
/>
);
}
@@ -1,5 +1,3 @@
import { tString } from '@/intl/translate';
import type { TranslationLanguage } from '@/intl/translations/types';
import type {
AIStreamResponseToolCallPending,
AIToolCallResult,
@@ -18,7 +16,6 @@ type AIControlDefinition<
createControl: (args: {
context: AIUIToolContext;
input: Input;
language: TranslationLanguage;
send: (result: Pick<AIToolCallResult, 'output' | 'summary'>) => Promise<void>;
}) => AIControl<Name, Input, Output>;
exposeAsTool: boolean;
@@ -58,15 +55,15 @@ export function createAIControl<
name: `ui--${def.name}`,
description: def.description,
inputSchema: zodToJsonSchema(def.inputSchema as any) as AIToolDefinition['inputSchema'],
createControl: ({ context, input, language, send }) => {
createControl: ({ context, input, send }) => {
const props: AIControlProps<z.infer<InputSchema>, z.infer<OutputSchema>> = {
...input,
onSubmit: async (output) => {
await send({
output,
summary: {
icon: 'comment-check',
text: tString(language, 'ai_control_submitted_answer'),
icon: 'check',
text: 'Submitted',
},
});
},
@@ -1,8 +1,7 @@
import { ConfirmControlDef } from './ConfirmControl';
import { MultiChoiceControlDef } from './MultiChoiceControl';
import { SingleChoiceControlDef } from './SingleChoiceControl';
const CONTROLS = [SingleChoiceControlDef, MultiChoiceControlDef, ConfirmControlDef];
const CONTROLS = [SingleChoiceControlDef, ConfirmControlDef];
export type AnyAIControlTool = (typeof CONTROLS)[number];
export type AnyAIControl = ReturnType<AnyAIControlTool['createControl']>;
@@ -35,7 +35,7 @@ export function AIMessageView(
wrapBlocksInSuspense: false,
withLinkPreviews,
}}
style="ai-response-document mt-2 space-y-4 *:origin-top-left *:animate-blur-in-slow empty:hidden"
style="ai-response-document mt-2 space-y-4 *:origin-top-left *:animate-blur-in-slow"
/>
{withToolCalls && step.toolCalls && step.toolCalls.length > 0 ? (
@@ -9,7 +9,6 @@ import {
AIModel,
type AIToolCallResult,
type AIToolDefinition,
SiteCoreChannelType,
type SiteInsightsSession,
} from '@gitbook/api';
import { streamRenderAIMessage } from './api';
@@ -62,11 +61,6 @@ export async function* streamAIChatResponse({
toolCall,
tools,
session,
channel: {
type: options?.asEmbeddable
? SiteCoreChannelType.Embed
: SiteCoreChannelType.Site,
},
}
);
@@ -224,7 +224,7 @@ export function AIChatProvider(props: {
try {
const result = await toolDef.execute(event.toolCall.input);
await streamResponse({
streamResponse({
toolCall: {
tool: event.toolCall.tool,
toolCallId: event.toolCallId,
@@ -233,7 +233,7 @@ export function AIChatProvider(props: {
},
});
} catch (error) {
await streamResponse({
streamResponse({
toolCall: {
tool: event.toolCall.tool,
toolCallId: event.toolCallId,
@@ -322,7 +322,6 @@ export function AIChatProvider(props: {
toolCallId: event.toolCallId,
},
input: event.toolCall.input as any,
language,
send: async (result) => {
await streamResponse({
toolCall: {
@@ -351,7 +350,6 @@ export function AIChatProvider(props: {
label: confirmation.label,
icon: confirmation.icon,
},
language,
send: async (result) => {
const output = ConfirmControlOutputSchema.parse(
result.output
@@ -408,13 +406,13 @@ export function AIChatProvider(props: {
// Execute the tool call if it doesn't require confirmation
if (toolToExecute) {
await executeToolCall(toolToExecute);
} else {
globalState.setState((state) => ({
...state,
loading: false,
error: false,
}));
}
globalState.setState((state) => ({
...state,
loading: false,
error: false,
}));
} catch (error) {
console.error('Error streaming AI response', error);
globalState.setState((state) => ({
@@ -163,26 +163,17 @@ export function AIChatSubtitle(props: {
return (
<EmbeddableFrameSubtitle
className={tcls(
'relative',
chat.loading || chat.control ? 'h-3 opacity-11' : 'h-0 opacity-0'
)}
className={tcls('relative', chat.loading ? 'h-3 opacity-11' : 'h-0 opacity-0')}
>
<span
className={tcls(
'absolute left-0',
chat.loading
? chat.messages[chat.messages.length - 1]?.content
? 'animate-blur-in-display-slow'
? 'animate-blur-in-slow'
: 'hidden'
: 'animate-blur-out-display-slow'
: 'animate-blur-out-slow'
)}
style={{
animationDelay:
chat.messages[chat.messages.length - 1]?.content && !chat.control
? '.3s'
: undefined,
}}
>
{t(language, 'ai_chat_working')}
</span>
@@ -191,28 +182,13 @@ export function AIChatSubtitle(props: {
'absolute left-0',
chat.loading
? chat.messages[chat.messages.length - 1]?.content
? 'animate-blur-out-display-slow'
: 'animate-blur-in-display-slow'
? 'animate-blur-out-slow'
: 'animate-blur-in-slow'
: 'hidden'
)}
style={{
animationDelay:
chat.messages[chat.messages.length - 1]?.content || chat.control
? undefined
: '.3s',
}}
>
{t(language, 'ai_chat_thinking')}
</span>
<span
className={tcls(
'absolute left-0',
chat.control ? 'animate-blur-in-display-slow' : 'animate-blur-out-display-slow'
)}
style={{ animationDelay: chat.control ? '.3s' : undefined }}
>
{t(language, 'ai_chat_waiting')}
</span>
</EmbeddableFrameSubtitle>
);
}
@@ -250,7 +226,7 @@ export function AIChatBody(props: {
<>
<ScrollContainer
className="min-h-[20%] shrink grow animate-fade-in-slow [container-type:size]"
contentClassName="py-4 gutter-stable flex flex-col gap-4 [scroll-behavior:smooth]"
contentClassName="py-4 gutter-stable flex flex-col gap-4"
orientation="vertical"
trailing={{ fade: false, button: true }}
active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
@@ -298,18 +274,21 @@ export function AIChatBody(props: {
)}
</ScrollContainer>
<div className="flex max-h-3/4 min-h-0 flex-col gap-2 pb-4">
<div className="flex min-h-0 flex-col gap-2 pb-4">
{/* Display an error banner when something went wrong. */}
{chat.error ? <AIChatError chatController={chatController} /> : null}
{chat.control ? <AIChatControl control={chat.control} /> : null}
<AIChatInput
loading={chat.loading}
disabled={chat.loading || chat.error}
onSubmit={(value) => {
chatController.postMessage({ message: value });
}}
/>
{chat.control ? (
<AIChatControl control={chat.control} />
) : (
<AIChatInput
loading={chat.loading}
disabled={chat.loading || chat.error}
onSubmit={(value) => {
chatController.postMessage({ message: value });
}}
/>
)}
</div>
</>
);
@@ -1,5 +1,4 @@
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
@@ -59,10 +58,7 @@ export function AIChatInput(props: {
size: 'small',
label: tString(language, 'send'),
}}
className={tcls(
chat.control ? 'animate-blur-out-display' : 'animate-blur-in-slow',
'bg-tint-base/9 backdrop-blur-lg contrast-more:bg-tint-base'
)}
className="animate-blur-in-slow bg-tint-base/9 backdrop-blur-lg contrast-more:bg-tint-base"
rows={1}
maxLength={2048}
keyboardShortcut={
@@ -73,7 +69,7 @@ export function AIChatInput(props: {
}
: undefined
}
disabled={disabled || loading || chat.control !== null}
disabled={disabled || loading}
aria-busy={loading}
ref={inputRef}
trailing={
@@ -68,10 +68,7 @@ export function AIChatMessages(props: {
'animate-blur-in-slow',
message.role === AIMessageRole.User
? 'max-w-[80%] origin-top-right self-end circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint px-4 py-2'
: 'origin-top-left text-tint-strong',
isLastMessage && message.role === AIMessageRole.Assistant
? 'grow'
: ''
: 'origin-top-left text-tint-strong'
)}
style={{
animationDelay: `${Math.min(originalIndex * 0.1, 0.6)}s`,
@@ -97,7 +94,7 @@ export function AIChatMessages(props: {
!chat.error &&
chat.query &&
chat.responseId &&
!chat.control ? (
chat.control ? (
<AIResponseFeedback
responseId={chat.responseId}
query={chat.query}
@@ -20,7 +20,7 @@ export default function AIChatSuggestedQuestions(props: {
return (
<div
className="flex max-w-full flex-col items-start gap-2 self-start"
className="flex max-w-full max-w-full flex-col items-start gap-2 self-start"
data-testid="ai-chat-suggested-questions"
>
{suggestions.map((question, index) => (
@@ -15,7 +15,7 @@ export function AIChatFollowupSuggestions(props: {
}
return (
<div className="mt-auto flex grow flex-col">
<div className="flex grow flex-col">
<div
className="sticky bottom-0 mt-auto flex max-w-full flex-col items-start gap-2"
data-testid="ai-chat-followup-suggestions"
@@ -26,7 +26,6 @@ export async function Announcement(props: {
return (
<AnnouncementBanner
announcement={customization.announcement}
locale={context.locale}
contentRef={resolvedContentRef}
/>
);
@@ -3,13 +3,8 @@
import { tString, useLanguage } from '@/intl/client';
import { setLocalStorageItem } from '@/lib/browser';
import type { ResolvedContentRef } from '@/lib/references';
import { getLocalizedMessage, getLocalizedTitle } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import {
type CustomizationAnnouncement,
SiteInsightsLinkPosition,
type TranslationLanguage,
} from '@gitbook/api';
import { type CustomizationAnnouncement, SiteInsightsLinkPosition } from '@gitbook/api';
import { Icon, type IconName } from '@gitbook/icons';
import { CONTAINER_STYLE } from '../layout';
import { Button, Link } from '../primitives';
@@ -21,14 +16,11 @@ import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
*/
export function AnnouncementBanner(props: {
announcement: CustomizationAnnouncement;
locale: TranslationLanguage | undefined;
contentRef: ResolvedContentRef | null;
}) {
const { announcement, locale, contentRef } = props;
const { announcement, contentRef } = props;
const language = useLanguage();
const message = getLocalizedMessage(announcement, locale);
const linkTitle = announcement.link ? getLocalizedTitle(announcement.link, locale) : undefined;
const hasLink = announcement.link && contentRef?.href;
const closeable = announcement.style !== 'danger';
@@ -69,7 +61,7 @@ export function AnnouncementBanner(props: {
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
/>
<div>
{message}
{announcement.message}
{hasLink ? (
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
{contentRef?.icon ? (
@@ -77,7 +69,9 @@ export function AnnouncementBanner(props: {
{contentRef?.icon}
</span>
) : null}
{linkTitle ? <span className="mr-1">{linkTitle}</span> : null}
{announcement.link?.title && (
<span className="mr-1">{announcement.link?.title}</span>
)}
<Icon
icon={
announcement.link?.to.kind === 'url'
@@ -193,10 +193,10 @@ function CodeBlockExpandable(props: {
<Button
icon={<ToggleChevron open={isExpanded} />}
size="xsmall"
variant="secondary"
variant="blank"
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="pointer-events-auto z-1 my-2 bg-tint! text-primary text-sm opacity-0 focus:opacity-11 group-hover/codeblock-expandable:opacity-11"
className="pointer-events-auto z-1 my-2 text-primary text-sm opacity-0 focus:opacity-11 group-hover/codeblock-expandable:opacity-11"
aria-expanded={isExpanded}
aria-controls={controls}
>
@@ -41,14 +41,14 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
ref={ref}
aria-busy={ariaBusy}
className={tcls(
'group/codeblock shiki relative flex shrink flex-col overflow-hidden print:overflow-visible',
'group/codeblock shiki grid shrink grid-flow-col overflow-hidden',
style
)}
/* Sets the code theme's mode (light or dark) for the site's theme mode (light or dark).
* Used to style UI elements (scrollbars, form controls) correctly and apply the right default to "plain" code blocks. */
data-color-scheme={`${theme.themes.light.type} ${theme.themes.dark.type}`}
>
<div className="flex items-center justify-start gap-2 text-sm">
<div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
{title ? (
<div
className="relative top-px z-20 inline-flex items-center justify-center circular-corners:rounded-t-xl rounded-corners:rounded-t-lg straight-corners:rounded-t-xs border border-tint-subtle border-b-0 bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base"
@@ -63,45 +63,43 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
</div>
) : null}
</div>
<div className="relative">
<CopyCodeButton
codeId={codeId}
style="absolute top-2 right-2 z-2 self-start justify-self-end font-sans leading-none opacity-0 backdrop-blur-md group-hover/codeblock:opacity-11"
/>
<pre
<CopyCodeButton
codeId={codeId}
style="z-2 mt-2 mr-2 self-start justify-self-end leading-none opacity-0 backdrop-blur-md [grid-area:2/1] group-hover/codeblock:opacity-11"
/>
<pre
className={tcls(
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base p-2 text-tint-strong [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base',
'circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs depth-subtle:shadow-xs',
title && 'rounded-ss-none!'
)}
style={{
backgroundColor: bg?.color,
...bg?.vars,
color: fg?.color,
...fg?.vars,
}}
>
<code
id={codeId}
className={tcls(
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base py-2 text-tint-strong contrast-more:border-tint contrast-more:bg-tint-base print:overflow-visible',
'circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs depth-subtle:shadow-xs',
title && 'rounded-ss-none!'
'inline-grid max-h-full min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
withWrap && 'whitespace-pre-wrap',
'[[aria-expanded=false]_&]:mask-b-from-50%'
)}
style={{
backgroundColor: bg?.color,
...bg?.vars,
color: fg?.color,
...fg?.vars,
}}
>
<code
id={codeId}
className={tcls(
'table max-h-full w-fit min-w-full [counter-reset:line] print:max-h-none print:whitespace-pre-wrap',
withWrap && 'whitespace-pre-wrap',
'[[aria-expanded=false]_&]:mask-b-from-50%'
)}
>
{theme.lines.map((line, index) => (
<CodeHighlightLine
bg={bg}
fg={fg}
key={index}
line={line}
isLast={index === theme.lines.length - 1}
withLineNumbers={withLineNumbers}
/>
))}
</code>
</pre>
</div>
{theme.lines.map((line, index) => (
<CodeHighlightLine
bg={bg}
fg={fg}
key={index}
line={line}
isLast={index === theme.lines.length - 1}
withLineNumbers={withLineNumbers}
/>
))}
</code>
</pre>
</div>
);
});
@@ -5,9 +5,7 @@ import { useEffect, useId, useMemo, useRef, useState } from 'react';
import { Loading } from '@/components/primitives/Loading';
import { tcls } from '@/lib/tailwind';
import Panzoom from '@panzoom/panzoom';
import { type ClientBlockProps, ClientCodeBlock } from './ClientCodeBlock';
import { MermaidPanZoomControls } from './MermaidPanZoomControls';
import { getPlainCodeBlock } from './highlight';
/**
@@ -16,9 +14,7 @@ import { getPlainCodeBlock } from './highlight';
export function MermaidCodeBlock(props: ClientBlockProps) {
const { block, style } = props;
const source = getPlainCodeBlock(block);
const wrapperRef = useRef<HTMLDivElement>(null);
const diagramRef = useRef<HTMLDivElement>(null);
const [panZoom, setPanZoom] = useState<ReturnType<typeof Panzoom> | null>(null);
const [error, setError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const { resolvedTheme } = useTheme();
@@ -27,13 +23,11 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
useEffect(() => {
const container = diagramRef.current;
const wrapper = wrapperRef.current;
if (!container || !wrapper) {
if (!container) {
return;
}
let cancelled = false;
let cleanupPanZoom: (() => void) | undefined;
setError(false);
setIsLoading(true);
@@ -43,15 +37,6 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
id,
darkMode,
})
.then(() => {
if (!cancelled) {
cleanupPanZoom = initPanzoom({
container,
wrapper,
onInit: setPanZoom,
});
}
})
.catch(() => {
if (!cancelled) {
setError(true);
@@ -65,8 +50,6 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
return () => {
cancelled = true;
cleanupPanZoom?.();
setPanZoom(null);
};
}, [source, id, darkMode]);
@@ -76,14 +59,7 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
return (
<div className={tcls('relative', style)} contentEditable={false}>
<div
ref={wrapperRef}
className={
isLoading
? 'invisible absolute inset-x-0 overflow-hidden'
: 'cursor-grab overflow-hidden active:cursor-grabbing'
}
>
<div className={isLoading ? 'invisible absolute inset-x-0' : undefined}>
<div
ref={diagramRef}
className="overflow-auto p-2 [&_svg]:h-auto [&_svg]:max-w-full"
@@ -94,7 +70,6 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
<Loading className="h-8 w-8" />
</div>
) : null}
{!isLoading && panZoom ? <MermaidPanZoomControls panZoom={panZoom} /> : null}
</div>
);
}
@@ -125,35 +100,6 @@ async function renderMermaidDiagram(args: {
bindFunctions?.(container);
}
/**
* Initialize panzoom on the diagram container.
*/
function initPanzoom(args: {
container: HTMLElement;
wrapper: HTMLElement;
onInit: (instance: ReturnType<typeof Panzoom> | null) => void;
}): () => void {
const { container, wrapper, onInit } = args;
const instance = Panzoom(container, {
maxScale: 5,
minScale: 0.5,
contain: 'outside',
cursor: 'grab',
panOnlyWhenZoomed: true,
});
onInit(instance);
wrapper.addEventListener('wheel', instance.zoomWithWheel, { passive: false });
return () => {
wrapper.removeEventListener('wheel', instance.zoomWithWheel);
instance.destroy();
onInit(null);
};
}
function useSafeId() {
const rawId = useId();
return useMemo(() => rawId.replace(/[^a-zA-Z0-9]/g, ''), [rawId]);
@@ -1,80 +0,0 @@
'use client';
import type { PanzoomObject } from '@panzoom/panzoom';
import { Button } from '@/components/primitives';
const PAN_STEP = 50;
/**
* Navigation and zoom controls for mermaid diagrams, positioned as an overlay.
*/
export function MermaidPanZoomControls(props: { panZoom: PanzoomObject }) {
const { panZoom } = props;
return (
<div className="absolute right-3 bottom-3 z-10 grid grid-cols-3 gap-0.5">
{/* Row 1: empty, pan up, zoom in */}
<div />
<Button
label="Pan up"
icon="chevron-up"
variant="secondary"
size="xsmall"
iconOnly
onClick={() => panZoom.pan(0, PAN_STEP, { relative: true })}
/>
<Button
label="Zoom in"
icon="plus"
variant="secondary"
size="xsmall"
iconOnly
onClick={() => panZoom.zoomIn()}
/>
{/* Row 2: pan left, reset, pan right */}
<Button
label="Pan left"
icon="chevron-left"
variant="secondary"
size="xsmall"
iconOnly
onClick={() => panZoom.pan(PAN_STEP, 0, { relative: true })}
/>
<Button
label="Reset view"
icon="refresh"
variant="secondary"
size="xsmall"
iconOnly
onClick={() => panZoom.reset()}
/>
<Button
label="Pan right"
icon="chevron-right"
variant="secondary"
size="xsmall"
iconOnly
onClick={() => panZoom.pan(-PAN_STEP, 0, { relative: true })}
/>
{/* Row 3: empty, pan down, zoom out */}
<div />
<Button
label="Pan down"
icon="chevron-down"
variant="secondary"
size="xsmall"
iconOnly
onClick={() => panZoom.pan(0, -PAN_STEP, { relative: true })}
/>
<Button
label="Zoom out"
icon="minus"
variant="secondary"
size="xsmall"
iconOnly
onClick={() => panZoom.zoomOut()}
/>
</div>
);
}
@@ -1,16 +1,13 @@
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
import { resolveContentRefFallback, resolveContentRefInDocument } from '@/lib/references';
import * as api from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
import type React from 'react';
import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink';
import { Button, type ButtonProps } from '../primitives';
import type { InlineProps } from './Inline';
import { InlineActionButton } from './InlineActionButton';
import { NotFoundRefHoverCard } from './NotFoundRefHoverCard';
export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
const { inline, context } = props;
const { inline } = props;
const buttonProps: ButtonProps = {
label: inline.data.label,
@@ -20,8 +17,7 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
};
const ButtonImplementation = () => {
// In print/PDF mode, skip interactive action buttons (AI/search providers are not mounted).
if (context.mode !== 'print' && 'action' in inline.data && 'query' in inline.data.action) {
if ('action' in inline.data && 'query' in inline.data.action) {
return (
<InlineActionButton
action={inline.data.action.action}
@@ -61,27 +57,21 @@ export async function InlineLinkButton(
const href =
resolved?.href ??
(inline.data.ref ? resolveContentRefFallback(inline.data.ref)?.href : undefined);
const sharedProps: React.ComponentProps<typeof Button> = {
...buttonProps,
insights: {
type: 'link_click' as const,
link: {
target: inline.data.ref,
position: api.SiteInsightsLinkPosition.Content,
},
},
href,
disabled: href === undefined,
};
const button =
href &&
context.contentContext &&
isSiteAuthLoginHref(context.contentContext.linker, href) ? (
<SiteAuthLoginButton {...sharedProps} />
) : (
<Button {...sharedProps} />
);
const button = (
<Button
{...buttonProps}
insights={{
type: 'link_click',
link: {
target: inline.data.ref,
position: api.SiteInsightsLinkPosition.Content,
},
}}
href={href}
disabled={href === undefined}
/>
);
if (inline.data.ref && !resolved) {
return <NotFoundRefHoverCard context={context}>{button}</NotFoundRefHoverCard>;
@@ -26,7 +26,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return null;
}
const { data, specUrl, publicURL, error } = await resolveOpenAPIOperationBlock({
const { data, specUrl, error } = await resolveOpenAPIOperationBlock({
block,
context: context.contentContext,
});
@@ -46,15 +46,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return (
<BaseOpenAPIOperation
data={data}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
expandAllResponses:
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
expandAllModelSections:
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
})}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -25,7 +25,7 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return null;
}
const { data, specUrl, publicURL, error } = await resolveOpenAPISchemasBlock({
const { data, specUrl, error } = await resolveOpenAPISchemasBlock({
block,
context: context.contentContext,
});
@@ -46,14 +46,9 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return (
<BaseOpenAPISchemas
data={data}
schemas={data.schemas}
grouped={block.data.grouped}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
expandAllModelSections: data['x-expandAllModelSections'],
})}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -26,7 +26,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return null;
}
const { data, specUrl, publicURL, error } = await resolveOpenAPIWebhookBlock({
const { data, specUrl, error } = await resolveOpenAPIWebhookBlock({
block,
context: context.contentContext,
});
@@ -48,15 +48,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return (
<BaseOpenAPIWebhook
data={data}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
expandAllResponses:
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
expandAllModelSections:
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
})}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -2,6 +2,8 @@ import type { JSONDocument } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { type OpenAPIContextInput, checkIsValidLocale } from '@gitbook/react-openapi';
import { tcls } from '@/lib/tailwind';
import type { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { DocumentView } from '../DocumentView';
@@ -22,21 +24,18 @@ import type {
*/
export function getOpenAPIContext(args: {
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>;
specUrl: string | null;
specUrl: string;
context: GitBookAnyContext | undefined;
expandAllResponses?: boolean;
expandAllModelSections?: boolean;
}): OpenAPIContextInput {
const { props, specUrl, context, expandAllResponses, expandAllModelSections } = args;
const { props, specUrl, context } = args;
const { block } = props;
const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE;
const locale = checkIsValidLocale(customizationLocale) ? customizationLocale : DEFAULT_LOCALE;
const proxyUrl =
context && props.context.mode !== 'print'
? context.linker.toAbsoluteURL(context.linker.toPathInSite('~scalar/proxy'))
: undefined;
const proxyUrl = context
? context.linker.toAbsoluteURL(context.linker.toPathInSite('~scalar/proxy'))
: undefined;
return {
specUrl,
@@ -76,7 +75,12 @@ export function getOpenAPIContext(args: {
ancestorBlocks={props.ancestorBlocks}
isEstimatedOffscreen={props.isEstimatedOffscreen}
context={props.context}
style={headingProps.deprecated ? 'line-through' : undefined}
style={tcls([
headingProps.deprecated ? 'line-through' : undefined,
headingProps.deprecated || !!headingProps.stability
? '[&>div]:pt-0'
: undefined,
])}
block={{
object: 'block',
key: `${block.key}-heading`,
@@ -93,8 +97,7 @@ export function getOpenAPIContext(args: {
}}
/>
),
expandAllResponses: expandAllResponses || props.context.mode === 'print',
expandAllModelSections: expandAllModelSections || props.context.mode === 'print',
defaultInteractiveOpened: props.context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
locale,
@@ -4,13 +4,12 @@
/* Layout Components */
.openapi-operation,
.openapi-schemas,
.openapi-webhook,
.openapi-schemas-single {
@apply flex-1 flex flex-col gap-6 mb-14 min-w-0;
.openapi-webhook {
@apply flex-1 flex flex-col gap-8 mb-14 min-w-0;
}
.openapi-schemas:not(.openapi-schemas-single) {
@apply gap-0;
.openapi-schemas {
@apply flex flex-col mb-14 gap-0 flex-1;
}
.openapi-schemas-title {
@@ -34,10 +33,6 @@
@apply flex flex-row gap-2 mt-[0.75em];
}
.openapi-summary-tags + .heading {
@apply pt-0;
}
.openapi-deprecated,
.openapi-stability {
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded straight-corners:rounded-none circular-corners:rounded-sm text-sm leading-[calc(max(1.20em,1.25rem))] before:content-none! after:!content-none;
@@ -149,7 +144,7 @@
}
.openapi-column-preview {
@apply flex flex-col flex-1 xl:max-2xl:pt-20 lg:pt-6 sticky self-start max-h-[calc(100vh-var(--toc-top-offset))] top-(--toc-top-offset);
@apply flex flex-col flex-1 xl:max-2xl:pt-20 lg:py-6 sticky max-h-[calc(100vh-var(--toc-top-offset))] top-(--toc-top-offset);
}
.openapi-column-preview-body {
@@ -238,7 +233,7 @@
}
.openapi-schema-type {
@apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem] whitespace-normal;
@apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem];
}
.openapi-schema-type:only-child {
@@ -4,7 +4,8 @@ import { tcls } from '@/lib/tailwind';
import { RecordColumnValue } from './RecordColumnValue';
import type { TableRecordKV, TableViewProps } from './Table';
import { getColumnWidth } from './layout';
import { getColumnWidth } from './ViewGrid';
import styles from './table.module.css';
import { getColumnVerticalAlignment } from './utils';
export function RecordRow(
@@ -17,15 +18,7 @@ export function RecordRow(
const { view, autoSizedColumns, fixedColumns, block, context } = props;
return (
<div
className={tcls(
'flex',
'border-tint-subtle',
'transition-colors',
'hover:bg-tint-hover'
)}
role="row"
>
<div className={styles.row} role="row">
{view.columns.map((column) => {
const columnWidth = getColumnWidth({
column,
@@ -40,10 +33,7 @@ export function RecordRow(
<div
key={column}
role="cell"
className={tcls(
'relative flex flex-1 border-r px-3 py-2 align-middle text-sm last:border-r-0',
'border-tint-subtle'
)}
className={tcls(styles.cell)}
style={{
width: columnWidth,
minWidth: columnWidth || '100px',
@@ -1,151 +0,0 @@
'use client';
import { useScrollListener } from '@/components/hooks/useScrollListener';
import { tcls } from '@/lib/tailwind';
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
interface StickyViewGridProps {
className?: string;
header: ReactNode;
tableClassName?: string;
children: ReactNode;
}
export function StickyViewGrid({
className,
header,
tableClassName,
children,
}: StickyViewGridProps) {
const rootRef = useRef<HTMLDivElement>(null);
const stickyHeaderRef = useRef<HTMLDivElement>(null);
const bodyScrollRef = useRef<HTMLDivElement>(null);
const bodyTableRef = useRef<HTMLDivElement>(null);
const onStickyHeaderWheel = useCallback((event: WheelEvent) => {
const bodyScrollElement = bodyScrollRef.current;
if (!bodyScrollElement) {
return;
}
const horizontalDelta = event.deltaX || (event.shiftKey && event.deltaY ? event.deltaY : 0);
if (horizontalDelta === 0) {
return;
}
bodyScrollElement.scrollLeft += horizontalDelta;
event.preventDefault();
}, []);
useEffect(() => {
const stickyHeaderElement = stickyHeaderRef.current;
if (!stickyHeaderElement) {
return;
}
stickyHeaderElement.addEventListener('wheel', onStickyHeaderWheel, { passive: false });
return () => {
stickyHeaderElement.removeEventListener('wheel', onStickyHeaderWheel);
};
}, [onStickyHeaderWheel]);
const syncStickyLayout = useCallback(() => {
const rootElement = rootRef.current;
const bodyScrollElement = bodyScrollRef.current;
const bodyTableElement = bodyTableRef.current;
if (!rootElement || !bodyScrollElement || !bodyTableElement) {
return;
}
rootElement.style.setProperty(
'--table-sticky-scroll-left',
`${-bodyScrollElement.scrollLeft}px`
);
rootElement.style.setProperty(
'--table-sticky-table-width',
`${bodyTableElement.scrollWidth}px`
);
rootElement.dataset.scrollable = `${
bodyScrollElement.scrollWidth > bodyScrollElement.clientWidth + 1
}`;
}, []);
useLayoutEffect(() => {
syncStickyLayout();
}, [syncStickyLayout]);
useScrollListener(syncStickyLayout, bodyScrollRef);
useEffect(() => {
const bodyScrollElement = bodyScrollRef.current;
const bodyTableElement = bodyTableRef.current;
if (!bodyScrollElement && !bodyTableElement) {
return;
}
const resizeObserver = new ResizeObserver(syncStickyLayout);
if (bodyScrollElement) {
resizeObserver.observe(bodyScrollElement);
}
if (bodyTableElement) {
resizeObserver.observe(bodyTableElement);
}
return () => {
resizeObserver.disconnect();
};
}, [syncStickyLayout]);
return (
<div className={className}>
<div
ref={rootRef}
className={tcls(
'group/table relative flex w-full min-w-0 max-w-full flex-col rounded-lg border-tint-subtle',
'data-[scrollable=true]:border'
)}
data-scrollable="false"
>
<div
ref={stickyHeaderRef}
className={tcls(
'-mx-px sticky z-10 w-full min-w-0 max-w-full overflow-hidden rounded-t-[inherit] px-px',
'[top:var(--toc-top-offset,var(--outline-top-offset,0px))]'
)}
>
<div
className={tcls(
'flex',
'flex-col',
tableClassName ?? 'w-fit',
'[transform:translateX(var(--table-sticky-scroll-left,0px))]'
)}
style={{ width: 'var(--table-sticky-table-width)' }}
>
{header}
</div>
</div>
<div
ref={bodyScrollRef}
className={tcls(
'w-full min-w-0 overflow-x-auto overflow-y-hidden overscroll-x-none border-tint-subtle',
'group-data-[scrollable=true]/table:mx-px',
'group-data-[scrollable=true]/table:border-0',
'group-data-[scrollable=true]/table:rounded-none'
)}
>
<div
ref={bodyTableRef}
className={tcls('flex', 'flex-col', tableClassName ?? 'w-fit')}
>
{children}
</div>
</div>
</div>
</div>
);
}
@@ -1,14 +1,10 @@
import type { DocumentBlockTable, DocumentTableRecord } from '@gitbook/api';
import assertNever from 'assert-never';
import { tcls } from '@/lib/tailwind';
import type { BlockProps } from '../Block';
import { isBlockOffscreen } from '../utils';
import { StickyViewGrid } from './StickyViewGrid';
import { ViewCards } from './ViewCards';
import { ViewGrid, ViewGridHeader } from './ViewGrid';
import { getViewGridLayout, hasVisibleHeader } from './layout';
import { ViewGrid } from './ViewGrid';
export type TableRecordKV = [string, DocumentTableRecord];
@@ -19,7 +15,7 @@ export interface TableViewProps<View> extends BlockProps<DocumentBlockTable> {
}
export function Table(props: BlockProps<DocumentBlockTable>) {
const { block, ancestorBlocks, document, context, style } = props;
const { block, ancestorBlocks, document } = props;
const isOffscreen = isBlockOffscreen({ block, ancestorBlocks, document });
const records: TableRecordKV[] = Object.entries(block.data.records).sort((a, b) => {
@@ -36,70 +32,15 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
{...props}
/>
);
case 'grid': {
const gridProps = {
...props,
view: block.data.view,
isOffscreen,
records,
};
const { tableWidth } = getViewGridLayout({
block,
view: block.data.view,
mode: context.mode,
});
const tableContainerClassName =
tableWidth === 'w-full' ? 'min-w-full w-fit' : tableWidth;
const withHeader = hasVisibleHeader(block, block.data.view);
const withStickyHeader =
withHeader && context.mode !== 'print' && block.data.view.stickyHeader === true;
if (withStickyHeader) {
return (
<StickyViewGrid
className={tcls(style, 'relative mx-auto grid w-full min-w-0')}
tableClassName={tableContainerClassName}
header={
<ViewGridHeader
{...gridProps}
tableClassName={tableContainerClassName}
className={tcls(
'mb-0 rounded-b-none border-t border-r border-l',
'group-data-[scrollable=false]/table:mb-1',
'group-data-[scrollable=false]/table:rounded-b-lg',
'group-data-[scrollable=true]/table:border-t-0',
'group-data-[scrollable=true]/table:border-r-0',
'group-data-[scrollable=true]/table:border-l-0'
)}
/>
}
>
<ViewGrid {...gridProps} tableClassName={tableContainerClassName} />
</StickyViewGrid>
);
}
case 'grid':
return (
<div className={tcls(style, 'relative mx-auto grid w-full min-w-0')}>
<div
className={tcls(
'w-full min-w-0 overflow-x-auto overflow-y-hidden overscroll-x-none border-tint-subtle '
)}
>
<div className={tcls('flex', 'flex-col', tableContainerClassName)}>
{withHeader ? (
<ViewGridHeader
{...gridProps}
tableClassName={tableContainerClassName}
className={tableContainerClassName}
/>
) : null}
<ViewGrid {...gridProps} tableClassName={tableContainerClassName} />
</div>
</div>
</div>
<ViewGrid
view={block.data.view}
isOffscreen={isOffscreen}
records={records}
{...props}
/>
);
}
default:
assertNever(block.data.view);
}
@@ -4,98 +4,118 @@ import { tcls } from '@/lib/tailwind';
import { RecordRow } from './RecordRow';
import type { TableViewProps } from './Table';
import { getColumnWidth, getViewGridLayout } from './layout';
import styles from './table.module.css';
import { getColumnAlignment } from './utils';
interface ViewGridHeaderProps extends TableViewProps<DocumentTableViewGrid> {
className?: string;
tableClassName?: string;
}
/* Columns are sized in 3 ways:
1. Set to auto-size by default, these columns share the available width
2. Explicitly set by the user by dragging column separator (we then turn off auto-size)
3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px
*/
export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
const { block, view, records, style, context } = props;
interface ViewGridProps extends TableViewProps<DocumentTableViewGrid> {
tableClassName?: string;
}
/* Calculate how many columns are auto-sized vs fixed width */
const columnWidths = context.mode === 'print' ? undefined : view.columnWidths;
const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]);
const fixedColumns = view.columns.filter((column) => columnWidths?.[column]);
export function ViewGridHeader(props: ViewGridHeaderProps) {
const { block, view, context, className, tableClassName } = props;
const { tableWidth, columnWidths, autoSizedColumns, fixedColumns } = getViewGridLayout({
block,
view,
mode: context.mode,
});
const tableWidth = autoSizedColumns.length > 0 ? 'w-full' : 'w-fit';
/* Only show the header when configured and not empty */
const withHeader =
!view.hideHeader &&
view.columns.some(
(columnId) => (block.data.definition[columnId]?.title.trim().length ?? 0) > 0
);
return (
<div
role="rowgroup"
className={tcls(
tableClassName ?? tableWidth,
'mb-1 flex flex-col rounded-lg border border-tint-subtle bg-tint',
className
)}
>
<div role="row" className={tcls('flex', 'w-full')}>
{view.columns.map((column) => {
const definition = block.data.definition[column];
if (!definition) {
return null;
}
return (
<div
key={column}
role="columnheader"
className={tcls(
'px-3 py-2 font-medium text-sm text-tint-strong',
getColumnAlignment(definition)
)}
style={{
width: getColumnWidth({
column,
columnWidths,
autoSizedColumns,
fixedColumns,
}),
minWidth: columnWidths?.[column] || '100px',
}}
title={definition.title}
>
{definition.title}
<div className={tcls(style, styles.tableWrapper)}>
{/* Table */}
<div role="table" className={tcls('flex', 'flex-col')}>
{/* Header */}
{withHeader && (
<div
role="rowgroup"
className={tcls(
tableWidth,
styles.rowGroup,
'straight-corners:rounded-none',
'circular-corners:rounded-xl'
)}
>
<div role="row" className={tcls('flex', 'w-full')}>
{view.columns.map((column) => {
const definition = block.data.definition[column]!;
return (
<div
key={column}
role="columnheader"
className={tcls(
styles.columnHeader,
getColumnAlignment(definition)
)}
style={{
width: getColumnWidth({
column,
columnWidths,
autoSizedColumns,
fixedColumns,
}),
minWidth: columnWidths?.[column] || '100px',
}}
title={definition.title}
>
{definition.title}
</div>
);
})}
</div>
);
})}
</div>
)}
<div
role="rowgroup"
className={tcls('flex', 'flex-col', tableWidth, '[&>*+*]:border-t')}
>
{records.map((record) => (
<RecordRow
key={record[0]}
record={record}
autoSizedColumns={autoSizedColumns}
fixedColumns={fixedColumns}
{...props}
/>
))}
</div>
</div>
</div>
);
}
export function ViewGrid(props: ViewGridProps) {
const { block, view, records, context, tableClassName } = props;
const { tableWidth, autoSizedColumns, fixedColumns } = getViewGridLayout({
block,
view,
mode: context.mode,
});
export const getColumnWidth = ({
column,
columnWidths,
autoSizedColumns,
fixedColumns,
}: {
column: string;
columnWidths: Record<string, number> | undefined;
autoSizedColumns: string[];
fixedColumns: string[];
}) => {
const columnWidth = columnWidths?.[column];
const body = (
<div
role="rowgroup"
className={tcls('flex', 'flex-col', tableClassName ?? tableWidth, '[&>*+*]:border-t')}
>
{records.map((record) => (
<RecordRow
key={record[0]}
record={record}
autoSizedColumns={autoSizedColumns}
fixedColumns={fixedColumns}
{...props}
/>
))}
</div>
);
/* Column was explicitly set by user or user turned off auto-sizing (in that case, columnWidth should've also been set to 100px) */
if (columnWidth) return `${columnWidth}px`;
return (
<div role="table" className={tcls('flex', 'flex-col', tableClassName ?? tableWidth)}>
{body}
</div>
);
}
/* Fallback minimum width for columns, so the columns don't become unreadable from being too narrow and instead table will become scrollable. */
const minAutoColumnWidth = '100px';
const totalFixedWidth = fixedColumns.reduce((sum, col) => {
return sum + (columnWidths?.[col] || 0);
}, 0);
/* Column should use auto-sizing, which means it grows to fill available space */
const availableWidth = `calc((100% - ${totalFixedWidth}px) / ${autoSizedColumns.length})`;
return `clamp(${minAutoColumnWidth}, ${availableWidth}, 100%)`;
};
@@ -1,68 +0,0 @@
import type { DocumentBlockTable, DocumentTableViewGrid } from '@gitbook/api';
import type { BlockProps } from '../Block';
export function hasVisibleHeader(block: DocumentBlockTable, view: DocumentTableViewGrid): boolean {
return (
!view.hideHeader &&
view.columns.some(
(columnId) => (block.data.definition[columnId]?.title.trim().length ?? 0) > 0
)
);
}
/* Columns are sized in 3 ways:
1. Set to auto-size by default, these columns share the available width
2. Explicitly set by the user by dragging column separator (we then turn off auto-size)
3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px
*/
export function getViewGridLayout({
block,
view,
mode,
}: {
block: DocumentBlockTable;
view: DocumentTableViewGrid;
mode: BlockProps<DocumentBlockTable>['context']['mode'];
}) {
const columnWidths = mode === 'print' ? undefined : view.columnWidths;
const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]);
const fixedColumns = view.columns.filter((column) => columnWidths?.[column]);
const tableWidth = autoSizedColumns.length > 0 ? 'w-full' : 'w-fit';
return {
columnWidths,
autoSizedColumns,
fixedColumns,
tableWidth,
withHeader: hasVisibleHeader(block, view),
};
}
export const getColumnWidth = ({
column,
columnWidths,
autoSizedColumns,
fixedColumns,
}: {
column: string;
columnWidths: Record<string, number> | undefined;
autoSizedColumns: string[];
fixedColumns: string[];
}) => {
const columnWidth = columnWidths?.[column];
/* Column was explicitly set by user or user turned off auto-sizing (in that case, columnWidth should've also been set to 100px) */
if (columnWidth) return `${columnWidth}px`;
/* Fallback minimum width for columns, so the columns don't become unreadable from being too narrow and instead table will become scrollable. */
const minAutoColumnWidth = '100px';
const totalFixedWidth = fixedColumns.reduce((sum, col) => {
return sum + (columnWidths?.[col] || 0);
}, 0);
/* Column should use auto-sizing, which means it grows to fill available space */
const availableWidth = `calc((100% - ${totalFixedWidth}px) / ${autoSizedColumns.length})`;
return `clamp(${minAutoColumnWidth}, ${availableWidth}, 100%)`;
};
@@ -0,0 +1,45 @@
@reference "../../RootLayout/globals.css";
/* Detect whether a scrollbar exists on the table */
@keyframes detect-scroll {
from,
to {
--can-scroll: ;
}
}
/* Apply styles to the Table if scrollbar exists */
.tableWrapper {
animation: detect-scroll linear;
animation-timeline: scroll(self x);
--border-radius-if-can-scroll: var(--can-scroll) 0.375rem;
--border-radius-if-cant-scroll: 0;
border-radius: var(--border-radius-if-can-scroll, var(--border-radius-if-cant-scroll));
--border-width-if-can-scroll: var(--can-scroll) 1px;
--border-width-if-cant-scroll: 0;
border-width: var(--border-width-if-can-scroll, var(--border-width-if-cant-scroll));
@apply relative grid w-full overflow-x-auto overflow-y-hidden mx-auto border-tint-subtle;
}
.columnHeader {
@apply text-sm font-medium py-2 px-3 text-tint-strong;
}
.row {
@apply flex border-tint-subtle hover:bg-tint-hover transition-colors;
}
.rowGroup {
@apply flex flex-col border rounded-lg bg-tint border-tint-subtle mb-1;
}
.cell {
@apply flex-1 flex align-middle border-tint-subtle py-2 px-3 text-sm relative;
}
.cell:not(:last-child) {
@apply border-r;
}
@@ -1,12 +1,7 @@
import {
getFullDateFormatter,
getNumericDateFormatter,
getShortDateFormatter,
} from '@/components/utils/dates';
import { formatDateFull, formatDateShort, formatNumericDate } from '@/components/utils/dates';
import { getRevisionTags, resolveBlockTags } from '@/lib/tags';
import { tcls } from '@/lib/tailwind';
import type { DocumentBlockUpdate, DocumentBlockUpdates, TranslationLanguage } from '@gitbook/api';
import { assertNever } from 'assert-never';
import type { DocumentBlockUpdate, DocumentBlockUpdates } from '@gitbook/api';
import { assert } from 'ts-essentials';
import { Tag } from '../Tag';
import type { BlockProps } from './Block';
@@ -36,7 +31,11 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
// Then get the format from the parent Updates block and use that format
const dateFormat = parentUpdates.data?.format ?? 'full';
const dateFormatter = getDateFormatter(dateFormat, contextProps.context.contentContext?.locale);
const displayDate = {
numeric: formatNumericDate(parsedDate),
full: formatDateFull(parsedDate),
short: formatDateShort(parsedDate),
}[dateFormat];
// Resolve tags from the block data using revision-level tag definitions
const revisionTags = getRevisionTags(contextProps.context.contentContext?.revision);
@@ -55,7 +54,7 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
dateTime={date}
className="inline-flex items-center font-medium text-neutral-10 text-sm tracking-wide"
>
{dateFormatter.format(parsedDate)}
{displayDate}
</time>
{resolvedTags.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1.5">
@@ -75,22 +74,6 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
);
}
function getDateFormatter(
format: 'numeric' | 'full' | 'short',
locale: TranslationLanguage | undefined
) {
switch (format) {
case 'short':
return getShortDateFormatter(locale);
case 'numeric':
return getNumericDateFormatter(locale);
case 'full':
return getFullDateFormatter(locale);
default:
assertNever(format);
}
}
/**
* Parse an ISO date string into a Date object.
*/
@@ -43,7 +43,7 @@ export function EmbeddableAIChat(props: EmbeddableAIChatProps) {
const chat = useAIChatState();
const { config: siteConfig } = useAI();
const chatController = useAIChatController();
const embedConfig = useEmbeddableConfiguration((state) => state);
const embedConfig = useEmbeddableConfiguration();
const language = useLanguage();
React.useEffect(() => {
@@ -12,7 +12,7 @@ type EmbeddableAIContextProviderProps = PropsWithChildren<AIConfig>;
*/
export function EmbeddableAIContextProvider(props: EmbeddableAIContextProviderProps) {
const { aiMode, suggestions, greeting, trademark, children } = props;
const embedConfig = useEmbeddableConfiguration((state) => state);
const embedConfig = useEmbeddableConfiguration();
return (
<AIContextProvider
@@ -3,10 +3,8 @@ import type { GitBookSiteContext } from '@/lib/context';
import { SiteInsightsDisplayContext } from '@gitbook/api';
import type { Metadata } from 'next';
import { HeaderMobileMenu } from '../Header/HeaderMobileMenu';
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
import { PageBody } from '../PageBody';
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
import { categorizeVariants } from '../SpaceLayout/categorizeVariants';
import { TableOfContents } from '../TableOfContents';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { EmbeddableDocsPageControlButtons } from './EmbeddableDocsPageControlButtons';
@@ -45,8 +43,6 @@ export async function EmbeddableDocsPage(
pageParams,
});
const variants = categorizeVariants(context);
return (
<EmbeddableFrame className="site-background">
<EmbeddableFrameSidebar>
@@ -80,20 +76,7 @@ export async function EmbeddableDocsPage(
<SiteSectionTabs
className="not-theme-bold:-mt-2 theme-bold:bg-tint-base"
sections={encodeClientSiteSections(context, context.sections)}
>
{variants.translations.length > 1 ? (
<TranslationsDropdown
context={context}
siteSpace={
variants.translations.find(
(space) => space.id === context.siteSpace.id
) ?? context.siteSpace
}
siteSpaces={variants.translations}
className="my-1.5 ml-2 self-start"
/>
) : null}
</SiteSectionTabs>
/>
) : null}
</div>
<EmbeddableFrameBody>
@@ -104,38 +87,7 @@ export async function EmbeddableDocsPage(
leading={{ fade: false, button: true }}
trailing={{ fade: false, button: true }}
>
<TableOfContents
context={context}
withTrademark={false}
header={
!context.sections && variants.translations.length > 1 ? (
<TranslationsDropdown
context={context}
siteSpace={
variants.translations.find(
(space) => space.id === context.siteSpace.id
) ?? context.siteSpace
}
siteSpaces={variants.translations}
className="max-md:[&_.button-content]:block"
/>
) : null
}
innerHeader={
variants.generic.length > 1 ? (
<div className="my-5 sidebar-default:mt-2 flex flex-col gap-2 px-5 empty:hidden">
{variants.generic.length > 1 ? (
<SpacesDropdown
context={context}
siteSpace={context.siteSpace}
siteSpaces={variants.generic}
className="w-full px-3"
/>
) : null}
</div>
) : null
}
/>
<TableOfContents context={context} withTrademark={false} />
<PageBody
context={context}
page={page}
@@ -5,7 +5,6 @@ import React, { useEffect, useRef } from 'react';
import { useAI, useAIChatController } from '@/components/AI';
import { CustomizationAIMode } from '@gitbook/api';
import { useTheme } from 'next-themes';
import { useRouter } from 'next/navigation';
import { createStore, useStore } from 'zustand';
import { integrationsAssistantTools } from '../Integrations';
@@ -37,12 +36,11 @@ export function EmbeddableIframeAPI(props: {
const router = useRouter();
const chatController = useAIChatController();
const { setTheme } = useTheme();
// Live ref to avoid adding them as dependencies
const refs = useRef({ router, chatController, baseURL, setTheme });
const refs = useRef({ router, chatController, baseURL });
useEffect(() => {
refs.current = { router, chatController, baseURL, setTheme };
refs.current = { router, chatController, baseURL };
});
React.useEffect(() => {
@@ -59,7 +57,7 @@ export function EmbeddableIframeAPI(props: {
}
channel.receive((payload) => {
const { baseURL, router, chatController, setTheme } = refs.current;
const { baseURL, router, chatController } = refs.current;
const message = payload as ParentToFrameMessage;
log('[gitbook] received message', message);
@@ -80,15 +78,6 @@ export function EmbeddableIframeAPI(props: {
integrationsAssistantTools.setState({
tools: message.settings.tools,
});
if (Object.prototype.hasOwnProperty.call(message.settings, 'colorScheme')) {
const colorScheme = message.settings.colorScheme;
if (!colorScheme) {
setTheme('system');
} else {
setTheme(colorScheme);
}
}
break;
}
case 'navigateToPage': {
@@ -106,11 +95,14 @@ export function EmbeddableIframeAPI(props: {
return null;
}
/** Subscribe to embed configuration from the parent (Zustand). */
export function useEmbeddableConfiguration<T>(
selector: (state: GitBookEmbeddableConfiguration) => T
): T {
return useStore(embeddableConfiguration, selector);
/**
* Hook to get the configuration from the parent window.
*/
export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
// @ts-expect-error - This is a workaround to allow the function to be optional.
fn: (state: GitBookEmbeddableConfiguration) => T = (state) => state
) {
return useStore(embeddableConfiguration, fn);
}
/**
@@ -159,7 +151,7 @@ export function EmbeddableIframeTabs(props: {
siteTitle: string;
}) {
const { ref, active = 'assistant', baseURL, siteTitle } = props;
const { tabs: configuredTabs, actions } = useEmbeddableConfiguration((state) => state);
const { tabs: configuredTabs, actions } = useEmbeddableConfiguration();
const { assistants, config } = useAI();
@@ -231,7 +223,7 @@ export function EmbeddableIframeTabs(props: {
}
export function EmbeddableIframeCloseButton() {
const { closeButton } = useEmbeddableConfiguration((state) => state);
const { closeButton } = useEmbeddableConfiguration();
if (!closeButton) {
return null;
@@ -37,7 +37,6 @@ export async function EmbeddableRootLayout({
? undefined
: context.customization.themes.default
}
defaultTheme={context.customization.themes.default}
externalLinksTarget={context.customization.externalLinks.target}
contextId={context.contextId}
proxyOrigin={context.site.proxy?.origin}
@@ -8,7 +8,6 @@ import {
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { getLocalizedTitle } from '@/lib/sites';
import { Link } from '../primitives';
export function FooterLinksGroup(props: {
@@ -16,11 +15,10 @@ export function FooterLinksGroup(props: {
context: GitBookAnyContext;
}) {
const { group, context } = props;
const title = getLocalizedTitle(group, context.locale);
return (
<nav className="flex flex-col gap-4 text-sm">
<h4 className="font-semibold">{title}</h4>
<h4 className="font-semibold">{group.title}</h4>
<ul className="flex flex-col items-start gap-4">
{group.links.map((link, index) => {
return (
@@ -36,7 +34,6 @@ export function FooterLinksGroup(props: {
async function FooterLink(props: { link: CustomizationContentLink; context: GitBookAnyContext }) {
const { link, context } = props;
const title = getLocalizedTitle(link, context.locale);
const resolved = await resolveContentRef(link.to, context);
return (
@@ -65,7 +62,7 @@ async function FooterLink(props: { link: CustomizationContentLink; context: GitB
},
}}
>
{title}
{link.title}
</Link>
);
}
@@ -1,4 +1,3 @@
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
import type { GitBookSiteContext } from '@/lib/context';
import {
type ContentRef,
@@ -8,16 +7,10 @@ import {
SiteInsightsLinkPosition,
} from '@gitbook/api';
import assertNever from 'assert-never';
import type React from 'react';
import { resolveContentRef } from '@/lib/references';
import { getLocalizedTitle } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import {
SiteAuthLoginButton,
SiteAuthLoginDropdownMenuItem,
SiteAuthLoginLink,
} from '../SiteAuth/SiteAuthLoginLink';
import { Button, Link, ToggleChevron } from '../primitives';
import {
type DropdownButtonProps,
@@ -35,7 +28,6 @@ export async function HeaderLink(props: {
const target = link.to ? await resolveContentRef(link.to, context) : null;
const headerPreset = customization.header.preset;
const linkStyle = link.style ?? 'link';
const title = getLocalizedTitle(link, context.locale);
if (link.links && link.links.length > 0) {
return (
@@ -43,16 +35,15 @@ export async function HeaderLink(props: {
className={`shrink ${customization.styling.search === 'prominent' ? 'right-0 left-auto' : null}`}
button={
!target || !link.to ? (
<HeaderItemDropdown headerPreset={headerPreset} title={title} />
<HeaderItemDropdown headerPreset={headerPreset} title={link.title} />
) : (
<HeaderLinkNavItem
linkTarget={link.to}
linkStyle={linkStyle}
headerPreset={headerPreset}
title={title}
title={link.title}
isDropdown
href={target?.href}
isSiteAuthLoginHref={isSiteAuthLoginHref(context.linker, target.href)}
/>
)
}
@@ -74,10 +65,9 @@ export async function HeaderLink(props: {
linkTarget={link.to}
linkStyle={linkStyle}
headerPreset={headerPreset}
title={title}
title={link.title}
isDropdown={false}
href={target?.href}
isSiteAuthLoginHref={target ? isSiteAuthLoginHref(context.linker, target.href) : false}
/>
);
}
@@ -89,7 +79,6 @@ export type HeaderLinkNavItemProps = {
title: string;
href?: string;
isDropdown: boolean;
isSiteAuthLoginHref: boolean;
} & DropdownButtonProps<HTMLElement>;
function HeaderLinkNavItem(props: HeaderLinkNavItemProps) {
@@ -110,16 +99,7 @@ function HeaderItemButton(
linkStyle: 'button-secondary' | 'button-primary';
}
) {
const {
linkTarget,
linkStyle,
headerPreset,
title,
href,
isDropdown,
isSiteAuthLoginHref,
...rest
} = props;
const { linkTarget, linkStyle, headerPreset, title, href, isDropdown, ...rest } = props;
const variant = (() => {
switch (linkStyle) {
case 'button-secondary':
@@ -130,25 +110,21 @@ function HeaderItemButton(
assertNever(linkStyle);
}
})();
const sharedProps: React.ComponentProps<typeof Button> = {
href,
variant,
size: 'medium' as const,
insights: {
type: 'link_click' as const,
link: {
target: linkTarget,
position: SiteInsightsLinkPosition.Header,
},
},
label: title,
...rest,
};
return isSiteAuthLoginHref ? (
<SiteAuthLoginButton {...sharedProps} />
) : (
<Button {...sharedProps} />
return (
<Button
href={href}
variant={variant}
size="medium"
insights={{
type: 'link_click',
link: {
target: linkTarget,
position: SiteInsightsLinkPosition.Header,
},
}}
label={title}
{...rest}
/>
);
}
@@ -178,28 +154,20 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese
}
function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
const { linkTarget, headerPreset, title, isDropdown, href, isSiteAuthLoginHref, ...rest } =
props;
const sharedProps = {
href: href ?? '#',
className: getHeaderLinkClassName({ headerPreset }),
insights: {
type: 'link_click' as const,
link: {
target: linkTarget,
position: SiteInsightsLinkPosition.Header,
},
},
...rest,
};
return isSiteAuthLoginHref ? (
<SiteAuthLoginLink {...sharedProps}>
{title}
{isDropdown ? <ToggleChevron /> : null}
</SiteAuthLoginLink>
) : (
<Link {...sharedProps}>
const { linkTarget, headerPreset, title, isDropdown, href, ...rest } = props;
return (
<Link
href={href ?? '#'}
className={getHeaderLinkClassName({ headerPreset })}
insights={{
type: 'link_click',
link: {
target: linkTarget,
position: SiteInsightsLinkPosition.Header,
},
}}
{...rest}
>
{title}
{isDropdown ? <ToggleChevron /> : null}
</Link>
@@ -236,21 +204,18 @@ async function SubHeaderLink(props: {
return null;
}
const title = getLocalizedTitle(link, context.locale);
const sharedProps = {
href: target.href,
insights: {
type: 'link_click' as const,
link: {
target: link.to,
position: SiteInsightsLinkPosition.Header,
},
},
};
return isSiteAuthLoginHref(context.linker, target.href) ? (
<SiteAuthLoginDropdownMenuItem {...sharedProps}>{title}</SiteAuthLoginDropdownMenuItem>
) : (
<DropdownMenuItem {...sharedProps}>{title}</DropdownMenuItem>
return (
<DropdownMenuItem
href={target.href}
insights={{
type: 'link_click',
link: {
target: link.to,
position: SiteInsightsLinkPosition.Header,
},
}}
>
{link.title}
</DropdownMenuItem>
);
}
@@ -1,4 +1,3 @@
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
import type { GitBookSiteContext } from '@/lib/context';
import {
type CustomizationContentLink,
@@ -10,11 +9,9 @@ import { Icon } from '@gitbook/icons';
import type React from 'react';
import { resolveContentRef } from '@/lib/references';
import { getLocalizedTitle } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { SocialAccountLink } from '../Footer/SocialAccounts';
import { SiteAuthLoginDropdownMenuItem } from '../SiteAuth/SiteAuthLoginLink';
import { ToggleChevron } from '../primitives';
import {
DropdownMenu,
@@ -86,32 +83,30 @@ async function MoreMenuLink(props: {
}) {
const { context, link } = props;
const title = getLocalizedTitle(link, context.locale);
const target = link.to ? await resolveContentRef(link.to, context) : null;
const sharedProps = {
href: target?.href,
insights: link.to
? {
type: 'link_click' as const,
link: {
target: link.to,
position: SiteInsightsLinkPosition.Header,
},
}
: undefined,
};
return 'links' in link && link.links.length > 0 ? (
<DropdownSubMenu label={title}>
<DropdownSubMenu label={link.title}>
{link.links.map((subLink, index) => {
return <MoreMenuLink key={index} {...props} link={subLink} />;
})}
</DropdownSubMenu>
) : isSiteAuthLoginHref(context.linker, target?.href) && sharedProps.href ? (
<SiteAuthLoginDropdownMenuItem {...sharedProps} href={sharedProps.href}>
{title}
</SiteAuthLoginDropdownMenuItem>
) : (
<DropdownMenuItem {...sharedProps}>{title}</DropdownMenuItem>
<DropdownMenuItem
href={target?.href}
insights={
link.to
? {
type: 'link_click',
link: {
target: link.to,
position: SiteInsightsLinkPosition.Header,
},
}
: undefined
}
>
{link.title}
</DropdownMenuItem>
);
}
@@ -2,7 +2,7 @@ import type { SiteSpace } from '@gitbook/api';
import { useMemo } from 'react';
import type { GitBookSiteContext } from '@/lib/context';
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
import { getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { Button, type ButtonProps, ToggleChevron } from '../primitives';
import { DropdownMenu } from '../primitives/DropdownMenu';
@@ -24,7 +24,6 @@ export function SpacesDropdown(props: {
icon?: ButtonProps['icon'];
}) {
const { context, siteSpace, siteSpaces, className, variant = 'secondary', icon } = props;
const currentLanguage = context.locale;
return (
<DropdownMenu
@@ -41,16 +40,14 @@ export function SpacesDropdown(props: {
trailing={<ToggleChevron />}
className={tcls('bg-tint-base', className)}
>
<span className="button-content">
{getLocalizedTitle(siteSpace, currentLanguage)}
</span>
<span className="button-content">{siteSpace.title}</span>
</Button>
}
>
<SpacesDropdownMenuItems
slimSpaces={siteSpaces.map((siteSp) => ({
id: siteSp.id,
title: getLocalizedTitle(siteSp, currentLanguage),
title: siteSp.title,
url: getSiteSpaceURL(context, siteSp),
isActive: siteSp.id === siteSpace.id,
spaceId: siteSp.space.id,
@@ -70,8 +67,7 @@ export function TranslationsDropdown(props: {
const { context, siteSpace, siteSpaces, className } = props;
// Memoize the emoji check to avoid repeated regex execution
const title = getLocalizedTitle(siteSpace, context.locale);
const hasEmojiPrefix = useMemo(() => startsWithEmoji(title), [title]);
const hasEmojiPrefix = useMemo(() => startsWithEmoji(siteSpace.title), [siteSpace.title]);
return (
<SpacesDropdown
@@ -1,6 +1,6 @@
import { CustomizationRootLayout } from '@/components/RootLayout';
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
import { CustomizationDefaultThemeMode } from '@gitbook/api';
import { CustomizationThemeMode } from '@gitbook/api';
/**
* Layout to be used for rendering the PDF.
@@ -12,10 +12,7 @@ export async function PDFRootLayout(props: {
const { context, children } = props;
return (
<CustomizationRootLayout
context={context}
forcedTheme={CustomizationDefaultThemeMode.Light}
>
<CustomizationRootLayout context={context} forcedTheme={CustomizationThemeMode.Light}>
{children}
</CustomizationRootLayout>
);
@@ -271,54 +271,6 @@ export function ActionOpenMCP(props: {
);
}
/**
* Action to copy the MCP install command for a CLI-based provider.
*/
export function ActionCopyMCPCommand(props: {
siteTitle: string;
mcpURL: string;
provider: 'claude-code' | 'codex';
type: PageActionType;
}) {
const { siteTitle, provider, mcpURL, type } = props;
const language = useLanguage();
const slug =
siteTitle
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '') || 'docs';
const providerInfo = React.useMemo<{ label: string; icon: IconName; command: string }>(() => {
switch (provider) {
case 'claude-code':
return {
label: 'Claude Code',
icon: 'claude',
command: `claude mcp add ${slug} --scope user --transport http ${mcpURL}`,
};
case 'codex':
return {
label: 'Codex',
icon: 'chatgpt',
command: `codex mcp add ${slug} --url ${mcpURL}`,
};
default:
assertNever(provider);
}
}, [provider, slug, mcpURL]);
return (
<CopyToClipboard
type={type}
data={providerInfo.command}
label={tString(language, 'connect_mcp_to', providerInfo.label)}
description={tString(language, 'copy_mcp_install_command', providerInfo.label)}
icon={providerInfo.icon}
/>
);
}
/**
* Action to view the page as a PDF.
*/
@@ -8,7 +8,6 @@ import React, { useRef } from 'react';
import { useAI } from '../AI';
import { ToggleChevron } from '../primitives';
import {
ActionCopyMCPCommand,
ActionCopyMCPURL,
ActionCopyMarkdown,
ActionOpenAssistant,
@@ -122,18 +121,6 @@ function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNod
siteTitle={siteTitle}
type="dropdown-menu-item"
/>
<ActionCopyMCPCommand
provider="claude-code"
mcpURL={urls.mcp}
siteTitle={siteTitle}
type="dropdown-menu-item"
/>
<ActionCopyMCPCommand
provider="codex"
mcpURL={urls.mcp}
siteTitle={siteTitle}
type="dropdown-menu-item"
/>
</React.Fragment>
) : null,
@@ -185,7 +185,7 @@ function getPageActionsURLs({
: undefined,
mcp:
context.site.visibility !== SiteVisibility.VisitorAuth
? context.linker.toAbsoluteURL(context.linker.toPathInSite('~gitbook/mcp'))
? context.linker.toAbsoluteURL(context.linker.toPathInSpace('~gitbook/mcp'))
: undefined,
};
}
@@ -1,8 +1,8 @@
import {
CustomizationDefaultThemeMode,
CustomizationIconsStyle,
CustomizationSidebarBackgroundStyle,
CustomizationSidebarListStyle,
CustomizationThemeMode,
type CustomizationThemedColor,
type CustomizationTint,
type SiteCustomizationSettings,
@@ -61,7 +61,7 @@ export async function CustomizationRootLayout(props: {
htmlClassName?: string;
/** The class name to apply to the body element. */
bodyClassName?: string;
forcedTheme?: CustomizationDefaultThemeMode | null;
forcedTheme?: CustomizationThemeMode | null;
context: GitBookAnyContext;
children: React.ReactNode;
}) {
@@ -108,7 +108,7 @@ export async function CustomizationRootLayout(props: {
: null,
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
(forcedTheme ?? customization.themes.default) === CustomizationDefaultThemeMode.Dark
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
? 'dark'
: '',
htmlClassName
@@ -319,7 +319,7 @@ html.dark .shiki span {
}
.highlight-line {
@apply min-w-min text-tint-strong table-row bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base relative hover:invert-5 hover:z-1 rounded-sm;
@apply text-tint-strong grid grid-cols-subgrid col-span-2 bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base relative hover:invert-5 hover:z-1 rounded-sm;
@apply only:hover:ring-transparent;
@apply [counter-increment:line];
@@ -345,7 +345,7 @@ html.dark .shiki span {
}
.highlight-line-number {
@apply table-cell whitespace-nowrap w-0 text-sm text-tint pl-4 pr-2 text-right bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base sticky -left-2;
@apply text-sm text-tint text-right pr-3.5 bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base rounded-l pl-2 sticky left-[-3px] mask-r-from-80%;
@apply before:content-[counter(line)] not-contrast-more:before:opacity-6;
.highlighted & {
@@ -353,11 +353,6 @@ html.dark .shiki span {
}
}
.highlight-line-content {
@apply table-cell text-sm px-4;
.highlight-line-number + & {
@apply pl-2;
}
@apply ml-3 block text-sm;
}
@@ -1,7 +1,6 @@
'use client';
import { t, useLanguage } from '@/intl/client';
import { getLocalizedTitle } from '@/lib/sites';
import { CustomizationSearchStyle, type SiteSection, type SiteSpace } from '@gitbook/api';
import { useRouter } from 'next/navigation';
import React, { useRef } from 'react';
@@ -257,10 +256,7 @@ export function SearchContainer({
<div className="border-tint-subtle border-t bg-tint-subtle px-4 py-1.5">
<SearchScopeControl
section={section}
spaceTitle={getLocalizedTitle(
siteSpace,
siteSpace.space.language
)}
spaceTitle={siteSpace.title}
withVariants={withVariants}
withSiteVariants={withSiteVariants}
withSections={withSections}
@@ -5,31 +5,37 @@ export type OrderedComputedResult =
| ComputedSectionResult
| ComputedRecordResult;
export type BaseComputedResult = {
export interface ComputedSectionResult {
type: 'section';
id: string;
title: string;
href: string;
score: number;
};
export type ComputedSectionResult = BaseComputedResult & {
type: 'section';
body: string;
href: string;
pageId: string;
spaceId: string;
};
}
export type ComputedPageResult = BaseComputedResult & {
export interface ComputedPageResult {
type: 'page';
id: string;
title: string;
href: string;
pageId: string;
spaceId: string;
breadcrumbs?: Array<{ icon?: IconName; label: string }>;
};
export type ComputedRecordResult = BaseComputedResult & {
breadcrumbs?: Array<{ icon?: IconName; label: string }>;
}
export interface ComputedRecordResult {
type: 'record';
id: string;
title: string;
description: string | undefined;
};
href: string;
}
export type SearchSiteContentScope =
| { mode: 'all' }
@@ -1,76 +0,0 @@
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import type React from 'react';
import { removeTrailingSlash } from '@/lib/paths';
import { Button, type ButtonProps } from '../primitives/Button';
import { DropdownMenuItem } from '../primitives/DropdownMenu';
import { Link, type LinkInsightsProps, type LinkProps } from '../primitives/Link';
/**
* Enrich a site auth login link with the current location relative to the site URL.
*/
function useSiteAuthLoginHrefWithLocation(href: string) {
const rawPathname = usePathname();
const searchParams = useSearchParams();
const currentSearch = searchParams?.toString();
const pathname = rawPathname ?? '/';
return useMemo(() => {
const baseURL = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
const resolved = URL.canParse(href) ? new URL(href) : new URL(href, baseURL);
const siteBasePath = removeTrailingSlash(
resolved.pathname.replace(/\/~gitbook\/auth\/login\/?$/, '')
);
const locationPath =
siteBasePath && pathname.startsWith(`${siteBasePath}/`)
? pathname.slice(siteBasePath.length)
: pathname === siteBasePath
? '/'
: pathname;
resolved.searchParams.set(
'location',
`${locationPath}${currentSearch ? `?${currentSearch}` : ''}`
);
return href.startsWith('http')
? resolved.toString()
: `${resolved.pathname}${resolved.search}`;
}, [currentSearch, href, pathname]);
}
/**
* Link component that preserves the current page location through the auth login flow.
*/
export function SiteAuthLoginLink(props: LinkProps) {
const href = useSiteAuthLoginHrefWithLocation(props.href);
return <Link {...props} href={href} />;
}
/**
* Button variant of SiteAuthLoginLink.
*/
export function SiteAuthLoginButton(props: ButtonProps) {
const href = useSiteAuthLoginHrefWithLocation(props.href ?? '#');
return <Button {...props} href={href} />;
}
/**
* Dropdown menu item variant of SiteAuthLoginLink.
*/
export function SiteAuthLoginDropdownMenuItem(
props: {
href: string;
target?: React.HTMLAttributeAnchorTarget;
active?: boolean;
className?: string;
children: React.ReactNode;
leadingIcon?: React.ReactNode | string;
} & LinkInsightsProps
) {
const href = useSiteAuthLoginHrefWithLocation(props.href);
return <DropdownMenuItem {...props} href={href} />;
}
@@ -1,5 +1,5 @@
import type { GitBookSiteContext } from '@/lib/context';
import { CustomizationDefaultThemeMode } from '@gitbook/api';
import { CustomizationThemeMode } from '@gitbook/api';
import type { Metadata, Viewport } from 'next';
import React from 'react';
import * as ReactDOM from 'react-dom';
@@ -22,7 +22,7 @@ import { SiteLayoutClientContexts } from './SiteLayoutClientContexts';
*/
export async function SiteLayout(props: {
context: GitBookSiteContext;
forcedTheme?: CustomizationDefaultThemeMode | null;
forcedTheme?: CustomizationThemeMode | null;
withTracking: boolean;
visitorAuthClaims: VisitorAuthClaims;
children: React.ReactNode;
@@ -52,7 +52,6 @@ export async function SiteLayout(props: {
forcedTheme ??
(customization.themes.toggeable ? undefined : customization.themes.default)
}
defaultTheme={customization.themes.default}
externalLinksTarget={customization.externalLinks.target}
proxyOrigin={context.site.proxy?.origin}
>
@@ -92,14 +91,10 @@ export async function generateSiteLayoutViewport(context: GitBookSiteContext): P
const { customization } = context;
return {
colorScheme: customization.themes.toggeable
? customization.themes.default === CustomizationDefaultThemeMode.Dark
? customization.themes.default === CustomizationThemeMode.Dark
? 'dark light'
: 'light dark'
: customization.themes.default === CustomizationDefaultThemeMode.Dark
? 'dark'
: customization.themes.default === CustomizationDefaultThemeMode.Light
? 'light'
: 'light dark', // 'system' → let browser decide based on OS preference
: customization.themes.default,
width: 'device-width',
initialScale: 1,
maximumScale: 1,
@@ -189,9 +184,7 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
capable: true,
title: site.title,
statusBarStyle:
customization.themes.default === CustomizationDefaultThemeMode.Dark
? 'black'
: 'default',
customization.themes.default === CustomizationThemeMode.Dark ? 'black' : 'default',
},
robots: (await isSiteIndexable(context)) ? 'index, follow' : 'noindex, nofollow',
};
@@ -1,6 +1,6 @@
'use client';
import type { CustomizationDefaultThemeMode, SiteExternalLinksTarget } from '@gitbook/api';
import type { CustomizationThemeMode, SiteExternalLinksTarget } from '@gitbook/api';
import { MotionConfig, useReducedMotion } from 'motion/react';
import { ThemeProvider } from 'next-themes';
import { NuqsAdapter } from 'nuqs/adapters/next/app';
@@ -15,15 +15,13 @@ import { isExternalLink } from '../utils/link';
* Client component context providers for the site layout.
*/
export function SiteLayoutClientContexts(props: {
forcedTheme: CustomizationDefaultThemeMode | undefined;
defaultTheme: CustomizationDefaultThemeMode | undefined;
forcedTheme: CustomizationThemeMode | undefined;
externalLinksTarget: SiteExternalLinksTarget;
contextId: string | undefined;
proxyOrigin: string | undefined;
children: React.ReactNode;
}) {
const { children, forcedTheme, defaultTheme, externalLinksTarget, contextId, proxyOrigin } =
props;
const { children, forcedTheme, externalLinksTarget, contextId, proxyOrigin } = props;
useClearRouterCache(contextId);
@@ -50,7 +48,6 @@ export function SiteLayoutClientContexts(props: {
disableTransitionOnChange
enableSystem
forcedTheme={forcedTheme}
defaultTheme={defaultTheme}
>
<NuqsAdapter>
<LinkContext.Provider value={linkContext}>
@@ -1,8 +1,8 @@
import type { GitBookSiteContext } from '@/lib/context';
import { getDataOrNull, getPageDocument } from '@/lib/data';
import {
CustomizationDefaultThemeMode,
CustomizationHeaderPreset,
CustomizationThemeMode,
type RevisionPageDocument,
SiteInsightsDisplayContext,
type TranslationLanguage,
@@ -17,7 +17,7 @@ import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
import { getResizedImageURL } from '@/lib/images';
import { resolveContentRef } from '@/lib/references';
import { getLocalizedTitle } from '@/lib/sites';
import { getSiteSectionTitle } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { getPageRSSURL } from '@/routes/rss';
import { PageContextProvider } from '../PageContext';
@@ -114,14 +114,10 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro
return {
colorScheme: customization.themes.toggeable
? customization.themes.default === CustomizationDefaultThemeMode.Dark
? customization.themes.default === CustomizationThemeMode.Dark
? 'dark light'
: 'light dark'
: customization.themes.default === CustomizationDefaultThemeMode.Dark
? 'dark'
: customization.themes.default === CustomizationDefaultThemeMode.Light
? 'light'
: 'light dark', // 'system' → let browser decide based on OS preference
: customization.themes.default,
};
}
@@ -130,7 +126,7 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro
*/
function getSiteStructureTitle(context: GitBookSiteContext): string | null {
const { visibleSections: sections, siteSpace, visibleSiteSpaces: siteSpaces } = context;
const currentLanguage = context.locale;
const currentLanguage = siteSpace.space.language;
const title = [];
if (
@@ -138,14 +134,14 @@ function getSiteStructureTitle(context: GitBookSiteContext): string | null {
sections.current.default === false && // Only if the current section is not the default one
sections.list.filter((section) => section.object === 'site-section').length > 1 // Only if there are multiple sections
) {
title.push(getLocalizedTitle(sections.current, currentLanguage));
title.push(getSiteSectionTitle(sections.current, currentLanguage));
}
if (
siteSpaces.length > 1 && // Only if there are multiple variants
siteSpace.default === false && // Only if the variant is not the default one
siteSpaces.filter((space) => space.space.language === siteSpace.space.language).length > 1 // Only if there are multiple variants *for the current language*. This filters out spaces that are "just" translations of each other, not versions.
) {
title.push(getLocalizedTitle(siteSpace, siteSpace.space.language));
title.push(siteSpace.title);
}
return title.join(' ');
}
@@ -6,8 +6,7 @@ import { tcls } from '@/lib/tailwind';
import { SiteInsightsDisplayContext } from '@gitbook/api';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink';
import { useSiteAdaptiveAuthLoginHref, useSpaceBasePath } from '../SpaceLayout/SpaceLayoutContext';
import { useSpaceBasePath } from '../SpaceLayout/SpaceLayoutContext';
import { CurrentPageProvider } from '../hooks';
import { SuspenseLoadedHint } from '../primitives';
@@ -16,7 +15,6 @@ import { SuspenseLoadedHint } from '../primitives';
*/
export function SitePageNotFound() {
const basePath = useSpaceBasePath();
const adaptiveAuthLoginHref = useSiteAdaptiveAuthLoginHref();
const language = useLanguage();
const router = useRouter();
const searchParams = useSearchParams();
@@ -51,24 +49,9 @@ export function SitePageNotFound() {
>
<div className={tcls('max-w-80')}>
<h2 className={tcls('text-2xl', 'font-semibold', 'mb-2')}>
{t(
language,
adaptiveAuthLoginHref ? 'notfound_adaptive_title' : 'notfound_title'
)}
{t(language, 'notfound_title')}
</h2>
<p className={tcls('text-base', 'mb-4')}>
{t(language, adaptiveAuthLoginHref ? 'notfound_adaptive' : 'notfound')}
</p>
{adaptiveAuthLoginHref ? (
<SiteAuthLoginButton
href={adaptiveAuthLoginHref}
variant="primary"
size="medium"
label={t(language, 'notfound_adaptive_login')}
>
{t(language, 'notfound_adaptive_login')}
</SiteAuthLoginButton>
) : null}
<p className={tcls('text-base', 'mb-4')}>{t(language, 'notfound')}</p>
</div>
<SuspenseLoadedHint />
@@ -1,11 +1,6 @@
import type { GitBookSiteContext } from '@/lib/context';
import { redirect } from 'next/navigation';
import {
SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH,
SITE_REDIRECT_SOURCE_PATH_PATTERN,
} from '@gitbook/api';
import { getDataOrNull } from '@/lib/data';
import { resolvePageId, resolvePagePath } from '@/lib/pages';
import { withLeadingSlash } from '@/lib/paths';
@@ -58,37 +53,38 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
// We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway.
// API has a limit of less than 512 characters for the source path, so we use the same limit here.
if (rawPathname.length < SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH) {
const SITE_REDIRECT_SOURCE_PATH_REGEX = new RegExp(SITE_REDIRECT_SOURCE_PATH_PATTERN);
if (rawPathname.length < 512) {
// Duplicated the regex pattern from SiteRedirectSourcePath API type.
const SITE_REDIRECT_SOURCE_PATH_REGEX =
/^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/;
const redirectPathname = withLeadingSlash(rawPathname);
// If a page can't be found, we try with the API, in case we have a redirect at site level.
const redirectSources = new Set(
[
if (SITE_REDIRECT_SOURCE_PATH_REGEX.test(redirectPathname)) {
const redirectSources = new Set<string>([
// Test the pathname relative to the root
// For example hello/world -> section/variant/hello/world
linker.toRelativePathInSite(linker.toPathInSpace(redirectPathname)),
withLeadingSlash(
linker.toRelativePathInSite(linker.toPathInSpace(redirectPathname))
),
// Test the pathname relative to the content/space
// For example hello/world -> /hello/world
redirectPathname,
]
.map(toSiteRedirectSourceCandidate)
.filter((source) => SITE_REDIRECT_SOURCE_PATH_REGEX.test(source))
);
for (const source of redirectSources) {
// We try to resolve the site redirect
const resolvedSiteRedirect =
source.length < SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH &&
(await getDataOrNull(
context.dataFetcher.getSiteRedirectBySource({
organizationId,
siteId: site.id,
source,
siteShareKey: shareKey,
})
));
if (resolvedSiteRedirect) {
return redirect(linker.toLinkForContent(resolvedSiteRedirect.target));
]);
for (const source of redirectSources) {
// We try to resolve the site redirect
const resolvedSiteRedirect =
source.length < 512 &&
(await getDataOrNull(
context.dataFetcher.getSiteRedirectBySource({
organizationId,
siteId: site.id,
source,
siteShareKey: shareKey,
})
));
if (resolvedSiteRedirect) {
return redirect(linker.toLinkForContent(resolvedSiteRedirect.target));
}
}
}
@@ -110,21 +106,6 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
return undefined;
}
/**
* Transform a pathname into a candidate source for site redirect matching.
* We also encode each segment to handle special characters in redirects.
*/
function toSiteRedirectSourceCandidate(pathname: string): string {
const normalized = withLeadingSlash(pathname);
return withLeadingSlash(
normalized
.slice(1)
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/')
);
}
/**
* Get the page path from the params.
*/
@@ -1,10 +1,5 @@
import type { GitBookSiteContext, SiteSections } from '@/lib/context';
import {
getLocalizedDescription,
getLocalizedTitle,
getSectionURL,
getSiteSpaceURL,
} from '@/lib/sites';
import { getSectionURL, getSiteSectionTitle, getSiteSpaceURL } from '@/lib/sites';
import type { SiteSection, SiteSectionGroup, SiteSpace } from '@gitbook/api';
import assertNever from 'assert-never';
@@ -29,7 +24,6 @@ export type ClientSiteSectionGroup = Pick<SiteSectionGroup, 'id' | 'title' | 'ic
*/
export function encodeClientSiteSections(context: GitBookSiteContext, sections: SiteSections) {
const { list, current } = sections;
const currentLanguage = context.locale;
const clientSections: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
@@ -45,7 +39,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
clientSections.push({
id: item.id,
title: getLocalizedTitle(item, currentLanguage),
title: item.title,
icon: item.icon,
object: item.object,
children,
@@ -72,7 +66,6 @@ function encodeChildren(
children: (SiteSection | SiteSectionGroup)[]
): (ClientSiteSection | ClientSiteSectionGroup)[] {
const clientChildren: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
const currentLanguage = context.locale;
for (const child of children) {
switch (child.object) {
@@ -90,7 +83,7 @@ function encodeChildren(
clientChildren.push({
id: child.id,
title: getLocalizedTitle(child, currentLanguage),
title: child.title,
icon: child.icon,
object: child.object,
children: nestedChildren,
@@ -106,11 +99,11 @@ function encodeChildren(
}
function encodeSection(context: GitBookSiteContext, section: SiteSection) {
const currentLanguage = context.locale;
const currentLanguage = context.siteSpace.space.language;
return {
id: section.id,
title: getLocalizedTitle(section, currentLanguage),
description: getLocalizedDescription(section, currentLanguage),
title: getSiteSectionTitle(section, currentLanguage),
description: section.description,
icon: section.icon,
object: section.object,
url: findBestTargetURL(context, section),
@@ -51,10 +51,6 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
props;
const { customization } = context;
const siteAdaptiveAuthLoginHref =
context.site.adaptiveContent?.enabled && context.site.urls.login
? context.linker.toPathInSite('~gitbook/auth/login')
: null;
const eventUrl = new URL(
context.linker.toAbsoluteURL(context.linker.toPathInSite('/~gitbook/__evt'))
@@ -67,10 +63,7 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
);
return (
<SpaceLayoutContextProvider
basePath={context.linker.toPathInSpace('')}
siteAdaptiveAuthLoginHref={siteAdaptiveAuthLoginHref}
>
<SpaceLayoutContextProvider basePath={context.linker.toPathInSpace('')}>
<AdaptiveVisitorContextProvider
contextId={context.contextId}
visitorClaimsURL={getVisitorClaimsUrl}
@@ -4,7 +4,6 @@ import React from 'react';
const SpaceLayoutContext = React.createContext({
basePath: '',
siteAdaptiveAuthLoginHref: null as string | null,
});
/**
@@ -13,15 +12,11 @@ const SpaceLayoutContext = React.createContext({
export function SpaceLayoutContextProvider(
props: React.PropsWithChildren<{
basePath: string;
siteAdaptiveAuthLoginHref?: string | null;
}>
) {
const { basePath, siteAdaptiveAuthLoginHref = null, children } = props;
const { basePath, children } = props;
const value = React.useMemo(
() => ({ basePath, siteAdaptiveAuthLoginHref }),
[basePath, siteAdaptiveAuthLoginHref]
);
const value = React.useMemo(() => ({ basePath }), [basePath]);
return <SpaceLayoutContext.Provider value={value}>{children}</SpaceLayoutContext.Provider>;
}
@@ -36,14 +31,3 @@ export function useSpaceBasePath() {
}
return context.basePath;
}
/**
* Return the site auth login path when adaptive content with login fallback is configured.
*/
export function useSiteAdaptiveAuthLoginHref() {
const context = React.useContext(SpaceLayoutContext);
if (!context) {
throw new Error('SpaceLayoutContext not found');
}
return context.siteAdaptiveAuthLoginHref;
}
@@ -9,7 +9,7 @@ export function categorizeVariants(context: GitBookSiteContext) {
const normalizeLanguage = (language: string | undefined) =>
language === undefined ? languages.en.locale : language;
const currentLanguage = normalizeLanguage(context.locale);
const currentLanguage = normalizeLanguage(siteSpace.space.language);
// Get all languages of the variants.
const variantLanguages = [
@@ -3,14 +3,14 @@ import type { SiteInsightsTrademarkPlacement } from '@gitbook/api';
import { getSpaceLanguage, tString } from '@/intl/server';
import { tcls } from '@/lib/tailwind';
import type { GitBookAnyContext } from '@/lib/context';
import type { GitBookSpaceContext } from '@/lib/context';
import { Button } from '../primitives';
/**
* Trademark link to the GitBook.
*/
export function Trademark(props: {
context: GitBookAnyContext;
context: GitBookSpaceContext;
placement: SiteInsightsTrademarkPlacement;
className?: string;
}) {
@@ -22,13 +22,9 @@ export function Trademark(props: {
url.searchParams.set('utm_source', 'content');
url.searchParams.set('utm_medium', 'trademark');
url.searchParams.set('utm_campaign', space.id);
if ('site' in context) {
url.searchParams.set('utm_content', context.site.id);
}
return (
<Button
data-testid="gb-trademark"
target="_blank"
variant="secondary"
size="large"
+12 -14
View File
@@ -1,40 +1,38 @@
import memoize from 'memoizee';
/**
* Format date as MM/DD/YYYY (e.g., "05/11/2025")
*/
export const getNumericDateFormatter = memoize((locale: Intl.LocalesArgument) => {
return new Intl.DateTimeFormat(locale ?? 'en-US', {
export function formatNumericDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric',
timeZone: 'UTC',
});
});
}).format(date);
}
/**
* Format date as "Month Day, Year" (e.g., "November 5, 2025")
*/
export const getFullDateFormatter = memoize((locale: Intl.LocalesArgument) => {
return new Intl.DateTimeFormat(locale ?? 'en-US', {
export function formatDateFull(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
});
}).format(date);
}
/**
* Format date as "Nov. 5 2025"
*/
export const getShortDateFormatter = memoize((locale: Intl.LocalesArgument) => {
return new Intl.DateTimeFormat(locale ?? 'en-US', {
export function formatDateShort(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
});
}).format(date);
}
/**
* Format date with weekday (e.g., "Monday, Nov 5, 2025").

Some files were not shown because too many files have changed in this diff Show More