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