Compare commits

..

6 Commits

Author SHA1 Message Date
Samy Pessé 1424f431a9 Merge branch 'fix-embeddable-cors' of https://github.com/GitbookIO/gitbook into fix-embeddable-cors 2025-12-05 09:50:54 +01:00
Samy Pessé c2401e9a78 Remove options test 2025-12-05 09:50:53 +01:00
Samy Pessé 57625cd365 Apply suggestions from code review
Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
2025-12-05 09:49:48 +01:00
Samy Pessé 0f330d5e00 Add test 2025-12-05 09:43:15 +01:00
Samy Pessé 91b9c7955b Changeset 2025-12-05 09:33:13 +01:00
Samy Pessé fc2c708ba8 Serve CORS headers on the embed/script.js route 2025-12-05 09:32:04 +01:00
78 changed files with 1029 additions and 2055 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Fix missing properties in allOf/oneOf
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add Input component
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add sidesheet component, use it for TOC and AIChat
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Enhance discriminator handling in OpenAPISchema
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix overflowing section groups
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix search results cursor
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/icons": minor
---
Update to FontAwesome 7.1.0
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add customizable suggested questions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Support `greeting` and fix suggested questions if there are no custom ones defined
-5
View File
@@ -1,5 +0,0 @@
---
'@gitbook/react-openapi': patch
---
Fix OpenAPI oneOf/allOf merge
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix CORS error when using embed script.js directly
+50 -139
View File
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "gitbook",
@@ -126,7 +125,7 @@
"@gitbook/react-openapi": "workspace:*",
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "^3.8.5",
"@opennextjs/cloudflare": "^1.14.4",
"@opennextjs/cloudflare": "^1.11.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15",
@@ -157,7 +156,7 @@
"micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24",
"next": "15.4.10",
"next": "15.4.0",
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
@@ -223,8 +222,8 @@
"gitbook-icons": "./bin/gitbook-icons.js",
},
"dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/fontawesome-free": "^6.6.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
},
"devDependencies": {
"@types/react": "catalog:",
@@ -234,7 +233,7 @@
"typescript": "catalog:",
},
"optionalDependencies": {
"@gitbook/fontawesome-pro": "1.0.16",
"@gitbook/fontawesome-pro": "1.0.15",
},
"peerDependencies": {
"react": "*",
@@ -346,7 +345,7 @@
"react-dom": "catalog:",
},
"catalog": {
"@gitbook/api": "0.154.0",
"@gitbook/api": "0.153.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
@@ -356,8 +355,8 @@
"bidc": "^0.0.2",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsdown": "^0.15.6",
"typescript": "^5.5.3",
"usehooks-ts": "^3.1.1",
@@ -385,25 +384,25 @@
"@argos-ci/util": ["@argos-ci/util@2.3.2", "", {}, "sha512-xtNHJxpWYNst/sMNn4Jv/vkODuFsJ+APr4FBeoFUdIa+Izjl4ZFHsYA2PUyu+ygIpQCkof8yZLL9U1/VpiyyIw=="],
"@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="],
"@ast-grep/napi": ["@ast-grep/napi@0.35.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.35.0", "@ast-grep/napi-darwin-x64": "0.35.0", "@ast-grep/napi-linux-arm64-gnu": "0.35.0", "@ast-grep/napi-linux-arm64-musl": "0.35.0", "@ast-grep/napi-linux-x64-gnu": "0.35.0", "@ast-grep/napi-linux-x64-musl": "0.35.0", "@ast-grep/napi-win32-arm64-msvc": "0.35.0", "@ast-grep/napi-win32-ia32-msvc": "0.35.0", "@ast-grep/napi-win32-x64-msvc": "0.35.0" } }, "sha512-3ucaaSxV6fxXoqHrE/rxAvP1THnDdY5jNzGlnvx+JvnY9C/dSRKc0jlRMRz59N3El572+/yNRUUpAV1T9aBJug=="],
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="],
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.35.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-T+MN4Oinc+sXjXCIHzfxDDWY7r2pKgPxM6zVeVlkMTrJV2mJtyKYBIS+CABhRM6kflps2T2I6l4DGaKV/8Ym9w=="],
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-f9Ol5oQKNRMBkvDtzBK1WiNn2/3eejF2Pn9xwTj7PhXuSFseedOspPYllxQo0gbwUlw/DJqGFTce/jarhR/rBw=="],
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.35.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-pEYiN6JI1HY2uWhMYJ9+3yIMyVYKuYdFzeD+dL7odA3qzK0o9N9AM3/NOt4ynU2EhufaWCJr0P5NoQ636qN6MQ=="],
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tO+VW5GDhT9jGkKOK+3b8+ohKjC98WTzn7wSskd/myyhK3oYL1WTKqCm07WSYBZOJvb3z+WaX+wOUrc4bvtyQ=="],
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NBuzQngABGKz7lhG08IQb+7nPqUx81Ol37xmS3ZhVSdSgM0mtp93rCbgFTkJcAFE8IMfCHQSg7G4g0Iotz4ABQ=="],
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MS9qalLRjUnF2PCzuTKTvCMVSORYHxxe3Qa0+SSaVULsXRBmuy5C/b1FeWwMFnwNnC0uie3VDet31Zujwi8q6A=="],
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EcvHPwyWpCL/96LuItBYGfeI5FaMTRvL+dHbO/hL5q1npqbb5qn+ppJwtNOjTPz8tayvgggxVk9T4C2O7taYA=="],
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA=="],
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-FDzNdlqmQnsiWXhnLxusw5AOfEcEM+5xtmrnAf3SBRFr86JyWD9qsynnFYC2pnP9hlMfifNH2TTmMpyGJW49Xw=="],
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ=="],
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wlmndjfBafT8u5p4DBnoRQyoCSGNuVSz7rT3TqhvlHcPzUouRWMn95epU9B1LNLyjXvr9xHeRjSktyCN28w57Q=="],
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9SqmnQqd4zTEUk6yx0TuW2ycZZs2+e569O/R0QnhSiQNpgwiJCYOe/yPS0BC9HkiaozQm6jjAcasWpFtz/dp+w=="],
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.35.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-gkhJeYc4rrZLX2icLxalPikTLMR57DuIYLwLr9g+StHYXIsGHrbfrE6Nnbdd8Izfs34ArFCrcwdaMrGlvOPSeg=="],
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0JkdBZi5l9vZhGEO38A1way0LmLRDU5Vos6MXrLIOVkymmzDTDlCdY394J1LMmmsfwWcyJg6J7Yv2dw41MCxDQ=="],
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.35.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-OdUuRa3chHCZ65y+qALfkUjz0W0Eg21YZ9TyPquV5why07M6HAK38mmYGzLxFH6294SvRQhs+FA/rAfbKeH0jA=="],
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="],
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.35.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pcQRUHqbroTN1oQ56V982a7IZTUUySQYWa2KEyksiifHGuBuitlzcyzFGjT96ThcqD9XW0UVJMvpoF2Qjh006Q=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
@@ -721,13 +720,13 @@
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.5.10", "", { "dependencies": { "tslib": "2" } }, "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q=="],
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.1.0", "", {}, "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA=="],
"@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@6.7.2", "", {}, "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="],
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@7.1.0", "", {}, "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA=="],
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.6.0", "", {}, "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow=="],
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
"@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.154.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-huEgWWa2H+H4ofYEAQl9r49u0gVjDHHWVNJKhd6SgGCfefjNPHkQj9zaky/5DBQzlu/+SDmutWiOtYdrykQO8w=="],
"@gitbook/api": ["@gitbook/api@0.153.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-ArNPFoqwId4Flicz8xPEdGqXQkzxyxP7S8Uv3wIfCX2e4cLhpcS7xCJGEHOJrqe1tNs1ovT1N2MWfpdIqzXqig=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
@@ -741,7 +740,7 @@
"@gitbook/expr": ["@gitbook/expr@workspace:packages/expr"],
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.16", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^7.1.0" } }, "sha512-jypE3FpC5U7lealzpGVq1Jie5Ay5jYv/UkOLxPhOpffjCd98A+Ex08M6GyINMDO7KCBaF5p8pbdl693hf0DwNg=="],
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.15", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.7.2" } }, "sha512-iiXDkwzvaRrRF853Vwjt8kWrd6yRJFb7BfhFyBNYvVmjVmxRfrvmstiOPUIdHS7jPaAWCWLzirhokwh8BFBTHw=="],
"@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
@@ -823,10 +822,6 @@
"@internationalized/string": ["@internationalized/string@3.2.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A=="],
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
@@ -873,23 +868,23 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.6", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g=="],
"@next/env": ["@next/env@15.4.10", "", {}, "sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg=="],
"@next/env": ["@next/env@15.4.0-canary.57", "", {}, "sha512-gDNgJA7YnuUWQ2Ei6oeMiuSSWWICyYGK0AbWQnOfub1LIyYPNXMSyvyLTkGwVyIixbLMUFdzjTgU+ZxYHil7Mg=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tUq3IBxdu1I+CfIQ7fOVHb5L0Ehu9lXmA3bcVVMDswnKnmL3V15LInWkFbQ/kuIDv87AT+UfJB4us+yJ1lNr/A=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-xla6AOfz68a6kq3gRQccWEvFC/VRGJmA/QuSLENSO7CZX5WIEkSz7r1FdXUjtGCQ1c2M+ndUAH7opdfLK1PQbw=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-zsFozes7iXAkfQYQGYi9xiDKrS7mjLCMl9DIZfm0cEmilYmJrO16rn04H4Y+NJZ5kBxJUXn+hAy4kXmToZYFHA=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-y3fmp+1Px/SJD+5ntve5QLZnGLycsxsVPkTzAc3zUiXYSOlTPqT8ynfmt6tt4fSo1tAhDPmryXpYKEAcoAPDJw=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-z5S7BWDhBp+VJUzDEVpDasJC9EebZ1Jw8R25ryEH+ojLl14kMuoDtULOdin3f4zXBEwPvJrkt1G+Aes+TSX4iQ=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-VqN0D+CdUxMSO+a1EaIEJajMKlHbsSIObd1zYiNJ3d5vWO8eg9K/f+AtBm6F4zaRlSLTURKLoZ+f6aGIbzhXFA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iWQKV+kCeg0iSQR0tYm8I5K0w7YJnvy3HsdIlYHAhHYiqFEr/3JkKL3p1KduxRiaMj3dCv5ZuyZuCGV1le89ew=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pm9t8LhfpNLtU5OjMny7OBjATRn3tiP+Kw4FQz/27XhgTmiwNbcqSAV4w+8TJ34e/X61xjj6vY32fsofxsQU5A=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yvx5Rj9Tb370IERMvHo9+gAxLQI6OIwmin0kELI8fvGYflaRz/cF1H2sk+a2y7WH3LY7RnKVRxoRLLVsch2WXQ=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-IldilrY7yfSlqOpTha7I7rYDDkyCC/KIMCjo2Nf0WqTLw3B5ha4islIF07YjNRbZS1Xl5WMqo9zZtx7VAE4epg=="],
"@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
@@ -925,9 +920,9 @@
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@opennextjs/aws": ["@opennextjs/aws@3.9.4", "", { "dependencies": { "@ast-grep/napi": "^0.40.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "^5.1.0", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.8.1" }, "peerDependencies": { "next": "< 14.2 || ^14.2 || 14.3.0-canary.0 - 14.3.0-canary.76 || ~15.0.5 || ~15.1.9 || ~15.2.6 || ~15.3.6 || ~15.4.8 || ~15.5.7 || ^16.0.7" }, "bin": { "open-next": "dist/index.js" } }, "sha512-i5gWKSP7QQGz59z85ZjOGtIfXK1ZgRLEwbt7/dkmuujhTuMBWygQqrb2EGRf8vRZH1Xvp0fYENUZx7NjgvxsIg=="],
"@opennextjs/aws": ["@opennextjs/aws@3.8.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-elpMb0fJZc0a1VtymedFa7P1lYcyOmt+Pwqyacpq2C/SvbETIeBlW/Xle/vY95ICtccKUxITI8MtonrCo2+2/Q=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.14.4", "", { "dependencies": { "@ast-grep/napi": "0.40.0", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.9.4", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": "14 - 14.2 || 14.3.0-canary.0 - 14.3.0-canary.76 || ~15.0.5 || ~15.1.9 || ~15.2.6 || ~15.3.6 || ~15.4.8 || ~15.5.7 || ^16.0.7", "wrangler": "^4.49.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-2dODhjsRsZog1VHxX9QwUkxq76aBwaEl+L67m8c47V6zZpIgAFODHMxtj5NPM3uz6pUiT4edYGNS5bR99ck3Jg=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.11.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.8.5", "@types/rclone.js": "^0.6.3", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "rclone.js": "^0.6.6", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "wrangler": "^4.38.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-2QVZLjCr3hPwkcGEetLarTZvlNIHsGV3j2+i+KNsjFIBgvsnxhmRqJbT0u8J9cVMFKNh19hSOTOmUKs1T3DWig=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -1543,6 +1538,8 @@
"@types/parse-cache-control": ["@types/parse-cache-control@1.0.4", "", {}, "sha512-YA9hcvRn3oYBdLK0PdWRsw5ehrqmfdCwfz68sSAxi2OB6L7B3jJEhrqlLfMvxnWfsZcUdbR6QI6NfkY1Y4e2zg=="],
"@types/rclone.js": ["@types/rclone.js@0.6.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-BssKAAVRY//fxGKso8SatyOwiD7X0toDofNnVxZlIXmN7UHrn2UBTxldNAjgUvWA91qJyeEPfKmeJpZVhLugXg=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
@@ -1637,6 +1634,8 @@
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
"adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
@@ -1731,10 +1730,6 @@
"call-bind": ["call-bind@1.0.7", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.1" } }, "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001668", "", {}, "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="],
@@ -1883,8 +1878,6 @@
"dts-resolver": ["dts-resolver@2.1.2", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
@@ -1929,8 +1922,6 @@
"es-module-lexer": ["es-module-lexer@1.4.1", "", {}, "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="],
"es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="],
@@ -2033,7 +2024,7 @@
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
@@ -2073,8 +2064,6 @@
"get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
@@ -2083,7 +2072,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"glob": ["glob@12.0.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw=="],
"glob": ["glob@11.0.1", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^4.0.1", "minimatch": "^10.0.0", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -2235,7 +2224,7 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
"jackspeak": ["jackspeak@4.0.2", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw=="],
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
@@ -2365,8 +2354,6 @@
"markdown-table": ["markdown-table@3.0.3", "", {}, "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mathjax": ["mathjax@3.2.2", "", {}, "sha512-Bt+SSVU8eBG27zChVewOicYs7Xsdt40qm4+UpHyX7k0/O9NliPc+x77k1/FEsPsjKPZGJvtRZM1vO+geW0OhGw=="],
"mathml-tag-names": ["mathml-tag-names@2.1.3", "", {}, "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg=="],
@@ -2489,7 +2476,7 @@
"miniflare": ["miniflare@4.20251008.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251008.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-sKCNYNzXG6l8qg0Oo7y8WcDKcpbgw0qwZsxNpdZilFTR4EavRow2TlcwuPSVN99jqAjhz0M4VXvTdSGdtJ2VfQ=="],
"minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
"minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -2517,7 +2504,7 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next": ["next@15.4.10", "", { "dependencies": { "@next/env": "15.4.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.8", "@next/swc-darwin-x64": "15.4.8", "@next/swc-linux-arm64-gnu": "15.4.8", "@next/swc-linux-arm64-musl": "15.4.8", "@next/swc-linux-x64-gnu": "15.4.8", "@next/swc-linux-x64-musl": "15.4.8", "@next/swc-win32-arm64-msvc": "15.4.8", "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ=="],
"next": ["next@15.4.0", "", { "dependencies": { "@next/env": "15.4.0-canary.57", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0", "@next/swc-darwin-x64": "15.4.0", "@next/swc-linux-arm64-gnu": "15.4.0", "@next/swc-linux-arm64-musl": "15.4.0", "@next/swc-linux-x64-gnu": "15.4.0", "@next/swc-linux-x64-musl": "15.4.0", "@next/swc-win32-arm64-msvc": "15.4.0", "@next/swc-win32-x64-msvc": "15.4.0", "sharp": "^0.34.1" }, "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-9JfEyg23carbb/AxUylRQh4zVN4dt1af9aJ+L3CFl/LKQIQkWUWN44YhB9M5JhxMjleF6rkfNOD7IqDcsb6zyQ=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
@@ -2675,6 +2662,8 @@
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"rclone.js": ["rclone.js@0.6.6", "", { "dependencies": { "adm-zip": "^0.5.9", "mri": "^1.2.0" }, "os": [ "!aix", "!android", ], "cpu": [ "!ppc", "!ia32", "!s390", "!ppc64", "!s390x", ], "bin": { "rclone": "bin/rclone.js" } }, "sha512-Dxh34cab/fNjFq5SSm0fYLNkGzG2cQSBy782UW9WwxJCEiVO4cGXkvaXcNlgv817dK8K8PuQ+NHUqSAMMhWujQ=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-aria": ["react-aria@3.44.0", "", { "dependencies": { "@internationalized/string": "^3.2.7", "@react-aria/breadcrumbs": "^3.5.29", "@react-aria/button": "^3.14.2", "@react-aria/calendar": "^3.9.2", "@react-aria/checkbox": "^3.16.2", "@react-aria/color": "^3.1.2", "@react-aria/combobox": "^3.14.0", "@react-aria/datepicker": "^3.15.2", "@react-aria/dialog": "^3.5.31", "@react-aria/disclosure": "^3.1.0", "@react-aria/dnd": "^3.11.3", "@react-aria/focus": "^3.21.2", "@react-aria/gridlist": "^3.14.1", "@react-aria/i18n": "^3.12.13", "@react-aria/interactions": "^3.25.6", "@react-aria/label": "^3.7.22", "@react-aria/landmark": "^3.0.7", "@react-aria/link": "^3.8.6", "@react-aria/listbox": "^3.15.0", "@react-aria/menu": "^3.19.3", "@react-aria/meter": "^3.4.27", "@react-aria/numberfield": "^3.12.2", "@react-aria/overlays": "^3.30.0", "@react-aria/progress": "^3.4.27", "@react-aria/radio": "^3.12.2", "@react-aria/searchfield": "^3.8.9", "@react-aria/select": "^3.17.0", "@react-aria/selection": "^3.26.0", "@react-aria/separator": "^3.4.13", "@react-aria/slider": "^3.8.2", "@react-aria/ssr": "^3.9.10", "@react-aria/switch": "^3.7.8", "@react-aria/table": "^3.17.8", "@react-aria/tabs": "^3.10.8", "@react-aria/tag": "^3.7.2", "@react-aria/textfield": "^3.18.2", "@react-aria/toast": "^3.0.8", "@react-aria/tooltip": "^3.8.8", "@react-aria/tree": "^3.1.4", "@react-aria/utils": "^3.31.0", "@react-aria/visually-hidden": "^3.8.28", "@react-types/shared": "^3.32.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-2Pq3GQxBgM4/2BlpKYXeaZ47a3tdIcYSW/AYvKgypE3XipxOdQMDG5Sr/NBn7zuJq+thzmtfRb0lB9bTbsmaRw=="],
@@ -2785,12 +2774,6 @@
"side-channel": ["side-channel@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" } }, "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
@@ -2977,7 +2960,7 @@
"url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="],
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
"urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="],
"use-callback-ref": ["use-callback-ref@1.3.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA=="],
@@ -3663,6 +3646,8 @@
"@floating-ui/vue/@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@fortawesome/fontawesome-svg-core/@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@6.6.0", "", {}, "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw=="],
"@hyperjump/json-schema/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -3715,12 +3700,6 @@
"@node-minify/core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"@opennextjs/aws/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"@opennextjs/aws/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"@opennextjs/aws/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -4029,6 +4008,8 @@
"@types/jsonwebtoken/@types/node": ["@types/node@20.16.11", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw=="],
"@types/rclone.js/@types/node": ["@types/node@22.18.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg=="],
"@types/swagger2openapi/@types/node": ["@types/node@20.16.11", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw=="],
"@types/ws/@types/node": ["@types/node@20.16.11", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw=="],
@@ -4089,8 +4070,6 @@
"bun-types/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
"call-bound/get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
@@ -4113,8 +4092,6 @@
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"dunder-proto/gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="],
"edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
@@ -4137,8 +4114,6 @@
"finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
"foreground-child/cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"fs-minipass/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="],
@@ -4257,16 +4232,6 @@
"send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"side-channel-list/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"side-channel-map/get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"side-channel-map/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"side-channel-weakmap/get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"side-channel-weakmap/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"spawndamnit/cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -4811,20 +4776,6 @@
"@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"@opennextjs/aws/express/body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
"@opennextjs/aws/express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
"@opennextjs/aws/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"@opennextjs/aws/express/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"@opennextjs/aws/express/router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"@opennextjs/aws/express/serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
"@opennextjs/aws/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
@@ -5027,12 +4978,6 @@
"bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"call-bound/get-intrinsic/es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"call-bound/get-intrinsic/gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"call-bound/get-intrinsic/has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"cliui/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
@@ -5121,8 +5066,6 @@
"remark-gfm/mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="],
"rimraf/glob/foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
"rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -5131,18 +5074,6 @@
"send/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"side-channel-map/get-intrinsic/es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"side-channel-map/get-intrinsic/gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"side-channel-map/get-intrinsic/has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"side-channel-weakmap/get-intrinsic/es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"side-channel-weakmap/get-intrinsic/gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"side-channel-weakmap/get-intrinsic/has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"stylelint/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
@@ -5293,16 +5224,6 @@
"@node-minify/core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"@opennextjs/aws/express/body-parser/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"@opennextjs/aws/express/qs/side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"@opennextjs/aws/express/router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"@opennextjs/aws/express/router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"@opennextjs/aws/express/serve-static/send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
@@ -5389,12 +5310,6 @@
"@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"@opennextjs/aws/express/body-parser/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"@opennextjs/aws/express/qs/side-channel/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"@opennextjs/aws/express/serve-static/send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"@scalar/draggable/vue/@vue/compiler-sfc/@babel/parser/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@scalar/draggable/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
@@ -5420,9 +5335,5 @@
"@aws-sdk/middleware-sdk-sqs/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="],
"@aws-sdk/middleware-sdk-sqs/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="],
"@opennextjs/aws/express/body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"@opennextjs/aws/express/serve-static/send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
}
}
+3 -3
View File
@@ -41,7 +41,7 @@
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.154.0",
"@gitbook/api": "0.153.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
@@ -49,8 +49,8 @@
"bidc": "^0.0.2",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsdown": "^0.15.6",
"typescript": "^5.5.3",
"usehooks-ts": "^3.1.1"
+2 -2
View File
@@ -19,7 +19,7 @@
"@gitbook/react-openapi": "workspace:*",
"@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "^3.8.5",
"@opennextjs/cloudflare": "^1.14.4",
"@opennextjs/cloudflare": "^1.11.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15",
@@ -50,7 +50,7 @@
"micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24",
"next": "15.4.10",
"next": "15.4.0",
"next-themes": "^0.4.6",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
@@ -16,7 +16,7 @@ export default async function Page(props: PageProps) {
const { context } = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(params);
return <SitePage context={context} pageParams={{ pathname }} staticRoute={false} />;
return <SitePage context={context} pageParams={{ pathname }} />;
}
export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -23,8 +23,7 @@ export default async function SiteDynamicLayout({
return (
<CustomizationRootLayout
htmlClassName="sheet-open:gutter-stable"
bodyClassName="site-background"
className="site-background"
forcedTheme={forcedTheme}
context={context}
>
@@ -12,7 +12,7 @@ export default async function Page(props: PageProps) {
const { context } = await getEmbeddableDynamicContext(params);
const pathname = getPagePathFromParams(params);
return <EmbeddableDocsPage context={context} pageParams={{ pathname }} staticRoute={false} />;
return <EmbeddableDocsPage context={context} pageParams={{ pathname }} />;
}
export async function generateMetadata(props: PageProps): Promise<Metadata> {
@@ -18,7 +18,7 @@ export default async function Page(props: PageProps) {
const { context } = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(params);
return <SitePage context={context} pageParams={{ pathname }} staticRoute />;
return <SitePage context={context} pageParams={{ pathname }} />;
}
export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -19,11 +19,7 @@ export default async function SiteStaticLayout({
const withTracking = shouldTrackEvents();
return (
<CustomizationRootLayout
htmlClassName="sheet-open:gutter-stable"
bodyClassName="site-background"
context={context}
>
<CustomizationRootLayout className="site-background" context={context}>
<SiteLayout
context={context}
withTracking={withTracking}
@@ -14,7 +14,7 @@ export default async function Page(props: PageProps) {
const { context } = await getEmbeddableStaticContext(params);
const pathname = getPagePathFromParams(params);
return <EmbeddableDocsPage context={context} pageParams={{ pathname }} staticRoute />;
return <EmbeddableDocsPage context={context} pageParams={{ pathname }} />;
}
export async function generateMetadata(props: PageProps): Promise<Metadata> {
@@ -7,6 +7,14 @@ import type { NextRequest } from 'next/server';
export const dynamic = 'force-static';
const EMBEDDABLE_RESPONSE_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Cross-Origin-Resource-Policy': 'cross-origin',
'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate=604800',
};
/**
* This route is used to serve the assistant.js script.
*/
@@ -69,8 +77,8 @@ export async function GET(
`,
{
headers: {
...EMBEDDABLE_RESPONSE_HEADERS,
'Content-Type': 'application/javascript',
'Cache-Control': 'public, max-age=86400, stale-while-revalidate=604800',
},
}
);
+8 -28
View File
@@ -3,7 +3,7 @@ import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@
import { getDynamicCustomizationSettings } from '@/lib/customization';
import type { SiteAPIToken } from '@gitbook/api';
import { jwtDecode } from 'jwt-decode';
import { forbidden, notFound } from 'next/navigation';
import { forbidden } from 'next/navigation';
import rison from 'rison';
export type RouteParamMode = 'url-host' | 'url';
@@ -80,27 +80,14 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
* Get the decoded page path from the params.
*/
export function getPagePathFromParams(params: RouteParams) {
// If decoding the param fails, return a 404 instead of crashing
try {
const decoded = decodeURIComponent(params.pagePath);
return decoded;
} catch (error) {
console.error(
`Returning 404 after failing to decode page path ${params.pagePath}: ${error}`
);
notFound();
}
const decoded = decodeURIComponent(params.pagePath);
return decoded;
}
function getSiteURLFromParams(params: RouteLayoutParams) {
try {
const decoded = decodeURIComponent(params.siteURL);
const url = new URL(`https://${decoded}`);
return url;
} catch (error) {
console.error(`Returning 404 after failing to decode site URL ${params.siteURL}: ${error}`);
notFound();
}
const decoded = decodeURIComponent(params.siteURL);
const url = new URL(`https://${decoded}`);
return url;
}
function getModeFromParams(mode: string): RouteParamMode {
@@ -115,13 +102,6 @@ function getModeFromParams(mode: string): RouteParamMode {
* Get the decoded site data from the params.
*/
function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
try {
const decoded = decodeURIComponent(params.siteData);
return rison.decode(decoded);
} catch (error) {
console.error(
`Returning 404 after failing to decode site data ${params.siteData}: ${error}`
);
notFound();
}
const decoded = decodeURIComponent(params.siteData);
return rison.decode(decoded);
}
@@ -128,10 +128,10 @@ function parseResponse<T>(
parse: (response: AIStreamResponse) => T | undefined | Promise<T | undefined>
): {
stream: EventIterator<T>;
response: Promise<{ responseId: string | null }>;
response: Promise<{ responseId: string }>;
} {
let resolveResponse: (value: { responseId: string | null }) => void;
const response = new Promise<{ responseId: string | null }>((resolve) => {
let resolveResponse: (value: { responseId: string }) => void;
const response = new Promise<{ responseId: string }>((resolve) => {
resolveResponse = resolve;
});
@@ -147,7 +147,7 @@ function parseResponse<T>(
if (event.type === 'response_finish') {
foundResponse = true;
resolveResponse({ responseId: event.response.id ?? null });
resolveResponse({ responseId: event.responseId });
}
}
+2 -10
View File
@@ -15,12 +15,7 @@ import { useSearch } from '../Search/useSearch';
// Unify assistants configuration context with the assistants hook in one place
export type AIConfig = {
aiMode: CustomizationAIMode;
suggestions?: string[];
trademark: boolean;
greeting?: {
title: string;
subtitle: string;
};
};
export type Assistant = Omit<GitBookAssistant, 'icon'> & {
@@ -54,11 +49,8 @@ export type Assistant = Omit<GitBookAssistant, 'icon'> & {
const AIContext = React.createContext<AIConfig | null>(null);
export function AIContextProvider(props: React.PropsWithChildren<AIConfig>): React.ReactElement {
const { aiMode, trademark, suggestions, greeting, children } = props;
const value = React.useMemo(
() => ({ aiMode, trademark, suggestions, greeting }),
[aiMode, trademark, suggestions, greeting]
);
const { aiMode, trademark, children } = props;
const value = React.useMemo(() => ({ aiMode, trademark }), [aiMode, trademark]);
return <AIContext.Provider value={value}>{children}</AIContext.Provider>;
}
@@ -296,7 +296,7 @@ export function AIChatProvider(props: {
case 'response_finish': {
globalState.setState((state) => ({
...state,
responseId: event.response.id ?? null,
responseId: event.responseId,
// Mark as not loading when the response is finished
// Even if the stream might continue as we receive 'response_followup_suggestion'
loading: false,
@@ -27,7 +27,6 @@ import { useTrackEvent } from '../Insights';
import { useNow } from '../hooks';
import { Button } from '../primitives';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { SideSheet } from '../primitives/SideSheet';
import { AIChatControlButton } from './AIChatControlButton';
import { AIChatIcon } from './AIChatIcon';
import { AIChatInput } from './AIChatInput';
@@ -70,23 +69,16 @@ export function AIChat() {
}, [chat.opened, trackEvent]);
return (
<SideSheet
side="right"
open={chat.opened}
onOpenChange={(open) => {
if (open) {
chatController.open();
} else {
chatController.close();
}
}}
<div
data-testid="ai-chat"
withScrim={true}
className={tcls(
'ai-chat mx-auto ml-8 not-hydrated:hidden w-96 transition-[width] duration-300 ease-quint lg:max-xl:w-80'
'ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl scroll-mt-36 px-4 py-4 transition-[width,opacity,margin,display] transition-discrete duration-300 sm:px-6 lg:fixed lg:w-80 lg:p-0 xl:w-96',
chat.opened
? 'lg:starting:ml-0 lg:starting:w-0 lg:starting:opacity-0'
: 'hidden lg:ml-0 lg:w-0! lg:opacity-0'
)}
>
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base">
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base transition-all duration-300 max-lg:circular-corners:rounded-3xl max-lg:rounded-corners:rounded-md max-lg:border lg:w-80 xl:w-96">
<EmbeddableFrameMain>
<EmbeddableFrameHeader>
<AIChatDynamicIcon trademark={config.trademark} />
@@ -109,15 +101,11 @@ export function AIChat() {
</EmbeddableFrameButtons>
</EmbeddableFrameHeader>
<EmbeddableFrameBody>
<AIChatBody
chatController={chatController}
chat={chat}
suggestions={config.suggestions}
/>
<AIChatBody chatController={chatController} chat={chat} />
</EmbeddableFrameBody>
</EmbeddableFrameMain>
</EmbeddableFrame>
</SideSheet>
</div>
);
}
@@ -201,14 +189,11 @@ export function AIChatBody(props: {
chat: AIChatState;
welcomeMessage?: string;
suggestions?: string[];
greeting?: {
title: string;
subtitle: string;
};
}) {
const { chatController, chat, suggestions, greeting } = props;
const { chatController, chat, suggestions } = props;
const { trademark } = useAI().config;
const [input, setInput] = React.useState('');
const language = useLanguage();
const now = useNow(60 * 60 * 1000); // Refresh every hour for greeting
@@ -228,8 +213,8 @@ export function AIChatBody(props: {
className="shrink grow basis-80 animate-fade-in-slow [container-type:size]"
contentClassName="p-4 gutter-stable flex flex-col gap-4"
orientation="vertical"
trailing={{ fade: false, button: true }}
active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
fadeEdges={['leading']}
active={`message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
>
{isEmpty ? (
<div className="flex grow flex-col">
@@ -244,20 +229,19 @@ export function AIChatBody(props: {
className="size-8 text-primary [@container(min-height:400px)]:size-16"
/>
</div>
<div className="flex flex-col items-start gap-1 [@container(min-height:400px)]:items-center">
<div className="flex flex-col items-start [@container(min-height:400px)]:items-center">
<h5
className="animate-blur-in-slow font-bold text-lg text-tint-strong leading-tight [@container(min-height:400px)]:text-center"
className="animate-blur-in-slow font-bold text-lg text-tint-strong [@container(min-height:400px)]:text-center"
style={{ animationDelay: '.5s' }}
data-testid="ai-chat-time-greeting"
>
{greeting?.title || timeGreeting}
{timeGreeting}
</h5>
<p
className="animate-blur-in-slow text-tint leading-tight [@container(min-height:400px)]:text-center"
className="animate-blur-in-slow text-tint [@container(min-height:400px)]:text-center"
style={{ animationDelay: '.6s' }}
>
{greeting?.subtitle ||
t(language, 'ai_chat_assistant_description')}
{t(language, 'ai_chat_assistant_description')}
</p>
</div>
</div>
@@ -278,10 +262,13 @@ export function AIChatBody(props: {
{chat.error ? <AIChatError chatController={chatController} /> : null}
<AIChatInput
value={input}
onChange={setInput}
loading={chat.loading}
disabled={chat.loading || chat.error}
onSubmit={(value) => {
chatController.postMessage({ message: value });
onSubmit={() => {
chatController.postMessage({ message: input });
setInput('');
}}
/>
</div>
@@ -1,3 +1,4 @@
import { tcls } from '@/lib/tailwind';
import { Icon, IconStyle } from '@gitbook/icons';
import type React from 'react';
@@ -20,18 +21,12 @@ export function AIChatIcon({
<Icon
icon="sparkle"
{...props}
className={className}
style={{
animation: {
intro: 'spin 2s forwards cubic-bezier(0.16,1,0.3,1)',
thinking: 'spin 2s infinite forwards cubic-bezier(0.16,1,0.3,1)',
working: 'spin 2s infinite forwards cubic-bezier(0.16,1,0.3,1)',
done: '',
confirm: '',
default: '',
error: '',
}[state],
}}
className={tcls(
className,
(state === 'thinking' || state === 'working') &&
'animate-[spin_2s_infinite_forwards_cubic-bezier(0.16,1,0.3,1)]',
state === 'intro' && 'animate-[spin_2s_forwards_cubic-bezier(0.16,1,0.3,1)]'
)}
/>
);
}
@@ -55,36 +50,28 @@ export function AIChatIcon({
<path
d="M12.8916 1.06265C12.921 0.979101 13.0392 0.979127 13.0685 1.06267C13.239 1.5478 13.3439 1.84646 13.516 2.1032C13.6683 2.33042 13.8578 2.53033 14.0766 2.6945C14.3239 2.88 14.6165 3.00068 15.0919 3.19671C15.1761 3.23142 15.1761 3.3506 15.0919 3.38531C14.6165 3.58134 14.3239 3.70203 14.0766 3.88752C13.8578 4.05169 13.6683 4.2516 13.516 4.47882C13.3439 4.73556 13.239 5.03423 13.0685 5.51937C13.0392 5.60291 12.921 5.60292 12.8916 5.51938C12.7212 5.03423 12.6162 4.73557 12.4442 4.47882C12.2919 4.2516 12.1023 4.05169 11.8835 3.88752C11.6363 3.70202 11.3436 3.58134 10.8682 3.38531C10.7841 3.3506 10.7841 3.23141 10.8683 3.1967C11.3436 3.00067 11.6363 2.87999 11.8835 2.6945C12.1023 2.53033 12.2919 2.33042 12.4442 2.1032C12.6162 1.84646 12.7212 1.54779 12.8916 1.06265Z"
stroke="currentColor"
strokeWidth={state === 'intro' ? '1.2' : '1'}
strokeWidth="1.2"
strokeLinejoin="round"
shapeRendering="crispEdges"
className="transition-opacity duration-300"
style={{
animation: {
intro: 'fadeIn .5s .5s backwards, spin 2s .5s forwards cubic-bezier(0.43,1.54,0.64,1)',
thinking: 'spin 2s 1s infinite forwards cubic-bezier(0.16,1,0.3,1)',
working: 'spin 2s 1s infinite forwards cubic-bezier(0.16,1,0.3,1)',
done: '',
confirm: '',
default: '',
error: '',
}[state],
transitionDelay:
state === 'default' || state === 'thinking' ? '.3s' : undefined,
opacity: ['done', 'confirm', 'error'].includes(state) ? 0 : 1,
transformOrigin: '13px 3.5px',
}}
className={tcls(
state === 'intro' &&
'animate-[fadeIn_.5s_.7s_both,spin_2s_1s_forwards_cubic-bezier(.43,1.54,.64,1)]',
(state === 'working' || state === 'thinking') &&
'animate-[fadeIn_.5s_.3s_both,spin_2s_1s_infinite_forwards_cubic-bezier(0.16,1,0.3,1)]',
state === 'done' && 'animate-[fadeOut_.5s_both]',
state === 'confirm' && 'animate-[fadeOut_.5s_both]',
state === 'default' && 'animate-[fadeIn_0s_both]',
state === 'error' && 'hidden'
)}
style={{ transformOrigin: '13px 3.5px' }}
/>
{/* Error */}
<g
clipPath="url(#clip0_153_2034)"
className="transition-opacity duration-300"
style={{
color: 'rgb(var(--danger-9))',
opacity: state === 'error' ? 1 : 0,
transitionDelay: state === 'error' ? '.3s' : undefined,
}}
className={tcls(
'text-danger-subtle',
state === 'error' ? 'animate-[fadeIn_.5s_.3s_both]' : 'hidden'
)}
>
<path
d="M13.0312 1.42059L13.0312 3.95184"
@@ -107,34 +94,29 @@ export function AIChatIcon({
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
className="transition-opacity duration-300"
style={{
opacity: state === 'done' ? 1 : 0,
transitionDelay: state === 'done' ? '.3s' : undefined,
}}
className={tcls(
state === 'done'
? 'animate-[fadeIn_.5s_.3s_both]'
: 'animate-[fadeOut_.5s_both]',
state === 'intro' && 'hidden',
state === 'confirm' && 'hidden'
)}
/>
{/* Confirm */}
<path
className="transition-opacity duration-300"
style={{
fill: 'rgb(var(--primary-9))',
opacity: state === 'confirm' ? 1 : 0,
transitionDelay: state === 'confirm' ? '.3s' : undefined,
animation: {
intro: '',
thinking: '',
working: '',
done: '',
confirm: 'bounceSmall 1s infinite both',
default: '',
error: '',
}[state],
}}
className={tcls(
'fill-primary-9',
state === 'confirm'
? 'animate-[fadeIn_.5s_.3s_both,bounceSmall_1s_infinite_both]'
: state === 'thinking'
? 'animate-[fadeOut_.5s_both]'
: 'hidden'
)}
d="M12.9463 5.24512C13.3688 5.24422 13.713 5.58625 13.7139 6.00879C13.7146 6.43114 13.3725 6.77338 12.9502 6.77441C12.5279 6.77505 12.1845 6.43408 12.1836 6.01172C12.1828 5.58953 12.5242 5.24649 12.9463 5.24512ZM13.0391 0.0751953C14.0688 0.0730893 14.9049 0.90586 14.9072 1.93555C14.9084 2.5063 14.6484 3.04679 14.2012 3.40137L13.7773 3.7373C13.6151 3.86604 13.5201 4.06239 13.5205 4.26953V4.30371C13.5211 4.62139 13.2639 4.879 12.9463 4.87988C12.6288 4.88032 12.3701 4.62417 12.3691 4.30664V4.27246C12.3679 3.71272 12.6238 3.18263 13.0625 2.83496L13.4854 2.49902C13.6565 2.36341 13.7562 2.1568 13.7559 1.93848C13.755 1.54463 13.4358 1.22503 13.042 1.22559H12.9385C12.488 1.22679 12.1225 1.59352 12.123 2.04395L12.124 2.21875C12.1245 2.53649 11.8676 2.79522 11.5498 2.7959C11.2321 2.79653 10.9746 2.53928 10.9736 2.22168L10.9727 2.04688C10.9706 0.960578 11.8493 0.0778178 12.9355 0.0751953H13.0391Z"
/>
{/* Background */}
{/* Background */}
<path
d="M3.5625 8.78512L7.26347 10.9219C7.88227 11.2791 8.64467 11.2791 9.26347 10.9219L14.25 8.0429C14.5833 7.85045 15 8.09101 15 8.47591V10.2777C15 10.4563 14.9047 10.6214 14.75 10.7107L9.26347 13.8784C8.64467 14.2356 7.88228 14.2356 7.26347 13.8784L3.5625 11.7416C2.70833 11.2978 1 9.93199 1 8.01949M1 8.01949C1 6.6448 1.84765 5.98698 2.62903 5.71701C3.15426 5.53555 3.71577 5.70568 4.19701 5.98353L7.26347 7.75395C7.88228 8.11122 8.64467 8.11122 9.26347 7.75395L10.9095 6.80362M1 8.01949C1 6.4945 2.03973 5.30731 2.5596 4.90434L7.37937 2.12165C7.79013 1.88449 8.26417 1.80476 8.71747 1.88245"
stroke="currentColor"
@@ -142,17 +124,7 @@ export function AIChatIcon({
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
style={{
animation: {
intro: 'fadeIn 2s forwards',
thinking: '',
working: '',
done: '',
confirm: '',
default: '',
error: '',
}[state],
}}
className={tcls(state === 'intro' && 'animate-[fadeIn_2s_forwards]')}
/>
{/* Logo */}
@@ -182,65 +154,45 @@ export function AIChatIcon({
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
style={{
animation: {
intro: 'pathEnter 1.5s both ease-out',
thinking: 'pathLoading 2s infinite both',
working: 'pathLoading 2s infinite both',
done: 'pathEnter 1s forwards ease',
confirm: '',
default: '',
error: '',
}[state],
}}
className={tcls(
(state === 'thinking' || state === 'working') &&
'animate-[pathLoading_2s_infinite_both]',
state === 'intro' && 'animate-[pathEnter_2s_both]',
state === 'done' && 'animate-[pathEnter_1s_forwards_ease]'
)}
/>
</g>
</svg>
);
}
interface AISearchIconProps extends React.SVGProps<SVGSVGElement> {
className?: string;
state?: 'default' | 'intro' | 'thinking' | 'working' | 'done' | 'error' | 'confirm';
}
export function AISearchIcon({ className = 'size-4', state = 'default' }: AISearchIconProps) {
export function AISearchIcon({
className = 'size-4',
state = 'default',
}: Pick<AIChatIconProps, 'className' | 'state'>) {
return (
<div
style={{
position: 'relative',
animation: {
intro: 'fadeIn 1s both, orbit 1s cubic-bezier(0.16,1,0.3,1)',
thinking:
'fadeIn 1s both, orbit 1s orbit 2s 1s infinite forwards linear ease-out,',
working: '',
done: '',
confirm: '',
default: '',
error: '',
}[state],
}}
className={tcls(
'relative',
state === 'intro' && 'animate-[fadeIn_1s_both,orbit_1s_cubic-bezier(0.16,1,0.3,1)]',
state === 'thinking' || state === 'working'
? 'animate-[fadeIn_1s_both,orbit_1s_ease-out,orbit_2s_1s_infinite_forwards_linear]'
: ''
)}
>
<Icon icon="search" className={className} />
<Icon
icon="sparkle"
iconStyle={IconStyle.Solid}
style={{
position: 'absolute',
top: '15.7%',
left: '15.6%',
width: '50%',
height: '50%',
animation: {
intro: 'spin 2s .5s forwards cubic-bezier(0.16,1,0.3,1)',
thinking: 'spin 2s infinite forwards cubic-bezier(0.16,1,0.3,1)',
working: 'spin 2s infinite forwards cubic-bezier(0.16,1,0.3,1)',
done: '',
confirm: '',
default: '',
error: '',
}[state],
}}
className={tcls(
'absolute top-[15.7%] left-[15.6%] size-[50%]',
state === 'thinking' || state === 'working'
? 'animate-[spin_2s_infinite_forwards_cubic-bezier(0.16,1,0.3,1)]'
: '',
state === 'intro'
? 'animate-[spin_2s_.5s_forwards_cubic-bezier(0.16,1,0.3,1)]'
: ''
)}
/>
</div>
);
@@ -1,26 +1,38 @@
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useAIChatState } from '../AI/useAIChat';
import { HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
import { Input } from '../primitives/Input';
import { Button, HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
export function AIChatInput(props: {
value: string;
disabled?: boolean;
/**
* When true, the input is disabled
*/
loading: boolean;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
}) {
const { onSubmit, disabled, loading } = props;
const { value, onChange, onSubmit, disabled, loading } = props;
const language = useLanguage();
const chat = useAIChatState();
const inputRef = useRef<HTMLTextAreaElement>(null);
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const textarea = event.currentTarget;
onChange(textarea.value);
// Auto-resize
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
};
useEffect(() => {
if (chat.opened && !disabled && !loading) {
// Add a small delay to ensure the input is rendered before focusing
@@ -45,33 +57,57 @@ export function AIChatInput(props: {
);
return (
<Input
data-testid="ai-chat-input"
name="ai-chat-input"
multiline
resize
sizing="large"
label="Assistant chat input"
placeholder={tString(language, 'ai_chat_input_placeholder')}
onSubmit={(val) => onSubmit(val as string)}
submitButton={{
label: tString(language, 'send'),
}}
className="animate-blur-in-slow bg-tint-base/9 backdrop-blur-lg contrast-more:bg-tint-base"
rows={1}
maxLength={2048}
keyboardShortcut={
!disabled && !loading
? {
keys: ['mod', 'i'],
className: 'bg-tint-base group-focus-within/input:hidden',
}
: undefined
}
disabled={disabled || loading}
aria-busy={loading}
ref={inputRef}
trailing={
<div className="depth-subtle:has-[textarea:focus]:-translate-y-px relative flex animate-blur-in-slow flex-col overflow-hidden circular-corners:rounded-3xl rounded-corners:rounded-xl bg-tint-base/9 depth-subtle:shadow-sm shadow-tint/6 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:shadow-primary-subtle has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base dark:shadow-tint-1">
<textarea
ref={inputRef}
disabled={disabled || loading}
data-loading={loading}
data-testid="ai-chat-input"
className={tcls(
'resize-none',
'focus:outline-hidden',
'focus:ring-0',
'w-full',
'px-3',
'py-3',
'pb-12',
'h-auto',
'bg-transparent',
'peer',
'max-h-64',
'placeholder:text-tint/8',
'transition-colors',
'disabled:bg-tint-subtle',
'delay-300',
'disabled:delay-0',
'disabled:cursor-not-allowed',
'data-[loading=true]:cursor-progress',
'data-[loading=true]:opacity-50'
)}
value={value}
rows={1}
placeholder={tString(language, 'ai_chat_input_placeholder')}
onChange={handleInput}
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.preventDefault();
event.currentTarget.blur();
return;
}
if (event.key === 'Enter' && !event.shiftKey && value.trim()) {
event.preventDefault();
event.currentTarget.style.height = 'auto';
onSubmit(value);
}
}}
/>
{!disabled ? (
<div className="absolute top-2.5 right-3 animate-[fadeIn_0.2s_0.5s_ease-in-out_both] peer-focus:hidden">
<KeyboardShortcut keys={['mod', 'i']} className="bg-tint-base" />
</div>
) : null}
<div className="absolute inset-x-0 bottom-0 flex items-center gap-2 px-2 py-2">
<HoverCardRoot openDelay={500}>
<HoverCard
className="max-w-xs bg-tint p-2 text-sm text-tint"
@@ -99,8 +135,7 @@ export function AIChatInput(props: {
</div>
</HoverCard>
<HoverCardTrigger>
{/* Negative margin to compensate for Input's padding, so the badge appears flush with the cursor */}
<div className="-ml-1 flex cursor-help items-center gap-1 circular-corners:rounded-2xl rounded-corners:rounded-md px-2.5 py-1.5 text-tint/7 text-xs transition-all hover:bg-tint">
<div className="flex cursor-help items-center gap-1 circular-corners:rounded-2xl rounded-corners:rounded-md px-2.5 py-1.5 text-tint/7 text-xs transition-all hover:bg-tint">
<span className="-ml-1 circular-corners:rounded-2xl rounded-corners:rounded-sm bg-tint-11/7 px-1 py-0.5 font-mono font-semibold text-[0.65rem] text-contrast-tint-11 leading-none">
{t(language, 'ai_chat_context_badge')}
</span>{' '}
@@ -111,7 +146,14 @@ export function AIChatInput(props: {
</div>
</HoverCardTrigger>
</HoverCardRoot>
}
/>
<Button
label={tString(language, 'send')}
size="medium"
className="ml-auto"
disabled={disabled || !value.trim()}
onClick={() => onSubmit(value)}
/>
</div>
</div>
);
}
@@ -7,16 +7,14 @@ export default function AIChatSuggestedQuestions(props: {
suggestions?: string[];
}) {
const language = useLanguage();
const { chatController, suggestions: _suggestions } = props;
const suggestions =
_suggestions && _suggestions.length > 0
? _suggestions
: [
tString(language, 'ai_chat_suggested_questions_about_this_page'),
tString(language, 'ai_chat_suggested_questions_read_next'),
tString(language, 'ai_chat_suggested_questions_example'),
];
const {
chatController,
suggestions = [
tString(language, 'ai_chat_suggested_questions_about_this_page'),
tString(language, 'ai_chat_suggested_questions_read_next'),
tString(language, 'ai_chat_suggested_questions_example'),
],
} = props;
return (
<div className="flex flex-col items-start gap-2 self-start">
@@ -35,68 +35,66 @@ export function AnnouncementBanner(props: {
data-nosnippet=""
>
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className={tcls(CONTAINER_STYLE)}>
<div className="relative">
<Tag
href={contentRef?.href ?? ''}
className={tcls(
'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl 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
iconOnly
icon="close"
label={tString(language, 'close')}
variant="blank"
size="default"
onClick={dismissAnnouncement}
className={`absolute top-0 right-0 mt-2 mr-2 circular-corners:rounded-lg rounded-sm straight-corners:rounded-none p-1.5 transition-all hover:ring-1 ${style.close}`}
/>
) : null}
</div>
<div className={tcls('relative', CONTAINER_STYLE)}>
<Tag
href={contentRef?.href ?? ''}
className={tcls(
'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl 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
iconOnly
icon="close"
label={tString(language, 'close')}
variant="blank"
size="default"
onClick={dismissAnnouncement}
className={`absolute top-0 right-4 mt-2 mr-2 circular-corners:rounded-lg rounded-sm straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
/>
) : null}
</div>
</div>
</div>
@@ -41,7 +41,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
aria-describedby={describedById}
className={tcls(
'fixed',
'z-50',
'z-10',
'bg-tint-base',
'rounded-sm',
'straight-corners:rounded-none',
@@ -52,9 +52,9 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'depth-flat:shadow-none',
'p-4',
'pr-8',
'bottom-[max(env(safe-area-inset-bottom),1rem)]',
'right-[max(env(safe-area-inset-right),1rem)]',
'left-[max(env(safe-area-inset-left),4rem)]',
'bottom-4',
'right-4',
'left-16',
'max-w-md',
'text-balance',
'sm:left-auto',
@@ -156,10 +156,8 @@
/** Text input */
.contentkit-textinput {
@apply w-full circular-corners:rounded-3xl ring-primary-hover rounded-corners:rounded-lg border border-tint text-tint-strong transition-all placeholder:text-tint/8 flex resize-none flex-1 px-2 py-1.5 text-sm bg-tint-base whitespace-pre-line;
@apply shadow-tint/6 depth-subtle:focus-within:-translate-y-px depth-subtle:shadow-sm depth-subtle:focus-within:shadow-lg dark:shadow-tint-1;
@apply focus:border-primary-hover focus:shadow-primary-subtle focus:ring-2 hover:border-tint-hover focus:hover:border-primary-hover;
@apply disabled:cursor-not-allowed disabled:border-tint-subtle disabled:bg-tint-subtle;
@apply w-full rounded border border-tint text-tint-strong placeholder:text-tint flex resize-none flex-1 px-2 py-1.5 text-sm bg-transparent whitespace-pre-line;
@apply focus:outline-primary focus:border-primary;
}
/** Form */
@@ -98,7 +98,6 @@ export function EmbeddableAIChat(props: EmbeddableAIChatProps) {
chatController={chatController}
chat={chat}
suggestions={configuration.suggestions}
greeting={configuration.greeting}
/>
</EmbeddableFrameBody>
</EmbeddableFrameMain>
@@ -30,9 +30,7 @@ type EmbeddableDocsPageProps = {
/**
* Page component for the embed docs page.
*/
export async function EmbeddableDocsPage(
props: EmbeddableDocsPageProps & { staticRoute: boolean }
) {
export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
const { context, pageParams } = props;
const { page, document, ancestors, withPageFeedback } = await getSitePageData({
context,
@@ -79,9 +77,9 @@ export async function EmbeddableDocsPage(
orientation="vertical"
className="not-hydrated:animate-blur-in-slow"
contentClassName="p-4"
leading={{ fade: !context.sections, button: true }}
fadeEdges={context.sections ? [] : ['leading']}
>
<TableOfContents context={context} />
<TableOfContents className="pt-0" context={context} />
<PageBody
context={context}
page={page}
@@ -89,7 +87,6 @@ export async function EmbeddableDocsPage(
document={document}
withPageFeedback={withPageFeedback}
insightsDisplayContext={SiteInsightsDisplayContext.Embed}
staticRoute={props.staticRoute}
/>
</ScrollContainer>
</EmbeddableFrameBody>
@@ -41,7 +41,6 @@ export async function EmbeddableRootLayout({
>
<AIContextProvider
aiMode={context.customization.ai.mode}
suggestions={context.customization.ai.suggestions}
trademark={context.customization.trademark.enabled}
>
<SpaceLayoutServerContext
@@ -58,7 +57,7 @@ export async function EmbeddableRootLayout({
{children}
{context.customization.trademark.enabled ? (
<TrademarkLink
className="border-tint-solid/3 border-t bg-tint-solid/1 px-4 py-2.5 text-tint/8 ring-0"
className="rounded-none border-tint-solid/3 border-t bg-tint-solid/1 px-4 py-2.5 text-tint/8 ring-0"
context={context}
placement={SiteInsightsTrademarkPlacement.Embed}
/>
@@ -42,7 +42,6 @@ export function Header(props: {
`h-[${HEADER_HEIGHT_DESKTOP}px]`,
'sticky',
'top-0',
'pt-[env(safe-area-inset-top)]',
'z-30',
'w-full',
'flex-none',
@@ -1,13 +1,16 @@
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useRef, useState } from 'react';
import { tString, useLanguage } from '@/intl/client';
import { useScrollListener } from '../hooks/useScrollListener';
import { Button, type ButtonProps } from '../primitives';
const globalClassName = 'navigation-open';
const SCROLL_DISTANCE = 320;
/**
* Button to show/hide the table of content on mobile.
*/
@@ -15,6 +18,25 @@ export function HeaderMobileMenu(props: ButtonProps) {
const language = useLanguage();
const pathname = usePathname();
const hasScrollRef = useRef(false);
const [isOpen, setIsOpen] = useState(false);
const toggleNavigation = () => {
if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) {
document.body.classList.remove(globalClassName);
setIsOpen(false);
} else {
document.body.classList.add(globalClassName);
window.scrollTo(0, 0);
setIsOpen(true);
}
};
const windowRef = useRef(typeof window === 'undefined' ? null : window);
useScrollListener(() => {
hasScrollRef.current = window.scrollY >= SCROLL_DISTANCE;
}, windowRef);
// Close the navigation when navigating to a page
useEffect(() => {
@@ -28,10 +50,8 @@ export function HeaderMobileMenu(props: ButtonProps) {
variant="blank"
size="default"
label={tString(language, 'table_of_contents_button_label')}
onClick={() => {
document.body.classList.toggle(globalClassName);
}}
// Since the button is hidden behind the TOC after toggling, we don't need to keep track of its active state.
onClick={toggleNavigation}
active={isOpen}
{...props}
/>
);
@@ -43,11 +43,6 @@ export function PageAside(props: {
'opacity-0',
'xl:flex',
'xl:page-no-toc:fixed',
'xl:page-no-toc:mr-[max(0px,calc((100vw-96rem)/2-20rem))]',
'xl:page-no-toc:right-8',
'xl:page-no-toc:bottom-0',
'z-20',
'overflow-hidden',
@@ -107,7 +102,7 @@ export function PageAside(props: {
className={tcls(
'flex flex-col',
'min-w-56 shrink-0',
'page-has-toc:overflow-hidden',
'overflow-hidden',
'w-full',
'xl:max-2xl:rounded-corners:page-api-block:rounded-md',
'xl:max-2xl:circular-corners:page-api-block:rounded-xl',
@@ -213,7 +208,7 @@ async function PageAsideFooter(props: { context: GitBookSiteContext }) {
<div
className={tcls(
'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 [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'bg-tint-base theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'border-tint-subtle xl:max-2xl:page-api-block:border-t xl:max-2xl:page-api-block:p-2',
'pt-4'
)}
@@ -1,20 +0,0 @@
import React from 'react';
/**
* Conditionally wrap children in a Suspense component based on SSR status.
* We need to do that because otherwise ISR pages will trigger the suspense boundary on every request,
* causing flickering and bad UX.
*/
export default function OptionalSuspense(props: {
staticRoute: boolean;
fallback: React.ReactNode;
children: React.ReactNode;
}) {
const { staticRoute, fallback, children } = props;
if (staticRoute) {
return <>{children}</>;
}
return <React.Suspense fallback={fallback}>{children}</React.Suspense>;
}
@@ -1,5 +1,6 @@
import type { GitBookSiteContext } from '@/lib/context';
import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
import React from 'react';
import { getSpaceLanguage } from '@/intl/server';
import { t } from '@/intl/translate';
@@ -11,7 +12,6 @@ import { TrackPageViewEvent } from '../Insights';
import { PageFeedbackForm } from '../PageFeedback';
import { CurrentPageProvider } from '../hooks/useCurrentPage';
import { DateRelative, SuspenseLoadedHint } from '../primitives';
import OptionalSuspense from './OptionalSuspense';
import { PageBodyBlankslate } from './PageBodyBlankslate';
import { PageCover } from './PageCover';
import { PageFooterNavigation } from './PageFooterNavigation';
@@ -27,17 +27,8 @@ export function PageBody(props: {
document: JSONDocument | null;
withPageFeedback: boolean;
insightsDisplayContext: SiteInsightsDisplayContext;
staticRoute: boolean;
}) {
const {
page,
context,
ancestors,
document,
withPageFeedback,
insightsDisplayContext,
staticRoute,
} = props;
const { page, context, ancestors, document, withPageFeedback, insightsDisplayContext } = props;
const { customization } = context;
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
@@ -74,7 +65,7 @@ export function PageBody(props: {
// Allow words to break if they are too long.
'break-anywhere',
'@container',
pageWidthWide ? 'page-width-wide max-w-[92rem]' : 'page-width-default',
pageWidthWide ? 'page-width-wide 3xl:px-8' : 'page-width-default',
siteWidthWide ? 'site-width-wide' : 'site-width-default',
page.layout.tableOfContents && hasVisibleTOCItems
? 'page-has-toc'
@@ -93,8 +84,7 @@ export function PageBody(props: {
withRSSFeed={contentHasUpdates}
/>
{document && !isNodeEmpty(document) ? (
<OptionalSuspense
staticRoute={staticRoute}
<React.Suspense
fallback={
<DocumentViewSkeleton
document={document}
@@ -116,7 +106,7 @@ export function PageBody(props: {
withLinkPreviews,
}}
/>
</OptionalSuspense>
</React.Suspense>
) : (
<PageBodyBlankslate page={page} context={context} />
)}
@@ -46,7 +46,7 @@ export async function PageBodyBlankslate(props: {
<Card
key={child.id}
leadingIcon={icon}
title={child.linkTitle || child.title}
title={child.title}
href={resolved.href}
insights={{
type: 'link_click',
@@ -62,14 +62,7 @@ export async function PageBodyBlankslate(props: {
pages: context.revision.pages,
page: child,
});
return (
<Card
key={child.id}
title={child.linkTitle || child.title}
leadingIcon={icon}
href={href}
/>
);
return <Card key={child.id} title={child.title} leadingIcon={icon} href={href} />;
})
);
@@ -43,7 +43,7 @@ export async function PageFooterNavigation(props: {
<NavigationCard
icon="chevron-left"
label={t(language, 'previous_page')}
title={previous.linkTitle || previous.title}
title={previous.title}
href={previousHref}
insights={{
type: 'link_click',
@@ -62,7 +62,7 @@ export async function PageFooterNavigation(props: {
<NavigationCard
icon="chevron-right"
label={t(language, 'next_page')}
title={next.linkTitle || next.title}
title={next.title}
href={nextHref}
insights={{
type: 'link_click',
@@ -6,10 +6,10 @@ import React, { type ButtonHTMLAttributes } from 'react';
import { useLanguage } from '@/intl/client';
import { t, tString } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
import { useTrackEvent } from '../Insights';
import { Button, ButtonGroup, Input } from '../primitives';
const MIN_COMMENT_LENGTH = 3;
import { useTrackEvent } from '../Insights';
import { Button, ButtonGroup } from '../primitives';
const MAX_COMMENT_LENGTH = 512;
/**
@@ -24,6 +24,7 @@ export function PageFeedbackForm(props: {
const trackEvent = useTrackEvent();
const inputRef = React.useRef<HTMLTextAreaElement>(null);
const [rating, setRating] = React.useState<PageFeedbackRating>();
const [comment, setComment] = React.useState('');
const [submitted, setSubmitted] = React.useState(false);
const onSubmitRating = (rating: PageFeedbackRating) => {
@@ -85,21 +86,43 @@ export function PageFeedbackForm(props: {
</ButtonGroup>
</div>
{rating ? (
<Input
ref={inputRef}
label={tString(languages, 'was_this_helpful_comment')}
multiline
submitButton
rows={3}
name="page-feedback-comment"
onSubmit={(comment) => onSubmitComment(rating, comment as string)}
maxLength={MAX_COMMENT_LENGTH}
minLength={MIN_COMMENT_LENGTH}
disabled={submitted}
submitMessage={tString(languages, 'was_this_helpful_thank_you')}
className="animate-blur-in"
resize
/>
<div className="flex flex-col gap-2">
{!submitted ? (
<>
<textarea
ref={inputRef}
name="comment"
className="mx-0.5 max-h-40 min-h-16 grow rounded-sm straight-corners:rounded-none bg-tint-base p-2 ring-1 ring-tint ring-inset placeholder:text-sm placeholder:text-tint contrast-more:ring-tint-12 contrast-more:placeholder:text-tint-strong"
placeholder={tString(languages, 'was_this_helpful_comment')}
aria-label={tString(languages, 'was_this_helpful_comment')}
onChange={(e) => setComment(e.target.value)}
value={comment}
rows={3}
maxLength={MAX_COMMENT_LENGTH}
/>
<div className="flex items-center justify-between gap-4">
<Button
size="small"
onClick={() => onSubmitComment(rating, comment)}
label={tString(languages, 'submit')}
/>
{comment.length > MAX_COMMENT_LENGTH * 0.8 ? (
<span
className={
comment.length === MAX_COMMENT_LENGTH
? 'text-red-500'
: ''
}
>
{comment.length} / {MAX_COMMENT_LENGTH}
</span>
) : null}
</div>
</>
) : (
<p>{t(languages, 'was_this_helpful_thank_you')}</p>
)}
</div>
) : null}
</div>
);
@@ -57,15 +57,13 @@ function preloadFont(fontData: FontData) {
* It takes care of setting the theme and the language.
*/
export async function CustomizationRootLayout(props: {
/** The class name to apply to the html element. */
htmlClassName?: string;
/** The class name to apply to the body element. */
bodyClassName?: string;
className?: string;
forcedTheme?: CustomizationThemeMode | null;
context: GitBookAnyContext;
children: React.ReactNode;
}) {
const { htmlClassName, bodyClassName, context, forcedTheme, children } = props;
const { className, context, forcedTheme, children } = props;
const customization =
'customization' in context ? context.customization : defaultCustomization();
@@ -109,8 +107,7 @@ export async function CustomizationRootLayout(props: {
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
? 'dark'
: '',
htmlClassName
: ''
)}
>
<head>
@@ -182,7 +179,7 @@ export async function CustomizationRootLayout(props: {
}
`}</style>
</head>
<body className={tcls(bodyClassName, 'sheet-open:overflow-hidden')}>
<body className={className}>
<IconsProvider
assetsURL={GITBOOK_ICONS_URL}
assetsURLToken={GITBOOK_ICONS_TOKEN}
@@ -58,7 +58,7 @@ export function SearchContainer({
viewport,
siteSpaces,
}: SearchContainerProps) {
const { assistants, config } = useAI();
const { assistants } = useAI();
const [state, setSearchState] = useSearch();
const searchAsk = useSearchAskState();
@@ -200,8 +200,7 @@ export function SearchContainer({
siteSpaceId: siteSpace.id,
siteSpaceIds,
scope: state?.scope ?? 'default',
withAI,
suggestions: config.suggestions,
withAI: withAI,
});
const searchValue = state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? '';
@@ -1,9 +1,13 @@
'use client';
import React, { useEffect, useRef } from 'react';
import React from 'react';
import { useEffect, useRef } from 'react';
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import { Input } from '../primitives';
import { Button, variantClasses } from '../primitives';
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
import { useClassnames } from '../primitives/StyleProvider';
interface SearchInputProps {
onChange: (value: string) => void;
@@ -16,11 +20,14 @@ interface SearchInputProps {
children?: React.ReactNode;
}
// Size classes for medium size button
const sizeClasses = ['text-sm', 'px-3.5', 'py-1.5', '@2xl:circular-corners:px-4'];
/**
* Input to trigger search.
*/
export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
function SearchInput(props, containerRef) {
function SearchInput(props, ref) {
const {
onChange,
onKeyDown,
@@ -35,6 +42,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
const inputRef = useRef<HTMLInputElement>(null);
const language = useLanguage();
const buttonStyles = useClassnames(['ButtonStyles']);
useEffect(() => {
if (isOpen) {
@@ -50,45 +58,74 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
}, [isOpen, value]);
return (
<div className="relative flex @max-2xl:size-9.5 grow">
<Input
data-testid="search-input"
name="search-input"
ref={inputRef}
containerRef={containerRef as React.RefObject<HTMLDivElement | null>}
sizing="medium"
label={tString(language, withAI ? 'search_or_ask' : 'search')}
className="@max-2xl:absolute inset-y-0 right-0 z-30 @max-2xl:max-w-9.5 grow theme-bold:border-header-link/4 theme-bold:bg-header-link/1 @max-2xl:px-2.5 theme-bold:text-header-link theme-bold:shadow-none! theme-bold:backdrop-blur-xl @max-2xl:focus-within:w-56 @max-2xl:focus-within:max-w-[calc(100vw-5rem)] theme-bold:focus-within:border-header-link/6 theme-bold:focus-within:ring-header-link/5 theme-bold:hover:border-header-link/5 @max-2xl:has-[input[aria-expanded=true]]:w-56 @max-2xl:has-[input[aria-expanded=true]]:max-w-[calc(100vw-5rem)] @max-2xl:[&_input]:opacity-0 theme-bold:[&_input]:placeholder:text-header-link/8 @max-2xl:focus-within:[&_input]:opacity-11 @max-2xl:has-[input[aria-expanded=true]]:[&_input]:opacity-11"
placeholder={`${tString(language, withAI ? 'search_or_ask' : 'search')}`}
onFocus={onFocus}
onKeyDown={onKeyDown}
leading={
<Icon
icon="search"
className="size-4 shrink-0 text-tint theme-bold:text-header-link/8"
<div className={tcls('relative flex size-9 grow', className)}>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: this div needs an onClick to show the input on mobile, where it's normally hidden.
Normally you'd also need to add a keyboard trigger to do the same without a pointer, but in this case the input already be focused on its own. */}
<div
ref={ref}
onClick={onFocus}
className={tcls(
// Apply button styles
buttonStyles,
variantClasses.header,
sizeClasses,
// Additional custom styles
'has-[input:focus]:-translate-y-px h-9 grow @2xl:cursor-text cursor-pointer px-2.5 has-[input:focus]:bg-tint-base has-[input:focus]:depth-subtle:shadow-lg has-[input:focus]:depth-subtle:shadow-primary-subtle has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-primary-hover',
'theme-bold:border-header-link/3 has-[input:focus-visible]:theme-bold:border-header-link/5 has-[input:focus-visible]:theme-bold:bg-header-link/3 has-[input:focus-visible]:theme-bold:ring-header-link/5',
'theme-bold:before:absolute theme-bold:before:inset-0 theme-bold:before:bg-header-background/7 theme-bold:before:backdrop-blur-xl ', // Special overlay to make the transparent colors of theme-bold visible.
'@max-2xl:absolute relative @max-2xl:right-0 z-30 max-w-none shrink grow justify-start',
isOpen ? '@max-2xl:w-56' : '@max-2xl:w-[38px]'
)}
>
{value && isOpen ? (
<Button
variant="blank"
label={tString(language, 'clear')}
size="medium"
iconOnly
icon="circle-xmark"
className="-ml-1.5 -mr-1 animate-scale-in px-1.5 theme-bold:text-header-link theme-bold:hover:bg-header-link/3"
onClick={() => {
onChange('');
inputRef.current?.focus();
}}
/>
}
onChange={(event) => {
onChange(event.target.value);
}}
value={value}
maxLength={512}
autoComplete="off"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded={value && isOpen ? 'true' : 'false'}
clearButton={{
className:
'theme-bold:text-header-link theme-bold:hover:bg-header-link/3 text-base',
}}
keyboardShortcut={{
className:
'theme-bold:border-header-link/4 theme-bold:bg-header-background theme-bold:text-header-link',
keys: isOpen ? ['esc'] : ['mod', 'k'],
}}
{...rest}
type="text"
/>
) : (
<Icon
icon="magnifying-glass"
className="size-4 shrink-0 animate-scale-in"
/>
)}
{children}
<input
{...rest}
type="text"
onFocus={onFocus}
onKeyDown={onKeyDown}
onChange={(event) => onChange(event.target.value)}
value={value}
// We only show "search or ask" if the search input actually handles both search and ask.
placeholder={`${tString(language, withAI ? 'search_or_ask' : 'search')}`}
maxLength={512}
size={10}
data-testid="search-input"
className={tcls(
'peer z-10 min-w-0 grow bg-transparent py-0.5 text-tint-strong theme-bold:text-header-link outline-hidden transition-[width] duration-300 contain-paint placeholder:text-tint theme-bold:placeholder:text-current theme-bold:placeholder:opacity-7',
isOpen ? '' : '@max-2xl:opacity-0'
)}
role="combobox"
autoComplete="off"
aria-autocomplete="list"
aria-haspopup="listbox"
aria-expanded={value && isOpen ? 'true' : 'false'}
// Forward
ref={inputRef}
/>
<KeyboardShortcut
keys={isOpen ? ['esc'] : ['mod', 'k']}
className="last:-mr-1 theme-bold:border-header-link/5 theme-bold:bg-header-background theme-bold:text-header-link"
/>
</div>
</div>
);
}
@@ -35,9 +35,8 @@ export function useSearchResults(props: {
siteSpaceIds: string[];
scope: SearchScope;
withAI: boolean;
suggestions?: string[];
}) {
const { disabled, query, siteSpaceId, siteSpaceIds, scope, suggestions } = props;
const { disabled, query, siteSpaceId, siteSpaceIds, scope } = props;
const trackEvent = useTrackEvent();
@@ -79,22 +78,6 @@ export function useSearchResults(props: {
const questions = new Set<string>();
const recommendedQuestions: ResultType[] = [];
if (suggestions && suggestions.length > 0) {
suggestions.forEach((question) => {
questions.add(question);
});
setResultsState({
results: suggestions.map((question, index) => ({
type: 'recommended-question',
id: `recommended-question-${index}`,
question,
})),
fetching: false,
error: false,
});
return;
}
const timeout = setTimeout(async () => {
if (cancelled) {
return;
@@ -173,11 +156,7 @@ export function useSearchResults(props: {
return;
}
const aiEnrichedResults = withAI
? withAskTriggers(results, query, assistants)
: results;
setResultsState({ results: aiEnrichedResults, fetching: false, error: false });
setResultsState({ results, fetching: false, error: false });
trackEvent({
type: 'search_type_query',
@@ -196,9 +175,16 @@ export function useSearchResults(props: {
cancelled = true;
clearTimeout(timeout);
};
}, [query, scope, trackEvent, withAI, siteSpaceId, siteSpaceIds, disabled, suggestions]);
}, [query, scope, trackEvent, withAI, siteSpaceId, siteSpaceIds, disabled]);
return resultsState;
const aiEnrichedResults: ResultType[] = React.useMemo(() => {
if (!withAI) {
return resultsState.results;
}
return withAskTriggers(resultsState.results, query, assistants);
}, [resultsState.results, query, withAI]);
return { ...resultsState, results: aiEnrichedResults };
}
/**
@@ -56,7 +56,6 @@ export async function SiteLayout(props: {
>
<AIContextProvider
aiMode={customization.ai?.mode}
suggestions={context.customization.ai?.suggestions}
trademark={customization.trademark.enabled}
>
<SpaceLayout
@@ -101,7 +100,6 @@ export async function generateSiteLayoutViewport(context: GitBookSiteContext): P
width: 'device-width',
initialScale: 1,
maximumScale: 1,
viewportFit: 'cover',
};
}
@@ -53,7 +53,7 @@ export type PageMetaLinks = {
/**
* Fetch and render a page.
*/
export async function SitePage(props: SitePageProps & { staticRoute: boolean }) {
export async function SitePage(props: SitePageProps) {
const {
context,
page,
@@ -99,7 +99,6 @@ export async function SitePage(props: SitePageProps & { staticRoute: boolean })
document={document}
withPageFeedback={withPageFeedback}
insightsDisplayContext={SiteInsightsDisplayContext.Site}
staticRoute={props.staticRoute}
/>
</div>
<PageClientLayout pageMetaLinks={pageMetaLinks} />
@@ -297,7 +296,7 @@ async function getPageDataWithFallback(args: {
}) {
const { context: baseContext, pagePathParams } = args;
const { context, pageTarget } = await fetchPageData(baseContext, pagePathParams);
const pageMetaLinks = await (pageTarget?.page
const pageMetaLinks = await (pageTarget?.page && shouldResolveMetaLinks(context.site.id)
? resolvePageMetaLinks(context, pageTarget.page.id)
: null);
@@ -359,6 +358,29 @@ async function resolvePageMetaLinks(
};
}
/**
* Determine whether to resolve meta links for a site based on a percentage rollout.
*/
function shouldResolveMetaLinks(siteId: string): boolean {
const META_LINKS_PERCENTAGE_ROLLOUT = 25;
const ALLOWED_SITES: Record<string, boolean> = {
site_CZrtk: true,
};
if (ALLOWED_SITES[siteId] || process.env.NODE_ENV === 'development') {
return true;
}
// compute a simple hash of the siteId
let hash = 0;
for (let i = 0; i < siteId.length; i++) {
hash = (hash << 5) - hash + siteId.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash % 100) < META_LINKS_PERCENTAGE_ROLLOUT;
}
/**
* Get the <title> for a page.
*/
@@ -40,7 +40,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
orientation="vertical"
style={{ maxHeight: `${MAX_ITEMS * 3 + 2}rem` }}
className="pb-4"
active={`#${currentSection.id}`}
active={currentSection.id}
>
<div className="flex w-full flex-col px-2">
{sectionsAndGroups.map((item) => {
@@ -18,7 +18,7 @@ import type {
} from './encodeClientSiteSections';
const SCREEN_OFFSET = 16; // 1rem
const MAX_ITEMS_PER_COLUMN = 8; // number of items per column
const MAX_ITEMS_PER_COLUMN = 5; // number of items per column
/**
* A set of navigational links representing site sections for multi-section sites
*/
@@ -76,12 +76,8 @@ export function SiteSectionTabs(props: {
? 'md:-mr-8 -mr-4 sm:-mr-6'
: 'after:contents[] after:absolute after:inset-y-2 after:right-0 after:border-transparent after:border-r after:transition-colors'
)}
active={`#${currentSection.id}`}
trailing={{
fade: true,
button: true,
className: children ? 'after:border-tint' : '',
}}
active={currentSection.id}
trailingEdgeScrollClassName={children ? 'after:border-tint' : ''}
>
<NavigationMenu.List
className={tcls(
@@ -227,7 +223,7 @@ function SectionGroupTileList(props: {
{hasSections && (
<ul
className={tcls(
'flex min-w-48 grid-flow-row flex-col gap-x-2 gap-y-1 self-start p-3 md:grid',
'flex grid-flow-row flex-col gap-x-2 gap-y-1 p-2 md:grid',
hasGroups ? 'bg-tint-base' : ''
)}
style={{
@@ -248,7 +244,7 @@ function SectionGroupTileList(props: {
{hasGroups && (
<ul
className={tcls(
'flex min-w-48 flex-col items-start justify-start gap-x-8 gap-y-2 p-3 md:flex-row md:gap-y-8',
'flex grid-flow-col flex-col items-start gap-x-2 gap-y-4 p-2 md:grid md:gap-y-1',
hasSections
? 'border-tint-subtle bg-tint-subtle max-md:border-t md:border-l'
: ''
@@ -280,7 +276,7 @@ function SectionGroupTile(props: {
const { url, icon, title, description } = child;
const isActive = child.id === currentSection.id;
return (
<li className="group/section-tile flex shrink-0 grow md:max-w-68">
<li className="group/section-tile flex w-full shrink grow md:w-68">
<Link
href={url}
className={tcls(
@@ -321,7 +317,7 @@ function SectionGroupTile(props: {
const { title, icon, children } = child;
return (
<li className="flex shrink-0 flex-col gap-1">
<li className="flex w-full shrink grow flex-col gap-1">
<div className="mt-3 mb-2 flex gap-2.5 px-3 font-semibold text-tint-subtle text-xs uppercase tracking-wider">
{icon && (
<SectionIcon className="mt-0.5" isActive={false} icon={icon as IconName} />
@@ -331,7 +327,7 @@ function SectionGroupTile(props: {
<ul
className="flex grid-flow-row flex-col gap-x-2 gap-y-1 md:grid"
style={{
gridTemplateColumns: `repeat(${Math.ceil(children.length / MAX_ITEMS_PER_COLUMN)}, minmax(0, auto))`,
gridTemplateColumns: `repeat(${Math.ceil(children.length / MAX_ITEMS_PER_COLUMN)}, minmax(0, 1fr))`,
}}
>
{children.map((nestedChild) => (
@@ -128,7 +128,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
'lg:justify-center',
CONTAINER_STYLE,
'site-width-wide:max-w-screen-4xl',
'transition-[max-width] duration-300',
'hydrated:transition-[max-width] duration-300',
// Ensure the footer is display below the viewport even if the content is not enough
withFooter && [
@@ -141,82 +141,79 @@ export function SpaceLayout(props: SpaceLayoutProps) {
<TableOfContents
context={context}
header={
<div
className={tcls(
'pr-4',
'flex',
withTopHeader ? 'lg:hidden' : '',
'grow-0',
'dark:shadow-light/1',
'text-base/tight',
'items-center'
)}
>
<HeaderLogo context={context} />
{variants.translations.length > 1 ? (
<TranslationsDropdown
context={context}
siteSpace={
variants.translations.find(
(space) => space.id === siteSpace.id
) ?? siteSpace
}
siteSpaces={variants.translations}
className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden"
/>
) : null}
</div>
withTopHeader ? null : (
<div
className={tcls(
'hidden',
'pr-4',
'mt-2',
'lg:flex',
'grow-0',
'dark:shadow-light/1',
'text-base/tight',
'items-center'
)}
>
<HeaderLogo context={context} />
{variants.translations.length > 1 ? (
<TranslationsDropdown
context={context}
siteSpace={
variants.translations.find(
(space) => space.id === siteSpace.id
) ?? siteSpace
}
siteSpaces={variants.translations}
className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden"
/>
) : null}
</div>
)
}
// Displays the search button and/or the space dropdown in the ToC
// according to the header/variant settings.
// E.g if there is no header, the search button will be displayed in the ToC.
innerHeader={
!withTopHeader || variants.generic.length > 1 ? (
<div
className={tcls(
'my-5 sidebar-default:mt-2 flex flex-col gap-2 px-5 empty:hidden',
variants.generic.length > 1 ? '' : 'max-lg:hidden'
)}
>
{!withTopHeader && (
<div className="flex gap-2 max-lg:hidden">
<SearchContainer
style={CustomizationSearchStyle.Subtle}
withVariants={variants.generic.length > 1}
withSiteVariants={
visibleSections?.list.some(
(s) =>
s.object === 'site-section' &&
s.siteSpaces.length > 1
) ?? false
}
withSections={withSections}
section={visibleSections?.current}
siteSpace={siteSpace}
siteSpaces={visibleSiteSpaces}
viewport="desktop"
/>
</div>
)}
{!withTopHeader && withSections && visibleSections && (
<SiteSectionList
className="hidden lg:block"
sections={encodeClientSiteSections(
context,
visibleSections
)}
/>
)}
{variants.generic.length > 1 ? (
<SpacesDropdown
context={context}
<>
{!withTopHeader && (
<div className="flex gap-2">
<SearchContainer
style={CustomizationSearchStyle.Subtle}
withVariants={variants.generic.length > 1}
withSiteVariants={
visibleSections?.list.some(
(s) =>
s.object === 'site-section' &&
s.siteSpaces.length > 1
) ?? false
}
withSections={withSections}
section={visibleSections?.current}
siteSpace={siteSpace}
siteSpaces={variants.generic}
className="w-full px-3"
siteSpaces={visibleSiteSpaces}
className="max-lg:hidden"
viewport="desktop"
/>
) : null}
</div>
) : null
</div>
)}
{!withTopHeader && withSections && visibleSections && (
<SiteSectionList
className={tcls('hidden', 'lg:block')}
sections={encodeClientSiteSections(
context,
visibleSections
)}
/>
)}
{variants.generic.length > 1 ? (
<SpacesDropdown
context={context}
siteSpace={siteSpace}
siteSpaces={variants.generic}
className="w-full px-3 py-2"
/>
) : null}
</>
}
/>
{children}
@@ -32,8 +32,7 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
'my-2',
'border-tint-subtle',
'sidebar-list-default:border-l',
'sidebar-list-line:border-l',
'break-anywhere'
'sidebar-list-line:border-l'
)}
/>
) : null
@@ -14,9 +14,9 @@ export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boole
<li className="flex flex-col">
<div
className={tcls(
'-top-4 sticky z-1 flex items-center gap-3 px-3',
'-top-6 sticky z-1 flex items-center gap-3 px-3 pt-6',
'font-semibold text-xs uppercase tracking-wide',
'mt-2 pt-4 pb-3', // Add extra padding to make the header fade a bit nicer
'pb-3', // Add extra padding to make the header fade a bit nicer
'-mb-1.5', // Then pull the page items a bit closer, effective bottom padding is 1.5 units / 6px.
'mask-[linear-gradient(rgba(0,0,0,1)_70%,rgba(0,0,0,0))]', // Fade out effect of fixed page items. We want the fade to start past the header, this is a good approximation.
'bg-tint-base',
@@ -25,9 +25,9 @@ export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boole
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
'lg:[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
'lg:[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint',
isFirst ? '-mt-4' : ''
'[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
'[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint',
isFirst ? '-mt-6' : ''
)}
>
<TOCPageIcon page={page} />
@@ -0,0 +1,91 @@
'use client';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
type ComponentPropsWithoutRef,
} from 'react';
import { assert } from 'ts-essentials';
interface TOCScrollContainerContextType {
getContainer: (listener: (element: HTMLDivElement) => void) => () => void;
}
const TOCScrollContainerContext = React.createContext<TOCScrollContainerContextType | null>(null);
function useTOCScrollContainerContext() {
const ctx = React.useContext(TOCScrollContainerContext);
assert(ctx);
return ctx;
}
/**
* Table of contents scroll container.
*/
export function TOCScrollContainer(props: ComponentPropsWithoutRef<'div'>) {
const ref = useRef<HTMLDivElement>(null);
const listeners = useRef<((element: HTMLDivElement) => void)[]>([]);
const getContainer: TOCScrollContainerContextType['getContainer'] = useCallback((listener) => {
if (ref.current) {
listener(ref.current);
return () => {};
}
listeners.current.push(listener);
return () => {
listeners.current = listeners.current.filter((l) => l !== listener);
};
}, []);
const value: TOCScrollContainerContextType = useMemo(() => ({ getContainer }), [getContainer]);
useEffect(() => {
const element = ref.current;
if (!element) {
return;
}
listeners.current.forEach((listener) => listener(element));
return () => {
listeners.current = [];
};
}, []);
return (
<TOCScrollContainerContext.Provider value={value}>
<div ref={ref} data-testid="toc-scroll-container" {...props} />
</TOCScrollContainerContext.Provider>
);
}
// Offset to scroll the table of contents item by.
const TOC_ITEM_OFFSET = 100;
/**
* Scrolls the table of contents container to the page item when it's initially active.
*/
export function useScrollToActiveTOCItem(props: {
anchorRef: React.RefObject<HTMLAnchorElement | null>;
isActive: boolean;
}) {
const { isActive, anchorRef } = props;
const { getContainer } = useTOCScrollContainerContext();
useEffect(() => {
const anchor = anchorRef.current;
if (isActive && anchor) {
return getContainer((container) => {
if (isOutOfView(anchor, container)) {
container.scrollTo({ top: anchor.offsetTop - TOC_ITEM_OFFSET });
}
});
}
}, [isActive, getContainer, anchorRef]);
}
function isOutOfView(element: HTMLElement, container: HTMLElement) {
const tocItemTop = element.offsetTop;
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.clientHeight;
return (
tocItemTop < containerTop + TOC_ITEM_OFFSET ||
tocItemTop > containerBottom - TOC_ITEM_OFFSET
);
}
@@ -3,16 +3,12 @@ import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
import type React from 'react';
import { tcls } from '@/lib/tailwind';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { SideSheet } from '../primitives/SideSheet';
import { PagesList } from './PagesList';
import { TOCScrollContainer } from './TOCScroller';
import { TableOfContentsScript } from './TableOfContentsScript';
import { Trademark } from './Trademark';
import { encodeClientTableOfContents } from './encodeClientTableOfContents';
/**
* Sidebar container, responsible for setting the right dimensions and position for the sidebar.
*/
export async function TableOfContents(props: {
context: GitBookSiteContext;
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
@@ -26,38 +22,23 @@ export async function TableOfContents(props: {
return (
<>
<SideSheet
side="left"
<aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
data-testid="table-of-contents"
id="table-of-contents"
toggleClass="navigation-open"
withScrim={true}
withCloseButton={true}
className={tcls(
'group/table-of-contents',
'group',
'text-sm',
'grow-0',
'shrink-0',
'basis-full',
'lg:basis-72',
'lg:page-no-toc:basis-56',
'w-72',
'lg:max-xl:w-56',
'lg:page-no-toc:w-56',
'4xl:page-no-toc:w-72',
'lg:page-no-toc:fixed',
'lg:page-no-toc:left-8',
'lg:page-no-toc:ml-[max(0px,calc((100vw-96rem)/2-20rem))]',
'max-lg:not-sidebar-filled:bg-tint-base',
'max-lg:not-sidebar-filled:border-r',
'border-tint-subtle',
'lg:flex!',
'lg:animate-none!',
'relative',
'z-1',
'lg:sticky',
'lg:mr-12',
// 'lg:z-0!',
'lg:z-20',
// Server-side static positioning
'lg:top-0',
@@ -79,23 +60,30 @@ export async function TableOfContents(props: {
'lg:page-no-toc:[html[style*="--outline-top-offset"]_&]:top-(--outline-top-offset)!',
'lg:page-no-toc:[html[style*="--outline-height"]_&]:top-(--outline-height)!',
'pt-6 pb-4',
'pt-4',
'pb-4',
'lg:sidebar-filled:pr-6',
'lg:page-no-toc:pr-0',
'hidden',
'navigation-open:flex!',
'lg:flex',
'lg:page-no-toc:hidden',
'xl:page-no-toc:flex',
'lg:site-header-none:page-no-toc:flex',
'flex-col',
'gap-4',
'navigation-open:border-b',
'border-tint-subtle',
className
)}
>
{header ? header : null}
{header && header}
<div // The actual sidebar, either shown with a filled bg or transparent.
className={tcls(
'-ms-5',
'relative flex min-h-0 grow flex-col border-tint-subtle',
'lg:-ms-5',
'relative flex grow flex-col overflow-hidden border-tint-subtle',
'sidebar-filled:bg-tint-subtle',
'theme-muted:bg-tint-subtle',
@@ -103,7 +91,6 @@ export async function TableOfContents(props: {
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
'[html.sidebar-filled.theme-gradient_&]:border',
'max-lg:sidebar-filled:border',
'page-no-toc:bg-transparent!',
'page-no-toc:border-none!',
@@ -112,33 +99,32 @@ export async function TableOfContents(props: {
'page-has-toc:[html.sidebar-filled.circular-corners_&]:rounded-3xl'
)}
>
{innerHeader ? innerHeader : null}
<ScrollContainer
data-testid="toc-scroll-container"
orientation="vertical"
contentClassName="flex flex-col p-2 gutter-stable"
active="[data-active=true]"
leading={{
fade: true,
button: {
className: '-mt-4',
},
}}
{innerHeader ? (
<div className="my-4 flex flex-col space-y-4 px-5 empty:hidden">
{innerHeader}
</div>
) : null}
<TOCScrollContainer // The scrollview inside the sidebar
className={tcls(
'flex grow flex-col p-2 pt-4',
customization.trademark.enabled && 'lg:pb-20',
'hide-scrollbar overflow-y-auto'
)}
>
<PagesList
pages={pages}
isRoot={true}
style="page-no-toc:hidden grow border-tint-subtle sidebar-list-line:border-l"
style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
/>
</ScrollContainer>
{customization.trademark.enabled ? (
<Trademark
context={context}
placement={SiteInsightsTrademarkPlacement.Sidebar}
/>
) : null}
{customization.trademark.enabled ? (
<Trademark
context={context}
placement={SiteInsightsTrademarkPlacement.Sidebar}
/>
) : null}
</TOCScrollContainer>
</div>
</SideSheet>
</aside>
<TableOfContentsScript />
</>
);
@@ -8,6 +8,7 @@ import { tcls } from '@/lib/tailwind';
import { useCurrentPagePath } from '../hooks';
import { Link, type LinkInsightsProps, type LinkProps } from '../primitives';
import { useScrollToActiveTOCItem } from './TOCScroller';
/**
* Client component for a page document to toggle its children and be marked as active.
@@ -80,6 +81,8 @@ function LinkItem(
}
) {
const { isActive, href, insights, children, onActiveClick } = props;
const anchorRef = useRef<HTMLAnchorElement>(null);
useScrollToActiveTOCItem({ anchorRef, isActive });
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
if (isActive && onActiveClick) {
@@ -90,7 +93,7 @@ function LinkItem(
return (
<Link
data-active={isActive}
ref={anchorRef}
href={href}
insights={insights}
aria-current={isActive ? 'page' : undefined}
@@ -19,7 +19,46 @@ export function Trademark(props: {
return (
<div
className={tcls(
'mt-auto circular-corners:rounded-2xl rounded-lg straight-corners:rounded-none px-2 sidebar-default:pr-4 pb-2',
'relative',
'z-2',
'lg:absolute',
'left-0',
'right-2',
'bottom-0',
'pointer-events-none',
'sidebar-filled:pl-2',
'sidebar-filled:pb-2',
'sidebar-filled:page-no-toc:p-0',
'bg-tint-base',
'sidebar-filled:bg-tint-subtle',
'theme-muted:bg-tint-subtle',
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
'rounded-lg',
'straight-corners:rounded-none',
'circular-corners:rounded-2xl',
'before:hidden',
'lg:before:block',
'before:content-[""]',
'before:absolute',
'before:inset-x-0',
'before:bottom-full',
'before:h-8',
'before:bg-linear-to-b',
'before:from-transparent',
'before:to-tint-base',
'sidebar-filled:before:to-tint-subtle',
'theme-muted:before:to-tint-subtle',
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-subtle',
'[html.sidebar-filled.theme-muted_&]:before:to-tint-base',
'[html.sidebar-filled.theme-bold.tint_&]:before:to-tint-base',
'page-no-toc:before:to-transparent!',
className
)}
>
@@ -62,12 +101,16 @@ export function TrademarkLink(props: {
'sidebar-filled:px-3',
'lg:sidebar-filled:page-no-toc:px-5',
'lg:page-no-toc:bg-tint-base',
'rounded-lg',
'straight-corners:rounded-none',
'circular-corners:rounded-2xl',
'hover:bg-tint',
'hover:text-tint-strong',
'ring-1',
'ring-2',
'lg:ring-1',
'ring-inset',
'ring-tint-subtle',
@@ -1,3 +1,4 @@
export { TableOfContents } from './TableOfContents';
export { PagesList } from './PagesList';
export { TOCScrollContainer } from './TOCScroller';
export { Trademark } from './Trademark';
@@ -25,7 +25,7 @@ export const ToggleableLinkItemStyles = [
'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
'contrast-more:hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint-12',
'before:contents[] before:-left-px before:absolute before:inset-y-0',
'sidebar-list-line:rounded-l-none! sidebar-list-line:before:w-px [&+div_a]:sidebar-list-default:rounded-l-none [&+div_a]:pl-5 [&+div_a]:sidebar-list-default:before:w-px',
'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px [&+div_a]:sidebar-list-default:rounded-l-none [&+div_a]:pl-5 [&+div_a]:sidebar-list-default:before:w-px',
];
export const ToggleableLinkItemActiveStyles = [
@@ -27,11 +27,7 @@ export function ThemeToggler() {
};
return (
<ButtonGroup
role="radiogroup"
className="xl:page-no-toc:-m-2 gap-2 circular-corners:rounded-full bg-tint-base xl:page-no-toc:p-2"
combinedShape={false}
>
<ButtonGroup role="radiogroup" className="gap-2" combinedShape={false}>
<ThemeButton
active={mounted && theme === 'light'}
icon="sun-bright"
@@ -70,7 +66,11 @@ function ThemeButton(props: {
aria-checked={active}
variant="blank"
size="default"
active={active}
className={
active
? 'bg-primary theme-muted:bg-primary-hover text-primary-strong ring-primary hover:bg-primary contrast-more:text-primary-strong contrast-more:ring-1 [html.sidebar-filled.theme-bold.tint_&]:bg-primary-hover'
: ''
}
icon={icon}
iconOnly
/>
@@ -1,83 +0,0 @@
/**
* Hook to manage a controlled state.
*
* From https://github.com/adobe/react-spectrum/blob/main/packages/%40react-stately/utils/src/useControlledState.ts
*/
import React, {
type SetStateAction,
useCallback,
useEffect,
useReducer,
useRef,
useState,
} from 'react';
// Use the earliest effect possible to reset the ref below.
const useEarlyEffect: typeof React.useLayoutEffect =
typeof document !== 'undefined' ? React.useInsertionEffect || React.useLayoutEffect : () => {};
export function useControlledState<T, C = T>(
value: Exclude<T, undefined>,
defaultValue: Exclude<T, undefined> | undefined,
onChange?: (v: C, ...args: any[]) => void
): [T, (value: SetStateAction<T>, ...args: any[]) => void];
export function useControlledState<T, C = T>(
value: Exclude<T, undefined> | undefined,
defaultValue: Exclude<T, undefined>,
onChange?: (v: C, ...args: any[]) => void
): [T, (value: SetStateAction<T>, ...args: any[]) => void];
export function useControlledState<T, C = T>(
value: T,
defaultValue: T,
onChange?: (v: C, ...args: any[]) => void
): [T, (value: SetStateAction<T>, ...args: any[]) => void] {
// Store the value in both state and a ref. The state value will only be used when uncontrolled.
// The ref is used to track the most current value, which is passed to the function setState callback.
const [stateValue, setStateValue] = useState(value || defaultValue);
const valueRef = useRef(stateValue);
const isControlledRef = useRef(value !== undefined);
const isControlled = value !== undefined;
useEffect(() => {
const wasControlled = isControlledRef.current;
if (wasControlled !== isControlled && process.env.NODE_ENV !== 'production') {
console.warn(
`WARN: A component changed from ${wasControlled ? 'controlled' : 'uncontrolled'} to ${isControlled ? 'controlled' : 'uncontrolled'}.`
);
}
isControlledRef.current = isControlled;
}, [isControlled]);
// After each render, update the ref to the current value.
// This ensures that the setState callback argument is reset.
// Note: the effect should not have any dependencies so that controlled values always reset.
const currentValue = isControlled ? value : stateValue;
useEarlyEffect(() => {
valueRef.current = currentValue;
});
const [, forceUpdate] = useReducer(() => ({}), {});
const setValue = useCallback(
(value: SetStateAction<T>, ...args: any[]) => {
// @ts-ignore - TS doesn't know that T cannot be a function.
const newValue = typeof value === 'function' ? value(valueRef.current) : value;
if (!Object.is(valueRef.current, newValue)) {
// Update the ref so that the next setState callback has the most recent value.
valueRef.current = newValue;
setStateValue(newValue);
// Always trigger a re-render, even when controlled, so that the layout effect above runs to reset the value.
forceUpdate();
// Trigger onChange. Note that if setState is called multiple times in a single event,
// onChange will be called for each one instead of only once.
onChange?.(newValue, ...args);
}
},
[onChange]
);
return [currentValue, setValue];
}
+3 -3
View File
@@ -9,9 +9,9 @@ export const HEADER_HEIGHT_DESKTOP = 64 as const;
* Style for the container to adapt between normal and full width.
*/
export const CONTAINER_STYLE: ClassValue = [
'px-4 pl-[max(env(safe-area-inset-left),1rem)] pr-[max(env(safe-area-inset-right),1rem)]',
'sm:px-6 sm:pl-[max(env(safe-area-inset-left),1.5rem)] sm:pr-[max(env(safe-area-inset-right),1.5rem)]',
'md:px-8 md:pl-[max(env(safe-area-inset-left),2rem)] md:pr-[max(env(safe-area-inset-right),2rem)]',
'px-4',
'sm:px-6',
'md:px-8',
'max-w-screen-2xl',
'mx-auto',
];
@@ -28,7 +28,7 @@ export function HoverCard(
<RadixHoverCard.Portal>
<RadixHoverCard.Content
side={props.side ?? 'top'}
className="pointer-events-none z-50 w-screen max-w-md animate-scale-in px-4 data-[state='closed']:animate-scale-out sm:w-auto"
className="pointer-events-none z-40 w-screen max-w-md animate-scale-in px-4 data-[state='closed']:animate-scale-out sm:w-auto"
>
<div
className={tcls(
@@ -1,317 +0,0 @@
'use client';
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import React, { type ReactNode } from 'react';
import { useControlledState } from '../hooks/useControlledState';
import { Button, type ButtonProps } from './Button';
import { KeyboardShortcut, type KeyboardShortcutProps } from './KeyboardShortcut';
type CustomInputProps = {
label: string;
inline?: boolean;
leading?: IconName | React.ReactNode;
trailing?: React.ReactNode;
sizing?: 'medium' | 'large'; // The `size` prop is already taken by the HTML input element.
containerRef?: React.RefObject<HTMLDivElement | null>;
/**
* A submit button, shown to the right of the input.
*/
submitButton?: boolean | ButtonProps;
/**
* A message to be shown to the right of the input when the value has been submitted.
*/
submitMessage?: string | ReactNode;
/**
* A clear button, shown to the left of the input.
*/
clearButton?: boolean | ButtonProps;
/**
* A keyboard shortcut, shown to the right of the input.
*/
keyboardShortcut?: boolean | KeyboardShortcutProps;
onSubmit?: (value: string | number | readonly string[] | undefined) => void;
resize?: boolean;
};
export type InputProps = CustomInputProps &
(
| ({ multiline?: false } & React.InputHTMLAttributes<HTMLInputElement>)
| ({ multiline: true } & React.TextareaHTMLAttributes<HTMLTextAreaElement>)
);
type InputElement = HTMLInputElement | HTMLTextAreaElement;
/**
* Input component with core functionality (submitting, clearing, validating) and shared styles.
*/
export const Input = React.forwardRef<InputElement, InputProps>((props, passedRef) => {
const {
// Custom props
multiline,
sizing = 'medium',
inline = false,
leading,
trailing,
className,
clearButton,
submitButton,
submitMessage,
label,
keyboardShortcut,
onSubmit,
containerRef,
resize = false,
// HTML attributes we need to read
value: passedValue,
'aria-label': ariaLabel,
'aria-busy': ariaBusy,
placeholder,
disabled,
onChange,
onKeyDown,
maxLength,
minLength,
// Rest are HTML attributes to pass through
...htmlProps
} = props;
const [value, setValue] = useControlledState(passedValue, passedValue ?? '');
const [submitted, setSubmitted] = React.useState(false);
const [height, setHeight] = React.useState<number>();
const inputRef = React.useRef<InputElement>(null);
const ref = (passedRef as React.RefObject<HTMLInputElement | HTMLTextAreaElement>) ?? inputRef;
const language = useLanguage();
const hasValue = value.toString().trim().length > 0;
const hasValidValue =
hasValue &&
(maxLength ? value.toString().length <= maxLength : true) &&
(minLength ? value.toString().length >= minLength : true);
const sizes = {
medium: {
container: `${multiline ? 'p-2' : 'px-4 py-2'} gap-2 circular-corners:rounded-3xl rounded-corners:rounded-xl`,
input: '-m-2 p-2',
gap: 'gap-2',
},
large: {
container: `${multiline ? 'p-3' : 'px-6 py-3 '} gap-3 circular-corners:rounded-3xl rounded-corners:rounded-xl`,
input: '-m-3 p-3',
gap: 'gap-3',
},
};
const handleChange = (event: React.ChangeEvent<InputElement>) => {
const newValue = event.target.value;
setValue(newValue);
onChange?.(event as React.ChangeEvent<HTMLInputElement & HTMLTextAreaElement>);
// Reset submitted state when user edits the value to allow re-submission
if (submitted) {
setSubmitted(false);
}
if (multiline && resize && ref.current) {
// TODO: replace with `field-sizing: content` when more broadly supported. https://caniuse.com/?search=field-sizing
// Reset the height to auto, then set it to the scroll height. If we don't reset, the height will only ever grow.
setHeight(ref.current.scrollHeight);
}
};
const handleClear = () => {
if (!ref.current) return;
setValue('');
};
const handleClick = () => {
ref.current?.focus();
};
const handleSubmit = () => {
if (hasValue && onSubmit) {
onSubmit(value);
setSubmitted(true);
setValue('');
}
};
const handleKeyDown = (event: React.KeyboardEvent<InputElement>) => {
onKeyDown?.(event as React.KeyboardEvent<HTMLInputElement & HTMLTextAreaElement>);
// If the user wants to handle the keydown by itself, we let him do it.
if (event.defaultPrevented) return;
if (event.key === 'Enter' && !event.shiftKey && hasValue) {
event.preventDefault();
handleSubmit();
} else if (event.key === 'Escape') {
event.preventDefault();
event.currentTarget.blur();
}
};
const inputClassName = tcls(
'peer -m-2 max-h-64 grow resize-none text-left outline-none placeholder:text-tint/8 aria-busy:cursor-progress',
sizes[sizing].input
);
const inputProps = {
className: inputClassName,
value: value,
onKeyDown: handleKeyDown,
'aria-busy': ariaBusy,
onChange: handleChange,
'aria-label': ariaLabel ?? label,
placeholder: placeholder ?? label,
disabled: disabled,
maxLength: maxLength,
minLength: minLength,
style: {
height: multiline && resize && hasValue && height ? `${height}px` : undefined,
},
};
const Tag: React.ElementType = inline ? 'span' : 'div';
return (
<Tag
className={tcls(
'group/input relative flex min-h-min overflow-hidden border border-tint bg-tint-base align-middle shadow-tint/6 ring-primary-hover transition-all dark:shadow-tint-1',
disabled
? 'cursor-not-allowed border-tint-subtle bg-tint-subtle opacity-7'
: [
'depth-subtle:focus-within:-translate-y-px depth-subtle:hover:-translate-y-px depth-subtle:shadow-xs',
'focus-within:border-primary-hover focus-within:depth-subtle:shadow-lg focus-within:shadow-primary-subtle focus-within:ring-2 hover:cursor-text hover:border-tint-hover depth-subtle:hover:not-focus-within:shadow-md focus-within:hover:border-primary-hover',
],
multiline ? 'flex-col' : 'flex-row',
ariaBusy ? 'cursor-progress' : '',
sizes[sizing].container,
className
)}
onClick={handleClick}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
handleClick();
}
}}
ref={containerRef}
>
<Tag
className={tcls(
'flex grow',
sizes[sizing].gap,
multiline ? 'items-start' : 'items-center'
)}
>
{leading ? (
<Tag
className={tcls(
clearButton && hasValue ? 'group-focus-within/input:hidden' : '',
multiline ? 'my-1.25' : ''
)}
>
{typeof leading === 'string' ? (
<Icon
icon={leading as IconName}
className="size-4 shrink-0 text-tint"
/>
) : (
leading
)}
</Tag>
) : null}
{clearButton ? (
<Button
variant="blank"
size="medium"
label={tString(language, 'clear')}
iconOnly
icon="circle-xmark"
onClick={handleClear}
{...(typeof clearButton === 'object' ? clearButton : {})}
className={tcls(
'-mx-1.5 hidden shrink-0 animate-fade-in p-1.5 text-tint',
multiline ? '-my-0.25' : '-my-1.5',
hasValue ? 'group-focus-within/input:flex' : '',
typeof clearButton === 'object' ? clearButton.className : ''
)}
/>
) : null}
{multiline ? (
<textarea
{...inputProps}
ref={ref as React.RefObject<HTMLTextAreaElement>}
{...(htmlProps as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
/>
) : (
<input
{...inputProps}
ref={ref as React.RefObject<HTMLInputElement>}
type="text"
size={1} // Size controls the intrinsic width of the input, but we want to control the width with CSS
{...(htmlProps as React.InputHTMLAttributes<HTMLInputElement>)}
/>
)}
{keyboardShortcut !== false ? (
<Tag
className={
multiline ? `absolute top-0 right-0 ${sizes[sizing].container}` : ''
}
>
{typeof keyboardShortcut === 'object' ? (
<KeyboardShortcut {...keyboardShortcut} />
) : onSubmit && !submitted && hasValue ? (
<KeyboardShortcut
keys={['enter']}
className="hidden bg-tint-base group-focus-within/input:flex"
/>
) : null}
</Tag>
) : null}
</Tag>
{trailing || submitButton || maxLength ? (
<Tag className="flex items-center gap-2 empty:hidden">
{trailing}
{maxLength && !submitted && value.toString().length > maxLength * 0.8 ? (
<span
className={tcls(
'shrink-0 animate-fade-in text-xs tabular-nums',
value.toString().length >= maxLength
? 'text-danger-subtle'
: 'text-tint-subtle'
)}
>
{value.toString().length} / {maxLength}
</span>
) : null}
{submitted && submitMessage ? (
typeof submitMessage === 'string' ? (
<Tag className="ml-auto flex animate-fade-in items-center gap-1 p-1.5 text-success-subtle">
<Icon icon="check-circle" className="size-4" />
{submitMessage}
</Tag>
) : (
submitMessage
)
) : submitButton ? (
<Button
variant="primary"
size="medium"
label={tString(language, 'submit')}
onClick={handleSubmit}
icon={multiline ? undefined : 'arrow-right'}
disabled={disabled || !hasValidValue}
iconOnly={!multiline}
className="ml-auto"
{...(typeof submitButton === 'object' ? submitButton : {})}
/>
) : null}
</Tag>
) : null}
</Tag>
);
});
@@ -1,13 +1,9 @@
'use client';
import { tcls } from '@/lib/tailwind';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import * as React from 'react';
export type KeyboardShortcutProps = {
keys: string[];
} & React.HTMLAttributes<HTMLDivElement>;
function getOperatingSystem() {
const platform = navigator.platform.toLowerCase();
@@ -17,7 +13,7 @@ function getOperatingSystem() {
return 'win';
}
export function KeyboardShortcut(props: KeyboardShortcutProps) {
export function KeyboardShortcut(props: { keys: string[]; className?: ClassValue }) {
const { keys, className } = props;
const [operatingSystem, setOperatingSystem] = React.useState<string | null>(null);
@@ -45,7 +41,7 @@ export function KeyboardShortcut(props: KeyboardShortcutProps) {
break;
case 'enter':
element = <Icon icon="arrow-turn-down-left" className="size-[.9em]" />;
element = <Icon icon="arrow-turn-down-left" className="size-[.75em]" />;
break;
}
return (
@@ -4,7 +4,7 @@ import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import * as React from 'react';
import { useScrollListener } from '../hooks/useScrollListener';
import { Button, type ButtonProps } from './Button';
import { Button } from './Button';
/**
* A container that encapsulates a scrollable area with usability features.
@@ -17,26 +17,17 @@ export type ScrollContainerProps = {
className?: string;
contentClassName?: string;
/** Optional class(es) to apply when there the container can be scrolled on the leading (left or top) edge */
leadingEdgeScrollClassName?: string;
/** Optional class(es) to apply when there the container can be scrolled on the trailing (right or bottom) edge */
trailingEdgeScrollClassName?: string;
/** The direction of the scroll container. */
orientation: 'horizontal' | 'vertical';
leading?: {
/** Whether to fade out the leading edge of the container. */
fade: boolean;
/** Whether to show a button to scroll back. */
button: boolean | ButtonProps;
/** Optional class(es) to apply when there the container can be scrolled on the leading (left or top) edge */
className?: string;
};
trailing?: {
/** Whether to fade out the trailing edge of the container. */
fade: boolean;
/** Whether to show a button to scroll forward. */
button: boolean | ButtonProps;
/** Optional class(es) to apply when there the container can be scrolled on the trailing (right or bottom) edge */
className?: string;
};
/** Whether to fade out the edges of the container. */
fadeEdges?: ('leading' | 'trailing')[];
/** The ID or ref of the active item to scroll to. */
active?: string | React.RefObject<HTMLElement | null>;
@@ -48,9 +39,10 @@ export function ScrollContainer(props: ScrollContainerProps) {
className,
contentClassName,
orientation,
fadeEdges = ['leading', 'trailing'],
active,
leading = { fade: true, button: true },
trailing = { fade: true, button: true },
leadingEdgeScrollClassName,
trailingEdgeScrollClassName,
...rest
} = props;
@@ -110,9 +102,7 @@ export function ScrollContainer(props: ScrollContainerProps) {
return;
}
const activeItem =
typeof active === 'string'
? containerRef.current?.querySelector(active)
: active.current;
typeof active === 'string' ? document.getElementById(active) : active.current;
if (!activeItem || !container.contains(activeItem)) {
return;
}
@@ -148,30 +138,28 @@ export function ScrollContainer(props: ScrollContainerProps) {
return (
<div
className={tcls(
'group/scroll-container relative flex shrink grow',
orientation === 'horizontal' ? 'min-w-0' : 'min-h-0',
'group/scroll-container relative flex overflow-hidden',
className,
scrollPosition > 0 ? leading?.className : '',
scrollPosition < scrollSize ? trailing?.className : ''
scrollPosition > 0 ? leadingEdgeScrollClassName : '',
scrollPosition < scrollSize ? trailingEdgeScrollClassName : ''
)}
{...rest}
>
{/* Scrollable content */}
<div
className={tcls(
'flex flex-1 overflow-hidden',
orientation === 'horizontal' ? 'min-w-0' : 'min-h-0',
'flex shrink grow',
orientation === 'horizontal' ? 'no-scrollbar' : 'hide-scrollbar',
orientation === 'horizontal' ? 'overflow-x-scroll' : 'flex-col overflow-y-auto',
leading.fade && scrollPosition > 0
fadeEdges.includes('leading') && scrollPosition > 0
? orientation === 'horizontal'
? 'mask-l-from-[calc(100%-1rem)]'
: 'mask-t-from-[calc(100%-1rem)]'
? 'mask-l-from-[calc(100%-2rem)]'
: 'mask-t-from-[calc(100%-2rem)]'
: '',
trailing.fade && scrollPosition < scrollSize
fadeEdges.includes('trailing') && scrollPosition < scrollSize
? orientation === 'horizontal'
? 'mask-r-from-[calc(100%-1rem)]'
: 'mask-b-from-[calc(100%-1rem)]'
? 'mask-r-from-[calc(100%-2rem)]'
: 'mask-b-from-[calc(100%-2rem)]'
: '',
contentClassName
)}
@@ -181,52 +169,44 @@ export function ScrollContainer(props: ScrollContainerProps) {
</div>
{/* Scroll buttons back & forward */}
{leading.button !== false ? (
<Button
icon={orientation === 'horizontal' ? 'chevron-left' : 'chevron-up'}
iconOnly
size="xsmall"
variant="secondary"
tabIndex={-1}
onClick={scrollBack}
label={tString(language, 'scroll_back')}
{...(typeof leading.button === 'object' ? leading.button : {})}
className={tcls(
'z-10 bg-tint-base!',
orientation === 'horizontal'
? '-translate-y-1/2! top-1/2 left-0 ml-2'
: '-translate-x-1/2! top-0 left-1/2 mt-2',
'absolute z-10 not-pointer-none:block hidden scale-0 opacity-0 transition-[scale,opacity]',
scrollPosition > 0
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
: 'pointer-events-none',
typeof leading.button === 'object' ? leading.button.className : ''
)}
/>
) : null}
{trailing.button !== false ? (
<Button
icon={orientation === 'horizontal' ? 'chevron-right' : 'chevron-down'}
iconOnly
size="xsmall"
variant="secondary"
tabIndex={-1}
onClick={scrollFurther}
label={tString(language, 'scroll_further')}
{...(typeof trailing.button === 'object' ? trailing.button : {})}
className={tcls(
'z-10 bg-tint-base!',
orientation === 'horizontal'
? '-translate-y-1/2! top-1/2 right-0 mr-2'
: '-translate-x-1/2! bottom-0 left-1/2 mb-2',
'absolute z-10 not-pointer-none:block hidden scale-0 transition-[scale,opacity]',
scrollPosition < scrollSize
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
: 'pointer-events-none',
typeof trailing.button === 'object' ? trailing.button.className : ''
)}
/>
) : null}
<Button
icon={orientation === 'horizontal' ? 'chevron-left' : 'chevron-up'}
iconOnly
size="xsmall"
variant="secondary"
tabIndex={-1}
className={tcls(
'bg-tint-base!',
orientation === 'horizontal'
? '-translate-y-1/2! top-1/2 left-0 ml-2'
: '-translate-x-1/2! top-0 left-1/2 mt-2',
'absolute not-pointer-none:block hidden scale-0 opacity-0 transition-[scale,opacity]',
scrollPosition > 0
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
: 'pointer-events-none'
)}
onClick={scrollBack}
label={tString(language, 'scroll_back')}
/>
<Button
icon={orientation === 'horizontal' ? 'chevron-right' : 'chevron-down'}
iconOnly
size="xsmall"
variant="secondary"
tabIndex={-1}
className={tcls(
'bg-tint-base!',
orientation === 'horizontal'
? '-translate-y-1/2! top-1/2 right-0 mr-2'
: '-translate-x-1/2! bottom-0 left-1/2 mb-2',
'absolute not-pointer-none:block hidden scale-0 transition-[scale,opacity]',
scrollPosition < scrollSize
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
: 'pointer-events-none'
)}
onClick={scrollFurther}
label={tString(language, 'scroll_further')}
/>
</div>
);
}
@@ -234,7 +214,7 @@ export function ScrollContainer(props: ScrollContainerProps) {
/**
* Scroll to an element in a container.
*/
function scrollToElementInContainer(element: Element, container: HTMLElement) {
function scrollToElementInContainer(element: HTMLElement, container: HTMLElement) {
const containerRect = container.getBoundingClientRect();
const rect = element.getBoundingClientRect();
@@ -249,6 +229,6 @@ function scrollToElementInContainer(element: Element, container: HTMLElement) {
(rect.left - containerRect.left) -
container.clientWidth / 2 +
rect.width / 2,
behavior: 'auto',
behavior: 'smooth',
});
}
@@ -1,194 +0,0 @@
'use client';
import { useLanguage } from '@/intl/client';
import { tString } from '@/intl/translate';
import { type ClassValue, tcls } from '@/lib/tailwind';
import React from 'react';
import { useIsMobile } from '../hooks/useIsMobile';
import { Button } from './Button';
/**
* SideSheet - A slide-in panel component that can appear from the left or right side.
*
* Supports both controlled and uncontrolled modes:
* - Controlled: Provide both `open` and `onOpenChange` props. Parent manages state.
* - Uncontrolled: Omit `open` prop. Component manages its own state internally.
*/
export function SideSheet(
props: {
/** Which side the sheet slides in from */
side: 'left' | 'right';
/**
* Optional CSS class to monitor and sync with `document.body.classList`.
* When set, a MutationObserver watches for the class and syncs the sheet state accordingly.
* Adding this class opens the sheet, removing it closes it.
* Works in both controlled and uncontrolled modes.
*/
toggleClass?: string;
/**
* Modal behavior: true (always modal), false (never modal), or 'mobile' (modal only on mobile).
* Defaults to 'mobile'.
*/
modal?: true | false | 'mobile';
/**
* Controls visibility. If provided, component is controlled (parent manages state).
* If undefined, component is uncontrolled (manages its own state).
*/
open?: boolean;
/** Called when the open state changes. Receives the new state (true/false). Only used in controlled mode. */
onOpenChange?: (open: boolean) => void;
/** Show a backdrop overlay when modal */
withScrim?: boolean;
/** Show a close button when modal */
withCloseButton?: boolean;
} & React.HTMLAttributes<HTMLDivElement>
) {
const {
side,
children,
className,
toggleClass,
open: openState,
modal = 'mobile',
withScrim,
withCloseButton,
onOpenChange,
...rest
} = props;
const isMobile = useIsMobile();
const isModal = modal === 'mobile' ? isMobile : modal;
// Internal state for uncontrolled mode (only used when open prop is undefined)
const [open, setOpen] = React.useState(openState ?? false);
// Determine actual open state: controlled (from prop) or uncontrolled (from internal state)
const isOpen = openState !== undefined ? openState : open;
const handleClose = React.useCallback(() => {
if (openState !== undefined) {
// Controlled mode: parent manages state, notify via callback with new state
onOpenChange?.(false);
} else {
// Uncontrolled mode: update internal state and sync body class if needed
setOpen(false);
if (toggleClass) {
document.body.classList.remove(toggleClass);
}
}
}, [openState, onOpenChange, toggleClass]);
// Sync the sheet state with the body class if the toggleClass is set
React.useEffect(() => {
if (!toggleClass) {
return;
}
const callback = (mutationList: MutationRecord[]) => {
for (const mutation of mutationList) {
if (mutation.attributeName === 'class') {
const shouldBeOpen = document.body.classList.contains(toggleClass);
if (openState !== undefined) {
// Controlled mode: sync with parent's state
// Notify parent of state change via onOpenChange
if (shouldBeOpen !== openState) {
onOpenChange?.(shouldBeOpen);
}
} else {
// Uncontrolled mode: sync internal state with body class
setOpen(shouldBeOpen);
}
}
}
};
const observer = new MutationObserver(callback);
observer.observe(document.body, { attributes: true });
return () => observer.disconnect();
}, [toggleClass, openState, onOpenChange]);
return (
<>
{withScrim ? (
<SideSheetScrim
className={tcls(isModal && isOpen ? '' : 'hidden opacity-0 backdrop-blur-none')}
onClick={handleClose}
/>
) : null}
<aside
className={tcls(
'side-sheet',
isOpen
? side === 'left'
? 'hydrated:animate-enter-from-left'
: 'hydrated:animate-enter-from-right'
: side === 'left'
? 'hydrated:animate-exit-to-left'
: 'hydrated:animate-exit-to-right',
'fixed inset-y-0 z-41', // Above the side sheet scrim on z-40
side === 'left' ? 'left-0' : 'right-0',
withCloseButton ? 'max-w-[calc(100vw-5rem)]' : 'max-w-[calc(100vw-3rem)]',
isOpen ? '' : 'hidden',
className
)}
aria-expanded={isOpen}
aria-modal={isModal}
{...rest}
>
{children}
{withCloseButton ? (
<SideSheetCloseButton
className={tcls(
side === 'left' ? 'left-full ml-4' : 'right-full mr-4',
isModal && isOpen ? 'animate-blur-in' : 'hidden animate-blur-out'
)}
onClick={handleClose}
/>
) : null}
</aside>
</>
);
}
/** Backdrop overlay shown behind the modal sheet */
export function SideSheetScrim(props: { className?: ClassValue; onClick?: () => void }) {
const { className, onClick } = props;
return (
<div
id="side-sheet-scrim"
onClick={() => {
onClick?.();
}}
onKeyUp={(e) => {
if (e.key === 'Escape') {
onClick?.();
}
}}
className={tcls(
'fixed inset-0 z-40 items-start bg-tint-base/3 not-hydrated:opacity-0 starting:opacity-0 backdrop-blur-md starting:backdrop-blur-none transition-[opacity,display,backdrop-filter] transition-discrete duration-250 dark:bg-tint-base/6',
className
)}
/>
);
}
/** Close button displayed outside the sheet when modal */
export function SideSheetCloseButton(props: { className?: ClassValue; onClick?: () => void }) {
const { className, onClick } = props;
const language = useLanguage();
return (
<Button
icon="xmark"
variant="secondary"
size="default"
iconOnly
label={tString(language, 'close')}
className={tcls('absolute top-4 bg-tint-base! transition-discrete', className)}
onClick={() => {
onClick?.();
}}
/>
);
}
@@ -13,4 +13,3 @@ export * from './Popover';
export * from './LoadingStateProvider';
export * from './HoverCard';
export * from './DropdownMenu';
export * from './Input';
+1 -1
View File
@@ -61,13 +61,13 @@ export function servePagesSitemap(context: GitBookSiteContext) {
const lastModified = page.updatedAt || page.createdAt;
const url: { loc: string; priority: number; lastmod?: string } = {
priority: normalizedPriority,
loc: linker.toAbsoluteURL(
linker.toPathForPage({
pages: context.revision.pages,
page,
})
),
priority: normalizedPriority,
};
if (lastModified) {
+9 -13
View File
@@ -328,10 +328,10 @@ const config: Config = {
blurOut: 'blurOut 200ms ease-in both',
'blurOut-slow': 'blurOut 500ms ease-in both',
enterFromLeft: 'enterFromLeft 300ms cubic-bezier(0.83, 0, 0.17, 1) both',
enterFromRight: 'enterFromRight 300ms cubic-bezier(0.83, 0, 0.17, 1) both',
exitToLeft: 'exitToLeft 300ms cubic-bezier(0.83, 0, 0.17, 1) both',
exitToRight: 'exitToRight 300ms cubic-bezier(0.83, 0, 0.17, 1) both',
enterFromLeft: 'enterFromLeft 250ms cubic-bezier(0.83, 0, 0.17, 1) both',
enterFromRight: 'enterFromRight 250ms cubic-bezier(0.83, 0, 0.17, 1) both',
exitToLeft: 'exitToLeft 250ms cubic-bezier(0.83, 0, 0.17, 1) both',
exitToRight: 'exitToRight 250ms cubic-bezier(0.83, 0, 0.17, 1) both',
heightIn: 'heightIn 200ms ease both',
crawl: 'crawl 2s ease-in-out infinite',
@@ -474,18 +474,18 @@ const config: Config = {
},
enterFromRight: {
from: { opacity: '0', transform: 'translateX(50%)', display: 'none' },
to: { opacity: '1', transform: 'translateX(0)', display: 'inherit' },
to: { opacity: '1', transform: 'translateX(0)', display: 'block' },
},
enterFromLeft: {
from: { opacity: '0', transform: 'translateX(-50%)', display: 'none' },
to: { opacity: '1', transform: 'translateX(0)', display: 'inherit' },
to: { opacity: '1', transform: 'translateX(0)', display: 'block' },
},
exitToRight: {
from: { opacity: '1', transform: 'translateX(0)', display: 'inherit' },
from: { opacity: '1', transform: 'translateX(0)', display: 'block' },
to: { opacity: '0', transform: 'translateX(50%)', display: 'none' },
},
exitToLeft: {
from: { opacity: '1', transform: 'translateX(0)', display: 'inherit' },
from: { opacity: '1', transform: 'translateX(0)', display: 'block' },
to: { opacity: '0', transform: 'translateX(-50%)', display: 'none' },
},
scaleIn: {
@@ -576,11 +576,7 @@ const config: Config = {
* Variant when the Table of Content navigation is open.
*/
addVariant('navigation-open', 'body.navigation-open &');
addVariant('chat-open', 'body:has(.ai-chat[aria-expanded="true"]) &');
addVariant(
'sheet-open',
'html:has(.side-sheet[aria-modal="true"][aria-expanded="true"]) &, &:has(.side-sheet[aria-modal="true"][aria-expanded="true"])'
);
addVariant('chat-open', 'body:has(.ai-chat:not(.hidden)) &');
/**
* Variant when a header is displayed.
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'bun:test';
import { getContentTestURL } from './utils';
const EMBED_SCRIPT_URL = getContentTestURL(
'https://gitbook.gitbook.io/test-gitbook-open/~gitbook/embed/script.js'
);
describe('embed script', () => {
it('serves the embeddable script with permissive headers', async () => {
const response = await fetch(EMBED_SCRIPT_URL, {
headers: {
Origin: 'https://example.com',
},
});
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toContain('application/javascript');
expect(response.headers.get('access-control-allow-origin')).toBe('*');
expect(response.headers.get('cross-origin-resource-policy')).toBe('cross-origin');
const body = await response.text();
expect(body).toContain('w.GitBook');
});
});
+3 -3
View File
@@ -17,11 +17,11 @@
"sideEffects": false,
"version": "0.3.4",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/fontawesome-free": "^7.1.0"
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/fontawesome-free": "^6.6.0"
},
"optionalDependencies": {
"@gitbook/fontawesome-pro": "1.0.16"
"@gitbook/fontawesome-pro": "1.0.15"
},
"devDependencies": {
"bun-types": "catalog:",
+75 -355
View File
@@ -36,19 +36,9 @@ function OpenAPISchemaProperty(
context: OpenAPIClientContext;
circularRefs: CircularRefsIds;
className?: string;
discriminator?: OpenAPIV3.DiscriminatorObject;
discriminatorValue?: string;
} & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'>
) {
const {
circularRefs: parentCircularRefs,
context,
className,
property,
discriminator,
discriminatorValue,
...rest
} = props;
const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props;
const { schema } = property;
@@ -69,7 +59,7 @@ function OpenAPISchemaProperty(
const circularRefs = new Map(parentCircularRefs);
circularRefs.set(schema, id);
const properties = getSchemaProperties(schema, discriminator, discriminatorValue);
const properties = getSchemaProperties(schema);
const ancestors = new Set(circularRefs.keys());
const alternatives = getSchemaAlternatives(schema, ancestors);
@@ -77,15 +67,26 @@ function OpenAPISchemaProperty(
const header = <OpenAPISchemaPresentation id={id} context={context} property={property} />;
const content = (() => {
if (alternatives?.schemas) {
const { schemas, discriminator } = alternatives;
return (
<OpenAPISchemaAlternatives
alternatives={alternatives}
schema={schema}
circularRefs={circularRefs}
context={context}
parentDiscriminator={discriminator}
parentDiscriminatorValue={discriminatorValue}
/>
<div className="openapi-schema-alternatives">
{schemas.map((alternativeSchema, index) => (
<div key={index} className="openapi-schema-alternative">
<OpenAPISchemaAlternative
schema={alternativeSchema}
discriminator={discriminator}
circularRefs={circularRefs}
context={context}
/>
{index < schemas.length - 1 ? (
<OpenAPISchemaAlternativeSeparator
schema={schema}
context={context}
/>
) : null}
</div>
))}
</div>
);
}
@@ -186,30 +187,11 @@ function OpenAPIRootSchema(props: {
const id = useId();
const properties = getSchemaProperties(schema);
const description = resolveDescription(schema);
const ancestors = new Set(parentCircularRefs.keys());
const alternatives = getSchemaAlternatives(schema, ancestors);
const circularRefs = new Map(parentCircularRefs);
circularRefs.set(schema, id);
// Handle root-level oneOf/allOf/anyOf
if (alternatives?.schemas) {
return (
<>
{description ? (
<Markdown source={description} className="openapi-schema-root-description" />
) : null}
<OpenAPISchemaAlternatives
alternatives={alternatives}
schema={schema}
circularRefs={circularRefs}
context={context}
/>
</>
);
}
if (properties?.length) {
const circularRefs = new Map(parentCircularRefs);
circularRefs.set(schema, id);
return (
<>
{description ? (
@@ -246,116 +228,6 @@ export function OpenAPIRootSchemaFromServer(props: {
);
}
/**
* Get the discriminator value for a schema.
*/
function getDiscriminatorValue(
schema: OpenAPIV3.SchemaObject,
discriminator: OpenAPIV3.DiscriminatorObject | undefined
): string | undefined {
if (!discriminator) {
return undefined;
}
if (discriminator.mapping) {
const mappingEntry = Object.entries(discriminator.mapping).find(([key, ref]) => {
if (schema.title === ref || (!!schema.title && ref.endsWith(`/${schema.title}`))) {
return true;
}
// Fallback: check if the title contains the key (normalized)
if (schema.title?.toLowerCase().replace(/\s/g, '').includes(key.toLowerCase())) {
return true;
}
return false;
});
if (mappingEntry) {
return mappingEntry[0];
}
}
if (!discriminator.propertyName || !schema.properties) {
return undefined;
}
const property = schema.properties[discriminator.propertyName];
if (!property || checkIsReference(property)) {
return undefined;
}
if (property.const) {
return String(property.const);
}
if (property.enum?.length === 1) {
return String(property.enum[0]);
}
return;
}
/**
* Render alternatives (oneOf/allOf/anyOf) for a schema.
*/
function OpenAPISchemaAlternatives(props: {
alternatives: SchemaAlternatives;
schema: OpenAPIV3.SchemaObject;
circularRefs: CircularRefsIds;
context: OpenAPIClientContext;
parentDiscriminator?: OpenAPIV3.DiscriminatorObject;
parentDiscriminatorValue?: string;
}) {
const {
alternatives,
schema,
circularRefs,
context,
parentDiscriminator,
parentDiscriminatorValue,
} = props;
if (!alternatives?.schemas) {
return null;
}
const { schemas, discriminator: alternativeDiscriminator, type } = alternatives;
return (
<div className="openapi-schema-alternatives">
{schemas.map((alternativeSchema, index) => {
// If the alternative has its own discriminator, use it.
// Otherwise, for allOf, inherit from parent discriminator.
const effectiveDiscriminator =
alternativeDiscriminator ||
(type === 'allOf' ? parentDiscriminator : undefined);
// If we are inheriting and using parent discriminator, pass down the value.
const effectiveDiscriminatorValue =
!alternativeDiscriminator && type === 'allOf'
? parentDiscriminatorValue
: undefined;
return (
<div key={index} className="openapi-schema-alternative">
<OpenAPISchemaAlternative
schema={alternativeSchema}
discriminator={effectiveDiscriminator}
discriminatorValue={effectiveDiscriminatorValue}
circularRefs={circularRefs}
context={context}
/>
{index < schemas.length - 1 ? (
<OpenAPISchemaAlternativeSeparator schema={schema} context={context} />
) : null}
</div>
);
})}
</div>
);
}
/**
* Render a tab for an alternative schema.
* It renders directly the properties if relevant;
@@ -364,14 +236,11 @@ function OpenAPISchemaAlternatives(props: {
function OpenAPISchemaAlternative(props: {
schema: OpenAPIV3.SchemaObject;
discriminator: OpenAPIV3.DiscriminatorObject | undefined;
discriminatorValue?: string;
circularRefs: CircularRefsIds;
context: OpenAPIClientContext;
}) {
const { schema, discriminator, circularRefs, context } = props;
const discriminatorValue =
props.discriminatorValue || getDiscriminatorValue(schema, discriminator);
const properties = getSchemaProperties(schema, discriminator, discriminatorValue);
const properties = getSchemaProperties(schema, discriminator);
return properties?.length ? (
<OpenAPIDisclosure
@@ -388,8 +257,6 @@ function OpenAPISchemaAlternative(props: {
) : (
<OpenAPISchemaProperty
property={{ schema }}
discriminator={discriminator}
discriminatorValue={discriminatorValue}
circularRefs={circularRefs}
context={context}
/>
@@ -515,7 +382,7 @@ export function OpenAPISchemaPresentation(props: {
<div id={id} className="openapi-schema-presentation">
<OpenAPISchemaName
schema={schema}
type={getSchemaTitle(schema, { ignoreAlternatives: !propertyName })}
type={getSchemaTitle(schema)}
propertyName={propertyName}
isDiscriminatorProperty={isDiscriminatorProperty}
required={required}
@@ -563,87 +430,17 @@ export function OpenAPISchemaPresentation(props: {
);
}
/**
* Process properties from a schema object into property entries.
*/
function processSchemaProperties(
schema: OpenAPIV3.SchemaObject,
discriminator?: OpenAPIV3.DiscriminatorObject | undefined,
discriminatorValue?: string | undefined,
allRequired?: Set<string>
): OpenAPISchemaPropertyEntry[] {
const result: OpenAPISchemaPropertyEntry[] = [];
if (schema.properties) {
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
const isDiscriminator = discriminator?.propertyName === propertyName;
if (checkIsReference(propertySchema)) {
if (!isDiscriminator || !discriminatorValue) {
return;
}
}
let finalSchema = propertySchema;
if (isDiscriminator && discriminatorValue) {
finalSchema = {
...propertySchema,
const: discriminatorValue,
enum: [discriminatorValue],
};
}
result.push({
propertyName,
required: Array.isArray(schema.required)
? schema.required.includes(propertyName)
: allRequired?.has(propertyName)
? true
: undefined,
isDiscriminatorProperty: isDiscriminator,
schema: finalSchema,
});
});
}
if (schema.additionalProperties && !checkIsReference(schema.additionalProperties)) {
result.push({
propertyName: 'Other properties',
schema: schema.additionalProperties === true ? {} : schema.additionalProperties,
});
}
return result;
}
/**
* Merge properties into a result array, with later properties overriding earlier ones.
*/
function mergeProperties(
result: OpenAPISchemaPropertyEntry[],
newProperties: OpenAPISchemaPropertyEntry[]
): void {
for (const prop of newProperties) {
const existingIndex = result.findIndex((p) => p.propertyName === prop.propertyName);
if (existingIndex >= 0) {
result[existingIndex] = prop;
} else {
result.push(prop);
}
}
}
/**
* Get the sub-properties of a schema.
*/
function getSchemaProperties(
schema: OpenAPIV3.SchemaObject,
discriminator?: OpenAPIV3.DiscriminatorObject | undefined,
discriminatorValue?: string | undefined
discriminator?: OpenAPIV3.DiscriminatorObject | undefined
): null | OpenAPISchemaPropertyEntry[] {
// check array AND schema.items as this is sometimes null despite what the type indicates
if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) {
const items = schema.items;
const itemProperties = getSchemaProperties(items, discriminator, discriminatorValue);
const itemProperties = getSchemaProperties(items);
if (itemProperties) {
return itemProperties.map((prop) => ({
...prop,
@@ -665,55 +462,34 @@ function getSchemaProperties(
return [{ propertyName: 'items', schema: items }];
}
// Handle allOf by collecting properties from all schemas in the allOf
if (schema.allOf && Array.isArray(schema.allOf)) {
const allOfSchemas = schema.allOf.filter(
(s): s is OpenAPIV3.SchemaObject => !checkIsReference(s)
);
if (allOfSchemas.length > 0) {
const result: OpenAPISchemaPropertyEntry[] = [];
const allRequired = new Set<string>();
// Collect required fields and properties from all allOf schemas
for (const allOfSchema of allOfSchemas) {
if (Array.isArray(allOfSchema.required)) {
allOfSchema.required.forEach((req) => allRequired.add(req));
}
const allOfProperties = getSchemaProperties(
allOfSchema,
discriminator,
discriminatorValue
);
if (allOfProperties) {
mergeProperties(result, allOfProperties);
}
}
// Collect required fields from the schema itself
if (Array.isArray(schema.required)) {
schema.required.forEach((req) => allRequired.add(req));
}
// Include direct properties from the schema itself
mergeProperties(
result,
processSchemaProperties(schema, discriminator, discriminatorValue, allRequired)
);
// Update required status for all properties
result.forEach((prop) => {
if (prop.propertyName && allRequired.has(prop.propertyName)) {
prop.required = true;
}
});
return result.length > 0 ? result : null;
}
}
if (schema.type === 'object' || schema.properties) {
return processSchemaProperties(schema, discriminator, discriminatorValue);
const result: OpenAPISchemaPropertyEntry[] = [];
if (schema.properties) {
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
if (checkIsReference(propertySchema)) {
return;
}
result.push({
propertyName,
required: Array.isArray(schema.required)
? schema.required.includes(propertyName)
: undefined,
isDiscriminatorProperty: discriminator?.propertyName === propertyName,
schema: propertySchema,
});
});
}
if (schema.additionalProperties && !checkIsReference(schema.additionalProperties)) {
result.push({
propertyName: 'Other properties',
schema: schema.additionalProperties === true ? {} : schema.additionalProperties,
});
}
return result;
}
return null;
@@ -912,90 +688,34 @@ function flattenAlternatives(
): OpenAPIV3.SchemaObject[] {
// Get the parent schema's required fields from the most recent ancestor
const latestAncestor = Array.from(ancestors).pop();
const result: OpenAPIV3.SchemaObject[] = [];
for (const schemaOrRef of schemasOrRefs) {
return schemasOrRefs.reduce<OpenAPIV3.SchemaObject[]>((acc, schemaOrRef) => {
if (checkIsReference(schemaOrRef)) {
continue;
return acc;
}
const flattened = flattenSchema(schemaOrRef, alternativeType, ancestors, latestAncestor);
if (flattened) {
result.push(...flattened);
}
}
return result;
}
/**
* Flatten a schema that is an alternative of another schema.
*/
function flattenSchema(
schema: OpenAPIV3.SchemaObject,
alternativeType: AlternativeType,
ancestors: Set<OpenAPIV3.SchemaObject>,
latestAncestor: OpenAPIV3.SchemaObject | undefined
): OpenAPIV3.SchemaObject[] {
if (schema[alternativeType] && !ancestors.has(schema)) {
const alternatives = getSchemaAlternatives(schema, ancestors);
if (alternatives?.schemas) {
return alternatives.schemas.map((s) => {
const required = mergeRequiredFields(s, latestAncestor);
return {
...s,
...(required ? { required } : {}),
};
});
}
const required = mergeRequiredFields(schema, latestAncestor);
return [{ ...schema, ...(required ? { required } : {}) }];
}
// if a schema has allOf that can be safely merged, merge it
if (
(alternativeType === 'oneOf' || alternativeType === 'anyOf') &&
schema.allOf &&
Array.isArray(schema.allOf) &&
!ancestors.has(schema)
) {
const allOfSchemas = schema.allOf.filter(
(s): s is OpenAPIV3.SchemaObject => !checkIsReference(s)
);
if (allOfSchemas.length > 0) {
const merged = mergeAlternatives('allOf', allOfSchemas);
if (merged && merged.length > 0) {
// Only merge if all schemas were successfully merged into one (safe to merge)
if (merged.length === 1) {
return merged.map((s) => {
const required = mergeRequiredFields(s, latestAncestor);
const result: OpenAPIV3.SchemaObject = {
...s,
...(required ? { required } : {}),
};
if (schema.title && !s.title) {
result.title = schema.title;
}
return result;
});
}
if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) {
const alternatives = getSchemaAlternatives(schemaOrRef, ancestors);
if (alternatives?.schemas) {
acc.push(
...alternatives.schemas.map((schema) => ({
...schema,
required: mergeRequiredFields(schema, latestAncestor),
}))
);
}
return acc;
}
}
const required = mergeRequiredFields(schema, latestAncestor);
// For direct schemas, handle required fields
const schema = {
...schemaOrRef,
required: mergeRequiredFields(schemaOrRef, latestAncestor),
};
return [
{
...schema,
...(required ? { required } : {}),
},
];
acc.push(schema);
return acc;
}, []);
}
/**
@@ -34,6 +34,6 @@ export function isFormData(contentType?: string): boolean {
return !!contentType && contentType.toLowerCase().includes('multipart/form-data');
}
export function isPlainObject(value: unknown): value is Record<string, unknown> {
export function isPlainObject(value: unknown): boolean {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -1037,90 +1037,4 @@ describe('generateSchemaExample', () => {
})
).toBeUndefined();
});
it('merges object properties from oneOf -> allOf', () => {
const schema = {
type: 'object',
properties: {
discriminator: {
type: 'string',
},
},
oneOf: [
{
allOf: [
{
type: 'object',
properties: {
bar: {
type: 'string',
},
},
},
{
type: 'object',
properties: {
baz: {
type: 'number',
},
},
},
{
type: 'string', // This will return a string, but should be ignored
},
],
},
],
} satisfies OpenAPIV3.SchemaObject;
const result = generateSchemaExample(schema);
expect(result).toBeDefined();
expect(result).toHaveProperty('discriminator');
expect(result).toHaveProperty('bar');
expect(result).toHaveProperty('baz');
});
it('merges object properties from anyOf -> allOf', () => {
const schema = {
type: 'object',
properties: {
discriminator: {
type: 'string',
},
},
anyOf: [
{
allOf: [
{
type: 'object',
properties: {
bar: {
type: 'string',
},
},
},
{
type: 'object',
properties: {
baz: {
type: 'number',
},
},
},
{
type: 'string', // This will return a string, but should be ignored
},
],
},
],
} satisfies OpenAPIV3.SchemaObject;
const result = generateSchemaExample(schema);
expect(result).toBeDefined();
expect(result).toHaveProperty('discriminator');
expect(result).toHaveProperty('bar');
expect(result).toHaveProperty('baz');
});
});
@@ -1,5 +1,4 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { isPlainObject } from './contentTypeChecks';
import { checkIsReference } from './utils';
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
@@ -148,23 +147,6 @@ const getExampleFromSchema = (
return result;
}
// Process allOf items and merge object results into the response
function mergeAllOfIntoResponse(
allOfItems: Record<string, unknown>[],
response: Record<string, unknown>,
parent: Record<string, unknown> | undefined
): void {
const allOfResults = allOfItems
.map((item: Record<string, unknown>) =>
getExampleFromSchema(item, options, level + 1, parent, undefined, resultCache)
)
.filter(isPlainObject);
if (allOfResults.length > 0) {
Object.assign(response, ...allOfResults);
}
}
// Check if the result is already cached
if (resultCache.has(schema)) {
return resultCache.get(schema);
@@ -325,48 +307,45 @@ const getExampleFromSchema = (
}
if (schema.anyOf !== undefined) {
const anyOfItem = schema.anyOf[0];
if (anyOfItem) {
// If anyOf[0] has allOf, process allOf items individually to merge object results
if (anyOfItem?.allOf !== undefined && Array.isArray(anyOfItem.allOf)) {
mergeAllOfIntoResponse(anyOfItem.allOf, response, anyOfItem);
} else {
const anyOfResult = getExampleFromSchema(
anyOfItem,
options,
level + 1,
undefined,
undefined,
resultCache
);
if (isPlainObject(anyOfResult)) {
Object.assign(response, anyOfResult);
}
}
}
Object.assign(
response,
getExampleFromSchema(
schema.anyOf[0],
options,
level + 1,
undefined,
undefined,
resultCache
)
);
} else if (schema.oneOf !== undefined) {
const oneOfItem = schema.oneOf[0];
if (oneOfItem) {
// If oneOf[0] has allOf, process allOf items individually to merge object results
if (oneOfItem?.allOf !== undefined && Array.isArray(oneOfItem.allOf)) {
mergeAllOfIntoResponse(oneOfItem.allOf, response, oneOfItem);
} else {
const oneOfResult = getExampleFromSchema(
oneOfItem,
options,
level + 1,
undefined,
undefined,
resultCache
);
if (isPlainObject(oneOfResult)) {
Object.assign(response, oneOfResult);
}
}
}
Object.assign(
response,
getExampleFromSchema(
schema.oneOf[0],
options,
level + 1,
undefined,
undefined,
resultCache
)
);
} else if (schema.allOf !== undefined) {
mergeAllOfIntoResponse(schema.allOf, response, schema);
Object.assign(
response,
...schema.allOf
.map((item: Record<string, any>) =>
getExampleFromSchema(
item,
options,
level + 1,
schema,
undefined,
resultCache
)
)
.filter((item: any) => item !== undefined)
);
}
return cache(schema, response);
+10 -16
View File
@@ -218,10 +218,7 @@ function getStatusCodeCategory(statusCode: number | string): number | string {
return category;
}
export function getSchemaTitle(
schema: OpenAPIV3.SchemaObject,
options?: { ignoreAlternatives?: boolean }
): string {
export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
// Otherwise try to infer a nice title
let type = 'any';
@@ -229,7 +226,7 @@ export function getSchemaTitle(
type = `${schema.type} · enum`;
// check array AND schema.items as this is sometimes null despite what the type indicates
} else if (schema.type === 'array' && !!schema.items) {
type = `${getSchemaTitle(schema.items, options)}[]`;
type = `${getSchemaTitle(schema.items)}[]`;
} else if (Array.isArray(schema.type)) {
type = schema.type.join(' | ');
} else if (schema.type || schema.properties) {
@@ -245,17 +242,14 @@ export function getSchemaTitle(
}
}
// Skip alternative type labels if ignoreAlternatives is true (useful when rendering alternatives)
if (!options?.ignoreAlternatives) {
if ('anyOf' in schema) {
type = 'any of';
} else if ('oneOf' in schema) {
type = 'one of';
} else if ('allOf' in schema) {
type = 'all of';
} else if ('not' in schema) {
type = 'not';
}
if ('anyOf' in schema) {
type = 'any of';
} else if ('oneOf' in schema) {
type = 'one of';
} else if ('allOf' in schema) {
type = 'all of';
} else if ('not' in schema) {
type = 'not';
}
return type;