mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-19 17:15:24 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 210cdcaa68 | |||
| d4247d25f1 | |||
| 33e87a0183 | |||
| 75a217d5c9 | |||
| fed63b4675 | |||
| 569d4046be | |||
| 50653abd08 | |||
| 84082225af | |||
| 7d882cef5f | |||
| 126d4b78b6 | |||
| e8224bad6c | |||
| 6063437513 | |||
| ab17ff47b0 | |||
| d97b404c7a | |||
| 3c141f0231 | |||
| e3d391e01b | |||
| 62cfec874a | |||
| ccd21e4943 | |||
| 696d9d0d01 | |||
| b77c4fc5b9 | |||
| 7e1ea48080 | |||
| b5d6f2e908 | |||
| 832b44f0ac | |||
| 4909044831 | |||
| 4ed84c209a | |||
| ca01888fe2 | |||
| 6855c05781 | |||
| ab867eb69c | |||
| deabce98ae | |||
| f884477a65 | |||
| c56b569a05 | |||
| 1ba412ff6c | |||
| 7c2823cd26 | |||
| 16bfafe32b | |||
| da648cabd8 | |||
| a36e437580 | |||
| d5dd5e7ba1 | |||
| c4a2604b4d | |||
| 7f4e644733 | |||
| 5d3f78b112 | |||
| 60bcf43304 | |||
| b40465e7b7 | |||
| 3151864f81 | |||
| 80803c92f2 | |||
| 31e77d4e17 | |||
| 8e5a2aa6e6 | |||
| fe6f6420e3 | |||
| aff6285eb9 | |||
| 5f132bc979 | |||
| 4297de3571 | |||
| 6455f2e3ff | |||
| dee52fa80a | |||
| 4ac29817a2 | |||
| f09ca60930 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix Variant dropdown not showing in GitBook embed
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve Assistant form controls
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Redirect sitemap.md and .well-known/sitemap.md to llms.txt.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Use localized titles/descriptions for site entities
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Import site redirect patterns from @gitbook/api
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Improve AI agent detection and readability by outputting 200 for page not found.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Support x-gitbook-token-placeholder as TryIt prefill fallback for HTTP auth (basic and bearer)
|
||||
@@ -1,3 +1,77 @@
|
||||
# AGENTS Instructions
|
||||
|
||||
- After making code changes, run `bun format`.
|
||||
## 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.
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.30.0",
|
||||
"turbo": "^2.8.16",
|
||||
"vercel": "^50.31.1",
|
||||
"turbo": "^2.9.2",
|
||||
"vercel": "50.37.3",
|
||||
},
|
||||
},
|
||||
"packages/browser-types": {
|
||||
@@ -109,7 +109,7 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.22.3",
|
||||
"version": "0.23.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/workers-types": "^4.20251011.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
@@ -129,6 +129,7 @@
|
||||
"@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",
|
||||
@@ -138,6 +139,7 @@
|
||||
"@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:",
|
||||
@@ -149,6 +151,7 @@
|
||||
"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",
|
||||
@@ -159,7 +162,6 @@
|
||||
"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",
|
||||
@@ -218,19 +220,19 @@
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-essentials": "^10.0.1",
|
||||
"typescript": "catalog:",
|
||||
"vercel": "^50.15.1",
|
||||
"vercel": "50.37.3",
|
||||
"wrangler": "^4.43.0",
|
||||
},
|
||||
},
|
||||
"packages/icons": {
|
||||
"name": "@gitbook/icons",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.6",
|
||||
"bin": {
|
||||
"gitbook-icons": "./bin/gitbook-icons.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@gitbook/fontawesome-pro": "1.0.16",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
"@gitbook/fontawesome-pro": "1.0.23",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "catalog:",
|
||||
@@ -245,7 +247,7 @@
|
||||
},
|
||||
"packages/openapi-parser": {
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"version": "3.0.10",
|
||||
"version": "3.0.11",
|
||||
"dependencies": {
|
||||
"@scalar/json-magic": "^0.9.4",
|
||||
"@scalar/openapi-parser": "^0.24.5",
|
||||
@@ -262,7 +264,7 @@
|
||||
},
|
||||
"packages/react-contentkit": {
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.7.12",
|
||||
"version": "0.7.15",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
@@ -304,7 +306,7 @@
|
||||
},
|
||||
"packages/react-openapi": {
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "1.5.10",
|
||||
"version": "1.5.11",
|
||||
"dependencies": {
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
@@ -344,12 +346,13 @@
|
||||
"@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.169.0",
|
||||
"@gitbook/api": "0.175.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
@@ -742,11 +745,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.1.0", "", {}, "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA=="],
|
||||
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.2.0", "", {}, "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw=="],
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.169.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-kzJT8P5HCnqeYG3kFmBfvKMjQhTuUVmNV5Wvyh87QnoOA3odwh2C3qONpTmBA6cNE3ymDnxs8SJT9FG/qquRZQ=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.175.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-2xIPSsXV7ensBbPNs8K/pxt8xIbmsbwBx8E1fn3Do5WhubcRcGy7UduqIsyzIAYeA24K32VoNN1i4GnoUAa3HQ=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
@@ -760,7 +763,7 @@
|
||||
|
||||
"@gitbook/expr": ["@gitbook/expr@workspace:packages/expr"],
|
||||
|
||||
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.16", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^7.1.0" } }, "sha512-jypE3FpC5U7lealzpGVq1Jie5Ay5jYv/UkOLxPhOpffjCd98A+Ex08M6GyINMDO7KCBaF5p8pbdl693hf0DwNg=="],
|
||||
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.23", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^7.2.0" } }, "sha512-igxgeuZvZIDghZhju8vAV3GjznwOUU2RHMGxGLxwf6QhL4soTJIveD+84C6EGQRieX8O1H6b4TOac3Hhom+Jcg=="],
|
||||
|
||||
"@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
|
||||
|
||||
@@ -1010,6 +1013,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -1576,6 +1581,18 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -1702,61 +1719,65 @@
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="],
|
||||
|
||||
"@vercel/backends": ["@vercel/backends@0.0.45", "", { "dependencies": { "@vercel/build-utils": "13.8.0", "@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-KIdt/z4LfH7NgFMqgSuKi0H9UIasly7ByzP+/ZXulgNrWyeJKT9KCas3SDT65o5tU6x1D/jBysZA9AnOt8Ivew=="],
|
||||
"@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/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.8.0", "", { "dependencies": { "@vercel/python-analysis": "0.9.1" } }, "sha512-moQS4Qd0pvluPd6WRTHxLN3Hh0oSObVNdFv3V0spiEmCk/wm6571up3n1th2PQFqf1a3gheNfxzL7h4I9CWs2A=="],
|
||||
"@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/cervel": ["@vercel/cervel@0.0.32", "", { "dependencies": { "@vercel/backends": "0.0.45" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" }, "bin": { "cervel": "bin/cervel.mjs" } }, "sha512-g/LIa97d/m3yIZGyBjwl4FOQK1Lg5HU2+N3uHiGVajLoSgJM2aBiwETDZc8bVCr8IO3IqXZQsT2hGy1Jnyko1g=="],
|
||||
"@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/detect-agent": ["@vercel/detect-agent@1.2.0", "", {}, "sha512-kY0+TPdYr170bVmVSCg0XvwakWQMENy79uf/sM86DF/HVabaagdTjPwSPujlZid6Bgr66R4kHhjg4bkOxEzFDA=="],
|
||||
"@vercel/detect-agent": ["@vercel/detect-agent@1.2.1", "", {}, "sha512-U/BJCltQSTFTHwaiCQQTQG3GonTbRoEewjV+OU2mMjcHLAoPOh6CP1SXA2XNmqiqI3c82nkRNJ7piZ14RqmTXw=="],
|
||||
|
||||
"@vercel/elysia": ["@vercel/elysia@0.1.48", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-QlmOHUSOx/uE67Y6u/o8VbNF7ebZ8SZFbrEoBo7iZAd0MC2Vn4xUmlrHFXNR/FEkHFXWkhFKda5Ade3XMqMY8A=="],
|
||||
"@vercel/elysia": ["@vercel/elysia@0.1.55", "", { "dependencies": { "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "sha512-LKXOglYMdihN4CKMFZsuf9Oa7ymO1YEWQUdey61fe4qcJzywrEJD70ZFO0bNuRtXMn7Gf0iiGXKmPIPBUklH1g=="],
|
||||
|
||||
"@vercel/error-utils": ["@vercel/error-utils@2.0.3", "", {}, "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ=="],
|
||||
|
||||
"@vercel/express": ["@vercel/express@0.1.57", "", { "dependencies": { "@vercel/cervel": "0.0.32", "@vercel/nft": "1.1.1", "@vercel/node": "5.6.15", "@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-/Ih1eiJrBGSW6JPzexB8y8AMX/8MzuDUf0xzmpYbKLCbehp3NNuxde5RxT+6cALglFivDETsJBCFwaFuIh3ZqQ=="],
|
||||
"@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/fastify": ["@vercel/fastify@0.1.51", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-c9CwFQqmoUm5eEGwAcqb98DcxsRmSZGngCAo1B+nlDhaACKcrJ7Ro0tYdTDhi8Lkt+OcPnRWr4+pepgWwH94GQ=="],
|
||||
"@vercel/fastify": ["@vercel/fastify@0.1.58", "", { "dependencies": { "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "sha512-QRuhhcPneqOvHEUT5He3gGSEkH7JTqNhSIQG298mLEx2fL8OfVJHLHq/50GHVBhRLzIkFA2lfLQbRuwqRSyDvA=="],
|
||||
|
||||
"@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.0", "", { "dependencies": { "@sinclair/typebox": "0.25.24", "@vercel/build-utils": "13.8.0", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "sha512-avJ5IFev2h2K6E/Pd7qd00cFLALj3OyEmQE3UoGs1dmoncINFqa1RoIZDJ9wIhWm1Euan4wrFMRsXkCwNhEGhw=="],
|
||||
"@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/go": ["@vercel/go@3.4.4", "", {}, "sha512-jfIx/gUe2YAp46/H4uqdA+PLQQuRwb5lwU37m/e5wrnMCUZ2ksuhg50xPHcBBweboYeE6WuT4e1W6Cwp3tBPzg=="],
|
||||
"@vercel/go": ["@vercel/go@3.4.6", "", {}, "sha512-ig91qRY+f4lLXWoRvnhEvu5DcT7LXtALBo/jJqxvlyOsHRBP4mdAvUgg9sygu2NOeMSV/cy249yJqQniP32HWw=="],
|
||||
|
||||
"@vercel/h3": ["@vercel/h3@0.1.57", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-I7Q1ity7xEdIVw4OQuKOAR8i6DrvfjpKy+y2uTwbqBD80Gg0KpsMS/KKA+UjvCuclFmP/EHYS3kpMLc/O1rVqg=="],
|
||||
"@vercel/h3": ["@vercel/h3@0.1.64", "", { "dependencies": { "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "sha512-zH57M8boAs+YKbT0gtnnq4aBYNKjpXwd1p7h8PlnH3MrQmAAapwb9SCAndRLu8STJSUCsYbT/JLk66rWLTMXNQ=="],
|
||||
|
||||
"@vercel/hono": ["@vercel/hono@0.2.51", "", { "dependencies": { "@vercel/nft": "1.1.1", "@vercel/node": "5.6.15", "@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-iYEjjF4qR3gTZpVoB4sMQNm5OOdKw5/P2bL1CtolDTeGaea9KwqUrLyXmdK4N41HIRZr/wduID31mmHhSdC3sA=="],
|
||||
"@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/hydrogen": ["@vercel/hydrogen@1.3.6", "", { "dependencies": { "@vercel/static-config": "3.2.0", "ts-morph": "12.0.0" } }, "sha512-Ec8dKEjGIM4BfThcRLtQs5zaJ4+iJbgLZwkytwi7Blk8VrK6W2F1dtLDmVQYZdVnQcnmHmTx8mxUuMkfP06Mnw=="],
|
||||
|
||||
"@vercel/koa": ["@vercel/koa@0.1.31", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-Gj4sjqNA80/gnHpC0tRPCTtVEct69tUK21fsjVmawCa+nDNQ4bqXu3dSewna7GtCcp9KnrWgzAIaeVAQUl53mQ=="],
|
||||
"@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/nestjs": ["@vercel/nestjs@0.2.52", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-yfy4rpWJ1BRWZ1xBBPew0egVIblFe6G7laCKDzyESnbw19zY0F16g9HCu1H/0IfuKIQvOc95dtbmBsqLm9jyqw=="],
|
||||
"@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/next": ["@vercel/next@4.16.1", "", { "dependencies": { "@vercel/nft": "1.1.1" } }, "sha512-gwy3XQRZ/f6RdKuC7BZIRMAzUOQf/R5+k9LMf1LcOm1CVZOLpDhDkeZMTG5vb3Lk9LWwBrJJ2ohhZaYuYEOHaw=="],
|
||||
"@vercel/next": ["@vercel/next@4.16.3", "", { "dependencies": { "@vercel/nft": "1.5.0" } }, "sha512-yVLrMxMI+Taq47C94lWVUf981WerJ+COrJbgltHcMY8HDdXdgthYe06+na3dni31AL6BYShS8VLpE54QsAdM9Q=="],
|
||||
|
||||
"@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/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/node": ["@vercel/node@5.6.15", "", { "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.8.0", "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@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-xc5fxmdk8jtuUY8y9/8W5UhTn8R1Ii1Fb3q+V8Zv+2moU9enrrBADA9ercHgE0/DtoiNDpb9Wmvnrb4bUcFOzA=="],
|
||||
"@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/python": ["@vercel/python@6.22.0", "", { "dependencies": { "@vercel/python-analysis": "0.9.1" } }, "sha512-wyHIWKXrDp+VkiWTIkz/++m037lwIy1PUNLRooYFUUBylBWpF/oYy42Kj8vyAKtsXO+14SBtTCekLE+9+2wmhA=="],
|
||||
"@vercel/prepare-flags-definitions": ["@vercel/prepare-flags-definitions@0.2.1", "", {}, "sha512-ouXTsqn7I9xZ1KKezgvn/w3tZeQHL/tc52j9GHiOYi6kT8xgdbT8s2x8C9BQr44iceX0hfhtZwk9q7NuI2Tqbw=="],
|
||||
|
||||
"@vercel/python-analysis": ["@vercel/python-analysis@0.9.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.3", "smol-toml": "1.5.2", "zod": "3.22.4" } }, "sha512-ZwEi/F2DPxFPYmfjHFy7qM3+JTWRxD1EMpbIotNNhUyd/pnIG0wNt7S73RJSx62n1Y7pmFOFowoImnhULQgKvA=="],
|
||||
"@vercel/python": ["@vercel/python@6.28.0", "", { "dependencies": { "@vercel/python-analysis": "0.11.0" } }, "sha512-/Ley7HPz/AXhxO7unK5+4hEtsMUxXa02SJ8Tiaz//nEtRQMUcVREIyAUkOOfecjiCpg2hMHSVyCtABFjhzAbgA=="],
|
||||
|
||||
"@vercel/redwood": ["@vercel/redwood@2.4.10", "", { "dependencies": { "@vercel/nft": "1.1.1", "@vercel/static-config": "3.2.0", "semver": "6.3.1", "ts-morph": "12.0.0" } }, "sha512-7C5lUn9g9kLm1KpX55b8iizVPOB6087+kVyQyKyXGk8bbkYySL26yb+LIwyL/7mXwHlq/JTC0AxVdC3nNmPZuw=="],
|
||||
"@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/remix-builder": ["@vercel/remix-builder@5.7.0", "", { "dependencies": { "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@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-R44EHl+PQjX5PrCmyGust+bk+65eT5omxOLhEIJkSI90Kx/vvuyKnFNic/Zwo//GCMjxt9httvQUr/6vIBbjHg=="],
|
||||
"@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/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.0", "", { "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", "@vercel/gatsby-plugin-vercel-builder": "2.1.0", "@vercel/static-config": "3.2.0", "ts-morph": "12.0.0" } }, "sha512-3SHWntz8swxL6ve750dY8kyl4NwVUplYtun/ei7y11q2UnI70WnWmm6L9fi02Wy1o3RGhbvOnlFLcHOUBm3DXQ=="],
|
||||
"@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-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=="],
|
||||
|
||||
@@ -1860,8 +1881,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -2202,7 +2221,7 @@
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.4.1", "", {}, "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="],
|
||||
"es-module-lexer": ["es-module-lexer@1.5.0", "", {}, "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
@@ -2310,8 +2329,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -2598,7 +2615,7 @@
|
||||
|
||||
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
|
||||
|
||||
"leven": ["leven@4.0.0", "", {}, "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw=="],
|
||||
"leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -2868,8 +2885,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -2960,8 +2975,6 @@
|
||||
|
||||
"pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="],
|
||||
|
||||
"pip-requirements-js": ["pip-requirements-js@1.0.3", "", { "dependencies": { "ohm-js": "^17.1.0" } }, "sha512-1O9Bx0mPOZht3tW4LuxOA46qkD8A1AGymWXz3UwIMqGQgiTiOaFptsCf+9IE67qcbBrg8KHG6l8ePF7CoFRW/A=="],
|
||||
|
||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="],
|
||||
@@ -3328,19 +3341,7 @@
|
||||
|
||||
"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.8.16", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.16", "turbo-darwin-arm64": "2.8.16", "turbo-linux-64": "2.8.16", "turbo-linux-arm64": "2.8.16", "turbo-windows-64": "2.8.16", "turbo-windows-arm64": "2.8.16" }, "bin": { "turbo": "bin/turbo" } }, "sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA=="],
|
||||
|
||||
"turbo-darwin-64": ["turbo-darwin-64@2.8.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWa4hUMWrpADC6Q/wIHRkBLw6X6MV9nx6X7hSXbTrrMz0KdaKhmfudUZ3sS76bJFmgArBU25cSc0AUyyrswYxg=="],
|
||||
|
||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NBgaqBDLQSZlJR4D5XCkQq6noaO0RvIgwm5eYFJYL3bH5dNu8o0UBpq7C5DYnQI8+ybyoHFjT5/icN4LeUYLow=="],
|
||||
|
||||
"turbo-linux-64": ["turbo-linux-64@2.8.16", "", { "os": "linux", "cpu": "x64" }, "sha512-VYPdcCRevI9kR/hr1H1xwXy7QQt/jNKiim1e1mjANBXD2E9VZWMkIL74J1Huad5MbU3/jw7voHOqDPLJPC2p6w=="],
|
||||
|
||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-beq8tgUVI3uwkQkXJMiOr/hfxQRw54M3elpBwqgYFfemiK5LhCjjcwO0DkE8GZZfElBIlk+saMAQOZy3885wNQ=="],
|
||||
|
||||
"turbo-windows-64": ["turbo-windows-64@2.8.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Ig7b46iUgiOIkea/D3Z7H+zNzvzSnIJcLYFpZLA0RxbUTrbLhv9qIPwv3pT9p/abmu0LXVKHxaOo+p26SuDhzw=="],
|
||||
|
||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-fOWjbEA2PiE2HEnFQrwNZKYEdjewyPc2no9GmrXklZnTCuMsxeCN39aVlKpKpim03Zq/ykIuvApGwq8ZbfS2Yw=="],
|
||||
"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=="],
|
||||
|
||||
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
|
||||
|
||||
@@ -3408,7 +3409,7 @@
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vercel": ["vercel@50.31.1", "", { "dependencies": { "@vercel/backends": "0.0.45", "@vercel/blob": "2.3.0", "@vercel/build-utils": "13.8.0", "@vercel/detect-agent": "1.2.0", "@vercel/elysia": "0.1.48", "@vercel/express": "0.1.57", "@vercel/fastify": "0.1.51", "@vercel/fun": "1.3.0", "@vercel/go": "3.4.4", "@vercel/h3": "0.1.57", "@vercel/hono": "0.2.51", "@vercel/hydrogen": "1.3.6", "@vercel/koa": "0.1.31", "@vercel/nestjs": "0.2.52", "@vercel/next": "4.16.1", "@vercel/node": "5.6.15", "@vercel/python": "6.22.0", "@vercel/redwood": "2.4.10", "@vercel/remix-builder": "5.7.0", "@vercel/ruby": "2.3.2", "@vercel/rust": "1.0.5", "@vercel/static-build": "2.9.0", "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-cu8ukSJVuKecKavhMWMP/sgcetQ/6qsEaIGf9626tWXpRddUsLLJAsGyZUoZxo+EshMEp5kjuLZvTonyOUBLcA=="],
|
||||
"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=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
@@ -4362,6 +4363,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -4496,8 +4499,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -4516,26 +4517,20 @@
|
||||
|
||||
"@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/@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/async-listen": ["async-listen@3.0.0", "", {}, "sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg=="],
|
||||
|
||||
"@vercel/node/es-module-lexer": ["es-module-lexer@1.4.1", "", {}, "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@vercel/node/path-to-regexp": ["path-to-regexp@6.1.0", "", {}, "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="],
|
||||
@@ -4546,12 +4541,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -5432,6 +5423,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -5466,6 +5459,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -5576,10 +5571,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -5592,38 +5583,18 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -5976,22 +5947,10 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6070,18 +6029,6 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
+5
-4
@@ -7,8 +7,8 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.30.0",
|
||||
"turbo": "^2.8.16",
|
||||
"vercel": "^50.31.1"
|
||||
"turbo": "^2.9.2",
|
||||
"vercel": "50.37.3"
|
||||
},
|
||||
"packageManager": "bun@1.3.7",
|
||||
"overrides": {
|
||||
@@ -17,7 +17,8 @@
|
||||
"@types/react-dom": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"esbuild": "0.24.2"
|
||||
"esbuild": "0.24.2",
|
||||
"axios": "1.8.4"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -41,7 +42,7 @@
|
||||
"catalog": {
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@gitbook/api": "0.169.0",
|
||||
"@gitbook/api": "0.175.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
@@ -285,6 +285,44 @@ 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
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"scripts": {
|
||||
"build": "bun run build-lib && bun run build-standalone",
|
||||
"build-lib": "tsdown",
|
||||
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"build-standalone": "bun run ./scripts/build-standalone.ts",
|
||||
"clean": "rm -rf ./dist",
|
||||
"unit": "bun test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
@@ -83,6 +83,11 @@ 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';
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,6 +55,7 @@ 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',
|
||||
@@ -85,6 +86,43 @@ 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(
|
||||
@@ -99,6 +137,7 @@ function getIframe() {
|
||||
const client = getClient();
|
||||
|
||||
widgetIframe?.remove();
|
||||
lastPushedColorScheme = undefined;
|
||||
widgetIframe = document.createElement('iframe');
|
||||
widgetIframe.id = 'gitbook-widget-iframe';
|
||||
widgetIframe.src = client.getFrameURL({
|
||||
@@ -111,6 +150,8 @@ function getIframe() {
|
||||
widgetWindow.classList.add('hidden');
|
||||
widgetButton.classList.remove('open');
|
||||
});
|
||||
|
||||
pushColorSchemeToFrame();
|
||||
}
|
||||
return { iframe: widgetIframe, frame: _frame };
|
||||
}
|
||||
@@ -188,9 +229,12 @@ const GitBook = (...args: StandaloneCalls) => {
|
||||
}
|
||||
}
|
||||
|
||||
getIframe().frame.configure({
|
||||
const { frame } = getIframe();
|
||||
// Always propagate configuration updates, even when color-scheme doesn't change.
|
||||
frame.configure({
|
||||
...frameConfiguration,
|
||||
});
|
||||
pushColorSchemeToFrame();
|
||||
break;
|
||||
}
|
||||
case 'clearChat':
|
||||
|
||||
@@ -8,14 +8,6 @@
|
||||
|
||||
--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 */
|
||||
@@ -29,13 +21,20 @@
|
||||
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@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;
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,56 @@
|
||||
# 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
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
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[] = [
|
||||
{
|
||||
@@ -233,7 +246,11 @@ const testCases: TestsCase[] = [
|
||||
name: 'run-ai-docs.nvidia.com',
|
||||
contentBaseURL: 'https://run-ai-docs.nvidia.com',
|
||||
tests: [
|
||||
{ name: 'Home', url: '/' },
|
||||
{
|
||||
name: 'Home',
|
||||
url: '/',
|
||||
cookies: CLOSED_ONETRUST_COOKIE_BANNER,
|
||||
},
|
||||
{ name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
CustomizationBackground,
|
||||
CustomizationCorners,
|
||||
CustomizationDefaultMonospaceFont,
|
||||
CustomizationDefaultThemeMode,
|
||||
CustomizationDepth,
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationIconsStyle,
|
||||
CustomizationSidebarListStyle,
|
||||
CustomizationThemeMode,
|
||||
SiteSocialAccountPlatform,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookStandalone } from '@gitbook/embed';
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
waitForNotFound,
|
||||
} from './util';
|
||||
|
||||
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 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 overrideAIInitialState = () => {
|
||||
const greeting = document.querySelector('[data-testid="ai-chat-greeting-title"]');
|
||||
@@ -69,7 +69,6 @@ const searchTestCases: Test[] = [
|
||||
mode: CustomizationAIMode.None,
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
await waitForCookiesDialog(page);
|
||||
const searchInput = page.getByTestId('search-input');
|
||||
@@ -95,7 +94,6 @@ const searchTestCases: Test[] = [
|
||||
mode: CustomizationAIMode.None,
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
await waitForCookiesDialog(page);
|
||||
await page.keyboard.press('ControlOrMeta+K');
|
||||
@@ -149,7 +147,6 @@ const searchTestCases: Test[] = [
|
||||
mode: CustomizationAIMode.Assistant,
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
await waitForCookiesDialog(page);
|
||||
const searchInput = page.locator('css=[data-testid="search-input"]');
|
||||
@@ -186,7 +183,6 @@ const searchTestCases: Test[] = [
|
||||
mode: CustomizationAIMode.Assistant,
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
await waitForCookiesDialog(page);
|
||||
await page.keyboard.press('ControlOrMeta+I');
|
||||
@@ -203,7 +199,6 @@ const searchTestCases: Test[] = [
|
||||
mode: CustomizationAIMode.Assistant,
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
await waitForCookiesDialog(page);
|
||||
await page.getByTestId('ai-chat-button').click();
|
||||
@@ -220,7 +215,6 @@ const searchTestCases: Test[] = [
|
||||
mode: CustomizationAIMode.Assistant,
|
||||
},
|
||||
})}&ask=`,
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
await waitForCookiesDialog(page);
|
||||
await expect(page.getByTestId('search-input')).not.toBeFocused();
|
||||
@@ -238,7 +232,6 @@ 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();
|
||||
@@ -327,7 +320,6 @@ const testCases: TestsCase[] = [
|
||||
await expect(navigationLink).toBeVisible();
|
||||
},
|
||||
},
|
||||
...searchTestCases,
|
||||
{
|
||||
name: 'Not found',
|
||||
url: 'content-not-found',
|
||||
@@ -623,7 +615,6 @@ const testCases: TestsCase[] = [
|
||||
url: '',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
...searchTestCases,
|
||||
{
|
||||
name: 'Not found',
|
||||
url: 'content-not-found',
|
||||
@@ -731,95 +722,6 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Site Preview',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
tests: [
|
||||
{
|
||||
name: 'Main content',
|
||||
url: async () => {
|
||||
const data = await getSiteAPIToken(
|
||||
'https://gitbook.gitbook.io/test-gitbook-open/'
|
||||
);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('token', data.apiToken);
|
||||
|
||||
return `url/preview/${data.site}/?${searchParams.toString()}`;
|
||||
},
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
await expect(page.locator('[data-testid="table-of-contents"]')).toBeVisible();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'With sections',
|
||||
url: async () => {
|
||||
const data = await getSiteAPIToken('https://gitbook.com/docs');
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('token', data.apiToken);
|
||||
|
||||
return `url/preview/${data.site}/?${searchParams.toString()}`;
|
||||
},
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
const sectionTabs = page.getByLabel('Sections');
|
||||
await expect(sectionTabs).toBeVisible();
|
||||
|
||||
const sectionTabLinks = sectionTabs.getByRole('link');
|
||||
for (const link of await sectionTabLinks.all()) {
|
||||
const href = await link.getAttribute('href');
|
||||
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/preview/${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: 'Site Previews',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
@@ -964,6 +866,11 @@ 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/',
|
||||
@@ -1190,7 +1097,7 @@ const testCases: TestsCase[] = [
|
||||
name: 'With cover for dark mode',
|
||||
url: `page-options/page-with-dark-cover${getCustomizationURL({
|
||||
themes: {
|
||||
default: CustomizationThemeMode.Dark,
|
||||
default: CustomizationDefaultThemeMode.Dark,
|
||||
toggeable: false,
|
||||
},
|
||||
})}`,
|
||||
@@ -1477,7 +1384,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,
|
||||
},
|
||||
@@ -1497,7 +1404,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,
|
||||
},
|
||||
@@ -2248,7 +2155,7 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'Docs Embed - Basic',
|
||||
contentBaseURL: 'https://gitbook.com/docs/~gitbook/embed/demo/',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/~gitbook/embed/demo/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Standalone UX',
|
||||
@@ -2295,7 +2202,7 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'Docs Embed - Assistant + Docs',
|
||||
contentBaseURL: 'https://gitbook.com/docs/~gitbook/embed/demo/',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/~gitbook/embed/demo/',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
tests: [
|
||||
{
|
||||
@@ -2323,7 +2230,7 @@ const testCases: TestsCase[] = [
|
||||
run: async (page) => {
|
||||
await page.evaluate(() => {
|
||||
const GitBook = window.GitBook as unknown as GitBookStandalone;
|
||||
GitBook('navigateToPage', '/getting-started/quickstart');
|
||||
GitBook('navigateToPage', '/text-page');
|
||||
});
|
||||
await expect(page.locator('#gitbook-widget-window')).toBeVisible();
|
||||
const iframe = page.frameLocator('#gitbook-widget-iframe');
|
||||
@@ -2332,7 +2239,7 @@ const testCases: TestsCase[] = [
|
||||
});
|
||||
await expect(iframe.owner()).toHaveAttribute(
|
||||
'src',
|
||||
expect.stringContaining('getting-started/quickstart')
|
||||
expect.stringContaining('text-page')
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -2393,7 +2300,7 @@ const testCases: TestsCase[] = [
|
||||
onClick: () => {
|
||||
const GitBook =
|
||||
window.GitBook as unknown as GitBookStandalone;
|
||||
GitBook('navigateToPage', '/getting-started/quickstart');
|
||||
GitBook('navigateToPage', '/text-page');
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2434,7 +2341,7 @@ const testCases: TestsCase[] = [
|
||||
await expect(iframe.getByTestId('embed-docs-page')).toBeVisible();
|
||||
await expect(iframe.owner()).toHaveAttribute(
|
||||
'src',
|
||||
expect.stringContaining('getting-started/quickstart')
|
||||
expect.stringContaining('text-page')
|
||||
);
|
||||
|
||||
await expect(actions.nth(1)).toHaveAccessibleName('Open external link');
|
||||
@@ -2518,6 +2425,12 @@ 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({
|
||||
@@ -2529,6 +2442,12 @@ 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({
|
||||
@@ -2544,6 +2463,12 @@ 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({
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CustomizationCorners,
|
||||
CustomizationDefaultFont,
|
||||
CustomizationDefaultMonospaceFont,
|
||||
CustomizationDefaultThemeMode,
|
||||
CustomizationDepth,
|
||||
type CustomizationHeaderItem,
|
||||
CustomizationHeaderPreset,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
CustomizationSidebarBackgroundStyle,
|
||||
CustomizationSidebarListStyle,
|
||||
CustomizationTheme,
|
||||
CustomizationThemeMode,
|
||||
type CustomizationThemedColor,
|
||||
type SiteCustomizationSettings,
|
||||
SiteExternalLinksTarget,
|
||||
@@ -84,9 +84,9 @@ export const allLocales: CustomizationLocale[] = [
|
||||
CustomizationLocale.Zh,
|
||||
];
|
||||
|
||||
export const allThemeModes: CustomizationThemeMode[] = [
|
||||
CustomizationThemeMode.Light,
|
||||
CustomizationThemeMode.Dark,
|
||||
export const allThemeModes: CustomizationDefaultThemeMode[] = [
|
||||
CustomizationDefaultThemeMode.Light,
|
||||
CustomizationDefaultThemeMode.Dark,
|
||||
];
|
||||
|
||||
export const allTintColors: Array<{
|
||||
@@ -340,7 +340,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
groups: [],
|
||||
},
|
||||
themes: {
|
||||
default: CustomizationThemeMode.Light,
|
||||
default: CustomizationDefaultThemeMode.System,
|
||||
toggeable: true,
|
||||
},
|
||||
pdf: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.22.3",
|
||||
"version": "0.23.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@cloudflare/workers-types": "^4.20251011.0",
|
||||
@@ -21,6 +21,7 @@
|
||||
"@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",
|
||||
@@ -30,6 +31,7 @@
|
||||
"@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:",
|
||||
@@ -51,7 +53,7 @@
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"negotiator": "^1.0.0",
|
||||
"leven": "^4.1.0",
|
||||
"next": "15.4.11",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
@@ -109,7 +111,7 @@
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-essentials": "^10.0.1",
|
||||
"typescript": "catalog:",
|
||||
"vercel": "^50.15.1",
|
||||
"vercel": "50.37.3",
|
||||
"wrangler": "^4.43.0",
|
||||
"rss-parser": "^3.13.0"
|
||||
},
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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 };
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
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);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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 };
|
||||
+22
-7
@@ -21,6 +21,11 @@ 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(),
|
||||
@@ -58,7 +63,7 @@ export async function POST(request: NextRequest) {
|
||||
]);
|
||||
|
||||
const results = searchResults
|
||||
.map((resultItem) => {
|
||||
.flatMap((resultItem): SearchResultGroup[] => {
|
||||
if (resultItem.type === 'record') {
|
||||
const result: OrderedComputedResult = {
|
||||
type: 'record',
|
||||
@@ -66,8 +71,14 @@ export async function POST(request: NextRequest) {
|
||||
title: resultItem.title,
|
||||
description: resultItem.description,
|
||||
href: resultItem.url,
|
||||
score: resultItem.score,
|
||||
};
|
||||
return result;
|
||||
return [
|
||||
{
|
||||
score: resultItem.score,
|
||||
items: [result],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const found = findSiteSpaceBy(
|
||||
@@ -77,8 +88,9 @@ export async function POST(request: NextRequest) {
|
||||
const siteSection = found?.siteSection;
|
||||
const siteSectionGroup = found?.siteSectionGroup;
|
||||
|
||||
return resultItem.pages.map((pageItem) =>
|
||||
transformSitePageResult(context, {
|
||||
return resultItem.pages.map((pageItem) => ({
|
||||
score: pageItem.score,
|
||||
items: transformSitePageResult(context, {
|
||||
pageItem,
|
||||
spaceItem: resultItem,
|
||||
siteSpace: found?.siteSpace,
|
||||
@@ -86,10 +98,11 @@ export async function POST(request: NextRequest) {
|
||||
spaceURL: found?.siteSpace.urls.published,
|
||||
siteSection: siteSection ?? undefined,
|
||||
siteSectionGroup: (siteSectionGroup as SiteSectionGroup) ?? undefined,
|
||||
})
|
||||
);
|
||||
}),
|
||||
}));
|
||||
})
|
||||
.flat(2);
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.flatMap((group) => group.items);
|
||||
|
||||
return NextResponse.json(results);
|
||||
}
|
||||
@@ -119,6 +132,7 @@ function transformSitePageResult(
|
||||
: linker.toPathInSpace(pageItem.path),
|
||||
pageId: pageItem.id,
|
||||
spaceId: spaceItem.id,
|
||||
score: pageItem.score,
|
||||
breadcrumbs: [
|
||||
siteSectionGroup && {
|
||||
icon: siteSectionGroup?.icon as IconName,
|
||||
@@ -157,6 +171,7 @@ function transformSitePageResult(
|
||||
body: section.body,
|
||||
pageId: pageItem.id,
|
||||
spaceId: spaceItem.id,
|
||||
score: section.score,
|
||||
})) ?? [];
|
||||
|
||||
return [page, ...pageSections];
|
||||
|
||||
@@ -10,5 +10,6 @@ export async function GET(
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getStaticSiteContext(await params);
|
||||
|
||||
return serveLLMsTxt(context, { withMarkdownPages: true });
|
||||
}
|
||||
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
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,6 +1,7 @@
|
||||
'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';
|
||||
@@ -29,6 +30,7 @@ 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
|
||||
@@ -36,17 +38,18 @@ function ConfirmControl(props: GetAIControlProps<typeof ConfirmControlDef>) {
|
||||
onClick={() => {
|
||||
onSubmit({ result: 'cancelled' });
|
||||
}}
|
||||
variant="secondary"
|
||||
truncate={false}
|
||||
variant="blank"
|
||||
icon="xmark"
|
||||
label="Cancel"
|
||||
label={tString(language, 'cancel')}
|
||||
/>
|
||||
<Button
|
||||
data-testid="ai-chat-tool-confirm-accept"
|
||||
onClick={() => {
|
||||
onSubmit({ result: 'confirmed' });
|
||||
}}
|
||||
className="flex-1"
|
||||
variant="primary"
|
||||
truncate={false}
|
||||
icon={icon}
|
||||
label={label}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
'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,6 +1,8 @@
|
||||
'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';
|
||||
@@ -11,6 +13,7 @@ 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()
|
||||
@@ -21,7 +24,7 @@ export const SingleChoiceControlDef = createAIControl({
|
||||
name: 'single-choice',
|
||||
exposeAsTool: true,
|
||||
description:
|
||||
'Use this control when you need the user to choose exactly one option from a predefined list.',
|
||||
'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.',
|
||||
inputSchema: z.object({
|
||||
prompt: z
|
||||
.string()
|
||||
@@ -44,7 +47,7 @@ export const SingleChoiceControlDef = createAIControl({
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Optionally provide supporting details to help the user understand this option.'
|
||||
'Optionally provide supporting details to help the user understand this option. Keep it concise and do not repeat the label.'
|
||||
),
|
||||
})
|
||||
.describe('Define one selectable option the user can pick.')
|
||||
@@ -70,14 +73,16 @@ 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">
|
||||
<div className="no-scrollbar flex flex-1 flex-col gap-1 overflow-auto">
|
||||
<p className="mb-1 font-semibold text-sm">{prompt}</p>
|
||||
<p className="px-2 pt-1 font-semibold text-sm">{prompt}</p>
|
||||
<ScrollContainer orientation="vertical" contentClassName="flex flex-col gap-2">
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedId === option.id;
|
||||
return (
|
||||
@@ -89,17 +94,18 @@ function SingleChoiceControl(props: GetAIControlProps<typeof SingleChoiceControl
|
||||
setSelectedId(option.id);
|
||||
}}
|
||||
className={tcls(
|
||||
'circular-corners:rounded-3xl rounded-corners:rounded-xl border px-3 py-2 text-left transition-colors',
|
||||
'text-left transition-colors',
|
||||
'circular-corners:rounded-3xl rounded-corners:rounded-xl px-2 py-1 text-left transition-colors',
|
||||
isSelected
|
||||
? 'border-primary-original bg-primary-subtle text-tint-strong'
|
||||
: 'border-tint bg-tint-base hover:bg-tint-subtle'
|
||||
? '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-3">
|
||||
<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-full border transition-colors',
|
||||
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border bg-tint-base transition-colors',
|
||||
isSelected
|
||||
? 'border-primary-original'
|
||||
: 'border-tint-subtle'
|
||||
@@ -115,7 +121,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-sm text-tint-subtle">
|
||||
<p className="mt-0.5 text-tint-subtle text-xs">
|
||||
{option.description}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -129,58 +135,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'
|
||||
)}
|
||||
>
|
||||
<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'
|
||||
)}
|
||||
>
|
||||
<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={
|
||||
<span
|
||||
aria-hidden
|
||||
className={tcls(
|
||||
'size-2.5 rounded-full transition-colors',
|
||||
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border bg-tint-base transition-colors',
|
||||
selectedId === OTHER_OPTION_ID
|
||||
? 'bg-primary-original'
|
||||
: 'bg-transparent'
|
||||
? 'border-primary-original'
|
||||
: 'border-tint-subtle'
|
||||
)}
|
||||
/>
|
||||
</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
|
||||
className={tcls(
|
||||
'size-2.5 rounded-full transition-colors',
|
||||
selectedId === OTHER_OPTION_ID
|
||||
? 'bg-primary-original'
|
||||
: 'bg-transparent'
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
|
||||
<Button
|
||||
data-testid="ai-chat-tool-single-choice-submit"
|
||||
variant="primary"
|
||||
label="Submit answer"
|
||||
label={tString(language, 'submit')}
|
||||
disabled={!canSubmit}
|
||||
onClick={() => {
|
||||
if (!canSubmit || !selectedId) {
|
||||
@@ -190,12 +196,16 @@ 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 });
|
||||
onSubmit({
|
||||
id: selectedId,
|
||||
label: options.find((option) => option.id === selectedId)?.label || '',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</AIToolContainer>
|
||||
|
||||
@@ -6,10 +6,10 @@ export function AIToolContainer(props: ComponentPropsWithRef<'div'>) {
|
||||
<div
|
||||
{...props}
|
||||
className={tcls(
|
||||
'min-h-0 animate-present-slow circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-subtle p-2',
|
||||
'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',
|
||||
props.className
|
||||
)}
|
||||
style={{ animationDelay: '0.5s', ...props.style }}
|
||||
style={{ animationDelay: '0.3s', ...props.style }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { tString } from '@/intl/translate';
|
||||
import type { TranslationLanguage } from '@/intl/translations/types';
|
||||
import type {
|
||||
AIStreamResponseToolCallPending,
|
||||
AIToolCallResult,
|
||||
@@ -16,6 +18,7 @@ type AIControlDefinition<
|
||||
createControl: (args: {
|
||||
context: AIUIToolContext;
|
||||
input: Input;
|
||||
language: TranslationLanguage;
|
||||
send: (result: Pick<AIToolCallResult, 'output' | 'summary'>) => Promise<void>;
|
||||
}) => AIControl<Name, Input, Output>;
|
||||
exposeAsTool: boolean;
|
||||
@@ -55,15 +58,15 @@ export function createAIControl<
|
||||
name: `ui--${def.name}`,
|
||||
description: def.description,
|
||||
inputSchema: zodToJsonSchema(def.inputSchema as any) as AIToolDefinition['inputSchema'],
|
||||
createControl: ({ context, input, send }) => {
|
||||
createControl: ({ context, input, language, send }) => {
|
||||
const props: AIControlProps<z.infer<InputSchema>, z.infer<OutputSchema>> = {
|
||||
...input,
|
||||
onSubmit: async (output) => {
|
||||
await send({
|
||||
output,
|
||||
summary: {
|
||||
icon: 'check',
|
||||
text: 'Submitted',
|
||||
icon: 'comment-check',
|
||||
text: tString(language, 'ai_control_submitted_answer'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ConfirmControlDef } from './ConfirmControl';
|
||||
import { MultiChoiceControlDef } from './MultiChoiceControl';
|
||||
import { SingleChoiceControlDef } from './SingleChoiceControl';
|
||||
|
||||
const CONTROLS = [SingleChoiceControlDef, ConfirmControlDef];
|
||||
const CONTROLS = [SingleChoiceControlDef, MultiChoiceControlDef, 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"
|
||||
style="ai-response-document mt-2 space-y-4 *:origin-top-left *:animate-blur-in-slow empty:hidden"
|
||||
/>
|
||||
|
||||
{withToolCalls && step.toolCalls && step.toolCalls.length > 0 ? (
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
AIModel,
|
||||
type AIToolCallResult,
|
||||
type AIToolDefinition,
|
||||
SiteCoreChannelType,
|
||||
type SiteInsightsSession,
|
||||
} from '@gitbook/api';
|
||||
import { streamRenderAIMessage } from './api';
|
||||
@@ -61,6 +62,11 @@ 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);
|
||||
streamResponse({
|
||||
await streamResponse({
|
||||
toolCall: {
|
||||
tool: event.toolCall.tool,
|
||||
toolCallId: event.toolCallId,
|
||||
@@ -233,7 +233,7 @@ export function AIChatProvider(props: {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
streamResponse({
|
||||
await streamResponse({
|
||||
toolCall: {
|
||||
tool: event.toolCall.tool,
|
||||
toolCallId: event.toolCallId,
|
||||
@@ -322,6 +322,7 @@ export function AIChatProvider(props: {
|
||||
toolCallId: event.toolCallId,
|
||||
},
|
||||
input: event.toolCall.input as any,
|
||||
language,
|
||||
send: async (result) => {
|
||||
await streamResponse({
|
||||
toolCall: {
|
||||
@@ -350,6 +351,7 @@ export function AIChatProvider(props: {
|
||||
label: confirmation.label,
|
||||
icon: confirmation.icon,
|
||||
},
|
||||
language,
|
||||
send: async (result) => {
|
||||
const output = ConfirmControlOutputSchema.parse(
|
||||
result.output
|
||||
@@ -406,13 +408,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,17 +163,26 @@ export function AIChatSubtitle(props: {
|
||||
|
||||
return (
|
||||
<EmbeddableFrameSubtitle
|
||||
className={tcls('relative', chat.loading ? 'h-3 opacity-11' : 'h-0 opacity-0')}
|
||||
className={tcls(
|
||||
'relative',
|
||||
chat.loading || chat.control ? '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-slow'
|
||||
? 'animate-blur-in-display-slow'
|
||||
: 'hidden'
|
||||
: 'animate-blur-out-slow'
|
||||
: 'animate-blur-out-display-slow'
|
||||
)}
|
||||
style={{
|
||||
animationDelay:
|
||||
chat.messages[chat.messages.length - 1]?.content && !chat.control
|
||||
? '.3s'
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{t(language, 'ai_chat_working')}
|
||||
</span>
|
||||
@@ -182,13 +191,28 @@ export function AIChatSubtitle(props: {
|
||||
'absolute left-0',
|
||||
chat.loading
|
||||
? chat.messages[chat.messages.length - 1]?.content
|
||||
? 'animate-blur-out-slow'
|
||||
: 'animate-blur-in-slow'
|
||||
? 'animate-blur-out-display-slow'
|
||||
: 'animate-blur-in-display-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>
|
||||
);
|
||||
}
|
||||
@@ -226,7 +250,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"
|
||||
contentClassName="py-4 gutter-stable flex flex-col gap-4 [scroll-behavior:smooth]"
|
||||
orientation="vertical"
|
||||
trailing={{ fade: false, button: true }}
|
||||
active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
|
||||
@@ -274,21 +298,18 @@ export function AIChatBody(props: {
|
||||
)}
|
||||
</ScrollContainer>
|
||||
|
||||
<div className="flex min-h-0 flex-col gap-2 pb-4">
|
||||
<div className="flex max-h-3/4 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} />
|
||||
) : (
|
||||
<AIChatInput
|
||||
loading={chat.loading}
|
||||
disabled={chat.loading || chat.error}
|
||||
onSubmit={(value) => {
|
||||
chatController.postMessage({ message: value });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{chat.control ? <AIChatControl control={chat.control} /> : null}
|
||||
<AIChatInput
|
||||
loading={chat.loading}
|
||||
disabled={chat.loading || chat.error}
|
||||
onSubmit={(value) => {
|
||||
chatController.postMessage({ message: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -58,7 +59,10 @@ export function AIChatInput(props: {
|
||||
size: 'small',
|
||||
label: tString(language, 'send'),
|
||||
}}
|
||||
className="animate-blur-in-slow bg-tint-base/9 backdrop-blur-lg contrast-more:bg-tint-base"
|
||||
className={tcls(
|
||||
chat.control ? 'animate-blur-out-display' : 'animate-blur-in-slow',
|
||||
'bg-tint-base/9 backdrop-blur-lg contrast-more:bg-tint-base'
|
||||
)}
|
||||
rows={1}
|
||||
maxLength={2048}
|
||||
keyboardShortcut={
|
||||
@@ -69,7 +73,7 @@ export function AIChatInput(props: {
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
disabled={disabled || loading}
|
||||
disabled={disabled || loading || chat.control !== null}
|
||||
aria-busy={loading}
|
||||
ref={inputRef}
|
||||
trailing={
|
||||
|
||||
@@ -68,7 +68,10 @@ 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'
|
||||
: 'origin-top-left text-tint-strong',
|
||||
isLastMessage && message.role === AIMessageRole.Assistant
|
||||
? 'grow'
|
||||
: ''
|
||||
)}
|
||||
style={{
|
||||
animationDelay: `${Math.min(originalIndex * 0.1, 0.6)}s`,
|
||||
@@ -94,7 +97,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 max-w-full flex-col items-start gap-2 self-start"
|
||||
className="flex 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="flex grow flex-col">
|
||||
<div className="mt-auto 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,6 +26,7 @@ export async function Announcement(props: {
|
||||
return (
|
||||
<AnnouncementBanner
|
||||
announcement={customization.announcement}
|
||||
locale={context.locale}
|
||||
contentRef={resolvedContentRef}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
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 } from '@gitbook/api';
|
||||
import {
|
||||
type CustomizationAnnouncement,
|
||||
SiteInsightsLinkPosition,
|
||||
type TranslationLanguage,
|
||||
} from '@gitbook/api';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { Button, Link } from '../primitives';
|
||||
@@ -16,11 +21,14 @@ import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
|
||||
*/
|
||||
export function AnnouncementBanner(props: {
|
||||
announcement: CustomizationAnnouncement;
|
||||
locale: TranslationLanguage | undefined;
|
||||
contentRef: ResolvedContentRef | null;
|
||||
}) {
|
||||
const { announcement, contentRef } = props;
|
||||
const { announcement, locale, 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';
|
||||
@@ -61,7 +69,7 @@ export function AnnouncementBanner(props: {
|
||||
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
|
||||
/>
|
||||
<div>
|
||||
{announcement.message}
|
||||
{message}
|
||||
{hasLink ? (
|
||||
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
|
||||
{contentRef?.icon ? (
|
||||
@@ -69,9 +77,7 @@ export function AnnouncementBanner(props: {
|
||||
{contentRef?.icon}
|
||||
</span>
|
||||
) : null}
|
||||
{announcement.link?.title && (
|
||||
<span className="mr-1">{announcement.link?.title}</span>
|
||||
)}
|
||||
{linkTitle ? <span className="mr-1">{linkTitle}</span> : null}
|
||||
<Icon
|
||||
icon={
|
||||
announcement.link?.to.kind === 'url'
|
||||
|
||||
@@ -193,10 +193,10 @@ function CodeBlockExpandable(props: {
|
||||
<Button
|
||||
icon={<ToggleChevron open={isExpanded} />}
|
||||
size="xsmall"
|
||||
variant="blank"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="pointer-events-auto z-1 my-2 text-primary text-sm opacity-0 focus:opacity-11 group-hover/codeblock-expandable:opacity-11"
|
||||
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"
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={controls}
|
||||
>
|
||||
|
||||
@@ -63,43 +63,45 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<CopyCodeButton
|
||||
codeId={codeId}
|
||||
style="absolute top-2 right-2 z-2 self-start justify-self-end leading-none opacity-0 backdrop-blur-md 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 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!'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: bg?.color,
|
||||
...bg?.vars,
|
||||
color: fg?.color,
|
||||
...fg?.vars,
|
||||
}}
|
||||
>
|
||||
<code
|
||||
id={codeId}
|
||||
<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
|
||||
className={tcls(
|
||||
'table max-h-full w-fit [counter-reset:line] print:max-h-none print:whitespace-pre-wrap',
|
||||
withWrap && 'whitespace-pre-wrap',
|
||||
'[[aria-expanded=false]_&]:mask-b-from-50%'
|
||||
'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!'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: bg?.color,
|
||||
...bg?.vars,
|
||||
color: fg?.color,
|
||||
...fg?.vars,
|
||||
}}
|
||||
>
|
||||
{theme.lines.map((line, index) => (
|
||||
<CodeHighlightLine
|
||||
bg={bg}
|
||||
fg={fg}
|
||||
key={index}
|
||||
line={line}
|
||||
isLast={index === theme.lines.length - 1}
|
||||
withLineNumbers={withLineNumbers}
|
||||
/>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,9 @@ 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';
|
||||
|
||||
/**
|
||||
@@ -14,7 +16,9 @@ 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();
|
||||
@@ -23,11 +27,13 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
|
||||
|
||||
useEffect(() => {
|
||||
const container = diagramRef.current;
|
||||
if (!container) {
|
||||
const wrapper = wrapperRef.current;
|
||||
if (!container || !wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
let cleanupPanZoom: (() => void) | undefined;
|
||||
setError(false);
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -37,6 +43,15 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
|
||||
id,
|
||||
darkMode,
|
||||
})
|
||||
.then(() => {
|
||||
if (!cancelled) {
|
||||
cleanupPanZoom = initPanzoom({
|
||||
container,
|
||||
wrapper,
|
||||
onInit: setPanZoom,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setError(true);
|
||||
@@ -50,6 +65,8 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cleanupPanZoom?.();
|
||||
setPanZoom(null);
|
||||
};
|
||||
}, [source, id, darkMode]);
|
||||
|
||||
@@ -59,7 +76,14 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
|
||||
|
||||
return (
|
||||
<div className={tcls('relative', style)} contentEditable={false}>
|
||||
<div className={isLoading ? 'invisible absolute inset-x-0' : undefined}>
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={
|
||||
isLoading
|
||||
? 'invisible absolute inset-x-0 overflow-hidden'
|
||||
: 'cursor-grab overflow-hidden active:cursor-grabbing'
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={diagramRef}
|
||||
className="overflow-auto p-2 [&_svg]:h-auto [&_svg]:max-w-full"
|
||||
@@ -70,6 +94,7 @@ export function MermaidCodeBlock(props: ClientBlockProps) {
|
||||
<Loading className="h-8 w-8" />
|
||||
</div>
|
||||
) : null}
|
||||
{!isLoading && panZoom ? <MermaidPanZoomControls panZoom={panZoom} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -100,6 +125,35 @@ 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]);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
'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,13 +1,16 @@
|
||||
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 } = props;
|
||||
const { inline, context } = props;
|
||||
|
||||
const buttonProps: ButtonProps = {
|
||||
label: inline.data.label,
|
||||
@@ -17,7 +20,8 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
|
||||
};
|
||||
|
||||
const ButtonImplementation = () => {
|
||||
if ('action' in inline.data && 'query' in inline.data.action) {
|
||||
// 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) {
|
||||
return (
|
||||
<InlineActionButton
|
||||
action={inline.data.action.action}
|
||||
@@ -57,21 +61,27 @@ 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 = (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: api.SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
href={href}
|
||||
disabled={href === undefined}
|
||||
/>
|
||||
);
|
||||
const button =
|
||||
href &&
|
||||
context.contentContext &&
|
||||
isSiteAuthLoginHref(context.contentContext.linker, href) ? (
|
||||
<SiteAuthLoginButton {...sharedProps} />
|
||||
) : (
|
||||
<Button {...sharedProps} />
|
||||
);
|
||||
|
||||
if (inline.data.ref && !resolved) {
|
||||
return <NotFoundRefHoverCard context={context}>{button}</NotFoundRefHoverCard>;
|
||||
|
||||
@@ -50,6 +50,10 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
|
||||
props,
|
||||
specUrl: publicURL,
|
||||
context: context.contentContext,
|
||||
expandAllResponses:
|
||||
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
|
||||
expandAllModelSections:
|
||||
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
|
||||
})}
|
||||
className="openapi-block"
|
||||
/>
|
||||
|
||||
@@ -46,12 +46,13 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
|
||||
|
||||
return (
|
||||
<BaseOpenAPISchemas
|
||||
schemas={data.schemas}
|
||||
data={data}
|
||||
grouped={block.data.grouped}
|
||||
context={getOpenAPIContext({
|
||||
props,
|
||||
specUrl: publicURL,
|
||||
context: context.contentContext,
|
||||
expandAllModelSections: data['x-expandAllModelSections'],
|
||||
})}
|
||||
className="openapi-block"
|
||||
/>
|
||||
|
||||
@@ -52,6 +52,10 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
|
||||
props,
|
||||
specUrl: publicURL,
|
||||
context: context.contentContext,
|
||||
expandAllResponses:
|
||||
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
|
||||
expandAllModelSections:
|
||||
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
|
||||
})}
|
||||
className="openapi-block"
|
||||
/>
|
||||
|
||||
@@ -2,8 +2,6 @@ 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';
|
||||
@@ -26,16 +24,19 @@ export function getOpenAPIContext(args: {
|
||||
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>;
|
||||
specUrl: string | null;
|
||||
context: GitBookAnyContext | undefined;
|
||||
expandAllResponses?: boolean;
|
||||
expandAllModelSections?: boolean;
|
||||
}): OpenAPIContextInput {
|
||||
const { props, specUrl, context } = args;
|
||||
const { props, specUrl, context, expandAllResponses, expandAllModelSections } = args;
|
||||
const { block } = props;
|
||||
|
||||
const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE;
|
||||
const locale = checkIsValidLocale(customizationLocale) ? customizationLocale : DEFAULT_LOCALE;
|
||||
|
||||
const proxyUrl = context
|
||||
? context.linker.toAbsoluteURL(context.linker.toPathInSite('~scalar/proxy'))
|
||||
: undefined;
|
||||
const proxyUrl =
|
||||
context && props.context.mode !== 'print'
|
||||
? context.linker.toAbsoluteURL(context.linker.toPathInSite('~scalar/proxy'))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
specUrl,
|
||||
@@ -75,12 +76,7 @@ export function getOpenAPIContext(args: {
|
||||
ancestorBlocks={props.ancestorBlocks}
|
||||
isEstimatedOffscreen={props.isEstimatedOffscreen}
|
||||
context={props.context}
|
||||
style={tcls([
|
||||
headingProps.deprecated ? 'line-through' : undefined,
|
||||
headingProps.deprecated || !!headingProps.stability
|
||||
? '[&>div]:pt-0'
|
||||
: undefined,
|
||||
])}
|
||||
style={headingProps.deprecated ? 'line-through' : undefined}
|
||||
block={{
|
||||
object: 'block',
|
||||
key: `${block.key}-heading`,
|
||||
@@ -97,7 +93,8 @@ export function getOpenAPIContext(args: {
|
||||
}}
|
||||
/>
|
||||
),
|
||||
defaultInteractiveOpened: props.context.mode === 'print',
|
||||
expandAllResponses: expandAllResponses || props.context.mode === 'print',
|
||||
expandAllModelSections: expandAllModelSections || props.context.mode === 'print',
|
||||
id: block.meta?.id,
|
||||
blockKey: block.key,
|
||||
locale,
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
/* Layout Components */
|
||||
.openapi-operation,
|
||||
.openapi-schemas,
|
||||
.openapi-webhook {
|
||||
@apply flex-1 flex flex-col gap-8 mb-14 min-w-0;
|
||||
.openapi-webhook,
|
||||
.openapi-schemas-single {
|
||||
@apply flex-1 flex flex-col gap-6 mb-14 min-w-0;
|
||||
}
|
||||
|
||||
.openapi-schemas {
|
||||
@apply flex flex-col mb-14 gap-0 flex-1;
|
||||
.openapi-schemas:not(.openapi-schemas-single) {
|
||||
@apply gap-0;
|
||||
}
|
||||
|
||||
.openapi-schemas-title {
|
||||
@@ -33,6 +34,10 @@
|
||||
@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;
|
||||
@@ -144,7 +149,7 @@
|
||||
}
|
||||
|
||||
.openapi-column-preview {
|
||||
@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);
|
||||
@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);
|
||||
}
|
||||
|
||||
.openapi-column-preview-body {
|
||||
@@ -233,7 +238,7 @@
|
||||
}
|
||||
|
||||
.openapi-schema-type {
|
||||
@apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem];
|
||||
@apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem] whitespace-normal;
|
||||
}
|
||||
|
||||
.openapi-schema-type:only-child {
|
||||
|
||||
@@ -4,8 +4,7 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { RecordColumnValue } from './RecordColumnValue';
|
||||
import type { TableRecordKV, TableViewProps } from './Table';
|
||||
import { getColumnWidth } from './ViewGrid';
|
||||
import styles from './table.module.css';
|
||||
import { getColumnWidth } from './layout';
|
||||
import { getColumnVerticalAlignment } from './utils';
|
||||
|
||||
export function RecordRow(
|
||||
@@ -18,7 +17,15 @@ export function RecordRow(
|
||||
const { view, autoSizedColumns, fixedColumns, block, context } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.row} role="row">
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'border-tint-subtle',
|
||||
'transition-colors',
|
||||
'hover:bg-tint-hover'
|
||||
)}
|
||||
role="row"
|
||||
>
|
||||
{view.columns.map((column) => {
|
||||
const columnWidth = getColumnWidth({
|
||||
column,
|
||||
@@ -33,7 +40,10 @@ export function RecordRow(
|
||||
<div
|
||||
key={column}
|
||||
role="cell"
|
||||
className={tcls(styles.cell)}
|
||||
className={tcls(
|
||||
'relative flex flex-1 border-r px-3 py-2 align-middle text-sm last:border-r-0',
|
||||
'border-tint-subtle'
|
||||
)}
|
||||
style={{
|
||||
width: columnWidth,
|
||||
minWidth: columnWidth || '100px',
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
'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,10 +1,14 @@
|
||||
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 } from './ViewGrid';
|
||||
import { ViewGrid, ViewGridHeader } from './ViewGrid';
|
||||
import { getViewGridLayout, hasVisibleHeader } from './layout';
|
||||
|
||||
export type TableRecordKV = [string, DocumentTableRecord];
|
||||
|
||||
@@ -15,7 +19,7 @@ export interface TableViewProps<View> extends BlockProps<DocumentBlockTable> {
|
||||
}
|
||||
|
||||
export function Table(props: BlockProps<DocumentBlockTable>) {
|
||||
const { block, ancestorBlocks, document } = props;
|
||||
const { block, ancestorBlocks, document, context, style } = props;
|
||||
const isOffscreen = isBlockOffscreen({ block, ancestorBlocks, document });
|
||||
|
||||
const records: TableRecordKV[] = Object.entries(block.data.records).sort((a, b) => {
|
||||
@@ -32,15 +36,70 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
case 'grid':
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewGrid
|
||||
view={block.data.view}
|
||||
isOffscreen={isOffscreen}
|
||||
records={records}
|
||||
{...props}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
default:
|
||||
assertNever(block.data.view);
|
||||
}
|
||||
|
||||
@@ -4,118 +4,98 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { RecordRow } from './RecordRow';
|
||||
import type { TableViewProps } from './Table';
|
||||
import styles from './table.module.css';
|
||||
import { getColumnWidth, getViewGridLayout } from './layout';
|
||||
import { getColumnAlignment } from './utils';
|
||||
|
||||
/* 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 ViewGridHeaderProps extends TableViewProps<DocumentTableViewGrid> {
|
||||
className?: string;
|
||||
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]);
|
||||
interface ViewGridProps extends TableViewProps<DocumentTableViewGrid> {
|
||||
tableClassName?: string;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
export function ViewGridHeader(props: ViewGridHeaderProps) {
|
||||
const { block, view, context, className, tableClassName } = props;
|
||||
const { tableWidth, columnWidths, autoSizedColumns, fixedColumns } = getViewGridLayout({
|
||||
block,
|
||||
view,
|
||||
mode: context.mode,
|
||||
});
|
||||
|
||||
return (
|
||||
<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
|
||||
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>
|
||||
</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 const getColumnWidth = ({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}: {
|
||||
column: string;
|
||||
columnWidths: Record<string, number> | undefined;
|
||||
autoSizedColumns: string[];
|
||||
fixedColumns: string[];
|
||||
}) => {
|
||||
const columnWidth = columnWidths?.[column];
|
||||
export function ViewGrid(props: ViewGridProps) {
|
||||
const { block, view, records, context, tableClassName } = props;
|
||||
const { tableWidth, autoSizedColumns, fixedColumns } = getViewGridLayout({
|
||||
block,
|
||||
view,
|
||||
mode: context.mode,
|
||||
});
|
||||
|
||||
/* 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`;
|
||||
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>
|
||||
);
|
||||
|
||||
/* 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%)`;
|
||||
};
|
||||
return (
|
||||
<div role="table" className={tcls('flex', 'flex-col', tableClassName ?? tableWidth)}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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%)`;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
@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,7 +1,12 @@
|
||||
import { formatDateFull, formatDateShort, formatNumericDate } from '@/components/utils/dates';
|
||||
import {
|
||||
getFullDateFormatter,
|
||||
getNumericDateFormatter,
|
||||
getShortDateFormatter,
|
||||
} from '@/components/utils/dates';
|
||||
import { getRevisionTags, resolveBlockTags } from '@/lib/tags';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { DocumentBlockUpdate, DocumentBlockUpdates } from '@gitbook/api';
|
||||
import type { DocumentBlockUpdate, DocumentBlockUpdates, TranslationLanguage } from '@gitbook/api';
|
||||
import { assertNever } from 'assert-never';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { Tag } from '../Tag';
|
||||
import type { BlockProps } from './Block';
|
||||
@@ -31,11 +36,7 @@ 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 displayDate = {
|
||||
numeric: formatNumericDate(parsedDate),
|
||||
full: formatDateFull(parsedDate),
|
||||
short: formatDateShort(parsedDate),
|
||||
}[dateFormat];
|
||||
const dateFormatter = getDateFormatter(dateFormat, contextProps.context.contentContext?.locale);
|
||||
|
||||
// Resolve tags from the block data using revision-level tag definitions
|
||||
const revisionTags = getRevisionTags(contextProps.context.contentContext?.revision);
|
||||
@@ -54,7 +55,7 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
|
||||
dateTime={date}
|
||||
className="inline-flex items-center font-medium text-neutral-10 text-sm tracking-wide"
|
||||
>
|
||||
{displayDate}
|
||||
{dateFormatter.format(parsedDate)}
|
||||
</time>
|
||||
{resolvedTags.length > 0 ? (
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
@@ -74,6 +75,22 @@ 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();
|
||||
const embedConfig = useEmbeddableConfiguration((state) => state);
|
||||
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();
|
||||
const embedConfig = useEmbeddableConfiguration((state) => state);
|
||||
|
||||
return (
|
||||
<AIContextProvider
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -36,11 +37,12 @@ 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 });
|
||||
const refs = useRef({ router, chatController, baseURL, setTheme });
|
||||
useEffect(() => {
|
||||
refs.current = { router, chatController, baseURL };
|
||||
refs.current = { router, chatController, baseURL, setTheme };
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -57,7 +59,7 @@ export function EmbeddableIframeAPI(props: {
|
||||
}
|
||||
|
||||
channel.receive((payload) => {
|
||||
const { baseURL, router, chatController } = refs.current;
|
||||
const { baseURL, router, chatController, setTheme } = refs.current;
|
||||
const message = payload as ParentToFrameMessage;
|
||||
|
||||
log('[gitbook] received message', message);
|
||||
@@ -78,6 +80,15 @@ 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': {
|
||||
@@ -95,14 +106,11 @@ export function EmbeddableIframeAPI(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
/** Subscribe to embed configuration from the parent (Zustand). */
|
||||
export function useEmbeddableConfiguration<T>(
|
||||
selector: (state: GitBookEmbeddableConfiguration) => T
|
||||
): T {
|
||||
return useStore(embeddableConfiguration, selector);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +159,7 @@ export function EmbeddableIframeTabs(props: {
|
||||
siteTitle: string;
|
||||
}) {
|
||||
const { ref, active = 'assistant', baseURL, siteTitle } = props;
|
||||
const { tabs: configuredTabs, actions } = useEmbeddableConfiguration();
|
||||
const { tabs: configuredTabs, actions } = useEmbeddableConfiguration((state) => state);
|
||||
|
||||
const { assistants, config } = useAI();
|
||||
|
||||
@@ -223,7 +231,7 @@ export function EmbeddableIframeTabs(props: {
|
||||
}
|
||||
|
||||
export function EmbeddableIframeCloseButton() {
|
||||
const { closeButton } = useEmbeddableConfiguration();
|
||||
const { closeButton } = useEmbeddableConfiguration((state) => state);
|
||||
|
||||
if (!closeButton) {
|
||||
return null;
|
||||
|
||||
@@ -37,6 +37,7 @@ 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,6 +8,7 @@ import {
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getLocalizedTitle } from '@/lib/sites';
|
||||
import { Link } from '../primitives';
|
||||
|
||||
export function FooterLinksGroup(props: {
|
||||
@@ -15,10 +16,11 @@ 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">{group.title}</h4>
|
||||
<h4 className="font-semibold">{title}</h4>
|
||||
<ul className="flex flex-col items-start gap-4">
|
||||
{group.links.map((link, index) => {
|
||||
return (
|
||||
@@ -34,6 +36,7 @@ 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 (
|
||||
@@ -62,7 +65,7 @@ async function FooterLink(props: { link: CustomizationContentLink; context: GitB
|
||||
},
|
||||
}}
|
||||
>
|
||||
{link.title}
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import {
|
||||
type ContentRef,
|
||||
@@ -7,10 +8,16 @@ 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,
|
||||
@@ -28,6 +35,7 @@ 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 (
|
||||
@@ -35,15 +43,16 @@ export async function HeaderLink(props: {
|
||||
className={`shrink ${customization.styling.search === 'prominent' ? 'right-0 left-auto' : null}`}
|
||||
button={
|
||||
!target || !link.to ? (
|
||||
<HeaderItemDropdown headerPreset={headerPreset} title={link.title} />
|
||||
<HeaderItemDropdown headerPreset={headerPreset} title={title} />
|
||||
) : (
|
||||
<HeaderLinkNavItem
|
||||
linkTarget={link.to}
|
||||
linkStyle={linkStyle}
|
||||
headerPreset={headerPreset}
|
||||
title={link.title}
|
||||
title={title}
|
||||
isDropdown
|
||||
href={target?.href}
|
||||
isSiteAuthLoginHref={isSiteAuthLoginHref(context.linker, target.href)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -65,9 +74,10 @@ export async function HeaderLink(props: {
|
||||
linkTarget={link.to}
|
||||
linkStyle={linkStyle}
|
||||
headerPreset={headerPreset}
|
||||
title={link.title}
|
||||
title={title}
|
||||
isDropdown={false}
|
||||
href={target?.href}
|
||||
isSiteAuthLoginHref={target ? isSiteAuthLoginHref(context.linker, target.href) : false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -79,6 +89,7 @@ export type HeaderLinkNavItemProps = {
|
||||
title: string;
|
||||
href?: string;
|
||||
isDropdown: boolean;
|
||||
isSiteAuthLoginHref: boolean;
|
||||
} & DropdownButtonProps<HTMLElement>;
|
||||
|
||||
function HeaderLinkNavItem(props: HeaderLinkNavItemProps) {
|
||||
@@ -99,7 +110,16 @@ function HeaderItemButton(
|
||||
linkStyle: 'button-secondary' | 'button-primary';
|
||||
}
|
||||
) {
|
||||
const { linkTarget, linkStyle, headerPreset, title, href, isDropdown, ...rest } = props;
|
||||
const {
|
||||
linkTarget,
|
||||
linkStyle,
|
||||
headerPreset,
|
||||
title,
|
||||
href,
|
||||
isDropdown,
|
||||
isSiteAuthLoginHref,
|
||||
...rest
|
||||
} = props;
|
||||
const variant = (() => {
|
||||
switch (linkStyle) {
|
||||
case 'button-secondary':
|
||||
@@ -110,21 +130,25 @@ function HeaderItemButton(
|
||||
assertNever(linkStyle);
|
||||
}
|
||||
})();
|
||||
return (
|
||||
<Button
|
||||
href={href}
|
||||
variant={variant}
|
||||
size="medium"
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}}
|
||||
label={title}
|
||||
{...rest}
|
||||
/>
|
||||
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} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,20 +178,28 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese
|
||||
}
|
||||
|
||||
function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
|
||||
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}
|
||||
>
|
||||
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}>
|
||||
{title}
|
||||
{isDropdown ? <ToggleChevron /> : null}
|
||||
</Link>
|
||||
@@ -204,18 +236,21 @@ async function SubHeaderLink(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
href={target.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: link.to,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{link.title}
|
||||
</DropdownMenuItem>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import {
|
||||
type CustomizationContentLink,
|
||||
@@ -9,9 +10,11 @@ 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,
|
||||
@@ -83,30 +86,32 @@ 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={link.title}>
|
||||
<DropdownSubMenu label={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
|
||||
href={target?.href}
|
||||
insights={
|
||||
link.to
|
||||
? {
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: link.to,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{link.title}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem {...sharedProps}>{title}</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function SpacesDropdown(props: {
|
||||
icon?: ButtonProps['icon'];
|
||||
}) {
|
||||
const { context, siteSpace, siteSpaces, className, variant = 'secondary', icon } = props;
|
||||
const currentLanguage = context.siteSpace.space.language;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
@@ -70,7 +70,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.siteSpace.space.language);
|
||||
const title = getLocalizedTitle(siteSpace, context.locale);
|
||||
const hasEmojiPrefix = useMemo(() => startsWithEmoji(title), [title]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
import { CustomizationDefaultThemeMode } from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Layout to be used for rendering the PDF.
|
||||
@@ -12,7 +12,10 @@ export async function PDFRootLayout(props: {
|
||||
const { context, children } = props;
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout context={context} forcedTheme={CustomizationThemeMode.Light}>
|
||||
<CustomizationRootLayout
|
||||
context={context}
|
||||
forcedTheme={CustomizationDefaultThemeMode.Light}
|
||||
>
|
||||
{children}
|
||||
</CustomizationRootLayout>
|
||||
);
|
||||
|
||||
@@ -271,6 +271,54 @@ 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,6 +8,7 @@ import React, { useRef } from 'react';
|
||||
import { useAI } from '../AI';
|
||||
import { ToggleChevron } from '../primitives';
|
||||
import {
|
||||
ActionCopyMCPCommand,
|
||||
ActionCopyMCPURL,
|
||||
ActionCopyMarkdown,
|
||||
ActionOpenAssistant,
|
||||
@@ -121,6 +122,18 @@ 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.toPathInSpace('~gitbook/mcp'))
|
||||
? context.linker.toAbsoluteURL(context.linker.toPathInSite('~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?: CustomizationThemeMode | null;
|
||||
forcedTheme?: CustomizationDefaultThemeMode | 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) === CustomizationThemeMode.Dark
|
||||
(forcedTheme ?? customization.themes.default) === CustomizationDefaultThemeMode.Dark
|
||||
? 'dark'
|
||||
: '',
|
||||
htmlClassName
|
||||
|
||||
@@ -345,7 +345,7 @@ html.dark .shiki span {
|
||||
}
|
||||
|
||||
.highlight-line-number {
|
||||
@apply table-cell whitespace-nowrap text-sm text-tint pr-4 pl-2 text-right bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base rounded-l sticky -left-2;
|
||||
@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 before:content-[counter(line)] not-contrast-more:before:opacity-6;
|
||||
|
||||
.highlighted & {
|
||||
@@ -353,6 +353,11 @@ html.dark .shiki span {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.highlight-line-content {
|
||||
@apply table-cell text-sm;
|
||||
@apply table-cell text-sm px-4;
|
||||
|
||||
.highlight-line-number + & {
|
||||
@apply pl-2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,37 +5,31 @@ export type OrderedComputedResult =
|
||||
| ComputedSectionResult
|
||||
| ComputedRecordResult;
|
||||
|
||||
export interface ComputedSectionResult {
|
||||
export type BaseComputedResult = {
|
||||
id: string;
|
||||
title: string;
|
||||
href: string;
|
||||
score: number;
|
||||
};
|
||||
|
||||
export type ComputedSectionResult = BaseComputedResult & {
|
||||
type: 'section';
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
href: string;
|
||||
|
||||
pageId: string;
|
||||
spaceId: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ComputedPageResult {
|
||||
export type ComputedPageResult = BaseComputedResult & {
|
||||
type: 'page';
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
href: string;
|
||||
|
||||
pageId: string;
|
||||
spaceId: string;
|
||||
|
||||
breadcrumbs?: Array<{ icon?: IconName; label: string }>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ComputedRecordResult {
|
||||
export type ComputedRecordResult = BaseComputedResult & {
|
||||
type: 'record';
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | undefined;
|
||||
href: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type SearchSiteContentScope =
|
||||
| { mode: 'all' }
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
'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 { CustomizationThemeMode } from '@gitbook/api';
|
||||
import { CustomizationDefaultThemeMode } 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?: CustomizationThemeMode | null;
|
||||
forcedTheme?: CustomizationDefaultThemeMode | null;
|
||||
withTracking: boolean;
|
||||
visitorAuthClaims: VisitorAuthClaims;
|
||||
children: React.ReactNode;
|
||||
@@ -52,6 +52,7 @@ 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}
|
||||
>
|
||||
@@ -91,10 +92,14 @@ export async function generateSiteLayoutViewport(context: GitBookSiteContext): P
|
||||
const { customization } = context;
|
||||
return {
|
||||
colorScheme: customization.themes.toggeable
|
||||
? customization.themes.default === CustomizationThemeMode.Dark
|
||||
? customization.themes.default === CustomizationDefaultThemeMode.Dark
|
||||
? 'dark light'
|
||||
: 'light dark'
|
||||
: customization.themes.default,
|
||||
: customization.themes.default === CustomizationDefaultThemeMode.Dark
|
||||
? 'dark'
|
||||
: customization.themes.default === CustomizationDefaultThemeMode.Light
|
||||
? 'light'
|
||||
: 'light dark', // 'system' → let browser decide based on OS preference
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
@@ -184,7 +189,9 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
|
||||
capable: true,
|
||||
title: site.title,
|
||||
statusBarStyle:
|
||||
customization.themes.default === CustomizationThemeMode.Dark ? 'black' : 'default',
|
||||
customization.themes.default === CustomizationDefaultThemeMode.Dark
|
||||
? 'black'
|
||||
: 'default',
|
||||
},
|
||||
robots: (await isSiteIndexable(context)) ? 'index, follow' : 'noindex, nofollow',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import type { CustomizationThemeMode, SiteExternalLinksTarget } from '@gitbook/api';
|
||||
import type { CustomizationDefaultThemeMode, SiteExternalLinksTarget } from '@gitbook/api';
|
||||
import { MotionConfig, useReducedMotion } from 'motion/react';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { NuqsAdapter } from 'nuqs/adapters/next/app';
|
||||
@@ -15,13 +15,15 @@ import { isExternalLink } from '../utils/link';
|
||||
* Client component context providers for the site layout.
|
||||
*/
|
||||
export function SiteLayoutClientContexts(props: {
|
||||
forcedTheme: CustomizationThemeMode | undefined;
|
||||
forcedTheme: CustomizationDefaultThemeMode | undefined;
|
||||
defaultTheme: CustomizationDefaultThemeMode | undefined;
|
||||
externalLinksTarget: SiteExternalLinksTarget;
|
||||
contextId: string | undefined;
|
||||
proxyOrigin: string | undefined;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { children, forcedTheme, externalLinksTarget, contextId, proxyOrigin } = props;
|
||||
const { children, forcedTheme, defaultTheme, externalLinksTarget, contextId, proxyOrigin } =
|
||||
props;
|
||||
|
||||
useClearRouterCache(contextId);
|
||||
|
||||
@@ -48,6 +50,7 @@ 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,
|
||||
@@ -114,10 +114,14 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro
|
||||
|
||||
return {
|
||||
colorScheme: customization.themes.toggeable
|
||||
? customization.themes.default === CustomizationThemeMode.Dark
|
||||
? customization.themes.default === CustomizationDefaultThemeMode.Dark
|
||||
? 'dark light'
|
||||
: 'light dark'
|
||||
: customization.themes.default,
|
||||
: customization.themes.default === CustomizationDefaultThemeMode.Dark
|
||||
? 'dark'
|
||||
: customization.themes.default === CustomizationDefaultThemeMode.Light
|
||||
? 'light'
|
||||
: 'light dark', // 'system' → let browser decide based on OS preference
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,7 +130,7 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro
|
||||
*/
|
||||
function getSiteStructureTitle(context: GitBookSiteContext): string | null {
|
||||
const { visibleSections: sections, siteSpace, visibleSiteSpaces: siteSpaces } = context;
|
||||
const currentLanguage = siteSpace.space.language;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
const title = [];
|
||||
if (
|
||||
|
||||
@@ -6,7 +6,8 @@ import { tcls } from '@/lib/tailwind';
|
||||
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useSpaceBasePath } from '../SpaceLayout/SpaceLayoutContext';
|
||||
import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink';
|
||||
import { useSiteAdaptiveAuthLoginHref, useSpaceBasePath } from '../SpaceLayout/SpaceLayoutContext';
|
||||
import { CurrentPageProvider } from '../hooks';
|
||||
import { SuspenseLoadedHint } from '../primitives';
|
||||
|
||||
@@ -15,6 +16,7 @@ import { SuspenseLoadedHint } from '../primitives';
|
||||
*/
|
||||
export function SitePageNotFound() {
|
||||
const basePath = useSpaceBasePath();
|
||||
const adaptiveAuthLoginHref = useSiteAdaptiveAuthLoginHref();
|
||||
const language = useLanguage();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -49,9 +51,24 @@ export function SitePageNotFound() {
|
||||
>
|
||||
<div className={tcls('max-w-80')}>
|
||||
<h2 className={tcls('text-2xl', 'font-semibold', 'mb-2')}>
|
||||
{t(language, 'notfound_title')}
|
||||
{t(
|
||||
language,
|
||||
adaptiveAuthLoginHref ? 'notfound_adaptive_title' : 'notfound_title'
|
||||
)}
|
||||
</h2>
|
||||
<p className={tcls('text-base', 'mb-4')}>{t(language, 'notfound')}</p>
|
||||
<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}
|
||||
</div>
|
||||
<SuspenseLoadedHint />
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export type ClientSiteSectionGroup = Pick<SiteSectionGroup, 'id' | 'title' | 'ic
|
||||
*/
|
||||
export function encodeClientSiteSections(context: GitBookSiteContext, sections: SiteSections) {
|
||||
const { list, current } = sections;
|
||||
const currentLanguage = context.siteSpace.space.language;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
const clientSections: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
|
||||
|
||||
@@ -72,7 +72,7 @@ function encodeChildren(
|
||||
children: (SiteSection | SiteSectionGroup)[]
|
||||
): (ClientSiteSection | ClientSiteSectionGroup)[] {
|
||||
const clientChildren: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
|
||||
const currentLanguage = context.siteSpace.space.language;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
for (const child of children) {
|
||||
switch (child.object) {
|
||||
@@ -106,7 +106,7 @@ function encodeChildren(
|
||||
}
|
||||
|
||||
function encodeSection(context: GitBookSiteContext, section: SiteSection) {
|
||||
const currentLanguage = context.siteSpace.space.language;
|
||||
const currentLanguage = context.locale;
|
||||
return {
|
||||
id: section.id,
|
||||
title: getLocalizedTitle(section, currentLanguage),
|
||||
|
||||
@@ -51,6 +51,10 @@ 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'))
|
||||
@@ -63,7 +67,10 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<SpaceLayoutContextProvider basePath={context.linker.toPathInSpace('')}>
|
||||
<SpaceLayoutContextProvider
|
||||
basePath={context.linker.toPathInSpace('')}
|
||||
siteAdaptiveAuthLoginHref={siteAdaptiveAuthLoginHref}
|
||||
>
|
||||
<AdaptiveVisitorContextProvider
|
||||
contextId={context.contextId}
|
||||
visitorClaimsURL={getVisitorClaimsUrl}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
|
||||
const SpaceLayoutContext = React.createContext({
|
||||
basePath: '',
|
||||
siteAdaptiveAuthLoginHref: null as string | null,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -12,11 +13,15 @@ const SpaceLayoutContext = React.createContext({
|
||||
export function SpaceLayoutContextProvider(
|
||||
props: React.PropsWithChildren<{
|
||||
basePath: string;
|
||||
siteAdaptiveAuthLoginHref?: string | null;
|
||||
}>
|
||||
) {
|
||||
const { basePath, children } = props;
|
||||
const { basePath, siteAdaptiveAuthLoginHref = null, children } = props;
|
||||
|
||||
const value = React.useMemo(() => ({ basePath }), [basePath]);
|
||||
const value = React.useMemo(
|
||||
() => ({ basePath, siteAdaptiveAuthLoginHref }),
|
||||
[basePath, siteAdaptiveAuthLoginHref]
|
||||
);
|
||||
|
||||
return <SpaceLayoutContext.Provider value={value}>{children}</SpaceLayoutContext.Provider>;
|
||||
}
|
||||
@@ -31,3 +36,14 @@ 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(siteSpace.space.language);
|
||||
const currentLanguage = normalizeLanguage(context.locale);
|
||||
|
||||
// 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 { GitBookSpaceContext } from '@/lib/context';
|
||||
import type { GitBookAnyContext } from '@/lib/context';
|
||||
import { Button } from '../primitives';
|
||||
|
||||
/**
|
||||
* Trademark link to the GitBook.
|
||||
*/
|
||||
export function Trademark(props: {
|
||||
context: GitBookSpaceContext;
|
||||
context: GitBookAnyContext;
|
||||
placement: SiteInsightsTrademarkPlacement;
|
||||
className?: string;
|
||||
}) {
|
||||
@@ -22,6 +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
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
import memoize from 'memoizee';
|
||||
|
||||
/**
|
||||
* Format date as MM/DD/YYYY (e.g., "05/11/2025")
|
||||
*/
|
||||
export function formatNumericDate(date: Date): string {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
export const getNumericDateFormatter = memoize((locale: Intl.LocalesArgument) => {
|
||||
return new Intl.DateTimeFormat(locale ?? '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 function formatDateFull(date: Date): string {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
export const getFullDateFormatter = memoize((locale: Intl.LocalesArgument) => {
|
||||
return new Intl.DateTimeFormat(locale ?? 'en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
}).format(date);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Format date as "Nov. 5 2025"
|
||||
*/
|
||||
export function formatDateShort(date: Date): string {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
export const getShortDateFormatter = memoize((locale: Intl.LocalesArgument) => {
|
||||
return new Intl.DateTimeFormat(locale ?? 'en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
}).format(date);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Format date with weekday (e.g., "Monday, Nov 5, 2025").
|
||||
|
||||
@@ -11,13 +11,12 @@ export const DEFAULT_LOCALE = 'en' satisfies TranslationLocale;
|
||||
* Get the locale to use for a space.
|
||||
*/
|
||||
export function getSpaceLocale(context: GitBookAnyContext): TranslationLocale {
|
||||
const { space } = context;
|
||||
const customization = 'site' in context ? context.customization : null;
|
||||
|
||||
// If the language is configured in the space, use it in priority
|
||||
if (space.language) {
|
||||
if (checkIsValidLocale(space.language)) {
|
||||
return space.language;
|
||||
if (context.locale) {
|
||||
if (checkIsValidLocale(context.locale)) {
|
||||
return context.locale;
|
||||
}
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ export const de = {
|
||||
notfound_link:
|
||||
'Dieser Link verweist auf eine Seite, die entfernt wurde oder nicht mehr existiert.',
|
||||
notfound: 'Die gesuchte Seite existiert nicht.',
|
||||
notfound_adaptive_title: 'Seite nicht verfügbar',
|
||||
notfound_adaptive:
|
||||
'Diese Seite existiert möglicherweise, aber Sie müssen sich eventuell anmelden, um darauf zuzugreifen.',
|
||||
notfound_adaptive_login: 'Anmelden',
|
||||
unexpected_error_title: 'Ein Fehler ist aufgetreten',
|
||||
unexpected_error:
|
||||
'Entschuldigung, ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
|
||||
@@ -88,7 +92,7 @@ export const de = {
|
||||
'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.',
|
||||
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
|
||||
more: 'Mehr',
|
||||
link_tooltip_external_link: 'Externe Verlinkung zu',
|
||||
link_tooltip_external_link: 'Externer Link zu',
|
||||
link_tooltip_email: 'E-Mail senden an',
|
||||
link_tooltip_page_anchor: 'Zum Abschnitt springen',
|
||||
open_in_new_tab: 'In neuem Tab öffnen',
|
||||
@@ -103,9 +107,10 @@ export const de = {
|
||||
ai_chat_clear_conversation: 'Unterhaltung löschen',
|
||||
ai_chat_thinking: 'Denke nach...',
|
||||
ai_chat_working: 'Arbeite...',
|
||||
ai_chat_waiting: 'Warte auf Ihre Antwort…',
|
||||
ai_chat_context_badge: 'KI',
|
||||
ai_chat_error: 'Etwas ist schief gelaufen.',
|
||||
ai_chat_hold_message_1: 'Überlege es mir durch…',
|
||||
ai_chat_hold_message_1: 'Ich denke nach…',
|
||||
ai_chat_hold_message_2: 'Einen Moment…',
|
||||
ai_chat_context_title: 'Basierend auf Ihrem Kontext',
|
||||
ai_chat_context_description:
|
||||
@@ -124,9 +129,9 @@ export const de = {
|
||||
searched_for: 'Gesucht nach ${1}',
|
||||
ai_chat_tools_listed_pages: 'Docs durchsucht',
|
||||
ai_chat_tools_read_page: 'Seite ${1} gelesen',
|
||||
ai_chat_tools_mcp_tool: 'Aufgerufen ${1}',
|
||||
ai_chat_ask: 'Frage ${1}',
|
||||
ai_chat_ask_about_page: 'Frage ${1} zu dieser Seite',
|
||||
ai_chat_tools_mcp_tool: '${1} aufgerufen',
|
||||
ai_chat_ask: '${1} fragen',
|
||||
ai_chat_ask_about_page: '${1} zu dieser Seite befragen',
|
||||
copy_for_llms: 'Für LLMs kopieren',
|
||||
copy_page_markdown: 'Seite als Markdown für LLMs kopieren',
|
||||
copy_page: 'Seite kopieren',
|
||||
@@ -135,11 +140,13 @@ export const de = {
|
||||
open_in: 'Öffnen in ${1}',
|
||||
open_in_llms_pre_prompt: 'Lies ${1} und beantworte Fragen zum Inhalt.',
|
||||
connect_mcp_to: 'Mit ${1} verbinden',
|
||||
install_mcp_on: 'MCP-Server auf ${1} installieren',
|
||||
install_mcp_on: 'Dieses MCP in ${1} verwenden',
|
||||
connect_with_mcp: 'Mit MCP verbinden',
|
||||
copy_mcp_url: 'MCP-Server-URL kopieren',
|
||||
copy_mcp_url: 'Dieses MCP zu jedem kompatiblen Client hinzufügen',
|
||||
copy_mcp_install_command: 'Dieses MCP in ${1} verwenden',
|
||||
press_to_confirm: 'Drücke ${1} zum Bestätigen',
|
||||
tool_call_skipped: 'Übersprungen "${1}"',
|
||||
ai_control_submitted_answer: 'Antwort übermittelt',
|
||||
scroll_back: 'Zurück scrollen',
|
||||
scroll_further: 'Weiter scrollen',
|
||||
hint_info: 'Information',
|
||||
@@ -148,4 +155,7 @@ export const de = {
|
||||
hint_success: 'Erfolg',
|
||||
download: 'Herunterladen',
|
||||
open: 'Öffnen',
|
||||
form_other_prompt: 'Geben Sie Ihre Antwort ein...',
|
||||
form_other_field: 'Sonstiges...',
|
||||
cancel: 'Abbrechen',
|
||||
};
|
||||
|
||||
@@ -69,6 +69,9 @@ export const en = {
|
||||
notfound_title: 'Page not found',
|
||||
notfound_link: 'This link points to a page that has been removed or no longer exists.',
|
||||
notfound: "The page you are looking for doesn't exist.",
|
||||
notfound_adaptive_title: 'Page unavailable',
|
||||
notfound_adaptive: 'This page may exist, but you may need to log in to access it.',
|
||||
notfound_adaptive_login: 'Log in',
|
||||
unexpected_error_title: 'An error occurred',
|
||||
unexpected_error: 'Sorry, an unexpected error has occurred. Please try again later.',
|
||||
unexpected_error_retry: 'Try again',
|
||||
@@ -98,8 +101,9 @@ export const en = {
|
||||
ai_chat_assistant_greeting_evening: 'Good evening',
|
||||
ai_chat_assistant_greeting_night: 'Good night',
|
||||
ai_chat_clear_conversation: 'Clear conversation',
|
||||
ai_chat_thinking: 'Thinking...',
|
||||
ai_chat_working: 'Working...',
|
||||
ai_chat_thinking: 'Thinking…',
|
||||
ai_chat_working: 'Working…',
|
||||
ai_chat_waiting: 'Waiting for your answer…',
|
||||
ai_chat_context_badge: 'AI',
|
||||
ai_chat_error: 'Something went wrong.',
|
||||
ai_chat_hold_message_1: 'Thinking it through…',
|
||||
@@ -132,11 +136,13 @@ export const en = {
|
||||
open_in: 'Open in ${1}',
|
||||
open_in_llms_pre_prompt: 'Read ${1} and answer questions about the content.',
|
||||
connect_mcp_to: 'Connect to ${1}',
|
||||
install_mcp_on: 'Install MCP Server on ${1}',
|
||||
install_mcp_on: 'Use this MCP in ${1}',
|
||||
connect_with_mcp: 'Connect with MCP',
|
||||
copy_mcp_url: 'Copy the MCP Server URL',
|
||||
copy_mcp_url: 'Add this MCP to any compatible client',
|
||||
copy_mcp_install_command: 'Use this MCP in ${1}',
|
||||
press_to_confirm: 'Press ${1} to confirm',
|
||||
tool_call_skipped: 'Skipped "${1}"',
|
||||
ai_control_submitted_answer: 'Submitted answer',
|
||||
scroll_back: 'Scroll back',
|
||||
scroll_further: 'Scroll further',
|
||||
hint_info: 'Information',
|
||||
@@ -145,4 +151,7 @@ export const en = {
|
||||
hint_success: 'Success',
|
||||
download: 'Download',
|
||||
open: 'Open',
|
||||
form_other_prompt: 'Enter your answer...',
|
||||
form_other_field: 'Other...',
|
||||
cancel: 'Cancel',
|
||||
};
|
||||
|
||||
@@ -44,13 +44,13 @@ export const es: TranslationLanguage = {
|
||||
previous_page: 'Anterior',
|
||||
page_last_modified: 'Última actualización ${1}',
|
||||
was_this_helpful: '¿Te fue útil?',
|
||||
was_this_helpful_positive: 'Sí, lo fue!',
|
||||
was_this_helpful_positive: '¡Sí, lo fue!',
|
||||
was_this_helpful_neutral: 'No estoy seguro',
|
||||
was_this_helpful_negative: 'No',
|
||||
was_this_helpful_thank_you: '¡Gracias!',
|
||||
was_this_helpful_comment: '¿Algo más que te gustaría añadir?',
|
||||
was_this_helpful_positive_label: 'Útil',
|
||||
was_this_helpful_negative_label: 'No útil',
|
||||
was_this_helpful_negative_label: 'No fue útil',
|
||||
submit: 'Enviar',
|
||||
annotation_button_label: 'Abrir anotación',
|
||||
code_copied: '¡Copiado!',
|
||||
@@ -71,6 +71,10 @@ export const es: TranslationLanguage = {
|
||||
notfound_title: 'Página no encontrada',
|
||||
notfound_link: 'Este enlace apunta a una página que ha sido eliminada o ya no existe.',
|
||||
notfound: 'La página que buscas no existe.',
|
||||
notfound_adaptive_title: 'Página no disponible',
|
||||
notfound_adaptive:
|
||||
'Esta página puede existir, pero es posible que debas iniciar sesión para acceder.',
|
||||
notfound_adaptive_login: 'Iniciar sesión',
|
||||
unexpected_error_title: 'Ocurrió un error',
|
||||
unexpected_error:
|
||||
'Lo sentimos, ha ocurrido un error inesperado. Por favor, intenta de nuevo más tarde.',
|
||||
@@ -99,11 +103,12 @@ export const es: TranslationLanguage = {
|
||||
ai_chat_assistant_description: 'Estoy aquí para ayudarte con la documentación.',
|
||||
ai_chat_assistant_greeting_morning: 'Buenos días',
|
||||
ai_chat_assistant_greeting_afternoon: 'Buenas tardes',
|
||||
ai_chat_assistant_greeting_evening: 'Buenas noches',
|
||||
ai_chat_assistant_greeting_evening: 'Buenas tardes',
|
||||
ai_chat_assistant_greeting_night: 'Buenas noches',
|
||||
ai_chat_clear_conversation: 'Limpiar conversación',
|
||||
ai_chat_thinking: 'Pensando...',
|
||||
ai_chat_working: 'Trabajando...',
|
||||
ai_chat_waiting: 'Esperando tu respuesta…',
|
||||
ai_chat_context_badge: 'IA',
|
||||
ai_chat_error: 'Algo salió mal.',
|
||||
ai_chat_hold_message_1: 'Pensándolo bien…',
|
||||
@@ -122,11 +127,11 @@ export const es: TranslationLanguage = {
|
||||
ai_chat_suggested_questions_about_this_page: '¿De qué trata esta página?',
|
||||
ai_chat_suggested_questions_read_next: '¿Qué debería leer después?',
|
||||
ai_chat_suggested_questions_example: '¿Puedes dar un ejemplo?',
|
||||
searched_for: 'Buscado ${1}',
|
||||
searched_for: 'Se buscó: ${1}',
|
||||
ai_chat_tools_listed_pages: 'Exploró los docs',
|
||||
ai_chat_tools_read_page: 'Leyó la página ${1}',
|
||||
ai_chat_tools_mcp_tool: 'Llamó a ${1}',
|
||||
ai_chat_ask: 'Preguntar ${1}',
|
||||
ai_chat_ask: 'Preguntar a ${1}',
|
||||
ai_chat_ask_about_page: 'Preguntar a ${1} sobre esta página',
|
||||
copy_for_llms: 'Copiar para LLMs',
|
||||
copy_page_markdown: 'Copiar página como Markdown para LLMs',
|
||||
@@ -136,11 +141,13 @@ export const es: TranslationLanguage = {
|
||||
open_in: 'Abrir en ${1}',
|
||||
open_in_llms_pre_prompt: 'Lee ${1} y responde preguntas sobre el contenido.',
|
||||
connect_mcp_to: 'Conectar a ${1}',
|
||||
install_mcp_on: 'Instalar servidor MCP en ${1}',
|
||||
install_mcp_on: 'Usar este MCP en ${1}',
|
||||
connect_with_mcp: 'Conectar con MCP',
|
||||
copy_mcp_url: 'Copiar URL del servidor MCP',
|
||||
copy_mcp_url: 'Agregar este MCP a cualquier cliente compatible',
|
||||
copy_mcp_install_command: 'Usar este MCP en ${1}',
|
||||
press_to_confirm: 'Presiona ${1} para confirmar',
|
||||
tool_call_skipped: 'Omitido "${1}"',
|
||||
ai_control_submitted_answer: 'Respuesta enviada',
|
||||
scroll_back: 'Desplazar hacia atrás',
|
||||
scroll_further: 'Desplazar más',
|
||||
hint_info: 'Información',
|
||||
@@ -149,4 +156,7 @@ export const es: TranslationLanguage = {
|
||||
hint_success: 'Éxito',
|
||||
download: 'Descargar',
|
||||
open: 'Abrir',
|
||||
form_other_prompt: 'Escribe tu respuesta...',
|
||||
form_other_field: 'Otro...',
|
||||
cancel: 'Cancelar',
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export const fr = {
|
||||
search_or_ask: 'Rechercher',
|
||||
search_input_placeholder: 'Rechercher dans le contenu',
|
||||
search_ask_input_placeholder: 'Rechercher ou poser une question',
|
||||
search_no_results_for: 'Aucun résultat pour « ${1} ».',
|
||||
search_no_results_for: 'Aucun résultat pour « ${1} ».',
|
||||
search_no_results: 'Aucun résultat',
|
||||
search_results_count: '${1} résultats',
|
||||
search_page_result_title: 'Page avec titre ${1}',
|
||||
@@ -40,12 +40,12 @@ export const fr = {
|
||||
next_page: 'Suivant',
|
||||
previous_page: 'Précédent',
|
||||
page_last_modified: 'Mis à jour ${1}',
|
||||
was_this_helpful: 'Ce contenu vous a-t-il été utile ?',
|
||||
was_this_helpful_positive: 'Oui, tout à fait !',
|
||||
was_this_helpful: 'Ce contenu vous a-t-il été utile ?',
|
||||
was_this_helpful_positive: 'Oui, tout à fait !',
|
||||
was_this_helpful_neutral: 'Pas sûr',
|
||||
was_this_helpful_negative: 'Non',
|
||||
was_this_helpful_thank_you: 'Merci pour votre retour !',
|
||||
was_this_helpful_comment: 'Un commentaire à ajouter ?',
|
||||
was_this_helpful_thank_you: 'Merci pour votre retour !',
|
||||
was_this_helpful_comment: 'Un commentaire à ajouter ?',
|
||||
was_this_helpful_positive_label: 'Utile',
|
||||
was_this_helpful_negative_label: 'Pas utile',
|
||||
submit: 'Envoyer',
|
||||
@@ -68,6 +68,10 @@ export const fr = {
|
||||
notfound_title: 'Page introuvable',
|
||||
notfound_link: "Ce lien pointe vers une page qui a été supprimée ou n'existe plus.",
|
||||
notfound: 'La page que vous cherchez n’existe pas.',
|
||||
notfound_adaptive_title: 'Page inaccessible',
|
||||
notfound_adaptive:
|
||||
'Cette page existe peut-être, mais vous devez peut-être vous connecter pour y accéder.',
|
||||
notfound_adaptive_login: 'Se connecter',
|
||||
unexpected_error_title: 'Erreur inattendue',
|
||||
unexpected_error: 'Désolé, une erreur est survenue. Veuillez réessayer plus tard.',
|
||||
unexpected_error_retry: 'Réessayer',
|
||||
@@ -99,11 +103,12 @@ export const fr = {
|
||||
ai_chat_clear_conversation: 'Réinitialiser la conversation',
|
||||
ai_chat_thinking: 'Réflexion en cours…',
|
||||
ai_chat_working: 'Traitement en cours…',
|
||||
ai_chat_waiting: 'En attente de votre réponse…',
|
||||
ai_chat_context_badge: 'IA',
|
||||
ai_chat_error: 'Une erreur est survenue.',
|
||||
ai_chat_hold_message_1: 'Je réfléchis…',
|
||||
ai_chat_hold_message_2: 'Un instant…',
|
||||
ai_chat_context_title: 'En se basant sur votre contexte',
|
||||
ai_chat_context_title: 'Basé sur votre contexte',
|
||||
ai_chat_context_description:
|
||||
'L’assistant utilise l’IA et votre contexte pour répondre et vous aider.',
|
||||
ai_chat_context_pages_youve_read: 'Pages que vous avez consultées',
|
||||
@@ -131,11 +136,13 @@ export const fr = {
|
||||
open_in: 'Ouvrir dans ${1}',
|
||||
open_in_llms_pre_prompt: 'Lire ${1} et répondre aux questions sur le contenu.',
|
||||
connect_mcp_to: 'Se connecter à ${1}',
|
||||
install_mcp_on: 'Installer le serveur MCP sur ${1}',
|
||||
install_mcp_on: 'Utiliser ce MCP dans ${1}',
|
||||
connect_with_mcp: 'Se connecter avec MCP',
|
||||
copy_mcp_url: "Copier l'URL du serveur MCP",
|
||||
copy_mcp_url: 'Ajouter ce MCP à un client compatible',
|
||||
copy_mcp_install_command: 'Utiliser ce MCP dans ${1}',
|
||||
press_to_confirm: 'Appuyez sur ${1} pour confirmer',
|
||||
tool_call_skipped: 'Ignoré "${1}"',
|
||||
ai_control_submitted_answer: 'Réponse envoyée',
|
||||
scroll_back: "Défiler vers l'arrière",
|
||||
scroll_further: 'Défiler plus loin',
|
||||
hint_info: 'Information',
|
||||
@@ -144,4 +151,7 @@ export const fr = {
|
||||
hint_success: 'Succès',
|
||||
download: 'Télécharger',
|
||||
open: 'Ouvrir',
|
||||
form_other_prompt: 'Saisissez votre réponse...',
|
||||
form_other_field: 'Autre...',
|
||||
cancel: 'Annuler',
|
||||
};
|
||||
|
||||
@@ -71,6 +71,10 @@ export const it: TranslationLanguage = {
|
||||
notfound_title: 'Pagina non trovata',
|
||||
notfound_link: 'Questo link punta a una pagina che è stata rimossa o non esiste più.',
|
||||
notfound: 'La pagina che cerchi non esiste.',
|
||||
notfound_adaptive_title: 'Pagina non disponibile',
|
||||
notfound_adaptive:
|
||||
"Questa pagina potrebbe esistere, ma potrebbe essere necessario effettuare l'accesso per visualizzarla.",
|
||||
notfound_adaptive_login: 'Accedi',
|
||||
unexpected_error_title: 'Si è verificato un errore',
|
||||
unexpected_error: 'Spiacenti, si è verificato un errore imprevisto. Riprova più tardi.',
|
||||
unexpected_error_retry: 'Riprova',
|
||||
@@ -103,6 +107,7 @@ export const it: TranslationLanguage = {
|
||||
ai_chat_clear_conversation: 'Cancella conversazione',
|
||||
ai_chat_thinking: 'Sto pensando...',
|
||||
ai_chat_working: 'Sto lavorando...',
|
||||
ai_chat_waiting: 'In attesa della tua risposta…',
|
||||
ai_chat_context_badge: 'IA',
|
||||
ai_chat_error: 'Qualcosa è andato storto.',
|
||||
ai_chat_hold_message_1: 'Ci sto pensando…',
|
||||
@@ -121,12 +126,12 @@ export const it: TranslationLanguage = {
|
||||
ai_chat_suggested_questions_about_this_page: 'Di cosa parla questa pagina?',
|
||||
ai_chat_suggested_questions_read_next: 'Cosa dovrei leggere dopo?',
|
||||
ai_chat_suggested_questions_example: 'Puoi fare un esempio?',
|
||||
searched_for: 'Ricerca per ${1}',
|
||||
searched_for: 'Ricerca: ${1}',
|
||||
ai_chat_tools_listed_pages: 'Ha esplorato la documentazione',
|
||||
ai_chat_tools_read_page: 'Ha letto la pagina ${1}',
|
||||
ai_chat_tools_mcp_tool: 'Ha chiamato ${1}',
|
||||
ai_chat_ask: 'Chiedi a ${1}',
|
||||
ai_chat_ask_about_page: 'Chiedi a ${1} di questa pagina',
|
||||
ai_chat_ask_about_page: 'Chiedi a ${1} riguardo a questa pagina',
|
||||
copy_for_llms: 'Copia per LLM',
|
||||
copy_page_markdown: 'Copia la pagina come Markdown per LLM',
|
||||
copy_page: 'Copia pagina',
|
||||
@@ -135,17 +140,22 @@ export const it: TranslationLanguage = {
|
||||
open_in: 'Apri in ${1}',
|
||||
open_in_llms_pre_prompt: 'Leggi ${1} e rispondi alle domande sul contenuto.',
|
||||
connect_mcp_to: 'Connetti a ${1}',
|
||||
install_mcp_on: 'Installa il server MCP su ${1}',
|
||||
install_mcp_on: 'Usa questo MCP in ${1}',
|
||||
connect_with_mcp: 'Connettiti con MCP',
|
||||
copy_mcp_url: "Copia l'URL del server MCP",
|
||||
copy_mcp_url: 'Aggiungi questo MCP a qualsiasi client compatibile',
|
||||
copy_mcp_install_command: 'Usa questo MCP in ${1}',
|
||||
press_to_confirm: 'Premi ${1} per confermare',
|
||||
tool_call_skipped: 'Saltato "${1}"',
|
||||
ai_control_submitted_answer: 'Risposta inviata',
|
||||
scroll_back: 'Scorri indietro',
|
||||
scroll_further: 'Scorri ancora',
|
||||
hint_info: 'Informazioni',
|
||||
scroll_further: 'Scorri avanti',
|
||||
hint_info: 'Informazione',
|
||||
hint_warning: 'Avviso',
|
||||
hint_danger: 'Pericolo',
|
||||
hint_success: 'Successo',
|
||||
download: 'Scarica',
|
||||
open: 'Apri',
|
||||
form_other_prompt: 'Inserisci la tua risposta...',
|
||||
form_other_field: 'Altro...',
|
||||
cancel: 'Annulla',
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export const ja: TranslationLanguage = {
|
||||
annotation_button_label: '注釈を開く',
|
||||
code_copied: 'コピーしました!',
|
||||
code_copy: 'コピー',
|
||||
code_block_collapsed: 'すべての${1}行を表示',
|
||||
code_block_collapsed: 'すべての ${1} 行を表示',
|
||||
code_block_expanded: '折りたたむ',
|
||||
table_of_contents_button_label: '目次を開く',
|
||||
cookies_title: 'クッキー',
|
||||
@@ -71,6 +71,10 @@ export const ja: TranslationLanguage = {
|
||||
notfound_title: 'ページが見つかりません',
|
||||
notfound_link: 'このリンクは、削除されたか、もはや存在しないページを指しています。',
|
||||
notfound: 'お探しのページは存在しません。',
|
||||
notfound_adaptive_title: 'ページにアクセスできません',
|
||||
notfound_adaptive:
|
||||
'このページは存在する可能性がありますが、アクセスするにはログインが必要な場合があります。',
|
||||
notfound_adaptive_login: 'ログイン',
|
||||
unexpected_error_title: 'エラーが発生しました',
|
||||
unexpected_error: '申し訳ありません、予期せぬエラーが発生しました。もう一度お試しください。',
|
||||
unexpected_error_retry: '再試行',
|
||||
@@ -86,10 +90,10 @@ export const ja: TranslationLanguage = {
|
||||
pdf_mode_all: '全てのページ',
|
||||
pdf_limit_reached: '${1}ページのPDFを生成できませんでした、${2}で生成が停止しました。',
|
||||
pdf_limit_reached_continue: 'さらに${1}ページで拡張',
|
||||
more: '詳細',
|
||||
link_tooltip_external_link: '外部リンク先',
|
||||
more: 'もっと見る',
|
||||
link_tooltip_external_link: '外部リンクへ',
|
||||
link_tooltip_email: 'メールを送信',
|
||||
link_tooltip_page_anchor: 'ページ内リンク先',
|
||||
link_tooltip_page_anchor: 'セクションへジャンプ',
|
||||
open_in_new_tab: '新しいタブで開く',
|
||||
ai_answer: 'AI回答',
|
||||
ai_chat_assistant_name: 'GitBookアシスタント',
|
||||
@@ -98,10 +102,11 @@ export const ja: TranslationLanguage = {
|
||||
ai_chat_assistant_greeting_morning: 'おはようございます',
|
||||
ai_chat_assistant_greeting_afternoon: 'こんにちは',
|
||||
ai_chat_assistant_greeting_evening: 'こんばんは',
|
||||
ai_chat_assistant_greeting_night: 'おやすみ',
|
||||
ai_chat_assistant_greeting_night: 'おやすみなさい',
|
||||
ai_chat_clear_conversation: '会話をクリア',
|
||||
ai_chat_thinking: '考え中...',
|
||||
ai_chat_working: '作業中...',
|
||||
ai_chat_waiting: '回答をお待ちしています…',
|
||||
ai_chat_context_badge: 'AI',
|
||||
ai_chat_error: '何らかのエラーが発生しました。',
|
||||
ai_chat_hold_message_1: '考え中…',
|
||||
@@ -134,17 +139,22 @@ export const ja: TranslationLanguage = {
|
||||
open_in: '${1} で開く',
|
||||
open_in_llms_pre_prompt: '${1} を読んで内容に関する質問に答えてください。',
|
||||
connect_mcp_to: '${1} に接続',
|
||||
install_mcp_on: '${1} にMCPサーバーをインストール',
|
||||
install_mcp_on: '${1} でこのMCPを使用',
|
||||
connect_with_mcp: 'MCPに接続',
|
||||
copy_mcp_url: 'MCPサーバーのURLをコピー',
|
||||
copy_mcp_url: '互換性のあるクライアントにこのMCPを追加',
|
||||
copy_mcp_install_command: '${1} でこのMCPを使用',
|
||||
press_to_confirm: '確認するには${1}を押してください',
|
||||
tool_call_skipped: '"${1}" をスキップしました',
|
||||
scroll_back: '戻る',
|
||||
scroll_further: '進む',
|
||||
ai_control_submitted_answer: '回答を送信しました',
|
||||
scroll_back: '前にスクロール',
|
||||
scroll_further: '先にスクロール',
|
||||
hint_info: '情報',
|
||||
hint_warning: '警告',
|
||||
hint_danger: '危険',
|
||||
hint_success: '成功',
|
||||
download: 'ダウンロード',
|
||||
open: '開く',
|
||||
form_other_prompt: '回答を入力...',
|
||||
form_other_field: 'その他...',
|
||||
cancel: 'キャンセル',
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ export const ko: TranslationLanguage = {
|
||||
locale: 'ko',
|
||||
language: '한국어',
|
||||
flag: '🇰🇷',
|
||||
powered_by_gitbook: 'Powered by GitBook',
|
||||
powered_by_gitbook: 'GitBook 제공',
|
||||
sponsored_via_gitbook: 'GitBook 후원',
|
||||
switch_to_dark_theme: '다크 테마로 전환',
|
||||
switch_to_light_theme: '라이트 테마로 전환',
|
||||
@@ -71,6 +71,9 @@ export const ko: TranslationLanguage = {
|
||||
notfound_title: '페이지를 찾을 수 없음',
|
||||
notfound_link: '이 링크는 삭제되었거나 더 이상 존재하지 않는 페이지를 가리킵니다.',
|
||||
notfound: '찾으시는 페이지가 존재하지 않습니다.',
|
||||
notfound_adaptive_title: '페이지에 접근할 수 없음',
|
||||
notfound_adaptive: '이 페이지는 존재할 수 있지만, 접근하려면 로그인해야 할 수 있습니다.',
|
||||
notfound_adaptive_login: '로그인',
|
||||
unexpected_error_title: '오류가 발생했습니다',
|
||||
unexpected_error: '죄송합니다. 예상치 못한 오류가 발생했습니다. 나중에 다시 시도해 주세요.',
|
||||
unexpected_error_retry: '다시 시도',
|
||||
@@ -102,6 +105,7 @@ export const ko: TranslationLanguage = {
|
||||
ai_chat_clear_conversation: '대화 지우기',
|
||||
ai_chat_thinking: '생각 중...',
|
||||
ai_chat_working: '작업 중...',
|
||||
ai_chat_waiting: '답변을 기다리는 중…',
|
||||
ai_chat_context_badge: 'AI',
|
||||
ai_chat_error: '문제가 발생했습니다.',
|
||||
ai_chat_hold_message_1: '생각하고 있습니다…',
|
||||
@@ -134,17 +138,22 @@ export const ko: TranslationLanguage = {
|
||||
open_in: '${1}에서 열기',
|
||||
open_in_llms_pre_prompt: '${1}을(를) 읽고 콘텐츠에 대한 질문에 답하세요.',
|
||||
connect_mcp_to: '${1}에 연결',
|
||||
install_mcp_on: '${1}에 MCP 서버 설치',
|
||||
install_mcp_on: '${1}에서 이 MCP 사용',
|
||||
connect_with_mcp: 'MCP로 연결',
|
||||
copy_mcp_url: 'MCP 서버 URL 복사',
|
||||
copy_mcp_url: '호환 클라이언트에 이 MCP 추가',
|
||||
copy_mcp_install_command: '${1}에서 이 MCP 사용',
|
||||
press_to_confirm: '확인하려면 ${1} 누르기',
|
||||
tool_call_skipped: '"${1}" 건너뜀',
|
||||
ai_control_submitted_answer: '답변을 제출했습니다',
|
||||
scroll_back: '뒤로 스크롤',
|
||||
scroll_further: '더 스크롤',
|
||||
hint_info: '정보',
|
||||
hint_warning: '주의',
|
||||
hint_warning: '경고',
|
||||
hint_danger: '위험',
|
||||
hint_success: '성공',
|
||||
download: '다운로드',
|
||||
open: '열기',
|
||||
form_other_prompt: '답변을 입력하세요...',
|
||||
form_other_field: '기타...',
|
||||
cancel: '취소',
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ export const nl: TranslationLanguage = {
|
||||
locale: 'nl',
|
||||
language: 'Nederlands',
|
||||
flag: '🇳🇱',
|
||||
powered_by_gitbook: 'Powered by GitBook',
|
||||
powered_by_gitbook: 'Mogelijk gemaakt door GitBook',
|
||||
sponsored_via_gitbook: 'Gesponsord door GitBook',
|
||||
switch_to_dark_theme: 'Schakel over naar donkere modus',
|
||||
switch_to_light_theme: 'Schakel over naar lichte modus',
|
||||
@@ -52,7 +52,7 @@ export const nl: TranslationLanguage = {
|
||||
was_this_helpful_positive_label: 'Nuttig',
|
||||
was_this_helpful_negative_label: 'Niet nuttig',
|
||||
submit: 'Versturen',
|
||||
annotation_button_label: 'Open annotatie',
|
||||
annotation_button_label: 'Annotatie openen',
|
||||
code_copied: 'Gekopieerd!',
|
||||
code_copy: 'Kopiëren',
|
||||
code_block_collapsed: 'Toon alle ${1} regels',
|
||||
@@ -71,14 +71,18 @@ export const nl: TranslationLanguage = {
|
||||
notfound_title: 'Pagina niet gevonden',
|
||||
notfound_link: 'Deze link verwijst naar een pagina die is verwijderd of niet meer bestaat.',
|
||||
notfound: 'De pagina die je zoekt, bestaat niet.',
|
||||
notfound_adaptive_title: 'Pagina niet beschikbaar',
|
||||
notfound_adaptive:
|
||||
'Deze pagina bestaat mogelijk, maar je moet mogelijk inloggen om toegang te krijgen.',
|
||||
notfound_adaptive_login: 'Inloggen',
|
||||
unexpected_error_title: 'Er is een fout opgetreden',
|
||||
unexpected_error: 'Sorry, er is een onverwachte fout opgetreden. Probeer het later opnieuw.',
|
||||
unexpected_error_retry: 'Opnieuw proberen',
|
||||
rss_feed: 'RSS-feed',
|
||||
open_rss_feed: 'Abonneer je op de feed van deze pagina',
|
||||
pdf_download: 'Exporteer als PDF',
|
||||
pdf_goback: 'Ga terug naar inhoud',
|
||||
pdf_print: 'Print of opslaan als PDF',
|
||||
pdf_goback: 'Ga terug naar de inhoud',
|
||||
pdf_print: 'Afdrukken of opslaan als PDF',
|
||||
pdf_images_loading: 'Afbeeldingen laden (${1}/${2})',
|
||||
pdf_images_loaded: 'Alle afbeeldingen geladen',
|
||||
pdf_page_of: '${1} van ${2}',
|
||||
@@ -90,7 +94,7 @@ export const nl: TranslationLanguage = {
|
||||
link_tooltip_external_link: 'Externe link naar',
|
||||
link_tooltip_email: 'E-mail versturen naar',
|
||||
link_tooltip_page_anchor: 'Spring naar sectie',
|
||||
open_in_new_tab: 'Open in nieuw tabblad',
|
||||
open_in_new_tab: 'Open in een nieuw tabblad',
|
||||
ai_answer: 'AI-antwoord',
|
||||
ai_chat_assistant_name: 'GitBook Assistent',
|
||||
ai_chat_assistant_name_unbranded: 'AI Assistent',
|
||||
@@ -102,6 +106,7 @@ export const nl: TranslationLanguage = {
|
||||
ai_chat_clear_conversation: 'Gesprek wissen',
|
||||
ai_chat_thinking: 'Denkt na...',
|
||||
ai_chat_working: 'Werkt...',
|
||||
ai_chat_waiting: 'Wacht op je antwoord…',
|
||||
ai_chat_context_badge: 'AI',
|
||||
ai_chat_error: 'Er is iets misgegaan.',
|
||||
ai_chat_hold_message_1: 'Even nadenken…',
|
||||
@@ -124,8 +129,8 @@ export const nl: TranslationLanguage = {
|
||||
ai_chat_tools_listed_pages: 'Docs doorzocht',
|
||||
ai_chat_tools_read_page: 'Pagina ${1} gelezen',
|
||||
ai_chat_tools_mcp_tool: '${1} aangeroepen',
|
||||
ai_chat_ask: 'Vraag ${1}',
|
||||
ai_chat_ask_about_page: 'Vraag ${1} over deze pagina',
|
||||
ai_chat_ask: 'Vraag het aan ${1}',
|
||||
ai_chat_ask_about_page: 'Vraag ${1} naar deze pagina',
|
||||
copy_for_llms: 'Kopiëren voor LLMs',
|
||||
copy_page_markdown: 'Pagina kopiëren als Markdown voor LLMs',
|
||||
copy_page: 'Pagina kopiëren',
|
||||
@@ -134,11 +139,13 @@ export const nl: TranslationLanguage = {
|
||||
open_in: 'Open in ${1}',
|
||||
open_in_llms_pre_prompt: 'Lees ${1} en beantwoord vragen over de inhoud.',
|
||||
connect_mcp_to: 'Verbind met ${1}',
|
||||
install_mcp_on: 'Installeer MCP-server op ${1}',
|
||||
install_mcp_on: 'Gebruik deze MCP in ${1}',
|
||||
connect_with_mcp: 'Verbind met MCP',
|
||||
copy_mcp_url: 'Kopieer MCP-server URL',
|
||||
copy_mcp_url: 'Voeg deze MCP toe aan elke compatibele client',
|
||||
copy_mcp_install_command: 'Gebruik deze MCP in ${1}',
|
||||
press_to_confirm: 'Druk op ${1} om te bevestigen',
|
||||
tool_call_skipped: '"${1}" overgeslagen',
|
||||
ai_control_submitted_answer: 'Antwoord verzonden',
|
||||
scroll_back: 'Terug scrollen',
|
||||
scroll_further: 'Verder scrollen',
|
||||
hint_info: 'Informatie',
|
||||
@@ -147,4 +154,7 @@ export const nl: TranslationLanguage = {
|
||||
hint_success: 'Succes',
|
||||
download: 'Downloaden',
|
||||
open: 'Openen',
|
||||
form_other_prompt: 'Voer je antwoord in...',
|
||||
form_other_field: 'Anders...',
|
||||
cancel: 'Annuleren',
|
||||
};
|
||||
|
||||
@@ -72,6 +72,9 @@ export const no: TranslationLanguage = {
|
||||
notfound_link:
|
||||
'Denne lenken peker til en side som har blitt fjernet eller ikke lenger eksisterer.',
|
||||
notfound: 'Siden du leter etter eksisterer ikke.',
|
||||
notfound_adaptive_title: 'Siden er ikke tilgjengelig',
|
||||
notfound_adaptive: 'Denne siden kan finnes, men du må kanskje logge inn for å få tilgang.',
|
||||
notfound_adaptive_login: 'Logg inn',
|
||||
unexpected_error_title: 'En feil oppstod',
|
||||
unexpected_error: 'Beklager, en uventet feil har oppstått. Vennligst prøv igjen senere.',
|
||||
unexpected_error_retry: 'Prøv igjen',
|
||||
@@ -95,7 +98,7 @@ export const no: TranslationLanguage = {
|
||||
ai_answer: 'AI-svar',
|
||||
ai_chat_assistant_name: 'GitBook-assistent',
|
||||
ai_chat_assistant_name_unbranded: 'AI-assistent',
|
||||
ai_chat_assistant_description: 'Jeg er her for å hjelpe deg med docs.',
|
||||
ai_chat_assistant_description: 'Jeg er her for å hjelpe deg med dokumentasjonen.',
|
||||
ai_chat_assistant_greeting_morning: 'God morgen',
|
||||
ai_chat_assistant_greeting_afternoon: 'God ettermiddag',
|
||||
ai_chat_assistant_greeting_evening: 'God kveld',
|
||||
@@ -103,6 +106,7 @@ export const no: TranslationLanguage = {
|
||||
ai_chat_clear_conversation: 'Tøm samtale',
|
||||
ai_chat_thinking: 'Tenker...',
|
||||
ai_chat_working: 'Arbeider...',
|
||||
ai_chat_waiting: 'Venter på svaret ditt…',
|
||||
ai_chat_context_badge: 'AI',
|
||||
ai_chat_error: 'Noe gikk galt.',
|
||||
ai_chat_hold_message_1: 'Tenker det gjennom…',
|
||||
@@ -124,7 +128,7 @@ export const no: TranslationLanguage = {
|
||||
searched_for: 'Søkte etter ${1}',
|
||||
ai_chat_tools_listed_pages: 'Bladde gjennom docs',
|
||||
ai_chat_tools_read_page: 'Leste side ${1}',
|
||||
ai_chat_tools_mcp_tool: 'Kallte ${1}',
|
||||
ai_chat_tools_mcp_tool: 'Kalte ${1}',
|
||||
ai_chat_ask: 'Spør ${1}',
|
||||
ai_chat_ask_about_page: 'Spør ${1} om denne siden',
|
||||
copy_for_llms: 'Kopier for LLMs',
|
||||
@@ -135,11 +139,13 @@ export const no: TranslationLanguage = {
|
||||
open_in: 'Åpne i ${1}',
|
||||
open_in_llms_pre_prompt: 'Les ${1} og besvar spørsmål om innholdet.',
|
||||
connect_mcp_to: 'Koble til ${1}',
|
||||
install_mcp_on: 'Installer MCP-server på ${1}',
|
||||
install_mcp_on: 'Bruk denne MCP i ${1}',
|
||||
connect_with_mcp: 'Koble til MCP',
|
||||
copy_mcp_url: 'Kopier MCP-server URL',
|
||||
copy_mcp_url: 'Legg til denne MCP i enhver kompatibel klient',
|
||||
copy_mcp_install_command: 'Bruk denne MCP i ${1}',
|
||||
press_to_confirm: 'Trykk ${1} for å bekrefte',
|
||||
tool_call_skipped: 'Hoppet over "${1}"',
|
||||
ai_control_submitted_answer: 'Svar sendt',
|
||||
scroll_back: 'Rull tilbake',
|
||||
scroll_further: 'Rull videre',
|
||||
hint_info: 'Informasjon',
|
||||
@@ -148,4 +154,7 @@ export const no: TranslationLanguage = {
|
||||
hint_success: 'Suksess',
|
||||
download: 'Last ned',
|
||||
open: 'Åpne',
|
||||
form_other_prompt: 'Skriv inn svaret ditt...',
|
||||
form_other_field: 'Annet...',
|
||||
cancel: 'Avbryt',
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ export const pt_br = {
|
||||
switch_to_dark_theme: 'Mudar para modo escuro',
|
||||
switch_to_light_theme: 'Mudar para modo claro',
|
||||
switch_to_system_theme: 'Mudar para configuração do sistema',
|
||||
search: 'Busca',
|
||||
search: 'Buscar',
|
||||
clear: 'Limpar',
|
||||
search_back: 'Voltar aos resultados da busca',
|
||||
search_or_ask: 'Perguntar ou buscar',
|
||||
@@ -36,7 +36,7 @@ export const pt_br = {
|
||||
search_ask_sources_no_answer: 'Páginas relacionadas',
|
||||
search_ask_no_answer:
|
||||
'Não foi possível encontrar uma resposta para sua pergunta. Você pode tentar reformulá-la ou ser mais específico.',
|
||||
search_ask_error: 'Algo deu errado. Por favor tente novamente mais tarde.',
|
||||
search_ask_error: 'Algo deu errado. Por favor, tente novamente mais tarde.',
|
||||
on_this_page: 'Nesta página',
|
||||
next_page: 'Próximo',
|
||||
previous_page: 'Anterior',
|
||||
@@ -48,7 +48,7 @@ export const pt_br = {
|
||||
was_this_helpful_thank_you: 'Obrigado!',
|
||||
was_this_helpful_comment: 'Gostaria de adicionar algo?',
|
||||
was_this_helpful_positive_label: 'Útil',
|
||||
was_this_helpful_negative_label: 'Não útil',
|
||||
was_this_helpful_negative_label: 'Não foi útil',
|
||||
submit: 'Enviar',
|
||||
annotation_button_label: 'Abrir anotação',
|
||||
code_copied: 'Copiado!',
|
||||
@@ -58,7 +58,7 @@ export const pt_br = {
|
||||
table_of_contents_button_label: 'Abrir o índice',
|
||||
cookies_title: 'Cookies',
|
||||
cookies_prompt:
|
||||
'Este site usa cookies para entregar suas funcionalidades e analizar o tráfego. Ao navegar neste site, você aceita a ${1}.',
|
||||
'Este site usa cookies para entregar suas funcionalidades e analisar o tráfego. Ao navegar neste site, você aceita a ${1}.',
|
||||
cookies_prompt_privacy: 'política de privacidade',
|
||||
cookies_accept: 'Aceitar',
|
||||
cookies_reject: 'Rejeitar',
|
||||
@@ -69,10 +69,14 @@ export const pt_br = {
|
||||
notfound_title: 'Página não encontrada',
|
||||
notfound_link: 'Este link aponta para uma página que foi removida ou não existe mais.',
|
||||
notfound: 'A página que você está procurando não existe.',
|
||||
notfound_adaptive_title: 'Página indisponível',
|
||||
notfound_adaptive:
|
||||
'Esta página pode existir, mas talvez você precise fazer login para acessá-la.',
|
||||
notfound_adaptive_login: 'Fazer login',
|
||||
unexpected_error_title: 'Aconteceu um erro',
|
||||
unexpected_error:
|
||||
'Desculpe, aconteceu um erro inesperado. Por favor tente novamente mais tarde.',
|
||||
unexpected_error_retry: ' Tentar novamente',
|
||||
'Desculpe, aconteceu um erro inesperado. Por favor, tente novamente mais tarde.',
|
||||
unexpected_error_retry: 'Tentar novamente',
|
||||
rss_feed: 'Feed RSS',
|
||||
open_rss_feed: 'Assinar o feed desta página',
|
||||
pdf_download: 'Exportar como PDF',
|
||||
@@ -84,8 +88,8 @@ export const pt_br = {
|
||||
pdf_mode_only_page: 'Somente esta página',
|
||||
pdf_mode_all: 'Todas as páginas',
|
||||
pdf_limit_reached:
|
||||
'Não foi possível gerar o PDF para ${1} páginas, generation stopped at ${2}.',
|
||||
pdf_limit_reached_continue: 'Extender com mais ${1} páginas.',
|
||||
'Não foi possível gerar o PDF para ${1} páginas, a geração foi interrompida em ${2}.',
|
||||
pdf_limit_reached_continue: 'Estender com mais ${1} páginas.',
|
||||
more: 'Mais',
|
||||
link_tooltip_external_link: 'Link externo para',
|
||||
link_tooltip_email: 'Enviar e-mail para',
|
||||
@@ -97,11 +101,12 @@ export const pt_br = {
|
||||
ai_chat_assistant_description: 'Estou aqui para ajudá-lo com a documentação.',
|
||||
ai_chat_assistant_greeting_morning: 'Bom dia',
|
||||
ai_chat_assistant_greeting_afternoon: 'Boa tarde',
|
||||
ai_chat_assistant_greeting_evening: 'Boa noite',
|
||||
ai_chat_assistant_greeting_evening: 'Boa tarde',
|
||||
ai_chat_assistant_greeting_night: 'Boa noite',
|
||||
ai_chat_clear_conversation: 'Limpar conversa',
|
||||
ai_chat_thinking: 'Pensando...',
|
||||
ai_chat_working: 'Trabalhando...',
|
||||
ai_chat_waiting: 'Aguardando sua resposta…',
|
||||
ai_chat_context_badge: 'IA',
|
||||
ai_chat_error: 'Algo deu errado.',
|
||||
ai_chat_hold_message_1: 'Pensando sobre isso…',
|
||||
@@ -134,11 +139,13 @@ export const pt_br = {
|
||||
open_in: 'Abrir no ${1}',
|
||||
open_in_llms_pre_prompt: 'Leia ${1} e responda perguntas sobre o conteúdo.',
|
||||
connect_mcp_to: 'Conectar a ${1}',
|
||||
install_mcp_on: 'Instalar servidor MCP em ${1}',
|
||||
install_mcp_on: 'Usar este MCP no ${1}',
|
||||
connect_with_mcp: 'Conectar com MCP',
|
||||
copy_mcp_url: 'Copiar URL do servidor MCP',
|
||||
copy_mcp_url: 'Adicionar este MCP a qualquer cliente compatível',
|
||||
copy_mcp_install_command: 'Usar este MCP no ${1}',
|
||||
press_to_confirm: 'Pressione ${1} para confirmar',
|
||||
tool_call_skipped: 'Pulado "${1}"',
|
||||
ai_control_submitted_answer: 'Resposta enviada',
|
||||
scroll_back: 'Rolar para trás',
|
||||
scroll_further: 'Rolar para frente',
|
||||
hint_info: 'Informação',
|
||||
@@ -147,4 +154,7 @@ export const pt_br = {
|
||||
hint_success: 'Sucesso',
|
||||
download: 'Baixar',
|
||||
open: 'Abrir',
|
||||
form_other_prompt: 'Digite sua resposta...',
|
||||
form_other_field: 'Outro...',
|
||||
cancel: 'Cancelar',
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ru = {
|
||||
search_ask_input_placeholder: 'Найти в содержимом или задать вопрос',
|
||||
search_no_results_for: 'Нет результатов для "${1}".',
|
||||
search_no_results: 'Нет результатов',
|
||||
search_results_count: '${1} — число результатов',
|
||||
search_results_count: '${1} результатов',
|
||||
search_page_result_title: 'Страница с заголовком ${1}',
|
||||
search_section_result_title: 'Раздел с заголовком ${1}',
|
||||
search_section_result_content: 'Раздел с контентом ${1}',
|
||||
@@ -63,12 +63,16 @@ export const ru = {
|
||||
cookies_accept: 'Принять',
|
||||
cookies_reject: 'Отклонить',
|
||||
close: 'Закрыть',
|
||||
view: 'Посмотреть',
|
||||
view: 'Просмотреть',
|
||||
edit_on_git: 'Редактировать в ${1}',
|
||||
edit: 'Редактировать',
|
||||
notfound_title: 'Страница не найдена',
|
||||
notfound_link: 'Эта ссылка ведёт на страницу, которая была удалена или больше не существует.',
|
||||
notfound: 'Страница, которую вы ищете, не существует.',
|
||||
notfound_adaptive_title: 'Страница недоступна',
|
||||
notfound_adaptive:
|
||||
'Эта страница может существовать, но для доступа к ней может потребоваться вход.',
|
||||
notfound_adaptive_login: 'Войти',
|
||||
unexpected_error_title: 'Произошла ошибка',
|
||||
unexpected_error: 'Извините, произошла непредвиденная ошибка. Пожалуйста, попробуйте позже.',
|
||||
unexpected_error_retry: 'Попробуйте снова',
|
||||
@@ -83,7 +87,7 @@ export const ru = {
|
||||
pdf_mode_only_page: 'Только эта страница',
|
||||
pdf_mode_all: 'Все страницы',
|
||||
pdf_limit_reached:
|
||||
'Не удалось создать PDF-файл для ${1} страниц, генерация остановлена на ${2}.',
|
||||
'Не удалось создать PDF-файл для ${1} страниц, генерация остановлена на ${2}.',
|
||||
pdf_limit_reached_continue: 'Расширьте ещё на ${1} страниц.',
|
||||
more: 'Ещё',
|
||||
link_tooltip_external_link: 'Внешняя ссылка на',
|
||||
@@ -93,7 +97,7 @@ export const ru = {
|
||||
ai_answer: 'ИИ-ответ',
|
||||
ai_chat_assistant_name: 'GitBook-помощник',
|
||||
ai_chat_assistant_name_unbranded: 'ИИ-помощник',
|
||||
ai_chat_assistant_description: 'Я здесь, чтобы помочь вам с базой знаний.',
|
||||
ai_chat_assistant_description: 'Я здесь, чтобы помочь вам с документацией.',
|
||||
ai_chat_assistant_greeting_morning: 'Доброе утро',
|
||||
ai_chat_assistant_greeting_afternoon: 'Добрый день',
|
||||
ai_chat_assistant_greeting_evening: 'Добрый вечер',
|
||||
@@ -101,6 +105,7 @@ export const ru = {
|
||||
ai_chat_clear_conversation: 'Очистить диалог',
|
||||
ai_chat_thinking: 'Думает…',
|
||||
ai_chat_working: 'Работает…',
|
||||
ai_chat_waiting: 'Ожидание вашего ответа…',
|
||||
ai_chat_context_badge: 'ИИ',
|
||||
ai_chat_error: 'Что-то пошло не так.',
|
||||
ai_chat_hold_message_1: 'Обдумываю…',
|
||||
@@ -109,7 +114,7 @@ export const ru = {
|
||||
ai_chat_context_description:
|
||||
'Помощник использует ИИ и ваш контекст для генерации ответов и выполнения действий.',
|
||||
ai_chat_context_pages_youve_read: 'Страницы, которые вы прочитали',
|
||||
ai_chat_context_info_provided_by_the_site: 'Информация предоставлена сайтом',
|
||||
ai_chat_context_info_provided_by_the_site: 'Информация, предоставленная сайтом',
|
||||
ai_chat_context_previous_messages: 'Предыдущие сообщения',
|
||||
ai_chat_context_disclaimer: 'Ответы ИИ могут содержать ошибки.',
|
||||
ai_chat_input_placeholder: 'Спросите, найдите или объясните...',
|
||||
@@ -123,8 +128,8 @@ export const ru = {
|
||||
ai_chat_tools_listed_pages: 'Просмотрены документы',
|
||||
ai_chat_tools_read_page: 'Прочитана страница ${1}',
|
||||
ai_chat_tools_mcp_tool: 'Вызван ${1}',
|
||||
ai_chat_ask: 'Спросить ${1}',
|
||||
ai_chat_ask_about_page: 'Спросить ${1} об этой странице',
|
||||
ai_chat_ask: 'Спросить у ${1}',
|
||||
ai_chat_ask_about_page: 'Спросить у ${1} об этой странице',
|
||||
copy_for_llms: 'Скопировать для LLM',
|
||||
copy_page_markdown: 'Скопировать страницу как Markdown для LLM',
|
||||
copy_page: 'Скопировать страницу',
|
||||
@@ -133,11 +138,13 @@ export const ru = {
|
||||
open_in: 'Открыть в ${1}',
|
||||
open_in_llms_pre_prompt: 'Прочитай ${1} и ответь на вопросы о содержимом.',
|
||||
connect_mcp_to: 'Подключиться к ${1}',
|
||||
install_mcp_on: 'Установить MCP-сервер на ${1}',
|
||||
install_mcp_on: 'Использовать этот MCP в ${1}',
|
||||
connect_with_mcp: 'Подключиться к MCP',
|
||||
copy_mcp_url: 'Скопировать URL MCP-сервера',
|
||||
copy_mcp_url: 'Добавить этот MCP в любой совместимый клиент',
|
||||
copy_mcp_install_command: 'Использовать этот MCP в ${1}',
|
||||
press_to_confirm: 'Нажмите ${1} для подтверждения',
|
||||
tool_call_skipped: 'Пропущен "${1}"',
|
||||
ai_control_submitted_answer: 'Ответ отправлен',
|
||||
scroll_back: 'Прокрутить назад',
|
||||
scroll_further: 'Прокрутить дальше',
|
||||
hint_info: 'Информация',
|
||||
@@ -146,4 +153,7 @@ export const ru = {
|
||||
hint_success: 'Успех',
|
||||
download: 'Скачать',
|
||||
open: 'Открыть',
|
||||
form_other_prompt: 'Введите ответ...',
|
||||
form_other_field: 'Другое...',
|
||||
cancel: 'Отмена',
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user