mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-11-13 15:57:12 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0939cefce | |||
| c3df458378 | |||
| ddccd3d4f0 | |||
| adfd921b3a | |||
| 5997a66638 | |||
| 65210b7d1d | |||
| 265ca4bd9d | |||
| 3e86382cdc | |||
| 19660edf91 | |||
| e3d22380fc | |||
| 9b5b8c85ec |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix horizontal page scroll caused by card carousels bleeding past the viewport.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Remove the ChatGPT-specific plain text response for Markdown and llms.txt routes; these now always serve `text/markdown`.
|
||||
@@ -4,6 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# A newer run for the same PR supersedes the previous one. Each push to main gets a group of its
|
||||
# own, so main runs are never queued behind or cancelled by another run.
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request_target' && format('preview-pr-{0}', github.event.pull_request.number) || format('preview-run-{0}', github.run_id) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
|
||||
jobs:
|
||||
approval:
|
||||
# Ensure that external contributors' PRs are manually approved before deployment
|
||||
@@ -18,8 +23,9 @@ jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (preview)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
needs: approval
|
||||
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
if: ${{ !cancelled() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
environment:
|
||||
name: 2v-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
@@ -44,8 +50,9 @@ jobs:
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (preview)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
needs: approval
|
||||
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
if: ${{ !cancelled() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
environment:
|
||||
name: 2c-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
@@ -82,7 +89,7 @@ jobs:
|
||||
comment-deployments:
|
||||
runs-on: ubuntu-latest
|
||||
name: Comment Deployments (preview)
|
||||
if: always() && github.event_name != 'push'
|
||||
if: ${{ !cancelled() && github.event_name != 'push' }}
|
||||
needs:
|
||||
- deploy-v2-vercel
|
||||
- deploy-v2-cloudflare
|
||||
@@ -95,6 +102,25 @@ jobs:
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: 'Summary of the deployments'
|
||||
|
||||
# A deploy that didn't succeed has no URL (e.g. a re-run hitting Vercel's "deploymentId
|
||||
# already exists"), so keep the URL the comment already shows instead of blanking it.
|
||||
- name: Resolve deployment URLs
|
||||
id: urls
|
||||
env:
|
||||
PREVIOUS_COMMENT: ${{ steps.fc.outputs.comment-body }}
|
||||
VERCEL_RESULT: ${{ needs.deploy-v2-vercel.result }}
|
||||
VERCEL_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
|
||||
CLOUDFLARE_RESULT: ${{ needs.deploy-v2-cloudflare.result }}
|
||||
CLOUDFLARE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
|
||||
run: |
|
||||
previous_url() {
|
||||
sed -n "s/^| $1 | \[\([^]]*\)\].*/\1/p" <<<"$PREVIOUS_COMMENT" | head -n 1
|
||||
}
|
||||
if [ "$VERCEL_RESULT" != success ]; then VERCEL_URL=$(previous_url Vercel); fi
|
||||
if [ "$CLOUDFLARE_RESULT" != success ]; then CLOUDFLARE_URL=$(previous_url Cloudflare); fi
|
||||
echo "vercel=$VERCEL_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "cloudflare=$CLOUDFLARE_URL" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create or update GitHub comment
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
@@ -105,21 +131,21 @@ jobs:
|
||||
|
||||
| Version | URL | Status |
|
||||
| --- | --- | --- |
|
||||
| Vercel | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}](${{ needs.deploy-v2-vercel.outputs.deployment-url }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
|
||||
| Cloudflare | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v2-cloudflare.result == 'success' && '✅' || '❌' }} |
|
||||
| Vercel | [${{ steps.urls.outputs.vercel }}](${{ steps.urls.outputs.vercel }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
|
||||
| Cloudflare | [${{ steps.urls.outputs.cloudflare }}](${{ steps.urls.outputs.cloudflare }}) | ${{ needs.deploy-v2-cloudflare.result == 'success' && '✅' || '❌' }} |
|
||||
|
||||
### Test content
|
||||
|
||||
| Site | `2v` | `2c` |
|
||||
| --- | --- | --- |
|
||||
| GitBook | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) |
|
||||
| E2E | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
|
||||
| GitBook | [${{ steps.urls.outputs.vercel }}/url/gitbook.com/docs](${{ steps.urls.outputs.vercel }}/url/gitbook.com/docs) | [${{ steps.urls.outputs.cloudflare }}/url/gitbook.com/docs](${{ steps.urls.outputs.cloudflare }}/url/gitbook.com/docs) |
|
||||
| E2E | [${{ steps.urls.outputs.vercel }}/url/gitbook.gitbook.io/test-gitbook-open](${{ steps.urls.outputs.vercel }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ steps.urls.outputs.cloudflare }}/url/gitbook.gitbook.io/test-gitbook-open](${{ steps.urls.outputs.cloudflare }}/url/gitbook.gitbook.io/test-gitbook-open) |
|
||||
edit-mode: replace
|
||||
visual-testing-v2-vercel:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2
|
||||
needs: deploy-v2-vercel
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
if: ${{ !cancelled() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -143,7 +169,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
|
||||
if: ${{ !cancelled() && needs.deploy-v2-cloudflare.result == 'success' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -167,7 +193,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2
|
||||
needs: deploy-v2-vercel
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
if: ${{ !cancelled() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -191,7 +217,7 @@ jobs:
|
||||
# Style recalc depends on the client bundle, not the host, so this runs here only and
|
||||
# not in the Cloudflare job. Kept out of `e2e-customers`, which is a visual suite.
|
||||
- name: Run style invalidation tests
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
working-directory: packages/gitbook
|
||||
run: bun run e2e-style-perf
|
||||
env:
|
||||
@@ -200,7 +226,7 @@ jobs:
|
||||
|
||||
# Runs on failure too: a blown style budget is exactly when the numbers are worth seeing.
|
||||
- name: Build style invalidation report
|
||||
if: always() && github.event_name != 'push'
|
||||
if: ${{ !cancelled() && github.event_name != 'push' }}
|
||||
id: style-perf
|
||||
working-directory: packages/gitbook
|
||||
run: |
|
||||
@@ -219,7 +245,7 @@ jobs:
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find style invalidation comment
|
||||
if: always() && github.event_name != 'push' && steps.style-perf.outputs.body != ''
|
||||
if: ${{ !cancelled() && github.event_name != 'push' && steps.style-perf.outputs.body != '' }}
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc-style-perf
|
||||
with:
|
||||
@@ -228,7 +254,7 @@ jobs:
|
||||
body-includes: 'Style invalidation on a large API reference'
|
||||
|
||||
- name: Create or update style invalidation comment
|
||||
if: always() && github.event_name != 'push' && steps.style-perf.outputs.body != ''
|
||||
if: ${{ !cancelled() && github.event_name != 'push' && steps.style-perf.outputs.body != '' }}
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc-style-perf.outputs.comment-id }}
|
||||
@@ -239,7 +265,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
|
||||
if: ${{ !cancelled() && needs.deploy-v2-cloudflare.result == 'success' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -263,7 +289,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Browserless Testing v2 (Vercel)
|
||||
needs: deploy-v2-vercel
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
if: ${{ !cancelled() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"@changesets/cli": "^2.31.1",
|
||||
"oxfmt": "^0.62.0",
|
||||
"oxlint": "^1.93.0",
|
||||
"turbo": "^2.11.10",
|
||||
"turbo": "^2.11.11",
|
||||
"vercel": "50.37.3",
|
||||
},
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
},
|
||||
"packages/embed": {
|
||||
"name": "@gitbook/embed",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"bidc": "catalog:",
|
||||
@@ -109,7 +109,7 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.28.2",
|
||||
"version": "0.28.3",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
"@cloudflare/workers-types": "^4.20251011.0",
|
||||
@@ -128,8 +128,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "4.1.15",
|
||||
"@opennextjs/cloudflare": "1.20.16",
|
||||
"@opennextjs/aws": "4.1.16",
|
||||
"@opennextjs/cloudflare": "1.20.17",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
@@ -159,7 +159,7 @@
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^16.4.5",
|
||||
"next": "^16.4.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -227,7 +227,7 @@
|
||||
},
|
||||
"packages/icons": {
|
||||
"name": "@gitbook/icons",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"bin": {
|
||||
"gitbook-icons": "./bin/gitbook-icons.js",
|
||||
},
|
||||
@@ -265,7 +265,7 @@
|
||||
},
|
||||
"packages/react-contentkit": {
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.19",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
@@ -341,9 +341,9 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"next@16.4.5": "patches/next@16.4.5.patch",
|
||||
"@shikijs/langs@3.21.0": "patches/@shikijs%2Flangs@3.21.0.patch",
|
||||
"mermaid@11.16.1": "patches/mermaid@11.16.1.patch",
|
||||
"next@16.4.6": "patches/next@16.4.6.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
@@ -888,29 +888,29 @@
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.6", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g=="],
|
||||
|
||||
"@next/env": ["@next/env@16.4.5", "", {}, "sha512-JwSPwpdkZL5VMl2jA/q+JI8MZdweAL3xmw5/tb9y/TLtmS0YXZnmYDzSihwk8xTj1OL+WM53gTqaj96yuLp6Xw=="],
|
||||
"@next/env": ["@next/env@16.4.6", "", {}, "sha512-SunBjJ6t+oBtecS3dTjdVuMhUogP5wD/vrGL5EVuQYM1iS4O5ilL51HkiJ3sPjMzRHiCChvr4dbaSJbR7F3lxA=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W5TniBzWKASbNNwHZ/S/f1MxQUXU/LdjklVMWA0Ujbfl7SZnUzFls45hO0i/sF7Bx1jJBWp7WWmoQajXtX/O8A=="],
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kJ4VVZkley17xKaJx72QN2IBqjrj7zenCQZaUF0l9ShS/1Mt2kW4+Z5KGG6vgr3L/J562xTOizVyM1iA/u5nlg=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-uWq4qkHapBDS1oFltWS4YxTb9pLHAtLNcpbuK6SmRsebLYZqpBML/j/C0op+50JYGTpZgVL/DWNt/6oWlamrrw=="],
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-eIPGZIqC+8JKaeZmvnCANJQF4xWkkHqSnAAajtFxgwDMAOnCgxNvP52SeWZ41PyVwoe+Le/NQuZSaQ8j7YshSA=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-FdRcyOx5u5P51mlAqLB9EXdd8jZO5fsdHtk4BAQSkqOywCsbTNN/rmw+jO7pnmjNdAtu2AG46NzbCL2FPDrrgw=="],
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-9nI6O8vvczH2D9ZNdMGqb/3NuT15T9ksOZnAmx9/gjF2cOi74+xxyf1Hk96uIMfm14c9y6wHeBq6SH8g6udvcA=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-cyvmmgtRXR8aGVT86wi9Py53IlfyCx7P2fZxuAXAoTummDnhKtUyjrMeXfapqp4qgD+gT5RqyGD/VfA16jEK7Q=="],
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-7/qPhTxDp2ScluOJOhLuhWmlA17o1yZ0SIgpISBCpBxXgtnK4+KGywBpFkfLPiU1rnSM1faFGzYkH6i7JdTrqA=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hPc+Fl+N22DAvqZHq559NmamXboOX5Lu8I5Wl0DezfrhTk1Zz21NQeYoZDmNI/NjTcrdsVxM72Fzn2lI+Q+vAw=="],
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oOpbNa8XOW1qCGd9VvzC4fbkkRxEls7/WI6tz+YHC0xRA7KpV1U7DrAnCPjzVyOk/karzWEepCOcTIDmwK8FwQ=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-gbACYE8IP/jlprNTpiWXHb8N9CofOzpnEwH2SrIz1dUbiJf1FbDssDupCAPeP1Taxe8mgG20sGZamJI7KFOdZw=="],
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-aM8CZ+OR2EwXaAYw7KGkW2GDmNDsLBIRe1MMRZCJPTdw/xhTD0IMUVO4sf3KqrzuRouGzwwLZn34+gFeVXiUQA=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+8gj++ZMo8H20Xh2ijjNqyFk5LPLw0AC4Fs4Tq/G94rjjQXhOjN4jgSWsWKPFsyY26hfIeemJIwKtMxOsp5GBA=="],
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-U0p7/tEtqvHX2crhd841CDwKtp5CFkKXVjOhmfccR3Pvp64sj8KBwRqsvltqLOwVBbj0NtkxwXSDpEHYGbxeXQ=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-qNZj66pYXnaWLiF9R9GUbfBdrCv7kC6TaZfIwB2KH5WPc/8pEM3nVOj8hpQhWJU8S94rlH5MPDcX15+RrgUhiw=="],
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-3ZG15/5WIdlTQsIEvc9sPTTSL5XFrlLklIKmfF4fwsDnoAhu6NcBDXsk3mbGxNyo6aVLEGex+fHCv2MahY7a9g=="],
|
||||
|
||||
"@node-minify/core": ["@node-minify/core@9.0.2", "", { "dependencies": { "@node-minify/utils": "9.0.1", "glob": "10.3.3", "mkdirp": "3.0.1" } }, "sha512-FNhv29Wom6wKrrFKaeAfmZqz7TX5A1E6P+bpd0VIc+DYWMLUIhAViS8riaZg3A1oD0s06s+5BG2Fg7RqMKiKHw=="],
|
||||
"@node-minify/core": ["@node-minify/core@10.5.0", "", { "dependencies": { "@node-minify/utils": "10.5.0" } }, "sha512-duptyliRQY+fXszqbm5CP+NauASUwkOlx/c4bwlT7tj5AQWhG9F26D3bfU/okKto9RDTGJy3cLwPvJC/j9lCkA=="],
|
||||
|
||||
"@node-minify/terser": ["@node-minify/terser@9.0.1", "", { "dependencies": { "@node-minify/utils": "9.0.1", "terser": "5.36.0" } }, "sha512-WF78Ex+/xNZZDYvzwB7+sLUYQbJzyyS36ZjMhVqhORejHOsoOcTjQ9TdOgKZoY7wlfxjnLxeMKkO8R/R1KL9aQ=="],
|
||||
"@node-minify/terser": ["@node-minify/terser@10.5.0", "", { "dependencies": { "@node-minify/utils": "10.5.0", "terser": "5.46.0" } }, "sha512-NvR9vYMQ/exd2lNKQ9TtoJD98gqpzQu0+bIZRqA3mci38hmacPr7H5cP4coh1vz5ZZp+WkYSrOwCm2FiC2P5MQ=="],
|
||||
|
||||
"@node-minify/utils": ["@node-minify/utils@9.0.1", "", { "dependencies": { "gzip-size": "6.0.0" } }, "sha512-aC1+mhKTP3IMa2VcuGl3ui92LO/7CPQWldNGzu3BVGKiMNJ70AKJW/R6huuYCSuQyHDGM9oFwiVClsZnFxn67g=="],
|
||||
"@node-minify/utils": ["@node-minify/utils@10.5.0", "", { "dependencies": { "fast-glob": "^3.3.3", "gzip-size": "7.0.0" } }, "sha512-3DZEYgersSgia5V/cee35sw6B7TTZlcCKyAGiuhT274/r1obMEtHt1lRmyCd52uX5QlZqDOTrD4bqHCxKzIYLg=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
@@ -934,9 +934,9 @@
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@opennextjs/aws": ["@opennextjs/aws@4.1.15", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^9.0.2", "@node-minify/terser": "^9.0.1", "@tsconfig/node18": "^18.2.7", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.1.1", "esbuild": "0.28.2", "express": "^5.2.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.9.1" }, "peerDependencies": { "diff": "^9.0.0", "next": ">=15.5.29 <16 || >=16.4.5" }, "optionalPeers": ["diff"], "bin": { "open-next": "dist/index.js" } }, "sha512-ulv7rFEAeieD0d/zDMnw4IFPU+zY/atVUhn7ezgFqIurLB5oBYeHCUGlcGk3Pg7uMPeI03SNRjHW1+xL9EwMPw=="],
|
||||
"@opennextjs/aws": ["@opennextjs/aws@4.1.16", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^10.5.0", "@node-minify/terser": "^10.5.0", "@tsconfig/node18": "^18.2.7", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.1.1", "esbuild": "0.28.2", "express": "^5.2.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.9.1" }, "peerDependencies": { "diff": "^9.0.0", "next": ">=15.5.29 <16 || >=16.4.5" }, "optionalPeers": ["diff"], "bin": { "open-next": "dist/index.js" } }, "sha512-eHb6/LCrpr+3DmbmE+/q5GkcrbSs5IRpO/O0kcay7FK5WYDhcML0K5NhO7I9H62z42wscVIi75Hv3ASsXDeEGw=="],
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.16", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@dotenvx/dotenvx": "^2.33.0", "@opennextjs/aws": "4.1.15", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^13.0.6", "ts-tqdm": "^0.8.6", "yargs": "^18.2.0" }, "peerDependencies": { "next": ">=15.5.29 <16 || >=16.4.5", "rclone.js": "^0.6.6", "wrangler": "^4.148.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-3c1Cwex5e2jXy4o4RZAusy8o9Uz4eowuPCjdVLYEZ/MnkAzN2jh9dOgsRjgu3wjigXuNe8rmPECmOTo+xKQHsw=="],
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.20.17", "", { "dependencies": { "@ast-grep/napi": "^0.45.3", "@dotenvx/dotenvx": "^2.33.0", "@opennextjs/aws": "4.1.16", "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^13.0.6", "resolve.exports": "^2.0.4", "ts-tqdm": "^0.8.6", "yargs": "^18.2.0" }, "peerDependencies": { "next": ">=15.5.29 <16 || >=16.4.5", "rclone.js": "^0.6.6", "wrangler": "^4.148.0" }, "optionalPeers": ["rclone.js"], "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-jGEC9ue8LLHvOFXp62LjDUSSz7JpD+PQB+Y3r0lU78PtQ84wI16+EMF9TXEDkTek3g3fGWTLTI07pbZt8/gj8w=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -2124,7 +2124,7 @@
|
||||
|
||||
"focus-trap": ["focus-trap@7.6.1", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-nB8y4nQl8PshahLpGKZOq1sb0xrMVFSn6at7u/qOsBZTlZRzaapISGENcB6mOkoezbClZyiMwEF/dGY8AZ00rA=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -2190,7 +2190,7 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
||||
"gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="],
|
||||
|
||||
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||
|
||||
@@ -2338,7 +2338,7 @@
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="],
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
||||
|
||||
@@ -2636,7 +2636,7 @@
|
||||
|
||||
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
|
||||
|
||||
"next": ["next@16.4.5", "", { "dependencies": { "@next/env": "16.4.5", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.4.5", "@next/swc-darwin-x64": "16.4.5", "@next/swc-linux-arm64-gnu": "16.4.5", "@next/swc-linux-arm64-musl": "16.4.5", "@next/swc-linux-x64-gnu": "16.4.5", "@next/swc-linux-x64-musl": "16.4.5", "@next/swc-win32-arm64-msvc": "16.4.5", "@next/swc-win32-x64-msvc": "16.4.5", "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-rmdfsWosjTUO+6R0S3KokoIk6Kj0sWDRTe86tZHdqaWH/oXq/udX3/4xb+YQi7aWbyEFoglDU54kaifLCdXbBw=="],
|
||||
"next": ["next@16.4.6", "", { "dependencies": { "@next/env": "16.4.6", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.4.6", "@next/swc-darwin-x64": "16.4.6", "@next/swc-linux-arm64-gnu": "16.4.6", "@next/swc-linux-arm64-musl": "16.4.6", "@next/swc-linux-x64-gnu": "16.4.6", "@next/swc-linux-x64-musl": "16.4.6", "@next/swc-win32-arm64-msvc": "16.4.6", "@next/swc-win32-x64-msvc": "16.4.6", "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-QZJiYhaZLJMo2KpkQGQIwvmd79WVsAWeYIU554a8d7zArS3si/IRy3lBNV/rtYhE70oLwdLYhPAXLPx8HwcwtA=="],
|
||||
|
||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
||||
@@ -2902,7 +2902,7 @@
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||
"resolve.exports": ["resolve.exports@2.0.4", "", {}, "sha512-zepuGL0ulf+nt+XaFrlKeaiTJeyPFPLLY62+ncq1/VTHBdt3HsfXOH60priZ6tnXQozdGcDeIF5JVTanvIcYWQ=="],
|
||||
|
||||
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||
|
||||
@@ -3072,7 +3072,7 @@
|
||||
|
||||
"term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
|
||||
|
||||
"terser": ["terser@5.36.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="],
|
||||
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
@@ -3448,8 +3448,6 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||
|
||||
"@node-minify/core/glob": ["glob@10.3.3", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" } }, "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw=="],
|
||||
|
||||
"@opennextjs/aws/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"@oxc-transform/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=="],
|
||||
@@ -3616,6 +3614,8 @@
|
||||
|
||||
"@vercel/backends/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
|
||||
"@vercel/backends/resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||
|
||||
"@vercel/backends/rolldown": ["rolldown@1.0.0-rc.1", "", { "dependencies": { "@oxc-project/types": "=0.110.0", "@rolldown/pluginutils": "1.0.0-rc.1" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.1", "@rolldown/binding-darwin-arm64": "1.0.0-rc.1", "@rolldown/binding-darwin-x64": "1.0.0-rc.1", "@rolldown/binding-freebsd-x64": "1.0.0-rc.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.1", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.1", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.1", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.1", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.1", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.1", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.1", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.1", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.1" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-M3AeZjYE6UclblEf531Hch0WfVC/NOL43Cc+WdF3J50kk5/fvouHhDumSGTh0oRjbZ8C4faaVr5r6Nx1xMqDGg=="],
|
||||
|
||||
"@vercel/backends/zod": ["zod@3.22.4", "", {}, "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg=="],
|
||||
@@ -3790,8 +3790,6 @@
|
||||
|
||||
"finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"foreground-child/cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||
|
||||
"globby/fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
|
||||
@@ -4060,10 +4058,6 @@
|
||||
|
||||
"@jridgewell/source-map/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@node-minify/core/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"@opennextjs/aws/express/body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
||||
|
||||
"@opennextjs/aws/express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
|
||||
@@ -4426,10 +4420,6 @@
|
||||
|
||||
"remark-gfm/mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
|
||||
|
||||
"rimraf/glob/foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
|
||||
|
||||
"rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"rimraf/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
@@ -4482,12 +4472,6 @@
|
||||
|
||||
"@headlessui/react/@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
|
||||
|
||||
"@node-minify/core/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"@node-minify/core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"@node-minify/core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"@opennextjs/aws/express/body-parser/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"@opennextjs/aws/express/mime-types/mime-db": ["mime-db@1.53.0", "", {}, "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg=="],
|
||||
@@ -4570,8 +4554,6 @@
|
||||
|
||||
"@headlessui/react/@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
|
||||
|
||||
"@node-minify/core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@opennextjs/aws/express/body-parser/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"@opennextjs/aws/express/qs/side-channel/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
|
||||
"mermaid@11.16.1": "patches/mermaid@11.16.1.patch",
|
||||
"next@16.4.5": "patches/next@16.4.5.patch",
|
||||
"next@16.4.6": "patches/next@16.4.6.patch",
|
||||
"@shikijs/langs@3.21.0": "patches/@shikijs%2Flangs@3.21.0.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/embed
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 65210b7: Respect reduced motion for enter and exit animations, keep the header separator and input focus visible in forced colors mode, and size mobile surfaces with dynamic viewport units.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
|
||||
@@ -158,6 +158,8 @@
|
||||
z-index: 9998;
|
||||
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
|
||||
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
|
||||
/* stylelint-disable-next-line plugin/no-unsupported-browser-features -- the `vh` declaration above is the fallback on host pages whose browser lacks `dvh` */
|
||||
height: calc(min(var(--gitbook-widget-window-height), calc(100dvh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
|
||||
background-color: var(--gitbook-widget-background-translucent);
|
||||
backdrop-filter: blur(48px);
|
||||
border: 1px solid var(--gitbook-widget-border-color);
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# gitbook
|
||||
|
||||
## 0.28.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ddccd3d: Bump Next.js to 16.4.6 and OpenNext (Cloudflare 1.20.17, AWS 4.1.16), keeping the dev-mode OOM patch.
|
||||
- 0c3a51e: Fix horizontal page scroll caused by card carousels bleeding past the viewport.
|
||||
- 5997a66: Remove duplicated declarations from the global stylesheet.
|
||||
- c3df458: Keep inline code and highlighted text readable when a paragraph crosses the bottom edge of a background page cover.
|
||||
- 265ca4b: Add a skip link, keyboard navigation between content tabs, focusable table of contents toggles, and hide decorative icons from assistive technologies.
|
||||
- 9b5b8c8: Load the OpenAPI stylesheet only on pages with OpenAPI blocks.
|
||||
- adfd921: Limit the MCP `searchDocumentation` tool to one language: the one the client passes in the new optional `language` argument, or else the language of the site variant the MCP endpoint is served from.
|
||||
- 19660ed: Flush insights events when the page is hidden, preload the search index at low priority and prioritize the dark page cover on dark-default sites.
|
||||
- 19660ed: Fix the search button accessible name, hoverable link previews, IME-safe Enter on Safari, cookie banner semantics, active outline item announcement and textarea shrinking.
|
||||
- 65210b7: Respect reduced motion for enter and exit animations, keep the header separator and input focus visible in forced colors mode, and size mobile surfaces with dynamic viewport units.
|
||||
- 3e86382: Send only the site origin as referrer to third parties, and only let the embedding page open the embed messaging channel.
|
||||
- 7d1cd9f: Remove the ChatGPT-specific plain text response for Markdown and llms.txt routes; these now always serve `text/markdown`.
|
||||
- Updated dependencies [265ca4b]
|
||||
- Updated dependencies [65210b7]
|
||||
- @gitbook/icons@0.5.1
|
||||
- @gitbook/embed@0.7.1
|
||||
- @gitbook/react-contentkit@0.7.19
|
||||
|
||||
## 0.28.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.28.2",
|
||||
"version": "0.28.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
@@ -20,8 +20,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "4.1.15",
|
||||
"@opennextjs/cloudflare": "1.20.16",
|
||||
"@opennextjs/aws": "4.1.16",
|
||||
"@opennextjs/cloudflare": "1.20.17",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
@@ -51,7 +51,7 @@
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^16.4.5",
|
||||
"next": "^16.4.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
|
||||
@@ -57,7 +57,6 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
<div
|
||||
role="dialog"
|
||||
data-testid="cookies-dialog"
|
||||
aria-modal="true"
|
||||
aria-label={tString(language, 'cookies_title')}
|
||||
aria-describedby={describedById}
|
||||
className={tcls(
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getCodeTextFromId } from './utils';
|
||||
import { useCopyToClipboard } from '@/components/hooks';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
@@ -15,41 +14,30 @@ export function CopyCodeButton(props: { codeId: string; style: ClassValue }) {
|
||||
const { codeId, style } = props;
|
||||
|
||||
const language = useLanguage();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!copied) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [copied]);
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
|
||||
const onClick = () => {
|
||||
const codeText = getCodeTextFromId(codeId);
|
||||
if (codeText === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(codeText);
|
||||
|
||||
setCopied(true);
|
||||
copy(codeText);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="xsmall"
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
className={tcls(style, 'translate-y-0!', 'print:hidden')}
|
||||
>
|
||||
{t(language, copied ? 'code_copied' : 'code_copy')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
size="xsmall"
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
className={tcls(style, 'translate-y-0!', 'print:hidden')}
|
||||
>
|
||||
{t(language, copied ? 'code_copied' : 'code_copy')}
|
||||
</Button>
|
||||
{/* A label swap on the focused button isn't announced, the live region is. */}
|
||||
<span role="status" className="sr-only">
|
||||
{copied ? t(language, 'code_copied') : null}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
.contentkit-modal {
|
||||
@apply flex flex-col gap-6 relative z-50 bg-tint
|
||||
rounded shadow-lg opacity-0 transition-opacity duration-300 border border-tint
|
||||
w-full max-w-[768px] max-h-[90vh] overflow-y-auto;
|
||||
w-full max-w-[768px] max-h-[90dvh] overflow-y-auto;
|
||||
}
|
||||
.contentkit-modal.contentkit-modal-opened {
|
||||
@apply opacity-[1];
|
||||
|
||||
@@ -16,6 +16,8 @@ import { matchesText } from '../Table/searchMatch';
|
||||
import { MIN_RECORDS_FOR_SEARCH } from '../Table/shouldShowSearch';
|
||||
import { Input } from '@/components/primitives';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
// Imported here, behind the `next/dynamic` boundary, so only pages with OpenAPI blocks load it.
|
||||
import './style.css';
|
||||
|
||||
// Only what survives the RSC boundary. Rebuilding the renderers on the client is what lets this
|
||||
// whole subtree sit behind a `next/dynamic` chunk instead of the route's eager entry.
|
||||
|
||||
@@ -17,7 +17,6 @@ import { PlainCodeBlock } from '../CodeBlock';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
import { Heading } from '../Heading';
|
||||
import type { OpenAPIBlockClientContextProps } from './OpenAPIBlockClient';
|
||||
import './style.css';
|
||||
import { SCALAR_RUNTIME_PATH } from './scalar-runtime-path';
|
||||
import { DEFAULT_LOCALE, getSpaceLocale } from '@/intl/server';
|
||||
import { getAssetURL } from '@/lib/assets';
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Global, not in style.css: badges also show in the sidebar and outline of pages without OpenAPI blocks. */
|
||||
.openapi-method,
|
||||
.openapi-statuscode {
|
||||
@apply m-0 h-5 min-w-9 justify-center rounded-md text-xs straight-corners:rounded-none circular-corners:rounded-lg uppercase font-mono items-center shrink-0 font-semibold px-1.5 py-0.5 text-tint-12/8 leading-tight align-middle inline-flex whitespace-nowrap;
|
||||
}
|
||||
|
||||
.openapi-method-small {}
|
||||
|
||||
.openapi-method-medium {
|
||||
@apply m-0 px-2.5 py-1 h-6 text-[0.813rem];
|
||||
}
|
||||
|
||||
.toclink .openapi-method {
|
||||
@apply text-[0.625rem] flex items-center justify-center;
|
||||
}
|
||||
|
||||
.openapi-method-get,
|
||||
.openapi-statuscode-success {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100;
|
||||
}
|
||||
|
||||
.openapi-method-post,
|
||||
.openapi-statuscode-redirection {
|
||||
@apply bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100;
|
||||
}
|
||||
|
||||
.openapi-method-put,
|
||||
.openapi-statuscode-informational {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100;
|
||||
}
|
||||
|
||||
.openapi-method-patch {
|
||||
@apply bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100;
|
||||
}
|
||||
|
||||
.openapi-method-delete,
|
||||
.openapi-statuscode-error {
|
||||
@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100;
|
||||
}
|
||||
|
||||
.openapi-method-head,
|
||||
.openapi-method-options,
|
||||
.openapi-method-trace,
|
||||
.openapi-method-hook {
|
||||
@apply bg-tint;
|
||||
}
|
||||
@@ -143,53 +143,6 @@ button.openapi-mcp {
|
||||
@apply !mb-0;
|
||||
}
|
||||
|
||||
/* Method Tags */
|
||||
.openapi-method,
|
||||
.openapi-statuscode {
|
||||
@apply m-0 h-5 min-w-9 justify-center rounded-md text-xs straight-corners:rounded-none circular-corners:rounded-lg uppercase font-mono items-center shrink-0 font-semibold px-1.5 py-0.5 text-tint-12/8 leading-tight align-middle inline-flex whitespace-nowrap;
|
||||
}
|
||||
|
||||
.openapi-method-small {}
|
||||
|
||||
.openapi-method-medium {
|
||||
@apply m-0 px-2.5 py-1 h-6 text-[0.813rem];
|
||||
}
|
||||
|
||||
.toclink .openapi-method {
|
||||
@apply text-[0.625rem] flex items-center justify-center;
|
||||
}
|
||||
|
||||
.openapi-method-get,
|
||||
.openapi-statuscode-success {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100;
|
||||
}
|
||||
|
||||
.openapi-method-post,
|
||||
.openapi-statuscode-redirection {
|
||||
@apply bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100;
|
||||
}
|
||||
|
||||
.openapi-method-put,
|
||||
.openapi-statuscode-informational {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100;
|
||||
}
|
||||
|
||||
.openapi-method-patch {
|
||||
@apply bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100;
|
||||
}
|
||||
|
||||
.openapi-method-delete,
|
||||
.openapi-statuscode-error {
|
||||
@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100;
|
||||
}
|
||||
|
||||
.openapi-method-head,
|
||||
.openapi-method-options,
|
||||
.openapi-method-trace,
|
||||
.openapi-method-hook {
|
||||
@apply bg-tint;
|
||||
}
|
||||
|
||||
/* URL */
|
||||
.openapi-url {
|
||||
@apply font-mono text-sm text-tint;
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { type PromptActionId, setPromptAction, usePromptAction } from './promptAction';
|
||||
import { useCopyToClipboard } from '@/components/hooks';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -27,25 +28,10 @@ export function PromptActions(props: { prompt: string; openInAIProviders: boolea
|
||||
const { prompt, openInAIProviders } = props;
|
||||
const language = useLanguage();
|
||||
const selectedAction = usePromptAction();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!copied) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, COPIED_MESSAGE_DURATION);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [copied]);
|
||||
const { copied, copy } = useCopyToClipboard(COPIED_MESSAGE_DURATION);
|
||||
|
||||
const copyPrompt = () => {
|
||||
navigator.clipboard.writeText(prompt);
|
||||
setCopied(true);
|
||||
copy(prompt);
|
||||
};
|
||||
|
||||
// The pick follows the visitor from site to site, so fall back to copying wherever the agent
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { getNextTabIndex } from './DynamicTabs';
|
||||
|
||||
describe('getNextTabIndex', () => {
|
||||
it('moves with the arrow keys and wraps around', () => {
|
||||
expect(getNextTabIndex('ArrowRight', 0, 3)).toBe(1);
|
||||
expect(getNextTabIndex('ArrowRight', 2, 3)).toBe(0);
|
||||
expect(getNextTabIndex('ArrowLeft', 0, 3)).toBe(2);
|
||||
});
|
||||
|
||||
it('jumps to the first and last tab', () => {
|
||||
expect(getNextTabIndex('Home', 2, 3)).toBe(0);
|
||||
expect(getNextTabIndex('End', 0, 3)).toBe(2);
|
||||
});
|
||||
|
||||
it('mirrors the arrows in right-to-left', () => {
|
||||
expect(getNextTabIndex('ArrowLeft', 0, 3, true)).toBe(1);
|
||||
expect(getNextTabIndex('ArrowRight', 0, 3, true)).toBe(2);
|
||||
});
|
||||
|
||||
it('ignores other keys and an unknown tab', () => {
|
||||
expect(getNextTabIndex('Enter', 0, 3)).toBeNull();
|
||||
expect(getNextTabIndex('ArrowRight', -1, 3)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -151,10 +151,15 @@ const TabItemList = memo(function TabItemList(props: {
|
||||
}).filter((x) => x !== undefined),
|
||||
[overflowing, tabs]
|
||||
);
|
||||
// Roving tabindex: one tab stop for the strip, the active tab when it isn't in the overflow menu.
|
||||
const visibleTabs = tabs.filter((tab) => !overflowing.has(getTabButtonId(tab.id)));
|
||||
const focusableTabId =
|
||||
visibleTabs.find((tab) => tab.id === activeTabId)?.id ?? visibleTabs[0]?.id ?? null;
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
role="tablist"
|
||||
onKeyDown={moveFocusBetweenTabs}
|
||||
className={tcls(
|
||||
'group/tabs',
|
||||
'overflow-hidden',
|
||||
@@ -182,6 +187,7 @@ const TabItemList = memo(function TabItemList(props: {
|
||||
key={tab.id}
|
||||
ref={itemRef}
|
||||
isActive={tab.id === activeTabId}
|
||||
isFocusable={tab.id === focusableTabId}
|
||||
isMeasuring={isMeasuring}
|
||||
tab={tab}
|
||||
onSelect={onSelect}
|
||||
@@ -241,16 +247,18 @@ function TabsDropdownMenu(props: {
|
||||
const TabItem = memo(function TabItem(props: {
|
||||
ref: React.Ref<HTMLButtonElement>;
|
||||
isActive: boolean;
|
||||
isFocusable: boolean;
|
||||
isMeasuring: boolean;
|
||||
tab: TabsItem;
|
||||
onSelect: (tabId: string) => void;
|
||||
}) {
|
||||
const { ref, tab, isActive, isMeasuring, onSelect } = props;
|
||||
const { ref, tab, isActive, isFocusable, isMeasuring, onSelect } = props;
|
||||
return (
|
||||
<TabButton
|
||||
ref={ref}
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
tabIndex={isFocusable ? 0 : -1}
|
||||
aria-controls={tab.id}
|
||||
id={getTabButtonId(tab.id)}
|
||||
title={tab.title}
|
||||
@@ -343,6 +351,55 @@ function TabButton(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrow keys, Home and End move focus between the tabs of a tablist; Enter or Space then selects.
|
||||
*/
|
||||
function moveFocusBetweenTabs(event: React.KeyboardEvent<HTMLElement>) {
|
||||
const current = event.target instanceof HTMLElement ? event.target : null;
|
||||
if (current?.getAttribute('role') !== 'tab') {
|
||||
return;
|
||||
}
|
||||
const tabs = Array.from(
|
||||
event.currentTarget.querySelectorAll<HTMLElement>('[role="tab"]:not([disabled])')
|
||||
);
|
||||
const index = tabs.indexOf(current);
|
||||
const isRTL = getComputedStyle(event.currentTarget).direction === 'rtl';
|
||||
const next = getNextTabIndex(event.key, index, tabs.length, isRTL);
|
||||
if (next === null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
tabs[next]?.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Index of the tab to focus for a key press, or null when the key doesn't move focus.
|
||||
*/
|
||||
export function getNextTabIndex(
|
||||
key: string,
|
||||
index: number,
|
||||
count: number,
|
||||
isRTL = false
|
||||
): number | null {
|
||||
if (index < 0 || count === 0) {
|
||||
return null;
|
||||
}
|
||||
const forward = isRTL ? 'ArrowLeft' : 'ArrowRight';
|
||||
const backward = isRTL ? 'ArrowRight' : 'ArrowLeft';
|
||||
switch (key) {
|
||||
case forward:
|
||||
return (index + 1) % count;
|
||||
case backward:
|
||||
return (index - 1 + count) % count;
|
||||
case 'Home':
|
||||
return 0;
|
||||
case 'End':
|
||||
return count - 1;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID for a tab button.
|
||||
*/
|
||||
|
||||
@@ -125,6 +125,9 @@ function Color(props: MarkedLeafProps<DocumentMarkColor>) {
|
||||
const { mark, children } = props;
|
||||
return (
|
||||
<span
|
||||
data-highlight={
|
||||
mark.data.background && mark.data.background !== 'default' ? '' : undefined
|
||||
}
|
||||
className={tcls([
|
||||
textColorToStyle[mark.data.text ?? 'default'],
|
||||
backgroundColorToStyle[mark.data.background ?? 'default'],
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { isForeignHandshake } from './channel';
|
||||
|
||||
describe('isForeignHandshake', () => {
|
||||
const parent = {} as MessageEventSource;
|
||||
const other = {} as MessageEventSource;
|
||||
|
||||
it('accepts a handshake from the expected window', () => {
|
||||
expect(isForeignHandshake({ data: { type: 'bidc-connect' }, source: parent }, parent)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a handshake from another window', () => {
|
||||
expect(isForeignHandshake({ data: { type: 'bidc-connect' }, source: other }, parent)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores messages that are not handshakes', () => {
|
||||
expect(isForeignHandshake({ data: { type: 'other' }, source: other }, parent)).toBe(false);
|
||||
expect(isForeignHandshake({ data: null, source: other }, parent)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -9,11 +9,33 @@ export function log(...data: any[]) {
|
||||
console.log(...data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a message is a bidc handshake sent by a window other than the expected one.
|
||||
*/
|
||||
export function isForeignHandshake(
|
||||
event: Pick<MessageEvent, 'data' | 'source'>,
|
||||
expectedSource: MessageEventSource
|
||||
): boolean {
|
||||
return event.data?.type === 'bidc-connect' && event.source !== expectedSource;
|
||||
}
|
||||
|
||||
export const getChannel = memoize(() => {
|
||||
if (typeof window === 'undefined' || window.parent === window) {
|
||||
return null;
|
||||
}
|
||||
log('[gitbook] create channel with parent window');
|
||||
|
||||
// bidc accepts a handshake from any window; registered first and in capture so it runs before
|
||||
// bidc's own listener and only the embedding page can open the channel.
|
||||
window.addEventListener(
|
||||
'message',
|
||||
(event) => {
|
||||
if (isForeignHandshake(event, window.parent)) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return createChannel();
|
||||
});
|
||||
|
||||
@@ -55,6 +55,8 @@ export async function Header(props: {
|
||||
'flex-none',
|
||||
'shadow-[0px_1px_0px]',
|
||||
'shadow-tint-12/2',
|
||||
// Box shadows are dropped in forced colors mode, the separator needs a real border.
|
||||
'forced-colors:border-b',
|
||||
|
||||
'bg-tint-base/9',
|
||||
'theme-muted:bg-tint-subtle/9',
|
||||
|
||||
@@ -9,7 +9,7 @@ interface HeaderLinksProps {
|
||||
|
||||
export async function HeaderLinks({ children, style }: HeaderLinksProps) {
|
||||
return (
|
||||
<div
|
||||
<nav
|
||||
className={tcls(
|
||||
'header-links',
|
||||
'@4xl:[&>.button+.button]:-ml-2 z-20 ml-auto flex min-w-9 shrink grow @7xl:grow-0 items-center justify-end @4xl:gap-x-6 gap-x-4',
|
||||
@@ -17,6 +17,6 @@ export async function HeaderLinks({ children, style }: HeaderLinksProps) {
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,13 +193,18 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* When the page is unloaded, flush all events.
|
||||
*/
|
||||
// `beforeunload` doesn't fire reliably on mobile, hiding the page is the last dependable signal.
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('beforeunload', flushEventsSync);
|
||||
const flushWhenHidden = () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
flushEventsSync();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', flushWhenHidden);
|
||||
window.addEventListener('pagehide', flushEventsSync);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', flushEventsSync);
|
||||
document.removeEventListener('visibilitychange', flushWhenHidden);
|
||||
window.removeEventListener('pagehide', flushEventsSync);
|
||||
};
|
||||
}, [flushEventsSync]);
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
|
||||
>
|
||||
<a
|
||||
href={`#${section.id}`}
|
||||
aria-current={activeId === section.id ? 'true' : undefined}
|
||||
data-cover-aware-text
|
||||
className={tcls(
|
||||
'relative',
|
||||
|
||||
@@ -16,6 +16,7 @@ import { PageCover } from './PageCover';
|
||||
import { PageFooterNavigation } from './PageFooterNavigation';
|
||||
import { PageHeader } from './PageHeader';
|
||||
import { PreservePageLayout } from './PreservePageLayout';
|
||||
import { MAIN_CONTENT_ID } from '@/components/layout';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { t } from '@/intl/translate';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
@@ -103,6 +104,7 @@ export async function PageBody(props: {
|
||||
/>
|
||||
) : null}
|
||||
<main
|
||||
id={MAIN_CONTENT_ID}
|
||||
// Comments on the whole page target anywhere in it that isn't a block.
|
||||
{...(withCommentAnchors ? { 'data-comment-page': '' } : {})}
|
||||
className={tcls(
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { StaticImageData } from 'next/image';
|
||||
import { assert } from 'ts-essentials';
|
||||
|
||||
import type { RevisionPageDocument, RevisionPageDocumentCover } from '@gitbook/api';
|
||||
import {
|
||||
CustomizationDefaultThemeMode,
|
||||
type RevisionPageDocument,
|
||||
type RevisionPageDocumentCover,
|
||||
} from '@gitbook/api';
|
||||
|
||||
import { CONTENT_STYLE } from '../layout';
|
||||
import { getCoverHeight } from './coverHeight';
|
||||
@@ -144,6 +148,11 @@ export async function PageCover(props: {
|
||||
y={cover.yPos}
|
||||
height={height}
|
||||
mask={mask}
|
||||
priorityTheme={
|
||||
context.customization.themes.default === CustomizationDefaultThemeMode.Dark
|
||||
? 'dark'
|
||||
: 'light'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -25,10 +25,14 @@ interface PageCoverImageProps {
|
||||
// Only if the `height` was customized by the user (and thus defined), we use it to set the cover's height and skip the default behaviour of fixed aspect-ratio.
|
||||
height: number | undefined;
|
||||
mask?: 'none' | 'radial';
|
||||
/** Theme most visitors see first, its cover is the LCP candidate. */
|
||||
priorityTheme?: 'light' | 'dark';
|
||||
}
|
||||
|
||||
export function PageCoverImage(props: PageCoverImageProps) {
|
||||
const { imgs, y, height, mask } = props;
|
||||
const { imgs, y, height, mask, priorityTheme = 'light' } = props;
|
||||
// The light cover is never demoted to low: a visitor can still have switched to the light theme.
|
||||
const darkFirst = priorityTheme === 'dark' && Boolean(imgs.dark);
|
||||
const { objectPositionY, isLoading } = useCoverPosition(imgs, y, {
|
||||
height,
|
||||
aspectRatio: PAGE_COVER_SIZE,
|
||||
@@ -48,7 +52,7 @@ export function PageCoverImage(props: PageCoverImageProps) {
|
||||
src={imgs.light.src}
|
||||
srcSet={imgs.light.srcSet}
|
||||
sizes={imgs.light.sizes}
|
||||
fetchPriority="high"
|
||||
fetchPriority={darkFirst ? 'auto' : 'high'}
|
||||
alt="Page cover"
|
||||
className={tcls('w-full', 'object-cover', imgs.dark ? 'dark:hidden' : '')}
|
||||
style={{
|
||||
@@ -69,7 +73,7 @@ export function PageCoverImage(props: PageCoverImageProps) {
|
||||
src={imgs.dark.src}
|
||||
srcSet={imgs.dark.srcSet}
|
||||
sizes={imgs.dark.sizes}
|
||||
fetchPriority="low"
|
||||
fetchPriority={darkFirst ? 'high' : 'low'}
|
||||
alt="Page cover"
|
||||
className={tcls('w-full', 'object-cover', 'dark:inline', 'hidden')}
|
||||
style={{
|
||||
|
||||
@@ -56,7 +56,6 @@ function preloadFont(fontData: FontData) {
|
||||
ReactDOM.preload(url, {
|
||||
as: 'font',
|
||||
crossOrigin: 'anonymous',
|
||||
fetchPriority: 'high',
|
||||
type: format ? `font/${format}` : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@config '../../../tailwind.config.ts';
|
||||
|
||||
@import "./prose.css";
|
||||
@import "../DocumentView/OpenAPI/method-badge.css";
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
@@ -33,7 +34,6 @@
|
||||
@utility hide-scrollbar {
|
||||
/* Hide scrollbar by default */
|
||||
scrollbar-color: transparent transparent;
|
||||
transition: scrollbar-color 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
/* Show scrollbar on hover */
|
||||
@@ -52,21 +52,14 @@
|
||||
rgba(0, 0, 0, 1) 96px,
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 1) 96px,
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
@utility linear-mask-util {
|
||||
mask-image: linear-gradient(to bottom, white, white);
|
||||
mask-image: linear-gradient(to bottom, white, white);
|
||||
}
|
||||
|
||||
@utility grid-area-1-1 {
|
||||
grid-area: 1 / 1;
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
|
||||
@utility gutter-stable {
|
||||
@@ -102,41 +95,6 @@
|
||||
border-top-right-radius: 30%;
|
||||
}
|
||||
|
||||
&:before {
|
||||
transform: rotate(-135deg) skewX(-45deg) scale(1.414, 0.707)
|
||||
translate(0, -50%);
|
||||
}
|
||||
&:after {
|
||||
transform: rotate(135deg) skewY(-45deg) scale(0.707, 1.414) translate(50%);
|
||||
}
|
||||
position: relative;
|
||||
background-color: orange;
|
||||
text-align: left;
|
||||
transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-color: inherit;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-color: inherit;
|
||||
}
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-top-right-radius: 30%;
|
||||
&:before {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-top-right-radius: 30%;
|
||||
}
|
||||
&:after {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-top-right-radius: 30%;
|
||||
}
|
||||
|
||||
&:before {
|
||||
transform: rotate(-135deg) skewX(-45deg) scale(1.414, 0.707)
|
||||
translate(0, -50%);
|
||||
@@ -152,21 +110,14 @@
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
word-break: break-word;
|
||||
@supports (overflow-wrap: anywhere) {
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
@utility bg-gradient-primary {
|
||||
@apply bg-linear-to-bl from-primary-4 to-tint-base to-60% bg-fixed;
|
||||
@apply bg-linear-to-bl from-primary-4 to-tint-base to-60% bg-fixed;
|
||||
}
|
||||
|
||||
@utility bg-gradient-tint {
|
||||
@apply bg-linear-to-bl from-tint-4 to-tint-base to-60% bg-fixed;
|
||||
@apply bg-linear-to-bl from-tint-4 to-tint-base to-60% bg-fixed;
|
||||
}
|
||||
|
||||
@utility site-background {
|
||||
@@ -186,24 +137,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@apply leading-relaxed;
|
||||
@@ -575,6 +508,18 @@ html.dark .highlight-line.diff-deleted .highlight-line-content::before {
|
||||
@apply text-contrast-cover;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Inline code and highlights sit on their own background, not the cover, so they keep the regular
|
||||
text color. Inherited, the transparent fill of a split element hides their text behind that
|
||||
background. Kept out of the utilities layer so a highlight's own text color still applies.
|
||||
*/
|
||||
@media not (prefers-contrast: more) {
|
||||
[data-over-cover] :is(code, [data-highlight]) {
|
||||
color: var(--cover-text-below);
|
||||
-webkit-text-fill-color: currentcolor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Zoomable images */
|
||||
@@ -680,3 +625,275 @@ html:has(.zoom-modal) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Enter and exit animations, referenced by the `animation` theme in tailwind.config.ts. */
|
||||
@keyframes present {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(1rem) scale(90%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: rotateX(-10deg) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: rotateX(0deg) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes scaleOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: rotateX(0deg) scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: rotateX(-10deg) scale(0.95);
|
||||
}
|
||||
}
|
||||
@keyframes blurIn {
|
||||
0% {
|
||||
filter: blur(6px);
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
100% {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes blurOut {
|
||||
0% {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
filter: blur(6px);
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
@keyframes blurInDisplay {
|
||||
0% {
|
||||
filter: blur(6px);
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
display: none;
|
||||
}
|
||||
100% {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes blurOutDisplay {
|
||||
0% {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
filter: blur(6px);
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@keyframes enterFromLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%);
|
||||
display: none;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@keyframes enterFromRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(50%);
|
||||
display: none;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@keyframes exitToLeft {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
display: flex;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@keyframes exitToRight {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
display: flex;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(50%);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Reduced motion keeps the fades and drops the movement. Redefining the keyframes rather than
|
||||
* disabling the animations keeps their duration, so code waiting on `animationend` is unaffected.
|
||||
* Each variant keeps the identity transform and filter of the full animation's end state: with
|
||||
* `fill-mode: both` they persist, and dropping them would change stacking and text rasterization.
|
||||
*/
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes present {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: rotateX(0deg) scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: rotateX(0deg) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes scaleOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: rotateX(0deg) scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: rotateX(0deg) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes blurIn {
|
||||
from {
|
||||
filter: blur(0px);
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes blurOut {
|
||||
from {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
filter: blur(0px);
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes blurInDisplay {
|
||||
from {
|
||||
filter: blur(0px);
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
display: none;
|
||||
}
|
||||
to {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes blurOutDisplay {
|
||||
from {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
filter: blur(0px);
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@keyframes enterFromLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(0);
|
||||
display: none;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@keyframes enterFromRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(0);
|
||||
display: none;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@keyframes exitToLeft {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
display: flex;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(0);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@keyframes exitToRight {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
display: flex;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(0);
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* A loader must keep showing activity, so it slows down instead of stopping. */
|
||||
.animate-spin {
|
||||
animation-duration: 3s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { SearchLiveResultsAnnouncer } from './SearchLiveResultsAnnouncer';
|
||||
import { SearchScopeControl } from './SearchScopeControl';
|
||||
import { useSearchController } from './useSearchController';
|
||||
import { useSearchPopupFocusTrap } from './useSearchPopupFocusTrap';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
const SearchFrame = dynamic(() => import('./SearchFrame').then((mod) => mod.SearchFrame), {
|
||||
@@ -203,6 +203,7 @@ export function SearchContainer({
|
||||
active={isSearchOpen}
|
||||
onClick={open}
|
||||
className={className}
|
||||
aria-label={tString(language, withSearchAI ? 'search_or_ask' : 'search')}
|
||||
aria-expanded={isSearchOpen}
|
||||
aria-controls={resultsId}
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { SearchResultsRef } from './SearchResults';
|
||||
import { useSearchState, useSetSearchState } from './useSearch';
|
||||
import { useSearchResults } from './useSearchResults';
|
||||
import { useSearchResultsCursor } from './useSearchResultsCursor';
|
||||
import { isComposingKeyEvent } from '@/lib/keyboard';
|
||||
import { getLocalizedTitle } from '@/lib/sites';
|
||||
|
||||
function useInitialAskBootstrap(props: {
|
||||
@@ -95,7 +96,7 @@ function useSearchKeyboardNavigation(props: {
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
moveCursorBy(1);
|
||||
} else if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
|
||||
} else if (event.key === 'Enter' && !isComposingKeyEvent(event)) {
|
||||
event.preventDefault();
|
||||
if (cursor !== null && cursor >= results.length) {
|
||||
onAskSelect(cursor - results.length);
|
||||
|
||||
@@ -74,10 +74,12 @@ export async function SiteLayout(props: {
|
||||
// Start the search-index download from the HTML itself. `crossOrigin` must match the
|
||||
// client `fetch()` (cors + same-origin credentials) or the preload is ignored and the
|
||||
// index downloads twice — the omission was exactly that bug before.
|
||||
// Low priority: it's only needed once search opens and must not compete with the LCP.
|
||||
ReactDOM.preload(`${context.linker.siteBasePath}~gitbook/site-index`, {
|
||||
as: 'fetch',
|
||||
type: 'application/json',
|
||||
crossOrigin: 'anonymous',
|
||||
fetchPriority: 'low',
|
||||
});
|
||||
|
||||
scripts.forEach(({ script }) => {
|
||||
|
||||
@@ -159,7 +159,7 @@ export function ConsentLayout(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||
const { children, className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<main className="site-background flex min-h-screen flex-col" {...rest}>
|
||||
<main className="site-background flex min-h-dvh flex-col" {...rest}>
|
||||
<div
|
||||
className={tcls(
|
||||
'flex w-full flex-1 items-center justify-center',
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
useSpaceBasePath,
|
||||
} from '../SpaceLayout/SpaceLayoutContext';
|
||||
import { TrackPageViewEvent } from '@/components/Insights';
|
||||
import { MAIN_CONTENT_ID } from '@/components/layout';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -102,6 +103,7 @@ export function SitePageNotFound() {
|
||||
<CurrentPageProvider page={null}>
|
||||
<div className="lg:has-sidebar:-ml-84 w-full">
|
||||
<main
|
||||
id={MAIN_CONTENT_ID}
|
||||
className={tcls(
|
||||
// `page-no-toc` hides the TOC (like PageBody), and `layout-wide` takes the
|
||||
// now-empty sidebar out of flow so the content stays centered on all layouts.
|
||||
|
||||
@@ -22,7 +22,7 @@ const POPUP_OFFSET = 4;
|
||||
const OPEN_DELAY_MS = 200;
|
||||
const MOTION = 'duration-300 ease-[cubic-bezier(0.83,0,0.17,1)]';
|
||||
// Bounds the scrolling content too: the popup settles at `height: auto`, so `h-full` doesn't.
|
||||
const MAX_POPUP_HEIGHT = 'max-h-[calc(100vh-8rem)]';
|
||||
const MAX_POPUP_HEIGHT = 'max-h-[calc(100dvh-8rem)]';
|
||||
const MAX_ITEMS_PER_COLUMN = 10; // number of items per column
|
||||
const GROUP_MASONRY_THRESHOLD = 3; // if a section group has more than this many child groups, it will be shown in a masonry grid
|
||||
const COLUMN_WIDTH = '18rem';
|
||||
|
||||
@@ -12,6 +12,7 @@ import { CurrentContentProvider } from '../hooks';
|
||||
import { InsightsProvider, VisitorProvider } from '../Insights';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { NavigationLoader } from '../primitives/NavigationLoader';
|
||||
import { SkipToContent } from '../primitives/SkipToContent';
|
||||
import { SearchContainer, getSearchBaseProps } from '../Search';
|
||||
import {
|
||||
SiteSectionList,
|
||||
@@ -131,6 +132,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
|
||||
return (
|
||||
<SpaceLayoutServerContext {...props}>
|
||||
<SkipToContent />
|
||||
<Announcement context={context} />
|
||||
<Header withTopHeader={withTopHeader} variants={variants} context={context} />
|
||||
<NavigationLoader />
|
||||
@@ -160,8 +162,8 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
// Ensure the footer is display below the viewport even if the content is not enough
|
||||
withTopHeader
|
||||
? [
|
||||
'site-header:min-h-[calc(100vh-65px)]',
|
||||
'site-header-sections:min-h-[calc(100vh-109px)]',
|
||||
'site-header:min-h-[calc(100dvh-65px)]',
|
||||
'site-header-sections:min-h-[calc(100dvh-109px)]',
|
||||
]
|
||||
: 'lg:min-h-screen'
|
||||
)}
|
||||
|
||||
@@ -26,6 +26,7 @@ export function ToggleableLinkItem(
|
||||
isActive || pathnames.some((pathname) => currentPagePath.startsWith(`${pathname}/`));
|
||||
const [isOpen, setIsOpen] = React.useState(defaultIsOpen);
|
||||
const hasBeenToggled = useRef(false);
|
||||
const linkId = React.useId();
|
||||
|
||||
// Update the visibility of the children if one of the descendants becomes active.
|
||||
React.useEffect(() => {
|
||||
@@ -60,10 +61,16 @@ export function ToggleableLinkItem(
|
||||
}
|
||||
|
||||
return (
|
||||
<DescendantsRenderer descendants={descendants} isOpen={isOpen} setIsOpen={handleToggle}>
|
||||
<DescendantsRenderer
|
||||
descendants={descendants}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={handleToggle}
|
||||
labelledBy={linkId}
|
||||
>
|
||||
{({ descendants, toggler }) => (
|
||||
<>
|
||||
<LinkItem
|
||||
id={linkId}
|
||||
href={href}
|
||||
insights={insights}
|
||||
isActive={isActive}
|
||||
@@ -88,12 +95,12 @@ export function ToggleableLinkItem(
|
||||
}
|
||||
|
||||
function LinkItem(
|
||||
props: Pick<LinkProps, 'href' | 'insights' | 'children'> & {
|
||||
props: Pick<LinkProps, 'href' | 'insights' | 'children' | 'id'> & {
|
||||
isActive: boolean;
|
||||
onActiveClick?: () => void;
|
||||
}
|
||||
) {
|
||||
const { isActive, href, insights, children, onActiveClick } = props;
|
||||
const { id, isActive, href, insights, children, onActiveClick } = props;
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (isActive && onActiveClick) {
|
||||
@@ -104,6 +111,7 @@ function LinkItem(
|
||||
|
||||
return (
|
||||
<Link
|
||||
id={id}
|
||||
data-active={isActive}
|
||||
href={href}
|
||||
insights={insights}
|
||||
@@ -123,18 +131,21 @@ function DescendantsRenderer(props: {
|
||||
descendants: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
/** Id of the link the toggle belongs to, used as its accessible name. */
|
||||
labelledBy: string;
|
||||
children: (renderProps: {
|
||||
descendants: React.ReactNode;
|
||||
toggler: React.ReactNode;
|
||||
}) => React.ReactNode;
|
||||
}) {
|
||||
const { descendants, isOpen, setIsOpen } = props;
|
||||
const { descendants, isOpen, setIsOpen, labelledBy } = props;
|
||||
|
||||
return props.children({
|
||||
toggler: (
|
||||
<Toggler
|
||||
isLinkActive={isOpen}
|
||||
isOpen={isOpen}
|
||||
labelledBy={labelledBy}
|
||||
onToggle={() => {
|
||||
setIsOpen((prev) => !prev);
|
||||
}}
|
||||
@@ -144,8 +155,13 @@ function DescendantsRenderer(props: {
|
||||
});
|
||||
}
|
||||
|
||||
function Toggler(props: { isLinkActive: boolean; isOpen: boolean; onToggle: () => void }) {
|
||||
const { isOpen, onToggle } = props;
|
||||
function Toggler(props: {
|
||||
isLinkActive: boolean;
|
||||
isOpen: boolean;
|
||||
labelledBy: string;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const { isOpen, labelledBy, onToggle } = props;
|
||||
return (
|
||||
<Button
|
||||
icon={
|
||||
@@ -164,9 +180,12 @@ function Toggler(props: { isLinkActive: boolean; isOpen: boolean; onToggle: () =
|
||||
size="xsmall"
|
||||
iconOnly
|
||||
variant="blank"
|
||||
aria-hidden="true" // The button has no label or focus so hiding it from screen readers.
|
||||
className="-my-0.5 -mr-1 ml-auto min-h-6 min-w-6 text-current hover:bg-tint-base"
|
||||
tabIndex={-1} // Prevent focus on the button since it's already inside a clickable link that performs the same toggle action.
|
||||
// Named after its page and focusable: the link only toggles on the current page, so
|
||||
// this is the only keyboard way to expand another page's children.
|
||||
aria-labelledby={labelledBy}
|
||||
aria-expanded={isOpen}
|
||||
// Cancels the Button's expanded highlight: an open chevron must look like a closed one.
|
||||
className="-my-0.5 -mr-1 ml-auto min-h-6 min-w-6 text-current hover:bg-tint-base aria-expanded:bg-transparent aria-expanded:text-current aria-expanded:hover:bg-tint-hover"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from './useNow';
|
||||
export * from './useListOverflow';
|
||||
export * from './useCurrentPageMetadata';
|
||||
export * from './useBackToSpace';
|
||||
export * from './useCopyToClipboard';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Copy text to the clipboard and expose a `copied` flag that resets after `duration`.
|
||||
* The flag is only set once the write succeeded.
|
||||
*/
|
||||
export function useCopyToClipboard(duration = 1000) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!copied) {
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => setCopied(false), duration);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [copied, duration]);
|
||||
|
||||
const copy = React.useCallback(async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
} catch {
|
||||
// Clipboard access can be denied (permissions, insecure context): don't claim success.
|
||||
setCopied(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { copied, copy };
|
||||
}
|
||||
@@ -50,3 +50,6 @@ export const SIDE_COLUMN_WITHOUT_HEADER_AND_COVER: ClassValue = [
|
||||
'top-[240px]',
|
||||
'h-[calc(100vh-240px)]',
|
||||
];
|
||||
|
||||
/** Id of the `<main>` landmark, target of the skip link. */
|
||||
export const MAIN_CONTENT_ID = 'gitbook-main-content';
|
||||
|
||||
@@ -187,7 +187,7 @@ export const Button = React.forwardRef<
|
||||
className={domClassName}
|
||||
classNames={['ButtonStyles']}
|
||||
insights={insights}
|
||||
aria-label={label?.toString()}
|
||||
aria-label={typeof label === 'string' ? label : undefined}
|
||||
aria-pressed={active}
|
||||
target={target}
|
||||
data-active={active}
|
||||
@@ -200,7 +200,7 @@ export const Button = React.forwardRef<
|
||||
ref={ref as React.Ref<HTMLButtonElement>}
|
||||
type="button"
|
||||
className={tcls(buttonOnlyClassNames, domClassName)}
|
||||
aria-label={label?.toString()}
|
||||
aria-label={typeof label === 'string' ? label : undefined}
|
||||
aria-pressed={active}
|
||||
disabled={disabled}
|
||||
data-active={active}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function HoverCard(
|
||||
>
|
||||
<PreviewCard.Popup
|
||||
{...popupProps}
|
||||
className="animate-scale-in data-closed:animate-scale-out"
|
||||
className="pointer-events-auto animate-scale-in data-closed:animate-scale-out"
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useControlledState } from '../hooks/useControlledState';
|
||||
import { Button, type ButtonProps } from './Button';
|
||||
import { KeyboardShortcut, type KeyboardShortcutProps } from './KeyboardShortcut';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import { isComposingKeyEvent } from '@/lib/keyboard';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
type CustomInputProps = {
|
||||
@@ -166,8 +167,16 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
|
||||
|
||||
if (multiline && resize && ref.current) {
|
||||
// TODO: replace with `field-sizing: content` when more broadly supported. https://caniuse.com/?search=field-sizing
|
||||
// Reset the height to auto, then set it to the scroll height. If we don't reset, the height will only ever grow.
|
||||
setHeight(ref.current.scrollHeight);
|
||||
// scrollHeight never goes below the current height, so shrinking needs a measure with the
|
||||
// height reset. Only done on deletion: collapsing the field can shift the page scroll.
|
||||
const previousHeight = ref.current.style.height;
|
||||
const isShrinking = newValue.length < value.length;
|
||||
if (isShrinking) {
|
||||
ref.current.style.height = 'auto';
|
||||
}
|
||||
const nextHeight = ref.current.scrollHeight;
|
||||
ref.current.style.height = previousHeight;
|
||||
setHeight(nextHeight);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,12 +202,7 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
|
||||
// If the user wants to handle the keydown by itself, we let him do it.
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
if (
|
||||
event.key === 'Enter' &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing &&
|
||||
hasValue
|
||||
) {
|
||||
if (event.key === 'Enter' && !event.shiftKey && !isComposingKeyEvent(event) && hasValue) {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
} else if (event.key === 'Escape') {
|
||||
@@ -250,7 +254,7 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
|
||||
? 'cursor-not-allowed border-tint-subtle bg-tint-subtle text-tint/8'
|
||||
: [
|
||||
'depth-subtle:focus-within:-translate-y-px depth-subtle:hover:not-has-[button:hover]:-translate-y-px depth-subtle:shadow-xs',
|
||||
'focus-within:border-primary-hover focus-within:depth-subtle:shadow-lg focus-within:shadow-primary-subtle focus-within:ring-2 hover:not-has-[button:hover]:cursor-text hover:not-has-[button:hover]:border-tint-hover hover:not-has-[button:hover]:not-focus-within:bg-tint-subtle depth-subtle:hover:not-has-[button:hover]:not-focus-within:shadow-md focus-within:hover:not-has-[button:hover]:border-primary-hover',
|
||||
'focus-within:border-primary-hover focus-within:depth-subtle:shadow-lg focus-within:shadow-primary-subtle focus-within:ring-2 forced-colors:focus-within:outline-2 hover:not-has-[button:hover]:cursor-text hover:not-has-[button:hover]:border-tint-hover hover:not-has-[button:hover]:not-focus-within:bg-tint-subtle depth-subtle:hover:not-has-[button:hover]:not-focus-within:shadow-md focus-within:hover:not-has-[button:hover]:border-primary-hover',
|
||||
],
|
||||
multiline ? 'flex-col' : 'flex-row',
|
||||
sizes[sizing].container,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { MAIN_CONTENT_ID } from '../layout';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
|
||||
/**
|
||||
* Link shown on keyboard focus to jump over the header and navigation.
|
||||
*/
|
||||
export function SkipToContent() {
|
||||
const language = useLanguage();
|
||||
return (
|
||||
<a
|
||||
href={`#${MAIN_CONTENT_ID}`}
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-2 focus:top-2 focus:z-50 focus:rounded-md focus:bg-tint-base focus:px-4 focus:py-2 focus:text-sm focus:text-tint-strong focus:shadow-lg focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
{tString(language, 'skip_to_content')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export const ar: TranslationLanguage = {
|
||||
switch_to_light_theme: 'التبديل إلى السمة الفاتحة',
|
||||
switch_to_system_theme: 'التبديل إلى سمة النظام',
|
||||
search: 'بحث',
|
||||
skip_to_content: 'تخطي إلى المحتوى',
|
||||
clear: 'مسح',
|
||||
table_filtered_by_selection: 'يطابق الاختيار: ${1}',
|
||||
table_clear_selection: 'مسح الاختيار',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const bg: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Превключване към светла тема',
|
||||
switch_to_system_theme: 'Превключване към системната тема',
|
||||
search: 'Търсене',
|
||||
skip_to_content: 'Към съдържанието',
|
||||
clear: 'Изчистване',
|
||||
table_filtered_by_selection: 'Съответства на избора: ${1}',
|
||||
table_clear_selection: 'Изчисти избора',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const cs: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Přepnout na světlý motiv',
|
||||
switch_to_system_theme: 'Přepnout na systémový motiv',
|
||||
search: 'Hledat',
|
||||
skip_to_content: 'Přejít k obsahu',
|
||||
clear: 'Vymazat',
|
||||
table_filtered_by_selection: 'Odpovídá výběru: ${1}',
|
||||
table_clear_selection: 'Zrušit výběr',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const da: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Skift til lyst tema',
|
||||
switch_to_system_theme: 'Skift til systemtema',
|
||||
search: 'Søg',
|
||||
skip_to_content: 'Spring til indhold',
|
||||
clear: 'Ryd',
|
||||
table_filtered_by_selection: 'Matcher valg: ${1}',
|
||||
table_clear_selection: 'Ryd valg',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const de: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Zum hellen Modus wechseln',
|
||||
switch_to_system_theme: 'Zum Systemmodus wechseln',
|
||||
search: 'Suche',
|
||||
skip_to_content: 'Zum Inhalt springen',
|
||||
clear: 'Löschen',
|
||||
table_filtered_by_selection: 'Entspricht Auswahl: ${1}',
|
||||
table_clear_selection: 'Auswahl zurücksetzen',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const el: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Αλλαγή σε φωτεινό θέμα',
|
||||
switch_to_system_theme: 'Αλλαγή στο θέμα συστήματος',
|
||||
search: 'Αναζήτηση',
|
||||
skip_to_content: 'Μετάβαση στο περιεχόμενο',
|
||||
clear: 'Εκκαθάριση',
|
||||
table_filtered_by_selection: 'Ταιριάζει με την επιλογή: ${1}',
|
||||
table_clear_selection: 'Εκκαθάριση επιλογής',
|
||||
|
||||
@@ -8,6 +8,7 @@ export const en = {
|
||||
switch_to_light_theme: 'Switch to light theme',
|
||||
switch_to_system_theme: 'Switch to system theme',
|
||||
search: 'Search',
|
||||
skip_to_content: 'Skip to content',
|
||||
clear: 'Clear',
|
||||
table_filtered_by_selection: 'Matches selection: ${1}',
|
||||
table_clear_selection: 'Clear selection',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const es: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Cambiar a tema claro',
|
||||
switch_to_system_theme: 'Cambiar a tema del sistema',
|
||||
search: 'Buscar',
|
||||
skip_to_content: 'Saltar al contenido',
|
||||
clear: 'Limpiar',
|
||||
table_filtered_by_selection: 'Coincide con la selección: ${1}',
|
||||
table_clear_selection: 'Borrar selección',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const et: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Lülitu heledale teemale',
|
||||
switch_to_system_theme: 'Lülitu süsteemi teemale',
|
||||
search: 'Otsi',
|
||||
skip_to_content: 'Liigu sisu juurde',
|
||||
clear: 'Tühjenda',
|
||||
table_filtered_by_selection: 'Vastab valikule: ${1}',
|
||||
table_clear_selection: 'Tühjenda valik',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const fi: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Vaihda vaaleaan teemaan',
|
||||
switch_to_system_theme: 'Vaihda järjestelmän teemaan',
|
||||
search: 'Haku',
|
||||
skip_to_content: 'Siirry sisältöön',
|
||||
clear: 'Tyhjennä',
|
||||
table_filtered_by_selection: 'Vastaa valintaa: ${1}',
|
||||
table_clear_selection: 'Tyhjennä valinta',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const fr: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Activer le thème clair',
|
||||
switch_to_system_theme: 'Utiliser le thème du système',
|
||||
search: 'Rechercher',
|
||||
skip_to_content: 'Aller au contenu',
|
||||
clear: 'Effacer',
|
||||
table_filtered_by_selection: 'Correspond à la sélection : ${1}',
|
||||
table_clear_selection: 'Effacer la sélection',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const he: TranslationLanguage = {
|
||||
switch_to_light_theme: 'מעבר לערכת נושא בהירה',
|
||||
switch_to_system_theme: 'מעבר לערכת הנושא של המערכת',
|
||||
search: 'חיפוש',
|
||||
skip_to_content: 'דילוג לתוכן',
|
||||
clear: 'ניקוי',
|
||||
table_filtered_by_selection: 'תואם לבחירה: ${1}',
|
||||
table_clear_selection: 'נקה בחירה',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const hi: TranslationLanguage = {
|
||||
switch_to_light_theme: 'लाइट थीम पर जाएं',
|
||||
switch_to_system_theme: 'सिस्टम थीम पर जाएं',
|
||||
search: 'खोजें',
|
||||
skip_to_content: 'सामग्री पर जाएं',
|
||||
clear: 'साफ करें',
|
||||
table_filtered_by_selection: 'चयन से मेल खाता है: ${1}',
|
||||
table_clear_selection: 'चयन साफ़ करें',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const hr: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Prebaci na svijetlu temu',
|
||||
switch_to_system_theme: 'Prebaci na temu sustava',
|
||||
search: 'Pretraži',
|
||||
skip_to_content: 'Preskoči na sadržaj',
|
||||
clear: 'Očisti',
|
||||
table_filtered_by_selection: 'Odgovara odabiru: ${1}',
|
||||
table_clear_selection: 'Očisti odabir',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const hu: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Váltás világos témára',
|
||||
switch_to_system_theme: 'Váltás rendszer témára',
|
||||
search: 'Keresés',
|
||||
skip_to_content: 'Ugrás a tartalomra',
|
||||
clear: 'Törlés',
|
||||
table_filtered_by_selection: 'Megfelel a kijelölésnek: ${1}',
|
||||
table_clear_selection: 'Kijelölés törlése',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const id: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Beralih ke tema terang',
|
||||
switch_to_system_theme: 'Beralih ke tema sistem',
|
||||
search: 'Cari',
|
||||
skip_to_content: 'Lewati ke konten',
|
||||
clear: 'Bersihkan',
|
||||
table_filtered_by_selection: 'Sesuai pilihan: ${1}',
|
||||
table_clear_selection: 'Hapus pilihan',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const it: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Passa al tema chiaro',
|
||||
switch_to_system_theme: 'Passa al tema di sistema',
|
||||
search: 'Cerca',
|
||||
skip_to_content: 'Vai al contenuto',
|
||||
clear: 'Cancella',
|
||||
table_filtered_by_selection: 'Corrisponde alla selezione: ${1}',
|
||||
table_clear_selection: 'Cancella selezione',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ja: TranslationLanguage = {
|
||||
switch_to_light_theme: 'ライトテーマに切り替え',
|
||||
switch_to_system_theme: 'システムのテーマに切り替え',
|
||||
search: '検索',
|
||||
skip_to_content: 'コンテンツへスキップ',
|
||||
clear: 'クリア',
|
||||
table_filtered_by_selection: '選択に一致: ${1}',
|
||||
table_clear_selection: '選択をクリア',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ko: TranslationLanguage = {
|
||||
switch_to_light_theme: '라이트 테마로 전환',
|
||||
switch_to_system_theme: '시스템 테마로 전환',
|
||||
search: '검색',
|
||||
skip_to_content: '본문으로 건너뛰기',
|
||||
clear: '지우기',
|
||||
table_filtered_by_selection: '선택과 일치: ${1}',
|
||||
table_clear_selection: '선택 지우기',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const lt: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Perjungti į šviesią temą',
|
||||
switch_to_system_theme: 'Perjungti į sistemos temą',
|
||||
search: 'Ieškoti',
|
||||
skip_to_content: 'Pereiti prie turinio',
|
||||
clear: 'Išvalyti',
|
||||
table_filtered_by_selection: 'Atitinka pasirinkimą: ${1}',
|
||||
table_clear_selection: 'Išvalyti pasirinkimą',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const lv: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Pārslēgt uz gaišo motīvu',
|
||||
switch_to_system_theme: 'Pārslēgt uz sistēmas motīvu',
|
||||
search: 'Meklēt',
|
||||
skip_to_content: 'Pāriet uz saturu',
|
||||
clear: 'Notīrīt',
|
||||
table_filtered_by_selection: 'Atbilst izvēlei: ${1}',
|
||||
table_clear_selection: 'Notīrīt izvēli',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ms: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Tukar kepada tema cerah',
|
||||
switch_to_system_theme: 'Tukar kepada tema sistem',
|
||||
search: 'Cari',
|
||||
skip_to_content: 'Langkau ke kandungan',
|
||||
clear: 'Kosongkan',
|
||||
table_filtered_by_selection: 'Sepadan dengan pilihan: ${1}',
|
||||
table_clear_selection: 'Kosongkan pilihan',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const nl: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Schakel over naar lichte modus',
|
||||
switch_to_system_theme: 'Schakel over naar systeemmodus',
|
||||
search: 'Zoeken',
|
||||
skip_to_content: 'Naar inhoud',
|
||||
clear: 'Wissen',
|
||||
table_filtered_by_selection: 'Komt overeen met selectie: ${1}',
|
||||
table_clear_selection: 'Selectie wissen',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const no: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Bytt til lyst tema',
|
||||
switch_to_system_theme: 'Bytt til systemtema',
|
||||
search: 'Søk',
|
||||
skip_to_content: 'Hopp til innhold',
|
||||
clear: 'Tøm',
|
||||
table_filtered_by_selection: 'Samsvarer med valg: ${1}',
|
||||
table_clear_selection: 'Fjern valg',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const pl: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Przełącz na jasny motyw',
|
||||
switch_to_system_theme: 'Przełącz na motyw systemowy',
|
||||
search: 'Szukaj',
|
||||
skip_to_content: 'Przejdź do treści',
|
||||
clear: 'Wyczyść',
|
||||
table_filtered_by_selection: 'Zgodne z wyborem: ${1}',
|
||||
table_clear_selection: 'Wyczyść wybór',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const pt_br: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Mudar para modo claro',
|
||||
switch_to_system_theme: 'Mudar para configuração do sistema',
|
||||
search: 'Buscar',
|
||||
skip_to_content: 'Pular para o conteúdo',
|
||||
clear: 'Limpar',
|
||||
table_filtered_by_selection: 'Corresponde à seleção: ${1}',
|
||||
table_clear_selection: 'Limpar seleção',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const pt: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Mudar para o tema claro',
|
||||
switch_to_system_theme: 'Mudar para o tema do sistema',
|
||||
search: 'Pesquisar',
|
||||
skip_to_content: 'Saltar para o conteúdo',
|
||||
clear: 'Limpar',
|
||||
table_filtered_by_selection: 'Corresponde à seleção: ${1}',
|
||||
table_clear_selection: 'Limpar seleção',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ro: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Comută la tema luminoasă',
|
||||
switch_to_system_theme: 'Comută la tema sistemului',
|
||||
search: 'Caută',
|
||||
skip_to_content: 'Salt la conținut',
|
||||
clear: 'Șterge',
|
||||
table_filtered_by_selection: 'Se potrivește cu selecția: ${1}',
|
||||
table_clear_selection: 'Șterge selecția',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ru: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Переключиться на светлую тему',
|
||||
switch_to_system_theme: 'Переключиться на системную тему',
|
||||
search: 'Поиск',
|
||||
skip_to_content: 'Перейти к содержимому',
|
||||
clear: 'Очистить',
|
||||
table_filtered_by_selection: 'Соответствует выбору: ${1}',
|
||||
table_clear_selection: 'Очистить выбор',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const sk: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Prepnúť na svetlý motív',
|
||||
switch_to_system_theme: 'Prepnúť na systémový motív',
|
||||
search: 'Hľadať',
|
||||
skip_to_content: 'Preskočiť na obsah',
|
||||
clear: 'Vymazať',
|
||||
table_filtered_by_selection: 'Zodpovedá výberu: ${1}',
|
||||
table_clear_selection: 'Zrušiť výber',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const sl: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Preklopi na svetlo temo',
|
||||
switch_to_system_theme: 'Preklopi na sistemsko temo',
|
||||
search: 'Išči',
|
||||
skip_to_content: 'Preskoči na vsebino',
|
||||
clear: 'Počisti',
|
||||
table_filtered_by_selection: 'Ustreza izbiri: ${1}',
|
||||
table_clear_selection: 'Počisti izbiro',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const sv: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Byt till ljust tema',
|
||||
switch_to_system_theme: 'Byt till systemtema',
|
||||
search: 'Sök',
|
||||
skip_to_content: 'Hoppa till innehåll',
|
||||
clear: 'Rensa',
|
||||
table_filtered_by_selection: 'Matchar val: ${1}',
|
||||
table_clear_selection: 'Rensa val',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const th: TranslationLanguage = {
|
||||
switch_to_light_theme: 'เปลี่ยนเป็นธีมสว่าง',
|
||||
switch_to_system_theme: 'เปลี่ยนเป็นธีมของระบบ',
|
||||
search: 'ค้นหา',
|
||||
skip_to_content: 'ข้ามไปยังเนื้อหา',
|
||||
clear: 'ล้าง',
|
||||
table_filtered_by_selection: 'ตรงกับการเลือก: ${1}',
|
||||
table_clear_selection: 'ล้างการเลือก',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tr: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Açık temaya geç',
|
||||
switch_to_system_theme: 'Sistem temasına geç',
|
||||
search: 'Ara',
|
||||
skip_to_content: 'İçeriğe geç',
|
||||
clear: 'Temizle',
|
||||
table_filtered_by_selection: 'Seçimle eşleşiyor: ${1}',
|
||||
table_clear_selection: 'Seçimi temizle',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const uk: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Перемкнути на світлу тему',
|
||||
switch_to_system_theme: 'Перемкнути на системну тему',
|
||||
search: 'Пошук',
|
||||
skip_to_content: 'Перейти до вмісту',
|
||||
clear: 'Очистити',
|
||||
table_filtered_by_selection: 'Відповідає вибору: ${1}',
|
||||
table_clear_selection: 'Очистити вибір',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const vi: TranslationLanguage = {
|
||||
switch_to_light_theme: 'Chuyển sang giao diện sáng',
|
||||
switch_to_system_theme: 'Chuyển sang giao diện hệ thống',
|
||||
search: 'Tìm kiếm',
|
||||
skip_to_content: 'Chuyển đến nội dung',
|
||||
clear: 'Xóa',
|
||||
table_filtered_by_selection: 'Khớp với lựa chọn: ${1}',
|
||||
table_clear_selection: 'Xóa lựa chọn',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const yue: TranslationLanguage = {
|
||||
switch_to_light_theme: '切換到淺色主題',
|
||||
switch_to_system_theme: '切換到系統主題',
|
||||
search: '搜尋',
|
||||
skip_to_content: '跳至內容',
|
||||
clear: '清除',
|
||||
table_filtered_by_selection: '符合選擇:${1}',
|
||||
table_clear_selection: '清除選擇',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const zh_tw: TranslationLanguage = {
|
||||
switch_to_light_theme: '切換至淺色主題',
|
||||
switch_to_system_theme: '切換至系統主題',
|
||||
search: '搜尋',
|
||||
skip_to_content: '跳至內容',
|
||||
clear: '清除',
|
||||
table_filtered_by_selection: '符合選擇:${1}',
|
||||
table_clear_selection: '清除選擇',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const zh: TranslationLanguage = {
|
||||
switch_to_light_theme: '切换到浅色主题',
|
||||
switch_to_system_theme: '切换到系统主题',
|
||||
search: '搜索',
|
||||
skip_to_content: '跳转到内容',
|
||||
clear: '清除',
|
||||
table_filtered_by_selection: '符合选择:${1}',
|
||||
table_clear_selection: '清除选择',
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { isComposingKeyEvent } from './keyboard';
|
||||
|
||||
describe('isComposingKeyEvent', () => {
|
||||
it('is false for a regular Enter', () => {
|
||||
expect(isComposingKeyEvent({ nativeEvent: { isComposing: false, keyCode: 13 } })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('is true while composing', () => {
|
||||
expect(isComposingKeyEvent({ nativeEvent: { isComposing: true, keyCode: 13 } })).toBe(true);
|
||||
});
|
||||
|
||||
it('is true for the Safari confirming keydown', () => {
|
||||
expect(isComposingKeyEvent({ nativeEvent: { isComposing: false, keyCode: 229 } })).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Whether a keydown belongs to an in-progress IME composition and must not trigger a submit.
|
||||
*/
|
||||
export function isComposingKeyEvent(event: {
|
||||
nativeEvent: Pick<KeyboardEvent, 'isComposing' | 'keyCode'>;
|
||||
}): boolean {
|
||||
// Safari fires `compositionend` before the confirming keydown, so `isComposing` is already
|
||||
// false there; keyCode 229 is the only remaining signal.
|
||||
return event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
|
||||
}
|
||||
@@ -54,7 +54,8 @@ function makeContext(
|
||||
adaptiveContent: { enabled: adaptiveContent },
|
||||
urls: { login: `https://${host}/~gitbook/auth/login` },
|
||||
},
|
||||
siteSpace: { urls: { published: `https://${host}${siteBasePath}` } },
|
||||
siteSpace: { space: {}, urls: { published: `https://${host}${siteBasePath}` } },
|
||||
structure: { type: 'siteSpaces', structure: [] },
|
||||
customization: {
|
||||
ai: { mode: aiMode },
|
||||
pageActions: { items: pageActions },
|
||||
|
||||
@@ -3,8 +3,14 @@ import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
||||
import type { ZodRawShape } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AgentFeedbackSource, SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
import {
|
||||
AgentFeedbackSource,
|
||||
SiteInsightsDisplayContext,
|
||||
type SiteSpace,
|
||||
type SiteStructure,
|
||||
} from '@gitbook/api';
|
||||
|
||||
import type { SearchSiteContentScope } from '@/components/Search/search-types';
|
||||
import { resolveSitePageFromURL } from '@/components/SitePage/fetch';
|
||||
import { isAIEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
import {
|
||||
@@ -18,9 +24,16 @@ import { renderAskSourcesMarkdown, streamSiteAskAnswer } from '@/lib/ask';
|
||||
import { getAskVisitorAgent } from '@/lib/ask-visitor';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getExposableError, throwIfDataError } from '@/lib/data';
|
||||
import { getLogger } from '@/lib/logger';
|
||||
import { fromPageMarkdown, getMarkdownForPageInSpace, toPageMarkdown } from '@/lib/markdownPage';
|
||||
import { joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { findSiteSpaceBy } from '@/lib/sites';
|
||||
import {
|
||||
findSiteSpaceBy,
|
||||
getSiteSpaceLanguages,
|
||||
getSiteStructureSections,
|
||||
listAllSiteSpaces,
|
||||
normalizeLanguage,
|
||||
} from '@/lib/sites';
|
||||
import { trackServerInsightsEvents } from '@/lib/tracking';
|
||||
import { waitUntil } from '@/lib/waitUntil';
|
||||
|
||||
@@ -54,6 +67,8 @@ export function createSiteMcpTools(
|
||||
const { dataFetcher, linker, site } = context;
|
||||
const siteUrl = context.siteSpace.urls.published;
|
||||
const examplePageURL = siteUrl ? joinPathWithBaseURL(siteUrl, 'getting-started') : undefined;
|
||||
const endpointLanguage = normalizeLanguage(context.siteSpace.space.language);
|
||||
const siteLanguages = getSiteSpaceLanguages(listAllSiteSpaces(context.structure));
|
||||
|
||||
const tools: SiteMcpTool[] = [
|
||||
defineTool({
|
||||
@@ -61,6 +76,7 @@ export function createSiteMcpTools(
|
||||
description: `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.`,
|
||||
inputSchema: {
|
||||
query: z.string(),
|
||||
...languageArgument(siteLanguages, endpointLanguage),
|
||||
},
|
||||
annotations: {
|
||||
title: 'Search documentation',
|
||||
@@ -69,16 +85,27 @@ export function createSiteMcpTools(
|
||||
idempotentHint: true,
|
||||
openWorldHint: true,
|
||||
},
|
||||
handler: async ({ query }) => {
|
||||
handler: async ({ query, language }) => {
|
||||
// zod can't type `language`, since only multi-language sites declare it.
|
||||
const searchLanguage = typeof language === 'string' ? language : endpointLanguage;
|
||||
const results = await throwIfDataError(
|
||||
dataFetcher.searchSiteContent({
|
||||
organizationId: context.organizationId,
|
||||
siteId: site.id,
|
||||
query,
|
||||
scope: { mode: 'all' },
|
||||
scope: getLanguageSearchScope(context.structure, searchLanguage),
|
||||
})
|
||||
);
|
||||
|
||||
if (siteLanguages.length > 1) {
|
||||
const labels = {
|
||||
siteId: site.id,
|
||||
language: searchLanguage,
|
||||
languageSource: language ? 'argument' : 'endpoint',
|
||||
};
|
||||
getLogger().subLogger('mcp', { labels }).info('searchDocumentation language');
|
||||
}
|
||||
|
||||
trackMcpEvent({
|
||||
organizationId: context.organizationId,
|
||||
siteId: site.id,
|
||||
@@ -413,6 +440,66 @@ export function createSiteMcpTools(
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* The optional `language` argument of the search tool, only on sites with several languages.
|
||||
*/
|
||||
function languageArgument(languages: string[], defaultLanguage: string) {
|
||||
const [first, ...rest] = languages;
|
||||
if (!first || rest.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
language: z
|
||||
.enum([first, ...rest])
|
||||
.optional()
|
||||
// A language the site doesn't have searches English rather than failing the call.
|
||||
.catch('en')
|
||||
.describe(
|
||||
`The language the user is writing in. Pass it whenever you know it, so results come from the documentation in that language. Defaults to "${defaultLanguage}".`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search scope covering the site spaces in one language.
|
||||
*/
|
||||
function getLanguageSearchScope(
|
||||
structure: SiteStructure,
|
||||
language: string
|
||||
): SearchSiteContentScope {
|
||||
const sections =
|
||||
structure.type === 'sections'
|
||||
? getSiteStructureSections(structure, { ignoreGroups: true }).map(
|
||||
(section) => section.siteSpaces
|
||||
)
|
||||
: [structure.structure];
|
||||
const siteSpaces = sections.flatMap((variants) => getVariantsInLanguage(variants, language));
|
||||
|
||||
if (siteSpaces.length === listAllSiteSpaces(structure).length) {
|
||||
return { mode: 'all' };
|
||||
}
|
||||
|
||||
return { mode: 'specific', siteSpaceIds: siteSpaces.map((siteSpace) => siteSpace.id) };
|
||||
}
|
||||
|
||||
/**
|
||||
* The variants of a section in the given language, or in its default variant's language if it has none.
|
||||
*/
|
||||
function getVariantsInLanguage(variants: SiteSpace[], language: string): SiteSpace[] {
|
||||
const matching = variants.filter((variant) => getLanguage(variant) === language);
|
||||
if (matching.length > 0) {
|
||||
return matching;
|
||||
}
|
||||
|
||||
const fallback = variants.find((variant) => variant.default) ?? variants[0];
|
||||
return variants.filter((variant) => fallback && getLanguage(variant) === getLanguage(fallback));
|
||||
}
|
||||
|
||||
function getLanguage(siteSpace: SiteSpace): string {
|
||||
return normalizeLanguage(siteSpace.space.language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase a tool's argument shape so differently-shaped tools can share one array.
|
||||
*/
|
||||
|
||||
@@ -590,7 +590,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
response.headers.set('content-security-policy', getContentSecurityPolicy());
|
||||
// Basic security headers
|
||||
response.headers.set('strict-transport-security', 'max-age=31536000');
|
||||
response.headers.set('referrer-policy', 'no-referrer-when-downgrade');
|
||||
response.headers.set('referrer-policy', 'strict-origin-when-cross-origin');
|
||||
response.headers.set('x-content-type-options', 'nosniff');
|
||||
// Debug header
|
||||
response.headers.set('x-gitbook-route-type', routeType);
|
||||
|
||||
@@ -341,6 +341,7 @@ const config: Config = {
|
||||
heightOut: 'heightOut 500ms ease both',
|
||||
crawl: 'crawl 2s ease-in-out infinite',
|
||||
},
|
||||
// The enter/exit keyframes live in globals.css, next to their reduced-motion variants.
|
||||
keyframes: {
|
||||
bounceSmall: {
|
||||
'0%, 100%': {
|
||||
@@ -374,16 +375,6 @@ const config: Config = {
|
||||
},
|
||||
},
|
||||
|
||||
present: {
|
||||
from: {
|
||||
opacity: '0',
|
||||
transform: 'translateY(1rem) scale(90%)',
|
||||
},
|
||||
to: {
|
||||
opacity: '1',
|
||||
transform: 'translateY(0) scale(1)',
|
||||
},
|
||||
},
|
||||
rotateLoop: {
|
||||
'0%': {
|
||||
transform: 'rotate(0deg) scaleY(2.5) scaleX(2.5)',
|
||||
@@ -477,64 +468,6 @@ const config: Config = {
|
||||
transform: 'translateY(-10%)',
|
||||
},
|
||||
},
|
||||
enterFromRight: {
|
||||
from: { opacity: '0', transform: 'translateX(50%)', display: 'none' },
|
||||
to: { opacity: '1', transform: 'translateX(0)', display: 'flex' },
|
||||
},
|
||||
enterFromLeft: {
|
||||
from: { opacity: '0', transform: 'translateX(-50%)', display: 'none' },
|
||||
to: { opacity: '1', transform: 'translateX(0)', display: 'flex' },
|
||||
},
|
||||
exitToRight: {
|
||||
from: { opacity: '1', transform: 'translateX(0)', display: 'flex' },
|
||||
to: { opacity: '0', transform: 'translateX(50%)', display: 'none' },
|
||||
},
|
||||
exitToLeft: {
|
||||
from: { opacity: '1', transform: 'translateX(0)', display: 'flex' },
|
||||
to: { opacity: '0', transform: 'translateX(-50%)', display: 'none' },
|
||||
},
|
||||
scaleIn: {
|
||||
from: { opacity: '0', transform: 'rotateX(-10deg) scale(0.9)' },
|
||||
to: { opacity: '1', transform: 'rotateX(0deg) scale(1)' },
|
||||
},
|
||||
scaleOut: {
|
||||
from: { opacity: '1', transform: 'rotateX(0deg) scale(1)' },
|
||||
to: { opacity: '0', transform: 'rotateX(-10deg) scale(0.95)' },
|
||||
},
|
||||
blurIn: {
|
||||
'0%': { filter: 'blur(6px)', opacity: '0', transform: 'scale(0.95)' },
|
||||
'100%': { filter: 'blur(0px)', opacity: '1', transform: 'scale(1)' },
|
||||
},
|
||||
blurOut: {
|
||||
'0%': { filter: 'blur(0px)', opacity: '1', transform: 'scale(1)' },
|
||||
'100%': { filter: 'blur(6px)', opacity: '0', transform: 'scale(0.95)' },
|
||||
},
|
||||
blurInDisplay: {
|
||||
'0%': {
|
||||
filter: 'blur(6px)',
|
||||
opacity: '0',
|
||||
transform: 'scale(0.95)',
|
||||
display: 'none',
|
||||
},
|
||||
'100%': {
|
||||
filter: 'blur(0px)',
|
||||
opacity: '1',
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
},
|
||||
blurOutDisplay: {
|
||||
'0%': {
|
||||
filter: 'blur(0px)',
|
||||
opacity: '1',
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
to: {
|
||||
filter: 'blur(6px)',
|
||||
opacity: '0',
|
||||
transform: 'scale(0.95)',
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
fadeOut: {
|
||||
from: { opacity: '1' },
|
||||
to: { opacity: '0' },
|
||||
|
||||
@@ -33,6 +33,40 @@ it(
|
||||
{ timeout: 10_000 }
|
||||
);
|
||||
|
||||
it(
|
||||
'should search the language of the MCP endpoint unless the client passes one',
|
||||
async () => {
|
||||
const client = await connectMCPClient(
|
||||
getContentTestURL('https://gitbook.com/docs/~gitbook/mcp')
|
||||
);
|
||||
const translatedLink = /\/documentation\/(fr|ja|zh)(\/|$)/;
|
||||
|
||||
async function searchLinks(args: { query: string; language?: string }) {
|
||||
const response = await client.callTool({
|
||||
name: 'searchDocumentation',
|
||||
arguments: args,
|
||||
});
|
||||
const content = response.content as { text: string }[];
|
||||
return content.map((item) => item.text.match(/^Link: (.+)$/m)?.[1] ?? '');
|
||||
}
|
||||
|
||||
const endpointLinks = await searchLinks({ query: 'table' });
|
||||
expect(endpointLinks.length).toBeGreaterThan(0);
|
||||
expect(endpointLinks.filter((link) => translatedLink.test(link))).toEqual([]);
|
||||
|
||||
const frenchLinks = await searchLinks({ query: 'table', language: 'fr' });
|
||||
expect(frenchLinks.some((link) => link.includes('/documentation/fr/'))).toBe(true);
|
||||
expect(frenchLinks.filter((link) => /\/documentation\/(ja|zh)(\/|$)/.test(link))).toEqual(
|
||||
[]
|
||||
);
|
||||
|
||||
const unknownLanguageLinks = await searchLinks({ query: 'table', language: 'de' });
|
||||
expect(unknownLanguageLinks.length).toBeGreaterThan(0);
|
||||
expect(unknownLanguageLinks.filter((link) => translatedLink.test(link))).toEqual([]);
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
);
|
||||
|
||||
it(
|
||||
'should get a page from another site space through MCP',
|
||||
async () => {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/icons
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 265ca4b: Add a skip link, keyboard navigation between content tabs, focusable table of contents toggles, and hide decorative icons from assistive technologies.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
"@gitbook/fontawesome-pro": "1.0.23"
|
||||
|
||||
@@ -53,11 +53,14 @@ export const Icon = React.forwardRef(function Icon(
|
||||
const [iconStyle, icon] = getIconStyle(propIconStyle, propIcon);
|
||||
const source = context.iconSources?.[getInlineIconSourceKey(iconStyle, icon)];
|
||||
const maskId = React.useId();
|
||||
// Icons are decorative unless the caller names them; the control they sit in carries the name.
|
||||
const isDecorative = !rest['aria-label'] && !rest['aria-labelledby'] && !rest.role;
|
||||
|
||||
if (source) {
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
aria-hidden={isDecorative ? true : undefined}
|
||||
{...rest}
|
||||
viewBox={source.viewBox}
|
||||
fill="currentColor"
|
||||
@@ -77,6 +80,7 @@ export const Icon = React.forwardRef(function Icon(
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
aria-hidden={isDecorative ? true : undefined}
|
||||
{...rest}
|
||||
style={{
|
||||
...(size ? { width: size, height: size } : {}),
|
||||
@@ -84,7 +88,6 @@ export const Icon = React.forwardRef(function Icon(
|
||||
}}
|
||||
className={`gb-icon ${className}`}
|
||||
>
|
||||
<title>{icon}</title>
|
||||
<defs>
|
||||
<mask
|
||||
id={maskId}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @gitbook/react-contentkit
|
||||
|
||||
## 0.7.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [265ca4b]
|
||||
- @gitbook/icons@0.5.1
|
||||
|
||||
## 0.7.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.19",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
Reference in New Issue
Block a user