Compare commits

...

63 Commits

Author SHA1 Message Date
Steven Hall 8723182795 Revert some temporary logging that was implemented to investigate font issues. 2025-04-05 09:43:11 +01:00
Steven Hall c22490bd80 Prefer @vercel/og for icon generation. 2025-04-05 09:22:13 +01:00
Steven Hall 860fd6d02e Use system-ui font in icons. 2025-04-05 09:12:20 +01:00
Steven Hall c43c524804 Fix a typing issue using ArrayBuffer.detached 2025-04-05 09:06:17 +01:00
Steven H 66bb938d85 Use Inter font when generating icons to prevent ArrayBuffer re-use. (#3101) 2025-04-05 09:01:41 +01:00
Steven H 0e6d928496 Use Inter font when generating icons to prevent ArrayBuffer re-use. (#3100) 2025-04-05 08:47:56 +01:00
Steven H f07982d9f8 Investigate issue where icons and ogimage would occasionally fail. (#3099) 2025-04-05 08:15:02 +01:00
Steven H b92ecfad58 Implement retry logic for the DO cache to prevent when revalidating content. (#3098) 2025-04-04 23:44:32 +01:00
Zeno Kapitein 416bde70bc Link tooltips + AI link summaries (#3088)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2025-04-04 20:56:42 +02:00
Steven H 580f7ade15 Better debugging of GitBook v1s revalidate endpoint. (#3097) 2025-04-04 17:36:24 +01:00
Zeno Kapitein cdffd7c47b Desaturate text colours by decreasing chroma for the last steps of the color scale (#3096) 2025-04-04 18:00:01 +02:00
Greg Bergé 5a959cf041 Fix a regression on OpenAPI example bg (#3095) 2025-04-04 15:05:53 +00:00
Zeno Kapitein 31466546a2 Change overlay close icon to close (#3089) 2025-04-04 16:42:47 +02:00
Steven H 90ead98eaa Better error handling in revalidate v1 (#3094) 2025-04-04 14:59:14 +01:00
Samy Pessé 65e9cb9d5c Update next to "canary" for v2 (#3093) 2025-04-04 14:39:43 +02:00
Greg Bergé e59076a074 Improve OpenAPI schemas block ungrouped style (#3092) 2025-04-04 14:28:44 +02:00
Nolann B. 653920dec9 Add tests for generateSchemaExample (#3086) 2025-04-03 19:42:40 +02:00
Greg Bergé 23cedd2d8b Hide deprecated properties in examples (#3085) 2025-04-03 19:40:52 +02:00
Nolann B. 70c4182fd8 Improve OpenAPI schema style (#3079) 2025-04-03 16:10:20 +00:00
Samy Pessé b62b101065 Do not use insights cookie if disabled (#3087) 2025-04-03 17:30:43 +02:00
Zeno Kapitein b9c929beeb Update page breadcrumb color (for links-accent crumbs) (#3082) 2025-04-03 16:34:14 +02:00
Nolann B. 2b6c593289 Remove stable from x-stability (#3083) 2025-04-03 11:33:39 +02:00
Samy Pessé c0b339e406 Version Packages (#3075)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-02 13:36:04 +02:00
Nolann B. da485f5144 Fix read-only in generateSchemaExample (#3069) 2025-04-02 13:03:51 +02:00
Nolann B. 139a8050a5 Fix OpenAPI enum display (#3077) 2025-04-02 10:15:19 +02:00
Nolann B. da7b369ffd Fix missing headers in OpenAPIResponses (#3074) 2025-04-01 21:57:36 +02:00
Samy Pessé 432da64919 Version Packages (#3073)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 18:13:52 +02:00
Nolann B. 7d0b422200 Handle grouped OpenAPISchemas (#3071) 2025-04-01 17:24:43 +02:00
Zeno Kapitein fb90eb0acc Limit tinted background on bold theme to sites with filled sidebar (#3072) 2025-04-01 16:43:08 +02:00
Samy Pessé b647538e41 Version Packages (#3033)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 16:35:09 +02:00
Samy Pessé e50797a5c1 Adapt new computed content and add tag for translation (#3070) 2025-04-01 15:10:55 +02:00
Nolann B. 5b1e01c786 Support for x-stability property (#3064) 2025-03-31 13:49:37 +02:00
Nolann B. 813b2af06b Support for x-enumDescriptions and x-gitbook-enum (#3066) 2025-03-31 12:18:57 +02:00
Nolann B. 6f71da88d8 Fix padding in OpenAPI schemas (#3067) 2025-03-31 12:03:53 +02:00
Samy Pessé fa91eb7eaf Fix dark mode PDF (#3065) 2025-03-28 18:00:33 +01:00
Zeno Kapitein c756761591 Add breadcrumbs to search results (#2772) 2025-03-28 13:29:05 +01:00
Samy Pessé 434af90366 Fix image resizing when using proxy by passing signing identifier in site URL data (#3063)
Co-authored-by: Taran Vohra <taranvohra@outlook.com>
2025-03-28 12:28:12 +01:00
Valentino Hudhra 0f41e194cb Fix dismissed annoucement banner ignored when switching sites (#3057) 2025-03-28 10:56:18 +01:00
Nolann B. bd353489e9 Fix missing alternative schemas (#3058) 2025-03-28 06:53:28 +01:00
Samy Pessé 77fd393f15 Fix click on search items when embedded in an iframe and track announcement links (#3061) 2025-03-27 20:13:14 +01:00
Taran Vohra 8701b5e078 Use the site canonical URL when rewriting in the middleware (#3060) 2025-03-27 19:39:39 +01:00
Nolann B. 61db166f92 Add OpenAPI write-only indicator (#3059) 2025-03-27 18:21:38 +01:00
Zeno Kapitein 0d9b9cea0f Fix announcement banner margin (#3056) 2025-03-27 15:59:47 +00:00
Nolann B. 7bb37c7f0a Move filterSelectedOpenAPISchemas to @gitbook/openapi-parser (#3054) 2025-03-27 15:53:41 +01:00
Taran Vohra 5b2bf828e3 Use stable site URL data for route rewrite in the middleware (#3053) 2025-03-27 18:26:06 +05:30
Samy Pessé c85d3f9752 Serve a markdown version of the page when adding a .md extension (#3055)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-03-27 12:35:18 +00:00
Zeno Kapitein 4d7088bbf0 Stop variant selector from growing too large (#3043) 2025-03-27 10:42:18 +01:00
Samy Pessé dd6ab6044c Fix crash when using user mention or accessing invalid change requests (#3051) 2025-03-27 01:04:00 +01:00
Samy Pessé d236bf029c Fix flash for site with dark default mode (#3050) 2025-03-26 18:12:45 +01:00
Zeno Kapitein d70d566abe Support site announcement banner (#3008)
Co-authored-by: Valentino Hudhra <v.hudhra@gmail.com>
2025-03-26 16:37:05 +01:00
Nolann B. a25fdedea9 Replace $ref with $reference in json-decycle (#3049) 2025-03-26 14:16:03 +00:00
Nolann B. 373183a2cd Safe parse OpenAPI JSON schema (#3046) 2025-03-26 13:55:22 +01:00
Samy Pessé e9fa50dfb5 Trim search query before doing search (#3042) 2025-03-26 10:18:18 +00:00
Samy Pessé 57ca4e0e88 Fix crash when integration is not installed on the space (#3040) 2025-03-26 10:19:57 +01:00
Samy Pessé bc90adb73a Allow indexation of images to ensure favicons display on Google (#3041) 2025-03-26 10:17:57 +01:00
Nolann B. 1505ddbd5a Fix multiple request examples selector not showing (#3039) 2025-03-25 20:48:57 +01:00
Greg Bergé cd99ed57fb Fix OpenAPI spec rendering (#3037) 2025-03-25 15:13:18 +00:00
Samy Pessé 27de1cd01d Default to the first ID as being active in scroll detection (#3036) 2025-03-25 15:39:45 +01:00
Nolann B. ae78fc5394 Fix XML in code sample (#3030) 2025-03-25 15:11:45 +01:00
Samy Pessé 40e8e69c6d Disallow crawling by web-robots of search/ask URLs (#3035) 2025-03-25 13:51:09 +00:00
Nolann B. e84a46a558 Fix OpenAPI tabs indicator overflow (#3032) 2025-03-25 10:03:58 +01:00
Samy Pessé 18a72d4466 v2: improve caching (#3031) 2025-03-25 09:40:54 +01:00
Zeno Kapitein d7aa5fa003 Remove loading state for recommended questions (#3029) 2025-03-24 19:19:12 +01:00
175 changed files with 4909 additions and 1277 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Implement retry logic for the DO cache to prevent when revalidating content.
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/colors": patch
---
Desaturate text colors by decreasing chroma for the last steps of the color scale
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Improve OpenAPI schema style
+7
View File
@@ -0,0 +1,7 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
'gitbook': patch
---
Remove stable from x-stability
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve the error message returned by the revalidate endpoint.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Do not set cookie to identify visitor for insights when disabled.
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Improve OpenAPI schemas block ungrouped style. Classnames have changed, please refer to this PR to update GBX.
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-openapi": patch
---
Hide deprecated properties in examples
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Better error handling in cache revalidation.
+131 -47
View File
@@ -35,7 +35,7 @@
},
"packages/colors": {
"name": "@gitbook/colors",
"version": "0.3.0",
"version": "0.3.1",
"devDependencies": {
"typescript": "^5.5.3",
},
@@ -49,7 +49,7 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.8.1",
"version": "0.9.2",
"dependencies": {
"@gitbook/api": "*",
"@gitbook/cache-do": "workspace:*",
@@ -64,15 +64,16 @@
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.1.8",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
"@upstash/redis": "^1.27.1",
"ai": "^4.1.46",
"ajv": "^8.12.0",
"@vercel/og": "0.6.8.",
"ai": "^4.2.2",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"event-iterator": "^2.0.0",
"framer-motion": "^10.16.14",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
@@ -80,14 +81,15 @@
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.15",
"next": "14.2.25",
"memoizee": "^0.4.17",
"next": "14.2.26",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"partial-json": "^0.1.7",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hotkeys-hook": "^4.4.1",
@@ -104,6 +106,9 @@
"unified": "^11.0.5",
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5",
"zustand": "^5.0.3",
},
"devDependencies": {
"@argos-ci/playwright": "^4.3.0",
@@ -136,13 +141,14 @@
},
"packages/gitbook-v2": {
"name": "gitbook-v2",
"version": "0.2.1",
"version": "0.2.3",
"dependencies": {
"@gitbook/api": "*",
"@gitbook/cache-tags": "workspace:*",
"@sindresorhus/fnv1a": "^3.1.0",
"jwt-decode": "^4.0.0",
"next": "^15.2.3",
"next": "canary",
"p-memoize": "^7.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rison": "^0.1.1",
@@ -179,7 +185,7 @@
},
"packages/openapi-parser": {
"name": "@gitbook/openapi-parser",
"version": "2.1.0",
"version": "2.1.2",
"dependencies": {
"@scalar/openapi-parser": "^0.10.10",
"@scalar/openapi-types": "^0.1.9",
@@ -233,7 +239,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.1.5",
"version": "1.1.9",
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "^1.2.5",
@@ -261,18 +267,18 @@
},
"overrides": {
"@codemirror/state": "6.4.1",
"@gitbook/api": "0.102.0",
"@gitbook/api": "0.108.0",
"react": "18.3.1",
"react-dom": "18.3.1",
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@1.0.9", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.1.10", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "eventsource-parser": "^3.0.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.4", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-13sEGBxB6kgaMPGOgCLYibF6r8iv8mgjhuToFrOTU09bBxbFQd8ZoARarCfJN6VomCUbUvMKwjTBLb1vQnN+WA=="],
"@ai-sdk/react": ["@ai-sdk/react@1.1.19", "", { "dependencies": { "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/ui-utils": "1.1.16", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-zqSOWmJxpB45ZrwZ04+Q7Uo3xeGM0tva2eUYz2T4gv9Yk6MQAfOBA6+scsKg8CyIuUy4M4/C4pCY3eWQf7sfQg=="],
"@ai-sdk/react": ["@ai-sdk/react@1.2.6", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.4", "@ai-sdk/ui-utils": "1.2.5", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-5BFChNbcYtcY9MBStcDev7WZRHf0NpTrk8yfSoedWctB3jfWkFd1HECBvdc8w3mUQshF2MumLHtAhRO7IFtGGQ=="],
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.1.16", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q=="],
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.5", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.4", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-XDgqnJcaCkDez7qolvk+PDbs/ceJvgkNkxkOlc9uDWqxfDJxtvCZ+14MP/1qr4IBwGIgKVHzMDYDXvqVhSWLzg=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -622,7 +628,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.102.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9wPr5kyCHhTTwkaCYEMT4q3JjEl1q9mytgvpeusmeREfG4E0RZHYPEL8bmumO7T9wH/0vHjpfW2LSTUUs60AwQ=="],
"@gitbook/api": ["@gitbook/api@0.108.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-OytauuT3IMf0zl6rBvIacQ7YphzNhpuNAvEVQbysCGo3BhGYD1xv+vXVmtkAGUnFEAclw2fg2VjWPt3sOlQsAg=="],
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
@@ -750,25 +756,25 @@
"@msgpack/msgpack": ["@msgpack/msgpack@3.0.0-beta2", "", {}, "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw=="],
"@next/env": ["@next/env@14.2.25", "", {}, "sha512-JnzQ2cExDeG7FxJwqAksZ3aqVJrHjFwZQAEJ9gQZSoEhIow7SNoKZzju/AwQ+PLIR4NY8V0rhcVozx/2izDO0w=="],
"@next/env": ["@next/env@14.2.26", "", {}, "sha512-vO//GJ/YBco+H7xdQhzJxF7ub3SUwft76jwaeOyVVQFHCi5DCnkP16WHB+JBylo4vOKPoZBlR94Z8xBxNBdNJA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.25", "", { "os": "darwin", "cpu": "arm64" }, "sha512-09clWInF1YRd6le00vt750s3m7SEYNehz9C4PUcSu3bAdCTpjIV4aTYQZ25Ehrr83VR1rZeqtKUPWSI7GfuKZQ=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.26", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zDJY8gsKEseGAxG+C2hTMT0w9Nk9N1Sk1qV7vXYz9MEiyRoF5ogQX2+vplyUMIfygnjn9/A04I6yrUTRTuRiyQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.25", "", { "os": "darwin", "cpu": "x64" }, "sha512-V+iYM/QR+aYeJl3/FWWU/7Ix4b07ovsQ5IbkwgUK29pTHmq+5UxeDr7/dphvtXEq5pLB/PucfcBNh9KZ8vWbug=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.26", "", { "os": "darwin", "cpu": "x64" }, "sha512-U0adH5ryLfmTDkahLwG9sUQG2L0a9rYux8crQeC92rPhi3jGQEY47nByQHrVrt3prZigadwj/2HZ1LUUimuSbg=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-LFnV2899PJZAIEHQ4IMmZIgL0FBieh5keMnriMY1cK7ompR+JUd24xeTtKkcaw8QmxmEdhoE5Mu9dPSuDBgtTg=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-SINMl1I7UhfHGM7SoRiw0AbwnLEMUnJ/3XXVmhyptzriHbWvPPbbm0OEVG24uUKhuS1t0nvN/DBvm5kz6ZIqpg=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-QC5y5PPTmtqFExcKWKYgUNkHeHE/z3lUsu83di488nyP0ZzQ3Yse2G6TCxz6nNsQwgAx1BehAJTZez+UQxzLfw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-s6JaezoyJK2DxrwHWxLWtJKlqKqTdi/zaYigDXUJ/gmx/72CrzdVZfMvUc6VqnZ7YEvRijvYo+0o4Z9DencduA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.25", "", { "os": "linux", "cpu": "x64" }, "sha512-y6/ML4b9eQ2D/56wqatTJN5/JR8/xdObU2Fb1RBidnrr450HLCKr6IJZbPqbv7NXmje61UyxjF5kvSajvjye5w=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.26", "", { "os": "linux", "cpu": "x64" }, "sha512-FEXeUQi8/pLr/XI0hKbe0tgbLmHFRhgXOUiPScz2hk0hSmbGiU8aUqVslj/6C6KA38RzXnWoJXo4FMo6aBxjzg=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.25", "", { "os": "linux", "cpu": "x64" }, "sha512-sPX0TSXHGUOZFvv96GoBXpB3w4emMqKeMgemrSxI7A6l55VBJp/RKYLwZIB9JxSqYPApqiREaIIap+wWq0RU8w=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.26", "", { "os": "linux", "cpu": "x64" }, "sha512-BUsomaO4d2DuXhXhgQCVt2jjX4B4/Thts8nDoIruEJkhE5ifeQFtvW5c9JkdOtYvE5p2G0hcwQ0UbRaQmQwaVg=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.25", "", { "os": "win32", "cpu": "arm64" }, "sha512-ReO9S5hkA1DU2cFCsGoOEp7WJkhFzNbU/3VUF6XxNGUCQChyug6hZdYL/istQgfT/GWE6PNIg9cm784OI4ddxQ=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.26", "", { "os": "win32", "cpu": "arm64" }, "sha512-5auwsMVzT7wbB2CZXQxDctpWbdEnEW/e66DyXO1DcgHxIyhP06awu+rHKshZE+lPLIGiwtjo7bsyeuubewwxMw=="],
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.25", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZ/gc0o9neuCDyD5IumyTGHVun2dCox5TfPQI/BJTYwpSNYM3CZDI4i6TOdjeq1JMo+Ug4kPSMuZdwsycwFbAw=="],
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.26", "", { "os": "win32", "cpu": "ia32" }, "sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.25", "", { "os": "win32", "cpu": "x64" }, "sha512-KSznmS6eFjQ9RJ1nEc66kJvtGIL1iZMYmGEXsZPh2YtnLtqrgdVvKXJY2ScjjoFnG6nGLyPFR0UiEvDwVah4Tw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.26", "", { "os": "win32", "cpu": "x64" }, "sha512-2rdB3T1/Gp7bv1eQTTm9d1Y1sv9UuJ2LAwOE0Pe2prHKe32UNscj7YS13fRB37d0GAiGNR+Y7ZcW8YjDI8Ns0w=="],
"@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
@@ -836,6 +842,8 @@
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="],
@@ -1066,6 +1074,8 @@
"@replit/codemirror-css-color-picker": ["@replit/codemirror-css-color-picker@6.3.0", "", { "peerDependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-19biDANghUm7Fz7L1SNMIhK48tagaWuCOHj4oPPxc7hxPGkTVY2lU/jVZ8tsbTKQPVG7BO2CBDzs7CBwb20t4A=="],
"@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.4.0", "", {}, "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
"@scalar/api-client": ["@scalar/api-client@2.3.5", "", { "dependencies": { "@headlessui/tailwindcss": "^0.2.0", "@headlessui/vue": "^1.7.20", "@scalar/components": "0.13.37", "@scalar/draggable": "0.1.11", "@scalar/icons": "0.1.3", "@scalar/import": "0.3.2", "@scalar/oas-utils": "0.2.120", "@scalar/object-utils": "1.1.13", "@scalar/openapi-parser": "0.10.10", "@scalar/openapi-types": "0.1.9", "@scalar/postman-to-openapi": "0.1.43", "@scalar/snippetz": "0.2.16", "@scalar/themes": "0.9.79", "@scalar/types": "0.1.1", "@scalar/use-codemirror": "0.11.82", "@scalar/use-hooks": "0.1.33", "@scalar/use-toasts": "0.7.9", "@scalar/use-tooltip": "1.0.6", "@vueuse/core": "^10.10.0", "@vueuse/integrations": "^11.2.0", "focus-trap": "^7", "fuse.js": "^7.0.0", "microdiff": "^1.4.0", "nanoid": "^5.0.9", "pretty-bytes": "^6.1.1", "pretty-ms": "^8.0.0", "shell-quote": "^1.8.1", "vue": "^3.5.12", "vue-router": "^4.3.0", "whatwg-mimetype": "^4.0.0", "yaml": "^2.4.5", "zod": "^3.23.8" } }, "sha512-bVBP8H3laa4VG3dExv4Ak/Kf+q9z8aK/Glpv6CPE0wxgZJWxwkJoSrvwxUE+46JktSObUrhu4xQiqSCmBz8esQ=="],
@@ -1120,6 +1130,8 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@shuding/opentype.js": ["@shuding/opentype.js@1.4.0-beta.0", "", { "dependencies": { "fflate": "^0.7.3", "string.prototype.codepointat": "^0.2.1" }, "bin": { "ot": "bin/ot" } }, "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA=="],
"@sinclair/typebox": ["@sinclair/typebox@0.25.24", "", {}, "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ=="],
"@sindresorhus/fnv1a": ["@sindresorhus/fnv1a@3.1.0", "", {}, "sha512-KV321z5m/0nuAg83W1dPLy85HpHDk7Sdi4fJbwvacWsEhAh+rZUW4ZfGcXmUIvjZg4ss2bcwNlRhJ7GBEUG08w=="],
@@ -1314,8 +1326,6 @@
"@unhead/schema": ["@unhead/schema@1.11.14", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-V9W9u5tF1/+TiLqxu+Qvh1ShoMDkPEwHoEo4DKdDG6ko7YlbzFfDxV6el9JwCren45U/4Vy/4Xi7j8OH02wsiA=="],
"@upstash/redis": ["@upstash/redis@1.34.3", "", { "dependencies": { "crypto-js": "^4.2.0" } }, "sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ=="],
"@vercel/build-utils": ["@vercel/build-utils@9.0.1", "", {}, "sha512-pG/izEqA0AGyqQj6QBfGoTOKU9FPG18sYw9qpncEq00uA+J4Ly4e8ssNbENsXtnXqkMjeoS3c5ncR3jT0bOyiA=="],
"@vercel/error-utils": ["@vercel/error-utils@2.0.3", "", {}, "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ=="],
@@ -1336,6 +1346,8 @@
"@vercel/node": ["@vercel/node@5.0.2", "", { "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "16.18.11", "@vercel/build-utils": "9.0.1", "@vercel/error-utils": "2.0.3", "@vercel/nft": "0.27.10", "@vercel/static-config": "3.0.0", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", "es-module-lexer": "1.4.1", "esbuild": "0.14.47", "etag": "1.8.1", "node-fetch": "2.6.9", "path-to-regexp": "6.2.1", "ts-morph": "12.0.0", "ts-node": "10.9.1", "typescript": "4.9.5", "undici": "5.28.4" } }, "sha512-UcUVBC6i4j3WPxLA5GYnSvRd/E1fpqJ5dnMZMLromGCTzIXaw+Uzj7bsSSQ2Y9yPtwnWWrcEDmF3IumSTcdr/w=="],
"@vercel/og": ["@vercel/og@0.6.8", "", { "dependencies": { "@resvg/resvg-wasm": "2.4.0", "satori": "0.12.2", "yoga-wasm-web": "0.3.3" } }, "sha512-e4kQK9mP8ntpo3dACWirGod/hHv4qO5JMj9a/0a2AZto7b4persj5YP7t1Er372gTtYFTYxNhMx34jRvHooglw=="],
"@vercel/python": ["@vercel/python@4.7.0", "", {}, "sha512-mkHmzYYZBLFLdvSdgrnBl1Qc1+LI5YIafSNJOj8oW4YU8vvALLMbwgZp42pZnyXW0e/3uHcesiRp4P0jSB0wyg=="],
"@vercel/redwood": ["@vercel/redwood@2.1.12", "", { "dependencies": { "@vercel/nft": "0.27.10", "@vercel/routing-utils": "5.0.0", "@vercel/static-config": "3.0.0", "semver": "6.3.1", "ts-morph": "12.0.0" } }, "sha512-9CLwF8QKmxWlt1CMxoVD5gxTfwnojo1UlFONRSoUfjpNC+nTHEMCADVyawmDkANXs7Olx4QQSwJqsaWT8A9Jgg=="],
@@ -1392,7 +1404,7 @@
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
"ai": ["ai@4.1.47", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/react": "1.1.19", "@ai-sdk/ui-utils": "1.1.16", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-9UZ8Mkv1HlprCJfQ0Kq+rgKbfkrkDtJjslr1WOHBRzvC70jDJJYp8r0Qq4vlF7zs9VBkGyy8Rhm5zQJJIUjvgw=="],
"ai": ["ai@4.3.0", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.4", "@ai-sdk/react": "1.2.6", "@ai-sdk/ui-utils": "1.2.5", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-PxyQYKhWaU3LiZEpeKRaekVonZIbWdKAwgnqm0CSAxy1MFufmYEC5SM5Mc9uiK2DoHcbAL3d1jyaQ2fSDAJL8w=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
@@ -1502,6 +1514,8 @@
"camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="],
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001668", "", {}, "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="],
"capnp-ts": ["capnp-ts@0.7.0", "", { "dependencies": { "debug": "^4.3.1", "tslib": "^2.2.0" } }, "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g=="],
@@ -1584,12 +1598,20 @@
"cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="],
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
"crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
"css-background-parser": ["css-background-parser@0.1.0", "", {}, "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="],
"css-box-shadow": ["css-box-shadow@1.0.0-3", "", {}, "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="],
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
"css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="],
"css-gradient-parser": ["css-gradient-parser@0.0.16", "", {}, "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA=="],
"css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="],
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
@@ -1806,6 +1828,8 @@
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
"fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="],
"file-entry-cache": ["file-entry-cache@10.0.7", "", { "dependencies": { "flat-cache": "^6.1.7" } }, "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
@@ -1950,6 +1974,8 @@
"hastscript": ["hastscript@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw=="],
"hex-rgb": ["hex-rgb@4.3.0", "", {}, "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw=="],
"highlight.js": ["highlight.js@11.10.0", "", {}, "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ=="],
"highlightjs-curl": ["highlightjs-curl@1.3.0", "", {}, "sha512-50UEfZq1KR0Lfk2Tr6xb/MUIZH3h10oNC0OTy9g7WELcs5Fgy/mKN1vEhuKTkKbdo8vr5F9GXstu2eLhApfQ3A=="],
@@ -2114,6 +2140,8 @@
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
@@ -2280,7 +2308,7 @@
"mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
"mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
@@ -2318,7 +2346,7 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next": ["next@14.2.25", "", { "dependencies": { "@next/env": "14.2.25", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.25", "@next/swc-darwin-x64": "14.2.25", "@next/swc-linux-arm64-gnu": "14.2.25", "@next/swc-linux-arm64-musl": "14.2.25", "@next/swc-linux-x64-gnu": "14.2.25", "@next/swc-linux-x64-musl": "14.2.25", "@next/swc-win32-arm64-msvc": "14.2.25", "@next/swc-win32-ia32-msvc": "14.2.25", "@next/swc-win32-x64-msvc": "14.2.25" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-N5M7xMc4wSb4IkPvEV5X2BRRXUmhVHNyaXwEM86+voXthSZz8ZiRyQW4p9mwAoAPIm6OzuVZtn7idgEJeAJN3Q=="],
"next": ["next@14.2.26", "", { "dependencies": { "@next/env": "14.2.26", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.26", "@next/swc-darwin-x64": "14.2.26", "@next/swc-linux-arm64-gnu": "14.2.26", "@next/swc-linux-arm64-musl": "14.2.26", "@next/swc-linux-x64-gnu": "14.2.26", "@next/swc-linux-x64-musl": "14.2.26", "@next/swc-win32-arm64-msvc": "14.2.26", "@next/swc-win32-ia32-msvc": "14.2.26", "@next/swc-win32-x64-msvc": "14.2.26" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw=="],
"next-themes": ["next-themes@0.2.1", "", { "peerDependencies": { "next": "*", "react": "*", "react-dom": "*" } }, "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A=="],
@@ -2392,6 +2420,8 @@
"p-map": ["p-map@7.0.2", "", {}, "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q=="],
"p-memoize": ["p-memoize@7.1.1", "", { "dependencies": { "mimic-fn": "^4.0.0", "type-fest": "^3.0.0" } }, "sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"package-json": ["package-json@6.5.0", "", { "dependencies": { "got": "^9.6.0", "registry-auth-token": "^4.0.0", "registry-url": "^5.0.0", "semver": "^6.2.0" } }, "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ=="],
@@ -2402,10 +2432,14 @@
"package-manager-manager": ["package-manager-manager@0.2.0", "", { "dependencies": { "js-yaml": "^4.1.0", "shellac": "^0.8.0" } }, "sha512-V02gl0bafXJ2gcY6j+5IHM7UdnYwmF+2OsFZuqVcha6iMSStD4dpIOBOsypnUIwOi4jLcPz6RQuyifmAE3mG8g=="],
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="],
"parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="],
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"parse-ms": ["parse-ms@2.1.0", "", {}, "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="],
@@ -2414,6 +2448,8 @@
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
@@ -2604,6 +2640,8 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"satori": ["satori@0.12.2", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.16", "css-to-react-native": "^3.0.0", "emoji-regex": "^10.2.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-wasm-web": "^0.3.3" } }, "sha512-3C/laIeE6UUe9A+iQ0A48ywPVCCMKCNSTU5Os101Vhgsjd3AAxGNjyq0uAA8kulMPK5n0csn8JlxPN9riXEjLA=="],
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
@@ -2686,6 +2724,8 @@
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="],
@@ -2750,6 +2790,8 @@
"timers-ext": ["timers-ext@0.1.8", "", { "dependencies": { "es5-ext": "^0.10.64", "next-tick": "^1.1.0" } }, "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tippy.js": ["tippy.js@6.3.7", "", { "dependencies": { "@popperjs/core": "^2.9.0" } }, "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ=="],
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
@@ -2802,7 +2844,7 @@
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
"type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="],
@@ -2820,6 +2862,8 @@
"unenv": ["unenv@2.0.0-rc.1", "", { "dependencies": { "defu": "^6.1.4", "mlly": "^1.7.4", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg=="],
"unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
@@ -2942,13 +2986,15 @@
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
"yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="],
"youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="],
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="],
@@ -3676,6 +3722,24 @@
"@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A=="],
"@radix-ui/react-tooltip/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
"@radix-ui/react-tooltip/@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=="],
"@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
"@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="],
"@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
"@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="],
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@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-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
"@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
"@react-aria/focus/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="],
@@ -3686,12 +3750,20 @@
"@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="],
"@scalar/api-client/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@scalar/code-highlight/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="],
"@scalar/oas-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="],
"@scalar/oas-utils/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@scalar/object-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="],
"@scalar/types/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@scalar/use-hooks/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"@smithy/abort-controller/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -3992,7 +4064,7 @@
"gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"gitbook-v2/next": ["next@15.2.3", "", { "dependencies": { "@next/env": "15.2.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.3", "@next/swc-darwin-x64": "15.2.3", "@next/swc-linux-arm64-gnu": "15.2.3", "@next/swc-linux-arm64-musl": "15.2.3", "@next/swc-linux-x64-gnu": "15.2.3", "@next/swc-linux-x64-musl": "15.2.3", "@next/swc-win32-arm64-msvc": "15.2.3", "@next/swc-win32-x64-msvc": "15.2.3", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w=="],
"gitbook-v2/next": ["next@15.3.0-canary.38", "", { "dependencies": { "@next/env": "15.3.0-canary.38", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.0-canary.38", "@next/swc-darwin-x64": "15.3.0-canary.38", "@next/swc-linux-arm64-gnu": "15.3.0-canary.38", "@next/swc-linux-arm64-musl": "15.3.0-canary.38", "@next/swc-linux-x64-gnu": "15.3.0-canary.38", "@next/swc-linux-x64-musl": "15.3.0-canary.38", "@next/swc-win32-arm64-msvc": "15.3.0-canary.38", "@next/swc-win32-x64-msvc": "15.3.0-canary.38", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-oSRXCgoVuQNkMKCbAhmbAopXIbcd3TEmcxvT8pVX5WhK7fJRSugCEem4PJ85KCeMl/76nyvqjG4g6KjtNRcB8w=="],
"global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
@@ -4024,6 +4096,8 @@
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"linebreak/base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="],
"lowlight/highlight.js": ["highlight.js@11.9.0", "", {}, "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="],
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
@@ -4042,6 +4116,8 @@
"mdast-util-gfm-task-list-item/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"meow/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"micro/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
"micro/content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
@@ -4070,6 +4146,8 @@
"normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"p-filter/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="],
"package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -4128,6 +4206,8 @@
"router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"satori/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"semver-diff/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
@@ -4616,6 +4696,8 @@
"@cloudflare/next-on-pages/miniflare/youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="],
"@cloudflare/next-on-pages/miniflare/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@codemirror/lang-json/@codemirror/language/@codemirror/view": ["@codemirror/view@6.34.1", "", { "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ=="],
"@codemirror/lang-json/@codemirror/language/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="],
@@ -4694,6 +4776,8 @@
"@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@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-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@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-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@scalar/api-client/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="],
@@ -4856,23 +4940,23 @@
"gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"gitbook-v2/next/@next/env": ["@next/env@15.2.3", "", {}, "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw=="],
"gitbook-v2/next/@next/env": ["@next/env@15.3.0-canary.38", "", {}, "sha512-RXi1aJKCOiuc5OD9JmRgeNGo4vpFU0Cid0C9X7rFR65HQBpaUx+NuCib2pz5KLqx/a2R4n1EThyxWLUUKYWvIQ=="],
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw=="],
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.0-canary.38", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/RxCcNPZaNayi24Mm8Bad/p1vbncvahNcP3y9bjtMaGG0ldUKWRH/AXfWaBCKPe0t2tp2AR4g7iTArwn8fl8xg=="],
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw=="],
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.0-canary.38", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAKsNcW4QHvPOGNk2fJulEkx390E6mcHgeV/ATiCWIeQZU5lVsuJEC1aUz1OGv/vEh0FzfCURdL6oavEZSjWOA=="],
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ=="],
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.0-canary.38", "", { "os": "linux", "cpu": "arm64" }, "sha512-SRktmhD+YFpjVYpfc8DKkQN9CaGx/SJkx4Ls2mzcvaf1IKgLDhNBFOOepO1tzgjxJRmJaXm3TdgomNwEX/Tr8g=="],
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q=="],
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.0-canary.38", "", { "os": "linux", "cpu": "arm64" }, "sha512-pBCa7pB/2gTbJ4l1yC9WWTuScGD7Gu+6frxXnb3pSE3S0cJDTOZCdWktyvdmTq6tR6OABZXCNty1GNDVr7lduQ=="],
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w=="],
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.0-canary.38", "", { "os": "linux", "cpu": "x64" }, "sha512-2TyYiVuMzoM0OAGNygagfVuQk9LnGm9ip/vDELpSSgJ1mxFIGg1M7g/zDBmmzmv4MGyACEiunGwdOsVI2tHyQA=="],
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg=="],
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.0-canary.38", "", { "os": "linux", "cpu": "x64" }, "sha512-ibeAJmyFHqHioq1DAiIh+OAiyu+iOFLs0x/jXIdiTPizGgommoh0QytRezC/AFEip5qlx2mPdgYVfTlk6FL+GA=="],
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q=="],
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.0-canary.38", "", { "os": "win32", "cpu": "arm64" }, "sha512-e1rF3XvjTAdWK6PRBwuTS2P6fsEbTuBLU5BRtbdtvwzuizskNEzz9XTd+h0/UxowoyvEjaLi9UdIHRsj0+bbTg=="],
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w=="],
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.0-canary.38", "", { "os": "win32", "cpu": "x64" }, "sha512-3H3IQhQG7KoMd7kTem0BsV0R8tepPcrXD6oe+ZJUXZbv1N3laeRExeqsCxdWjeMb26LWcVARBAJqu8A8v+iTzg=="],
"gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
+2 -2
View File
@@ -7,12 +7,12 @@
"turbo": "^2.4.4",
"vercel": "^39.3.0"
},
"packageManager": "bun@1.2.5",
"packageManager": "bun@1.2.8",
"overrides": {
"@codemirror/state": "6.4.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"@gitbook/api": "0.102.0"
"@gitbook/api": "0.108.0"
},
"private": true,
"scripts": {
+32 -9
View File
@@ -60,7 +60,7 @@ export function getCacheTag(
| {
tag: 'computed-document';
space: string;
integration: string;
sourceType: string;
}
/**
* All data related to the URL of a content
@@ -84,6 +84,14 @@ export function getCacheTag(
organization: string;
openAPISpec: string;
}
/**
* All data related to a translation
*/
| {
tag: 'translation';
organization: string;
translationSettings: string;
}
): string {
switch (spec.tag) {
case 'user':
@@ -99,13 +107,15 @@ export function getCacheTag(
case 'document':
return `space:${spec.space}:document:${spec.document}`;
case 'computed-document':
return `space:${spec.space}:computed-document:${spec.integration}`;
return `space:${spec.space}:computed-document:${spec.sourceType}`;
case 'site':
return `site:${spec.site}`;
case 'integration':
return `integration:${spec.integration}`;
case 'openapi':
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
case 'translation':
return `organization:${spec.organization}:translation:${spec.translationSettings}`;
default:
assertNever(spec);
}
@@ -157,6 +167,15 @@ export function getComputedContentSourceCacheTags(
})
);
break;
case 'translation-language':
tags.push(
getCacheTag({
tag: 'translation',
organization: inContext.organizationId,
translationSettings: dependency.ref.translationSettings,
})
);
break;
default:
// Do not throw for unknown dependency types
// as it might mean we are lacking behind the API version
@@ -169,18 +188,22 @@ export function getComputedContentSourceCacheTags(
getCacheTag({
tag: 'computed-document',
space: inContext.spaceId,
integration: source.integration,
sourceType: source.type,
})
);
}
// We invalidate the computed content when a new version of the integration is deployed.
tags.push(
getCacheTag({
tag: 'integration',
integration: source.integration,
})
);
if (source.type.startsWith('integration:')) {
const integration = source.type.split(':')[1];
tags.push(
getCacheTag({
tag: 'integration',
integration,
})
);
}
return tags;
}
+6
View File
@@ -1,5 +1,11 @@
# @gitbook/colors
## 0.3.1
### Patch Changes
- fb90eb0: Reduce chroma of first color scale step
## 0.3.0
### Minor Changes
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "0.3.0",
"version": "0.3.1",
"devDependencies": {
"typescript": "^5.5.3"
},
+1 -1
View File
@@ -220,7 +220,7 @@ export function colorScale(
continue;
}
const chromaRatio = index < 8 ? (index + 1) * 0.05 : 1;
const chromaRatio = index === 8 || index === 9 ? 1 : index * 0.05;
const shade = {
L: targetL, // Blend lightness
+6
View File
@@ -1,5 +1,11 @@
# gitbook-v2
## 0.2.3
### Patch Changes
- 5b2bf82: Use stable site URL data for route rewrite in the middleware
## 0.2.2
### Patch Changes
+4 -3
View File
@@ -1,9 +1,9 @@
{
"name": "gitbook-v2",
"version": "0.2.2",
"version": "0.2.3",
"private": true,
"dependencies": {
"next": "^15.2.3",
"next": "canary",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@gitbook/api": "*",
@@ -12,7 +12,8 @@
"server-only": "^0.0.1",
"warn-once": "^0.1.1",
"rison": "^0.1.1",
"jwt-decode": "^4.0.0"
"jwt-decode": "^4.0.0",
"p-memoize": "^7.1.1"
},
"devDependencies": {
"gitbook": "*",
@@ -20,7 +20,7 @@ export default async function SiteDynamicLayout({
const forcedTheme = await getThemeFromMiddleware();
return (
<CustomizationRootLayout customization={context.customization}>
<CustomizationRootLayout forcedTheme={forcedTheme} customization={context.customization}>
<SiteLayout
context={context}
forcedTheme={forcedTheme}
@@ -1,12 +0,0 @@
import type { NextRequest } from 'next/server';
import { serveLLMsTxt } from '@/routes/llms';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getDynamicSiteContext(await params);
return serveLLMsTxt(context);
}
@@ -1,12 +0,0 @@
import type { NextRequest } from 'next/server';
import { serveRobotsTxt } from '@/routes/robots';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getDynamicSiteContext(await params);
return serveRobotsTxt(context);
}
@@ -1,12 +0,0 @@
import type { NextRequest } from 'next/server';
import { servePagesSitemap } from '@/routes/sitemap';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getDynamicSiteContext(await params);
return servePagesSitemap(context);
}
@@ -1,12 +0,0 @@
import type { NextRequest } from 'next/server';
import { serveRootSitemap } from '@/routes/sitemap';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getDynamicSiteContext(await params);
return serveRootSitemap(context);
}
@@ -3,10 +3,9 @@ import {
generateSitePageMetadata,
generateSitePageViewport,
} from '@/components/SitePage';
import { getCacheTag } from '@gitbook/cache-tags';
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
import type { Metadata, Viewport } from 'next';
import { unstable_cacheTag as cacheTag } from 'next/cache';
export const dynamic = 'force-static';
@@ -15,19 +14,10 @@ type PageProps = {
};
export default async function Page(props: PageProps) {
'use cache';
const params = await props.params;
const { context } = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(params);
cacheTag(
getCacheTag({
tag: 'site',
site: context.site.id,
})
);
return <SitePage context={context} pageParams={{ pathname }} />;
}
@@ -4,10 +4,8 @@ import {
generateSiteLayoutMetadata,
generateSiteLayoutViewport,
} from '@/components/SiteLayout';
import { getCacheTag } from '@gitbook/cache-tags';
import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils';
import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env';
import { unstable_cacheTag as cacheTag } from 'next/cache';
interface SiteStaticLayoutProps {
params: Promise<RouteLayoutParams>;
@@ -17,17 +15,8 @@ export default async function SiteStaticLayout({
params,
children,
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
'use cache';
const { context, visitorAuthClaims } = await getStaticSiteContext(await params);
cacheTag(
getCacheTag({
tag: 'site',
site: context.site.id,
})
);
return (
<CustomizationRootLayout customization={context.customization}>
<SiteLayout
@@ -10,5 +10,5 @@ export async function GET(
{ params }: { params: Promise<RouteLayoutParams> }
) {
const { context } = await getStaticSiteContext(await params);
return serveLLMsTxt(context);
return serveLLMsTxt(context, { withMarkdownPages: true });
}
@@ -0,0 +1,11 @@
import { servePageMarkdown } from '@/routes/markdownPage';
import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils';
import type { NextRequest } from 'next/server';
export const dynamic = 'force-static';
export async function GET(_request: NextRequest, { params }: { params: Promise<RouteParams> }) {
const { context } = await getStaticSiteContext(await params);
const pathname = getPagePathFromParams(await params);
return servePageMarkdown(context, pathname);
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
import type { PublishedSiteContent, SiteAPIToken } from '@gitbook/api';
import { fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
import type { SiteAPIToken } from '@gitbook/api';
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context';
import { jwtDecode } from 'jwt-decode';
import { forbidden } from 'next/navigation';
import rison from 'rison';
@@ -98,7 +98,7 @@ function getModeFromParams(mode: string): RouteParamMode {
/**
* Get the decoded site data from the params.
*/
function getSiteURLDataFromParams(params: RouteLayoutParams): PublishedSiteContent {
function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
const decoded = decodeURIComponent(params.siteData);
return rison.decode(decoded);
}
+51 -8
View File
@@ -13,12 +13,42 @@ import type {
SiteStructure,
Space,
} from '@gitbook/api';
import { type GitBookDataFetcher, createDataFetcher, throwIfDataError } from '@v2/lib/data';
import {
type GitBookDataFetcher,
createDataFetcher,
getDataOrNull,
throwIfDataError,
} from '@v2/lib/data';
import { notFound } from 'next/navigation';
import { assert } from 'ts-essentials';
import { GITBOOK_URL } from './env';
import { type ImageResizer, createImageResizer } from './images';
import { type GitBookLinker, createLinker } from './links';
/**
* Data about the site URL. Provided by the middleware.
* These data are stable between pages in the same site space.
*/
export type SiteURLData = Pick<
PublishedSiteContent,
| 'organization'
| 'apiToken'
| 'site'
| 'siteSpace'
| 'space'
| 'revision'
| 'changeRequest'
| 'shareKey'
| 'siteSection'
| 'siteBasePath'
| 'basePath'
> & {
/**
* Identifier used for image resizing.
*/
imagesContextId: string;
};
/**
* Generic context when rendering content.
*/
@@ -105,7 +135,7 @@ export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & {
*/
export function getBaseContext(input: {
siteURL: URL | string;
siteURLData: PublishedSiteContent;
siteURLData: SiteURLData;
urlMode: 'url' | 'url-host';
}) {
const { urlMode, siteURLData } = input;
@@ -139,7 +169,7 @@ export function getBaseContext(input: {
}
const imageResizer = createImageResizer({
host: siteURL.host,
imagesContextId: siteURLData.imagesContextId,
// To ensure image resizing work for proxied sites,
// we serve images from the root of the site.
linker: linker,
@@ -157,7 +187,7 @@ export function getBaseContext(input: {
*/
export async function fetchSiteContextByURLLookup(
baseContext: GitBookBaseContext,
data: PublishedSiteContent
data: SiteURLData
): Promise<GitBookSiteContext> {
return await fetchSiteContextByIds(baseContext, {
organization: data.organization,
@@ -277,7 +307,7 @@ export async function fetchSpaceContextByIds(
})
),
ids.changeRequest
? throwIfDataError(
? getDataOrNull(
dataFetcher.getChangeRequest({
spaceId: ids.space,
changeRequestId: ids.changeRequest,
@@ -286,17 +316,30 @@ export async function fetchSpaceContextByIds(
: null,
]);
const revisionId = changeRequest?.revision ?? ids.revision ?? space.revision;
if (ids.changeRequest && !changeRequest) {
// When trying to render a change request with an invalid / non-existing ID,
// we should return a 404.
notFound();
}
const pages = await throwIfDataError(
const revisionId = ids.revision ?? changeRequest?.revision ?? space.revision;
const pages = await getDataOrNull(
dataFetcher.getRevisionPages({
spaceId: ids.space,
revisionId,
// We only care about the Git metadata when the Git sync is enabled,
// otherwise we can optimize performance by not fetching it
metadata: !!space.gitSync,
})
}),
// When trying to render a revision with an invalid / non-existing ID,
// we should handle gracefully the 404 and throw notFound.
ids.revision ? [404] : undefined
);
if (!pages) {
notFound();
}
return {
...baseContext,
+315 -201
View File
@@ -8,7 +8,8 @@ import {
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
import { wrapDataFetcherError } from './errors';
import { DataFetcherError, wrapDataFetcherError } from './errors';
import { memoize } from './memoize';
import type { GitBookDataFetcher } from './types';
interface DataFetcherInput {
@@ -94,6 +95,15 @@ export function createDataFetcher(
})
);
},
getRevisionPageMarkdown(params) {
return trace('getRevisionPageMarkdown', () =>
getRevisionPageMarkdown(input, {
spaceId: params.spaceId,
revisionId: params.revisionId,
pageId: params.pageId,
})
);
},
getReusableContent(params) {
return trace('getReusableContent', () =>
getReusableContent(input, {
@@ -165,29 +175,35 @@ export function createDataFetcher(
})
);
},
//
// API that are not tied to the token
// where the data is the same for all users
//
getUserById(userId) {
return trace('getUserById', () => getUserById({ apiToken: null }, { userId }));
return trace('getUserById', () => getUserById(input, { userId }));
},
streamAIResponse(params) {
return streamAIResponse(input, params);
},
};
}
async function getUserById(input: DataFetcherInput, params: { userId: string }) {
const getUserById = memoize(async function getUserById(
input: DataFetcherInput,
params: { userId: string }
) {
'use cache';
cacheLife('days');
return trace('getUserById.uncached', () => {
cacheLife('days');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.users.getUserById(params.userId);
return res.data;
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.users.getUserById(params.userId);
return res.data;
});
});
}
});
async function getSpace(
const getSpace = memoize(async function getSpace(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -196,24 +212,26 @@ async function getSpace(
) {
'use cache';
cacheLife('days');
cacheTag(
getCacheTag({
tag: 'space',
space: params.spaceId,
})
);
return trace('getSpace.uncached', () => {
cacheLife('days');
cacheTag(
getCacheTag({
tag: 'space',
space: params.spaceId,
})
);
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getSpaceById(params.spaceId, {
shareKey: params.shareKey,
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getSpaceById(params.spaceId, {
shareKey: params.shareKey,
});
return res.data;
});
return res.data;
});
}
});
async function getChangeRequest(
const getChangeRequest = memoize(async function getChangeRequest(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -222,23 +240,28 @@ async function getChangeRequest(
) {
'use cache';
cacheLife('minutes');
return trace('getChangeRequest.uncached', () => {
cacheLife('minutes');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getChangeRequestById(params.spaceId, params.changeRequestId);
cacheTag(
getCacheTag({
tag: 'change-request',
space: params.spaceId,
changeRequest: res.data.id,
})
);
return res.data;
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getChangeRequestById(
params.spaceId,
params.changeRequestId
);
cacheTag(
getCacheTag({
tag: 'change-request',
space: params.spaceId,
changeRequest: res.data.id,
})
);
return res.data;
});
});
}
});
async function getRevision(
const getRevision = memoize(async function getRevision(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -248,18 +271,20 @@ async function getRevision(
) {
'use cache';
cacheLife('max');
return trace('getRevision.uncached', () => {
cacheLife('max');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, {
metadata: params.metadata,
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, {
metadata: params.metadata,
});
return res.data;
});
return res.data;
});
}
});
async function getRevisionPages(
const getRevisionPages = memoize(async function getRevisionPages(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -269,18 +294,24 @@ async function getRevisionPages(
) {
'use cache';
cacheLife('max');
return trace('getRevisionPages.uncached', () => {
cacheLife('max');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.listPagesInRevisionById(params.spaceId, params.revisionId, {
metadata: params.metadata,
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.listPagesInRevisionById(
params.spaceId,
params.revisionId,
{
metadata: params.metadata,
}
);
return res.data.pages;
});
return res.data.pages;
});
}
});
async function getRevisionFile(
const getRevisionFile = memoize(async function getRevisionFile(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -290,21 +321,56 @@ async function getRevisionFile(
) {
'use cache';
cacheLife('max');
return trace('getRevisionFile.uncached', () => {
cacheLife('max');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getFileInRevisionById(
params.spaceId,
params.revisionId,
params.fileId,
{}
);
return res.data;
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getFileInRevisionById(
params.spaceId,
params.revisionId,
params.fileId,
{}
);
return res.data;
});
});
}
});
async function getRevisionPageByPath(
const getRevisionPageMarkdown = memoize(async function getRevisionPageMarkdown(
input: DataFetcherInput,
params: {
spaceId: string;
revisionId: string;
pageId: string;
}
) {
'use cache';
return trace('getRevisionPageMarkdown.uncached', () => {
cacheLife('max');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getPageInRevisionById(
params.spaceId,
params.revisionId,
params.pageId,
{
format: 'markdown',
}
);
if (!('markdown' in res.data)) {
throw new DataFetcherError('Page is not a document', 404);
}
return res.data.markdown;
});
});
});
const getRevisionPageByPath = memoize(async function getRevisionPageByPath(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -314,23 +380,25 @@ async function getRevisionPageByPath(
) {
'use cache';
cacheLife('max');
return trace('getRevisionPageByPath.uncached', () => {
cacheLife('max');
const encodedPath = encodeURIComponent(params.path);
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getPageInRevisionByPath(
params.spaceId,
params.revisionId,
encodedPath,
{}
);
const encodedPath = encodeURIComponent(params.path);
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getPageInRevisionByPath(
params.spaceId,
params.revisionId,
encodedPath,
{}
);
return res.data;
return res.data;
});
});
}
});
async function getDocument(
const getDocument = memoize(async function getDocument(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -339,16 +407,18 @@ async function getDocument(
) {
'use cache';
cacheLife('max');
return trace('getDocument.uncached', () => {
cacheLife('max');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {});
return res.data;
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {});
return res.data;
});
});
}
});
async function getComputedDocument(
const getComputedDocument = memoize(async function getComputedDocument(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -359,29 +429,31 @@ async function getComputedDocument(
) {
'use cache';
cacheLife('days');
return trace('getComputedDocument.uncached', () => {
cacheLife('days');
cacheTag(
...getComputedContentSourceCacheTags(
{
spaceId: params.spaceId,
organizationId: params.organizationId,
},
params.source
)
);
cacheTag(
...getComputedContentSourceCacheTags(
{
spaceId: params.spaceId,
organizationId: params.organizationId,
},
params.source
)
);
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getComputedDocument(params.spaceId, {
source: params.source,
seed: params.seed,
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getComputedDocument(params.spaceId, {
source: params.source,
seed: params.seed,
});
return res.data;
});
return res.data;
});
}
});
async function getReusableContent(
const getReusableContent = memoize(async function getReusableContent(
input: DataFetcherInput,
params: {
spaceId: string;
@@ -391,48 +463,54 @@ async function getReusableContent(
) {
'use cache';
cacheLife('max');
return trace('getReusableContent.uncached', () => {
cacheLife('max');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getReusableContentInRevisionById(
params.spaceId,
params.revisionId,
params.reusableContentId
);
return res.data;
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getReusableContentInRevisionById(
params.spaceId,
params.revisionId,
params.reusableContentId
);
return res.data;
});
});
}
});
async function getLatestOpenAPISpecVersionContent(
input: DataFetcherInput,
params: {
organizationId: string;
slug: string;
const getLatestOpenAPISpecVersionContent = memoize(
async function getLatestOpenAPISpecVersionContent(
input: DataFetcherInput,
params: {
organizationId: string;
slug: string;
}
) {
'use cache';
return trace('getLatestOpenAPISpecVersionContent.uncached', () => {
cacheTag(
getCacheTag({
tag: 'openapi',
organization: params.organizationId,
openAPISpec: params.slug,
})
);
cacheLife('days');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.orgs.getLatestOpenApiSpecVersionContent(
params.organizationId,
params.slug
);
return res.data;
});
});
}
) {
'use cache';
);
cacheTag(
getCacheTag({
tag: 'openapi',
organization: params.organizationId,
openAPISpec: params.slug,
})
);
cacheLife('days');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.orgs.getLatestOpenApiSpecVersionContent(
params.organizationId,
params.slug
);
return res.data;
});
}
async function getPublishedContentSite(
const getPublishedContentSite = memoize(async function getPublishedContentSite(
input: DataFetcherInput,
params: {
organizationId: string;
@@ -442,24 +520,32 @@ async function getPublishedContentSite(
) {
'use cache';
cacheLife('days');
cacheTag(
getCacheTag({
tag: 'site',
site: params.siteId,
})
);
return trace('getPublishedContentSite.uncached', () => {
cacheLife('days');
cacheTag(
getCacheTag({
tag: 'site',
site: params.siteId,
})
);
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.orgs.getPublishedContentSite(params.organizationId, params.siteId, {
shareKey: params.siteShareKey,
return trace('getPublishedContentSite', () => {
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.orgs.getPublishedContentSite(
params.organizationId,
params.siteId,
{
shareKey: params.siteShareKey,
}
);
return res.data;
});
});
return res.data;
});
}
});
async function getSiteRedirectBySource(
const getSiteRedirectBySource = memoize(async function getSiteRedirectBySource(
input: DataFetcherInput,
params: {
organizationId: string;
@@ -470,26 +556,32 @@ async function getSiteRedirectBySource(
) {
'use cache';
cacheTag(
getCacheTag({
tag: 'site',
site: params.siteId,
})
);
cacheLife('days');
return trace('getSiteRedirectBySource.uncached', () => {
cacheTag(
getCacheTag({
tag: 'site',
site: params.siteId,
})
);
cacheLife('days');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.orgs.getSiteRedirectBySource(params.organizationId, params.siteId, {
shareKey: params.siteShareKey,
source: params.source,
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.orgs.getSiteRedirectBySource(
params.organizationId,
params.siteId,
{
shareKey: params.siteShareKey,
source: params.source,
}
);
return res.data;
});
return res.data;
});
}
});
async function getEmbedByUrl(
const getEmbedByUrl = memoize(async function getEmbedByUrl(
input: DataFetcherInput,
params: {
url: string;
@@ -498,36 +590,40 @@ async function getEmbedByUrl(
) {
'use cache';
cacheLife('weeks');
return trace('getEmbedByUrl.uncached', () => {
cacheLife('weeks');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
return res.data;
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
return res.data;
});
});
}
});
async function searchSiteContent(
const searchSiteContent = memoize(async function searchSiteContent(
input: DataFetcherInput,
params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
) {
'use cache';
const { organizationId, siteId, query, scope } = params;
return trace('searchSiteContent.uncached', () => {
const { organizationId, siteId, query, scope } = params;
cacheLife('days');
cacheLife('days');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.orgs.searchSiteContent(organizationId, siteId, {
query,
...scope,
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.orgs.searchSiteContent(organizationId, siteId, {
query,
...scope,
});
return res.data.items;
});
return res.data.items;
});
}
});
async function renderIntegrationUi(
const renderIntegrationUi = memoize(async function renderIntegrationUi(
input: DataFetcherInput,
params: {
integrationName: string;
@@ -536,17 +632,35 @@ async function renderIntegrationUi(
) {
'use cache';
cacheTag(getCacheTag({ tag: 'integration', integration: params.integrationName }));
cacheLife('days');
return trace('renderIntegrationUi.uncached', () => {
cacheTag(getCacheTag({ tag: 'integration', integration: params.integrationName }));
cacheLife('days');
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.integrations.renderIntegrationUiWithPost(
params.integrationName,
params.request
);
return res.data;
return wrapDataFetcherError(async () => {
const api = await apiClient(input);
const res = await api.integrations.renderIntegrationUiWithPost(
params.integrationName,
params.request
);
return res.data;
});
});
});
async function* streamAIResponse(
input: DataFetcherInput,
params: Parameters<GitBookDataFetcher['streamAIResponse']>[0]
) {
const api = await apiClient(input);
const res = await api.orgs.streamAiResponseInSite(params.organizationId, params.siteId, {
input: params.input,
output: params.output,
model: params.model,
});
for await (const event of res) {
yield event;
}
}
let loggedServiceBinding = false;
+9 -1
View File
@@ -1,5 +1,5 @@
import { race, tryCatch } from '@/lib/async';
import { joinPath } from '@/lib/paths';
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
import { trace } from '@/lib/tracing';
import type { PublishedSiteContentLookup } from '@gitbook/api';
import { apiClient } from './api';
@@ -36,6 +36,13 @@ export async function getPublishedContentByURL(input: {
url: alternative.url,
visitorAuthToken: input.visitorAuthToken ?? undefined,
redirectOnError: input.redirectOnError,
// As this endpoint is cached by our API, we version the request
// to void getting stale data with missing properties.
// this could be improved by ensuring our API cache layer is versioned
// or invalidated when needed
// @ts-expect-error - cacheVersion is not a real query param
cacheVersion: 'v2',
},
{
signal,
@@ -100,6 +107,7 @@ export async function getPublishedContentByURL(input: {
const siteResult: PublishedSiteContentLookup = {
...data,
canonicalUrl: joinPathWithBaseURL(data.canonicalUrl, alternative.extraPath),
basePath: joinPath(data.basePath, lookup.basePath ?? ''),
pathname: joinPath(data.pathname, alternative.extraPath),
...(changeRequest ? { changeRequest } : {}),
@@ -0,0 +1,39 @@
import { describe, expect, it, mock } from 'bun:test';
import { memoize } from './memoize';
describe('memoize', () => {
it('should memoize the function', async () => {
const fn = mock(async () => Math.random());
const memoized = memoize(fn);
expect(await memoized()).toBe(await memoized());
});
it('should memoize the function with different arguments', async () => {
const fn = mock(async (a: number, b: number) => a + b);
const memoized = memoize(fn);
expect(await memoized(1, 2)).toBe(await memoized(1, 2));
expect(fn.mock.calls.length).toBe(1);
expect(await memoized(1, 2)).not.toBe(await memoized(2, 3));
expect(fn.mock.calls.length).toBe(2);
});
it('should memoize a function complex object', async () => {
const fn = mock(async (a: { foo: string; bar: number }) => a.foo + a.bar);
const memoized = memoize(fn);
expect(await memoized({ foo: 'foo', bar: 1 })).toBe(await memoized({ foo: 'foo', bar: 1 }));
expect(fn.mock.calls.length).toBe(1);
expect(await memoized({ foo: 'foo', bar: 1 })).not.toBe(
await memoized({ foo: 'foo', bar: 2 })
);
expect(fn.mock.calls.length).toBe(2);
});
it('should wrap concurrent async calls', async () => {
const fn = mock(async () => Math.random());
const memoized = memoize(fn);
const promise1 = memoized();
const promise2 = memoized();
expect(await promise1).toBe(await promise2);
expect(fn.mock.calls.length).toBe(1);
});
});
@@ -0,0 +1,43 @@
import pMemoize from 'p-memoize';
/**
* We wrap 'use cache' calls in a p-memoize function to avoid
* executing the function multiple times when doing concurrent calls.
*
* Hopefully one day this can be done directly by 'use cache'.
*/
export function memoize<F extends (...args: any[]) => any>(f: F): F {
return pMemoize(f, {
cacheKey: (args) => {
return JSON.stringify(deepSortValue(args));
},
});
}
function deepSortValue(value: unknown): unknown {
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
value === null ||
value === undefined
) {
return value;
}
if (Array.isArray(value)) {
return value.map(deepSortValue);
}
if (value && typeof value === 'object') {
return Object.entries(value)
.map(([key, subValue]) => {
return [key, deepSortValue(subValue)] as const;
})
.sort((a, b) => {
return a[0].localeCompare(b[0]);
});
}
return value;
}
+20
View File
@@ -97,6 +97,15 @@ export interface GitBookDataFetcher {
path: string;
}): Promise<DataFetcherResponse<api.RevisionPageDocument | api.RevisionPageGroup>>;
/**
* Get the markdown content of a page by its path.
*/
getRevisionPageMarkdown(params: {
spaceId: string;
revisionId: string;
pageId: string;
}): Promise<DataFetcherResponse<string>>;
/**
* Get a document by its space ID and document ID.
*/
@@ -170,4 +179,15 @@ export interface GitBookDataFetcher {
integrationName: string;
request: api.RenderIntegrationUI;
}): Promise<DataFetcherResponse<api.ContentKitRenderOutput>>;
/**
* Stream an AI response.
*/
streamAIResponse(params: {
organizationId: string;
siteId: string;
input: api.AIMessageInput[];
output: api.AIOutputFormat;
model: api.AIModel;
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
}
@@ -33,13 +33,13 @@ export interface CloudflareImageOptions {
* Create an image resizer for a rendering context.
*/
export function createImageResizer({
host,
imagesContextId,
linker,
}: {
/** The linker to use to create URLs. */
linker: GitBookLinker;
/** The host name of the current site. */
host: string;
/** The site identifier to use for verifying the image signature. */
imagesContextId: string;
}): ImageResizer {
if (!GITBOOK_IMAGE_RESIZE_URL || !GITBOOK_IMAGE_RESIZE_SIGNING_KEY) {
return createNoopImageResizer();
@@ -58,7 +58,7 @@ export function createImageResizer({
return async (options) => {
cachedSignature ??= await generateImageSignature({
host,
imagesContextId,
url: urlInput,
});
@@ -0,0 +1,14 @@
/**
* Get the site identifier to use for image resizing for an incoming request.
* This identifier can be obtained before resolving the request URL.
*/
export function getImageResizingContextId(url: URL): string {
if (url.host === 'proxy.gitbook.site' || url.host === 'proxy.gitbook-staging.site') {
// For proxy requests, we extract the site ID from the pathname
// e.g. https://proxy.gitbook.site/site/siteId/...
const pathname = url.pathname.slice(1).split('/');
return pathname.slice(0, 2).join('/');
}
return url.host;
}
@@ -2,3 +2,4 @@ export * from './types';
export * from './createImageResizer';
export * from './signatures';
export * from './utils';
export * from './getImageResizingContextId';
@@ -18,7 +18,7 @@ export const CURRENT_SIGNATURE_VERSION: SignatureVersion = '2';
type SignFnInput = {
url: string;
host: string;
imagesContextId: string;
};
type SignFn = (input: SignFnInput) => MaybePromise<string>;
@@ -60,7 +60,7 @@ const generateSignatureV2: SignFn = async (input) => {
assert(GITBOOK_IMAGE_RESIZE_SIGNING_KEY, 'GITBOOK_IMAGE_RESIZE_SIGNING_KEY is not set');
const all = [
input.url,
input.host, // The hostname is used to avoid serving images from other sites on the same domain
input.imagesContextId, // The hostname is used to avoid serving images from other sites on the same domain
GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
]
.filter(Boolean)
+12 -1
View File
@@ -72,6 +72,17 @@ export function createLinker(
const siteBasePath = withTrailingSlash(withLeadingSlash(servedOn.siteBasePath));
const spaceBasePath = withTrailingSlash(withLeadingSlash(servedOn.spaceBasePath));
const protocol = (() => {
if (servedOn.protocol) {
return servedOn.protocol;
}
if (servedOn.host) {
return servedOn.host.startsWith('localhost') ? 'http:' : 'https:';
}
return 'https:';
})();
const linker: GitBookLinker = {
toPathInSpace(relativePath: string): string {
@@ -97,7 +108,7 @@ export function createLinker(
return absolutePath;
}
return `${servedOn.protocol ?? 'https:'}//${joinPaths(servedOn.host, absolutePath)}`;
return `${protocol}//${joinPaths(servedOn.host, absolutePath)}`;
},
toPathForPage({ pages, page, anchor }) {
+3 -2
View File
@@ -1,5 +1,6 @@
import { CustomizationThemeMode, type PublishedSiteContent } from '@gitbook/api';
import { CustomizationThemeMode } from '@gitbook/api';
import { headers } from 'next/headers';
import type { SiteURLData } from './context';
export enum MiddlewareHeaders {
/**
@@ -57,7 +58,7 @@ export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> {
* Get the site URL data from the middleware headers.
* This function should only be called in a server action or a dynamic route.
*/
export async function getSiteURLDataFromMiddleware(): Promise<PublishedSiteContent> {
export async function getSiteURLDataFromMiddleware(): Promise<SiteURLData> {
const headersList = await headers();
const siteURLData = headersList.get(MiddlewareHeaders.SiteURLData);
+63 -14
View File
@@ -21,7 +21,9 @@ import {
throwIfDataError,
} from '@v2/lib/data';
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
import { getImageResizingContextId } from '@v2/lib/images';
import { MiddlewareHeaders } from '@v2/lib/middleware';
import type { SiteURLData } from './lib/context';
export const config = {
matcher: [
@@ -63,7 +65,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
return null;
}
const { url: siteURL, mode } = match;
const { url: siteRequestURL, mode } = match;
const imagesContextId = getImageResizingContextId(siteRequestURL);
/**
* Serve image resizing requests (all requests containing `/~gitbook/image`).
@@ -73,9 +76,9 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
* In GitBook v1: image resizing was done at the root of the hostname (docs.company.com/~gitbook/image)
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
*/
if (siteURL.pathname.endsWith('/~gitbook/image')) {
if (siteRequestURL.pathname.endsWith('/~gitbook/image')) {
return await serveResizedImage(request, {
host: siteURL.host,
imagesContextId: imagesContextId,
});
}
@@ -85,13 +88,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
// @ts-ignore - request typing
const visitorToken = getVisitorToken({
cookies: request.cookies.getAll(),
url: siteURL,
url: siteRequestURL,
});
const withAPIToken = async (apiToken: string | null) => {
const siteURLData = await throwIfDataError(
getPublishedContentByURL({
url: siteURL.toString(),
url: siteRequestURL.toString(),
visitorAuthToken: visitorToken?.token ?? null,
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
@@ -136,11 +139,15 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
cookies.push(...getResponseCookiesForVisitorAuth(siteURLData.siteBasePath, visitorToken));
// We use the host/origin from the canonical URL to ensure the links are
// correctly generated when the site is proxied. e.g. https://proxy.gitbook.com/site/siteId/...
const siteCanonicalURL = new URL(siteURLData.canonicalUrl);
//
// Make sure the URL is clean of any va token after a successful lookup
// The token is stored in a cookie that is set on the redirect response
//
const incomingURL = mode === 'url' ? requestURL : siteURL;
const incomingURL = mode === 'url' ? requestURL : siteCanonicalURL;
const requestURLWithoutToken = normalizeVisitorAuthURL(incomingURL);
if (
requestURLWithoutToken !== incomingURL &&
@@ -163,16 +170,19 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
const requestHeaders = new Headers(request.headers);
requestHeaders.set(MiddlewareHeaders.RouteType, routeType);
requestHeaders.set(MiddlewareHeaders.URLMode, mode);
requestHeaders.set(MiddlewareHeaders.SiteURL, `${siteURL.origin}${siteURLData.basePath}`);
requestHeaders.set(
MiddlewareHeaders.SiteURL,
`${siteCanonicalURL.origin}${siteURLData.basePath}`
);
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(siteURLData));
// Preview of customization/theme
const customization = siteURL.searchParams.get('customization');
const customization = siteRequestURL.searchParams.get('customization');
if (customization && validateSerializedCustomization(customization)) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Customization, customization);
}
const theme = siteURL.searchParams.get('theme');
const theme = siteRequestURL.searchParams.get('theme');
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Theme, theme);
@@ -188,18 +198,44 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
requestHeaders.set('x-forwarded-host', request.nextUrl.host);
requestHeaders.set('origin', request.nextUrl.origin);
const siteURLWithoutProtocol = `${siteURL.host}${siteURLData.basePath}`;
const siteURLWithoutProtocol = `${siteCanonicalURL.host}${siteURLData.basePath}`;
const { pathname, routeType: routeTypeFromPathname } = encodePathInSiteContent(
siteURLData.pathname
);
routeType = routeTypeFromPathname ?? routeType;
// We pick only stable data from the siteURL data to prevent re-rendering of
// the root layout when changing pages..
const stableSiteURLData: SiteURLData = {
site: siteURLData.site,
siteSection: siteURLData.siteSection,
siteSpace: siteURLData.siteSpace,
siteBasePath: siteURLData.siteBasePath,
basePath: siteURLData.basePath,
space: siteURLData.space,
organization: siteURLData.organization,
changeRequest: siteURLData.changeRequest,
revision: siteURLData.revision,
shareKey: siteURLData.shareKey,
apiToken: siteURLData.apiToken,
imagesContextId: imagesContextId,
};
const route = [
'sites',
routeType,
mode,
encodeURIComponent(siteURLWithoutProtocol),
encodeURIComponent(rison.encode(siteURLData)),
encodeURIComponent(
rison.encode(
// rison can't encode undefined values
Object.fromEntries(
Object.entries(stableSiteURLData).filter(
([_, v]) => typeof v !== 'undefined'
)
)
)
),
pathname,
].join('/');
@@ -226,10 +262,10 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
};
// For https://preview/<siteURL> requests,
if (siteURL.hostname === 'preview') {
if (siteRequestURL.hostname === 'preview') {
return serveWithQueryAPIToken(
// We scope the API token to the site ID.
`${siteURL.hostname}/${requestURL.pathname.slice(1).split('/')[0]}`,
`${siteRequestURL.hostname}/${requestURL.pathname.slice(1).split('/')[0]}`,
request,
withAPIToken
);
@@ -382,13 +418,26 @@ function encodePathInSiteContent(rawPathname: string): {
return { pathname };
}
// If the pathname is a markdown file, we rewrite it to ~gitbook/markdown/:pathname
if (pathname.match(/\.md$/)) {
const pagePathWithoutMD = pathname.slice(0, -3);
return {
pathname: `~gitbook/markdown/${encodeURIComponent(pagePathWithoutMD)}`,
// The markdown content is always static and doesn't depend on the dynamic parameter (customization, theme, etc)
routeType: 'static',
};
}
switch (pathname) {
case '~gitbook/icon':
return { pathname };
case 'llms.txt':
case 'sitemap.xml':
case 'sitemap-pages.xml':
case 'robots.txt':
return { pathname };
// LLMs.txt, sitemap, sitemap-pages and robots.txt are always static
// as they only depend on the site structure / pages.
return { pathname, routeType: 'static' };
case '~gitbook/pdf':
// PDF routes are always dynamic as they depend on the search params.
return { pathname, routeType: 'dynamic' };
+61
View File
@@ -1,5 +1,66 @@
# gitbook
## 0.9.2
### Patch Changes
- da7b369: Fix missing headers in OpenAPIResponses
- 139a805: Fix OpenAPI enum display
- Updated dependencies [da7b369]
- Updated dependencies [da485f5]
- Updated dependencies [139a805]
- @gitbook/react-openapi@1.1.9
## 0.9.1
### Patch Changes
- fb90eb0: Limit tinted background on bold theme to sites with filled sidebar
- 7d0b422: Handle grouped OpenAPISchemas
- Updated dependencies [7d0b422]
- Updated dependencies [fb90eb0]
- @gitbook/react-openapi@1.1.8
- @gitbook/colors@0.3.1
## 0.9.0
### Minor Changes
- 77fd393: Track event when visitor is opening a search result.
- d70d566: Support site announcement banner
- 77fd393: Track event when clicking announcement banner link.
### Patch Changes
- e84a46a: Fix OpenAPI tabs indicator overflow
- bc90adb: Fix favicon not being displayed in Google because `robots.txt` was preventing the indexation of the image route
- 434af90: Fix image resizing when using the proxy feature in a site.
- c756761: Add breadcrumbs to search results
- 40e8e69: Disallow crawling by web-robots of search/ask URLs
- 77fd393: Fix clicking search results when the site is embedded in an iframe.
- 1505ddb: Fix multiple request examples selector not showing
- 61db166: Add OpenAPI write-only indicator
- 6f71da8: Fix padding in schemas
- fa91eb7: Fix PDF generation when user has dark mode configured.
- 5b1e01c: Support for x-stability property
- 57ca4e0: Fix a crash when a page contains a block of an integration that is no longer installed
- d236bf0: Fix flash when loading sites with dark mode as default theme
- cd99ed5: Fix spec properties rendering and missing keys
- 813b2af: Support for x-enumDescriptions and x-gitbook-enum
- e9fa50d: Trim the search query to avoid showing a loading state when typing
- Updated dependencies [bd35348]
- Updated dependencies [ae78fc5]
- Updated dependencies [7bb37c7]
- Updated dependencies [373183a]
- Updated dependencies [1505ddb]
- Updated dependencies [61db166]
- Updated dependencies [5b1e01c]
- Updated dependencies [cd99ed5]
- Updated dependencies [813b2af]
- Updated dependencies [a25fded]
- @gitbook/react-openapi@1.1.7
- @gitbook/openapi-parser@2.1.2
## 0.8.2
### Patch Changes
+29
View File
@@ -28,6 +28,7 @@ import {
headerLinks,
runTestCases,
waitForCookiesDialog,
waitForNotFound,
} from './util';
const testCases: TestsCase[] = [
@@ -308,6 +309,18 @@ const testCases: TestsCase[] = [
url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings',
run: waitForCookiesDialog,
},
{
name: 'Invalid revision',
url: '~/revisions/idnotfound/blocks/headings',
run: waitForNotFound,
screenshot: false,
},
{
name: 'Invalid change request',
url: '~/changes/idnotfound/blocks/headings',
run: waitForNotFound,
screenshot: false,
},
],
},
{
@@ -390,6 +403,22 @@ const testCases: TestsCase[] = [
},
],
},
{
name: 'Markdown page',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
tests: [
{
name: 'Text page',
url: 'text-page.md',
screenshot: false,
run: async (_page, response) => {
expect(response?.status()).toBe(200);
expect(response?.headers()['content-type']).toContain('text/markdown');
},
},
],
},
{
name: 'Content tests',
contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/',
+12 -4
View File
@@ -16,7 +16,7 @@ import {
type CustomizationThemedColor,
type SiteCustomizationSettings,
} from '@gitbook/api';
import { type BrowserContext, type Page, expect, test } from '@playwright/test';
import { type BrowserContext, type Page, type Response, expect, test } from '@playwright/test';
import deepMerge from 'deepmerge';
import rison from 'rison';
import type { DeepPartial } from 'ts-essentials';
@@ -33,7 +33,7 @@ export interface Test {
/**
* Test to run
*/
run?: (page: Page) => Promise<unknown>;
run?: (page: Page, response: Response | null) => Promise<unknown>;
/**
* Whether the test should be fullscreened during testing.
*/
@@ -138,6 +138,11 @@ export async function waitForCookiesDialog(page: Page) {
await expect(dialog).toBeVisible();
}
export async function waitForNotFound(_page: Page, response: Response | null) {
expect(response).not.toBeNull();
expect(response?.status()).toBe(404);
}
/**
* Transform test cases into Playwright tests and run it.
*/
@@ -183,9 +188,9 @@ export function runTestCases(testCases: TestsCase[]) {
}
});
await page.goto(url);
const response = await page.goto(url);
if (testEntry.run) {
await testEntry.run(page);
await testEntry.run(page, response);
}
const screenshotOptions = testEntry.screenshot;
if (screenshotOptions !== false) {
@@ -271,6 +276,9 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
internationalization: {
locale: CustomizationLocale.En,
},
insights: {
trackingCookie: true,
},
favicon: {},
header: {
preset: CustomizationHeaderPreset.Default,
+12 -7
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.8.2",
"version": "0.9.2",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -29,12 +29,12 @@
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.1.8",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
"@upstash/redis": "^1.27.1",
"ai": "^4.1.46",
"ajv": "^8.12.0",
"@vercel/og": "0.6.8.",
"ai": "^4.2.2",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
@@ -45,8 +45,8 @@
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.15",
"next": "14.2.25",
"memoizee": "^0.4.17",
"next": "14.2.26",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
@@ -68,7 +68,12 @@
"tailwind-shades": "^1.1.2",
"unified": "^11.0.5",
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0"
"usehooks-ts": "^3.1.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5",
"event-iterator": "^2.0.0",
"partial-json": "^0.1.7",
"zustand": "^5.0.3"
},
"devDependencies": {
"@argos-ci/playwright": "^4.3.0",
@@ -14,7 +14,15 @@ interface JsonBody {
* The body should be a JSON with { tags: string[] }
*/
export async function POST(req: NextRequest) {
const json = (await req.json()) as JsonBody;
let json: JsonBody;
try {
json = await req.json();
} catch (err) {
return NextResponse.json({
error: `invalid json body: ${err}`,
});
}
if (!json.tags || !Array.isArray(json.tags)) {
return NextResponse.json(
@@ -25,10 +33,18 @@ export async function POST(req: NextRequest) {
);
}
const result = await revalidateTags(json.tags);
return NextResponse.json({
success: true,
stats: result.stats,
});
try {
const result = await revalidateTags(json.tags);
return NextResponse.json({
success: true,
stats: result.stats,
});
} catch (err: unknown) {
return NextResponse.json(
{
error: `${err}`,
},
{ status: 500 }
);
}
}
@@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks';
export function PageClientLayout(props: { withSections?: boolean }) {
// We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to a hash or to the top of the page
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined });
useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined });
useStripFallbackQueryParam();
return null;
@@ -1,7 +1,7 @@
'use client';
import { Button } from '@/components/primitives/Button';
import { t, useLanguage } from '@/intl/client';
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
export default function ErrorPage(props: {
@@ -35,9 +35,8 @@ export default function ErrorPage(props: {
}}
variant="secondary"
size="small"
>
{t(language, 'unexpected_error_retry')}
</Button>
label={tString(language, 'unexpected_error_retry')}
/>
</div>
</div>
</div>
@@ -0,0 +1,104 @@
'use client';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import { Icon } from '@gitbook/icons';
import { useEffect, useState } from 'react';
import { useVisitedPages } from '../Insights';
import { usePageContext } from '../PageContext';
import { Loading } from '../primitives';
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
/**
* Summarise a page's content for use in a link preview
*/
export function AIPageLinkSummary(props: {
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
showTrademark: boolean;
}) {
const { targetSpaceId, targetPageId, linkPreview, linkTitle, showTrademark = true } = props;
const currentPage = usePageContext();
const language = useLanguage();
const visitedPages = useVisitedPages((state) => state.pages);
const [highlight, setHighlight] = useState('');
useEffect(() => {
let canceled = false;
setHighlight('');
(async () => {
const stream = await streamLinkPageSummary({
currentSpaceId: currentPage.spaceId,
currentPageId: currentPage.pageId,
currentPageTitle: currentPage.title,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
});
for await (const highlight of stream) {
if (canceled) return;
setHighlight(highlight ?? '');
}
})();
return () => {
canceled = true;
};
}, [
currentPage.pageId,
currentPage.spaceId,
currentPage.title,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
]);
const shimmerBlocks = [
'w-[20%] [animation-delay:-1s]',
'w-[35%] [animation-delay:-0.8s]',
'w-[25%] [animation-delay:-0.6s]',
'w-[10%] [animation-delay:-0.4s]',
'w-[40%] [animation-delay:-0.2s]',
'w-[30%] [animation-delay:0s]',
];
return (
<div className="flex flex-col gap-1">
<div className="flex w-screen items-center gap-1 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{showTrademark ? (
<Loading className="size-4" busy={!highlight || highlight.length === 0} />
) : (
<Icon icon="sparkle" className="size-3" />
)}
<h6 className="text-tint">{t(language, 'link_tooltip_ai_summary')}</h6>
</div>
{highlight.length > 0 ? (
<p className="animate-fadeIn">{highlight}</p>
) : (
<div className="mt-2 flex flex-wrap gap-2">
{shimmerBlocks.map((block, index) => (
<div
key={`${index}-${block}`}
className={`${block} h-4 animate-pulse rounded straight-corners:rounded-none bg-tint-active`}
/>
))}
</div>
)}
{highlight.length > 0 ? (
<div className="animate-fadeIn text-tint-subtle text-xs">
{t(language, 'link_tooltip_ai_summary_description')}
</div>
) : null}
</div>
);
}
@@ -0,0 +1 @@
export * from './AIPageLinkSummary';
@@ -0,0 +1,124 @@
'use server';
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
import type { GitBookBaseContext } from '@v2/lib/context';
import { EventIterator } from 'event-iterator';
import type { MaybePromise } from 'p-map';
import * as partialJson from 'partial-json';
import type { DeepPartial } from 'ts-essentials';
import type { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
/**
* Get the latest value from a stream and the response id.
*/
export async function generate<T>(
promise: MaybePromise<{
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
}>
) {
const input = await promise;
let value: T | undefined;
for await (const event of input.stream) {
value = event;
}
const { responseId } = await input.response;
return {
responseId,
value,
};
}
/**
* Stream the generation of an object using the AI.
*/
export async function streamGenerateObject<T>(
context: GitBookBaseContext,
{
organizationId,
siteId,
}: {
organizationId: string;
siteId: string;
},
{
schema,
messages,
model = AIModel.Fast,
}: {
schema: z.ZodSchema<T>;
messages: AIMessageInput[];
model?: AIModel;
previousResponseId?: string;
}
) {
const rawStream = context.dataFetcher.streamAIResponse({
organizationId,
siteId,
input: messages,
output: {
type: 'object',
schema: zodToJsonSchema(schema),
},
model,
});
let json = '';
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
if (event.type === 'response_object') {
json += event.jsonChunk;
const parsed = partialJson.parse(json, partialJson.ALL);
return parsed;
}
});
}
/**
* Parse a stream from the API to extract the responseId.
*/
function parseResponse<T>(
responseStream: EventIterator<AIStreamResponse>,
parse: (response: AIStreamResponse) => T | undefined
): {
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
} {
let resolveResponse: (value: { responseId: string }) => void;
const response = new Promise<{ responseId: string }>((resolve) => {
resolveResponse = resolve;
});
const stream = new EventIterator<T>((queue) => {
(async () => {
let foundResponse = false;
for await (const event of responseStream) {
if (event.type === 'response_finish') {
foundResponse = true;
resolveResponse({ responseId: event.responseId });
} else {
const parsed = parse(event);
if (parsed !== undefined) {
queue.push(parsed);
}
}
}
if (!foundResponse) {
throw new Error('No response found');
}
})().then(
() => {
queue.stop();
},
(error) => {
queue.fail(error);
}
);
});
return { stream, response };
}
@@ -0,0 +1 @@
export * from './streamLinkPageSummary';
@@ -0,0 +1,166 @@
'use server';
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import { isV2 } from '@/lib/v2';
import { AIMessageRole } from '@gitbook/api';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { getServerActionBaseContext } from '@v2/lib/server-actions';
import { z } from 'zod';
import { streamGenerateObject } from './api';
/**
* Get a summary of a page, in the context of another page
*/
export async function* streamLinkPageSummary({
currentSpaceId,
currentPageId,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
}: {
currentSpaceId: string;
currentPageId: string;
currentPageTitle: string;
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
visitedPages?: Array<{ spaceId: string; pageId: string }>;
}) {
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const { stream } = await streamGenerateObject(
baseContext,
{
organizationId: siteURLData.organization,
siteId: siteURLData.site,
},
{
schema: z.object({
highlight: z
.string()
.describe('The reason why the user should read the target page.'),
// questions: z.array(z.string().describe('The questions to sea')).max(3),
}),
messages: [
{
role: AIMessageRole.Developer,
content: `# 1. Role
You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
# 2. Task
Extract a contextually-relevant fact that:
- Directly answers the specific need or question implied by the link's placement
- States a capability, limitation, or specification from the target page
- Connects precisely to the user's current paragraph or sentence
- Completes the user's understanding based on what they're currently reading
# 3. Instructions
1. First, identify the exact need, question, or gap in the current paragraph where the link appears
2. Find the specific fact in the target page that addresses this exact contextual need
3. Ensure the fact relates directly to the context of the paragraph containing the link
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
},
{
role: AIMessageRole.Developer,
content: `# 4. Current page
The content of the current page is:`,
attachments: [
{
type: 'page' as const,
spaceId: currentSpaceId,
pageId: currentPageId,
},
],
},
...(visitedPages
? [
{
role: AIMessageRole.Developer,
content: '# 5. Previous pages',
},
...visitedPages.map(({ spaceId, pageId }) => ({
role: AIMessageRole.Developer,
content: `## Page ${pageId}`,
attachments: [
{
type: 'page' as const,
spaceId,
pageId,
},
],
})),
]
: []),
{
role: AIMessageRole.Developer,
content: `# 6. Target page
The content of the target page is:`,
attachments: [
{
type: 'page' as const,
spaceId: targetSpaceId,
pageId: targetPageId,
},
],
},
{
role: AIMessageRole.Developer,
content: `# 7. Link preview
The content of the link preview is:
> ${linkPreview}
> Page ID: ${targetPageId}`,
},
{
role: AIMessageRole.Developer,
content: `# 8. Guidelines & Examples
ALWAYS:
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
- ALWAYS connect target page information specifically to the current paragraph context
- ALWAYS focus on the gap in knowledge that the link is meant to fill
- ALWAYS consider user's navigation history to ensure contextual continuity
- ALWAYS use action verbs like "click", "select", "use", "create", "enable"
NEVER:
- NEVER include ANY unspecifc language like "learn", "how to", "discover", etc. State the fact directly.
- NEVER select general facts unrelated to the specific link context
- NEVER ignore the specific context where the link appears
- NEVER repeat the same fact in different words
## Examples
Current paragraph: "When organizing content, headings are limited to 3 levels. For more advanced editing, you can use (multiple select)[/multiple-select] to move multiple blocks at once."
Preview: "Multiple Select: Select multiple content blocks at once."
"Shift selects content between two points, useful for reorganizing your current heading structure."
"Shift and Ctrl/Cmd keys are the modifiers for selecting multiple blocks."
Current paragraph: "Most changes can be published directly, but for major revisions, if you want others to review changes before publishing, create a (change request)[/change-requests]."
Preview: "Change Requests: Collaborative content editing workflow."
"Each reviewer's approval is tracked separately, with specific change highlighting for your major revisions."
"Each reviewer receives an email notification and can approve or request changes."
Current paragraph: "Your team mentioned issues with conflicting edits. Need to collaborate in real-time? You can use (live edit mode)[/live-edit]."
Preview: "Live Edit: Real-time collaborative editing."
"Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
"Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
},
{
role: AIMessageRole.User,
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
},
].filter(filterOutNullable),
}
);
for await (const value of stream) {
const highlight = value.highlight;
if (!highlight) {
continue;
}
yield highlight;
}
}
@@ -0,0 +1,32 @@
import { resolveContentRef } from '@/lib/references';
import type { GitBookSiteContext } from '@v2/lib/context';
import { AnnouncementBanner } from './AnnouncementBanner';
/**
* Server-side component to resolve content refs and pass down to client-side component
*/
export async function Announcement(props: {
context: GitBookSiteContext;
}) {
const { context } = props;
const { customization } = context;
if (
!customization.announcement ||
!customization.announcement.enabled ||
!customization.announcement.message
) {
return null;
}
const resolvedContentRef = customization.announcement?.link
? await resolveContentRef(customization.announcement?.link?.to, context)
: null;
return (
<AnnouncementBanner
announcement={customization.announcement}
contentRef={resolvedContentRef}
/>
);
}
@@ -0,0 +1,140 @@
'use client';
import * as storage from '@/lib/local-storage';
import type { ResolvedContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { type CustomizationAnnouncement, SiteInsightsLinkPosition } from '@gitbook/api';
import { Icon, type IconName } from '@gitbook/icons';
import { CONTAINER_STYLE } from '../layout';
import { Link, linkStyles } from '../primitives';
import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
/**
* Client-side component to enable closing the banner
*/
export function AnnouncementBanner(props: {
announcement: CustomizationAnnouncement;
contentRef: ResolvedContentRef | null;
}) {
const { announcement, contentRef } = props;
const hasLink = announcement.link && contentRef?.href;
const closeable = announcement.style !== 'danger';
const Tag = hasLink ? Link : 'div';
const style = BANNER_STYLES[announcement.style];
return (
<div className="announcement-banner theme-bold:bg-header-background pt-4 pb-2">
<div className="scroll-nojump">
<div className={tcls('relative', CONTAINER_STYLE)}>
<Tag
href={contentRef?.href ?? ''}
className={tcls(
'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
style.container,
closeable && 'pr-12',
hasLink && style.hover
)}
insights={
announcement.link
? {
type: 'link_click',
link: {
target: announcement.link.to,
position: SiteInsightsLinkPosition.Announcement,
},
}
: undefined
}
>
<Icon
icon={style.icon as IconName}
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
/>
<div>
{announcement.message}
{hasLink ? (
<div className={tcls(linkStyles, style.link, 'ml-1 inline')}>
{contentRef?.icon ? (
<span className="mr-1 ml-2 *:inline">
{contentRef?.icon}
</span>
) : null}
{announcement.link?.title && (
<span className="mr-1">{announcement.link?.title}</span>
)}
<Icon
icon={
announcement.link?.to.kind === 'url'
? 'arrow-up-right'
: 'chevron-right'
}
className={tcls('mb-0.5 inline size-3')}
/>
</div>
) : null}
</div>
</Tag>
{closeable ? (
<button
className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
type="button"
onClick={dismissAnnouncement}
>
<Icon icon="close" className="size-4" />
</button>
) : null}
</div>
</div>
</div>
);
}
/**
* Dismiss the announcement banner and store the dismissal state in local storage.
* @see AnnouncementScript
*/
function dismissAnnouncement() {
storage.setItem(ANNOUNCEMENT_STORAGE_KEY, {
visible: false,
at: Date.now(),
});
document.documentElement.classList.add(ANNOUNCEMENT_CSS_CLASS);
}
const BANNER_STYLES = {
info: {
container: 'bg-info ring-info-subtle',
hover: 'hover:bg-info-hover active:bg-info-active',
icon: 'circle-info',
iconColor: 'text-info-subtle',
close: 'hover:bg-tint-base hover:ring-info-subtle',
link: '',
},
warning: {
container: 'bg-warning decoration-warning/6 ring-warning-subtle',
hover: 'hover:bg-warning-hover',
icon: 'circle-exclamation',
iconColor: 'text-warning-subtle',
close: 'hover:bg-tint-base hover:ring-warning-subtle',
link: 'links-default:text-warning links-default:hover:text-warning-strong links-default:decoration-warning/6 links-accent:decoration-warning',
},
danger: {
container: 'bg-danger decoration-danger/6 ring-danger-subtle',
hover: 'hover:bg-danger-hover',
icon: 'triangle-exclamation',
iconColor: 'text-danger-subtle',
close: 'hover:bg-tint-base hover:ring-danger-subtle',
link: 'links-default:text-danger links-default:hover:text-danger-strong links-default:decoration-danger/6 links-accent:decoration-danger',
},
success: {
container: 'bg-success decoration-success/6 ring-success-subtle',
hover: 'hover:bg-success-hover',
icon: 'circle-check',
iconColor: 'text-success-subtle',
close: 'hover:bg-tint-base hover:ring-success-subtle',
link: 'links-default:text-success links-default:hover:text-success-strong links-default:decoration-success/6 links-accent:decoration-success',
},
};
@@ -0,0 +1,39 @@
'use client';
import * as React from 'react';
import {
ANNOUNCEMENT_CSS_CLASS,
ANNOUNCEMENT_DAYS_TILL_RESET,
ANNOUNCEMENT_STORAGE_KEY,
} from './constants';
import { checkStorageForDismissedScript } from './script';
/**
* Inject a script to read the local storage state for the announcement banner and apply the appropriate CSS class to the <html> element as early as possible.
* Bypasses react state to prevent flickering.
*/
export function AnnouncementDismissedScript() {
const scriptArgs = JSON.stringify([
ANNOUNCEMENT_STORAGE_KEY,
ANNOUNCEMENT_DAYS_TILL_RESET,
ANNOUNCEMENT_CSS_CLASS,
]).slice(1, -1);
// makes sure the banner dismissed state is kept when navigating between sites
React.useEffect(() => {
checkStorageForDismissedScript(
ANNOUNCEMENT_STORAGE_KEY,
ANNOUNCEMENT_DAYS_TILL_RESET,
ANNOUNCEMENT_CSS_CLASS
);
}, []);
return (
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `(${checkStorageForDismissedScript.toString()})(${scriptArgs})`,
}}
/>
);
}
@@ -0,0 +1,12 @@
/**
* The local storage key for the announcement banner.
*/
export const ANNOUNCEMENT_STORAGE_KEY = '@gitbook/announcement';
/**
* The CSS class to hide the announcement banner. Applies to the <html> element.
*/
export const ANNOUNCEMENT_CSS_CLASS = 'announcement-hidden';
/**
* The number of days until the announcement banner resets.
*/
export const ANNOUNCEMENT_DAYS_TILL_RESET = 7;
@@ -0,0 +1,2 @@
export * from './Announcement';
export * from './AnnouncementDismissedScript';
@@ -0,0 +1,34 @@
/**
* Read the local storage state for the announcement banner and apply the appropriate CSS class to the <html> element.
*
* NOTE: this script is stringified and run in the browser, so it must be self-contained and have syntax supported in all browsers.
*/
export function checkStorageForDismissedScript(
storageKey: string,
daysTillReset: number,
cssClass: string
) {
let showBanner = true;
try {
const announcementStateStr = window.localStorage.getItem(storageKey);
const announcementState = announcementStateStr
? JSON.parse(announcementStateStr)
: undefined;
if (announcementState && !announcementState.visible) {
const dismissedAt = announcementState.at;
const nowTime = new Date().getTime();
// Check if enough days have passed since dismissal
const daysSinceDismissal = Math.floor((nowTime - dismissedAt) / (1000 * 60 * 60 * 24));
if (daysSinceDismissal < daysTillReset) {
showBanner = false;
}
}
} catch {}
if (!showBanner) {
document.documentElement.classList.add(cssClass);
}
}
@@ -97,9 +97,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
onClick={() => {
onUpdateState(true);
}}
>
{t(language, 'cookies_accept')}
</Button>
label={tString(language, 'cookies_accept')}
/>
<Button
variant="secondary"
size="small"
@@ -107,9 +106,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
onClick={() => {
onUpdateState(false);
}}
>
{t(language, 'cookies_reject')}
</Button>
label={tString(language, 'cookies_reject')}
/>
</div>
</div>
);
@@ -54,7 +54,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
const { nodes, blockStyle, isOffscreen: defaultIsOffscreen = false, ...contextProps } = props;
let isOffscreen = defaultIsOffscreen;
return nodes.map((node) => {
return nodes.map((node, index) => {
isOffscreen =
isOffscreen ||
isBlockOffscreen({
@@ -65,7 +65,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
return (
<Block
key={node.key}
key={node.key || `${node.type}-${index}`}
block={node}
style={[
'mx-auto w-full decoration-primary/6',
@@ -20,7 +20,15 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
return (
<Tag
id={id}
className={tcls(textStyle.textSize, 'heading', 'group', 'relative', 'grid', style)}
className={tcls(
textStyle.textSize,
'heading',
'group',
'relative',
'grid',
'scroll-m-12',
style
)}
>
<div
className={tcls(
@@ -104,7 +104,7 @@ const HINT_STYLES: {
'[&_.can-override-text]:text-neutral-strong',
],
container:
'bg-info border-info theme-muted-tint:bg-info-solid/2 theme-bold-tint:bg-info-solid/2',
'bg-info border-info theme-muted-tint:bg-info-solid/2 [html.sidebar-filled.theme-bold.tint_&]:bg-info-solid/2',
containerWithHeader: 'border-info-solid bg-info-subtle',
},
warning: {
@@ -1,19 +1,22 @@
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)
? await resolveContentRef(inline.data.ref, context.contentContext, {
resolveAnchorText: true,
})
: null;
if (!resolved) {
if (!context.contentContext || !resolved) {
return (
<span title="Broken link" className="underline">
<Inlines
@@ -25,24 +28,33 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
</span>
);
}
const isExternal = inline.data.ref.kind === 'url';
return (
<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]}
/>
</StyledLink>
<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,159 @@
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';
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';
export async function InlineLinkTooltip(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 hasAISummary =
!isExternal &&
!isSamePage &&
'customization' in context &&
context.customization.ai?.pageLinkSummaries.enabled &&
(inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor');
return (
<Tooltip.Provider delayDuration={200}>
<Tooltip.Root>
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="z-40 w-screen max-w-md animate-present px-4 sm:w-auto">
<div className="overflow-hidden rounded-md straight-corners:rounded-none shadow-lg shadow-tint-12/4 ring-1 ring-tint-subtle dark:shadow-tint-1 ">
<div className="bg-tint-base p-4">
<div className="flex items-start gap-4">
<div className="flex flex-col">
{breadcrumbs && breadcrumbs.length > 0 ? (
<div className="mb-1 flex grow flex-wrap items-center gap-x-2 gap-y-0.5 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{breadcrumbs.map((crumb, index) => {
const Tag = crumb.href ? StyledLink : 'div';
return (
<Fragment key={crumb.label}>
{index !== 0 ? (
<Icon
icon="chevron-right"
className="size-3 text-tint-subtle"
/>
) : null}
<Tag
className={tcls(
'flex gap-1',
crumb.href &&
'links-default:text-tint no-underline hover:underline contrast-more:underline contrast-more:decoration-current'
)}
href={crumb.href ?? '#'}
>
{crumb.icon ? (
<span className="mt-0.5 text-tint-subtle empty:hidden">
{crumb.icon}
</span>
) : null}
{crumb.label}
</Tag>
</Fragment>
);
})}
</div>
) : null}
<div
className={tcls(
'flex gap-2 leading-snug',
isExternal && 'text-sm [overflow-wrap:anywhere]'
)}
>
{resolved.icon ? (
<div className="mt-1 text-tint-subtle empty:hidden">
{resolved.icon}
</div>
) : null}
<h5 className="font-semibold">{resolved.text}</h5>
</div>
</div>
{!isSamePage && resolved.href ? (
<Button
className={tcls(
'-mx-2 -my-2 ml-auto',
breadcrumbs?.length === 0
? 'place-self-center'
: null
)}
variant="blank"
href={resolved.href}
target="_blank"
label={tString(language, 'open_in_new_tab')}
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>
) : null}
</div>
{hasAISummary && 'page' in context && 'page' in inline.data.ref ? (
<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
}
/>
</div>
) : null}
</div>
<Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
@@ -24,14 +24,16 @@ export function Inlines<T extends DocumentInline | DocumentText>(
) {
const { nodes, document, ancestorInlines, ...contextProps } = props;
return nodes.map((node) => {
return nodes.map((node, index) => {
const key = node.key || `key-${index}`;
if (node.object === 'text') {
return <Text key={node.key} text={node} />;
return <Text key={key} text={node} />;
}
return (
<Inline
key={node.key}
key={key}
inline={node}
document={document}
ancestorInlines={ancestorInlines}
@@ -34,7 +34,11 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
dataFetcher.renderIntegrationUi({
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') {
return null;
@@ -1,18 +1,14 @@
import type { JSONDocument } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { OpenAPIOperation as BaseOpenAPIOperation } from '@gitbook/react-openapi';
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
import { tcls } from '@/lib/tailwind';
import type { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { DocumentView } from '../DocumentView';
import { Heading } from '../Heading';
import './scalar.css';
import './style.css';
import type { AnyOpenAPIOperationsBlock } from '@/lib/openapi/types';
import { getOpenAPIContext } from './context';
/**
* Render an openapi block or an openapi-operation block.
@@ -55,51 +51,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return (
<BaseOpenAPIOperation
data={data}
context={{
specUrl,
icons: {
chevronDown: <Icon icon="chevron-down" />,
chevronRight: <Icon icon="chevron-right" />,
plus: <Icon icon="plus" />,
},
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
renderDocument: (documentProps) => (
<DocumentView
document={documentProps.document as JSONDocument}
context={props.context}
style="space-y-6"
blockStyle="max-w-full"
/>
),
renderHeading: (headingProps) => (
<Heading
document={props.document}
ancestorBlocks={props.ancestorBlocks}
isEstimatedOffscreen={props.isEstimatedOffscreen}
context={props.context}
style={headingProps.deprecated ? 'line-through' : undefined}
block={{
object: 'block',
key: `${block.key}-heading`,
meta: block.meta,
data: {},
type: 'heading-2',
nodes: [
{
key: `${block.key}-heading-text`,
object: 'text',
leaves: [
{ text: headingProps.title, object: 'leaf', marks: [] },
],
},
],
}}
/>
),
defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
}}
context={getOpenAPIContext({ props, specUrl })}
className="openapi-block"
/>
);
@@ -1,6 +1,5 @@
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { OpenAPISchemas as BaseOpenAPISchemas } from '@gitbook/react-openapi';
import type { BlockProps } from '../Block';
@@ -8,6 +7,7 @@ import type { BlockProps } from '../Block';
import './scalar.css';
import './style.css';
import type { OpenAPISchemasBlock } from '@/lib/openapi/types';
import { getOpenAPIContext } from './context';
/**
* Render an openapi-schemas block.
@@ -49,18 +49,9 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return (
<BaseOpenAPISchemas
data={data}
context={{
specUrl,
icons: {
chevronDown: <Icon icon="chevron-down" />,
chevronRight: <Icon icon="chevron-right" />,
plus: <Icon icon="plus" />,
},
defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
}}
schemas={data.schemas}
grouped={block.data.grouped}
context={getOpenAPIContext({ props, specUrl })}
className="openapi-block"
/>
);
@@ -0,0 +1,73 @@
import type { JSONDocument } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import type { OpenAPIContext } from '@gitbook/react-openapi';
import { tcls } from '@/lib/tailwind';
import type { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { DocumentView } from '../DocumentView';
import { Heading } from '../Heading';
import './scalar.css';
import './style.css';
import type { AnyOpenAPIOperationsBlock, OpenAPISchemasBlock } from '@/lib/openapi/types';
/**
* Get the OpenAPI context to render a block.
*/
export function getOpenAPIContext(args: {
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock>;
specUrl: string;
}): OpenAPIContext {
const { props, specUrl } = args;
const { block } = props;
return {
specUrl,
icons: {
chevronDown: <Icon icon="chevron-down" />,
chevronRight: <Icon icon="chevron-right" />,
plus: <Icon icon="plus" />,
},
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
renderDocument: (documentProps) => (
<DocumentView
document={documentProps.document as JSONDocument}
context={props.context}
style="space-y-6"
blockStyle="max-w-full"
/>
),
renderHeading: (headingProps) => (
<Heading
document={props.document}
ancestorBlocks={props.ancestorBlocks}
isEstimatedOffscreen={props.isEstimatedOffscreen}
context={props.context}
style={tcls([
headingProps.deprecated ? 'line-through' : undefined,
headingProps.deprecated || !!headingProps.stability
? '[&>div]:mt-0'
: undefined,
])}
block={{
object: 'block',
key: `${block.key}-heading`,
meta: block.meta,
data: {},
type: 'heading-2',
nodes: [
{
key: `${block.key}-heading-text`,
object: 'text',
leaves: [{ text: headingProps.title, object: 'leaf', marks: [] }],
},
],
}}
/>
),
defaultInteractiveOpened: props.context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
};
}
@@ -1,5 +1,6 @@
/* Layout Components */
.openapi-operation {
.openapi-operation,
.openapi-schemas {
@apply flex-1 flex flex-col gap-8 mb-14 min-w-0;
}
@@ -17,13 +18,30 @@
}
.openapi-summary {
@apply flex flex-col items-start justify-start gap-3;
@apply flex flex-col items-start justify-start gap-3 scroll-m-12;
}
.openapi-deprecated {
.openapi-summary-tags {
@apply flex flex-row gap-2 mt-[0.75em];
}
.openapi-deprecated,
.openapi-stability {
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
}
.openapi-stability-alpha {
@apply text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/6 ring-amber-500/5;
}
.openapi-stability-beta {
@apply text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/6 ring-blue-500/5;
}
.openapi-stability-experimental {
@apply text-violet-700 dark:text-violet-300 bg-violet-50 dark:bg-violet-900/6 ring-violet-500/5;
}
.openapi-deprecated-sunset-date {
@apply font-semibold font-mono truncate;
}
@@ -131,7 +149,11 @@
/* Schema Structure */
.openapi-schema-root {
@apply mt-2;
/* unstyled */
}
.openapi-schema-root-description.openapi-markdown {
@apply prose-sm text-balance mt-1.5 !text-[0.813rem] text-tint overflow-hidden !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
}
.openapi-schema-properties {
@@ -182,7 +204,7 @@
.openapi-schema-name {
/* To make double click on the property name select only the name,
we disable selection on the parent and re-enable it on the children. */
@apply select-none flex gap-x-2.5 items-baseline text-sm flex-wrap;
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5;
}
.openapi-schema-name .openapi-deprecated {
@@ -209,6 +231,10 @@
@apply text-primary-subtle/9 text-[0.813rem];
}
.openapi-schema-writeonly {
@apply text-success dark:text-success-subtle/9 text-[0.813rem];
}
.openapi-schema-type {
@apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem];
}
@@ -240,11 +266,11 @@
/* Schema Enum */
.openapi-schema-enum {
@apply flex flex-row text-sm leading-relaxed flex-wrap text-tint;
@apply text-sm leading-relaxed max-w-full text-tint;
}
.openapi-schema-enum-value {
@apply text-sm;
@apply text-sm mr-1.5;
}
.openapi-schema-enum-value:first-child {
@@ -257,7 +283,7 @@
/* Schema Description */
.openapi-schema-description.openapi-markdown {
@apply prose-sm text-tint overflow-hidden !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
@apply prose-sm text-tint overflow-hidden text-pretty !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
}
.openapi-schema-description.openapi-markdown pre:has(code) {
@@ -277,13 +303,15 @@
/* Schema Examples */
.openapi-schema-example,
.openapi-schema-pattern {
.openapi-schema-pattern,
.openapi-schema-default {
@apply prose-sm text-tint;
}
.openapi-schema-example code,
.openapi-schema-pattern code,
.openapi-schema-enum-value code {
.openapi-schema-enum-value code,
.openapi-schema-default code {
@apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
}
@@ -297,7 +325,7 @@
}
.openapi-securities-description.openapi-markdown {
@apply prose-sm text-tint !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
@apply prose-sm text-tint !font-normal select-text text-pretty prose-strong:font-semibold prose-strong:text-inherit;
}
.openapi-securities-label {
@@ -323,7 +351,7 @@
}
.openapi-requestbody-description.openapi-markdown {
@apply prose-sm text-tint !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
@apply prose-sm text-tint !font-normal text-pretty select-text prose-strong:font-semibold prose-strong:text-inherit;
}
/* Responses */
@@ -340,7 +368,7 @@
}
.openapi-response-description.openapi-markdown {
@apply text-left prose-sm text-[0.813rem] h-auto relative leading-[1.125rem] text-tint !font-normal truncate select-text prose-strong:font-semibold prose-strong:text-inherit;
@apply text-left prose-sm text-[0.813rem] text-pretty h-auto relative leading-[1.125rem] text-tint !font-normal truncate select-text prose-strong:font-semibold prose-strong:text-inherit;
}
.openapi-response-description.openapi-markdown::-webkit-scrollbar {
@@ -456,24 +484,38 @@
@apply flex flex-row items-center py-2 px-3 justify-end border-t border-tint-subtle;
}
/* Response Example */
.openapi-response-example {
/* Panel */
.openapi-panel {
@apply border rounded bg-tint border-tint-subtle;
}
.openapi-response-example-empty {
.openapi-panel-heading {
@apply font-medium px-4 py-2 text-xs uppercase;
}
.openapi-panel-body {
@apply relative;
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
}
.openapi-panel-footer {
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
}
.openapi-panel-footer .openapi-markdown {
@apply text-[0.813rem] text-tint;
}
/* Example */
.openapi-example-empty {
@apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center;
}
/* Common Elements */
.openapi-select {
@apply max-w-60 rounded font-mono text-xs leading-6 px-1 py-0.5 border border-tint-subtle bg-tint;
@apply max-w-60 rounded font-mono text-xs leading-6 px-1 py-0.5 truncate border border-tint-subtle bg-tint;
}
.openapi-select {
min-width: fit-content;
max-width: fit-content;
}
.openapi-select:focus {
width: auto;
}
@@ -521,7 +563,7 @@
/* Tabs */
.openapi-tabs-list {
@apply flex flex-row gap-1.5 py-1.5 px-2.5 border-b border-tint-subtle w-full overflow-x-scroll;
@apply flex flex-row gap-1.5 py-1.5 px-2.5 w-full overflow-x-scroll;
scrollbar-width: none;
-ms-overflow-style: none;
}
@@ -531,19 +573,11 @@
}
.openapi-tabs-tab[aria-selected="true"] {
@apply text-primary after:absolute after:-bottom-2 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
@apply text-primary after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
}
.openapi-tabs-panel {
@apply flex-1 text-sm focus-visible:outline-none;
}
.openapi-tabs-footer {
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
}
.openapi-tabs-footer .openapi-markdown {
@apply text-[0.813rem] text-tint;
@apply flex-1 text-sm relative focus-visible:outline-none;
}
/* Disclosure group */
@@ -654,7 +688,8 @@
@apply border border-tint-subtle rounded-lg;
}
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema,
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root {
@apply p-2.5;
}
@@ -691,3 +726,7 @@
opacity: 0;
}
}
.openapi-copy-button {
@apply hover:brightness-95;
}
@@ -168,7 +168,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
key={index}
href={ref.href}
target="_blank"
style={['flex', 'flex-row', 'items-center', 'gap-2']}
className="flex flex-row items-center gap-2"
insights={
ref.file
? {
@@ -12,7 +12,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
const tabs: TabsItem[] = [];
const tabsBody: React.ReactNode[] = [];
block.nodes.forEach((tab) => {
block.nodes.forEach((tab, index) => {
tabs.push({
id: tab.meta?.id ?? tab.key!,
title: tab.data.title ?? '',
@@ -20,6 +20,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
tabsBody.push(
<Blocks
key={tab.key ?? index}
nodes={tab.nodes}
document={document}
ancestorBlocks={[...ancestorBlocks, block, tab]}
@@ -38,7 +38,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
'bg-tint-base/9',
'theme-muted:bg-tint-subtle/9',
'theme-bold-tint:bg-tint-subtle/9',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle/9',
'theme-gradient:bg-gradient-primary',
'theme-gradient-tint:bg-gradient-tint',
'contrast-more:bg-tint-base',
@@ -142,10 +142,9 @@ function HeaderItemButton(
position: SiteInsightsLinkPosition.Header,
},
}}
label={title}
{...rest}
>
{title}
</Button>
/>
);
}
@@ -63,7 +63,7 @@ export function SpacesDropdown(props: {
className
)}
>
<span className={tcls('truncate', 'grow')}>{siteSpace.title}</span>
<span className={tcls('line-clamp-1', 'grow')}>{siteSpace.title}</span>
<DropdownChevron />
</div>
)}
@@ -8,6 +8,7 @@ import { useDebounceCallback, useEventCallback } from 'usehooks-ts';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import { getAllBrowserCookiesMap } from '@/lib/browser-cookies';
import { getSession } from './sessions';
import { useVisitedPages } from './useVisitedPages';
import { getVisitorId } from './visitorId';
export type InsightsEventName = api.SiteInsightsEvent['type'];
@@ -63,9 +64,19 @@ type TrackEventCallback = <EventName extends InsightsEventName>(
const InsightsContext = React.createContext<TrackEventCallback>(() => {});
interface InsightsProviderProps extends InsightsEventContext {
/** If true, the events will be sent to the server. */
enabled: boolean;
/** If true, the visitor cookie tracking will be used */
visitorCookieTrackingEnabled: boolean;
/** The URL of the app. */
appURL: string;
/** The host of the API. */
apiHost: string;
/** The children of the provider. */
children: React.ReactNode;
}
@@ -73,8 +84,9 @@ interface InsightsProviderProps extends InsightsEventContext {
* Wrap the content of the app with the InsightsProvider to track events.
*/
export function InsightsProvider(props: InsightsProviderProps) {
const { enabled, appURL, apiHost, children, ...context } = props;
const { enabled, appURL, apiHost, children, visitorCookieTrackingEnabled, ...context } = props;
const addVisitedPage = useVisitedPages((state) => state.addPage);
const visitorIdRef = React.useRef<string | null>(null);
const eventsRef = React.useRef<{
[pathname: string]:
@@ -124,6 +136,14 @@ export function InsightsProvider(props: InsightsProviderProps) {
...eventsForPathname,
events: [],
};
// Mark the page as visited in our local state
if (eventsForPathname.pageContext.pageId) {
addVisitedPage({
spaceId: context.spaceId,
pageId: eventsForPathname.pageContext.pageId,
});
}
}
if (allEvents.length > 0) {
@@ -140,7 +160,8 @@ export function InsightsProvider(props: InsightsProviderProps) {
});
const flushBatchedEvents = useDebounceCallback(async () => {
const visitorId = visitorIdRef.current ?? (await getVisitorId(appURL));
const visitorId =
visitorIdRef.current ?? (await getVisitorId(appURL, visitorCookieTrackingEnabled));
visitorIdRef.current = visitorId;
flushEventsSync();
@@ -184,7 +205,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
* Get the visitor ID and store it in a ref.
*/
React.useEffect(() => {
getVisitorId(appURL).then((visitorId) => {
getVisitorId(appURL, visitorCookieTrackingEnabled).then((visitorId) => {
visitorIdRef.current = visitorId;
// When the page is unloaded, flush all events, but only if the visitor ID is set
window.addEventListener('beforeunload', flushEventsSync);
@@ -192,7 +213,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
return () => {
window.removeEventListener('beforeunload', flushEventsSync);
};
}, [flushEventsSync, appURL]);
}, [flushEventsSync, appURL, visitorCookieTrackingEnabled]);
return (
<InsightsContext.Provider value={trackEvent}>
@@ -2,3 +2,4 @@ export * from './InsightsProvider';
export * from './visitorId';
export * from './cookies';
export * from './TrackPageViewEvent';
export * from './useVisitedPages';
@@ -0,0 +1,25 @@
import { create } from 'zustand';
type VisitedPage = {
spaceId: string;
pageId: string;
};
/**
* A store for the pages that have been visited in the current session.
*/
export const useVisitedPages = create<{
pages: VisitedPage[];
addPage: (page: VisitedPage) => void;
}>((set) => ({
pages: [],
addPage: (page) =>
set((state) => {
const lastPage = state.pages[state.pages.length - 1];
if (lastPage && lastPage.spaceId === page.spaceId && lastPage.pageId === page.pageId) {
return { pages: state.pages };
}
return { pages: [...state.pages, page] };
}),
}));
@@ -13,10 +13,13 @@ let pendingVisitorId: Promise<string> | null = null;
/**
* Return the current visitor identifier.
*/
export async function getVisitorId(appURL: string): Promise<string> {
export async function getVisitorId(
appURL: string,
visitorCookieTrackingEnabled: boolean
): Promise<string> {
if (!visitorId) {
if (!pendingVisitorId) {
pendingVisitorId = fetchVisitorID(appURL).finally(() => {
pendingVisitorId = fetchVisitorID(appURL, visitorCookieTrackingEnabled).finally(() => {
pendingVisitorId = null;
});
}
@@ -30,10 +33,13 @@ export async function getVisitorId(appURL: string): Promise<string> {
/**
* Propose a visitor identifier to the GitBook.com server and get the devideId back.
*/
async function fetchVisitorID(appURL: string): Promise<string> {
async function fetchVisitorID(
appURL: string,
visitorCookieTrackingEnabled: boolean
): Promise<string> {
const withoutCookies = isCookiesTrackingDisabled();
if (withoutCookies) {
if (withoutCookies || !visitorCookieTrackingEnabled) {
return generateRandomId();
}
@@ -1,5 +1,4 @@
import {
type CustomizationSettings,
type Revision,
type RevisionPageDocument,
type RevisionPageGroup,
@@ -24,7 +23,7 @@ import { tString } from '@/intl/translate';
import { getPagePDFContainerId } from '@/lib/links';
import { resolvePageId } from '@/lib/pages';
import { tcls } from '@/lib/tailwind';
import { defaultCustomizationForSpace } from '@/lib/utils';
import { defaultCustomization } from '@/lib/utils';
import { type PDFSearchParams, getPDFSearchParams } from './urls';
import { PageControlButtons } from './PageControlButtons';
@@ -57,7 +56,7 @@ export async function PDFPage(props: {
const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
const customization =
'customization' in baseContext ? baseContext.customization : defaultCustomizationForSpace();
'customization' in baseContext ? baseContext.customization : defaultCustomization();
const language = getSpaceLanguage(customization);
// Compute the pages to render
@@ -180,7 +179,7 @@ export async function PDFPage(props: {
async function PDFSpaceIntro(props: {
space: Space;
customization: CustomizationSettings | SiteCustomizationSettings;
customization: SiteCustomizationSettings;
}) {
const { space, customization } = props;
@@ -1,5 +1,6 @@
import { CustomizationRootLayout } from '@/components/RootLayout';
import { defaultCustomizationForSpace } from '@/lib/utils';
import { defaultCustomization } from '@/lib/utils';
import { CustomizationThemeMode } from '@gitbook/api';
import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context';
/**
@@ -14,8 +15,9 @@ export async function PDFRootLayout(props: {
return (
<CustomizationRootLayout
customization={
'customization' in context ? context.customization : defaultCustomizationForSpace()
'customization' in context ? context.customization : defaultCustomization()
}
forcedTheme={CustomizationThemeMode.Light}
>
{children}
</CustomizationRootLayout>
@@ -5,7 +5,7 @@ import React from 'react';
import { useScrollActiveId } from '@/components/hooks';
import { Button } from '@/components/primitives';
import { t, useLanguage } from '@/intl/client';
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { type PDFSearchParams, getPDFURLSearchParams } from './urls';
@@ -58,9 +58,8 @@ export function PageControlButtons(props: {
only: true,
}).toString()}`}
variant="secondary"
>
{t(language, 'pdf_mode_only_page')}
</Button>
label={tString(language, 'pdf_mode_only_page')}
/>
)}
<Button
href={`?${getPDFURLSearchParams({
@@ -69,9 +68,8 @@ export function PageControlButtons(props: {
only: false,
}).toString()}`}
variant="secondary"
>
{t(language, 'pdf_mode_all')}
</Button>
label={tString(language, 'pdf_mode_all')}
/>
{trademark ? <div className={tcls('mt-5')}>{trademark}</div> : null}
</div>
@@ -228,7 +228,7 @@ export function PageAside(props: {
) : null}
<div
className={tcls(
'sticky bottom-0 z-10 mt-auto flex flex-col bg-tint-base theme-bold-tint:bg-tint-subtle theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle pb-4 page-api-block:xl:max-2xl:hidden page-api-block:xl:max-2xl:pb-0 page-api-block:xl:max-2xl:group-hover/aside:flex',
'sticky bottom-0 z-10 mt-auto flex flex-col bg-tint-base theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle pb-4 page-api-block:xl:max-2xl:hidden page-api-block:xl:max-2xl:pb-0 page-api-block:xl:max-2xl:group-hover/aside:flex [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'page-api-block:xl:max-2xl:bg-transparent'
)}
>
@@ -111,7 +111,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
'hover:bg-primary-hover',
'theme-muted:hover:bg-primary-active',
'theme-bold-tint:hover:bg-primary-active',
'[html.sidebar-filled.theme-bold.tint_&]:hover:bg-primary-active',
'theme-gradient:hover:bg-primary-active',
'tint:font-semibold',
@@ -7,7 +7,6 @@ import { t } from '@/intl/translate';
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
import type { AncestorRevisionPage } from '@/lib/pages';
import { tcls } from '@/lib/tailwind';
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
import { TrackPageViewEvent } from '../Insights';
import { PageFeedbackForm } from '../PageFeedback';
@@ -1,12 +1,14 @@
import type { RevisionPageDocument, RevisionPageDocumentCover } from '@gitbook/api';
import type { GitBookSiteContext } from '@v2/lib/context';
import type { StaticImageData } from 'next/image';
import { Image, type ImageSize } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import defaultPageCover from './default-page-cover.svg';
import defaultPageCoverSVG from './default-page-cover.svg';
const defaultPageCover = defaultPageCoverSVG as StaticImageData;
const PAGE_COVER_SIZE: ImageSize = { width: 1990, height: 480 };
/**
@@ -27,7 +27,7 @@ export async function PageHeader(props: {
>
{ancestors.length > 0 && (
<nav>
<ol className={tcls('flex', 'flex-wrap', 'items-center', 'gap-2')}>
<ol className={tcls('flex', 'flex-wrap', 'items-center', 'gap-2', 'text-tint')}>
{ancestors.map((breadcrumb, index) => {
const href = linker.toPathForPage({ pages, page: breadcrumb });
return (
@@ -35,7 +35,7 @@ export async function PageHeader(props: {
<li key={breadcrumb.id}>
<StyledLink
href={href}
style={tcls(
className={tcls(
'no-underline',
'hover:underline',
'text-xs',
@@ -0,0 +1,33 @@
'use client';
import React from 'react';
export type PageContextType = {
pageId: string;
spaceId: string;
title: string;
};
export const PageContext = React.createContext<PageContextType | null>(null);
/**
* Client side context provider to pass information about the current page.
*/
export function PageContextProvider(props: PageContextType & { children: React.ReactNode }) {
const { pageId, spaceId, title, children } = props;
const value = React.useMemo(() => ({ pageId, spaceId, title }), [pageId, spaceId, title]);
return <PageContext.Provider value={value}>{children}</PageContext.Provider>;
}
/**
* Hook to use the page context.
*/
export function usePageContext() {
const context = React.useContext(PageContext);
if (!context) {
throw new Error('usePageContext must be used within a PageContextProvider');
}
return context;
}
@@ -0,0 +1 @@
export * from './PageContext';
@@ -106,9 +106,8 @@ export function PageFeedbackForm(props: {
<Button
size="small"
onClick={() => onSubmitComment(rating, comment)}
>
{t(languages, 'submit')}
</Button>
label={tString(languages, 'submit')}
/>
{comment.length > MAX_COMMENT_LENGTH * 0.8 ? (
<span
className={
@@ -2,9 +2,9 @@ import {
CustomizationCorners,
CustomizationHeaderPreset,
CustomizationIconsStyle,
type CustomizationSettings,
CustomizationSidebarBackgroundStyle,
CustomizationSidebarListStyle,
CustomizationThemeMode,
type CustomizationThemedColor,
type CustomizationTint,
type SiteCustomizationSettings,
@@ -34,16 +34,18 @@ import { ClientContexts } from './ClientContexts';
import '@gitbook/icons/style.css';
import './globals.css';
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@v2/lib/env';
import { AnnouncementDismissedScript } from '../Announcement';
/**
* Layout shared between the content and the PDF renderer.
* It takes care of setting the theme and the language.
*/
export async function CustomizationRootLayout(props: {
customization: SiteCustomizationSettings | CustomizationSettings;
forcedTheme?: CustomizationThemeMode | null;
customization: SiteCustomizationSettings;
children: React.ReactNode;
}) {
const { customization, children } = props;
const { customization, forcedTheme, children } = props;
const language = getSpaceLanguage(customization);
const tintColor = getTintColor(customization);
@@ -85,7 +87,12 @@ export async function CustomizationRootLayout(props: {
'links' in customization.styling && `links-${customization.styling.links}`,
fontNotoColorEmoji.variable,
ibmPlexMono.variable,
fontData.type === 'default' ? fontData.variable : 'font-custom'
fontData.type === 'default' ? fontData.variable : 'font-custom',
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
? 'dark'
: ''
)}
>
<head>
@@ -96,6 +103,11 @@ export async function CustomizationRootLayout(props: {
{/* Inject custom font @font-face rules */}
{fontData.type === 'custom' ? <style>{fontData.fontFaceRules}</style> : null}
{/* Inject a script to detect if the announcmeent banner has been dismissed */}
{'announcement' in customization && customization.announcement?.enabled ? (
<AnnouncementDismissedScript />
) : null}
<style
nonce={
//Since I can't get the nonce to work for inline styles, we need to allow unsafe-inline
@@ -141,9 +153,9 @@ export async function CustomizationRootLayout(props: {
</head>
<body
className={tcls(
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'bg-tint-base',
'theme-muted:bg-tint-subtle',
'theme-bold-tint:bg-tint-subtle',
'theme-gradient:bg-gradient-primary',
'theme-gradient-tint:bg-gradient-tint'
@@ -175,7 +187,7 @@ export async function CustomizationRootLayout(props: {
* If the tint color is not set or it is a space customization, it will return the default color.
*/
function getTintColor(
customization: CustomizationSettings | SiteCustomizationSettings
customization: SiteCustomizationSettings
): CustomizationTint['color'] | undefined {
if ('tint' in customization.styling && customization.styling.tint) {
return {
@@ -222,7 +234,7 @@ function getTintMixColor(
* If it is a space customization, it will return the default styles.
*/
function getSidebarStyles(
customization: CustomizationSettings | SiteCustomizationSettings
customization: SiteCustomizationSettings
): SiteCustomizationSettings['styling']['sidebar'] {
if ('sidebar' in customization.styling) {
return {
@@ -242,7 +254,7 @@ function getSidebarStyles(
* If it is a space customization, it will return the default styles.
*/
function getSemanticColors(
customization: CustomizationSettings | SiteCustomizationSettings
customization: SiteCustomizationSettings
): Pick<
SiteCustomizationSettings['styling'],
'infoColor' | 'successColor' | 'warningColor' | 'dangerColor'
@@ -160,3 +160,7 @@ html {
html.dark {
color-scheme: dark light;
}
html.announcement-hidden .announcement-banner {
@apply hidden;
}
@@ -189,6 +189,9 @@ function SearchModalBody(
setSearchState((state) => (state ? { ...state, ask: true } : null));
};
// We trim the query to avoid invalidating the search when the user is typing between words.
const normalizedQuery = state.query.trim();
return (
<motion.div
transition={{
@@ -288,12 +291,14 @@ function SearchModalBody(
<SearchResults
ref={resultsRef}
global={isMultiVariants && state.global}
query={state.query}
query={normalizedQuery}
withAsk={withAsk}
onSwitchToAsk={onSwitchToAsk}
/>
) : null}
{state.query && state.ask && withAsk ? <SearchAskAnswer query={state.query} /> : null}
{normalizedQuery && state.ask && withAsk ? (
<SearchAskAnswer query={normalizedQuery} />
) : null}
</motion.div>
);
}
@@ -1,7 +1,6 @@
import { Icon } from '@gitbook/icons';
import React from 'react';
import { tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import React from 'react';
import { Link } from '../primitives';
import { HighlightQuery } from './HighlightQuery';
@@ -17,6 +16,14 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
) {
const { query, item, active } = props;
const breadcrumbs =
item.breadcrumbs?.map((crumb) => (
<span key={crumb.label} className="flex items-center gap-1">
{crumb.icon ? <Icon className="size-3" icon={crumb.icon as IconName} /> : null}
{crumb.label}
</span>
)) ?? [];
return (
<Link
ref={ref}
@@ -38,6 +45,14 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
? ['is-active', 'bg-primary', 'text-contrast-primary', 'hover:bg-primary-hover']
: null
)}
insights={{
type: 'search_open_result',
query,
result: {
pageId: item.pageId,
spaceId: item.spaceId,
},
}}
>
<div className="size-4">
<Icon
@@ -46,18 +61,44 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
/>
</div>
<div className={tcls('flex', 'flex-col', 'w-full')}>
{item.spaceTitle ? (
{breadcrumbs.length > 0 ? (
<div
className={tcls(
'text-xs',
'opacity-6',
'contrast-more:opacity-11',
'font-normal',
'uppercase',
'tracking-wider',
'mb-1'
'mb-1',
'flex',
'flex-wrap',
'gap-x-2',
'gap-y-1',
'items-center'
)}
>
{item.spaceTitle}
{(breadcrumbs.length > 3
? [
...breadcrumbs.slice(0, 2),
<Icon key="ellipsis" icon="ellipsis-h" className="size-3" />,
...breadcrumbs.slice(-1),
]
: breadcrumbs
).map((crumb, index) => (
<>
{index !== 0 ? (
<Icon
key={`${crumb.key}-icon`}
icon="chevron-right"
className="size-3"
/>
) : null}
<span key={crumb.key} className="line-clamp-1">
{crumb}
</span>
</>
))}
</div>
) : null}
<HighlightQuery query={query} text={item.title} />
@@ -77,7 +77,8 @@ export const SearchResults = React.forwardRef(function SearchResults(
let cancelled = false;
setResultsState({ results: [], fetching: true });
// Silently fetch the recommended questions, instead of showing a spinner
setResultsState({ results: [], fetching: false });
// We currently have a bug where the same question can be returned multiple times.
// This is a workaround to avoid that.
@@ -40,6 +40,14 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
'hover:bg-primary-hover',
]
)}
insights={{
type: 'search_open_result',
query,
result: {
pageId: item.pageId,
spaceId: item.spaceId,
},
}}
>
<div
className={tcls(
@@ -1,11 +1,17 @@
'use server';
import { resolvePageId } from '@/lib/pages';
import { findSiteSpaceById, getSiteStructureSections } from '@/lib/sites';
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import type {
RevisionPage,
SearchAIAnswer,
SearchAIRecommendedQuestionStream,
SearchPageResult,
SiteSpace,
SearchSpaceResult,
SiteSection,
SiteSectionGroup,
Space,
} from '@gitbook/api';
import type { GitBookBaseContext, GitBookSiteContext } from '@v2/lib/context';
@@ -13,13 +19,9 @@ import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/li
import { createStreamableValue } from 'ai/rsc';
import type * as React from 'react';
import { getAbsoluteHref } from '@/lib/links';
import { resolvePageId } from '@/lib/pages';
import { findSiteSpaceById } from '@/lib/sites';
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import { joinPathWithBaseURL } from '@/lib/paths';
import { isV2 } from '@/lib/v2';
import type { IconName } from '@gitbook/icons';
import { throwIfDataError } from '@v2/lib/data';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { DocumentView } from '../DocumentView';
@@ -30,18 +32,24 @@ export interface ComputedSectionResult {
type: 'section';
id: string;
title: string;
href: string;
body: string;
href: string;
pageId: string;
spaceId: string;
}
export interface ComputedPageResult {
type: 'page';
id: string;
title: string;
href: string;
/** When part of a multi-spaces search, the title of the space */
spaceTitle?: string;
pageId: string;
spaceId: string;
breadcrumbs?: Array<{ icon?: IconName; label: string }>;
}
export interface AskAnswerSource {
@@ -252,11 +260,27 @@ async function searchSiteContent(
return (
await Promise.all(
searchResults.map(async (spaceItem) => {
const sections = getSiteStructureSections(structure).flatMap((item) =>
item.object === 'site-section-group' ? [item, ...item.sections] : item
);
const siteSpace = findSiteSpaceById(structure, spaceItem.id);
const siteSection = sections.find(
(section) => section.id === siteSpace?.section
) as SiteSection;
const siteSectionGroup = siteSection?.sectionGroup
? sections.find((sectionGroup) => sectionGroup.id === siteSection.sectionGroup)
: null;
return Promise.all(
spaceItem.pages.map((item) =>
transformSitePageResult(item, siteSpace ?? undefined)
spaceItem.pages.map((pageItem) =>
transformSitePageResult(context, {
pageItem,
spaceItem,
space: siteSpace?.space,
spaceURL: siteSpace?.urls.published,
siteSection: siteSection ?? undefined,
siteSectionGroup: (siteSectionGroup as SiteSectionGroup) ?? undefined,
})
)
);
})
@@ -297,7 +321,7 @@ async function transformAnswer(
const spaceURL = siteSpace?.urls.published;
const href = spaceURL
? await getURLWithSections(page.page.path, spaceURL)
? joinPathWithBaseURL(spaceURL, page.page.path)
: context.linker.toPathForPage({
pages,
page: page.page,
@@ -330,55 +354,59 @@ async function transformAnswer(
};
}
async function transformSectionsAndPage(args: {
item: SearchPageResult;
space?: Space;
spaceURL?: string;
}): Promise<[ComputedPageResult, ComputedSectionResult[]]> {
const { item, space, spaceURL } = args;
const sections = await Promise.all(
item.sections?.map<Promise<ComputedSectionResult>>(async (section) => ({
type: 'section',
id: `${item.id}/${section.id}`,
title: section.title,
href: await getURLWithSections(section.path, spaceURL),
body: section.body,
})) ?? []
);
async function transformSitePageResult(
context: GitBookBaseContext,
args: {
pageItem: SearchPageResult;
spaceItem: SearchSpaceResult;
space?: Space;
spaceURL?: string;
siteSection?: SiteSection;
siteSectionGroup?: SiteSectionGroup;
}
): Promise<OrderedComputedResult[]> {
const { pageItem, spaceItem, space, spaceURL, siteSection, siteSectionGroup } = args;
const { linker } = context;
const page: ComputedPageResult = {
type: 'page',
id: item.id,
title: item.title,
href: await getURLWithSections(item.path, spaceURL),
spaceTitle: space?.title,
id: `${spaceItem.id}/${pageItem.id}`,
title: pageItem.title,
href: spaceURL
? linker.toLinkForContent(joinPathWithBaseURL(spaceURL, pageItem.path))
: linker.toPathInSpace(pageItem.path),
pageId: pageItem.id,
spaceId: spaceItem.id,
breadcrumbs: [
siteSectionGroup && {
icon: siteSectionGroup?.icon as IconName,
label: siteSectionGroup.title,
},
siteSection && {
icon: siteSection?.icon as IconName,
label: siteSection.title,
},
(!siteSection || siteSection?.siteSpaces.length > 1) && space
? {
label: space?.title,
}
: undefined,
].filter((item) => item !== undefined),
};
return [page, sections];
}
const pageSections = await Promise.all(
pageItem.sections?.map<Promise<ComputedSectionResult>>(async (section) => ({
type: 'section',
id: `${page.id}/${section.id}`,
title: section.title,
href: spaceURL
? linker.toLinkForContent(joinPathWithBaseURL(spaceURL, section.path))
: linker.toPathInSpace(pageItem.path),
body: section.body,
pageId: pageItem.id,
spaceId: spaceItem.id,
})) ?? []
);
async function transformSitePageResult(item: SearchPageResult, siteSpace?: SiteSpace) {
const [page, sections] = await transformSectionsAndPage({
item,
space: siteSpace?.space,
spaceURL: siteSpace?.urls.published,
});
return [page, ...sections];
}
// Resolve a relative path to an absolute URL
// if the search result is relative to another space, we use the space URL
async function getURLWithSections(path: string, spaceURL?: string) {
if (spaceURL) {
if (!spaceURL.endsWith('/')) {
spaceURL += '/';
}
if (path.startsWith('/')) {
path = path.slice(1);
}
return spaceURL + path;
}
return getAbsoluteHref(path);
return [page, ...pageSections];
}
@@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks';
export function PageClientLayout(props: { withSections?: boolean }) {
// We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to a hash or to the top of the page
useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined });
useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined });
useStripFallbackQueryParam();
return null;
@@ -11,12 +11,10 @@ import { getPagePath } from '@/lib/pages';
import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
import { getResizedImageURL } from '@v2/lib/images';
import { PageContextProvider } from '../PageContext';
import { PageClientLayout } from './PageClientLayout';
import { type PagePathParams, fetchPageData, getPathnameParam } from './fetch';
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
export type SitePageProps = {
context: GitBookSiteContext;
pageParams: PagePathParams;
@@ -67,7 +65,7 @@ export async function SitePage(props: SitePageProps) {
const document = await getPageDocument(context.dataFetcher, context.space, page);
return (
<>
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={context} />
) : null}
@@ -92,7 +90,7 @@ export async function SitePage(props: SitePageProps) {
<React.Suspense fallback={null}>
<PageClientLayout withSections={withSections} />
</React.Suspense>
</>
</PageContextProvider>
);
}
@@ -32,7 +32,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
<nav
aria-label="Sections"
className={tcls(
'-mx-5 before:contents[] relative border-tint-subtle border-b from-transparent sidebar-filled:to-tint-subtle theme-bold-tint:to-tint-subtle theme-muted:to-tint-subtle to-tint-base text-sm text-tint before:pointer-events-none before:absolute before:right-2 before:bottom-0 before:left-0 before:h-12 before:bg-gradient-to-b [html.sidebar-filled.theme-bold.tint_&]:to-tint-base [html.sidebar-filled.theme-muted_&]:to-tint-base',
'-mx-5 before:contents[] relative border-tint-subtle border-b from-transparent sidebar-filled:to-tint-subtle theme-muted:to-tint-subtle to-tint-base text-sm text-tint before:pointer-events-none before:absolute before:right-2 before:bottom-0 before:left-0 before:h-12 before:bg-gradient-to-b [html.sidebar-filled.theme-bold.tint_&]:to-tint-base [html.sidebar-filled.theme-bold.tint_&]:to-tint-subtle [html.sidebar-filled.theme-muted_&]:to-tint-base',
className
)}
>

Some files were not shown because too many files have changed in this diff Show More