mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-13 22:29:22 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44fb83655b | |||
| d0b93fcae0 | |||
| 2e5d9418d4 | |||
| 1000978049 | |||
| b475655e7a | |||
| 73e0cbb2d6 | |||
| 392f59450c | |||
| b4918f60ce | |||
| 8f7c304d58 | |||
| a3a944d7dd | |||
| 88a35ed057 | |||
| 67998b6f15 | |||
| ff6d1150a5 | |||
| 500c8cb649 | |||
| 6859f7d239 | |||
| dfa8a37be1 | |||
| 11a6511b7a | |||
| af98402655 | |||
| b7a0db3339 | |||
| 382a19885b | |||
| d99da6a3ae | |||
| 72cd0e59e6 | |||
| 42d88da73c | |||
| 2863fe0dc1 | |||
| 87218062be | |||
| 8a3910e208 | |||
| 6294bbb53c | |||
| a28a997851 | |||
| e2dd8a9288 | |||
| 17df1be3ef | |||
| 34a11d37a7 | |||
| 28a9ee701e | |||
| 50e8b2ec52 | |||
| 315717fa2d | |||
| da67711c90 | |||
| 60015f5807 | |||
| 8e3e5147cf | |||
| fecdbe8dde | |||
| 9316ccd1f4 | |||
| c3b620e975 | |||
| 8e75e3bec5 | |||
| 7a00880e5b | |||
| b6b597564d | |||
| d410381957 | |||
| 902c3c6c1a | |||
| 4fb2a4a6d7 | |||
| 2d64c78787 | |||
| fbfcca5dae | |||
| 664ae8bff6 | |||
| fa3d2aae08 | |||
| e7a591d9f6 | |||
| 2860beacdc | |||
| 231167d3e1 | |||
| c0ee60ec13 | |||
| f58b904ba6 | |||
| d5992f1a5c |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix UX issue about highlighting the search term in search result sections
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add support for inline icons.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix crash when integration script fails to render block.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve support for OAuth2 security type
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Adds Columns layout block to GBO
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Update Models page styling
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Optimize performances by using a smarter per-request cache arround data cached functions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Fix rendering of ogimage with SVG logos.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix crash when integration is triggering invalid requests.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
encode customization header
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Rework full-width layout, add support for full-width page option
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-contentkit": patch
|
||||
---
|
||||
|
||||
Add basic error handling when transitioning between states.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Don't crash ogimage generation on RTL text, as a workaround until we can support it.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/fonts": minor
|
||||
---
|
||||
|
||||
Initial version of the package
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix rendering of ogimage when logo or icon are AVIF images.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add margin to adjacent buttons
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix ogimage generation failing with some JPEG images.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Don't cache unexpected API errors for more than a few minutes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Fix an issue where PDF export URLs were not keeping their query params.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
add a global error boundary
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Make icons for page groups more contrasting
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Ignore case while highlighting search results.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Fix InlineLinkTooltip having a negative impact on performance, especially on larger pages.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix border being added to cards
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix crash during rendering of ogimage for VA sites with default icon.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
apply customization for dynamic context
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Reverse order of feedback smileys
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
New design for tab blocks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix ogimage using incorrect Google Font depending on language.
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing v1
|
||||
needs: deploy-v1-cloudflare
|
||||
timeout-minutes: 8
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v1
|
||||
needs: deploy-v1-cloudflare
|
||||
timeout-minutes: 8
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -240,7 +240,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2
|
||||
needs: deploy-v2-vercel
|
||||
timeout-minutes: 8
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Visual Testing Customers v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 8
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -56,13 +56,19 @@ git clone https://github.com/gitbookIO/gitbook.git
|
||||
bun install
|
||||
```
|
||||
|
||||
4. Start your local development server.
|
||||
4. Run build.
|
||||
|
||||
```
|
||||
bun build:v2
|
||||
```
|
||||
|
||||
5. Start your local development server.
|
||||
|
||||
```
|
||||
bun dev:v2
|
||||
```
|
||||
|
||||
5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
|
||||
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
|
||||
|
||||
examples:
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"name": "@gitbook/cache-tags",
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"assert-never": "^1.2.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -47,15 +47,27 @@
|
||||
"emoji-assets": "^8.0.0",
|
||||
},
|
||||
},
|
||||
"packages/fonts": {
|
||||
"name": "@gitbook/fonts",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"google-font-metadata": "^6.0.3",
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.12.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
@@ -73,8 +85,10 @@
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
"direction": "^2.0.1",
|
||||
"event-iterator": "^2.0.0",
|
||||
"framer-motion": "^10.16.14",
|
||||
"image-size": "^2.0.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsontoxml": "^1.0.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
@@ -150,13 +164,14 @@
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@opennextjs/cloudflare": "1.1.0",
|
||||
"@opennextjs/cloudflare": "1.2.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "^15.3.2",
|
||||
"object-identity": "^0.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
@@ -209,7 +224,7 @@
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.7.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"classnames": "^2.5.1",
|
||||
},
|
||||
@@ -267,10 +282,12 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
},
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.121.0",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
|
||||
|
||||
@@ -604,6 +621,8 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
|
||||
|
||||
"@evan/concurrency": ["@evan/concurrency@0.0.3", "", {}, "sha512-vjhkm2nrXoM39G4aP/U4CC5vFv/ZlMRSjbTII0N65J9R0EpgjdGswnHKS1KSjfGAp/9zKSNaojBgi0SxKnGapw=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
|
||||
@@ -632,7 +651,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.119.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-l2gfJ5+1HT3Sj1OQCh72lUOGU9LV9WbDjgKPjxIFz+NgOhSest1bJ3JhmHP66Xd4ETpmm0YQqnq4y18wKRMjBQ=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.121.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-o4/N24RM0Rg8S/3yPDjPmt6TbQF+1iZmg9q9QKxOxMqpQ2bZmMUqS7dSkeqEbEBMALx/m/x0xQlJbEJGbOwteg=="],
|
||||
|
||||
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
|
||||
|
||||
@@ -644,6 +663,8 @@
|
||||
|
||||
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="],
|
||||
|
||||
"@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
|
||||
|
||||
"@gitbook/icons": ["@gitbook/icons@workspace:packages/icons"],
|
||||
|
||||
"@gitbook/openapi-parser": ["@gitbook/openapi-parser@workspace:packages/openapi-parser"],
|
||||
@@ -798,9 +819,25 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@opennextjs/aws": ["@opennextjs/aws@3.6.4", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-/bn9N/6dVu9+sC7AptaGJylKUzyDDgqe3yKfvUxXOpy7whIq/+3Bw+q2/bilyQg6FcskbCWUt9nLvNf1hAVmfA=="],
|
||||
"@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.1.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.4", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-8DOzpLiewivA1pwRNGVPDXbrF79bzJiHscY+M1tekwvKqEqM26CRYafhrA5IDCeyteNaL9AZD6X4DLuCn/eeYQ=="],
|
||||
"@octokit/core": ["@octokit/core@6.1.5", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg=="],
|
||||
|
||||
"@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
|
||||
|
||||
"@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
|
||||
|
||||
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/request": ["@octokit/request@9.2.3", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w=="],
|
||||
|
||||
"@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@opennextjs/aws": ["@opennextjs/aws@3.6.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-wni+CWlRCyWfhNfekQBBPPkrDDnaGdZLN9hMybKI0wKOKTO+zhPOqR65Eh3V0pzWAi84Sureb5mdMuLwCxAAcw=="],
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.2.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.5", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-cOco+nHwlo/PLB1bThF8IIvaS8PgAT9MEI5ZttFO/qt6spgvr2lUaPkpjgSIQmI3sBIEG2cLUykvQ2nbbZEcVw=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -1470,6 +1507,8 @@
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
|
||||
|
||||
"better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.1.2", "", {}, "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="],
|
||||
@@ -1482,6 +1521,8 @@
|
||||
|
||||
"body-parser": ["body-parser@2.0.2", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "3.1.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.5.2", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "^3.0.0", "type-is": "~1.6.18" } }, "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="],
|
||||
|
||||
"boxen": ["boxen@4.2.0", "", { "dependencies": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", "chalk": "^3.0.0", "cli-boxes": "^2.2.0", "string-width": "^4.1.0", "term-size": "^2.1.0", "type-fest": "^0.8.1", "widest-line": "^3.1.0" } }, "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ=="],
|
||||
@@ -1506,6 +1547,8 @@
|
||||
|
||||
"bytes": ["bytes@3.1.0", "", {}, "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"cacheable": ["cacheable@1.8.9", "", { "dependencies": { "hookified": "^1.7.1", "keyv": "^5.3.1" } }, "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ=="],
|
||||
|
||||
"cacheable-request": ["cacheable-request@6.1.0", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^3.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^4.1.0", "responselike": "^1.0.2" } }, "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg=="],
|
||||
@@ -1602,10 +1645,16 @@
|
||||
|
||||
"css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="],
|
||||
|
||||
"css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
|
||||
|
||||
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
||||
|
||||
"css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"cva": ["cva@1.0.0-beta.2", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "typescript": ">= 4.5.5 < 6" }, "optionalPeers": ["typescript"] }, "sha512-dqcOFe247I5pKxfuzqfq3seLL5iMYsTgo40Uw7+pKZAntPgFtR7Tmy59P5IVIq/XgB0NQWoIvYDt9TwHkuK8Cg=="],
|
||||
@@ -1660,8 +1709,18 @@
|
||||
|
||||
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||
|
||||
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
||||
|
||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
|
||||
|
||||
"dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
|
||||
@@ -1694,7 +1753,7 @@
|
||||
|
||||
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"env-cmd": ["env-cmd@10.1.0", "", { "dependencies": { "commander": "^4.0.0", "cross-spawn": "^7.0.0" }, "bin": { "env-cmd": "bin/env-cmd.js" } }, "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA=="],
|
||||
|
||||
@@ -1800,6 +1859,8 @@
|
||||
|
||||
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
@@ -1904,6 +1965,8 @@
|
||||
|
||||
"google-auth-library": ["google-auth-library@5.10.1", "", { "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", "gcp-metadata": "^3.4.0", "gtoken": "^4.1.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" } }, "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg=="],
|
||||
|
||||
"google-font-metadata": ["google-font-metadata@6.0.3", "", { "dependencies": { "@evan/concurrency": "^0.0.3", "@octokit/core": "^6.1.2", "cac": "^6.7.14", "consola": "^3.3.3", "deepmerge": "^4.3.1", "json-stringify-pretty-compact": "^4.0.0", "linkedom": "^0.18.6", "pathe": "^1.1.2", "picocolors": "^1.1.1", "playwright": "^1.49.1", "stylis": "^4.3.4", "zod": "^3.24.1" }, "bin": { "gfm": "dist/cli.mjs" } }, "sha512-62+QB+nmBKppHrxwvILZkV/EvKctEAdzkKBIJY26TtwZkUjeEqmAQnE5uHvtMTdB8odqGpQu5LAKPqqUYHI9wA=="],
|
||||
|
||||
"google-p12-pem": ["google-p12-pem@2.0.5", "", { "dependencies": { "node-forge": "^0.10.0" }, "bin": { "gp12-pem": "build/src/bin/gp12-pem.js" } }, "sha512-7RLkxwSsMsYh9wQ5Vb2zRtkAHvqPvfoMGag+nugl1noYO7gf0844Yr9TIFA5NEBMAeVt2Z+Imu7CQMp3oNatzQ=="],
|
||||
|
||||
"googleapis": ["googleapis@47.0.0", "", { "dependencies": { "google-auth-library": "^5.6.1", "googleapis-common": "^3.2.0" } }, "sha512-+Fnjgcc3Na/rk57dwxqW1V0HJXJFjnt3aqFlckULqAqsPkmex/AyJJe6MSlXHC37ZmlXEb9ZtPGUp5ZzRDXpHg=="],
|
||||
@@ -1980,12 +2043,16 @@
|
||||
|
||||
"hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="],
|
||||
|
||||
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
|
||||
|
||||
"html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="],
|
||||
|
||||
"htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="],
|
||||
|
||||
"http-errors": ["http-errors@1.4.0", "", { "dependencies": { "inherits": "2.0.1", "statuses": ">= 1.2.1 < 2" } }, "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw=="],
|
||||
@@ -2004,6 +2071,8 @@
|
||||
|
||||
"ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
|
||||
|
||||
"image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"import-lazy": ["import-lazy@2.1.0", "", {}, "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A=="],
|
||||
@@ -2096,6 +2165,8 @@
|
||||
|
||||
"json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="],
|
||||
|
||||
"json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="],
|
||||
|
||||
"json-xml-parse": ["json-xml-parse@1.3.0", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="],
|
||||
|
||||
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
||||
@@ -2134,6 +2205,8 @@
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"linkedom": ["linkedom@0.18.11", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" } }, "sha512-K03GU3FUlnhBAP0jPb7tN7YJl7LbjZx30Z8h6wgLXusnKF7+BEZvfEbdkN/lO9LfFzxN3S0ZAriDuJ/13dIsLA=="],
|
||||
|
||||
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="],
|
||||
@@ -2364,12 +2437,16 @@
|
||||
|
||||
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"nuqs": ["nuqs@2.2.3", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "@remix-run/react": ">=2", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router-dom": ">=6" }, "optionalPeers": ["@remix-run/react", "next", "react-router-dom"] }, "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||
|
||||
"object-identity": ["object-identity@0.1.2", "", {}, "sha512-Px5puVllX5L2aBjbcfXpiG5xXeq6OE8RckryTeP2Zq+0PgYrCGJXmC6LblWgknKSJs11Je2W4U2NOWFj3t/QXQ=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="],
|
||||
|
||||
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
|
||||
@@ -2454,7 +2531,7 @@
|
||||
|
||||
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"pcre-to-regexp": ["pcre-to-regexp@1.1.0", "", {}, "sha512-KF9XxmUQJ2DIlMj3TqNqY1AWvyvTuIuq11CuuekxyaYMiFuMKGgQrePYMX5bXKLhLG3sDI4CsGAYHPaT7VV7+g=="],
|
||||
|
||||
@@ -2722,6 +2799,8 @@
|
||||
|
||||
"stylelint": ["stylelint@16.16.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "@csstools/media-query-list-parser": "^4.0.2", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.0.7", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg=="],
|
||||
|
||||
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
||||
|
||||
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
@@ -2826,6 +2905,8 @@
|
||||
|
||||
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
|
||||
|
||||
"uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
|
||||
|
||||
"uid-promise": ["uid-promise@1.0.0", "", {}, "sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig=="],
|
||||
|
||||
"undici": ["undici@5.28.4", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g=="],
|
||||
@@ -2852,6 +2933,8 @@
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
|
||||
|
||||
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
|
||||
|
||||
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
@@ -3666,6 +3749,8 @@
|
||||
|
||||
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||
|
||||
"@opennextjs/aws/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||
|
||||
"@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
||||
@@ -4012,6 +4097,8 @@
|
||||
|
||||
"@vercel/static-config/ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="],
|
||||
|
||||
"@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"@vue/compiler-sfc/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="],
|
||||
|
||||
"@vueuse/integrations/@vueuse/core": ["@vueuse/core@11.2.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.2.0", "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" } }, "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA=="],
|
||||
@@ -4060,6 +4147,8 @@
|
||||
|
||||
"decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="],
|
||||
|
||||
"edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
|
||||
@@ -4108,6 +4197,8 @@
|
||||
|
||||
"google-auth-library/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"google-font-metadata/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"googleapis-common/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="],
|
||||
|
||||
"got/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="],
|
||||
@@ -4176,6 +4267,8 @@
|
||||
|
||||
"package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"parse5/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"path-match/path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
|
||||
@@ -4268,6 +4361,8 @@
|
||||
|
||||
"ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
|
||||
|
||||
"unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"update-notifier/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
|
||||
|
||||
"url-parse-lax/prepend-http": ["prepend-http@2.0.0", "", {}, "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="],
|
||||
|
||||
+7
-3
@@ -7,10 +7,9 @@
|
||||
"turbo": "^2.5.0",
|
||||
"vercel": "^39.3.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.11",
|
||||
"packageManager": "bun@1.2.15",
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
@@ -34,7 +33,12 @@
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||
"clean": "turbo run clean"
|
||||
},
|
||||
"workspaces": ["packages/*"],
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.121.0"
|
||||
}
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
src/data/*.json
|
||||
@@ -0,0 +1,3 @@
|
||||
# `@gitbook/fonts`
|
||||
|
||||
Utilities to lookup default fonts supported by GitBook.
|
||||
@@ -0,0 +1,91 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { APIv2 } from 'google-font-metadata';
|
||||
|
||||
import { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
import type { FontDefinitions } from '../src/types';
|
||||
|
||||
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
|
||||
[CustomizationDefaultFont.Inter]: 'inter',
|
||||
[CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed',
|
||||
[CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif',
|
||||
[CustomizationDefaultFont.Lato]: 'lato',
|
||||
[CustomizationDefaultFont.Merriweather]: 'merriweather',
|
||||
[CustomizationDefaultFont.NotoSans]: 'noto-sans',
|
||||
[CustomizationDefaultFont.OpenSans]: 'open-sans',
|
||||
[CustomizationDefaultFont.Overpass]: 'overpass',
|
||||
[CustomizationDefaultFont.Poppins]: 'poppins',
|
||||
[CustomizationDefaultFont.Raleway]: 'raleway',
|
||||
[CustomizationDefaultFont.Roboto]: 'roboto',
|
||||
[CustomizationDefaultFont.RobotoSlab]: 'roboto-slab',
|
||||
[CustomizationDefaultFont.SourceSansPro]: 'source-sans-3',
|
||||
[CustomizationDefaultFont.Ubuntu]: 'ubuntu',
|
||||
[CustomizationDefaultFont.ABCFavorit]: 'inter',
|
||||
};
|
||||
|
||||
/**
|
||||
* Scripts to generate the list of all icons.
|
||||
*/
|
||||
async function main() {
|
||||
// @ts-expect-error - we build the object
|
||||
const output: FontDefinitions = {};
|
||||
|
||||
for (const font of Object.values(CustomizationDefaultFont)) {
|
||||
const googleFontName = googleFontsMap[font];
|
||||
const fontMetadata = APIv2[googleFontName.toLowerCase()];
|
||||
if (!fontMetadata) {
|
||||
throw new Error(`Font ${googleFontName} not found`);
|
||||
}
|
||||
|
||||
output[font] = {
|
||||
font: googleFontName,
|
||||
unicodeRange: fontMetadata.unicodeRange,
|
||||
variants: {
|
||||
'400': {},
|
||||
'700': {},
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(output[font].variants).forEach((weight) => {
|
||||
const variants = fontMetadata.variants[weight];
|
||||
const normalVariant = variants.normal;
|
||||
if (!normalVariant) {
|
||||
throw new Error(`Font ${googleFontName} has no normal variant`);
|
||||
}
|
||||
|
||||
output[font].variants[weight] = {};
|
||||
Object.entries(normalVariant).forEach(([script, url]) => {
|
||||
output[font].variants[weight][script] = url.url.woff;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await writeDataFile('fonts', JSON.stringify(output, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* We write both in dist and src as the build process might have happen already
|
||||
* and tsc doesn't copy the files.
|
||||
*/
|
||||
async function writeDataFile(name, content) {
|
||||
const srcData = path.resolve(__dirname, '../src/data');
|
||||
const distData = path.resolve(__dirname, '../dist/data');
|
||||
|
||||
// Ensure the directories exists
|
||||
await Promise.all([
|
||||
fs.mkdir(srcData, { recursive: true }),
|
||||
fs.mkdir(distData, { recursive: true }),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(path.resolve(srcData, `${name}.json`), content),
|
||||
fs.writeFile(path.resolve(distData, `${name}.json`), content),
|
||||
]);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`Error generating icons list: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@gitbook/fonts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"google-font-metadata": "^6.0.3",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun ./bin/generate.js",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w",
|
||||
"clean": "rm -rf ./dist && rm -rf ./src/data",
|
||||
"unit": "bun test"
|
||||
},
|
||||
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getDefaultFont should return correct object for Latin text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should return correct object for Greek text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle mixed script text 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different font weights: regular 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different font weights: bold 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different fonts: inter 1`] = `
|
||||
{
|
||||
"font": "Inter",
|
||||
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`getDefaultFont should handle different fonts: roboto 1`] = `
|
||||
{
|
||||
"font": "Roboto",
|
||||
"url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { FontDefinitions } from './types';
|
||||
|
||||
import rawFonts from './data/fonts.json' with { type: 'json' };
|
||||
|
||||
export const fonts: FontDefinitions = rawFonts;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { CustomizationDefaultFont } from '@gitbook/api';
|
||||
import { getDefaultFont } from './getDefaultFont';
|
||||
|
||||
describe('getDefaultFont', () => {
|
||||
it('should return null for invalid font', () => {
|
||||
const result = getDefaultFont({
|
||||
font: 'invalid-font' as CustomizationDefaultFont,
|
||||
text: 'Hello',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid weight', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello',
|
||||
weight: 999 as any,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for text not supported by any script', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: '😀', // Emoji not supported by Inter
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return correct object for Latin text', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello World',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return correct object for Cyrillic text', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Привет мир',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return correct object for Greek text', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Γεια σας',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle mixed script text', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello Привет',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle different font weights', () => {
|
||||
const regular = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello',
|
||||
weight: 400,
|
||||
});
|
||||
const bold = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello',
|
||||
weight: 700,
|
||||
});
|
||||
expect(regular).not.toBeNull();
|
||||
expect(bold).not.toBeNull();
|
||||
expect(regular).toMatchSnapshot('regular');
|
||||
expect(bold).toMatchSnapshot('bold');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: '',
|
||||
weight: 400,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle different fonts', () => {
|
||||
const inter = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Inter,
|
||||
text: 'Hello',
|
||||
weight: 400,
|
||||
});
|
||||
const roboto = getDefaultFont({
|
||||
font: CustomizationDefaultFont.Roboto,
|
||||
text: 'Hello',
|
||||
weight: 400,
|
||||
});
|
||||
expect(inter).not.toBeNull();
|
||||
expect(roboto).not.toBeNull();
|
||||
expect(inter).toMatchSnapshot('inter');
|
||||
expect(roboto).toMatchSnapshot('roboto');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { CustomizationDefaultFont } from '@gitbook/api';
|
||||
import { fonts } from './fonts';
|
||||
import type { FontWeight } from './types';
|
||||
|
||||
/**
|
||||
* Get the URL to load a font for a text.
|
||||
*/
|
||||
export function getDefaultFont(input: {
|
||||
/**
|
||||
* GitBook font to use.
|
||||
*/
|
||||
font: CustomizationDefaultFont;
|
||||
|
||||
/**
|
||||
* Text to display with the font.
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* Font weight to use.
|
||||
*/
|
||||
weight: FontWeight;
|
||||
}): { font: string; url: string } | null {
|
||||
if (!input.text.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fontDefinition = fonts[input.font];
|
||||
if (!fontDefinition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const variant = fontDefinition.variants[`${input.weight}`];
|
||||
if (!variant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const script = getBestUnicodeRange(input.text, fontDefinition.unicodeRange);
|
||||
if (!script) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return variant[script]
|
||||
? {
|
||||
font: input.font,
|
||||
url: variant[script],
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which named @font-face unicode-range covers
|
||||
* the greatest share of the characters in `text`.
|
||||
*
|
||||
* @param text The text you want to inspect.
|
||||
* @param ranges An object whose keys are range names and whose
|
||||
* values are CSS-style comma-separated unicode-range
|
||||
* declarations (e.g. "U+0370-03FF,U+1F00-1FFF").
|
||||
* @returns The key of the best-matching range, or `null`
|
||||
* when nothing matches at all.
|
||||
*/
|
||||
function getBestUnicodeRange(text: string, ranges: Record<string, string>): string | null {
|
||||
// ---------- helper: parse "U+XXXX" or "U+XXXX-YYYY" ----------
|
||||
const parseOne = (token: string): [number, number] | null => {
|
||||
token = token.trim().toUpperCase();
|
||||
if (!token.startsWith('U+')) return null;
|
||||
|
||||
const body = token.slice(2); // drop "U+"
|
||||
const [startHex, endHex] = body.split('-');
|
||||
const start = Number.parseInt(startHex, 16);
|
||||
const end = endHex ? Number.parseInt(endHex, 16) : start;
|
||||
|
||||
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
|
||||
return [start, end];
|
||||
};
|
||||
|
||||
// ---------- helper: build lookup table ----------
|
||||
const parsed: Record<string, [number, number][]> = {};
|
||||
for (const [label, list] of Object.entries(ranges)) {
|
||||
parsed[label] = list
|
||||
.split(',')
|
||||
.map(parseOne)
|
||||
.filter((x): x is [number, number] => x !== null);
|
||||
}
|
||||
|
||||
// ---------- tally code-point hits ----------
|
||||
const hits: Record<string, number> = Object.fromEntries(Object.keys(parsed).map((k) => [k, 0]));
|
||||
|
||||
for (let i = 0; i < text.length; ) {
|
||||
const cp = text.codePointAt(i)!;
|
||||
i += cp > 0xffff ? 2 : 1; // advance by 1 UTF-16 code-unit (or 2 for surrogates)
|
||||
|
||||
for (const [label, rangesArr] of Object.entries(parsed)) {
|
||||
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
|
||||
hits[label]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- choose the "best" ----------
|
||||
let winner: string | null = null;
|
||||
let maxCount = 0;
|
||||
|
||||
for (const [label, count] of Object.entries(hits)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
winner = label;
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? winner : null;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './getDefaultFont';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { CustomizationDefaultFont } from '@gitbook/api';
|
||||
|
||||
export type FontWeight = 400 | 700;
|
||||
|
||||
export type FontDefinition = {
|
||||
font: string;
|
||||
unicodeRange: {
|
||||
[script: string]: string;
|
||||
};
|
||||
variants: {
|
||||
[weight in string]: {
|
||||
[script: string]: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type FontDefinitions = { [fontName in CustomizationDefaultFont]: FontDefinition };
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2023"],
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"generate": {
|
||||
"inputs": ["bin/**/*", "package.json"],
|
||||
"outputs": ["src/data/*.json", "dist/data/*.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,12 @@
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-preview"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
@@ -61,6 +67,12 @@
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-staging"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
@@ -100,6 +112,12 @@
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-production"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
|
||||
@@ -49,17 +49,20 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
|
||||
if (localCacheEntry) {
|
||||
span.setAttribute('cacheHit', 'local');
|
||||
return localCacheEntry.json();
|
||||
const result = (await localCacheEntry.json()) as WithLastModified<
|
||||
CacheValue<CacheType>
|
||||
>;
|
||||
return this.returnNullOn404(result);
|
||||
}
|
||||
|
||||
const r2Object = await r2.get(cacheKey);
|
||||
if (!r2Object) return null;
|
||||
|
||||
span.setAttribute('cacheHit', 'r2');
|
||||
return {
|
||||
return this.returnNullOn404({
|
||||
value: await r2Object.json(),
|
||||
lastModified: r2Object.uploaded.getTime(),
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to get from cache', e);
|
||||
return null;
|
||||
@@ -68,6 +71,18 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: This is a workaround to handle 404 responses in the cache.
|
||||
// It should be handled by OpenNext cache interception directly. This should be removed once OpenNext cache interception is fixed.
|
||||
returnNullOn404<CacheType extends CacheEntryType = 'cache'>(
|
||||
cacheEntry: WithLastModified<CacheValue<CacheType>> | null
|
||||
): WithLastModified<CacheValue<CacheType>> | null {
|
||||
if (!cacheEntry?.value) return null;
|
||||
if ('meta' in cacheEntry.value && cacheEntry.value.meta?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
return cacheEntry;
|
||||
}
|
||||
|
||||
async set<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@opennextjs/cloudflare": "1.1.0",
|
||||
"@opennextjs/cloudflare": "1.2.1",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
@@ -14,7 +14,8 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
"server-only": "^0.0.1",
|
||||
"warn-once": "^0.1.1"
|
||||
"warn-once": "^0.1.1",
|
||||
"object-identity": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gitbook": "*",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import NextError from 'next/error';
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
}) {
|
||||
console.error('Global error:', error);
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<NextError statusCode={undefined as any} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
|
||||
import { getDynamicCustomizationSettings } from '@/lib/customization';
|
||||
import type { SiteAPIToken } from '@gitbook/api';
|
||||
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
@@ -67,6 +68,8 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
|
||||
siteURLData
|
||||
);
|
||||
|
||||
context.customization = await getDynamicCustomizationSettings(context.customization);
|
||||
|
||||
return {
|
||||
context,
|
||||
visitorAuthClaims: getVisitorAuthClaims(siteURLData),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { withStableRef } from './cache';
|
||||
|
||||
describe('withStableRef', () => {
|
||||
it('should return primitive values as is', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
expect(toStableRef(42)).toBe(42);
|
||||
expect(toStableRef('hello')).toBe('hello');
|
||||
expect(toStableRef(true)).toBe(true);
|
||||
expect(toStableRef(null)).toBe(null);
|
||||
expect(toStableRef(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return the same reference for identical objects', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const obj1 = { a: 1, b: 2 };
|
||||
const obj2 = { a: 1, b: 2 };
|
||||
|
||||
const ref1 = toStableRef(obj1);
|
||||
const ref2 = toStableRef(obj2);
|
||||
|
||||
expect(ref1).toBe(ref2);
|
||||
expect(ref1).toBe(obj1);
|
||||
expect(ref1).not.toBe(obj2);
|
||||
});
|
||||
|
||||
it('should return the same reference for identical arrays', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [1, 2, 3];
|
||||
|
||||
const ref1 = toStableRef(arr1);
|
||||
const ref2 = toStableRef(arr2);
|
||||
|
||||
expect(ref1).toBe(ref2);
|
||||
expect(ref1).toBe(arr1);
|
||||
expect(ref1).not.toBe(arr2);
|
||||
});
|
||||
|
||||
it('should return the same reference for identical nested objects', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const obj1 = { a: { b: 1 }, c: [2, 3] };
|
||||
const obj2 = { a: { b: 1 }, c: [2, 3] };
|
||||
|
||||
const ref1 = toStableRef(obj1);
|
||||
const ref2 = toStableRef(obj2);
|
||||
|
||||
expect(ref1).toBe(ref2);
|
||||
expect(ref1).toBe(obj1);
|
||||
expect(ref1).not.toBe(obj2);
|
||||
});
|
||||
|
||||
it('should return different references for different objects', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const obj1 = { a: 1 };
|
||||
const obj2 = { a: 2 };
|
||||
|
||||
const ref1 = toStableRef(obj1);
|
||||
const ref2 = toStableRef(obj2);
|
||||
|
||||
expect(ref1).not.toBe(ref2);
|
||||
});
|
||||
|
||||
it('should maintain reference stability across multiple calls', () => {
|
||||
const toStableRef = withStableRef();
|
||||
|
||||
const obj = { a: 1 };
|
||||
const ref1 = toStableRef(obj);
|
||||
const ref2 = toStableRef(obj);
|
||||
const ref3 = toStableRef(obj);
|
||||
|
||||
expect(ref1).toBe(ref2);
|
||||
expect(ref2).toBe(ref3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { identify } from 'object-identity';
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Equivalent to `React.cache` but with support for non-primitive arguments.
|
||||
* As `React.cache` only uses `Object.is` to compare arguments, it will not work with non-primitive arguments.
|
||||
*/
|
||||
export function cache<Args extends any[], Return>(fn: (...args: Args) => Return) {
|
||||
const cached = React.cache(fn);
|
||||
|
||||
return (...args: Args) => {
|
||||
const toStableRef = getWithStableRef();
|
||||
const stableArgs = args.map((value) => {
|
||||
return toStableRef(value);
|
||||
}) as Args;
|
||||
return cached(...stableArgs);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* To ensure memory is garbage collected between each request, we use a per-request cache to store the ref maps.
|
||||
*/
|
||||
const getWithStableRef = React.cache(withStableRef);
|
||||
|
||||
/**
|
||||
* Create a function that converts a value to a stable reference.
|
||||
*/
|
||||
export function withStableRef(): <T>(value: T) => T {
|
||||
const reverseIndex = new WeakMap<object, string>();
|
||||
const refIndex = new Map<string, object>();
|
||||
|
||||
return <T>(value: T) => {
|
||||
if (isPrimitive(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const objectValue = value as object;
|
||||
const index = reverseIndex.get(objectValue);
|
||||
if (index !== undefined) {
|
||||
return refIndex.get(index) as T;
|
||||
}
|
||||
|
||||
const hash = identify(objectValue);
|
||||
reverseIndex.set(objectValue, hash);
|
||||
|
||||
const existing = refIndex.get(hash);
|
||||
if (existing !== undefined) {
|
||||
return existing as T;
|
||||
}
|
||||
|
||||
// first time we've seen this shape
|
||||
refIndex.set(hash, objectValue);
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
function isPrimitive(value: any): boolean {
|
||||
return value === null || typeof value !== 'object';
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { GitBookAPIError } from '@gitbook/api';
|
||||
import { unstable_cacheLife as cacheLife } from 'next/cache';
|
||||
import type { DataFetcherErrorData, DataFetcherResponse } from './types';
|
||||
|
||||
export class DataFetcherError extends Error {
|
||||
@@ -47,11 +48,7 @@ export function getDataOrNull<T>(
|
||||
return response.then((result) => getDataOrNull(result, ignoreErrors));
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (ignoreErrors.includes(response.error.code)) return null;
|
||||
throw new DataFetcherError(response.error.message, response.error.code);
|
||||
}
|
||||
return response.data;
|
||||
return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +90,50 @@ export async function wrapDataFetcherError<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an async execution to handle errors and return a DataFetcherResponse.
|
||||
* This should be used inside 'use cache' functions.
|
||||
*/
|
||||
export async function wrapCacheDataFetcherError<T>(
|
||||
fn: () => Promise<T>
|
||||
): Promise<DataFetcherResponse<T>> {
|
||||
const result = await wrapDataFetcherError(fn);
|
||||
if (result.error && result.error.code >= 500) {
|
||||
// We don't want to cache errors for too long.
|
||||
// as the API might
|
||||
cacheLife('minutes');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore some data fetcher errors.
|
||||
*/
|
||||
export function ignoreDataFetcherErrors<T>(
|
||||
response: DataFetcherResponse<T>,
|
||||
ignoreErrors?: number[]
|
||||
): DataFetcherResponse<T>;
|
||||
export function ignoreDataFetcherErrors<T>(
|
||||
response: Promise<DataFetcherResponse<T>>,
|
||||
ignoreErrors?: number[]
|
||||
): Promise<DataFetcherResponse<T>>;
|
||||
export function ignoreDataFetcherErrors<T>(
|
||||
response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>,
|
||||
ignoreErrors: number[] = [404]
|
||||
): DataFetcherResponse<T> | Promise<DataFetcherResponse<T>> {
|
||||
if (response instanceof Promise) {
|
||||
return response.then((result) => ignoreDataFetcherErrors(result, ignoreErrors));
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (ignoreErrors.includes(response.error.code)) {
|
||||
return response;
|
||||
}
|
||||
throw new DataFetcherError(response.error.message, response.error.code);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a data fetcher exposable error from a JS error.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './api';
|
||||
export * from './types';
|
||||
export * from './pages';
|
||||
export * from './urls';
|
||||
export * from './errors';
|
||||
export * from './lookup';
|
||||
export * from './visitor';
|
||||
export * from './pages';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { race, tryCatch } from '@/lib/async';
|
||||
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { trace } from '@/lib/tracing';
|
||||
import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
|
||||
import type { PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
|
||||
import { apiClient } from './api';
|
||||
import { getExposableError } from './errors';
|
||||
import type { DataFetcherResponse } from './types';
|
||||
@@ -18,85 +18,32 @@ interface LookupPublishedContentByUrlInput {
|
||||
* Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*/
|
||||
export async function resolvePublishedContentByUrl(input: LookupPublishedContentByUrlInput) {
|
||||
return lookupPublishedContentByUrl({
|
||||
url: input.url,
|
||||
fetchLookupAPIResult: ({ url, signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
return trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrl',
|
||||
name: url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.resolvePublishedContentByUrl(
|
||||
{
|
||||
url,
|
||||
...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
|
||||
redirectOnError: input.redirectOnError,
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook getPublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*
|
||||
* @deprecated use resolvePublishedContentByUrl.
|
||||
*
|
||||
*/
|
||||
export async function getPublishedContentByURL(input: LookupPublishedContentByUrlInput) {
|
||||
return lookupPublishedContentByUrl({
|
||||
url: input.url,
|
||||
fetchLookupAPIResult: ({ url, signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
return trace(
|
||||
{
|
||||
operation: 'getPublishedContentByURL',
|
||||
name: url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.getPublishedContentByUrl(
|
||||
{
|
||||
url,
|
||||
visitorAuthToken: input.visitorPayload.jwtToken ?? undefined,
|
||||
redirectOnError: input.redirectOnError,
|
||||
// @ts-expect-error - cacheVersion is not a real query param
|
||||
cacheVersion: 'v2',
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type TryCatch<T> = ReturnType<typeof tryCatch<T>>;
|
||||
|
||||
async function lookupPublishedContentByUrl(input: {
|
||||
url: string;
|
||||
fetchLookupAPIResult: (args: {
|
||||
url: string;
|
||||
signal: AbortSignal;
|
||||
}) => TryCatch<Awaited<ReturnType<GitBookAPI['urls']['resolvePublishedContentByUrl']>>>;
|
||||
}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
|
||||
export async function lookupPublishedContentByUrl(
|
||||
input: LookupPublishedContentByUrlInput
|
||||
): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
|
||||
const lookupURL = new URL(input.url);
|
||||
const url = stripURLSearch(lookupURL);
|
||||
const lookup = getURLLookupAlternatives(url);
|
||||
|
||||
const result = await race(lookup.urls, async (alternative, { signal }) => {
|
||||
const callResult = await input.fetchLookupAPIResult({
|
||||
url: alternative.url,
|
||||
signal,
|
||||
});
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
const callResult = await trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrl',
|
||||
name: alternative.url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.resolvePublishedContentByUrl(
|
||||
{
|
||||
url: alternative.url,
|
||||
...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
|
||||
redirectOnError: input.redirectOnError,
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (callResult.error) {
|
||||
if (alternative.primary) {
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '../context';
|
||||
import { getDataOrNull } from './errors';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
|
||||
/**
|
||||
* Get the document for a page.
|
||||
*/
|
||||
export async function getPageDocument(
|
||||
dataFetcher: GitBookDataFetcher,
|
||||
space: Space,
|
||||
context: GitBookSpaceContext | GitBookSiteContext,
|
||||
page: RevisionPageDocument
|
||||
): Promise<JSONDocument | null> {
|
||||
const { dataFetcher, space } = context;
|
||||
|
||||
if (isV2()) {
|
||||
return getDataOrNull(
|
||||
dataFetcher.getRevisionPageDocument({
|
||||
spaceId: space.id,
|
||||
revisionId: space.revision,
|
||||
pageId: page.id,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (page.documentId) {
|
||||
return getDataOrNull(
|
||||
dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
|
||||
|
||||
@@ -106,6 +106,15 @@ export interface GitBookDataFetcher {
|
||||
pageId: string;
|
||||
}): Promise<DataFetcherResponse<string>>;
|
||||
|
||||
/**
|
||||
* Get the document of a page by its path.
|
||||
*/
|
||||
getRevisionPageDocument(params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
pageId: string;
|
||||
}): Promise<DataFetcherResponse<api.JSONDocument>>;
|
||||
|
||||
/**
|
||||
* Get a document by its space ID and document ID.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface CloudflareImageJsonFormat {
|
||||
* https://developers.cloudflare.com/images/image-resizing/resize-with-workers/
|
||||
*/
|
||||
export interface CloudflareImageOptions {
|
||||
format?: 'webp' | 'avif' | 'json' | 'jpeg';
|
||||
format?: 'webp' | 'avif' | 'json' | 'jpeg' | 'png';
|
||||
fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
|
||||
width?: number;
|
||||
height?: number;
|
||||
|
||||
@@ -16,10 +16,9 @@ import {
|
||||
import { serveResizedImage } from '@/routes/image';
|
||||
import {
|
||||
DataFetcherError,
|
||||
getPublishedContentByURL,
|
||||
getVisitorAuthBasePath,
|
||||
lookupPublishedContentByUrl,
|
||||
normalizeURL,
|
||||
resolvePublishedContentByUrl,
|
||||
throwIfDataError,
|
||||
} from '@v2/lib/data';
|
||||
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
|
||||
@@ -34,15 +33,6 @@ export const config = {
|
||||
|
||||
type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
|
||||
|
||||
/**
|
||||
* Temporary list of hosts to test adaptive content using the new resolution API.
|
||||
*/
|
||||
const ADAPTIVE_CONTENT_HOSTS = [
|
||||
'docs.gitbook.com',
|
||||
'adaptive-docs.gitbook-staging.com',
|
||||
'enriched-content-playground.gitbook-staging.io',
|
||||
];
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
try {
|
||||
const requestURL = new URL(request.url);
|
||||
@@ -101,11 +91,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
});
|
||||
|
||||
const withAPIToken = async (apiToken: string | null) => {
|
||||
const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
|
||||
? resolvePublishedContentByUrl
|
||||
: getPublishedContentByURL;
|
||||
const siteURLData = await throwIfDataError(
|
||||
resolve({
|
||||
lookupPublishedContentByUrl({
|
||||
url: siteRequestURL.toString(),
|
||||
visitorPayload: {
|
||||
jwtToken: visitorToken?.token ?? undefined,
|
||||
@@ -226,7 +213,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
const customization = siteRequestURL.searchParams.get('customization');
|
||||
if (customization && validateSerializedCustomization(customization)) {
|
||||
routeType = 'dynamic';
|
||||
requestHeaders.set(MiddlewareHeaders.Customization, customization);
|
||||
// We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel.
|
||||
requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
|
||||
}
|
||||
const theme = siteRequestURL.searchParams.get('theme');
|
||||
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
|
||||
@@ -271,6 +259,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
console.log(`rewriting ${request.nextUrl.toString()} to ${route}`);
|
||||
|
||||
const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
|
||||
rewrittenURL.search = request.nextUrl.search; // Preserve the original search params
|
||||
|
||||
const response = NextResponse.rewrite(rewrittenURL, {
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
|
||||
@@ -204,11 +204,11 @@ const testCases: TestsCase[] = [
|
||||
contentBaseURL: 'https://sosovalue-white-paper.gitbook.io',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.revrobotics.com',
|
||||
contentBaseURL: 'https://docs.revrobotics.com',
|
||||
tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
},
|
||||
// {
|
||||
// name: 'docs.revrobotics.com',
|
||||
// contentBaseURL: 'https://docs.revrobotics.com',
|
||||
// tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }],
|
||||
// },
|
||||
{
|
||||
name: 'chartschool.stockcharts.com',
|
||||
contentBaseURL: 'https://chartschool.stockcharts.com',
|
||||
|
||||
@@ -603,6 +603,11 @@ const testCases: TestsCase[] = [
|
||||
url: 'blocks/emojis',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Icons',
|
||||
url: 'blocks/icons',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'Links',
|
||||
url: 'blocks/links',
|
||||
@@ -666,6 +671,7 @@ const testCases: TestsCase[] = [
|
||||
name: 'Stepper',
|
||||
url: 'blocks/stepper',
|
||||
},
|
||||
{ name: 'Columns', url: 'blocks/columns' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { argosScreenshot } from '@argos-ci/playwright';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { getContentTestURL } from '../tests/utils';
|
||||
import { waitForIcons } from './util';
|
||||
|
||||
test.describe('PDF export', () => {
|
||||
test('export all pages as PDF (e2e)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?limit=10'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export all pages as PDF (GitBook docs)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(getContentTestURL('https://gitbook.com/docs/~gitbook/pdf?limit=10'));
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export a single page as PDF (e2e)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?page=Bw7LjWwgTjV8nIV4s7rs&only=yes&limit=2'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('export a single page as PDF (GitBook docs)', async ({ page }) => {
|
||||
// Set the header to disable the Vercel toolbar
|
||||
// But only on the main document as it'd cause CORS issues on other resources
|
||||
await page.route('**/*', async (route, request) => {
|
||||
if (request.resourceType() === 'document') {
|
||||
await route.continue({
|
||||
headers: {
|
||||
...request.headers(),
|
||||
'x-vercel-skip-toolbar': '1',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
getContentTestURL(
|
||||
'https://gitbook.com/docs/~gitbook/pdf?page=DfnNkU49mvLe2ythHAyx&only=yes&limit=2'
|
||||
)
|
||||
);
|
||||
|
||||
const printBtn = page.getByTestId('print-button');
|
||||
await expect(printBtn).toBeVisible();
|
||||
|
||||
await argosScreenshot(page, 'pdf - all pages', {
|
||||
viewports: ['macbook-13'],
|
||||
argosCSS: `
|
||||
/* Hide Intercom */
|
||||
.intercom-lightweight-app {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
threshold: undefined,
|
||||
fullPage: true,
|
||||
beforeScreenshot: async ({ runStabilization }) => {
|
||||
await runStabilization();
|
||||
await waitForIcons(page);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -346,7 +346,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
/**
|
||||
* Wait for all icons present on the page to be loaded.
|
||||
*/
|
||||
async function waitForIcons(page: Page) {
|
||||
export async function waitForIcons(page: Page) {
|
||||
await page.waitForFunction(() => {
|
||||
const urlStates: Record<
|
||||
string,
|
||||
|
||||
@@ -8,20 +8,21 @@
|
||||
"build:cloudflare": "next-on-pages --custom-entrypoint=./src/cloudflare-entrypoint.ts",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"e2e": "playwright test e2e/internal.spec.ts",
|
||||
"e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts",
|
||||
"e2e-customers": "playwright test e2e/customers.spec.ts",
|
||||
"unit": "bun test {src,packages}",
|
||||
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
|
||||
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
|
||||
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.119.0",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
@@ -80,7 +81,9 @@
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"zod": "^3.24.2",
|
||||
"zod-to-json-schema": "^3.24.5",
|
||||
"zustand": "^5.0.3"
|
||||
"zustand": "^5.0.3",
|
||||
"image-size": "^2.0.2",
|
||||
"direction": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^5.0.3",
|
||||
|
||||
@@ -19,14 +19,14 @@ export async function AdClassicRendering({
|
||||
insightsAd: SiteInsightsAd | null;
|
||||
context: GitBookBaseContext;
|
||||
}) {
|
||||
const smallImgSrc =
|
||||
const [smallImgSrc, logoSrc] = await Promise.all([
|
||||
'smallImage' in ad
|
||||
? await getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 })
|
||||
: null;
|
||||
const logoSrc =
|
||||
? getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 })
|
||||
: null,
|
||||
'logo' in ad
|
||||
? await getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 })
|
||||
: null;
|
||||
? getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 })
|
||||
: null,
|
||||
]);
|
||||
return (
|
||||
<Link
|
||||
rel="sponsored noopener"
|
||||
|
||||
@@ -43,10 +43,12 @@ interface FetchPlaceholderAdOptions {
|
||||
* and properly access user-agent and IP.
|
||||
*/
|
||||
export async function renderAd(options: FetchAdOptions) {
|
||||
const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const [context, result] = await Promise.all([
|
||||
isV2() ? getServerActionBaseContext() : getV1BaseContext(),
|
||||
options.source === 'live' ? fetchAd(options) : getPlaceholderAd(),
|
||||
]);
|
||||
|
||||
const mode = options.source === 'live' ? options.mode : 'classic';
|
||||
const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd();
|
||||
if (!result || !result.ad.description || !result.ad.statlink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
aria-describedby={describedById}
|
||||
className={tcls(
|
||||
'fixed',
|
||||
'z-10',
|
||||
'z-50',
|
||||
'bg-tint-base',
|
||||
'rounded',
|
||||
'straight-corners:rounded-none',
|
||||
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
} from '@/components/primitives';
|
||||
import type { ClassValue } from '@/lib/tailwind';
|
||||
|
||||
import { nullIfNever } from '@/lib/typescript';
|
||||
import { BlockContentRef } from './BlockContentRef';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
import { Columns } from './Columns';
|
||||
import { Divider } from './Divider';
|
||||
import type { DocumentContextProps } from './DocumentView';
|
||||
import { Drawing } from './Drawing';
|
||||
@@ -44,13 +46,6 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
|
||||
style?: ClassValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to `assertNever` that returns `null` instead of throwing an error.
|
||||
*/
|
||||
function nullIfNever(_value: never): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
|
||||
const { block, style, isEstimatedOffscreen, context } = props;
|
||||
|
||||
@@ -68,6 +63,8 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
|
||||
return <List {...props} block={block} />;
|
||||
case 'list-item':
|
||||
return <ListItem {...props} block={block} />;
|
||||
case 'columns':
|
||||
return <Columns {...props} block={block} />;
|
||||
case 'code':
|
||||
return <CodeBlock {...props} block={block} />;
|
||||
case 'hint':
|
||||
@@ -112,10 +109,8 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
|
||||
case 'image':
|
||||
case 'code-line':
|
||||
case 'tabs-item':
|
||||
throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
|
||||
case 'columns':
|
||||
case 'column':
|
||||
return null;
|
||||
throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
|
||||
default:
|
||||
return nullIfNever(block);
|
||||
}
|
||||
@@ -171,6 +166,7 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
|
||||
case 'integration':
|
||||
case 'stepper':
|
||||
case 'reusable-content':
|
||||
case 'columns':
|
||||
return <SkeletonCard id={id} style={style} />;
|
||||
case 'embed':
|
||||
case 'images':
|
||||
@@ -179,10 +175,8 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
|
||||
case 'image':
|
||||
case 'code-line':
|
||||
case 'tabs-item':
|
||||
throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
|
||||
case 'columns':
|
||||
case 'column':
|
||||
return null;
|
||||
throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
|
||||
default:
|
||||
return nullIfNever(block);
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
|
||||
style={[
|
||||
'mx-auto w-full decoration-primary/6',
|
||||
node.data && 'fullWidth' in node.data && node.data.fullWidth
|
||||
? 'max-w-screen-xl'
|
||||
: 'max-w-3xl',
|
||||
? 'max-w-screen-2xl'
|
||||
: 'page-full-width:ml-0 max-w-3xl',
|
||||
blockStyle,
|
||||
]}
|
||||
isEstimatedOffscreen={isOffscreen}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
||||
import { type BundledLanguage, bundledLanguages } from 'shiki/langs';
|
||||
|
||||
import { nullIfNever } from '@/lib/typescript';
|
||||
import { plainHighlight } from './plain-highlight';
|
||||
|
||||
export type HighlightLine = {
|
||||
@@ -263,16 +264,28 @@ function getPlainCodeBlockLine(
|
||||
if (node.object === 'text') {
|
||||
content += cleanupLine(node.leaves.map((leaf) => leaf.text).join(''));
|
||||
} else {
|
||||
const start = index + content.length;
|
||||
content += getPlainCodeBlockLine(node, index + content.length, inlines);
|
||||
const end = index + content.length;
|
||||
switch (node.type) {
|
||||
case 'annotation': {
|
||||
const start = index + content.length;
|
||||
content += getPlainCodeBlockLine(node, index + content.length, inlines);
|
||||
const end = index + content.length;
|
||||
|
||||
if (inlines) {
|
||||
inlines.push({
|
||||
inline: node,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
if (inlines) {
|
||||
inlines.push({
|
||||
inline: node,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'expression': {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
nullIfNever(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import type { DocumentBlockColumns, Length } from '@gitbook/api';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
|
||||
export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
||||
const { block, style, ancestorBlocks, document, context } = props;
|
||||
return (
|
||||
<div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
|
||||
{block.nodes.map((columnBlock) => {
|
||||
const width = columnBlock.data.width;
|
||||
const { className, style } = transformLengthToCSS(width);
|
||||
return (
|
||||
<Column key={columnBlock.key} className={className} style={style}>
|
||||
<Blocks
|
||||
key={columnBlock.key}
|
||||
nodes={columnBlock.nodes}
|
||||
document={document}
|
||||
ancestorBlocks={[...ancestorBlocks, block, columnBlock]}
|
||||
context={context}
|
||||
blockStyle="flip-heading-hash"
|
||||
style="w-full space-y-4 *:max-w-full"
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Column(props: {
|
||||
children?: React.ReactNode;
|
||||
className?: ClassValue;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<div className={tcls('flex-col', props.className)} style={props.style}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function transformLengthToCSS(length: Length | undefined) {
|
||||
if (!length) {
|
||||
return { className: ['md:w-full'] }; // default to full width if no length is specified
|
||||
}
|
||||
|
||||
if (typeof length === 'number') {
|
||||
return { style: undefined }; // not implemented yet with non-percentage lengths
|
||||
}
|
||||
|
||||
if (length.unit === '%') {
|
||||
return {
|
||||
className: [
|
||||
'md:flex-shrink-0',
|
||||
COLUMN_WIDTHS[Math.round(length.value * 0.01 * (COLUMN_WIDTHS.length - 1))],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return { style: undefined }; // not implemented yet with non-percentage lengths
|
||||
}
|
||||
|
||||
// Tailwind CSS classes for column widths.
|
||||
// The index of the array corresponds to the percentage width of the column.
|
||||
const COLUMN_WIDTHS = [
|
||||
'md:w-0',
|
||||
'md:w-1/12',
|
||||
'md:w-2/12',
|
||||
'md:w-3/12',
|
||||
'md:w-4/12',
|
||||
'md:w-5/12',
|
||||
'md:w-6/12',
|
||||
'md:w-7/12',
|
||||
'md:w-8/12',
|
||||
'md:w-9/12',
|
||||
'md:w-10/12',
|
||||
'md:w-11/12',
|
||||
'md:w-full',
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Columns';
|
||||
@@ -7,5 +7,5 @@ import type { BlockProps } from './Block';
|
||||
export function Divider(props: BlockProps<DocumentBlockDivider>) {
|
||||
const { style } = props;
|
||||
|
||||
return <hr className={tcls(style, 'border-tint-subtle')} />;
|
||||
return <hr className={tcls(style, 'page-full-width:max-w-full border-tint-subtle')} />;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ export interface DocumentContext {
|
||||
* @default true
|
||||
*/
|
||||
wrapBlocksInSuspense?: boolean;
|
||||
|
||||
/**
|
||||
* True if link previews should be rendered.
|
||||
* This is used to limit the number of link previews rendered in a document.
|
||||
* If false, no link previews will be rendered.
|
||||
* @default false
|
||||
*/
|
||||
shouldRenderLinkPreviews?: boolean;
|
||||
}
|
||||
|
||||
export interface DocumentContextProps {
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import type {
|
||||
DocumentInline,
|
||||
DocumentInlineAnnotation,
|
||||
DocumentInlineButton,
|
||||
DocumentInlineEmoji,
|
||||
DocumentInlineImage,
|
||||
DocumentInlineLink,
|
||||
DocumentInlineMath,
|
||||
DocumentInlineMention,
|
||||
JSONDocument,
|
||||
} from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import type { DocumentInline, JSONDocument } from '@gitbook/api';
|
||||
|
||||
import { nullIfNever } from '@/lib/typescript';
|
||||
import { Annotation } from './Annotation/Annotation';
|
||||
import type { DocumentContextProps } from './DocumentView';
|
||||
import { Emoji } from './Emoji';
|
||||
import { InlineButton } from './InlineButton';
|
||||
import { InlineIcon } from './InlineIcon';
|
||||
import { InlineImage } from './InlineImage';
|
||||
import { InlineLink } from './InlineLink';
|
||||
import { InlineMath } from './Math';
|
||||
@@ -39,16 +30,7 @@ export interface InlineProps<T extends DocumentInline> extends DocumentContextPr
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Inline<
|
||||
T extends
|
||||
| DocumentInlineImage
|
||||
| DocumentInlineAnnotation
|
||||
| DocumentInlineEmoji
|
||||
| DocumentInlineLink
|
||||
| DocumentInlineMath
|
||||
| DocumentInlineMention
|
||||
| DocumentInlineButton,
|
||||
>(props: InlineProps<T>) {
|
||||
export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
|
||||
const { inline, ...contextProps } = props;
|
||||
|
||||
switch (inline.type) {
|
||||
@@ -66,7 +48,13 @@ export function Inline<
|
||||
return <InlineImage {...contextProps} inline={inline} />;
|
||||
case 'button':
|
||||
return <InlineButton {...contextProps} inline={inline} />;
|
||||
case 'icon':
|
||||
return <InlineIcon {...contextProps} inline={inline} />;
|
||||
case 'expression':
|
||||
// The GitBook API should take care of evaluating expressions.
|
||||
// We should never need to render them.
|
||||
return null;
|
||||
default:
|
||||
assertNever(inline);
|
||||
return nullIfNever(inline);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,18 +17,22 @@ export async function InlineButton(props: InlineProps<api.DocumentInlineButton>)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
href={resolved.href}
|
||||
label={inline.data.label}
|
||||
// TODO: use a variant specifically for user-defined buttons.
|
||||
variant={inline.data.kind}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: api.SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
// Set the leading to have some vertical space between adjacent buttons
|
||||
<span className="inline-button leading-[3rem] [&:has(+.inline-button)]:mr-2">
|
||||
<Button
|
||||
href={resolved.href}
|
||||
label={inline.data.label}
|
||||
// TODO: use a variant specifically for user-defined buttons.
|
||||
variant={inline.data.kind}
|
||||
className="leading-normal"
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: api.SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { DocumentInlineIcon } from '@gitbook/api';
|
||||
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import type { InlineProps } from './Inline';
|
||||
|
||||
export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
|
||||
const { inline } = props;
|
||||
|
||||
return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { StyledLink } from '../primitives';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { InlineLinkTooltip } from './InlineLinkTooltip';
|
||||
import { Inlines } from './Inlines';
|
||||
|
||||
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const { inline, document, context, ancestorInlines } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!context.contentContext || !resolved) {
|
||||
return (
|
||||
<span title="Broken link" className="underline">
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
|
||||
return (
|
||||
<InlineLinkTooltip inline={inline} context={context.contentContext} resolved={resolved}>
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
</StyledLink>
|
||||
</InlineLinkTooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
|
||||
import { getSpaceLanguage, tString } from '@/intl/server';
|
||||
import { languages } from '@/intl/translations';
|
||||
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
import { StyledLink } from '../../primitives';
|
||||
import type { InlineProps } from '../Inline';
|
||||
import { Inlines } from '../Inlines';
|
||||
import { InlineLinkTooltip } from './InlineLinkTooltip';
|
||||
|
||||
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const { inline, document, context, ancestorInlines } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(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,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!context.contentContext || !resolved) {
|
||||
return (
|
||||
<span title="Broken link" className="underline">
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const content = (
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
</StyledLink>
|
||||
);
|
||||
|
||||
if (context.shouldRenderLinkPreviews) {
|
||||
return (
|
||||
<InlineLinkTooltipWrapper
|
||||
inline={inline}
|
||||
context={context.contentContext}
|
||||
resolved={resolved}
|
||||
>
|
||||
{content}
|
||||
</InlineLinkTooltipWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* An SSR component that renders a link with a tooltip.
|
||||
* Essentially it pulls the minimum amount of props from the context to render the tooltip.
|
||||
*/
|
||||
function InlineLinkTooltipWrapper(props: {
|
||||
inline: DocumentInlineLink;
|
||||
context: GitBookAnyContext;
|
||||
children: React.ReactNode;
|
||||
resolved: ResolvedContentRef;
|
||||
}) {
|
||||
const { inline, context, resolved, children } = props;
|
||||
|
||||
let breadcrumbs = resolved.ancestors ?? [];
|
||||
const language =
|
||||
'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
if (isExternal) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_external_link'),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_page_anchor'),
|
||||
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
|
||||
},
|
||||
];
|
||||
resolved.subText = undefined;
|
||||
}
|
||||
|
||||
const aiSummary: { pageId: string; spaceId: string } | undefined = (() => {
|
||||
if (isExternal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSamePage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('customization' in context) || !context.customization.ai?.pageLinkSummaries.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('page' in context) || !('page' in inline.data.ref)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor') {
|
||||
return {
|
||||
pageId: resolved.page?.id ?? inline.data.ref.page ?? context.page.id,
|
||||
spaceId: inline.data.ref.space ?? context.space.id,
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<InlineLinkTooltip
|
||||
breadcrumbs={breadcrumbs}
|
||||
isExternal={isExternal}
|
||||
isSamePage={isSamePage}
|
||||
aiSummary={aiSummary}
|
||||
openInNewTabLabel={tString(language, 'open_in_new_tab')}
|
||||
target={{
|
||||
href: resolved.href,
|
||||
text: resolved.text,
|
||||
subText: resolved.subText,
|
||||
icon: resolved.icon,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</InlineLinkTooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
const LoadingValueContext = React.createContext<React.ReactNode>(null);
|
||||
|
||||
// To avoid polluting the RSC payload with the tooltip implementation,
|
||||
// we lazily load it on the client side. This way, the tooltip is only loaded
|
||||
// when the user interacts with the link, and it doesn't block the initial render.
|
||||
|
||||
const InlineLinkTooltipImpl = dynamic(
|
||||
() => import('./InlineLinkTooltipImpl').then((mod) => mod.InlineLinkTooltipImpl),
|
||||
{
|
||||
// Disable server-side rendering for this component, it's only
|
||||
// visible on user interaction.
|
||||
ssr: false,
|
||||
loading: () => {
|
||||
// The fallback should be the children (the content of the link),
|
||||
// but as next/dynamic is aiming for feature parity with React.lazy,
|
||||
// it doesn't support passing children to the loading component.
|
||||
// https://github.com/vercel/next.js/issues/7906
|
||||
const children = React.useContext(LoadingValueContext);
|
||||
return <>{children}</>;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Tooltip for inline links. It's lazily loaded to avoid blocking the initial render
|
||||
* and polluting the RSC payload.
|
||||
*
|
||||
* The link text and href have already been rendered on the server for good SEO,
|
||||
* so we can be as lazy as possible with the tooltip.
|
||||
*/
|
||||
export function InlineLinkTooltip(props: {
|
||||
isSamePage: boolean;
|
||||
isExternal: boolean;
|
||||
aiSummary?: { pageId: string; spaceId: string };
|
||||
breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
|
||||
target: {
|
||||
href: string;
|
||||
text: string;
|
||||
subText?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
openInNewTabLabel: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { children, ...rest } = props;
|
||||
const [shouldLoad, setShouldLoad] = React.useState(false);
|
||||
|
||||
// Once the browser is idle, we set shouldLoad to true.
|
||||
// NOTE: to be slightly more performant, we could load when a link is hovered.
|
||||
// But I found this was too much of a delay for the tooltip to appear.
|
||||
// Loading on idle is a good compromise, as it allows the initial render to be fast,
|
||||
// while still loading the tooltip in the background and not polluting the RSC payload.
|
||||
React.useEffect(() => {
|
||||
if ('requestIdleCallback' in window) {
|
||||
(window as globalThis.Window).requestIdleCallback(() => setShouldLoad(true));
|
||||
} else {
|
||||
// fallback for old browsers
|
||||
setTimeout(() => setShouldLoad(true), 2000);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return shouldLoad ? (
|
||||
<LoadingValueContext.Provider value={children}>
|
||||
<InlineLinkTooltipImpl {...rest}>{children}</InlineLinkTooltipImpl>
|
||||
</LoadingValueContext.Provider>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
}
|
||||
+34
-67
@@ -1,55 +1,27 @@
|
||||
import type { DocumentInlineLink } from '@gitbook/api';
|
||||
|
||||
import type { ResolvedContentRef } from '@/lib/references';
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { tString } from '@/intl/translate';
|
||||
import { languages } from '@/intl/translations';
|
||||
import { getNodeText } from '@/lib/document';
|
||||
'use client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import type { GitBookAnyContext } from '@v2/lib/context';
|
||||
import { Fragment } from 'react';
|
||||
import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
|
||||
import { Button, StyledLink } from '../primitives';
|
||||
import { AIPageLinkSummary } from '../../Adaptive';
|
||||
import { Button, StyledLink } from '../../primitives';
|
||||
|
||||
export async function InlineLinkTooltip(props: {
|
||||
inline: DocumentInlineLink;
|
||||
context: GitBookAnyContext;
|
||||
export function InlineLinkTooltipImpl(props: {
|
||||
isSamePage: boolean;
|
||||
isExternal: boolean;
|
||||
aiSummary?: { pageId: string; spaceId: string };
|
||||
breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>;
|
||||
target: {
|
||||
href: string;
|
||||
text: string;
|
||||
subText?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
openInNewTabLabel: string;
|
||||
children: React.ReactNode;
|
||||
resolved: ResolvedContentRef;
|
||||
}) {
|
||||
const { inline, context, resolved, children } = props;
|
||||
|
||||
let breadcrumbs = resolved.ancestors;
|
||||
const language =
|
||||
'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
if (isExternal) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_external_link'),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_page_anchor'),
|
||||
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
|
||||
},
|
||||
];
|
||||
resolved.subText = undefined;
|
||||
}
|
||||
|
||||
const hasAISummary =
|
||||
!isExternal &&
|
||||
!isSamePage &&
|
||||
'customization' in context &&
|
||||
context.customization.ai?.pageLinkSummaries.enabled &&
|
||||
(inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor');
|
||||
const { isSamePage, isExternal, aiSummary, openInNewTabLabel, target, breadcrumbs, children } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
@@ -100,15 +72,15 @@ export async function InlineLinkTooltip(props: {
|
||||
isExternal && 'text-sm [overflow-wrap:anywhere]'
|
||||
)}
|
||||
>
|
||||
{resolved.icon ? (
|
||||
{target.icon ? (
|
||||
<div className="mt-1 text-tint-subtle empty:hidden">
|
||||
{resolved.icon}
|
||||
{target.icon}
|
||||
</div>
|
||||
) : null}
|
||||
<h5 className="font-semibold">{resolved.text}</h5>
|
||||
<h5 className="font-semibold">{target.text}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{!isSamePage && resolved.href ? (
|
||||
{!isSamePage && target.href ? (
|
||||
<Button
|
||||
className={tcls(
|
||||
'-mx-2 -my-2 ml-auto',
|
||||
@@ -117,40 +89,35 @@ export async function InlineLinkTooltip(props: {
|
||||
: null
|
||||
)}
|
||||
variant="blank"
|
||||
href={resolved.href}
|
||||
href={target.href}
|
||||
target="_blank"
|
||||
label={tString(language, 'open_in_new_tab')}
|
||||
label={openInNewTabLabel}
|
||||
size="small"
|
||||
icon="arrow-up-right-from-square"
|
||||
iconOnly={true}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{resolved.subText ? (
|
||||
<p className="mt-1 text-sm text-tint">{resolved.subText}</p>
|
||||
{target.subText ? (
|
||||
<p className="mt-1 text-sm text-tint">{target.subText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasAISummary && 'page' in context && 'page' in inline.data.ref ? (
|
||||
{aiSummary ? (
|
||||
<div className="border-tint-subtle border-t bg-tint p-4">
|
||||
<AIPageLinkSummary
|
||||
targetPageId={
|
||||
resolved.page?.id ??
|
||||
inline.data.ref.page ??
|
||||
context.page.id
|
||||
}
|
||||
targetSpaceId={inline.data.ref.space ?? context.space.id}
|
||||
linkTitle={getNodeText(inline)}
|
||||
linkPreview={`**${resolved.text}**: ${resolved.subText}`}
|
||||
showTrademark={
|
||||
'customization' in context &&
|
||||
context.customization.trademark.enabled
|
||||
}
|
||||
targetPageId={aiSummary.pageId}
|
||||
targetSpaceId={aiSummary.spaceId}
|
||||
showTrademark
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} />
|
||||
<Tooltip.Arrow
|
||||
className={
|
||||
typeof aiSummary !== 'undefined' ? 'fill-tint-3' : 'fill-tint-1'
|
||||
}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './InlineLink';
|
||||
@@ -5,8 +5,8 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import './contentkit.css';
|
||||
import { getDataOrNull } from '@v2/lib/data';
|
||||
import { contentKitServerContext } from './contentkit';
|
||||
import { fetchSafeIntegrationUI } from './render';
|
||||
import { renderIntegrationUi } from './server-actions';
|
||||
|
||||
export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegration>) {
|
||||
@@ -16,8 +16,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
throw new Error('integration block requires a content.spaceId');
|
||||
}
|
||||
|
||||
const { dataFetcher } = context.contentContext;
|
||||
|
||||
const initialInput: RenderIntegrationUI = {
|
||||
componentId: block.data.block,
|
||||
props: block.data.props,
|
||||
@@ -30,17 +28,27 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
},
|
||||
};
|
||||
|
||||
const initialOutput = await getDataOrNull(
|
||||
dataFetcher.renderIntegrationUi({
|
||||
integrationName: block.data.integration,
|
||||
request: initialInput,
|
||||
}),
|
||||
const initialResponse = await fetchSafeIntegrationUI(context.contentContext, {
|
||||
integrationName: block.data.integration,
|
||||
request: initialInput,
|
||||
});
|
||||
|
||||
// The API can respond with a 400 error if the integration is not installed
|
||||
// and 404 if the integration is not found.
|
||||
[404, 400]
|
||||
);
|
||||
if (!initialOutput || initialOutput.type === 'complete') {
|
||||
if (initialResponse.error) {
|
||||
if (initialResponse.error.code === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={tcls(style)}>
|
||||
<pre>
|
||||
Unexpected error with integration {block.data.integration}:{' '}
|
||||
{initialResponse.error.message}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const initialOutput = initialResponse.data;
|
||||
if (initialOutput.type === 'complete') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { RenderIntegrationUI } from '@gitbook/api';
|
||||
import type { GitBookBaseContext } from '@v2/lib/context';
|
||||
import { ignoreDataFetcherErrors } from '@v2/lib/data';
|
||||
|
||||
/**
|
||||
* Render an integration UI while ignoring some errors.
|
||||
*/
|
||||
export async function fetchSafeIntegrationUI(
|
||||
context: GitBookBaseContext,
|
||||
{
|
||||
integrationName,
|
||||
request,
|
||||
}: {
|
||||
integrationName: string;
|
||||
request: RenderIntegrationUI;
|
||||
}
|
||||
) {
|
||||
const output = await ignoreDataFetcherErrors(
|
||||
context.dataFetcher.renderIntegrationUi({
|
||||
integrationName,
|
||||
request,
|
||||
}),
|
||||
|
||||
// The API can respond with certain errors that are expected to happen.
|
||||
[
|
||||
404, // Integration has been uninstalled
|
||||
400, // Integration is rejecting its own request
|
||||
422, // Integration is triggering an invalid request, failing at the validation step
|
||||
502, // Integration is failing in an unexpected way
|
||||
]
|
||||
);
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import type { RenderIntegrationUI } from '@gitbook/api';
|
||||
import { ContentKitOutput } from '@gitbook/react-contentkit';
|
||||
import { throwIfDataError } from '@v2/lib/data';
|
||||
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { contentKitServerContext } from './contentkit';
|
||||
import { fetchSafeIntegrationUI } from './render';
|
||||
|
||||
/**
|
||||
* Server action to render an integration UI request from <ContentKit />.
|
||||
@@ -22,16 +22,19 @@ export async function renderIntegrationUi({
|
||||
request: RenderIntegrationUI;
|
||||
}) {
|
||||
const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const output = await fetchSafeIntegrationUI(serverAction, {
|
||||
integrationName: renderContext.integrationName,
|
||||
request,
|
||||
});
|
||||
|
||||
const output = await throwIfDataError(
|
||||
serverAction.dataFetcher.renderIntegrationUi({
|
||||
integrationName: renderContext.integrationName,
|
||||
request,
|
||||
})
|
||||
);
|
||||
if (output.error) {
|
||||
return {
|
||||
error: output.error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
children: <ContentKitOutput output={output} context={contentKitServerContext} />,
|
||||
output: output,
|
||||
children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
|
||||
output: output.data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.openapi-schemas {
|
||||
@apply flex flex-col mb-14 flex-1;
|
||||
@apply flex flex-col mb-14 gap-0 flex-1;
|
||||
}
|
||||
|
||||
.openapi-schemas-title {
|
||||
@@ -310,6 +310,26 @@
|
||||
@apply py-2 border-b border-tint-subtle max-w-full flex-1;
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-flows {
|
||||
@apply flex flex-col gap-2 divide-y divide-tint-subtle;
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-content {
|
||||
@apply prose *:!prose-sm *:text-tint;
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-content.openapi-markdown code {
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-content ul {
|
||||
@apply !my-0;
|
||||
}
|
||||
|
||||
.openapi-securities-url {
|
||||
@apply ml-0.5 px-0.5 rounded hover:bg-tint transition-colors;
|
||||
}
|
||||
|
||||
.openapi-securities-body {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
@@ -728,8 +748,12 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
/* Disclosure */
|
||||
.openapi-schemas-disclosure {
|
||||
@apply border-t border-x last:border-b border-tint-subtle !ring-0 first:!rounded-t-xl last:!rounded-b-xl !rounded-none;
|
||||
}
|
||||
|
||||
.openapi-schemas-disclosure > .openapi-disclosure-trigger {
|
||||
@apply flex items-center font-mono transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
|
||||
@apply flex items-center font-mono transition-all font-normal text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-5 truncate -outline-offset-1;
|
||||
}
|
||||
|
||||
.openapi-schemas-disclosure > .openapi-disclosure-trigger,
|
||||
@@ -773,7 +797,6 @@ body:has(.openapi-select-popover) {
|
||||
@apply bg-tint-subtle overflow-hidden;
|
||||
}
|
||||
|
||||
.openapi-disclosure:has(> .openapi-disclosure-trigger:hover),
|
||||
.openapi-disclosure[data-expanded="true"] {
|
||||
@apply ring-1 shadow-sm;
|
||||
}
|
||||
|
||||
@@ -23,18 +23,18 @@ export async function RecordCard(
|
||||
const coverFile = view.coverDefinition
|
||||
? getRecordValue<string[]>(record[1], view.coverDefinition)?.[0]
|
||||
: null;
|
||||
const cover =
|
||||
coverFile && context.contentContext
|
||||
? await resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
|
||||
: null;
|
||||
|
||||
const targetRef = view.targetDefinition
|
||||
? (record[1].values[view.targetDefinition] as ContentRef)
|
||||
: null;
|
||||
const target =
|
||||
|
||||
const [cover, target] = await Promise.all([
|
||||
coverFile && context.contentContext
|
||||
? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
|
||||
: null,
|
||||
targetRef && context.contentContext
|
||||
? await resolveContentRef(targetRef, context.contentContext)
|
||||
: null;
|
||||
? resolveContentRef(targetRef, context.contentContext)
|
||||
: null,
|
||||
]);
|
||||
|
||||
const coverIsSquareOrPortrait =
|
||||
cover?.file?.dimensions &&
|
||||
|
||||
@@ -84,9 +84,13 @@ export function DynamicTabs(
|
||||
);
|
||||
}, [id, tabs, tabsState]);
|
||||
|
||||
const orientation: string = 'horizontal'; // TODO: Get orientation from tab block options
|
||||
const position: string = 'start'; // TODO: Get position from tab block options
|
||||
const description = null; // TODO: Get description from tabs
|
||||
|
||||
// To avoid issue with hydration, we only use the state from localStorage
|
||||
// once the component has been mounted.
|
||||
// Otherwise because of the streaming/suspense approach, tabs can be first-rendered at different time
|
||||
// Otherwise because of the streaming/suspense approach, tabs can be first-rendered at different time
|
||||
// and get stuck into an inconsistent state.
|
||||
const mounted = useIsMounted();
|
||||
const active = mounted ? activeState : tabs[0];
|
||||
@@ -143,14 +147,15 @@ export function DynamicTabs(
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'rounded-lg',
|
||||
'straight-corners:rounded-sm',
|
||||
'ring-1',
|
||||
'ring-inset',
|
||||
'ring-tint-subtle',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'overflow-hidden',
|
||||
'pb-1',
|
||||
|
||||
orientation === 'horizontal' && position === 'start' && 'flex-col',
|
||||
orientation === 'vertical' && position === 'start' && 'md:flex-row',
|
||||
orientation === 'vertical' && position === 'end' && 'md:flex-row-reverse',
|
||||
|
||||
style
|
||||
)}
|
||||
>
|
||||
@@ -158,70 +163,37 @@ export function DynamicTabs(
|
||||
role="tablist"
|
||||
className={tcls(
|
||||
'group/tabs',
|
||||
'inline-flex',
|
||||
'flex',
|
||||
tabs.length > 3 ? 'md:flex-wrap' : '',
|
||||
'overflow-x-auto',
|
||||
'md:overflow-hidden',
|
||||
'gap-x-1',
|
||||
'flex-row',
|
||||
'self-stretch',
|
||||
'after:flex-[1]',
|
||||
'after:bg-tint-12/1',
|
||||
// if last tab is selected, apply rounded to :after element
|
||||
'[&:has(button.active-tab:last-of-type):after]:rounded-bl-md'
|
||||
orientation === 'vertical' && 'gap-1.5 md:max-w-[40%] md:flex-col',
|
||||
'snap-x',
|
||||
'snap-mandatory',
|
||||
'-mb-px',
|
||||
'peer',
|
||||
orientation === 'vertical' && position === 'start' && 'md:-mr-px md:mb-0',
|
||||
orientation === 'vertical' && position === 'end' && 'md:-ml-px md:mb-0'
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={tcls(
|
||||
hashLinkButtonWrapperStyles,
|
||||
'flex',
|
||||
'items-center',
|
||||
'gap-3.5',
|
||||
|
||||
//prev from active-tab
|
||||
'[&:has(+_.active-tab)]:rounded-br-md',
|
||||
|
||||
//next from active-tab
|
||||
'[.active-tab_+_&]:rounded-bl-md',
|
||||
|
||||
//next from active-tab
|
||||
'[.active-tab_+_:after]:rounded-br-md',
|
||||
|
||||
'after:transition-colors',
|
||||
'after:border-r',
|
||||
'after:absolute',
|
||||
'after:left-[unset]',
|
||||
'after:right-0',
|
||||
'after:border-tint',
|
||||
'after:top-[15%]',
|
||||
'after:h-[70%]',
|
||||
'after:w-[1px]',
|
||||
|
||||
'px-3.5',
|
||||
'py-2',
|
||||
|
||||
'last:after:border-transparent',
|
||||
|
||||
'text-tint',
|
||||
'bg-tint-12/1',
|
||||
'hover:text-tint-strong',
|
||||
'max-w-full',
|
||||
'truncate',
|
||||
|
||||
active.id === tab.id
|
||||
? [
|
||||
'shrink-0',
|
||||
'active-tab',
|
||||
'text-tint-strong',
|
||||
'bg-transparent',
|
||||
'after:[&.active-tab]:border-transparent',
|
||||
'after:[:has(+_&.active-tab)]:border-transparent',
|
||||
'after:[:has(&_+)]:border-transparent',
|
||||
]
|
||||
: null
|
||||
'tab -mx-4 flex overflow-hidden px-4 max-md:shrink-0 max-md:last:pr-0 [&:first-child>button]:before:hidden',
|
||||
active.id === tab.id && 'active-tab z-20',
|
||||
description && [
|
||||
'min-w-56 max-md:max-w-[calc(33.33%+1.75rem)]',
|
||||
orientation === 'horizontal' && 'md:max-w-[calc(33.33%+1.75rem)]',
|
||||
]
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-orientation={orientation as 'horizontal' | 'vertical'}
|
||||
aria-selected={active.id === tab.id}
|
||||
aria-controls={getTabPanelId(tab.id)}
|
||||
id={getTabButtonId(tab.id)}
|
||||
@@ -229,23 +201,107 @@ export function DynamicTabs(
|
||||
onSelectTab(tab);
|
||||
}}
|
||||
className={tcls(
|
||||
'inline-block',
|
||||
'text-sm',
|
||||
'transition-[color]',
|
||||
'font-[500]',
|
||||
hashLinkButtonWrapperStyles,
|
||||
description ? 'p-4' : 'px-4 py-1.5',
|
||||
'grow',
|
||||
'transition-colors',
|
||||
'text-left',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-1',
|
||||
'rounded-md',
|
||||
'straight-corners:rounded-none',
|
||||
'circular-corners:rounded-2xl',
|
||||
'focus:outline-none',
|
||||
'focus-visible:ring-2',
|
||||
'ring-inset',
|
||||
'border',
|
||||
'relative',
|
||||
'max-w-full',
|
||||
'truncate'
|
||||
|
||||
// Flared corners
|
||||
'before:absolute before:size-4 before:transition-all before:content-[""] focus-visible:before:hidden',
|
||||
'before:end-full',
|
||||
'before:bottom-0',
|
||||
'before:rounded-ee-md',
|
||||
'circular-corners:before:rounded-ee-2xl',
|
||||
'before:border-e before:border-b',
|
||||
'before:shadow-[4px_4px_0_1px_var(--tw-shadow-color)]',
|
||||
|
||||
'after:absolute after:size-4 after:transition-all after:content-[""] focus-visible:after:hidden',
|
||||
'after:start-full',
|
||||
'after:bottom-0',
|
||||
'after:rounded-es-md',
|
||||
'circular-corners:after:rounded-es-2xl',
|
||||
'after:border-s after:border-b',
|
||||
'after:shadow-[-4px_4px_0_1px_var(--tw-shadow-color)]',
|
||||
|
||||
active.id === tab.id
|
||||
? [
|
||||
'before:border-tint-subtle after:border-tint-subtle',
|
||||
'before:shadow-tint-1 after:shadow-tint-1',
|
||||
]
|
||||
: [
|
||||
'before:border-tint-subtle after:border-tint-subtle',
|
||||
'before:shadow-tint-2 after:shadow-tint-2',
|
||||
'hover:after:shadow-tint-4 hover:before:shadow-tint-4',
|
||||
],
|
||||
orientation === 'vertical' && 'md:after:hidden md:before:hidden',
|
||||
'straight-corners:before:hidden straight-corners:after:hidden',
|
||||
|
||||
'max-md:!rounded-b-none max-md:border-b-0',
|
||||
|
||||
// Position-specific adjustments
|
||||
orientation === 'horizontal' &&
|
||||
position === 'start' &&
|
||||
'md:!rounded-b-none md:border-b-0',
|
||||
orientation === 'vertical' &&
|
||||
position === 'start' &&
|
||||
'md:!rounded-r-none md:border-r-0',
|
||||
orientation === 'vertical' &&
|
||||
position === 'end' &&
|
||||
'md:!rounded-l-none md:border-l-0',
|
||||
|
||||
'text-tint',
|
||||
'snap-start',
|
||||
active.id === tab.id
|
||||
? ['border-tint-subtle bg-tint-base text-tint-strong']
|
||||
: [
|
||||
'border-tint-subtle bg-tint-subtle',
|
||||
'hover:z-10 hover:border-tint-subtle hover:bg-tint-hover hover:text-tint-strong',
|
||||
'focus-visible:z-10 focus-visible:bg-tint-hover focus-visible:text-tint-strong',
|
||||
]
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex flex-row items-center gap-1',
|
||||
description ? 'font-semibold text-base' : 'font-medium text-sm'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'line-clamp-2',
|
||||
active.id === tab.id && 'text-primary-subtle'
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
</div>
|
||||
<HashLinkButton
|
||||
id={getTabButtonId(tab.id)}
|
||||
block={block}
|
||||
label="Direct link to tab"
|
||||
className={tcls(
|
||||
'-mt-px ml-auto',
|
||||
orientation === 'vertical' || description
|
||||
? 'max-md:-mr-3'
|
||||
: '-mr-3'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{description ? (
|
||||
<p className="line-clamp-5 text-sm">{description}</p>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
<HashLinkButton
|
||||
id={getTabButtonId(tab.id)}
|
||||
block={block}
|
||||
label="Direct link to tab"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -255,7 +311,23 @@ export function DynamicTabs(
|
||||
role="tabpanel"
|
||||
id={getTabPanelId(tab.id)}
|
||||
aria-labelledby={getTabButtonId(tab.id)}
|
||||
className={tcls('p-4', tab.id !== active.id ? 'hidden' : null)}
|
||||
className={tcls(
|
||||
'p-4',
|
||||
'rounded-md',
|
||||
'straight-corners:rounded-none',
|
||||
'circular-corners:rounded-2xl',
|
||||
'z-10',
|
||||
'bg-tint-base',
|
||||
position === 'start' && '!rounded-tl-none',
|
||||
position === 'end' && '!rounded-tr-none',
|
||||
'transition-all',
|
||||
'ring-1',
|
||||
'ring-inset',
|
||||
'ring-tint-subtle',
|
||||
'grow',
|
||||
'depth-subtle:shadow-sm',
|
||||
tab.id !== active.id ? 'hidden' : null
|
||||
)}
|
||||
>
|
||||
{tabsBody[index]}
|
||||
</div>
|
||||
@@ -300,7 +372,6 @@ function getTabByTitle(input: TabsInput, state: TabsState): TabsItem | null {
|
||||
};
|
||||
})
|
||||
.filter(({ score }) => score >= 0)
|
||||
// .sortBy(({ score }) => -score)
|
||||
.sort(({ score: a }, { score: b }) => b - a)
|
||||
.map(({ item }) => item)[0] ?? null
|
||||
);
|
||||
|
||||
@@ -36,12 +36,16 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
<div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
|
||||
<div
|
||||
className={tcls(
|
||||
'mx-auto grid max-w-3xl justify-between gap-12 lg:max-w-none',
|
||||
'lg:!max-w-none mx-auto grid max-w-3xl site-full-width:max-w-screen-2xl justify-between gap-12',
|
||||
'grid-cols-[auto_auto]',
|
||||
'lg:grid-cols-[18rem_minmax(auto,_48rem)_auto]',
|
||||
'xl:grid-cols-[18rem_minmax(auto,_48rem)_14rem]',
|
||||
'site-full-width:lg:grid-cols-[18rem_minmax(auto,_80rem)_auto]',
|
||||
'site-full-width:xl:grid-cols-[18rem_minmax(auto,_80rem)_14rem]',
|
||||
'page-no-toc:lg:grid-cols-[minmax(auto,_48rem)_auto]',
|
||||
'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]'
|
||||
'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]',
|
||||
'[body:has(.site-full-width,.page-no-toc)_&]:lg:grid-cols-[minmax(auto,_90rem)_auto]',
|
||||
'[body:has(.site-full-width,.page-no-toc)_&]:xl:grid-cols-[14rem_minmax(auto,_90rem)_14rem]'
|
||||
)}
|
||||
>
|
||||
{
|
||||
@@ -102,7 +106,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
'col-span-2 page-has-toc:lg:col-span-1 page-has-toc:lg:col-start-2 page-no-toc:xl:col-span-1 page-no-toc:xl:col-start-2'
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-10 sm:flex-row sm:gap-6">
|
||||
<div className="mx-auto flex max-w-3xl site-full-width:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6">
|
||||
{partition(customization.footer.groups, FOOTER_COLUMNS).map(
|
||||
(column, columnIndex) => (
|
||||
<div
|
||||
|
||||
@@ -104,11 +104,9 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
|
||||
'lg:basis-40',
|
||||
'md:max-w-[40%]',
|
||||
'lg:max-w-lg',
|
||||
'lg:ml-[max(calc((100%-18rem-48rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - margin (3rem)
|
||||
'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem)
|
||||
'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
|
||||
'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]',
|
||||
'page-full-width:lg:ml-[max(calc((100%-18rem-103rem-3rem)/2),1.5rem)]',
|
||||
'page-full-width:2xl:ml-[max(calc((100%-18rem-96rem-14rem+3rem)/2),1.5rem)]',
|
||||
'md:mr-auto',
|
||||
'order-last',
|
||||
'md:order-[unset]',
|
||||
@@ -195,7 +193,6 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
|
||||
<div
|
||||
className={tcls(
|
||||
CONTAINER_STYLE,
|
||||
'page-default-width:max-w-[unset]',
|
||||
'grow',
|
||||
'flex',
|
||||
'items-end',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { SiteSpace } from '@gitbook/api';
|
||||
|
||||
import { getSiteSpaceURL } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { joinPath } from '@/lib/paths';
|
||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||
import { DropdownChevron, DropdownMenu } from './DropdownMenu';
|
||||
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
|
||||
import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
|
||||
|
||||
export function SpacesDropdown(props: {
|
||||
context: GitBookSiteContext;
|
||||
@@ -14,7 +13,6 @@ export function SpacesDropdown(props: {
|
||||
className?: string;
|
||||
}) {
|
||||
const { context, siteSpace, siteSpaces, className } = props;
|
||||
const { linker } = context;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
@@ -67,30 +65,14 @@ export function SpacesDropdown(props: {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{siteSpaces.map((otherSiteSpace, index) => (
|
||||
<SpacesDropdownMenuItem
|
||||
key={`${otherSiteSpace.id}-${index}`}
|
||||
variantSpace={{
|
||||
id: otherSiteSpace.id,
|
||||
title: otherSiteSpace.title,
|
||||
url: otherSiteSpace.urls.published
|
||||
? linker.toLinkForContent(otherSiteSpace.urls.published)
|
||||
: getFallbackSiteSpaceURL(otherSiteSpace, context),
|
||||
}}
|
||||
active={otherSiteSpace.id === siteSpace.id}
|
||||
/>
|
||||
))}
|
||||
<SpacesDropdownMenuItems
|
||||
slimSpaces={siteSpaces.map((space) => ({
|
||||
id: space.id,
|
||||
title: space.title,
|
||||
url: getSiteSpaceURL(context, space),
|
||||
}))}
|
||||
curPath={siteSpace.path}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the site is not published yet, `urls.published` is not available.
|
||||
* To ensure navigation works in preview, we compute a relative URL from the siteSpace path.
|
||||
*/
|
||||
function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) {
|
||||
const { linker, sections } = context;
|
||||
return linker.toPathInSite(
|
||||
sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,12 +6,28 @@ import { joinPath } from '@/lib/paths';
|
||||
import { useCurrentPagePath } from '../hooks';
|
||||
import { DropdownMenuItem } from './DropdownMenu';
|
||||
|
||||
function useVariantSpaceHref(variantSpaceUrl: string) {
|
||||
interface VariantSpace {
|
||||
id: Space['id'];
|
||||
title: Space['title'];
|
||||
url: string;
|
||||
}
|
||||
|
||||
// When switching to a different variant space, we reconstruct the URL by swapping the space path.
|
||||
function useVariantSpaceHref(variantSpaceUrl: string, currentSpacePath: string, active = false) {
|
||||
const currentPathname = useCurrentPagePath();
|
||||
|
||||
// We need to ensure that the variant space URL is not the same as the current space path.
|
||||
// If it is, we return only the variant space URL to redirect to the root of the variant space.
|
||||
// This is necessary in case the currentPathname is the same as the variantSpaceUrl,
|
||||
// otherwise we would redirect to the same space if the variant space that we are switching to is the default one.
|
||||
if (!active && currentPathname.startsWith(`${currentSpacePath}/`)) {
|
||||
return variantSpaceUrl;
|
||||
}
|
||||
|
||||
if (URL.canParse(variantSpaceUrl)) {
|
||||
const targetUrl = new URL(variantSpaceUrl);
|
||||
targetUrl.pathname = joinPath(targetUrl.pathname, currentPathname);
|
||||
|
||||
targetUrl.searchParams.set('fallback', 'true');
|
||||
|
||||
return targetUrl.toString();
|
||||
@@ -22,11 +38,12 @@ function useVariantSpaceHref(variantSpaceUrl: string) {
|
||||
}
|
||||
|
||||
export function SpacesDropdownMenuItem(props: {
|
||||
variantSpace: { id: Space['id']; title: Space['title']; url: string };
|
||||
variantSpace: VariantSpace;
|
||||
active: boolean;
|
||||
currentSpacePath: string;
|
||||
}) {
|
||||
const { variantSpace, active } = props;
|
||||
const variantHref = useVariantSpaceHref(variantSpace.url);
|
||||
const { variantSpace, active, currentSpacePath } = props;
|
||||
const variantHref = useVariantSpaceHref(variantSpace.url, currentSpacePath, active);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem key={variantSpace.id} href={variantHref} active={active}>
|
||||
@@ -34,3 +51,23 @@ export function SpacesDropdownMenuItem(props: {
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpacesDropdownMenuItems(props: {
|
||||
slimSpaces: VariantSpace[];
|
||||
curPath: string;
|
||||
}) {
|
||||
const { slimSpaces, curPath } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{slimSpaces.map((space) => (
|
||||
<SpacesDropdownMenuItem
|
||||
key={space.id}
|
||||
variantSpace={space}
|
||||
active={false}
|
||||
currentSpacePath={curPath}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context';
|
||||
import { getPageDocument } from '@v2/lib/data';
|
||||
import type { GitBookLinker } from '@v2/lib/links';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
@@ -29,6 +28,7 @@ import { PageControlButtons } from './PageControlButtons';
|
||||
import { PrintButton } from './PrintButton';
|
||||
import './pdf.css';
|
||||
import { sanitizeGitBookAppURL } from '@/lib/app';
|
||||
import { getPageDocument } from '@v2/lib/data';
|
||||
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function PDFPage(props: {
|
||||
}) {
|
||||
const baseContext = props.context;
|
||||
const searchParams = new URLSearchParams(props.searchParams);
|
||||
const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
|
||||
const pdfParams = getPDFSearchParams(searchParams);
|
||||
|
||||
const customization =
|
||||
'customization' in baseContext ? baseContext.customization : defaultCustomization();
|
||||
@@ -224,8 +224,7 @@ async function PDFPageDocument(props: {
|
||||
context: GitBookSpaceContext;
|
||||
}) {
|
||||
const { page, context } = props;
|
||||
const { space } = context;
|
||||
const document = await getPageDocument(context.dataFetcher, space, page);
|
||||
const document = await getPageDocument(context, page);
|
||||
|
||||
return (
|
||||
<PrintPage id={getPagePDFContainerId(page)}>
|
||||
@@ -246,6 +245,7 @@ async function PDFPageDocument(props: {
|
||||
page,
|
||||
},
|
||||
getId: (id) => getPagePDFContainerId(page, id),
|
||||
shouldRenderLinkPreviews: false, // We don't want to render link previews in the PDF.
|
||||
}}
|
||||
// We consider all pages as offscreen in PDF mode
|
||||
// to ensure we can efficiently render as many pages as possible
|
||||
|
||||
@@ -48,11 +48,9 @@ export function PageAside(props: {
|
||||
'group/aside',
|
||||
'hidden',
|
||||
'xl:flex',
|
||||
// 'page-no-toc:lg:flex',
|
||||
'flex-col',
|
||||
'basis-56',
|
||||
// 'page-no-toc:basis-40',
|
||||
// 'page-no-toc:xl:basis-56',
|
||||
'xl:ml-12',
|
||||
'grow-0',
|
||||
'shrink-0',
|
||||
'break-anywhere', // To prevent long words in headings from breaking the layout
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { t } from '@/intl/translate';
|
||||
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
|
||||
import { hasFullWidthBlock, hasMoreThan, isNodeEmpty } from '@/lib/document';
|
||||
import type { AncestorRevisionPage } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
@@ -17,6 +17,8 @@ import { PageFooterNavigation } from './PageFooterNavigation';
|
||||
import { PageHeader } from './PageHeader';
|
||||
import { PreservePageLayout } from './PreservePageLayout';
|
||||
|
||||
const LINK_PREVIEW_MAX_COUNT = 100;
|
||||
|
||||
export function PageBody(props: {
|
||||
context: GitBookSiteContext;
|
||||
page: RevisionPageDocument;
|
||||
@@ -27,7 +29,18 @@ export function PageBody(props: {
|
||||
const { page, context, ancestors, document, withPageFeedback } = props;
|
||||
const { customization } = context;
|
||||
|
||||
const asFullWidth = document ? hasFullWidthBlock(document) : false;
|
||||
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
|
||||
|
||||
// Render link previews only if there are less than LINK_PREVIEW_MAX_COUNT links in the document.
|
||||
const shouldRenderLinkPreviews = document
|
||||
? !hasMoreThan(
|
||||
document,
|
||||
(inline) => inline.object === 'inline' && inline.type === 'link',
|
||||
LINK_PREVIEW_MAX_COUNT
|
||||
)
|
||||
: false;
|
||||
const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q';
|
||||
const asFullWidth = pageFullWidth || contentFullWidth;
|
||||
const language = getSpaceLanguage(customization);
|
||||
const updatedAt = page.updatedAt ?? page.createdAt;
|
||||
|
||||
@@ -36,15 +49,11 @@ export function PageBody(props: {
|
||||
<main
|
||||
className={tcls(
|
||||
'relative min-w-0 flex-1',
|
||||
'py-8 lg:px-12',
|
||||
'mx-auto max-w-screen-2xl py-8',
|
||||
// Allow words to break if they are too long.
|
||||
'break-anywhere',
|
||||
// When in api page mode without the aside, we align with the border of the main content
|
||||
'page-api-block:xl:max-2xl:pr-0',
|
||||
// Max size to ensure one column in api is aligned with rest of content (2 x 3xl) + (gap-3 + 2) * px-12
|
||||
'page-api-block:mx-auto page-api-block:max-w-screen-2xl',
|
||||
// page.layout.tableOfContents ? null : 'xl:ml-56',
|
||||
asFullWidth ? 'page-full-width' : 'page-default-width',
|
||||
pageFullWidth ? 'page-full-width 2xl:px-8' : 'page-default-width',
|
||||
asFullWidth ? 'site-full-width' : 'site-default-width',
|
||||
page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
|
||||
)}
|
||||
>
|
||||
@@ -70,6 +79,7 @@ export function PageBody(props: {
|
||||
context={{
|
||||
mode: 'default',
|
||||
contentContext: context,
|
||||
shouldRenderLinkPreviews,
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
@@ -81,7 +91,7 @@ export function PageBody(props: {
|
||||
<PageFooterNavigation context={context} page={page} />
|
||||
) : null}
|
||||
|
||||
<div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
|
||||
<div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl page-full-width:max-w-screen-2xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
|
||||
{updatedAt ? (
|
||||
<p className="mr-auto text-sm">
|
||||
{t(language, 'page_last_modified', <DateRelative value={updatedAt} />)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user