mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4228625852 | |||
| e9c8ec79d2 | |||
| 6ede6ee7c6 | |||
| 7a11861d36 | |||
| 10754fbaf4 | |||
| 8f32f4fb1e | |||
| 717ad0b300 | |||
| a3eed52f24 | |||
| 27a34b7ee2 | |||
| bf3ad28c7f | |||
| 70c2e90e4b | |||
| d0a378cdb6 | |||
| 81f8ddcf27 | |||
| 9d6457f814 | |||
| 07c02ebcd2 | |||
| 4d3172d13f | |||
| 0aa34cc9a8 | |||
| 551d30cfa4 | |||
| 844a0a8ea7 | |||
| eb8ba66a66 | |||
| d738e118a0 | |||
| c16d08f5d5 | |||
| 61ec9b25b9 | |||
| b57561d395 | |||
| e00d1c5bbd | |||
| 9063b56c89 | |||
| 020d3668d7 | |||
| 4d30cbf058 | |||
| d3f6c38f05 | |||
| 6f1db32682 | |||
| 4ad6b2c067 | |||
| 07449443a0 | |||
| b09cc64fec | |||
| 1eed2004f6 | |||
| 91539b8187 | |||
| 16cfde87ee | |||
| 58b4a978ff | |||
| e34be0e89b | |||
| e60c754343 | |||
| 48ca21e471 | |||
| 3ba9e46f2b | |||
| 58f0cc8287 | |||
| 29e2b22b63 | |||
| 3c7cb09c1b | |||
| 0f1cd6745d | |||
| e90f57b138 | |||
| 7bd5939247 | |||
| 89de966e7a | |||
| 4e2d86318e | |||
| f647af2003 | |||
| 96e24a1f84 | |||
| 2a0f37f4b3 | |||
| 17d935dd4c | |||
| df07fbe242 | |||
| 1e53376151 | |||
| f294818775 | |||
| eed386a133 | |||
| e172b25ba8 | |||
| f7f6f9d06d | |||
| 9e01676fe9 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
OpenAPI styling tweaks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Prevent form submission during IME composition
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Support code themes customization
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add sidesheet component, use it for TOC and AIChat
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Set <html lang> based on translated language
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Support social links
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/embed": patch
|
||||
---
|
||||
|
||||
Fix signed JWT token usage
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Fix OpenAPI example not showing
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Serve a markdown version of the page when the "Accept" header contains "text/markdown".
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/openapi-parser": patch
|
||||
---
|
||||
|
||||
Update scalar dependencies.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Support `defaultExpanded` in Expandable block
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Improve OpenAPI server URL validation
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Header & TOC tweaks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix ordered list item index calculation
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Fix Scalar Try it button justification
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix rendering of images in reusable content from an external space with the new dereferencing in the API.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Grow InlineActionButton to take available width on line
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/icons": patch
|
||||
---
|
||||
|
||||
Simplify installation by using public fontawesome mirror.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gitbook/browser-types": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add support for custom cookie banner registration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Better handle huge section groups
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"@gitbook/embed": patch
|
||||
---
|
||||
|
||||
Fix Embed options in React
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Add OpenAPI 3.1 nullable array support
|
||||
@@ -5,8 +5,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- changeset-release/main
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -36,13 +34,10 @@ jobs:
|
||||
env:
|
||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||
- run: bun unit
|
||||
build-oss:
|
||||
# CI to check that the repository builds correctly on a machine without the credentials
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build (Open Source)
|
||||
name: Build
|
||||
timeout-minutes: 6
|
||||
env:
|
||||
BUN_NPM_TOKEN: ''
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
name: Preview
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
approval:
|
||||
# Ensure that external contributors' PRs are manually approved before deployment
|
||||
name: Approve Preview Deployment
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.fork }}
|
||||
environment:
|
||||
name: preview-approval
|
||||
steps:
|
||||
- name: Approval gate
|
||||
run: echo "Preview deployment approved."
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (preview)
|
||||
runs-on: ubuntu-latest
|
||||
needs: approval
|
||||
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
environment:
|
||||
name: 2v-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
@@ -18,6 +28,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Deploy to Vercel
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-vercel
|
||||
@@ -31,6 +43,8 @@ jobs:
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (preview)
|
||||
runs-on: ubuntu-latest
|
||||
needs: approval
|
||||
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
|
||||
environment:
|
||||
name: 2c-preview
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
@@ -39,6 +53,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Deploy to Cloudflare
|
||||
id: deploy
|
||||
uses: ./.github/composite/deploy-cloudflare
|
||||
@@ -65,7 +81,7 @@ jobs:
|
||||
comment-deployments:
|
||||
runs-on: ubuntu-latest
|
||||
name: Comment Deployments (preview)
|
||||
if: always() && !startsWith(github.ref, 'refs/heads/main')
|
||||
if: always() && github.event_name != 'push'
|
||||
needs:
|
||||
- deploy-v2-vercel
|
||||
- deploy-v2-cloudflare
|
||||
@@ -102,10 +118,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2
|
||||
needs: deploy-v2-vercel
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -123,10 +142,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -144,10 +166,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2
|
||||
needs: deploy-v2-vercel
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -165,10 +190,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -186,9 +214,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Browserless Testing v2 (Vercel)
|
||||
needs: deploy-v2-vercel
|
||||
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Setup Bun
|
||||
uses: ./.github/composite/setup-bun
|
||||
- name: Install dependencies
|
||||
@@ -217,4 +248,4 @@ jobs:
|
||||
# run: cd ./packages/gitbook && bun e2e-browserless
|
||||
# env:
|
||||
# BASE_URL: ${{needs.deploy-v2-cloudflare.outputs.deployment-url}}
|
||||
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
|
||||
|
||||
@@ -3,8 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (production)
|
||||
|
||||
@@ -3,8 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
jobs:
|
||||
deploy-v2-vercel:
|
||||
name: Deploy v2 to Vercel (staging)
|
||||
|
||||
@@ -4,9 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
|
||||
@@ -77,23 +77,7 @@ All pull-requests will be tested against both visual and performances testing to
|
||||
|
||||
## Fonts and Icons
|
||||
|
||||
GitBook Open uses fontawesome. During development, your local environment will use the free version. However, only the pro version will be accepted by CI. If you see the following error:
|
||||
|
||||
```
|
||||
The GitBook icon is missing. It indicates that the dependencies were installed without the correct font-awesome package. These changes have probably been persisted in the Bun lockfile. Read the README for more information.
|
||||
```
|
||||
|
||||
It means that you've changed the GBO dependencies and bundled in the free version. Only GitBook staff can help with this - if you're not on the GitBook team, please ping us in the PR and we'll help get things moving.
|
||||
|
||||
If you are GitBook staff, you'll need our NPM token in your local environment.
|
||||
|
||||
```
|
||||
.env.local
|
||||
|
||||
BUN_NPM_TOKEN=xxx
|
||||
```
|
||||
|
||||
and then reinstall dependencies.
|
||||
GitBook Open uses fontawesome. For self-hosting and local development, for licensing reasons, only the icons from the free version should be used.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.29.8",
|
||||
"turbo": "^2.7.4",
|
||||
"turbo": "^2.7.6",
|
||||
"vercel": "^39.4.2",
|
||||
},
|
||||
},
|
||||
@@ -56,6 +56,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "catalog:",
|
||||
"bun-types": "catalog:",
|
||||
"react": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
@@ -156,7 +157,8 @@
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"next": "15.4.10",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "15.4.11",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -174,7 +176,7 @@
|
||||
"remark-rehype": "^11.1.1",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^3.2.0",
|
||||
"shiki": "^3.21.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.5",
|
||||
@@ -195,6 +197,7 @@
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/negotiator": "^0.6.4",
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
@@ -222,8 +225,8 @@
|
||||
"gitbook-icons": "./bin/gitbook-icons.js",
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@gitbook/fontawesome-pro": "1.0.16",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "catalog:",
|
||||
@@ -232,9 +235,6 @@
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@gitbook/fontawesome-pro": "1.0.16",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
},
|
||||
@@ -243,8 +243,8 @@
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"version": "3.0.7",
|
||||
"dependencies": {
|
||||
"@scalar/json-magic": "^0.8.10",
|
||||
"@scalar/openapi-parser": "^0.23.12",
|
||||
"@scalar/json-magic": "^0.9.4",
|
||||
"@scalar/openapi-parser": "^0.24.5",
|
||||
"@scalar/openapi-types": "^0.5.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -345,7 +345,7 @@
|
||||
"react-dom": "catalog:",
|
||||
},
|
||||
"catalog": {
|
||||
"@gitbook/api": "0.155.0",
|
||||
"@gitbook/api": "0.159.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
@@ -722,11 +722,9 @@
|
||||
|
||||
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.1.0", "", {}, "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA=="],
|
||||
|
||||
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@7.1.0", "", {}, "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA=="],
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.155.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-g4UfJRnej/GZ9pORHcS9mV0kKiJLRvhMlGOeOoEVU/LSIz6s35VDqauSBs7rH00AOaMABDg7uEko8tUSA4aaww=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.159.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-IRZXT2NAcOlKlCcGnzRH4w82AuBXam5I7lw84qfCRbNDDycvFNBwIVMHu3JMOcGUTCkzj7QDElaNKYirsWDwFQ=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
@@ -872,7 +870,7 @@
|
||||
|
||||
"@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@15.4.10", "", {}, "sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg=="],
|
||||
"@next/env": ["@next/env@15.4.11", "", {}, "sha512-mIYp/091eYfPFezKX7ZPTWqrmSXq+ih6+LcUyKvLmeLQGhlPtot33kuEOd4U+xAA7sFfj21+OtCpIZx0g5SpvQ=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A=="],
|
||||
|
||||
@@ -1286,23 +1284,23 @@
|
||||
|
||||
"@scalar/draggable": ["@scalar/draggable@0.2.0", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-UetHRB5Bqo5egVYlS21roWBcICmyk8CKh2htsidO+bFGAjl2e7Te+rY0borhNrMclr0xezHlPuLpEs1dvgLS2g=="],
|
||||
|
||||
"@scalar/helpers": ["@scalar/helpers@0.2.6", "", {}, "sha512-A471YFBCj7ZOlGIkAYnU8oYgeyts82ZNX+4UicrlmKv3eAQ+kwboN3Dy0R6u1lcA/+I/zzeXi/fBObsT7P9qTA=="],
|
||||
"@scalar/helpers": ["@scalar/helpers@0.2.9", "", {}, "sha512-Y4ffJF0yELdwZ0BKgonqn3SumIgRn1WKyYCVHD+TDM7qRFChdGRypyt20+efHs26fmJeyBAIIv2laICj5uimiw=="],
|
||||
|
||||
"@scalar/icons": ["@scalar/icons@0.4.7", "", { "dependencies": { "@phosphor-icons/core": "^2.1.1", "@types/node": "^22.9.0", "chalk": "^5.4.1", "vue": "^3.5.17" } }, "sha512-0qXPGRdZ180TMfejWCPYy7ILszBrAraq4KBhPtcM12ghc5qkncFWWpTm5yXI/vrbm10t7wvtTK08CLZ36CnXlQ=="],
|
||||
|
||||
"@scalar/import": ["@scalar/import@0.4.31", "", { "dependencies": { "@scalar/helpers": "0.0.12", "@scalar/openapi-parser": "0.22.3", "yaml": "2.8.0" } }, "sha512-3+3dHz+EvS/C4YEQ8eEihZFVxRVZODsMEHsp48tvXTaIs1Qxb4kf3Nkaf9E3k+6rpM2CUrF59K1P8LvhXob48w=="],
|
||||
|
||||
"@scalar/json-magic": ["@scalar/json-magic@0.8.10", "", { "dependencies": { "@scalar/helpers": "0.2.6", "yaml": "^2.8.0" } }, "sha512-TWdKQ/hcy4erFQDp2MVlFoPesFep2VY96Q69cjLHmx5hxM0ZUBfmNB4lA8Uh3klgx5JmCDfSNIGjPFIpxlosUw=="],
|
||||
"@scalar/json-magic": ["@scalar/json-magic@0.9.4", "", { "dependencies": { "@scalar/helpers": "0.2.9", "yaml": "^2.8.0" } }, "sha512-PyfyWrH4ZkW0TM1ColiiHj4NRF8hUM61H0UzAkHLhRNnKFxi6hI+oqNrwqPnyk93hrpkpTRHC7Fl5T0BRwuzVg=="],
|
||||
|
||||
"@scalar/oas-utils": ["@scalar/oas-utils@0.6.3", "", { "dependencies": { "@hyperjump/browser": "^1.1.0", "@hyperjump/json-schema": "^1.9.6", "@scalar/helpers": "0.1.1", "@scalar/json-magic": "0.8.1", "@scalar/object-utils": "1.2.11", "@scalar/openapi-types": "0.5.1", "@scalar/themes": "0.13.23", "@scalar/types": "0.4.0", "@scalar/workspace-store": "0.20.0", "@types/har-format": "^1.2.15", "flatted": "^3.3.3", "js-base64": "^3.7.8", "microdiff": "^1.5.0", "nanoid": "5.1.5", "type-fest": "5.0.0", "yaml": "2.8.0", "zod": "4.1.11" } }, "sha512-+hEMZSLuTeXlrPqpajusZn2R8qs/OrH2Xstllr3GOCnJz7ATwniD69DT7Il49L1j2HJEtX4EpleRX9OMhdoWHg=="],
|
||||
|
||||
"@scalar/object-utils": ["@scalar/object-utils@1.2.11", "", { "dependencies": { "@scalar/helpers": "0.1.1", "flatted": "^3.3.3", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1", "type-fest": "5.0.0" } }, "sha512-V44CbfjNuYhoVmVZ36DDlTfyBNF8dMYzLTcDz5zvoSsx+SViXE8ScMd+kgUMHCtSJZV6izV2x3UzIbGQY/pWsQ=="],
|
||||
|
||||
"@scalar/openapi-parser": ["@scalar/openapi-parser@0.23.12", "", { "dependencies": { "@scalar/json-magic": "0.8.10", "@scalar/openapi-types": "0.5.3", "@scalar/openapi-upgrader": "0.1.6", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.8.0" } }, "sha512-Znq9J8sXUe20YdbK2myoyeFNXgWTeZlyQUJeQu+wNsXAfWHGT68bF7aZzl99TsPHHVl3WmKDeSrWiuexfJ1u6A=="],
|
||||
"@scalar/openapi-parser": ["@scalar/openapi-parser@0.24.5", "", { "dependencies": { "@scalar/json-magic": "0.9.4", "@scalar/openapi-types": "0.5.3", "@scalar/openapi-upgrader": "0.1.8", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.8.0" } }, "sha512-pTeKnmhVdSIfG3vysgDm6jsKc7Do1vXdy/4aqp7j8AEzXllf8RZjSgRSUhtvFYFQCr27fDZ117V3WPQUYtgmCw=="],
|
||||
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.5.3", "", { "dependencies": { "zod": "^4.1.11" } }, "sha512-m4n/Su3K01d15dmdWO1LlqecdSPKuNjuokrJLdiQ485kW/hRHbXW1QP6tJL75myhw/XhX5YhYAR+jrwnGjXiMw=="],
|
||||
|
||||
"@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.1.6", "", { "dependencies": { "@scalar/openapi-types": "0.5.3" } }, "sha512-XdrNZUr0ASLfR89OS2zP6enbq9f7UGQQxov+a3WF1Wz9DClniAL2ChJ2fbGOrqL5F2kjbV6Fw/iO3bsBTMyLZA=="],
|
||||
"@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.1.8", "", { "dependencies": { "@scalar/openapi-types": "0.5.3" } }, "sha512-2xuYLLs0fBadLIk4I1ObjMiCnOyLPEMPf24A1HtHQvhKGDnGlvT63F2rU2Xw8lxCjgHnzveMPnOJEbwIy64RCg=="],
|
||||
|
||||
"@scalar/postman-to-openapi": ["@scalar/postman-to-openapi@0.3.40", "", { "dependencies": { "@scalar/helpers": "0.0.12", "@scalar/oas-utils": "0.5.2", "@scalar/openapi-types": "0.5.0" } }, "sha512-FP1p2/mb0Y5GM9hc+TI9ldDM44VV9GHwdhJQ8xGpArtlt8nxtyKmncOXcgayBD7qk3ohV6W1Eftsr258Eq7gGQ=="],
|
||||
|
||||
@@ -1322,17 +1320,17 @@
|
||||
|
||||
"@scalar/workspace-store": ["@scalar/workspace-store@0.20.0", "", { "dependencies": { "@scalar/code-highlight": "0.2.0", "@scalar/helpers": "0.1.1", "@scalar/json-magic": "0.8.1", "@scalar/openapi-upgrader": "0.1.4", "@scalar/snippetz": "0.5.2", "@scalar/themes": "0.13.23", "@scalar/typebox": "0.1.1", "@scalar/types": "0.4.0", "github-slugger": "^2.0.0", "type-fest": "5.0.0", "vue": "^3.5.21", "yaml": "2.8.0" } }, "sha512-RklQSKjz5V/69rKjxQV70q4u34qzmUZVtWGUJLVvHxPsPxCtxPPRLYY+gdQDkGjTQYol/dR9xyvi2QGk3OWTKA=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-+5dPz8q6HgNqfQ28ycm/vA8dIVd2lNFOUqVRFCQLbs0KZ6emYI+1apLpX+wuL/aDSPLOkMgARwNjkA5UjGKS1Q=="],
|
||||
"@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.1.0" } }, "sha512-1WrYfaz5YT5aTAIMbYQhxlSHc8ArX+hCDNAIdKRqJHzfWQ3xDgh3PTvrAly+RWGuvi5Q4NlvPlTBdlSAXN6Stg=="],
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-3V7ko+YUAP02I4rUbDjCgvyM/H85hUIZBQAS19FjDcJMKL5SbjWTiG7TRKxX1V4ddxLxt2RO64wZinElp/3ngQ=="],
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0" } }, "sha512-Qze5YIsp223AmC69VZDQolcrcYPrVa9wV6cW2kVqsDrSWlwhW2EQZEn1Iw2oQU1tGYVg8Hj/xdp8mOv+9zI0vg=="],
|
||||
"@shikijs/langs": ["@shikijs/langs@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@3.2.0", "", { "dependencies": { "@shikijs/types": "3.2.0" } }, "sha512-XfzMSTu6iMl2FZIwKykld2OzFKDDlm4KbZrzW6sbKXEeJ1xq61HX4x4bE4+REBFqbbrvAQM8EAH11m/E3cxYDg=="],
|
||||
"@shikijs/themes": ["@shikijs/themes@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@3.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-1uOwfEO0vV+G8n/AO/6Yth7zshNdXvQ1pc4ygTrfE3cyuzVLukrZq72YkFUlsRijam7LvRTvnqL4aT5wx1X2Vw=="],
|
||||
"@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
@@ -1532,6 +1530,8 @@
|
||||
|
||||
"@types/ms": ["@types/ms@0.7.34", "", {}, "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="],
|
||||
|
||||
"@types/negotiator": ["@types/negotiator@0.6.4", "", {}, "sha512-elf6BsTq+AkyNsb2h5cGNst2Mc7dPliVoAPm1fXglC/BM3f2pFA40BaSSv3E5lyHteEawVKLP+8TwiY1DMNb3A=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
|
||||
@@ -1898,8 +1898,6 @@
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
|
||||
|
||||
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
@@ -2510,7 +2508,7 @@
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"next": ["next@15.4.10", "", { "dependencies": { "@next/env": "15.4.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.8", "@next/swc-darwin-x64": "15.4.8", "@next/swc-linux-arm64-gnu": "15.4.8", "@next/swc-linux-arm64-musl": "15.4.8", "@next/swc-linux-x64-gnu": "15.4.8", "@next/swc-linux-x64-musl": "15.4.8", "@next/swc-win32-arm64-msvc": "15.4.8", "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "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-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ=="],
|
||||
"next": ["next@15.4.11", "", { "dependencies": { "@next/env": "15.4.11", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.8", "@next/swc-darwin-x64": "15.4.8", "@next/swc-linux-arm64-gnu": "15.4.8", "@next/swc-linux-arm64-musl": "15.4.8", "@next/swc-linux-x64-gnu": "15.4.8", "@next/swc-linux-x64-musl": "15.4.8", "@next/swc-win32-arm64-msvc": "15.4.8", "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "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-IJRyXal45mIsshZI5XJne/intjusslUP1F+FHVBIyMGEqbYtIq1Irdx5vdWBBg58smviPDycmDeV6txsfkv1RQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -2552,9 +2550,9 @@
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.5.4", "", {}, "sha512-yNxcQ8sKvURiTwP0mV6bLQCYE7NKfKRRWunhbZnXgxSmB1OXa1lHrN3o4DZd+0Si0kU5blidK7BcROO8qv5TZA=="],
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.1.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "oniguruma-parser": "^0.5.4", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-SNwG909cSLo4vPyyPbU/VJkEc9WOXqu2ycBlfd1UCXLqk1IijcQktSBb2yRQ2UFPsDhpkaf+C1dtT3PkLK/yWA=="],
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
|
||||
|
||||
"openapi-fetch": ["openapi-fetch@0.13.5", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-AQK8T9GSKFREFlN1DBXTYsLjs7YV2tZcJ7zUWxbjMoQmj8dDSFRrzhLCbHPZWA1TMV3vACqfCxLEZcwf2wxV6Q=="],
|
||||
|
||||
@@ -2774,7 +2772,7 @@
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.1", "", {}, "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA=="],
|
||||
|
||||
"shiki": ["shiki@3.2.0", "", { "dependencies": { "@shikijs/core": "3.2.0", "@shikijs/engine-javascript": "3.2.0", "@shikijs/engine-oniguruma": "3.2.0", "@shikijs/langs": "3.2.0", "@shikijs/themes": "3.2.0", "@shikijs/types": "3.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lOF6wkvZCRVQrdfGilyXclTKIjCWKujPAjD6fddLwtQ6eSmgj43pFDbjUmxivtElDRlsGO8G2dLeeRpwNY4wWg=="],
|
||||
"shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="],
|
||||
|
||||
"side-channel": ["side-channel@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" } }, "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA=="],
|
||||
|
||||
@@ -2908,19 +2906,19 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"turbo": ["turbo@2.7.4", "", { "optionalDependencies": { "turbo-darwin-64": "2.7.4", "turbo-darwin-arm64": "2.7.4", "turbo-linux-64": "2.7.4", "turbo-linux-arm64": "2.7.4", "turbo-windows-64": "2.7.4", "turbo-windows-arm64": "2.7.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-bkO4AddmDishzJB2ze7aYYPaejMoJVfS0XnaR6RCdXFOY8JGJfQE+l9fKiV7uDPa5Ut44gmOWJL3894CIMeH9g=="],
|
||||
"turbo": ["turbo@2.7.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.7.6", "turbo-darwin-arm64": "2.7.6", "turbo-linux-64": "2.7.6", "turbo-linux-arm64": "2.7.6", "turbo-windows-64": "2.7.6", "turbo-windows-arm64": "2.7.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-PO9AvJLEsNLO+EYhF4zB+v10hOjsJe5kJW+S6tTbRv+TW7gf1Qer4mfjP9h3/y9h8ZiPvOrenxnEgDtFgaM5zw=="],
|
||||
|
||||
"turbo-darwin-64": ["turbo-darwin-64@2.7.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xDR30ltfkSsRfGzABBckvl1nz1cZ3ssTujvdj+TPwOweeDRvZ0e06t5DS0rmRBvyKpgGs42K/EK6Mn2qLlFY9A=="],
|
||||
"turbo-darwin-64": ["turbo-darwin-64@2.7.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-bYu0qnWju2Ha3EbIkPCk1SMLT3sltKh1P/Jy5FER6BmH++H5z+T5MHh3W1Xoers9rk4N1VdKvog9FO1pxQyjhw=="],
|
||||
|
||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.7.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P7sjqXtOL/+nYWPvcDGWhi8wf8M8mZHHB8XEzw2VX7VJrS8IGHyJHGD1AYfDvhAEcr7pnk3gGifz3/xyhI655w=="],
|
||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.7.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KCxTf3Y1hgNLYIWRLw8bwH8Zie9RyCGoxAlXYsCBI/YNqBSR+ZZK9KYzFxAqDaVaNvTwLFv3rJRGsXOFWg4+Uw=="],
|
||||
|
||||
"turbo-linux-64": ["turbo-linux-64@2.7.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GofFOxRO/IhG8BcPyMSSB3Y2+oKQotsaYbHxL9yD6JPb20/o35eo+zUSyazOtilAwDHnak5dorAJFoFU8MIg2A=="],
|
||||
"turbo-linux-64": ["turbo-linux-64@2.7.6", "", { "os": "linux", "cpu": "x64" }, "sha512-vjoU8zIfNgvJR3cMitgw7inEoi6bmuVuFawDl5yKtxjAEhDktFdRBpGS3WojD4l3BklBbIK689ssXcGf21LxRA=="],
|
||||
|
||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.7.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+RQKgNjksVPxYAyAgmDV7w/1qj++qca+nSNTAOKGOfJiDtSvRKoci89oftJ6anGs00uamLKVEQ712TI/tfNAIw=="],
|
||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.7.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-TcMpBvTqZf+1DptrVYLbZls7WY1UVNDTGaf0bo7/GCgWYv5eZHCVo4Td7kCJeDU4glbXg67REX0md0S0V6ghMg=="],
|
||||
|
||||
"turbo-windows-64": ["turbo-windows-64@2.7.4", "", { "os": "win32", "cpu": "x64" }, "sha512-rfak1+g+ON3czs1mDYsCS4X74ZmK6gOgRQTXjDICtzvR4o61paqtgAYtNPofcVsMWeF4wvCajSeoAkkeAnQ1kg=="],
|
||||
"turbo-windows-64": ["turbo-windows-64@2.7.6", "", { "os": "win32", "cpu": "x64" }, "sha512-1/MhkYldiihjneY8QnnDMbAkHXn/udTWSVYS94EMlkE9AShozsLTTOT1gDOpX06EfEW5njP09suhMvxbvwuwpQ=="],
|
||||
|
||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.7.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-1ZgBNjNRbDu/fPeqXuX9i26x3CJ/Y1gcwUpQ+Vp7kN9Un6RZ9kzs164f/knrjcu5E+szCRexVjRSJay1k5jApA=="],
|
||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.7.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-0wDVnUJLFAWm4ZzOQFDkbyyUqaszorTGf3Rdc22IRIyJTTLd6ajqdb+cWD89UZ1RKr953+PZR1gqgWQY4PDuhA=="],
|
||||
|
||||
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[install.scopes]
|
||||
"gitbook" = { token = "$BUN_NPM_TOKEN", url = "https://registry.npmjs.org" }
|
||||
+3
-3
@@ -7,10 +7,10 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.29.8",
|
||||
"turbo": "^2.7.4",
|
||||
"turbo": "^2.7.6",
|
||||
"vercel": "^39.4.2"
|
||||
},
|
||||
"packageManager": "bun@1.3.2",
|
||||
"packageManager": "bun@1.3.7",
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@types/react": "catalog:",
|
||||
@@ -41,7 +41,7 @@
|
||||
"catalog": {
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@gitbook/api": "0.155.0",
|
||||
"@gitbook/api": "0.159.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
@@ -46,6 +46,11 @@ export type GitBookAssistant = {
|
||||
ui?: boolean;
|
||||
};
|
||||
|
||||
export type GitBookCookieBannerHandler = (options: {
|
||||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
}) => void;
|
||||
|
||||
export type GitBookGlobal = {
|
||||
/**
|
||||
* Register an event listener.
|
||||
@@ -72,6 +77,18 @@ export type GitBookGlobal = {
|
||||
* Register a custom assistant to be available on the site.
|
||||
*/
|
||||
registerAssistant: (assistant: GitBookAssistant) => () => void;
|
||||
|
||||
/**
|
||||
* Register a custom cookie banner handler.
|
||||
* When registered, the built-in cookie banner will not be displayed.
|
||||
*/
|
||||
registerCookieBanner: (handler: GitBookCookieBannerHandler) => void;
|
||||
|
||||
/**
|
||||
* Whether the user has granted cookie consent.
|
||||
* Returns true if cookies are granted, false if rejected or unknown.
|
||||
*/
|
||||
hasApprovedCookies: () => boolean;
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"react": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
@@ -33,6 +34,7 @@
|
||||
"build-lib": "tsdown",
|
||||
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"clean": "rm -rf ./dist",
|
||||
"unit": "bun test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { createGitBook } from './createGitBook';
|
||||
|
||||
describe('createGitBook.getFrameURL', () => {
|
||||
it('builds the embed URL when the site URL has no trailing slash', () => {
|
||||
const client = createGitBook({ siteURL: 'https://example.com/docs' });
|
||||
|
||||
const url = new URL(client.getFrameURL({}));
|
||||
|
||||
expect(url.origin).toBe('https://example.com');
|
||||
expect(url.pathname).toBe('/docs/~gitbook/embed');
|
||||
expect(url.searchParams.toString()).toBe('');
|
||||
});
|
||||
|
||||
it('handles a trailing slash and adds visitor parameters', () => {
|
||||
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
|
||||
|
||||
const url = new URL(
|
||||
client.getFrameURL({
|
||||
visitor: {
|
||||
token: 'signed-token',
|
||||
unsignedClaims: {
|
||||
role: 'editor',
|
||||
count: 3,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(url.pathname).toBe('/docs/~gitbook/embed');
|
||||
expect(url.searchParams.get('jwt_token')).toBe('signed-token');
|
||||
expect(url.searchParams.get('visitor.role')).toBe('editor');
|
||||
expect(url.searchParams.get('visitor.count')).toBe('3');
|
||||
expect(url.searchParams.get('visitor.enabled')).toBe('false');
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ export function createGitBook(options: CreateGitBookOptions) {
|
||||
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`;
|
||||
|
||||
if (frameOptions.visitor?.token) {
|
||||
url.searchParams.set('token', frameOptions.visitor.token);
|
||||
url.searchParams.set('jwt_token', frameOptions.visitor.token);
|
||||
}
|
||||
|
||||
if (frameOptions.visitor?.unsignedClaims) {
|
||||
|
||||
@@ -11,13 +11,21 @@ import { useGitBook } from './GitBookProvider';
|
||||
export type GitBookFrameProps = {
|
||||
className?: string;
|
||||
} & GetFrameURLOptions &
|
||||
GitBookEmbeddableConfiguration;
|
||||
Partial<GitBookEmbeddableConfiguration>;
|
||||
|
||||
/**
|
||||
* Render a frame with the GitBook Assistant in it.
|
||||
*/
|
||||
export function GitBookFrame(props: GitBookFrameProps) {
|
||||
const { className, visitor, actions, greeting, suggestions, tools } = props;
|
||||
const {
|
||||
className,
|
||||
visitor,
|
||||
actions = [],
|
||||
greeting,
|
||||
suggestions = [],
|
||||
tools = [],
|
||||
tabs = ['assistant', 'docs'],
|
||||
} = props;
|
||||
|
||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||
const gitbook = useGitBook();
|
||||
@@ -33,13 +41,13 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
|
||||
useEffect(() => {
|
||||
gitbookFrame?.configure({
|
||||
tabs: ['assistant', 'docs'],
|
||||
tabs,
|
||||
actions,
|
||||
greeting,
|
||||
suggestions,
|
||||
tools,
|
||||
});
|
||||
}, [gitbookFrame, actions, greeting, suggestions, tools]);
|
||||
}, [gitbookFrame, actions, greeting, suggestions, tools, tabs]);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": []
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { type TestsCase, getCustomizationURL, runTestCases } from './util';
|
||||
|
||||
const testCases: TestsCase[] = [
|
||||
{
|
||||
name: 'Cookie Banner',
|
||||
contentBaseURL: 'https://gitbook.com/docs/',
|
||||
tests: [
|
||||
{
|
||||
name: 'should show built-in banner when no custom banner is registered',
|
||||
url: getCustomizationURL({
|
||||
privacyPolicy: {
|
||||
url: 'https://policies.gitbook.com/privacy/cookies',
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
// Check that built-in banner is visible
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 5000 });
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should not show built-in banner when custom banner is registered',
|
||||
url: getCustomizationURL({
|
||||
privacyPolicy: {
|
||||
url: 'https://policies.gitbook.com/privacy/cookies',
|
||||
},
|
||||
}),
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
// Register a custom cookie banner handler
|
||||
await page.waitForFunction(() => {
|
||||
return typeof window !== 'undefined' && window.GitBook !== undefined;
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
window.GitBook?.registerCookieBanner(() => {
|
||||
// Custom cookie banner handler - no-op for testing to avoid reload
|
||||
});
|
||||
});
|
||||
|
||||
// Check that built-in banner is not visible
|
||||
const dialog = page.getByTestId('cookies-dialog');
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
runTestCases(testCases);
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CustomizationIconsStyle,
|
||||
CustomizationSidebarListStyle,
|
||||
CustomizationThemeMode,
|
||||
SiteSocialAccountPlatform,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookStandalone } from '@gitbook/embed';
|
||||
import { expect } from '@playwright/test';
|
||||
@@ -677,70 +678,6 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Markdown page',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Text page',
|
||||
url: 'text-page.md',
|
||||
screenshot: false,
|
||||
run: async (_page, response) => {
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(response?.headers()['content-type']).toContain('text/markdown');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'llms.txt',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'llms.txt',
|
||||
url: 'llms.txt',
|
||||
screenshot: false,
|
||||
run: async (_page, response) => {
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(response?.headers()['content-type']).toContain('text/markdown');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'llms-full.txt',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'llms-full.txt',
|
||||
url: 'llms-full.txt',
|
||||
screenshot: false,
|
||||
run: async (_page, response) => {
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(response?.headers()['content-type']).toContain('text/markdown');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[page].md',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'blocks.md',
|
||||
url: 'blocks.md',
|
||||
screenshot: false,
|
||||
run: async (_page, response) => {
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(response?.headers()['content-type']).toContain('text/markdown');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Site subdirectory (proxy)',
|
||||
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
|
||||
@@ -1187,6 +1124,16 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: 'Reusable contents',
|
||||
contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/reusable-contents/',
|
||||
tests: [
|
||||
{
|
||||
name: 'All cases',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Page actions',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
@@ -1203,6 +1150,36 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Social links',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
tests: [
|
||||
{
|
||||
name: 'With social links',
|
||||
url: getCustomizationURL({
|
||||
socialAccounts: [
|
||||
{
|
||||
platform: SiteSocialAccountPlatform.Github,
|
||||
display: { footer: true },
|
||||
handle: 'GitbookIO',
|
||||
},
|
||||
{
|
||||
platform: SiteSocialAccountPlatform.Linkedin,
|
||||
display: { footer: true },
|
||||
handle: 'gitbook',
|
||||
},
|
||||
{
|
||||
platform: SiteSocialAccountPlatform.Twitter,
|
||||
display: { footer: false },
|
||||
handle: 'GitBookIO',
|
||||
},
|
||||
],
|
||||
}),
|
||||
fullPage: true,
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Ads',
|
||||
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import {
|
||||
CustomizationAIMode,
|
||||
CustomizationBackground,
|
||||
CustomizationCodeTheme,
|
||||
CustomizationCorners,
|
||||
CustomizationDefaultFont,
|
||||
CustomizationDefaultMonospaceFont,
|
||||
@@ -298,6 +299,16 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
background: CustomizationBackground.Plain,
|
||||
icons: CustomizationIconsStyle.Regular,
|
||||
links: CustomizationLinksStyle.Default,
|
||||
codeTheme: {
|
||||
default: {
|
||||
light: CustomizationCodeTheme.DefaultLight,
|
||||
dark: CustomizationCodeTheme.DefaultDark,
|
||||
},
|
||||
openapi: {
|
||||
light: CustomizationCodeTheme.DefaultLight,
|
||||
dark: CustomizationCodeTheme.DefaultDark,
|
||||
},
|
||||
},
|
||||
sidebar: {
|
||||
background: CustomizationSidebarBackgroundStyle.Default,
|
||||
list: CustomizationSidebarListStyle.Default,
|
||||
@@ -355,6 +366,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
url: 'https://www.gitbook.com/privacy',
|
||||
},
|
||||
socialPreview: {},
|
||||
socialAccounts: [],
|
||||
};
|
||||
|
||||
const encoded = rison.encode_object(deepMerge(DEFAULT_CUSTOMIZATION, partial));
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"next": "15.4.10",
|
||||
"next": "15.4.11",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -67,7 +67,7 @@
|
||||
"remark-rehype": "^11.1.1",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "^3.2.0",
|
||||
"shiki": "^3.21.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.5",
|
||||
@@ -77,7 +77,8 @@
|
||||
"usehooks-ts": "catalog:",
|
||||
"warn-once": "^0.1.1",
|
||||
"zod": "^3",
|
||||
"zustand": "^5.0.3"
|
||||
"zustand": "^5.0.3",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^5.0.9",
|
||||
@@ -94,6 +95,7 @@
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/rison": "^0.0.9",
|
||||
"@types/negotiator": "^0.6.4",
|
||||
"bun-types": "catalog:",
|
||||
"deepmerge": "^4.3.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
@@ -118,7 +120,7 @@
|
||||
"dev:cloudflare": "wrangler dev --port 8771 --env preview",
|
||||
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||
"dev:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
|
||||
"e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts --project=chromium",
|
||||
"e2e": "playwright test e2e/internal.spec.ts e2e/cookie-banner.spec.ts e2e/pdf.spec.ts --project=chromium",
|
||||
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
|
||||
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
|
||||
"e2e-browserless": "bun test ./tests/",
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@ export default async function SiteDynamicLayout({
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout
|
||||
className="site-background"
|
||||
htmlClassName="sheet-open:gutter-stable"
|
||||
bodyClassName="site-background"
|
||||
forcedTheme={forcedTheme}
|
||||
context={context}
|
||||
>
|
||||
|
||||
+5
-1
@@ -19,7 +19,11 @@ export default async function SiteStaticLayout({
|
||||
const withTracking = shouldTrackEvents();
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout className="site-background" context={context}>
|
||||
<CustomizationRootLayout
|
||||
htmlClassName="sheet-open:gutter-stable"
|
||||
bodyClassName="site-background"
|
||||
context={context}
|
||||
>
|
||||
<SiteLayout
|
||||
context={context}
|
||||
withTracking={withTracking}
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { GitBookAssistant } from '@gitbook/browser-types';
|
||||
import { useAIChatController, useAIChatState } from '.';
|
||||
import { AIChatIcon, AISearchIcon, getAIChatName } from '../AIChat';
|
||||
import { useIntegrationAssistants } from '../Integrations';
|
||||
import { useSearch } from '../Search/useSearch';
|
||||
import { useSetSearchState } from '../Search/useSearch';
|
||||
|
||||
// Unify assistants configuration context with the assistants hook in one place
|
||||
export type AIConfig = {
|
||||
@@ -84,7 +84,7 @@ export function useAI(): AIContext {
|
||||
const chat = useAIChatState();
|
||||
const chatController = useAIChatController();
|
||||
const language = useLanguage();
|
||||
const [, setSearchState] = useSearch();
|
||||
const setSearchState = useSetSearchState();
|
||||
|
||||
const assistants: Assistant[] = [];
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { IconName } from '@gitbook/icons';
|
||||
import * as React from 'react';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { integrationsAssistantTools } from '../Integrations';
|
||||
import { useSearch } from '../Search';
|
||||
import { useSetSearchState } from '../Search';
|
||||
import { type RenderAIMessageOptions, streamAIChatResponse } from './server-actions';
|
||||
import { useAIMessageContextRef } from './useAIMessageContext';
|
||||
|
||||
@@ -163,7 +163,7 @@ export function AIChatProvider(props: {
|
||||
|
||||
const messageContextRef = useAIMessageContextRef();
|
||||
const trackEvent = useTrackEvent();
|
||||
const [, setSearchState] = useSearch();
|
||||
const setSearchState = useSetSearchState();
|
||||
const language = useLanguage();
|
||||
|
||||
// Event listeners storage
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useTrackEvent } from '../Insights';
|
||||
import { useNow } from '../hooks';
|
||||
import { Button } from '../primitives';
|
||||
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||
import { SideSheet } from '../primitives/SideSheet';
|
||||
import { AIChatControlButton } from './AIChatControlButton';
|
||||
import { AIChatIcon } from './AIChatIcon';
|
||||
import { AIChatInput } from './AIChatInput';
|
||||
@@ -69,15 +70,22 @@ export function AIChat() {
|
||||
}, [chat.opened, trackEvent]);
|
||||
|
||||
return (
|
||||
<div
|
||||
<SideSheet
|
||||
side="right"
|
||||
open={chat.opened}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
chatController.open();
|
||||
} else {
|
||||
chatController.close();
|
||||
}
|
||||
}}
|
||||
withOverlay={true}
|
||||
className={tcls(
|
||||
'ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl scroll-mt-36 px-4 py-4 transition-[width,opacity,margin,display] transition-discrete duration-300 sm:px-6 lg:fixed lg:w-80 lg:p-0 xl:w-96',
|
||||
chat.opened
|
||||
? 'lg:starting:ml-0 lg:starting:w-0 lg:starting:opacity-0'
|
||||
: 'hidden lg:ml-0 lg:w-0! lg:opacity-0'
|
||||
'ai-chat mx-auto ml-8 not-hydrated:hidden w-96 transition-[width] duration-300 ease-quint lg:max-xl:w-80'
|
||||
)}
|
||||
>
|
||||
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base transition-all duration-300 max-lg:circular-corners:rounded-3xl max-lg:rounded-corners:rounded-md max-lg:border lg:w-80 xl:w-96">
|
||||
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base">
|
||||
<EmbeddableFrameMain data-testid="ai-chat">
|
||||
<EmbeddableFrameHeader>
|
||||
<AIChatDynamicIcon trademark={config.trademark} />
|
||||
@@ -107,7 +115,7 @@ export function AIChat() {
|
||||
</EmbeddableFrameBody>
|
||||
</EmbeddableFrameMain>
|
||||
</EmbeddableFrame>
|
||||
</div>
|
||||
</SideSheet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,8 +226,8 @@ export function AIChatBody(props: {
|
||||
className="shrink grow basis-80 animate-fade-in-slow [container-type:size]"
|
||||
contentClassName="p-4 gutter-stable flex flex-col gap-4"
|
||||
orientation="vertical"
|
||||
fadeEdges={['leading']}
|
||||
active={`message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
|
||||
trailing={{ fade: false, button: true }}
|
||||
active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
|
||||
>
|
||||
{isEmpty ? (
|
||||
<div className="flex grow flex-col">
|
||||
|
||||
@@ -14,6 +14,10 @@ const adaptiveVisitorReaderCache = new Map<
|
||||
ReturnType<typeof createResourceReader<AdaptiveVisitorClaims | null>>
|
||||
>();
|
||||
|
||||
export function clearAdaptiveVisitorClaimsCache() {
|
||||
adaptiveVisitorReaderCache.clear();
|
||||
}
|
||||
|
||||
function createResourceReader<T>(promise: Promise<T>) {
|
||||
let result: T | null | undefined;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useLanguage } from '@/intl/client';
|
||||
import { t, tString } from '@/intl/translate';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { useCustomCookieBanner, useIntegrationsLoaded } from '@/components/Integrations';
|
||||
import { isCookiesTrackingDisabled, setCookiesTracking } from '../Insights';
|
||||
|
||||
/**
|
||||
@@ -15,10 +16,18 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
const { privacyPolicy = 'https://policies.gitbook.com/privacy/cookies' } = props;
|
||||
const [show, setShow] = React.useState(false);
|
||||
const language = useLanguage();
|
||||
const integrationsLoaded = useIntegrationsLoaded();
|
||||
const { hasCustomCookieBanner } = useCustomCookieBanner();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Always wait for integrations to load, and if a custom banner is registered, hide the built-in banner
|
||||
if (!integrationsLoaded || hasCustomCookieBanner) {
|
||||
setShow(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setShow(isCookiesTrackingDisabled() === undefined);
|
||||
}, []);
|
||||
}, [hasCustomCookieBanner, integrationsLoaded]);
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
@@ -41,7 +50,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
aria-describedby={describedById}
|
||||
className={tcls(
|
||||
'fixed',
|
||||
'z-10',
|
||||
'z-50',
|
||||
'bg-tint-base',
|
||||
'rounded-sm',
|
||||
'straight-corners:rounded-none',
|
||||
@@ -52,9 +61,9 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
'depth-flat:shadow-none',
|
||||
'p-4',
|
||||
'pr-8',
|
||||
'bottom-4',
|
||||
'right-4',
|
||||
'left-16',
|
||||
'bottom-[max(env(safe-area-inset-bottom),1rem)]',
|
||||
'right-[max(env(safe-area-inset-right),1rem)]',
|
||||
'left-[max(env(safe-area-inset-left),4rem)]',
|
||||
'max-w-md',
|
||||
'text-balance',
|
||||
'sm:left-auto',
|
||||
|
||||
@@ -7,18 +7,18 @@ import {
|
||||
import { Card, type CardProps } from '@/components/primitives';
|
||||
import {
|
||||
type ResolvedContentRef,
|
||||
resolveContentRef,
|
||||
resolveContentRefFallback,
|
||||
resolveContentRefInDocument,
|
||||
} from '@/lib/references';
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
import { NotFoundRefHoverCard } from './NotFoundRefHoverCard';
|
||||
|
||||
export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>) {
|
||||
const { block, context, style } = props;
|
||||
const { document, block, context, style } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(block.data.ref, context.contentContext, {
|
||||
? await resolveContentRefInDocument(document, block.data.ref, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
iconStyle: ['text-xl', 'text-tint'],
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import type { DocumentBlockCode } from '@gitbook/api';
|
||||
import type { CustomizationThemedCodeTheme, DocumentBlockCode } from '@gitbook/api';
|
||||
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
||||
@@ -8,18 +8,19 @@ import { useInViewportListener } from '@/components/hooks/useInViewportListener'
|
||||
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
||||
import { Button, ToggleChevron } from '@/components/primitives';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import { useDebounceCallback } from 'usehooks-ts';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression';
|
||||
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
||||
import type { HighlightLine, RenderedInline } from './highlight';
|
||||
import type { HighlightTheme, RenderedInline } from './highlight';
|
||||
import { plainHighlight } from './plain-highlight';
|
||||
|
||||
type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||
inlines: RenderedInline[];
|
||||
inlineExprVariables: InlineExpressionVariables;
|
||||
mode: BlockProps<DocumentBlockCode>['context']['mode'];
|
||||
themes?: CustomizationThemedCodeTheme;
|
||||
};
|
||||
|
||||
export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
||||
@@ -29,7 +30,7 @@ export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
||||
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
||||
*/
|
||||
export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
const { block, mode, style, inlines, inlineExprVariables } = props;
|
||||
const { block, mode, style, inlines, inlineExprVariables, themes } = props;
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
const isInViewportRef = useRef(false);
|
||||
const [isInViewport, setIsInViewport] = useState(false);
|
||||
@@ -40,17 +41,17 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
visitorClaims,
|
||||
variables: inlineExprVariables,
|
||||
});
|
||||
const plainLines = useMemo(
|
||||
() => plainHighlight(block, inlines, { evaluateInlineExpression }),
|
||||
[block, inlines, evaluateInlineExpression]
|
||||
const plainTheme = useMemo(
|
||||
() => plainHighlight(block, inlines, { evaluateInlineExpression, themes }),
|
||||
[block, inlines, evaluateInlineExpression, themes]
|
||||
);
|
||||
const [lines, setLines] = useState<null | HighlightLine[]>(null);
|
||||
const [theme, setTheme] = useState<null | HighlightTheme>(null);
|
||||
const [highlighting, setHighlighting] = useState(false);
|
||||
|
||||
// Preload the highlighter when the block is mounted.
|
||||
useEffect(() => {
|
||||
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block));
|
||||
}, [block]);
|
||||
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block, themes));
|
||||
}, [block, themes]);
|
||||
|
||||
// When user scrolls, we need to wait for the scroll to finish before running the highlight
|
||||
const isScrollingRef = useRef(false);
|
||||
@@ -99,14 +100,16 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
if (typeof window !== 'undefined') {
|
||||
setHighlighting(true);
|
||||
import('./highlight').then(({ highlight }) => {
|
||||
highlight(block, inlines, { evaluateInlineExpression }).then((lines) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
highlight(block, inlines, { evaluateInlineExpression, themes }).then(
|
||||
(theme) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLines(lines);
|
||||
setHighlighting(false);
|
||||
});
|
||||
setTheme(theme);
|
||||
setHighlighting(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,12 +119,12 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
}
|
||||
|
||||
// Otherwise if the block is not in viewport, we reset to the plain lines
|
||||
setLines(null);
|
||||
}, [isInViewport, block, inlines, evaluateInlineExpression]);
|
||||
setTheme(null);
|
||||
}, [isInViewport, block, inlines, evaluateInlineExpression, themes]);
|
||||
|
||||
const expandable = block.data.expandable;
|
||||
|
||||
const numberOfLinesOfCode = lines?.length ?? plainLines.length;
|
||||
const numberOfLinesOfCode = theme?.lines.length ?? plainTheme.lines.length;
|
||||
const collapsedLineCount =
|
||||
block.data.collapsedLineCount || CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT;
|
||||
const isExpandable = Boolean(
|
||||
@@ -136,16 +139,17 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
aria-busy={highlighting}
|
||||
block={block}
|
||||
style={style}
|
||||
lines={lines ?? plainLines}
|
||||
theme={theme ?? plainTheme}
|
||||
id={codeBlockBodyId}
|
||||
/>
|
||||
);
|
||||
|
||||
return isExpandable ? (
|
||||
<CodeBlockExpandable
|
||||
lines={lines ?? plainLines}
|
||||
theme={theme ?? plainTheme}
|
||||
controls={codeBlockBodyId}
|
||||
collapsedLineCount={collapsedLineCount}
|
||||
style={style}
|
||||
>
|
||||
{renderer}
|
||||
</CodeBlockExpandable>
|
||||
@@ -156,21 +160,25 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
|
||||
function CodeBlockExpandable(props: {
|
||||
children: React.ReactNode;
|
||||
lines: HighlightLine[];
|
||||
theme: HighlightTheme;
|
||||
collapsedLineCount: number;
|
||||
controls?: string;
|
||||
style?: ClassValue;
|
||||
}) {
|
||||
const { children, controls, lines = [], collapsedLineCount } = props;
|
||||
const { children, controls, theme, collapsedLineCount, style } = props;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const language = useLanguage();
|
||||
return (
|
||||
<div className="group/codeblock-expandable relative">
|
||||
<div
|
||||
className={tcls('group/codeblock-expandable relative', style)}
|
||||
data-follow-color-scheme="true"
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
isExpanded
|
||||
? '[&_pre]:after:opacity-0'
|
||||
: '[&_pre]:h-[calc(2rem+var(--line-count)*var(--line-height))] [&_pre]:overflow-y-hidden [&_pre]:after:opacity-100',
|
||||
'[&_pre]:after:pointer-events-none [&_pre]:after:absolute [&_pre]:after:inset-0 [&_pre]:after:z-1 [&_pre]:after:bg-gradient-to-t [&_pre]:after:from-0% [&_pre]:after:from-tint-2 [&_pre]:after:to-70% [&_pre]:after:to-transparent [&_pre]:after:content-[""]'
|
||||
!isExpanded
|
||||
? '[&_pre]:h-[calc(2rem+var(--line-count)*var(--line-height))] [&_pre]:overflow-y-hidden'
|
||||
: ''
|
||||
)}
|
||||
style={
|
||||
{
|
||||
@@ -181,7 +189,7 @@ function CodeBlockExpandable(props: {
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute bottom-0 flex w-full justify-center">
|
||||
<div className="pointer-events-none absolute bottom-2 flex w-full justify-center">
|
||||
<Button
|
||||
icon={<ToggleChevron open={isExpanded} />}
|
||||
size="xsmall"
|
||||
@@ -194,7 +202,7 @@ function CodeBlockExpandable(props: {
|
||||
>
|
||||
{isExpanded
|
||||
? t(language, 'code_block_expanded')
|
||||
: t(language, 'code_block_collapsed', lines.length)}
|
||||
: t(language, 'code_block_collapsed', theme.lines.length)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { DocumentBlockCode } from '@gitbook/api';
|
||||
import type {
|
||||
CustomizationThemedCodeTheme,
|
||||
DocumentBlockCode,
|
||||
SiteCustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
|
||||
import { getNodeFragmentByType } from '@/lib/document';
|
||||
|
||||
@@ -13,8 +17,21 @@ import { type RenderedInline, getInlines, highlight } from './highlight';
|
||||
/**
|
||||
* Render a code block, can be client-side or server-side.
|
||||
*/
|
||||
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||
const { block, document, style, isEstimatedOffscreen, context } = props;
|
||||
export async function CodeBlock(
|
||||
props: BlockProps<DocumentBlockCode> & {
|
||||
themeKey?: keyof SiteCustomizationSettings['styling']['codeTheme'];
|
||||
themes?: CustomizationThemedCodeTheme;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
block,
|
||||
document,
|
||||
style,
|
||||
isEstimatedOffscreen,
|
||||
context,
|
||||
themeKey = 'default',
|
||||
themes: providedThemes,
|
||||
} = props;
|
||||
const inlines = getInlines(block);
|
||||
|
||||
let hasInlineExpression = false;
|
||||
@@ -50,10 +67,19 @@ export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||
return { inline, body };
|
||||
});
|
||||
|
||||
// Get code themes from customization
|
||||
const themes =
|
||||
providedThemes ??
|
||||
(context.contentContext && 'customization' in context.contentContext
|
||||
? context.contentContext.customization.styling.codeTheme[themeKey]
|
||||
: undefined);
|
||||
|
||||
if (!isEstimatedOffscreen && !hasInlineExpression && !block.data.expandable) {
|
||||
// In v2, we render the code block server-side
|
||||
const lines = await highlight(block, richInlines);
|
||||
return <CodeBlockRenderer block={block} style={style} lines={lines} />;
|
||||
const theme = await highlight(block, richInlines, {
|
||||
themes: themes,
|
||||
});
|
||||
return <CodeBlockRenderer block={block} style={style} theme={theme} />;
|
||||
}
|
||||
|
||||
const variables = context.contentContext
|
||||
@@ -74,6 +100,7 @@ export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||
inlines={richInlines}
|
||||
inlineExprVariables={variables}
|
||||
mode={context.mode}
|
||||
themes={themes}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,10 @@ import { tcls } from '@/lib/tailwind';
|
||||
import { AnnotationPopover } from '../Annotation/AnnotationPopover';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { CopyCodeButton } from './CopyCodeButton';
|
||||
import type { HighlightLine, HighlightToken } from './highlight';
|
||||
import type { HighlightLine, HighlightTheme, HighlightToken } from './highlight';
|
||||
|
||||
type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||
lines: HighlightLine[];
|
||||
theme: HighlightTheme;
|
||||
'aria-busy'?: boolean;
|
||||
id?: string;
|
||||
};
|
||||
@@ -22,7 +22,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
props: CodeBlockRendererProps,
|
||||
ref: React.ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { block, style, lines, 'aria-busy': ariaBusy } = props;
|
||||
const { block, style, theme, 'aria-busy': ariaBusy } = props;
|
||||
|
||||
const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1;
|
||||
const withWrap = block.data.overflow === 'wrap';
|
||||
@@ -30,15 +30,35 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
|
||||
const id = useId();
|
||||
const codeId = props.id || id;
|
||||
|
||||
/* Shiki returns a color + CSS variables in a single string, which isn't supported in React.
|
||||
So we parse the foreground & background into objects that can be passed into React's `style` prop. */
|
||||
const bg = parseShikiColorString(theme.bg);
|
||||
const fg = parseShikiColorString(theme.fg);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
aria-busy={ariaBusy}
|
||||
className={tcls('group/codeblock grid shrink grid-flow-col overflow-hidden', style)}
|
||||
className={tcls(
|
||||
'group/codeblock shiki grid shrink grid-flow-col overflow-hidden',
|
||||
style
|
||||
)}
|
||||
/* Sets the code theme's mode (light or dark) for the site's theme mode (light or dark).
|
||||
* Used to style UI elements (scrollbars, form controls) correctly and apply the right default to "plain" code blocks. */
|
||||
data-color-scheme={`${theme.themes.light.type} ${theme.themes.dark.type}`}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
|
||||
{title ? (
|
||||
<div className="relative top-px z-20 inline-flex items-center justify-center rounded-t straight-corners:rounded-t-s border border-tint-subtle border-b-0 bg-tint-subtle theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base">
|
||||
<div
|
||||
className="relative top-px z-20 inline-flex items-center justify-center circular-corners:rounded-t-xl rounded-corners:rounded-t-lg straight-corners:rounded-t-xs border border-tint-subtle border-b-0 bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base"
|
||||
style={{
|
||||
backgroundColor: bg?.color,
|
||||
...bg?.vars,
|
||||
color: fg?.color,
|
||||
...fg?.vars,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -49,23 +69,32 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
/>
|
||||
<pre
|
||||
className={tcls(
|
||||
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base p-2 [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base',
|
||||
'rounded-md straight-corners:rounded-xs shadow-xs',
|
||||
title && 'rounded-ss-none'
|
||||
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base p-2 text-tint-strong [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base',
|
||||
'circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs depth-subtle:shadow-xs',
|
||||
title && 'rounded-ss-none!'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: bg?.color,
|
||||
...bg?.vars,
|
||||
color: fg?.color,
|
||||
...fg?.vars,
|
||||
}}
|
||||
>
|
||||
<code
|
||||
id={codeId}
|
||||
className={tcls(
|
||||
'inline-grid min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
|
||||
withWrap && 'whitespace-pre-wrap'
|
||||
'inline-grid max-h-full min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
|
||||
withWrap && 'whitespace-pre-wrap',
|
||||
'[[aria-expanded=false]_&]:mask-b-from-50%'
|
||||
)}
|
||||
>
|
||||
{lines.map((line, index) => (
|
||||
{theme.lines.map((line, index) => (
|
||||
<CodeHighlightLine
|
||||
bg={bg}
|
||||
fg={fg}
|
||||
key={index}
|
||||
line={line}
|
||||
isLast={index === lines.length - 1}
|
||||
isLast={index === theme.lines.length - 1}
|
||||
withLineNumbers={withLineNumbers}
|
||||
/>
|
||||
))}
|
||||
@@ -77,13 +106,33 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
|
||||
function CodeHighlightLine(props: {
|
||||
line: HighlightLine;
|
||||
bg?: ShikiColorDefinition;
|
||||
fg?: ShikiColorDefinition;
|
||||
isLast: boolean;
|
||||
withLineNumbers: boolean;
|
||||
}) {
|
||||
const { line, isLast, withLineNumbers } = props;
|
||||
const { line, isLast, withLineNumbers, bg, fg } = props;
|
||||
return (
|
||||
<span className={tcls('highlight-line', line.highlighted && 'highlighted')}>
|
||||
{withLineNumbers && <span className="highlight-line-number" />}
|
||||
<span
|
||||
className={tcls('highlight-line', line.highlighted && 'highlighted')}
|
||||
style={{
|
||||
color: fg?.color,
|
||||
...fg?.vars,
|
||||
backgroundColor: bg?.color,
|
||||
...bg?.vars,
|
||||
}}
|
||||
>
|
||||
{withLineNumbers && (
|
||||
<span
|
||||
className="highlight-line-number"
|
||||
style={{
|
||||
color: fg?.color,
|
||||
...fg?.vars,
|
||||
backgroundColor: bg?.color,
|
||||
...bg?.vars,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="highlight-line-content">
|
||||
<CodeHighlightTokens tokens={line.tokens} />
|
||||
{!isLast && '\n'}
|
||||
@@ -112,13 +161,53 @@ function CodeHighlightToken(props: { token: HighlightToken }) {
|
||||
return token.content;
|
||||
}
|
||||
case 'shiki': {
|
||||
if (!token.token.color) {
|
||||
if (!token.token.htmlStyle) {
|
||||
return token.token.content;
|
||||
}
|
||||
|
||||
return <span style={{ color: token.token.color }}>{token.token.content}</span>;
|
||||
return <span style={token.token.htmlStyle}>{token.token.content}</span>;
|
||||
}
|
||||
default:
|
||||
assertNever(token);
|
||||
}
|
||||
}
|
||||
|
||||
type ShikiColorDefinition = {
|
||||
color: string | undefined;
|
||||
vars: Record<string, string> | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse Shiki color string format into separate color and CSS variables.
|
||||
*
|
||||
* Shiki returns colors in a format that combines a default color with CSS variables:
|
||||
* "defaultColor;--shiki-light:value1;--shiki-dark:value2"
|
||||
*
|
||||
* React does not parse these strings as valid CSS because they contain values & properties in one.
|
||||
* We split these so React can apply them separately via the style prop.
|
||||
*/
|
||||
function parseShikiColorString(
|
||||
originalColor: string | undefined
|
||||
): ShikiColorDefinition | undefined {
|
||||
if (!originalColor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parts = originalColor.split(';');
|
||||
const color = parts[0];
|
||||
const vars = parts.slice(1).reduce(
|
||||
(acc, item) => {
|
||||
const [key, value] = item.split(':');
|
||||
if (key && value) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
return {
|
||||
color,
|
||||
vars: Object.keys(vars).length > 0 ? vars : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { JSONDocument } from '@gitbook/api';
|
||||
import type {
|
||||
CustomizationThemedCodeTheme,
|
||||
JSONDocument,
|
||||
SiteCustomizationSettings,
|
||||
} from '@gitbook/api';
|
||||
import { useId } from 'react';
|
||||
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
@@ -8,8 +12,13 @@ import { convertCodeStringToBlock } from './utils';
|
||||
* Plain code block with syntax highlighting.
|
||||
* For simplicity, this is just a wrapper around the CodeBlock component, emulating a document.
|
||||
*/
|
||||
export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
||||
const { code, syntax } = props;
|
||||
export function PlainCodeBlock(props: {
|
||||
code: string;
|
||||
syntax: string;
|
||||
themeKey?: keyof SiteCustomizationSettings['styling']['codeTheme'];
|
||||
themes?: CustomizationThemedCodeTheme;
|
||||
}) {
|
||||
const { code, syntax, themeKey, themes } = props;
|
||||
const id = useId();
|
||||
|
||||
const block = convertCodeStringToBlock({ key: id, code, syntax });
|
||||
@@ -26,6 +35,8 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
||||
context={{
|
||||
mode: 'default',
|
||||
}}
|
||||
themeKey={themeKey}
|
||||
themes={themes}
|
||||
block={block}
|
||||
ancestorBlocks={[]}
|
||||
// We optimize perf by default
|
||||
|
||||
@@ -0,0 +1,832 @@
|
||||
import type { ThemeRegistration } from 'shiki/core';
|
||||
|
||||
export const customThemes: Record<string, ThemeRegistration> = {
|
||||
'default-light': createTheme({
|
||||
name: 'default-light',
|
||||
type: 'light',
|
||||
bg: 'inherit',
|
||||
fg: 'inherit',
|
||||
colors: {
|
||||
basic: {
|
||||
comment: 'rgb(var(--neutral-9))',
|
||||
string: 'rgb(var(--success-10))',
|
||||
constant: 'rgb(var(--warning-10))',
|
||||
escape: 'rgb(var(--success-10))',
|
||||
},
|
||||
keyword: {
|
||||
default: 'rgb(var(--danger-10))',
|
||||
operator: 'rgb(var(--tint-11))',
|
||||
regexpOperator: 'rgb(var(--primary-10))',
|
||||
regexpQuantifier: 'rgb(var(--warning-10))',
|
||||
},
|
||||
variable: {
|
||||
default: 'rgb(var(--tint-12))',
|
||||
language: 'rgb(var(--danger-10))',
|
||||
parameter: 'rgb(var(--warning-10))',
|
||||
},
|
||||
function: {
|
||||
default: 'rgb(var(--primary-10))',
|
||||
support: 'rgb(var(--primary-10))',
|
||||
},
|
||||
type: {
|
||||
custom: 'rgb(var(--primary-10))',
|
||||
primitive: 'rgb(var(--warning-10))',
|
||||
other: 'rgb(var(--tint-12))',
|
||||
classMeta: 'rgb(var(--tint-12))',
|
||||
},
|
||||
entity: {
|
||||
tag: 'rgb(var(--warning-10))',
|
||||
attributeId: 'rgb(var(--primary-10))',
|
||||
label: 'rgb(var(--danger-10))',
|
||||
},
|
||||
markup: {
|
||||
default: 'rgb(var(--primary-10))',
|
||||
code: 'rgb(var(--success-10))',
|
||||
link: 'rgb(var(--primary-10))',
|
||||
linkUrl: 'rgb(var(--warning-10))',
|
||||
separator: 'rgb(var(--tint-7))',
|
||||
},
|
||||
diff: {
|
||||
inserted: 'rgb(var(--success-10))',
|
||||
deleted: 'rgb(var(--danger-10))',
|
||||
changed: 'rgb(var(--tint-12))',
|
||||
},
|
||||
other: {
|
||||
punctuation: 'rgb(var(--tint-11))',
|
||||
preprocessor: 'rgb(var(--danger-10))',
|
||||
default: 'rgb(var(--tint-11))',
|
||||
invalid: 'rgb(var(--danger-10))',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'default-dark': createTheme({
|
||||
name: 'default-dark',
|
||||
type: 'dark',
|
||||
bg: 'inherit',
|
||||
fg: 'inherit',
|
||||
colors: {
|
||||
basic: {
|
||||
comment: 'rgb(var(--neutral-9))',
|
||||
string: 'rgb(var(--success-11))',
|
||||
constant: 'rgb(var(--warning-11))',
|
||||
escape: 'rgb(var(--success-11))',
|
||||
},
|
||||
keyword: {
|
||||
default: 'rgb(var(--danger-11))',
|
||||
operator: 'rgb(var(--tint-11))',
|
||||
regexpOperator: 'rgb(var(--primary-11))',
|
||||
regexpQuantifier: 'rgb(var(--warning-11))',
|
||||
},
|
||||
variable: {
|
||||
default: 'rgb(var(--tint-12))',
|
||||
language: 'rgb(var(--danger-11))',
|
||||
parameter: 'rgb(var(--warning-11))',
|
||||
},
|
||||
function: {
|
||||
default: 'rgb(var(--primary-11))',
|
||||
support: 'rgb(var(--primary-11))',
|
||||
},
|
||||
type: {
|
||||
custom: 'rgb(var(--primary-11))',
|
||||
primitive: 'rgb(var(--warning-11))',
|
||||
other: 'rgb(var(--tint-12))',
|
||||
classMeta: 'rgb(var(--tint-12))',
|
||||
},
|
||||
entity: {
|
||||
tag: 'rgb(var(--warning-11))',
|
||||
attributeId: 'rgb(var(--primary-11))',
|
||||
label: 'rgb(var(--danger-11))',
|
||||
},
|
||||
markup: {
|
||||
default: 'rgb(var(--primary-11))',
|
||||
code: 'rgb(var(--success-11))',
|
||||
link: 'rgb(var(--primary-11))',
|
||||
linkUrl: 'rgb(var(--warning-11))',
|
||||
separator: 'rgb(var(--tint-7))',
|
||||
},
|
||||
diff: {
|
||||
inserted: 'rgb(var(--success-11))',
|
||||
deleted: 'rgb(var(--danger-11))',
|
||||
changed: 'rgb(var(--tint-12))',
|
||||
},
|
||||
other: {
|
||||
punctuation: 'rgb(var(--tint-12))',
|
||||
preprocessor: 'rgb(var(--danger-11))',
|
||||
default: 'rgb(var(--tint-12))',
|
||||
invalid: 'rgb(var(--danger-11))',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'monochrome-light': createTheme({
|
||||
name: 'monochrome-light',
|
||||
type: 'light',
|
||||
bg: 'inherit',
|
||||
fg: 'inherit',
|
||||
colors: {
|
||||
basic: {
|
||||
comment: 'rgb(var(--tint-9))',
|
||||
string: 'rgb(var(--primary-12))',
|
||||
constant: 'rgb(var(--primary-11))',
|
||||
escape: 'rgb(var(--tint-12))',
|
||||
},
|
||||
keyword: {
|
||||
default: 'rgb(var(--primary-10))',
|
||||
operator: 'rgb(var(--primary-11))',
|
||||
regexpOperator: 'rgb(var(--primary-10))',
|
||||
regexpQuantifier: 'rgb(var(--primary-11))',
|
||||
},
|
||||
variable: {
|
||||
default: 'rgb(var(--tint-12))',
|
||||
language: 'rgb(var(--primary-10))',
|
||||
parameter: 'rgb(var(--tint-11))',
|
||||
},
|
||||
function: {
|
||||
default: 'rgb(var(--primary-9))',
|
||||
support: 'rgb(var(--primary-9))',
|
||||
},
|
||||
type: {
|
||||
custom: 'rgb(var(--primary-10))',
|
||||
primitive: 'rgb(var(--primary-11))',
|
||||
other: 'rgb(var(--primary-10))',
|
||||
classMeta: 'rgb(var(--tint-12))',
|
||||
},
|
||||
entity: {
|
||||
tag: 'rgb(var(--primary-11))',
|
||||
attributeId: 'rgb(var(--primary-9))',
|
||||
label: 'rgb(var(--primary-10))',
|
||||
},
|
||||
markup: {
|
||||
default: 'rgb(var(--primary-10))',
|
||||
code: 'rgb(var(--tint-12))',
|
||||
link: 'rgb(var(--primary-10))',
|
||||
linkUrl: 'rgb(var(--primary-11))',
|
||||
separator: 'rgb(var(--tint-7))',
|
||||
},
|
||||
diff: {
|
||||
inserted: 'rgb(var(--success-11))',
|
||||
deleted: 'rgb(var(--danger-11))',
|
||||
changed: 'rgb(var(--tint-12))',
|
||||
},
|
||||
other: {
|
||||
punctuation: 'rgb(var(--tint-10))',
|
||||
preprocessor: 'rgb(var(--primary-10))',
|
||||
default: 'rgb(var(--primary-11))',
|
||||
invalid: 'rgb(var(--danger-11))',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'monochrome-dark': createTheme({
|
||||
name: 'monochrome-dark',
|
||||
type: 'dark',
|
||||
bg: 'inherit',
|
||||
fg: 'inherit',
|
||||
colors: {
|
||||
basic: {
|
||||
comment: 'rgb(var(--tint-9))',
|
||||
string: 'rgb(var(--primary-12))',
|
||||
constant: 'rgb(var(--primary-11))',
|
||||
escape: 'rgb(var(--tint-12))',
|
||||
},
|
||||
keyword: {
|
||||
default: 'rgb(var(--primary-11))',
|
||||
operator: 'rgb(var(--primary-11))',
|
||||
regexpOperator: 'rgb(var(--primary-10))',
|
||||
regexpQuantifier: 'rgb(var(--primary-11))',
|
||||
},
|
||||
variable: {
|
||||
default: 'rgb(var(--tint-12))',
|
||||
language: 'rgb(var(--primary-10))',
|
||||
parameter: 'rgb(var(--tint-9))',
|
||||
},
|
||||
function: {
|
||||
default: 'rgb(var(--primary-9))',
|
||||
support: 'rgb(var(--primary-9))',
|
||||
},
|
||||
type: {
|
||||
custom: 'rgb(var(--primary-10))',
|
||||
primitive: 'rgb(var(--primary-11))',
|
||||
other: 'rgb(var(--primary-10))',
|
||||
classMeta: 'rgb(var(--tint-12))',
|
||||
},
|
||||
entity: {
|
||||
tag: 'rgb(var(--primary-11))',
|
||||
attributeId: 'rgb(var(--primary-9))',
|
||||
label: 'rgb(var(--primary-10))',
|
||||
},
|
||||
markup: {
|
||||
default: 'rgb(var(--primary-10))',
|
||||
code: 'rgb(var(--tint-12))',
|
||||
link: 'rgb(var(--primary-10))',
|
||||
linkUrl: 'rgb(var(--primary-11))',
|
||||
separator: 'rgb(var(--tint-7))',
|
||||
},
|
||||
diff: {
|
||||
inserted: 'rgb(var(--success-11))',
|
||||
deleted: 'rgb(var(--danger-11))',
|
||||
changed: 'rgb(var(--tint-12))',
|
||||
},
|
||||
other: {
|
||||
punctuation: 'rgb(var(--tint-10))',
|
||||
preprocessor: 'rgb(var(--primary-10))',
|
||||
default: 'rgb(var(--primary-11))',
|
||||
invalid: 'rgb(var(--danger-11))',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Color category mappings for syntax highlighting.
|
||||
* Organized into logical groups for better readability.
|
||||
*/
|
||||
export type ColorCategories = {
|
||||
/** Basic syntax elements */
|
||||
basic: {
|
||||
/** Comments (//, /*, #, etc.) */
|
||||
comment: string;
|
||||
/** String literals ("...", '...', `...`) */
|
||||
string: string;
|
||||
/** Constants: numeric, language constants (true, false, null), character constants */
|
||||
constant: string;
|
||||
/** Escape sequences in strings (\n, \t, etc.) */
|
||||
escape: string;
|
||||
};
|
||||
/** Keywords and operators */
|
||||
keyword: {
|
||||
/** Control flow (if, else, for, while, return), storage (const, let, var, function, class) */
|
||||
default: string;
|
||||
/** Operators (+, -, *, /, =, ==, etc.) */
|
||||
operator: string;
|
||||
/** Regexp operators (|, ^, $) */
|
||||
regexpOperator: string;
|
||||
/** Regexp quantifiers (*, +, ?, {n}) */
|
||||
regexpQuantifier: string;
|
||||
};
|
||||
/** Variables and parameters */
|
||||
variable: {
|
||||
/** Regular variables */
|
||||
default: string;
|
||||
/** Language variables (this, self, super) */
|
||||
language: string;
|
||||
/** Function/method parameters */
|
||||
parameter: string;
|
||||
};
|
||||
/** Functions */
|
||||
function: {
|
||||
/** User-defined function/method invocations */
|
||||
default: string;
|
||||
/** Built-in/library support functions */
|
||||
support: string;
|
||||
};
|
||||
/** Types and classes */
|
||||
type: {
|
||||
/** Custom type names (DocumentBlockCode, HighlightLine, etc.) */
|
||||
custom: string;
|
||||
/** Primitive types (number, boolean, string, etc.) */
|
||||
primitive: string;
|
||||
/** Other types (support.type, support.class, namespaces) */
|
||||
other: string;
|
||||
/** Class meta contexts (meta.class) */
|
||||
classMeta: string;
|
||||
};
|
||||
/** Entities (tags, attributes, labels) */
|
||||
entity: {
|
||||
/** HTML/XML tags and attributes */
|
||||
tag: string;
|
||||
/** Attribute IDs (entity.other.attribute-name.id) */
|
||||
attributeId: string;
|
||||
/** Labels (entity.name.label) */
|
||||
label: string;
|
||||
};
|
||||
/** Markup elements */
|
||||
markup: {
|
||||
/** Markup elements (bold, italic, headings, lists, quotes, etc.) */
|
||||
default: string;
|
||||
/** Inline code in markup */
|
||||
code: string;
|
||||
/** Links in markup */
|
||||
link: string;
|
||||
/** Link URLs in markup */
|
||||
linkUrl: string;
|
||||
/** Separators (meta.separator) */
|
||||
separator: string;
|
||||
};
|
||||
/** Diff highlighting */
|
||||
diff: {
|
||||
/** Inserted lines */
|
||||
inserted: string;
|
||||
/** Deleted lines */
|
||||
deleted: string;
|
||||
/** Changed lines */
|
||||
changed: string;
|
||||
};
|
||||
/** Other syntax elements */
|
||||
other: {
|
||||
/** Punctuation (., ,, ;, :, {}, [], (), etc.) */
|
||||
punctuation: string;
|
||||
/** Preprocessor directives (#include, #define, etc.) */
|
||||
preprocessor: string;
|
||||
/** Other/miscellaneous elements */
|
||||
default: string;
|
||||
/** Invalid syntax/errors */
|
||||
invalid: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a Shiki theme registration from semantic color categories.
|
||||
*
|
||||
* Maps our semantic color categories (comment, keyword, function, etc.) to Shiki's TextMate scopes.
|
||||
* TextMate scopes are hierarchical selectors (e.g., "keyword.control", "variable.parameter") that
|
||||
* Shiki uses to identify different syntax elements. This abstraction allows us to define themes
|
||||
* using semantic categories rather than low-level scope patterns.
|
||||
*/
|
||||
function createTheme(options: {
|
||||
name: string;
|
||||
type: 'light' | 'dark';
|
||||
bg: string;
|
||||
fg: string;
|
||||
colors: ColorCategories;
|
||||
}): ThemeRegistration {
|
||||
const { name, type, bg, fg, colors } = options;
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
bg,
|
||||
fg,
|
||||
settings: [
|
||||
// Comments
|
||||
{
|
||||
scope: ['comment'],
|
||||
settings: {
|
||||
foreground: colors.basic.comment,
|
||||
},
|
||||
},
|
||||
// Constants: language constants (true, false, null, etc.)
|
||||
{
|
||||
scope: ['constant.language'],
|
||||
settings: {
|
||||
foreground: colors.basic.constant,
|
||||
},
|
||||
},
|
||||
// Constants: numeric
|
||||
{
|
||||
scope: [
|
||||
'constant.numeric',
|
||||
'variable.other.enummember',
|
||||
'keyword.operator.plus.exponent',
|
||||
'keyword.operator.minus.exponent',
|
||||
'keyword.other.unit',
|
||||
'constant.sha.git-rebase',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.basic.constant,
|
||||
},
|
||||
},
|
||||
// Constants: regexp
|
||||
{
|
||||
scope: ['constant.regexp'],
|
||||
settings: {
|
||||
foreground: colors.basic.constant,
|
||||
},
|
||||
},
|
||||
// Constants: character
|
||||
{
|
||||
scope: ['constant.character', 'constant.other.option'],
|
||||
settings: {
|
||||
foreground: colors.basic.constant,
|
||||
},
|
||||
},
|
||||
// Constants: escape sequences
|
||||
{
|
||||
scope: ['constant.character.escape'],
|
||||
settings: {
|
||||
foreground: colors.basic.escape,
|
||||
},
|
||||
},
|
||||
// Constants: other
|
||||
{
|
||||
scope: [
|
||||
'constant.other',
|
||||
'constant.other.color',
|
||||
'constant.other.symbol',
|
||||
'constant.other.placeholder',
|
||||
'constant.other.character-class.regexp',
|
||||
'constant.character.set.regexp',
|
||||
'constant.character.character-class.regexp',
|
||||
'constant.other.character-class.set.regexp',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.basic.constant,
|
||||
},
|
||||
},
|
||||
// Constants: variable constants
|
||||
{
|
||||
scope: ['variable.other.constant'],
|
||||
settings: {
|
||||
foreground: colors.basic.constant,
|
||||
},
|
||||
},
|
||||
// Strings
|
||||
{
|
||||
scope: [
|
||||
'string',
|
||||
'meta.embedded.assembly',
|
||||
'string.tag',
|
||||
'string.value',
|
||||
'string.regexp',
|
||||
'meta.preprocessor.string',
|
||||
'support.constant.property-value',
|
||||
'support.constant.font-name',
|
||||
'support.constant.media-type',
|
||||
'support.constant.media',
|
||||
'constant.other.color.rgb-value',
|
||||
'constant.other.rgb-value',
|
||||
'support.constant.color',
|
||||
'punctuation.definition.group.regexp',
|
||||
'punctuation.definition.group.assertion.regexp',
|
||||
'punctuation.definition.character-class.regexp',
|
||||
'punctuation.character.set.begin.regexp',
|
||||
'punctuation.character.set.end.regexp',
|
||||
'keyword.operator.negation.regexp',
|
||||
'support.other.parenthesis.regexp',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.basic.string,
|
||||
},
|
||||
},
|
||||
// String expressions (template literals, interpolated)
|
||||
// Only color the punctuation, not the expression content
|
||||
{
|
||||
scope: [
|
||||
'punctuation.definition.template-expression.begin',
|
||||
'punctuation.definition.template-expression.end',
|
||||
'punctuation.section.embedded',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.variable.parameter,
|
||||
},
|
||||
},
|
||||
// Keywords: control flow
|
||||
{
|
||||
scope: [
|
||||
'keyword.control',
|
||||
'source.cpp keyword.operator.new',
|
||||
'keyword.operator.delete',
|
||||
'keyword.other.using',
|
||||
'keyword.other.directive.using',
|
||||
'keyword.other.operator',
|
||||
'entity.name.operator',
|
||||
'keyword.operator.new',
|
||||
'keyword.operator.expression',
|
||||
'keyword.operator.cast',
|
||||
'keyword.operator.sizeof',
|
||||
'keyword.operator.alignof',
|
||||
'keyword.operator.typeid',
|
||||
'keyword.operator.alignas',
|
||||
'keyword.operator.instanceof',
|
||||
'keyword.operator.logical.python',
|
||||
'keyword.operator.wordlike',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.keyword.default,
|
||||
},
|
||||
},
|
||||
// Keywords: storage (const, let, var, function, class, etc.)
|
||||
{
|
||||
scope: [
|
||||
'storage',
|
||||
'storage.type',
|
||||
'storage.modifier',
|
||||
'keyword.operator.noexcept',
|
||||
'storage.type.numeric.go',
|
||||
'storage.type.byte.go',
|
||||
'storage.type.boolean.go',
|
||||
'storage.type.string.go',
|
||||
'storage.type.uintptr.go',
|
||||
'storage.type.error.go',
|
||||
'storage.type.rune.go',
|
||||
'storage.type.cs',
|
||||
'storage.type.generic.cs',
|
||||
'storage.type.modifier.cs',
|
||||
'storage.type.variable.cs',
|
||||
'storage.type.annotation.java',
|
||||
'storage.type.generic.java',
|
||||
'storage.type.java',
|
||||
'storage.type.object.array.java',
|
||||
'storage.type.primitive.array.java',
|
||||
'storage.type.primitive.java',
|
||||
'storage.type.token.java',
|
||||
'storage.type.groovy',
|
||||
'storage.type.annotation.groovy',
|
||||
'storage.type.parameters.groovy',
|
||||
'storage.type.generic.groovy',
|
||||
'storage.type.object.array.groovy',
|
||||
'storage.type.primitive.array.groovy',
|
||||
'storage.type.primitive.groovy',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.keyword.default,
|
||||
},
|
||||
},
|
||||
// Keywords: general
|
||||
{
|
||||
scope: ['keyword'],
|
||||
settings: {
|
||||
foreground: colors.keyword.default,
|
||||
},
|
||||
},
|
||||
// Keywords: operators (default)
|
||||
{
|
||||
scope: ['keyword.operator'],
|
||||
settings: {
|
||||
foreground: colors.keyword.operator,
|
||||
},
|
||||
},
|
||||
// Keywords: regexp operators
|
||||
{
|
||||
scope: ['keyword.operator.or.regexp', 'keyword.control.anchor.regexp'],
|
||||
settings: {
|
||||
foreground: colors.keyword.regexpOperator,
|
||||
},
|
||||
},
|
||||
// Keywords: regexp quantifiers
|
||||
{
|
||||
scope: ['keyword.operator.quantifier.regexp'],
|
||||
settings: {
|
||||
foreground: colors.keyword.regexpQuantifier,
|
||||
},
|
||||
},
|
||||
// Variable language (this, self, super, etc.)
|
||||
{
|
||||
scope: ['variable.language'],
|
||||
settings: {
|
||||
foreground: colors.variable.language,
|
||||
},
|
||||
},
|
||||
// Parameters
|
||||
{
|
||||
scope: ['variable.parameter'],
|
||||
settings: {
|
||||
foreground: colors.variable.parameter,
|
||||
},
|
||||
},
|
||||
// Variables (regular variables, not parameters)
|
||||
{
|
||||
scope: [
|
||||
'variable',
|
||||
'meta.definition.variable.name',
|
||||
'support.variable',
|
||||
'entity.name.variable',
|
||||
'meta.object-literal.key',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.variable.default,
|
||||
},
|
||||
},
|
||||
// Functions: invocations (calls) - user-defined functions
|
||||
{
|
||||
scope: [
|
||||
'meta.function-call',
|
||||
'meta.method-call',
|
||||
'entity.name.function',
|
||||
'entity.name.method',
|
||||
'support.constant.handlebars',
|
||||
'source.powershell variable.other.member',
|
||||
'entity.name.operator.custom-literal',
|
||||
'keyword.other.special-method',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.function.default,
|
||||
},
|
||||
},
|
||||
// Support functions: built-in/library functions
|
||||
{
|
||||
scope: ['support.function', 'support.function.git-rebase'],
|
||||
settings: {
|
||||
foreground: colors.function.support,
|
||||
},
|
||||
},
|
||||
// Types: custom type names (DocumentBlockCode, HighlightLine, etc.) - must come first
|
||||
{
|
||||
scope: ['entity.name.type', 'entity.name.class', 'entity.other.inherited-class'],
|
||||
settings: {
|
||||
foreground: colors.type.custom,
|
||||
},
|
||||
},
|
||||
// Types: primitive (number, boolean, string, etc.) - must come before support.type
|
||||
{
|
||||
scope: [
|
||||
'support.type.primitive',
|
||||
'storage.type.primitive',
|
||||
'support.type.builtin',
|
||||
'entity.name.type.primitive',
|
||||
'support.type',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.type.primitive,
|
||||
},
|
||||
},
|
||||
// Types: other (support.class, etc.)
|
||||
{
|
||||
scope: [
|
||||
'support.class',
|
||||
'entity.name.namespace',
|
||||
'entity.other.attribute',
|
||||
'entity.name.scope-resolution',
|
||||
'meta.type.cast.expr',
|
||||
'meta.type.new.expr',
|
||||
'support.constant.math',
|
||||
'support.constant.json',
|
||||
'punctuation.separator.namespace.ruby',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.type.other,
|
||||
},
|
||||
},
|
||||
// Class meta contexts
|
||||
{
|
||||
scope: ['meta.class'],
|
||||
settings: {
|
||||
foreground: colors.type.classMeta,
|
||||
},
|
||||
},
|
||||
// DOM constants and namespace references (ReactDOM, window.document, etc.)
|
||||
{
|
||||
scope: [
|
||||
'support.constant.dom',
|
||||
'variable.other.object',
|
||||
'variable.other.object.property',
|
||||
'entity.name.class.jsx',
|
||||
'support.class.builtin',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.variable.parameter,
|
||||
},
|
||||
},
|
||||
// Entity names: tags, attributes
|
||||
{
|
||||
scope: [
|
||||
'entity.name.tag',
|
||||
'entity.other.attribute-name',
|
||||
'support.type.vendored.property-name',
|
||||
'support.type.property-name',
|
||||
'source.css variable',
|
||||
'source.coffee.embedded',
|
||||
'meta.structure.dictionary.key.python',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.entity.tag,
|
||||
},
|
||||
},
|
||||
// Attribute IDs (typically styled like functions)
|
||||
{
|
||||
scope: ['entity.other.attribute-name.id', 'punctuation.definition.entity'],
|
||||
settings: {
|
||||
foreground: colors.entity.attributeId,
|
||||
},
|
||||
},
|
||||
// Punctuation
|
||||
{
|
||||
scope: [
|
||||
'punctuation',
|
||||
'punctuation.definition.tag',
|
||||
'punctuation.definition.quote.begin.markdown',
|
||||
'punctuation.definition.list.begin.markdown',
|
||||
'punctuation.section.embedded.begin.php',
|
||||
'punctuation.section.embedded.end.php',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.other.punctuation,
|
||||
},
|
||||
},
|
||||
// Links: link text
|
||||
{
|
||||
scope: [
|
||||
'markup.underline.link',
|
||||
'string.other.link',
|
||||
'punctuation.definition.string.end.markdown',
|
||||
'punctuation.definition.string.begin.markdown',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.markup.link,
|
||||
},
|
||||
},
|
||||
// Links: link URLs
|
||||
{
|
||||
scope: ['meta.link'],
|
||||
settings: {
|
||||
foreground: colors.markup.linkUrl,
|
||||
},
|
||||
},
|
||||
// Preprocessor
|
||||
{
|
||||
scope: [
|
||||
'meta.preprocessor',
|
||||
'entity.name.function.preprocessor',
|
||||
'meta.preprocessor.numeric',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.other.preprocessor,
|
||||
},
|
||||
},
|
||||
// Diff: inserted
|
||||
{
|
||||
scope: ['markup.inserted', 'markup.inserted.diff'],
|
||||
settings: {
|
||||
foreground: colors.diff.inserted,
|
||||
},
|
||||
},
|
||||
// Diff: deleted
|
||||
{
|
||||
scope: ['markup.deleted', 'markup.deleted.diff'],
|
||||
settings: {
|
||||
foreground: colors.diff.deleted,
|
||||
},
|
||||
},
|
||||
// Diff: changed
|
||||
{
|
||||
scope: ['markup.changed', 'markup.changed.diff', 'meta.diff.header'],
|
||||
settings: {
|
||||
foreground: colors.diff.changed,
|
||||
},
|
||||
},
|
||||
// Markup: bold (with bold font style)
|
||||
{
|
||||
scope: ['markup.bold', 'punctuation.definition.bold'],
|
||||
settings: {
|
||||
foreground: colors.markup.default,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
},
|
||||
// Markup: italic (with italic font style)
|
||||
{
|
||||
scope: ['markup.italic', 'punctuation.definition.italic'],
|
||||
settings: {
|
||||
foreground: colors.markup.default,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
},
|
||||
// Markup: inline code
|
||||
{
|
||||
scope: ['markup.raw.inline'],
|
||||
settings: {
|
||||
foreground: colors.markup.code,
|
||||
},
|
||||
},
|
||||
// Markup: headings, lists, quotes, underline, strikethrough
|
||||
{
|
||||
scope: [
|
||||
'markup.heading',
|
||||
'markup.heading punctuation.definition.heading',
|
||||
'entity.name.section',
|
||||
'header',
|
||||
'markup.list',
|
||||
'markup.quote',
|
||||
'markup.underline',
|
||||
'markup.strikethrough',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.markup.default,
|
||||
},
|
||||
},
|
||||
// Separator
|
||||
{
|
||||
scope: ['meta.separator'],
|
||||
settings: {
|
||||
background: colors.markup.separator,
|
||||
foreground: fg,
|
||||
},
|
||||
},
|
||||
// Labels
|
||||
{
|
||||
scope: ['entity.name.label'],
|
||||
settings: {
|
||||
foreground: colors.entity.label,
|
||||
},
|
||||
},
|
||||
// Other
|
||||
{
|
||||
scope: [
|
||||
'meta.embedded',
|
||||
'source.groovy.embedded',
|
||||
'string meta.image.inline.markdown',
|
||||
'variable.legacy.builtin.python',
|
||||
'storage.modifier.import.java',
|
||||
'variable.language.wildcard.java',
|
||||
'storage.modifier.package.java',
|
||||
],
|
||||
settings: {
|
||||
foreground: colors.other.default,
|
||||
},
|
||||
},
|
||||
// Invalid
|
||||
{
|
||||
scope: ['invalid'],
|
||||
settings: {
|
||||
foreground: colors.other.invalid,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,8 @@ async function highlightWithInlines(block: DocumentBlockCode) {
|
||||
inline,
|
||||
body: null,
|
||||
}));
|
||||
return highlight(block, inlines);
|
||||
const result = await highlight(block, inlines);
|
||||
return result.lines;
|
||||
}
|
||||
|
||||
it('should parse plain code', async () => {
|
||||
@@ -224,7 +225,19 @@ it('should parse code with an inline on a single line', async () => {
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '"Hello World"',
|
||||
content: '"',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: 'Hello World',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '"',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -375,7 +388,19 @@ it('should parse code with an inline on a multiple line', async () => {
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '"Hello World"',
|
||||
content: '"',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: 'Hello World',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '"',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -631,7 +656,13 @@ it('should support multiple code tokens in an annotation', async () => {
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '.world',
|
||||
content: '.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: 'world',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -645,7 +676,13 @@ it('should support multiple code tokens in an annotation', async () => {
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: ');',
|
||||
content: ')',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: ';',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -699,7 +736,13 @@ it('should handle \\r', async () => {
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '.log',
|
||||
content: '.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: 'log',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -711,7 +754,19 @@ it('should handle \\r', async () => {
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '"Hello"',
|
||||
content: '"',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: 'Hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '"',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -734,7 +789,13 @@ it('should handle \\r', async () => {
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '.log',
|
||||
content: '.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: 'log',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -746,7 +807,19 @@ it('should handle \\r', async () => {
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '"World"',
|
||||
content: '"',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: 'World',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shiki',
|
||||
token: {
|
||||
content: '"',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
import type {
|
||||
DocumentBlockCode,
|
||||
DocumentBlockCodeLine,
|
||||
DocumentInlineAnnotation,
|
||||
import {
|
||||
CustomizationCodeTheme,
|
||||
type CustomizationThemedCodeTheme,
|
||||
type DocumentBlockCode,
|
||||
type DocumentBlockCodeLine,
|
||||
type DocumentInlineAnnotation,
|
||||
} from '@gitbook/api';
|
||||
import {
|
||||
type ThemeRegistrationAny,
|
||||
type ThemedToken,
|
||||
createCssVariablesTheme,
|
||||
createSingletonShorthands,
|
||||
createdBundledHighlighter,
|
||||
} from 'shiki/core';
|
||||
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
||||
import { type BundledLanguage, bundledLanguages } from 'shiki/langs';
|
||||
import { bundledThemes } from 'shiki/themes';
|
||||
|
||||
import { nullIfNever } from '@/lib/typescript';
|
||||
import { customThemes } from './customThemes';
|
||||
import { plainHighlight } from './plain-highlight';
|
||||
|
||||
export const DEFAULT_THEMES: CustomizationThemedCodeTheme = {
|
||||
light: CustomizationCodeTheme.DefaultLight,
|
||||
dark: CustomizationCodeTheme.DefaultDark,
|
||||
};
|
||||
|
||||
export type HighlightTheme = {
|
||||
bg?: string;
|
||||
fg?: string;
|
||||
themes: {
|
||||
light: ThemeRegistrationAny;
|
||||
dark: ThemeRegistrationAny;
|
||||
};
|
||||
lines: HighlightLine[];
|
||||
};
|
||||
|
||||
export type HighlightLine = {
|
||||
highlighted: boolean;
|
||||
tokens: HighlightToken[];
|
||||
@@ -36,12 +55,12 @@ export type RenderedInline = {
|
||||
|
||||
const isSafari =
|
||||
typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
const theme = createCssVariablesTheme();
|
||||
|
||||
// Merge bundled Shiki themes with our custom themes so both are available to the highlighter
|
||||
const { getSingletonHighlighter } = createSingletonShorthands(
|
||||
createdBundledHighlighter<any, any>({
|
||||
langs: bundledLanguages,
|
||||
themes: {},
|
||||
themes: { ...bundledThemes, ...customThemes },
|
||||
engine: () => createJavaScriptRegexEngine({ forgiving: true, target: 'ES2018' }),
|
||||
})
|
||||
);
|
||||
@@ -49,12 +68,15 @@ const { getSingletonHighlighter } = createSingletonShorthands(
|
||||
/**
|
||||
* Preload the highlighter for a code block.
|
||||
*/
|
||||
export async function preloadHighlight(block: DocumentBlockCode) {
|
||||
export async function preloadHighlight(
|
||||
block: DocumentBlockCode,
|
||||
themes: CustomizationThemedCodeTheme = DEFAULT_THEMES
|
||||
) {
|
||||
const langName = getBlockLang(block);
|
||||
if (langName) {
|
||||
await getSingletonHighlighter({
|
||||
langs: [langName],
|
||||
themes: [theme],
|
||||
themes: [themes.light, themes.dark],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -67,8 +89,9 @@ export async function highlight(
|
||||
inlines: RenderedInline[],
|
||||
options?: {
|
||||
evaluateInlineExpression?: (expr: string) => string;
|
||||
themes?: CustomizationThemedCodeTheme;
|
||||
}
|
||||
): Promise<HighlightLine[]> {
|
||||
): Promise<HighlightTheme> {
|
||||
const langName = getBlockLang(block);
|
||||
|
||||
if (!langName || (isSafari && ['powershell', 'cpp'].includes(langName))) {
|
||||
@@ -80,13 +103,20 @@ export async function highlight(
|
||||
return plainHighlight(block, inlines, options);
|
||||
}
|
||||
|
||||
const themes = options?.themes ?? DEFAULT_THEMES;
|
||||
|
||||
const code = getPlainCodeBlock(block, undefined, options);
|
||||
|
||||
const highlighter = await getSingletonHighlighter({
|
||||
langs: [langName],
|
||||
themes: [theme],
|
||||
themes: [themes.light, themes.dark],
|
||||
});
|
||||
|
||||
const resolvedThemes = {
|
||||
light: highlighter.getTheme(themes.light),
|
||||
dark: highlighter.getTheme(themes.dark),
|
||||
};
|
||||
|
||||
let tokenizeMaxLineLength = 400;
|
||||
// In some cases, people will use unindented code blocks with a single line.
|
||||
// In this case, we can safely increase the max line length to avoid not highlighting the code.
|
||||
@@ -94,38 +124,47 @@ export async function highlight(
|
||||
tokenizeMaxLineLength = 5000;
|
||||
}
|
||||
|
||||
const lines = highlighter.codeToTokensBase(code, {
|
||||
const result = highlighter.codeToTokens(code, {
|
||||
lang: langName,
|
||||
theme,
|
||||
themes: resolvedThemes,
|
||||
// Shiki's light-dark() CSS function provides different colors for light/dark modes based on the resolved themes
|
||||
defaultColor: 'light-dark()',
|
||||
tokenizeMaxLineLength,
|
||||
});
|
||||
|
||||
const lines = result.tokens;
|
||||
|
||||
let currentIndex = 0;
|
||||
return lines.map((tokens, index) => {
|
||||
const lineBlock = block.nodes[index];
|
||||
const result: HighlightToken[] = [];
|
||||
return {
|
||||
bg: result.bg,
|
||||
fg: result.fg,
|
||||
themes: resolvedThemes,
|
||||
lines: lines.map((tokens, index) => {
|
||||
const lineBlock = block.nodes[index];
|
||||
const result: HighlightToken[] = [];
|
||||
|
||||
const eatToken = (): PositionedToken | null => {
|
||||
const token = tokens.shift();
|
||||
if (token) {
|
||||
currentIndex += token.content.length;
|
||||
const eatToken = (): PositionedToken | null => {
|
||||
const token = tokens.shift();
|
||||
if (token) {
|
||||
currentIndex += token.content.length;
|
||||
}
|
||||
return token
|
||||
? { ...token, start: currentIndex - token.content.length, end: currentIndex }
|
||||
: null;
|
||||
};
|
||||
|
||||
while (tokens.length > 0) {
|
||||
result.push(...matchTokenAndInlines(eatToken, inlines));
|
||||
}
|
||||
return token
|
||||
? { ...token, start: currentIndex - token.content.length, end: currentIndex }
|
||||
: null;
|
||||
};
|
||||
|
||||
while (tokens.length > 0) {
|
||||
result.push(...matchTokenAndInlines(eatToken, inlines));
|
||||
}
|
||||
currentIndex += 1; // for the \n
|
||||
|
||||
currentIndex += 1; // for the \n
|
||||
|
||||
return {
|
||||
highlighted: Boolean(lineBlock?.data.highlighted),
|
||||
tokens: result,
|
||||
};
|
||||
});
|
||||
return {
|
||||
highlighted: Boolean(lineBlock?.data.highlighted),
|
||||
tokens: result,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { DocumentBlockCode } from '@gitbook/api';
|
||||
import type { CustomizationThemedCodeTheme, DocumentBlockCode } from '@gitbook/api';
|
||||
|
||||
import { getNodeText } from '@/lib/document';
|
||||
|
||||
import type { HighlightLine, HighlightToken, RenderedInline } from './highlight';
|
||||
import { bundledThemesInfo } from 'shiki/themes';
|
||||
import { customThemes } from './customThemes';
|
||||
import type { HighlightTheme, HighlightToken, RenderedInline } from './highlight';
|
||||
|
||||
/**
|
||||
* Parse a code block without highlighting it.
|
||||
@@ -12,42 +13,59 @@ export function plainHighlight(
|
||||
inlines: RenderedInline[],
|
||||
options?: {
|
||||
evaluateInlineExpression?: (expr: string) => string;
|
||||
themes?: CustomizationThemedCodeTheme;
|
||||
}
|
||||
): HighlightLine[] {
|
||||
): HighlightTheme {
|
||||
const inlinesCopy = Array.from(inlines);
|
||||
|
||||
return block.nodes.map((lineBlock) => {
|
||||
const tokens: HighlightToken[] = lineBlock.nodes.map((node) => {
|
||||
if (node.object === 'text') {
|
||||
return {
|
||||
type: 'plain',
|
||||
content: getNodeText(node),
|
||||
};
|
||||
}
|
||||
// Resolve theme objects: first check bundled Shiki themes, then custom themes, finally fallback to type-only
|
||||
// The type-only fallback ensures HighlightTheme always has valid theme objects even when theme names don't match
|
||||
const themes = {
|
||||
light: bundledThemesInfo.find((theme) => theme.id === options?.themes?.light) ??
|
||||
Object.values(customThemes).find((theme) => theme.name === options?.themes?.light) ?? {
|
||||
type: 'light',
|
||||
},
|
||||
dark: bundledThemesInfo.find((theme) => theme.id === options?.themes?.dark) ??
|
||||
Object.values(customThemes).find((theme) => theme.name === options?.themes?.dark) ?? {
|
||||
type: 'dark',
|
||||
},
|
||||
};
|
||||
|
||||
if (node.type === 'expression') {
|
||||
return {
|
||||
type: 'plain',
|
||||
content: options?.evaluateInlineExpression?.(node.data.expression) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const inline = inlinesCopy.shift();
|
||||
return {
|
||||
type: 'annotation',
|
||||
body: inline?.body ?? null,
|
||||
children: [
|
||||
{
|
||||
return {
|
||||
themes: themes,
|
||||
lines: block.nodes.map((lineBlock) => {
|
||||
const tokens: HighlightToken[] = lineBlock.nodes.map((node) => {
|
||||
if (node.object === 'text') {
|
||||
return {
|
||||
type: 'plain',
|
||||
content: getNodeText(node),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
highlighted: Boolean(lineBlock.data.highlighted),
|
||||
tokens,
|
||||
};
|
||||
});
|
||||
if (node.type === 'expression') {
|
||||
return {
|
||||
type: 'plain',
|
||||
content: options?.evaluateInlineExpression?.(node.data.expression) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const inline = inlinesCopy.shift();
|
||||
return {
|
||||
type: 'annotation',
|
||||
body: inline?.body ?? null,
|
||||
children: [
|
||||
{
|
||||
type: 'plain',
|
||||
content: getNodeText(node),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
highlighted: Boolean(lineBlock.data.highlighted),
|
||||
tokens,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DocumentBlockDrawing } from '@gitbook/api';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { resolveContentRefInDocument } from '@/lib/references';
|
||||
|
||||
import { Image } from '../utils';
|
||||
import type { BlockProps } from './Block';
|
||||
@@ -8,11 +8,11 @@ import { Caption } from './Caption';
|
||||
import { imageBlockSizes } from './Images';
|
||||
|
||||
export async function Drawing(props: BlockProps<DocumentBlockDrawing>) {
|
||||
const { block, context } = props;
|
||||
const { document, block, context } = props;
|
||||
|
||||
const resolved =
|
||||
block.data.ref && context.contentContext
|
||||
? await resolveContentRef(block.data.ref, context.contentContext)
|
||||
? await resolveContentRefInDocument(document, block.data.ref, context.contentContext)
|
||||
: null;
|
||||
if (!resolved) {
|
||||
return null;
|
||||
@@ -31,7 +31,7 @@ export async function Drawing(props: BlockProps<DocumentBlockDrawing>) {
|
||||
alt="Drawing"
|
||||
sizes={imageBlockSizes}
|
||||
zoom
|
||||
loading="lazy"
|
||||
loading={context.mode === 'print' ? 'eager' : 'lazy'}
|
||||
/>
|
||||
</Caption>
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
sources={{ light: { src: embed.icon } }}
|
||||
sizes={[{ width: 20 }]}
|
||||
resize={context.contentContext.imageResizer}
|
||||
loading="lazy"
|
||||
loading={context.mode === 'print' ? 'eager' : 'lazy'}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
||||
|
||||
const title = getNodeFragmentByType(block, 'expandable-title');
|
||||
const body = getNodeFragmentByType(block, 'expandable-body');
|
||||
// @ts-expect-error should be removed once we update the API
|
||||
const isDefaultExpanded = block.data.defaultExpanded ?? false;
|
||||
|
||||
const titleParagraph = title?.nodes[0];
|
||||
|
||||
@@ -28,7 +30,7 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
||||
return (
|
||||
<Details
|
||||
id={id}
|
||||
open={context.mode === 'print'}
|
||||
open={context.mode === 'print' || isDefaultExpanded}
|
||||
className={tcls('scroll-mt-(--content-scroll-margin)', style)}
|
||||
>
|
||||
<summary
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type DocumentBlockFile, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
|
||||
import { t } from '@/intl/translate';
|
||||
import { getSimplifiedContentType } from '@/lib/files';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { resolveContentRefInDocument } from '@/lib/references';
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { Button, Link } from '../primitives';
|
||||
@@ -12,13 +12,17 @@ import { Caption } from './Caption';
|
||||
import { FileIcon } from './FileIcon';
|
||||
|
||||
export async function File(props: BlockProps<DocumentBlockFile>) {
|
||||
const { block, context } = props;
|
||||
const { document, block, context } = props;
|
||||
|
||||
if (!context.contentContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contentRef = await resolveContentRef(block.data.ref, context.contentContext);
|
||||
const contentRef = await resolveContentRefInDocument(
|
||||
document,
|
||||
block.data.ref,
|
||||
context.contentContext
|
||||
);
|
||||
const file = contentRef?.file;
|
||||
|
||||
if (!file) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } from '@gitbook/api';
|
||||
|
||||
import { Image, type ImageResponsiveSize } from '@/components/utils';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { resolveContentRefInDocument } from '@/lib/references';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
@@ -81,12 +81,14 @@ async function ImageBlock(props: {
|
||||
isEstimatedOffscreen: boolean;
|
||||
withFrame?: boolean;
|
||||
}) {
|
||||
const { block, context, isEstimatedOffscreen, withFrame } = props;
|
||||
const { document, block, context, isEstimatedOffscreen, withFrame } = props;
|
||||
|
||||
const [src, darkSrc] = await Promise.all([
|
||||
context.contentContext ? resolveContentRef(block.data.ref, context.contentContext) : null,
|
||||
context.contentContext
|
||||
? resolveContentRefInDocument(document, block.data.ref, context.contentContext)
|
||||
: null,
|
||||
block.data.refDark && context.contentContext
|
||||
? resolveContentRef(block.data.refDark, context.contentContext)
|
||||
? resolveContentRefInDocument(document, block.data.refDark, context.contentContext)
|
||||
: null,
|
||||
]);
|
||||
|
||||
@@ -96,36 +98,6 @@ async function ImageBlock(props: {
|
||||
|
||||
return (
|
||||
<div className={tcls('relative', 'overflow-hidden')}>
|
||||
{/* Frame grid */}
|
||||
{withFrame && (
|
||||
<div
|
||||
className={tcls(
|
||||
'absolute',
|
||||
'-top-0.5',
|
||||
'-left-0.5',
|
||||
'right-px',
|
||||
'bottom-px',
|
||||
'opacity-40',
|
||||
'dark:opacity-[0.1]',
|
||||
'bg-[length:24px_24px,24px_24px]',
|
||||
'bg-[linear-gradient(to_right,_rgb(234,235,238)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(234,235,238)_1px,_transparent_1px)]',
|
||||
'dark:bg-[linear-gradient(to_right,_rgb(122,128,139)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(122,128,139)_1px,_transparent_1px)]',
|
||||
'bg-repeat'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shadow overlay */}
|
||||
{withFrame && (
|
||||
<div
|
||||
className={tcls(
|
||||
'pointer-events-none absolute inset-0 rounded-2xl',
|
||||
'shadow-[inset_0_0_10px_10px_rgba(255,255,255,0.9)]',
|
||||
'dark:shadow-[inset_0_0_10px_10px_rgb(29,29,29)]'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Caption {...props} fit>
|
||||
<Image
|
||||
alt={block.data.alt ?? ''}
|
||||
@@ -143,7 +115,7 @@ async function ImageBlock(props: {
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
loading={isEstimatedOffscreen ? 'lazy' : 'eager'}
|
||||
loading={isEstimatedOffscreen && context.mode !== 'print' ? 'lazy' : 'eager'}
|
||||
zoom
|
||||
inlineStyle={{
|
||||
maxWidth: '100%',
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
'use client';
|
||||
import { clearAdaptiveVisitorClaimsCache } from '@/components/Adaptive/AdaptiveVisitorContextProvider';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import { useAI, useAIChatController, useAIChatState } from '../AI';
|
||||
import { useSearch } from '../Search';
|
||||
import { useSetSearchState } from '../Search';
|
||||
import { Button, type ButtonProps, Input } from '../primitives';
|
||||
|
||||
type ClaimInput = {
|
||||
key: string;
|
||||
value: string | number | boolean | null;
|
||||
};
|
||||
|
||||
export function InlineActionButton(
|
||||
props: { action: 'ask' | 'search'; query?: string } & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
|
||||
props: {
|
||||
action: 'ask' | 'search' | 'set-claim';
|
||||
query?: string;
|
||||
claims?: ClaimInput[];
|
||||
} & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
|
||||
) {
|
||||
const { action, query, buttonProps } = props;
|
||||
const { action, query, claims, buttonProps } = props;
|
||||
|
||||
const { assistants } = useAI();
|
||||
const chatController = useAIChatController();
|
||||
const chatState = useAIChatState();
|
||||
const [, setSearchState] = useSearch();
|
||||
const setSearchState = useSetSearchState();
|
||||
const language = useLanguage();
|
||||
|
||||
const handleSubmit = (value: string) => {
|
||||
@@ -32,18 +42,40 @@ export function InlineActionButton(
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetClaimAction = () => {
|
||||
if (!claims?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedClaims: Record<string, string | number | boolean | null> = {};
|
||||
for (const claim of claims) {
|
||||
mergedClaims[claim.key] = claim.value;
|
||||
}
|
||||
|
||||
document.cookie = `gitbook-visitor-public=${encodeURIComponent(
|
||||
JSON.stringify(mergedClaims)
|
||||
)}; path=/; SameSite=Lax`;
|
||||
|
||||
clearAdaptiveVisitorClaimsCache();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const icon =
|
||||
action === 'ask' && buttonProps.icon === 'gitbook-assistant' && assistants.length > 0
|
||||
? assistants[0]?.icon
|
||||
: buttonProps.icon;
|
||||
|
||||
if (action === 'set-claim') {
|
||||
return <Button {...buttonProps} onClick={handleSetClaimAction} />;
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
return (
|
||||
<Input
|
||||
inline
|
||||
label={buttonProps.label as string}
|
||||
sizing="medium"
|
||||
className="inline-flex max-w-full leading-normal [transition-property:translate,opacity,box-shadow,background,border]"
|
||||
className="inline-flex max-w-full grow"
|
||||
submitButton={{
|
||||
label: tString(language, action === 'ask' ? 'send' : 'search'),
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveContentRef, resolveContentRefFallback } from '@/lib/references';
|
||||
import { resolveContentRefFallback, resolveContentRefInDocument } from '@/lib/references';
|
||||
import * as api from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
import { Button, type ButtonProps } from '../primitives';
|
||||
@@ -14,15 +14,18 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
|
||||
variant: inline.data.kind,
|
||||
icon: inline.data.icon as IconName | undefined,
|
||||
size: 'medium',
|
||||
className: 'leading-normal',
|
||||
};
|
||||
|
||||
const ButtonImplementation = () => {
|
||||
if ('action' in inline.data && 'query' in inline.data.action) {
|
||||
if ('action' in inline.data) {
|
||||
const actionData = inline.data.action;
|
||||
|
||||
return (
|
||||
<InlineActionButton
|
||||
action={inline.data.action.action}
|
||||
query={inline.data.action.query ?? ''}
|
||||
action={actionData.action}
|
||||
query={actionData.query ?? ''}
|
||||
// @ts-expect-error -- Temporary override until `set-claim` is available in @gitbook/api.
|
||||
claims={actionData.claims}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
@@ -37,9 +40,7 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
|
||||
|
||||
const inlineElement = (
|
||||
// Set the leading to have some vertical space between adjacent buttons
|
||||
<span className="inline-button leading-12 [&:has(+.inline-button)]:mr-2">
|
||||
<ButtonImplementation />
|
||||
</span>
|
||||
<ButtonImplementation />
|
||||
);
|
||||
|
||||
return inlineElement;
|
||||
@@ -48,13 +49,13 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
|
||||
export async function InlineLinkButton(
|
||||
props: InlineProps<api.DocumentInlineButton> & { buttonProps: ButtonProps }
|
||||
) {
|
||||
const { inline, context, buttonProps } = props;
|
||||
const { document, inline, context, buttonProps } = props;
|
||||
|
||||
if (!('ref' in inline.data)) return;
|
||||
|
||||
const resolved =
|
||||
context.contentContext && inline.data.ref
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext)
|
||||
? await resolveContentRefInDocument(document, inline.data.ref, context.contentContext)
|
||||
: null;
|
||||
|
||||
const href =
|
||||
|
||||
@@ -2,20 +2,22 @@ import type { GitBookBaseContext } from '@/lib/context';
|
||||
import type { DocumentInlineImage } from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
|
||||
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
|
||||
import { type ResolvedContentRef, resolveContentRefInDocument } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Image } from '../utils';
|
||||
import type { InlineProps } from './Inline';
|
||||
|
||||
export async function InlineImage(props: InlineProps<DocumentInlineImage>) {
|
||||
const { inline, context, ancestorInlines } = props;
|
||||
const { document, inline, context, ancestorInlines } = props;
|
||||
const { size = 'original' } = inline.data;
|
||||
|
||||
const [src, darkSrc] = await Promise.all([
|
||||
context.contentContext ? resolveContentRef(inline.data.ref, context.contentContext) : null,
|
||||
context.contentContext
|
||||
? resolveContentRefInDocument(document, inline.data.ref, context.contentContext)
|
||||
: null,
|
||||
inline.data.refDark && context.contentContext
|
||||
? resolveContentRef(inline.data.refDark, context.contentContext)
|
||||
? resolveContentRefInDocument(document, inline.data.refDark, context.contentContext)
|
||||
: null,
|
||||
]);
|
||||
|
||||
@@ -49,7 +51,7 @@ export async function InlineImage(props: InlineProps<DocumentInlineImage>) {
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
loading="lazy"
|
||||
loading={context.mode === 'print' ? 'eager' : 'lazy'}
|
||||
style={[size === 'line' ? ['max-h-lh', 'h-lh', 'w-auto'] : null]}
|
||||
inline
|
||||
zoom={!isInLink}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { getSpaceLanguage, tString } from '@/intl/server';
|
||||
import { type TranslationLanguage, languages } from '@/intl/translations';
|
||||
import {
|
||||
type ResolvedContentRef,
|
||||
resolveContentRef,
|
||||
resolveContentRefFallback,
|
||||
resolveContentRefInDocument,
|
||||
} from '@/lib/references';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { StyledLink } from '../../primitives';
|
||||
@@ -15,10 +15,10 @@ import { NotFoundRefHoverCard } from '../NotFoundRefHoverCard';
|
||||
import { InlineLinkTooltip } from './InlineLinkTooltip';
|
||||
|
||||
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const { inline, document, context, ancestorInlines } = props;
|
||||
const { document, inline, context, ancestorInlines } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
? await resolveContentRefInDocument(document, inline.data.ref, context.contentContext, {
|
||||
// We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link.
|
||||
resolveAnchorText: false,
|
||||
})
|
||||
|
||||
@@ -182,8 +182,16 @@ function getOrderedListItemPrefixContent(input: {
|
||||
}): string {
|
||||
const { parent, block } = input;
|
||||
const start = parent.data.start ?? 1;
|
||||
const index = parent.nodes.findIndex((node) => node.key === block.key) ?? 0;
|
||||
const value = index + start;
|
||||
|
||||
let index: number;
|
||||
// If the block has a key, use it to find the index. Otherwise, use the object reference.
|
||||
if (block.key) {
|
||||
index = parent.nodes.findIndex((node) => node.key === block.key);
|
||||
} else {
|
||||
index = parent.nodes.findIndex((node) => node === block);
|
||||
}
|
||||
|
||||
const value = index >= 0 ? index + start : Math.max(start, 1);
|
||||
switch (input.depth % 3) {
|
||||
// Use numbers
|
||||
case 0: {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { type DocumentInlineMention, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
|
||||
import { StyledLink } from '@/components/primitives';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { resolveContentRefInDocument } from '@/lib/references';
|
||||
|
||||
import type { InlineProps } from './Inline';
|
||||
|
||||
export async function Mention(props: InlineProps<DocumentInlineMention>) {
|
||||
const { inline, context } = props;
|
||||
const { document, inline, context } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
? await resolveContentRefInDocument(document, inline.data.ref, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -42,7 +42,17 @@ export function getOpenAPIContext(args: {
|
||||
check: <Icon icon="check" />,
|
||||
lock: <Icon icon="lock" />,
|
||||
},
|
||||
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
|
||||
renderCodeBlock: (codeProps) => (
|
||||
<PlainCodeBlock
|
||||
{...codeProps}
|
||||
themeKey="openapi"
|
||||
themes={
|
||||
context && 'customization' in context
|
||||
? context.customization.styling.codeTheme.openapi
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
renderDocument: (documentProps) => (
|
||||
<DocumentView
|
||||
document={documentProps.document as JSONDocument}
|
||||
|
||||
@@ -273,9 +273,9 @@ body {
|
||||
.scalar-activate-button {
|
||||
@apply flex gap-2 items-center;
|
||||
@apply bg-primary-original text-contrast-primary-original hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none circular-corners:rounded-full circular-corners:px-3 place-self-start;
|
||||
@apply shadow-sm shadow-primary dark:shadow-primary-1 hover:shadow-md active:shadow-none;
|
||||
@apply depth-subtle:shadow-xs hover:depth-subtle:shadow-md focus-visible:depth-subtle:shadow-md active:depth-subtle:shadow-xs;
|
||||
@apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
|
||||
@apply hover:scale-105 active:scale-100 transition-all;
|
||||
@apply hover:depth-subtle:-translate-y-px focus-visible:depth-subtle:-translate-y-px active:depth-subtle:translate-y-0 transition-all;
|
||||
@apply grow-0 shrink-0 truncate;
|
||||
@apply text-[13px] px-2 py-0.5 font-mono font-medium [word-spacing:-2px];
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@
|
||||
.openapi-panel,
|
||||
.openapi-codesample,
|
||||
.openapi-response-examples {
|
||||
@apply border shrink min-h-40 overflow-hidden rounded-lg straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
|
||||
@apply border shrink min-h-32 overflow-hidden rounded-lg straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base border-tint-subtle depth-subtle:shadow-sm;
|
||||
}
|
||||
|
||||
.openapi-response-examples-panel {
|
||||
@@ -541,7 +541,9 @@
|
||||
.openapi-panel pre,
|
||||
.openapi-codesample pre,
|
||||
.openapi-response-examples pre {
|
||||
@apply bg-transparent border-none rounded-none shrink shadow-none;
|
||||
@apply border-none rounded-none shrink shadow-none;
|
||||
/* Override the code block's background to transparent so it matches with the header and footer */
|
||||
@apply bg-transparent!
|
||||
}
|
||||
|
||||
.openapi-panel-heading {
|
||||
@@ -845,6 +847,14 @@ body:has(.openapi-select-popover) {
|
||||
@apply -mx-3 px-3 py-2.5 transition-all flex flex-col ring-tint-subtle;
|
||||
}
|
||||
|
||||
.openapi-schemas-disclosure .openapi-schema-properties > .openapi-disclosure > .openapi-disclosure-trigger {
|
||||
@apply px-3;
|
||||
}
|
||||
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-disclosure > .openapi-disclosure-trigger {
|
||||
@apply px-6;
|
||||
}
|
||||
|
||||
.openapi-disclosure:not(
|
||||
.openapi-disclosure-group .openapi-disclosure,
|
||||
.openapi-schema-alternatives .openapi-disclosure,
|
||||
@@ -873,7 +883,7 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger-label {
|
||||
@apply absolute right-3 mr-px px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
|
||||
@apply absolute right-3 font-sans! mr-px px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger-label span {
|
||||
|
||||
@@ -9,8 +9,12 @@ import { getTextAlignment } from './utils';
|
||||
export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
|
||||
const { block, style, ...contextProps } = props;
|
||||
|
||||
// InlineActionButtons use flex-grow to take the available width. This requires the parent to be a flex container.
|
||||
const inlineButtonStyle =
|
||||
'has-[.button,input]:flex has-[.button,input]:flex-wrap has-[.button,input]:gap-2 has-[.button,input]:items-center';
|
||||
|
||||
return (
|
||||
<p className={tcls(style, getTextAlignment(block.data?.align))}>
|
||||
<p className={tcls(inlineButtonStyle, style, getTextAlignment(block.data?.align))}>
|
||||
<Inlines {...contextProps} nodes={block.nodes} ancestorInlines={[]} />
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { DocumentBlockReusableContent } from '@gitbook/api';
|
||||
|
||||
import { getDataOrNull } from '@/lib/data';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { resolveContentRefInDocument } from '@/lib/references';
|
||||
import type { BlockProps } from './Block';
|
||||
import { UnwrappedBlocks } from './Blocks';
|
||||
|
||||
// TODO-DEREF: Remove this once we have rolled out the new reusable content deref in the API.
|
||||
export async function ReusableContent(props: BlockProps<DocumentBlockReusableContent>) {
|
||||
const { block, context, ancestorBlocks } = props;
|
||||
const { document, block, context, ancestorBlocks } = props;
|
||||
|
||||
console.error('Reusable content should be handled at the API level, this should not be called');
|
||||
|
||||
if (!context.contentContext) {
|
||||
throw new Error('Expected a content context to render a reusable content block');
|
||||
@@ -16,7 +19,7 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
|
||||
? context.contentContext.dataFetcher.withToken({ apiToken: block.meta.token })
|
||||
: context.contentContext.dataFetcher;
|
||||
|
||||
const resolved = await resolveContentRef(block.data.ref, {
|
||||
const resolved = await resolveContentRefInDocument(document, block.data.ref, {
|
||||
...context.contentContext,
|
||||
dataFetcher,
|
||||
});
|
||||
@@ -30,7 +33,7 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
|
||||
return null;
|
||||
}
|
||||
|
||||
const document = await getDataOrNull(
|
||||
const reusableContentDocument = await getDataOrNull(
|
||||
dataFetcher.getRevisionReusableContentDocument({
|
||||
spaceId: reusableContent.context.space.id,
|
||||
revisionId: reusableContent.context.revisionId,
|
||||
@@ -38,14 +41,14 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
|
||||
})
|
||||
);
|
||||
|
||||
if (!document) {
|
||||
if (!reusableContentDocument) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UnwrappedBlocks
|
||||
nodes={document.nodes}
|
||||
document={document}
|
||||
nodes={reusableContentDocument.nodes}
|
||||
document={reusableContentDocument}
|
||||
ancestorBlocks={[...ancestorBlocks, block]}
|
||||
context={{
|
||||
...context,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LinkBox, LinkOverlay } from '@/components/primitives';
|
||||
import { Image } from '@/components/utils';
|
||||
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
|
||||
import { type ResolvedContentRef, resolveContentRefInDocument } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import {
|
||||
CardsImageObjectFit,
|
||||
@@ -18,7 +18,7 @@ export async function RecordCard(
|
||||
record: TableRecordKV;
|
||||
}
|
||||
) {
|
||||
const { view, record, context, block, isOffscreen } = props;
|
||||
const { view, record, context, block, isOffscreen, document } = props;
|
||||
|
||||
const { dark, light } = getRecordCardCovers(record[1], view);
|
||||
const targetRef = view.targetDefinition
|
||||
@@ -27,13 +27,13 @@ export async function RecordCard(
|
||||
|
||||
const [lightCover, darkCover, target] = await Promise.all([
|
||||
light.contentRef && context.contentContext
|
||||
? resolveContentRef(light.contentRef, context.contentContext)
|
||||
? resolveContentRefInDocument(document, light.contentRef, context.contentContext)
|
||||
: null,
|
||||
dark.contentRef && context.contentContext
|
||||
? resolveContentRef(dark.contentRef, context.contentContext)
|
||||
? resolveContentRefInDocument(document, dark.contentRef, context.contentContext)
|
||||
: null,
|
||||
targetRef && context.contentContext
|
||||
? resolveContentRef(targetRef, context.contentContext)
|
||||
? resolveContentRefInDocument(document, targetRef, context.contentContext)
|
||||
: null,
|
||||
]);
|
||||
|
||||
@@ -120,7 +120,7 @@ export async function RecordCard(
|
||||
: ['h-auto', 'aspect-video'],
|
||||
objectFits
|
||||
)}
|
||||
loading={isOffscreen ? 'lazy' : 'eager'}
|
||||
loading={isOffscreen && context.mode !== 'print' ? 'lazy' : 'eager'}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
|
||||
@@ -12,7 +12,7 @@ import { StyledLink } from '@/components/primitives';
|
||||
import { Image } from '@/components/utils';
|
||||
import { getNodeFragmentByName } from '@/lib/document';
|
||||
import { getSimplifiedContentType } from '@/lib/files';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { resolveContentRefInDocument } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import type { BlockProps } from '../Block';
|
||||
@@ -176,7 +176,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
const files = await Promise.all(
|
||||
value.map((fileId) =>
|
||||
context.contentContext
|
||||
? resolveContentRef(
|
||||
? resolveContentRefInDocument(
|
||||
document,
|
||||
{
|
||||
kind: 'file',
|
||||
file: fileId,
|
||||
@@ -229,7 +230,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
},
|
||||
},
|
||||
}}
|
||||
loading="lazy"
|
||||
loading={context.mode === 'print' ? 'eager' : 'lazy'}
|
||||
/>
|
||||
) : (
|
||||
<FileIcon
|
||||
@@ -250,7 +251,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
}
|
||||
const resolved =
|
||||
value && context.contentContext
|
||||
? await resolveContentRef(value, context.contentContext, {
|
||||
? await resolveContentRefInDocument(document, value, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
iconStyle: ['mr-2', 'text-tint-subtle'],
|
||||
})
|
||||
@@ -293,7 +294,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
user: userId,
|
||||
};
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(contentRef, context.contentContext)
|
||||
? await resolveContentRefInDocument(
|
||||
document,
|
||||
contentRef,
|
||||
context.contentContext
|
||||
)
|
||||
: null;
|
||||
if (!resolved) {
|
||||
return null;
|
||||
@@ -366,7 +371,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
}
|
||||
|
||||
const image = context.contentContext
|
||||
? await resolveContentRef(
|
||||
? await resolveContentRefInDocument(
|
||||
document,
|
||||
'ref' in value ? value.ref : value,
|
||||
context.contentContext
|
||||
)
|
||||
@@ -404,7 +410,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
size: image.file?.dimensions,
|
||||
},
|
||||
}}
|
||||
loading="lazy"
|
||||
loading={context.mode === 'print' ? 'eager' : 'lazy'}
|
||||
/>
|
||||
{image.text}
|
||||
</StyledLink>
|
||||
|
||||
@@ -44,8 +44,8 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
// Date is only sticky on larger screens when we use flex-row layout
|
||||
'h-fit w-40 min-w-40 shrink-0 md:sticky md:top-[calc(var(--toc-top-offset)+8px)]!'
|
||||
// Date is only sticky on larger screens when we use flex-row layout, with 0px fallback to prevent flicker and flaky tests before JS sets the variable
|
||||
'h-fit w-40 min-w-40 shrink-0 md:sticky md:top-[calc(var(--toc-top-offset,0px)+8px)]!'
|
||||
)}
|
||||
>
|
||||
<time
|
||||
@@ -60,8 +60,7 @@ export function Update(props: BlockProps<DocumentBlockUpdate>) {
|
||||
{...contextProps}
|
||||
nodes={block.nodes}
|
||||
ancestorBlocks={[...ancestorBlocks, block]}
|
||||
// Remove padding-top from headings when they're the first child (similar to column-first-of-type pattern)
|
||||
style="[&>*:first-child]:!pt-0 flex-1 space-y-4"
|
||||
style="[&>*:first-child]:!pt-0 flex flex-1 flex-col [&>*+*]:mt-5"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,11 +9,11 @@ export function getTextAlignment(textAlignment: TextAlignment | undefined): Clas
|
||||
switch (textAlignment) {
|
||||
case undefined:
|
||||
case TextAlignment.Start:
|
||||
return ['text-start', 'self-start'];
|
||||
return ['text-start', 'self-start', 'justify-start'];
|
||||
case TextAlignment.Center:
|
||||
return ['text-center', 'self-center'];
|
||||
return ['text-center', 'self-center', 'justify-center'];
|
||||
case TextAlignment.End:
|
||||
return ['text-end', 'self-end'];
|
||||
return ['text-end', 'self-end', 'justify-end'];
|
||||
default:
|
||||
return nullIfNever(textAlignment);
|
||||
}
|
||||
|
||||
@@ -79,9 +79,10 @@ export async function EmbeddableDocsPage(
|
||||
orientation="vertical"
|
||||
className="not-hydrated:animate-blur-in-slow"
|
||||
contentClassName="p-4"
|
||||
fadeEdges={context.sections ? [] : ['leading']}
|
||||
leading={{ fade: !context.sections, button: true }}
|
||||
trailing={{ fade: false, button: true }}
|
||||
>
|
||||
<TableOfContents className="pt-0" context={context} />
|
||||
<TableOfContents context={context} withTrademark={false} />
|
||||
<PageBody
|
||||
context={context}
|
||||
page={page}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitbook/embed';
|
||||
import { createChannel } from 'bidc';
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { useAI, useAIChatController } from '@/components/AI';
|
||||
import { CustomizationAIMode } from '@gitbook/api';
|
||||
@@ -19,6 +19,12 @@ const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() =
|
||||
tools: [],
|
||||
}));
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: expected
|
||||
function log(...data: any[]) {
|
||||
// biome-ignore lint/suspicious/noConsole: expected
|
||||
console.log(...data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the API to communicate with the parent window.
|
||||
*/
|
||||
@@ -30,24 +36,32 @@ export function EmbeddableIframeAPI(props: {
|
||||
const router = useRouter();
|
||||
const chatController = useAIChatController();
|
||||
|
||||
// Live ref to avoid adding them as dependencies
|
||||
const refs = useRef({ router, chatController, baseURL });
|
||||
useEffect(() => {
|
||||
refs.current = { router, chatController, baseURL };
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
return chatController.on('open', () => {
|
||||
const { baseURL, router } = refs.current;
|
||||
router.push(`${baseURL}/assistant`);
|
||||
});
|
||||
}, [router, baseURL, chatController]);
|
||||
}, [chatController]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (window.parent === window) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[gitbook] create channel with parent window');
|
||||
log('[gitbook] create channel with parent window');
|
||||
const channel = createChannel();
|
||||
|
||||
channel.receive((payload) => {
|
||||
const { baseURL, router, chatController } = refs.current;
|
||||
const message = payload as ParentToFrameMessage;
|
||||
|
||||
console.log('[gitbook] received message', message);
|
||||
log('[gitbook] received message', message);
|
||||
|
||||
switch (message.type) {
|
||||
case 'clearChat': {
|
||||
@@ -79,10 +93,10 @@ export function EmbeddableIframeAPI(props: {
|
||||
});
|
||||
|
||||
return () => {
|
||||
console.log('[gitbook] cleanup');
|
||||
log('[gitbook] cleanup');
|
||||
channel.cleanup();
|
||||
};
|
||||
}, [chatController, router, baseURL]);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
||||
import { SpaceLayoutServerContext } from '../SpaceLayout';
|
||||
import { TrademarkLink } from '../TableOfContents/Trademark';
|
||||
import { Trademark } from '../TableOfContents/Trademark';
|
||||
import { NavigationLoader } from '../primitives/NavigationLoader';
|
||||
import { EmbeddableIframeAPI } from './EmbeddableIframeAPI';
|
||||
|
||||
@@ -57,8 +57,8 @@ export async function EmbeddableRootLayout({
|
||||
<div className="fixed inset-0 flex flex-col">
|
||||
{children}
|
||||
{context.customization.trademark.enabled ? (
|
||||
<TrademarkLink
|
||||
className="border-tint-solid/3 border-t bg-tint-solid/1 px-4 py-2.5 text-tint/8 ring-0"
|
||||
<Trademark
|
||||
className="rounded-none! border-x-0 border-t border-b-0 bg-tint-solid/1 depth-flat:bg-tint-solid/1 px-4 py-2.5 text-tint/8"
|
||||
context={context}
|
||||
placement={SiteInsightsTrademarkPlacement.Embed}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { tcls } from '@/lib/tailwind';
|
||||
import { ThemeToggler } from '../ThemeToggler';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { FooterLinksGroup } from './FooterLinksGroup';
|
||||
import { SocialLink } from './SocialLink';
|
||||
|
||||
const FOOTER_COLUMNS = 4;
|
||||
|
||||
@@ -20,8 +21,10 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
const hasGroups = groupCount > 0;
|
||||
const hasCopyright = customization.footer.copyright;
|
||||
const hasThemeToggle = customization.themes.toggeable;
|
||||
const socialLinks = customization.socialAccounts.filter((account) => account.display?.footer);
|
||||
|
||||
const mobileOnly = !hasLogo && !hasGroups && !hasCopyright && hasThemeToggle;
|
||||
const mobileOnly =
|
||||
!hasLogo && !hasGroups && !hasCopyright && !socialLinks.length && hasThemeToggle;
|
||||
|
||||
return (
|
||||
<footer
|
||||
@@ -137,6 +140,21 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Social Links
|
||||
socialLinks.length > 0 ? (
|
||||
<div className="col-span-full flex w-full grow items-center justify-center gap-2">
|
||||
{socialLinks.map((account) => (
|
||||
<SocialLink
|
||||
key={`${account.platform}-${account.handle}`}
|
||||
account={account}
|
||||
target={customization.externalLinks.target}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Legal
|
||||
customization.footer.copyright ? (
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
type SiteExternalLinksTarget,
|
||||
type SiteSocialAccount,
|
||||
SiteSocialAccountPlatform,
|
||||
} from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
import { Button } from '../primitives';
|
||||
|
||||
type SocialPlatformData = {
|
||||
label: string;
|
||||
icon: IconName;
|
||||
/** The href to the social platform. `$handle` will be replaced with the account handle. */
|
||||
href: string;
|
||||
};
|
||||
|
||||
const SOCIAL_PLATFORMS: Record<SiteSocialAccountPlatform, SocialPlatformData> = {
|
||||
[SiteSocialAccountPlatform.Twitter]: {
|
||||
label: 'X/Twitter',
|
||||
icon: 'x-twitter',
|
||||
href: 'https://x.com/$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Instagram]: {
|
||||
label: 'Instagram',
|
||||
icon: 'instagram',
|
||||
href: 'https://instagram.com/$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Facebook]: {
|
||||
label: 'Facebook',
|
||||
icon: 'facebook',
|
||||
href: 'https://facebook.com/$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Linkedin]: {
|
||||
label: 'LinkedIn',
|
||||
icon: 'linkedin',
|
||||
href: 'https://linkedin.com/$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Github]: {
|
||||
label: 'GitHub',
|
||||
icon: 'github',
|
||||
href: 'https://github.com/$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Discord]: {
|
||||
label: 'Discord',
|
||||
icon: 'discord',
|
||||
href: 'https://discord.com/$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Slack]: {
|
||||
label: 'Slack',
|
||||
icon: 'slack',
|
||||
href: 'https://join.slack.com/t/$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Youtube]: {
|
||||
label: 'YouTube',
|
||||
icon: 'youtube',
|
||||
href: 'https://youtube.com/@$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Tiktok]: {
|
||||
label: 'TikTok',
|
||||
icon: 'tiktok',
|
||||
href: 'https://tiktok.com/@$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Reddit]: {
|
||||
label: 'Reddit',
|
||||
icon: 'reddit',
|
||||
href: 'https://reddit.com/@$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Bluesky]: {
|
||||
label: 'Bluesky',
|
||||
icon: 'bluesky',
|
||||
href: 'https://bsky.app/profile/$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Mastodon]: {
|
||||
label: 'Mastodon',
|
||||
icon: 'mastodon',
|
||||
href: 'https://mastodon.social/@$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Threads]: {
|
||||
label: 'Threads',
|
||||
icon: 'threads',
|
||||
href: 'https://threads.net/@$handle',
|
||||
},
|
||||
[SiteSocialAccountPlatform.Medium]: {
|
||||
label: 'Medium',
|
||||
icon: 'medium',
|
||||
href: 'https://medium.com/@$handle',
|
||||
},
|
||||
};
|
||||
|
||||
export function SocialLink(props: { account: SiteSocialAccount; target: SiteExternalLinksTarget }) {
|
||||
const { account, target } = props;
|
||||
const platform = SOCIAL_PLATFORMS[account.platform];
|
||||
|
||||
return (
|
||||
<Button
|
||||
iconOnly
|
||||
label={platform.label}
|
||||
href={platform.href.replace('$handle', account.handle)}
|
||||
icon={platform.icon}
|
||||
variant="blank"
|
||||
size="large"
|
||||
target={target}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -42,6 +42,7 @@ export function Header(props: {
|
||||
`h-[${HEADER_HEIGHT_DESKTOP}px]`,
|
||||
'sticky',
|
||||
'top-0',
|
||||
'pt-[env(safe-area-inset-top)]',
|
||||
'z-30',
|
||||
'w-full',
|
||||
'flex-none',
|
||||
|
||||
@@ -115,13 +115,6 @@ function HeaderItemButton(
|
||||
href={href}
|
||||
variant={variant}
|
||||
size="medium"
|
||||
className={tcls(
|
||||
{
|
||||
'button-primary':
|
||||
'theme-bold:bg-header-link theme-bold:text-header-background theme-bold:shadow-none hover:theme-bold:bg-header-link hover:theme-bold:text-header-background hover:theme-bold:shadow-none',
|
||||
'button-secondary': '',
|
||||
}[linkStyle]
|
||||
)}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
|
||||
import { useScrollListener } from '../hooks/useScrollListener';
|
||||
import { Button, type ButtonProps } from '../primitives';
|
||||
|
||||
const globalClassName = 'navigation-open';
|
||||
|
||||
const SCROLL_DISTANCE = 320;
|
||||
|
||||
/**
|
||||
* Button to show/hide the table of content on mobile.
|
||||
*/
|
||||
@@ -18,25 +15,6 @@ export function HeaderMobileMenu(props: ButtonProps) {
|
||||
const language = useLanguage();
|
||||
|
||||
const pathname = usePathname();
|
||||
const hasScrollRef = useRef(false);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggleNavigation = () => {
|
||||
if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) {
|
||||
document.body.classList.remove(globalClassName);
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
document.body.classList.add(globalClassName);
|
||||
window.scrollTo(0, 0);
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const windowRef = useRef(typeof window === 'undefined' ? null : window);
|
||||
useScrollListener(() => {
|
||||
hasScrollRef.current = window.scrollY >= SCROLL_DISTANCE;
|
||||
}, windowRef);
|
||||
|
||||
// Close the navigation when navigating to a page
|
||||
useEffect(() => {
|
||||
@@ -50,8 +28,10 @@ export function HeaderMobileMenu(props: ButtonProps) {
|
||||
iconOnly
|
||||
variant="blank"
|
||||
label={tString(language, 'table_of_contents_button_label')}
|
||||
onClick={toggleNavigation}
|
||||
active={isOpen}
|
||||
onClick={() => {
|
||||
document.body.classList.toggle(globalClassName);
|
||||
}}
|
||||
// Since the button is hidden behind the TOC after toggling, we don't need to keep track of its active state.
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ interface VariantSpace {
|
||||
*/
|
||||
function useVariantSpaceHref(variantSpace: VariantSpace, currentSpacePath: string, active = false) {
|
||||
const currentPathname = useCurrentPagePath();
|
||||
const { metaLinks, currentPage } = useCurrentPageMetadata();
|
||||
const { metaLinks } = useCurrentPageMetadata();
|
||||
|
||||
// We first check if there is an alternate link for the variant space in the current page metadata.
|
||||
const pageHasAlternateForVariant = metaLinks?.alternates.find(
|
||||
@@ -27,17 +27,6 @@ function useVariantSpaceHref(variantSpace: VariantSpace, currentSpacePath: strin
|
||||
return pageHasAlternateForVariant.href;
|
||||
}
|
||||
|
||||
const firstAlternate = metaLinks?.alternates[0];
|
||||
const computed = firstAlternate
|
||||
? {
|
||||
pageID: firstAlternate.pageID,
|
||||
spaceID: firstAlternate.space?.id,
|
||||
}
|
||||
: {
|
||||
pageID: currentPage?.id,
|
||||
spaceID: currentPage?.spaceId,
|
||||
};
|
||||
|
||||
// If there is no alternate link, we reconstruct the URL by swapping the space path.
|
||||
|
||||
// We need to ensure that the variant space URL is not the same as the current space path.
|
||||
@@ -55,18 +44,9 @@ function useVariantSpaceHref(variantSpace: VariantSpace, currentSpacePath: strin
|
||||
|
||||
targetUrl.searchParams.set('fallback', 'true');
|
||||
|
||||
if (computed?.spaceID && computed?.pageID) {
|
||||
targetUrl.searchParams.set('fallbackPageID', computed.pageID);
|
||||
targetUrl.searchParams.set('fallbackSpaceID', computed.spaceID);
|
||||
}
|
||||
|
||||
return targetUrl.toString();
|
||||
}
|
||||
|
||||
if (computed?.spaceID && computed?.pageID) {
|
||||
return `${joinPath(variantSpaceUrl, currentPathname)}?fallback=true&fallbackPageID=${computed.pageID}&fallbackSpaceID=${computed.spaceID}`;
|
||||
}
|
||||
|
||||
// Fallback when the URL path is a relative path (in development mode)
|
||||
return `${joinPath(variantSpaceUrl, currentPathname)}?fallback=true`;
|
||||
}
|
||||
|
||||
@@ -31,3 +31,12 @@ export function isCookiesTrackingDisabled() {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if cookies are accepted.
|
||||
* Return false if cookies are rejected or unknown.
|
||||
*/
|
||||
export function hasApprovedCookies() {
|
||||
const state = getBrowserCookie(GRANTED_COOKIE);
|
||||
return state === 'yes';
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as zustand from 'zustand';
|
||||
|
||||
import { hasApprovedCookies, setCookiesTracking } from '@/components/Insights';
|
||||
import type {
|
||||
GitBookGlobal,
|
||||
GitBookIntegrationEvent,
|
||||
GitBookIntegrationEventCallback,
|
||||
GitBookIntegrationTool,
|
||||
} from '@gitbook/browser-types';
|
||||
import * as React from 'react';
|
||||
import * as zustand from 'zustand';
|
||||
import type { Assistant } from '../AI';
|
||||
|
||||
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
|
||||
@@ -26,6 +26,26 @@ export const integrationsAssistantTools = zustand.createStore<{
|
||||
|
||||
export const integrationAssistants = zustand.createStore<Array<Assistant>>(() => []);
|
||||
|
||||
// Store to track when integrations have been loaded
|
||||
export const integrationsStore = zustand.createStore<{
|
||||
loaded: boolean;
|
||||
}>(() => {
|
||||
return {
|
||||
loaded: false,
|
||||
};
|
||||
});
|
||||
|
||||
type CustomCookieBannerStore = {
|
||||
hasCustomCookieBanner: boolean;
|
||||
};
|
||||
|
||||
// Store for custom cookie banner registration
|
||||
export const customCookieBannerStore = zustand.createStore<CustomCookieBannerStore>(() => {
|
||||
return {
|
||||
hasCustomCookieBanner: false,
|
||||
};
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const gitbookGlobal: GitBookGlobal = {
|
||||
addEventListener: (event, callback) => {
|
||||
@@ -65,6 +85,24 @@ if (typeof window !== 'undefined') {
|
||||
integrationAssistants.setState((state) => state.filter((a) => a.id !== id), true);
|
||||
};
|
||||
},
|
||||
registerCookieBanner: (handler) => {
|
||||
customCookieBannerStore.setState((state) => ({
|
||||
...state,
|
||||
hasCustomCookieBanner: true,
|
||||
}));
|
||||
|
||||
handler({
|
||||
onApprove: () => {
|
||||
setCookiesTracking(true);
|
||||
window.location.reload();
|
||||
},
|
||||
onReject: () => {
|
||||
setCookiesTracking(false);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
},
|
||||
hasApprovedCookies: hasApprovedCookies,
|
||||
};
|
||||
window.GitBook = gitbookGlobal;
|
||||
}
|
||||
@@ -76,12 +114,30 @@ export function useIntegrationAssistants(): Array<Assistant> {
|
||||
return zustand.useStore(integrationAssistants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check if integrations have been loaded.
|
||||
*/
|
||||
export function useIntegrationsLoaded(): boolean {
|
||||
return zustand.useStore(integrationsStore, (state) => state.loaded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check if a custom cookie banner is registered.
|
||||
*/
|
||||
export function useCustomCookieBanner(): CustomCookieBannerStore {
|
||||
return zustand.useStore(customCookieBannerStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the `load` event to all integrations.
|
||||
*/
|
||||
export function LoadIntegrations() {
|
||||
React.useEffect(() => {
|
||||
// Only dispatch 'load' event when there are scripts to load
|
||||
|
||||
dispatchGitBookIntegrationEvent('load');
|
||||
|
||||
integrationsStore.setState({ loaded: true });
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
@@ -89,6 +145,6 @@ export function LoadIntegrations() {
|
||||
/**
|
||||
* Client function to dispatch a GitBook event.
|
||||
*/
|
||||
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: any[]) {
|
||||
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: unknown[]) {
|
||||
events.get(type)?.forEach((handler) => handler(...args));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import { tString } from '@/intl/translate';
|
||||
import type { TranslationLanguage } from '@/intl/translations';
|
||||
import assertNever from 'assert-never';
|
||||
import * as React from 'react';
|
||||
|
||||
export function ImagesLoadingStatus(props: {
|
||||
language: TranslationLanguage;
|
||||
}) {
|
||||
const { language } = props;
|
||||
const state = useImagesLoadingState();
|
||||
return (
|
||||
<p className="text-right text-slate-500 text-xs">
|
||||
{(() => {
|
||||
switch (state.status) {
|
||||
case 'pending':
|
||||
return null;
|
||||
case 'loading':
|
||||
return tString(
|
||||
language,
|
||||
'pdf_images_loading',
|
||||
`${state.loadedImages}`,
|
||||
`${state.totalImages}`
|
||||
);
|
||||
case 'ready':
|
||||
return tString(language, 'pdf_images_loaded');
|
||||
default:
|
||||
assertNever(state);
|
||||
}
|
||||
})()}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of images loading state.
|
||||
*/
|
||||
function useImagesLoadingState() {
|
||||
const [totalImages, setTotalImages] = React.useState<number | null>(null);
|
||||
const [loadedImages, setLoadedImages] = React.useState(0);
|
||||
const attachedImages = React.useRef(new WeakSet<HTMLImageElement>());
|
||||
const rafRef = React.useRef<number>(0);
|
||||
|
||||
const calculateImageStatus = React.useCallback(() => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
const images = Array.from(document.images);
|
||||
setTotalImages(images.length);
|
||||
setLoadedImages(images.filter((image) => image.complete).length);
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => () => cancelAnimationFrame(rafRef.current), []);
|
||||
|
||||
const attachListeners = React.useCallback(
|
||||
(images: HTMLImageElement[]) => {
|
||||
images.forEach((image) => {
|
||||
if (image.complete || attachedImages.current.has(image)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (image.loading !== 'eager') {
|
||||
console.warn('An image is not in "eager" mode', image);
|
||||
}
|
||||
|
||||
attachedImages.current.add(image);
|
||||
image.addEventListener('load', calculateImageStatus);
|
||||
image.addEventListener('error', calculateImageStatus);
|
||||
});
|
||||
},
|
||||
[calculateImageStatus]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
calculateImageStatus();
|
||||
attachListeners(Array.from(document.images));
|
||||
|
||||
let raf: number;
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = requestAnimationFrame(() => {
|
||||
const newImages: HTMLImageElement[] = [];
|
||||
let shouldRecalculate = false;
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type !== 'childList') {
|
||||
continue;
|
||||
}
|
||||
if (mutation.removedNodes.length > 0) {
|
||||
// Images may have been removed; recalculate counts.
|
||||
shouldRecalculate = true;
|
||||
}
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node instanceof HTMLImageElement) {
|
||||
newImages.push(node);
|
||||
shouldRecalculate = true;
|
||||
} else if (node instanceof HTMLElement) {
|
||||
const nestedImages = Array.from(
|
||||
node.getElementsByTagName('img')
|
||||
) as HTMLImageElement[];
|
||||
if (nestedImages.length > 0) {
|
||||
newImages.push(...nestedImages);
|
||||
shouldRecalculate = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (newImages.length > 0) {
|
||||
attachListeners(newImages);
|
||||
}
|
||||
if (shouldRecalculate) {
|
||||
calculateImageStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
observer.disconnect();
|
||||
Array.from(document.images).forEach((image) => {
|
||||
image.removeEventListener('load', calculateImageStatus);
|
||||
image.removeEventListener('error', calculateImageStatus);
|
||||
});
|
||||
};
|
||||
}, [attachListeners, calculateImageStatus]);
|
||||
|
||||
if (totalImages === null) {
|
||||
return { status: 'pending' } as const;
|
||||
}
|
||||
|
||||
return {
|
||||
status: totalImages === loadedImages ? 'ready' : 'loading',
|
||||
totalImages,
|
||||
loadedImages,
|
||||
} as const;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import { notFound } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DocumentView } from '@/components/DocumentView';
|
||||
import { TrademarkLink } from '@/components/TableOfContents/Trademark';
|
||||
import { Trademark } from '@/components/TableOfContents/Trademark';
|
||||
import type { PolymorphicComponentProp } from '@/components/utils/types';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { tString } from '@/intl/translate';
|
||||
@@ -24,11 +24,12 @@ import { tcls } from '@/lib/tailwind';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
import { type PDFSearchParams, getPDFSearchParams } from './urls';
|
||||
|
||||
import { PDFPrintControls } from './PDFPrintControls';
|
||||
import { PageControlButtons } from './PageControlButtons';
|
||||
import { PrintButton } from './PrintButton';
|
||||
import './pdf.css';
|
||||
import { sanitizeGitBookAppURL } from '@/lib/app';
|
||||
import { getPageDocument } from '@/lib/data';
|
||||
import { ImagesLoadingStatus } from './ImagesLoadingStatus';
|
||||
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
@@ -118,28 +119,9 @@ export async function PDFPage(props: {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={tcls('fixed', 'right-12', 'top-12', 'print:hidden', 'z-50')}>
|
||||
<PrintButton
|
||||
title={tString(language, 'pdf_print')}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'text-sm',
|
||||
'text-tint',
|
||||
'hover:text-primary',
|
||||
'p-4',
|
||||
'rounded-full',
|
||||
'bg-white',
|
||||
'shadow-xs',
|
||||
'hover:shadow-md',
|
||||
'border-slate-300',
|
||||
'border'
|
||||
)}
|
||||
>
|
||||
<Icon icon="print" className={tcls('size-6')} />
|
||||
</PrintButton>
|
||||
<div className="fixed top-12 right-12 z-50 flex flex-col items-end gap-2 print:hidden">
|
||||
<PDFPrintControls language={language} />
|
||||
<ImagesLoadingStatus language={language} />
|
||||
</div>
|
||||
|
||||
<PageControlButtons
|
||||
@@ -148,7 +130,7 @@ export async function PDFPage(props: {
|
||||
total={total}
|
||||
trademark={
|
||||
customization.trademark.enabled ? (
|
||||
<TrademarkLink
|
||||
<Trademark
|
||||
context={context}
|
||||
placement={SiteInsightsTrademarkPlacement.Pdf}
|
||||
/>
|
||||
@@ -187,10 +169,8 @@ async function PDFSpaceIntro(props: {
|
||||
|
||||
return (
|
||||
<PrintPage isFirst>
|
||||
<div className={tcls('flex', 'items-center', 'justify-center', 'py-12')}>
|
||||
<h1 className={tcls('text-6xl', 'font-bold')}>
|
||||
{customization.title ?? space.title}
|
||||
</h1>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<h1 className="font-bold text-6xl">{customization.title ?? space.title}</h1>
|
||||
</div>
|
||||
</PrintPage>
|
||||
);
|
||||
@@ -201,18 +181,8 @@ async function PDFPageGroup(props: { space: Space; page: RevisionPageGroup }) {
|
||||
|
||||
return (
|
||||
<PrintPage id={getPagePDFContainerId(page)}>
|
||||
<div
|
||||
className={tcls(
|
||||
'break-before-page',
|
||||
'mt-10',
|
||||
'print:mt-0',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'py-12'
|
||||
)}
|
||||
>
|
||||
<h1 className={tcls('text-5xl', 'font-bold')}>{page.title}</h1>
|
||||
<div className="mt-10 flex break-before-page items-center justify-center py-12 print:mt-0">
|
||||
<h1 className="font-bold text-5xl">{page.title}</h1>
|
||||
</div>
|
||||
</PrintPage>
|
||||
);
|
||||
@@ -227,9 +197,9 @@ async function PDFPageDocument(props: {
|
||||
|
||||
return (
|
||||
<PrintPage id={getPagePDFContainerId(page)}>
|
||||
<h1 className={tcls('text-4xl', 'font-bold')}>{page.title}</h1>
|
||||
<h1 className="font-bold text-4xl">{page.title}</h1>
|
||||
{page.description ? (
|
||||
<p className={tcls('decoration-primary/6', 'mt-2', 'mb-3')}>{page.description}</p>
|
||||
<p className="mt-2 mb-3 decoration-primary/6">{page.description}</p>
|
||||
) : null}
|
||||
|
||||
{document ? (
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { tString } from '@/intl/translate';
|
||||
import type { TranslationLanguage } from '@/intl/translations';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { PrintButton } from './PrintButton';
|
||||
|
||||
export function PDFPrintControls(props: { language: TranslationLanguage }) {
|
||||
const { language } = props;
|
||||
|
||||
return (
|
||||
<PrintButton
|
||||
title={tString(language, 'pdf_print')}
|
||||
className="flex items-center justify-center rounded-full border border-slate-300 bg-white p-4 text-sm text-tint shadow-xs hover:text-primary hover:shadow-md"
|
||||
>
|
||||
<Icon icon="print" className="size-6" />
|
||||
</PrintButton>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { ComponentPropsWithRef } from 'react';
|
||||
|
||||
import type { PolymorphicComponentProp } from '@/components/utils/types';
|
||||
|
||||
export function PrintButton(props: PolymorphicComponentProp<'button'>) {
|
||||
const { className, children, ...rest } = props;
|
||||
|
||||
const onClick = React.useCallback(() => {
|
||||
window.print();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button {...rest} data-testid="print-button" onClick={onClick} className={className}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
export function PrintButton(props: Omit<ComponentPropsWithRef<'button'>, 'onClick'>) {
|
||||
return <button {...props} data-testid="print-button" onClick={() => window.print()} />;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { hasFullWidthBlock, hasMoreThan, hasTopLevelBlock, isNodeEmpty } from '@
|
||||
import type { AncestorRevisionPage } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
import { InlineActionButton } from '../DocumentView/InlineActionButton';
|
||||
import { TrackPageViewEvent } from '../Insights';
|
||||
import { PageFeedbackForm } from '../PageFeedback';
|
||||
import { CurrentPageProvider } from '../hooks/useCurrentPage';
|
||||
@@ -65,6 +66,8 @@ export function PageBody(props: {
|
||||
(page) => page.type !== 'document' || (page.type === 'document' && !page.hidden)
|
||||
).length > 0;
|
||||
|
||||
const pageHasToc = page.layout.tableOfContents && hasVisibleTOCItems;
|
||||
|
||||
return (
|
||||
<CurrentPageProvider page={{ spaceId: context.space.id, pageId: page.id }}>
|
||||
<main
|
||||
@@ -76,12 +79,25 @@ export function PageBody(props: {
|
||||
'@container',
|
||||
pageWidthWide ? 'page-width-wide 3xl:px-8' : 'page-width-default',
|
||||
siteWidthWide ? 'site-width-wide' : 'site-width-default',
|
||||
page.layout.tableOfContents && hasVisibleTOCItems
|
||||
? 'page-has-toc'
|
||||
: 'page-no-toc'
|
||||
pageHasToc ? 'page-has-toc' : 'page-no-toc'
|
||||
)}
|
||||
>
|
||||
<PreservePageLayout siteWidthWide={siteWidthWide} />
|
||||
<div>
|
||||
Special button:
|
||||
<InlineActionButton
|
||||
action="set-claim"
|
||||
claims={[
|
||||
{
|
||||
key: 'format',
|
||||
value: ['docker', 'helm', 'generic', 'maven'][
|
||||
Math.floor(Math.random() * 4)
|
||||
] as string,
|
||||
},
|
||||
]}
|
||||
buttonProps={{ label: 'Ask' }}
|
||||
/>
|
||||
</div>
|
||||
<PreservePageLayout siteWidthWide={siteWidthWide} pageHasToc={pageHasToc} />
|
||||
{page.cover && page.layout.cover && page.layout.coverSize === 'hero' ? (
|
||||
<PageCover as="hero" page={page} cover={page.cover} context={context} />
|
||||
) : null}
|
||||
|
||||
@@ -22,10 +22,6 @@ export async function PageHeader(props: {
|
||||
const { context, page, ancestors, withRSSFeed } = props;
|
||||
const { revision, linker } = context;
|
||||
|
||||
if (!page.layout.title && !page.layout.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasAncestors = ancestors.length > 0;
|
||||
|
||||
// Show page actions if *any* of the actions are enabled
|
||||
@@ -36,6 +32,21 @@ export async function PageHeader(props: {
|
||||
withRSSFeed,
|
||||
].some(Boolean);
|
||||
|
||||
/* When title and description are hidden, only display the page actions if there are any. */
|
||||
if (!page.layout.title && !page.layout.description) {
|
||||
if (!hasPageActions) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<PageActionsDropdown
|
||||
siteTitle={context.site.title}
|
||||
urls={getPageActionsURLs({ context, page, withRSSFeed })}
|
||||
actions={context.customization.pageActions}
|
||||
className="absolute top-8 right-0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
className={tcls(
|
||||
@@ -49,7 +60,7 @@ export async function PageHeader(props: {
|
||||
hasAncestors ? 'page-has-ancestors' : 'page-no-ancestors'
|
||||
)}
|
||||
>
|
||||
{page.layout.tableOfContents && hasPageActions ? (
|
||||
{hasPageActions ? (
|
||||
<PageActionsDropdown
|
||||
siteTitle={context.site.title}
|
||||
urls={getPageActionsURLs({ context, page, withRSSFeed })}
|
||||
|
||||
@@ -11,9 +11,10 @@ import * as React from 'react';
|
||||
* 3. Page 2 with full width block: `body:has(.site-width-wide)` is true
|
||||
*
|
||||
* This component ensures that the layout is preserved while transitioning between the 2 page states (in step 2).
|
||||
* It also preserves the page TOC state (page-has-toc/page-no-toc) to prevent logo sizing issues during navigation.
|
||||
*/
|
||||
export function PreservePageLayout(props: { siteWidthWide: boolean }) {
|
||||
const { siteWidthWide } = props;
|
||||
export function PreservePageLayout(props: { siteWidthWide: boolean; pageHasToc: boolean }) {
|
||||
const { siteWidthWide, pageHasToc } = props;
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
// We use the header as it's an element preserved between page transitions
|
||||
@@ -28,7 +29,15 @@ export function PreservePageLayout(props: { siteWidthWide: boolean }) {
|
||||
} else {
|
||||
header.classList.remove('site-width-wide');
|
||||
}
|
||||
}, [siteWidthWide]);
|
||||
|
||||
if (pageHasToc) {
|
||||
header.classList.add('page-has-toc');
|
||||
header.classList.remove('page-no-toc');
|
||||
} else {
|
||||
header.classList.add('page-no-toc');
|
||||
header.classList.remove('page-has-toc');
|
||||
}
|
||||
}, [siteWidthWide, pageHasToc]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import * as ReactDOM from 'react-dom';
|
||||
|
||||
import { type FontData, getFontData } from '@/fonts';
|
||||
import { fontNotoColorEmoji, fonts } from '@/fonts/default';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { getSpaceLanguage, getSpaceLocale } from '@/intl/server';
|
||||
import { getAssetURL } from '@/lib/assets';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -34,6 +34,7 @@ import type { GitBookAnyContext } from '@/lib/context';
|
||||
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@/lib/env';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
import { AnnouncementDismissedScript } from '../Announcement';
|
||||
import { OperatingSystemClassScript } from './OperatingSystemClassScript';
|
||||
|
||||
function preloadFont(fontData: FontData) {
|
||||
if (fontData.type === 'custom') {
|
||||
@@ -56,16 +57,19 @@ function preloadFont(fontData: FontData) {
|
||||
* It takes care of setting the theme and the language.
|
||||
*/
|
||||
export async function CustomizationRootLayout(props: {
|
||||
/** The class name to apply to the html element. */
|
||||
htmlClassName?: string;
|
||||
/** The class name to apply to the body element. */
|
||||
className?: string;
|
||||
bodyClassName?: string;
|
||||
forcedTheme?: CustomizationThemeMode | null;
|
||||
context: GitBookAnyContext;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { className, context, forcedTheme, children } = props;
|
||||
const { htmlClassName, bodyClassName, context, forcedTheme, children } = props;
|
||||
const customization =
|
||||
'customization' in context ? context.customization : defaultCustomization();
|
||||
|
||||
const locale = getSpaceLocale(context);
|
||||
const language = getSpaceLanguage(context);
|
||||
const tintColor = getTintColor(customization);
|
||||
const mixColor = getTintMixColor(customization.styling.primaryColor, tintColor);
|
||||
@@ -88,7 +92,7 @@ export async function CustomizationRootLayout(props: {
|
||||
return (
|
||||
<html
|
||||
suppressHydrationWarning
|
||||
lang={customization.internationalization.locale}
|
||||
lang={locale}
|
||||
className={tcls(
|
||||
customization.styling.corners && `${customization.styling.corners}-corners`,
|
||||
'theme' in customization.styling && `theme-${customization.styling.theme}`,
|
||||
@@ -106,7 +110,8 @@ export async function CustomizationRootLayout(props: {
|
||||
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
|
||||
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
|
||||
? 'dark'
|
||||
: ''
|
||||
: '',
|
||||
htmlClassName
|
||||
)}
|
||||
>
|
||||
<head>
|
||||
@@ -114,6 +119,8 @@ export async function CustomizationRootLayout(props: {
|
||||
<link rel="privacy-policy" href={customization.privacyPolicy.url} />
|
||||
) : null}
|
||||
|
||||
<OperatingSystemClassScript />
|
||||
|
||||
{/* Inject custom font @font-face rules */}
|
||||
{fontData.type === 'custom' ? <style>{fontData.fontFaceRules}</style> : null}
|
||||
{monospaceFontData.type === 'custom' ? (
|
||||
@@ -131,7 +138,7 @@ export async function CustomizationRootLayout(props: {
|
||||
undefined
|
||||
}
|
||||
>{`
|
||||
:root {
|
||||
:root, .light, .dark [data-color-scheme$="light"], .dark [data-follow-color-scheme="true"]:has([data-color-scheme$="light"]) {
|
||||
${generateColorVariable('primary', customization.styling.primaryColor.light)}
|
||||
${generateColorVariable('tint', tintColor ? tintColor.light : DEFAULT_TINT_COLOR, { mix: mixColor && { color: mixColor.color.light, ratio: mixColor.ratio.light } })}
|
||||
${generateColorVariable('neutral', DEFAULT_TINT_COLOR)}
|
||||
@@ -158,7 +165,7 @@ export async function CustomizationRootLayout(props: {
|
||||
${generateColorVariable('success', successColor.light)}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.dark, :root:not(.dark) [data-color-scheme^="dark"], :root:not(.dark) [data-follow-color-scheme="true"]:has([data-color-scheme^="dark"]) {
|
||||
${generateColorVariable('primary', customization.styling.primaryColor.dark, { darkMode: true })}
|
||||
${generateColorVariable('tint', tintColor ? tintColor.dark : DEFAULT_TINT_COLOR, { darkMode: true, mix: mixColor && { color: mixColor?.color.dark, ratio: mixColor.ratio.dark } })}
|
||||
${generateColorVariable('neutral', DEFAULT_TINT_COLOR, { darkMode: true })}
|
||||
@@ -178,7 +185,7 @@ export async function CustomizationRootLayout(props: {
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body className={className}>
|
||||
<body className={tcls(bodyClassName, 'sheet-open:overflow-hidden')}>
|
||||
<IconsProvider
|
||||
assetsURL={GITBOOK_ICONS_URL}
|
||||
assetsURLToken={GITBOOK_ICONS_TOKEN}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
// This function has to be pure because we do a `.toString()` on it.
|
||||
function setOperatingSystemClass() {
|
||||
if (typeof navigator === 'undefined') return;
|
||||
|
||||
const OS_MAC_CLASS = 'os-mac';
|
||||
const platform = navigator.platform.toLowerCase();
|
||||
const isMac = platform.includes('mac');
|
||||
const root = document.documentElement;
|
||||
|
||||
if (isMac) {
|
||||
root.classList.add(OS_MAC_CLASS);
|
||||
} else {
|
||||
root.classList.remove(OS_MAC_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a script to detect the OS and apply a class to the <html> element as early as possible.
|
||||
* Bypasses react state to prevent flickering.
|
||||
*/
|
||||
export function OperatingSystemClassScript() {
|
||||
React.useEffect(() => {
|
||||
setOperatingSystemClass();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<script
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(${setOperatingSystemClass.toString()})()`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -296,88 +296,36 @@ html.announcement-hidden #announcement-banner {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
/* Shiki theme */
|
||||
:root {
|
||||
--shiki-color-text: rgb(var(--tint-11));
|
||||
--shiki-token-punctuation: rgb(var(--tint-11));
|
||||
--shiki-token-comment: rgb(var(--neutral-9));
|
||||
--shiki-token-link: rgb(var(--primary-10));
|
||||
|
||||
--shiki-token-constant: rgb(var(--warning-10));
|
||||
--shiki-token-string: rgb(var(--warning-10));
|
||||
--shiki-token-string-expression: rgb(var(--success-10));
|
||||
--shiki-token-keyword: rgb(var(--danger-10));
|
||||
--shiki-token-parameter: rgb(var(--warning-10));
|
||||
--shiki-token-function: rgb(var(--primary-10));
|
||||
|
||||
--shiki-token-inserted: rgb(var(--success-10));
|
||||
--shiki-token-deleted: rgb(var(--danger-10));
|
||||
--shiki-token-changed: rgb(var(--tint-12));
|
||||
}
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--shiki-color-text: rgb(var(--tint-12));
|
||||
--shiki-token-punctuation: rgb(var(--tint-12));
|
||||
--shiki-token-comment: rgb(var(--neutral-11));
|
||||
--shiki-token-link: rgb(var(--primary-11));
|
||||
|
||||
--shiki-token-constant: rgb(var(--warning-11));
|
||||
--shiki-token-string: rgb(var(--warning-11));
|
||||
--shiki-token-string-expression: rgb(var(--success-11));
|
||||
--shiki-token-keyword: rgb(var(--danger-11));
|
||||
--shiki-token-parameter: rgb(var(--warning-11));
|
||||
--shiki-token-function: rgb(var(--primary-11));
|
||||
|
||||
--shiki-token-inserted: rgb(var(--success-11));
|
||||
--shiki-token-deleted: rgb(var(--danger-11));
|
||||
--shiki-token-changed: rgb(var(--tint-12));
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
/* Override select colors to have more contrast */
|
||||
--shiki-token-comment: rgb(var(--neutral-9));
|
||||
|
||||
--shiki-token-constant: rgb(var(--warning-11));
|
||||
--shiki-token-string: rgb(var(--warning-11));
|
||||
--shiki-token-string-expression: rgb(var(--success-11));
|
||||
--shiki-token-keyword: rgb(var(--danger-11));
|
||||
--shiki-token-parameter: rgb(var(--warning-11));
|
||||
--shiki-token-function: rgb(var(--primary-11));
|
||||
}
|
||||
|
||||
.code-monochrome {
|
||||
--shiki-token-constant: rgb(var(--tint-11));
|
||||
--shiki-token-string: rgb(var(--tint-12));
|
||||
--shiki-token-string-expression: rgb(var(--tint-12));
|
||||
--shiki-token-keyword: rgb(var(--primary-10));
|
||||
--shiki-token-parameter: rgb(var(--tint-9));
|
||||
--shiki-token-function: rgb(var(--primary-9));
|
||||
}
|
||||
|
||||
html.dark.code-monochrome {
|
||||
--shiki-token-constant: rgb(var(--tint-11));
|
||||
--shiki-token-string: rgb(var(--tint-12));
|
||||
--shiki-token-string-expression: rgb(var(--tint-12));
|
||||
--shiki-token-keyword: rgb(var(--primary-11));
|
||||
--shiki-token-parameter: rgb(var(--tint-10));
|
||||
--shiki-token-function: rgb(var(--primary-10));
|
||||
}
|
||||
|
||||
html.font-Lato {
|
||||
/* Lato's default ligatures impact readability, so we turn them off */
|
||||
font-variant-ligatures: no-common-ligatures;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
/* Shiki themes can define font styling (style, weight, decoration) via CSS variables.
|
||||
* These variables are prefixed with --shiki-{mode}-{property} and allow themes to customize
|
||||
* typography beyond just colors. We apply them here so code blocks respect theme font styling. */
|
||||
.shiki, .shiki span {
|
||||
font-style: var(--shiki-light-font-style) !important;
|
||||
font-weight: var(--shiki-light-font-weight) !important;
|
||||
text-decoration: var(--shiki-light-text-decoration) !important;
|
||||
}
|
||||
|
||||
html.dark .shiki,
|
||||
html.dark .shiki span {
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
|
||||
.highlight-line {
|
||||
@apply grid grid-cols-subgrid col-span-2 relative ring-1 ring-transparent hover:ring-tint-hover hover:z-1 rounded-sm;
|
||||
@apply text-tint-strong grid grid-cols-subgrid col-span-2 bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base relative hover:invert-5 hover:z-1 rounded-sm;
|
||||
@apply only:hover:ring-transparent;
|
||||
@apply [counter-increment:line];
|
||||
|
||||
&.highlighted {
|
||||
@apply bg-tint-active;
|
||||
/* Use `invert-` to get a dynamic color that contrasts with the background, regardless of the codeblock's theme. */
|
||||
@apply bg-tint-base invert-10 hover:invert-15;
|
||||
@apply first:rounded-t-md first:*:mt-1;
|
||||
@apply last:rounded-b-md last:*:mb-1;
|
||||
@apply rounded-none;
|
||||
@@ -397,11 +345,11 @@ html.font-Lato {
|
||||
}
|
||||
|
||||
.highlight-line-number {
|
||||
@apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-linear-to-r from-80% from-tint-subtle contrast-more:from-tint-base theme-muted:from-tint-base [html.theme-bold.sidebar-filled_&]:from-tint-base to-transparent;
|
||||
@apply before:text-tint before:content-[counter(line)];
|
||||
@apply text-sm text-tint text-right pr-3.5 bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base rounded-l pl-2 sticky left-[-3px] mask-r-from-80%;
|
||||
@apply before:content-[counter(line)] not-contrast-more:before:opacity-6;
|
||||
|
||||
.highlight-line.highlighted > & {
|
||||
@apply before:text-tint bg-linear-to-r from-80% from-tint-active to-transparent;
|
||||
.highlighted & {
|
||||
@apply bg-tint-base
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useTrackEvent } from '../Insights';
|
||||
import { Button, Link } from '../primitives';
|
||||
import { useSearchAskContext } from './SearchAskContext';
|
||||
import { type AskAnswerResult, type AskAnswerSource, streamAskQuestion } from './server-actions';
|
||||
import { useSearch, useSearchLink } from './useSearch';
|
||||
import { useSearchLink, useSetSearchState } from './useSearch';
|
||||
|
||||
export type SearchAskState =
|
||||
| {
|
||||
@@ -130,7 +130,7 @@ function TransitionAnswerBody(props: {
|
||||
}, [answer]);
|
||||
|
||||
const language = useLanguage();
|
||||
const [, setSearchState] = useSearch();
|
||||
const setSearchState = useSetSearchState();
|
||||
|
||||
return display ? (
|
||||
<div className={tcls('flex w-full flex-col gap-4')}>
|
||||
|
||||
@@ -9,14 +9,14 @@ import { useAI } from '../AI';
|
||||
import { AIChatButton } from '../AIChat';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { Popover } from '../primitives';
|
||||
import { Popover, useBodyLoaded } from '../primitives';
|
||||
import { SearchAskAnswer } from './SearchAskAnswer';
|
||||
import { useSearchAskState } from './SearchAskContext';
|
||||
import { SearchAskProvider } from './SearchAskContext';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { SearchResults, type SearchResultsRef } from './SearchResults';
|
||||
import { SearchScopeControl } from './SearchScopeControl';
|
||||
import { useSearch } from './useSearch';
|
||||
import { useSearchState, useSetSearchState } from './useSearch';
|
||||
import { useSearchResults } from './useSearchResults';
|
||||
import { useSearchResultsCursor } from './useSearchResultsCursor';
|
||||
|
||||
@@ -60,12 +60,14 @@ export function SearchContainer({
|
||||
}: SearchContainerProps) {
|
||||
const { assistants, config } = useAI();
|
||||
|
||||
const [state, setSearchState] = useSearch();
|
||||
const state = useSearchState();
|
||||
const setSearchState = useSetSearchState();
|
||||
const searchAsk = useSearchAskState();
|
||||
const router = useRouter();
|
||||
const trackEvent = useTrackEvent();
|
||||
const resultsRef = useRef<SearchResultsRef>(null);
|
||||
const searchInputRef = useRef<HTMLDivElement>(null);
|
||||
const isLoaded = useBodyLoaded();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -79,11 +81,13 @@ export function SearchContainer({
|
||||
if (assistants.length === 0) return;
|
||||
if (state?.ask === undefined || state?.ask === null) return;
|
||||
|
||||
initialRef.current = true;
|
||||
|
||||
// For simplicity we're only triggering the first assistant
|
||||
assistants[0]?.open(state?.ask ?? undefined);
|
||||
}, [state?.ask, assistants.length, assistants[0]?.open]);
|
||||
// Because this is in the layout, we need to await for the body to be loaded.
|
||||
if (isLoaded) {
|
||||
assistants[0]?.open(state.ask ?? undefined);
|
||||
initialRef.current = true;
|
||||
}
|
||||
}, [state?.ask, assistants.length, assistants[0]?.open, isLoaded]);
|
||||
|
||||
const onClose = React.useCallback(
|
||||
async (to?: string) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user