Compare commits

..

3 Commits

Author SHA1 Message Date
Tal Gluck 50cafe9ec6 linting 2025-12-03 14:24:35 +01:00
Tal Gluck 2f05549c72 update snippet generator 2025-12-03 12:19:41 +01:00
Tal Gluck 2da0ccf4ba update code sample generator for python 2025-12-03 12:03:29 +01:00
103 changed files with 1347 additions and 3116 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
-6
View File
@@ -1,6 +0,0 @@
---
"@gitbook/embed": minor
"gitbook": patch
---
Improve Docs Embed with separate Assistant and Docs tabs
-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
+50 -138
View File
@@ -126,7 +126,7 @@
"@gitbook/react-openapi": "workspace:*", "@gitbook/react-openapi": "workspace:*",
"@modelcontextprotocol/sdk": "1.17.5", "@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "^3.8.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-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15",
@@ -157,7 +157,7 @@
"micromark-extension-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"next": "15.4.10", "next": "15.4.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nuqs": "^2.2.3", "nuqs": "^2.2.3",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
@@ -223,8 +223,8 @@
"gitbook-icons": "./bin/gitbook-icons.js", "gitbook-icons": "./bin/gitbook-icons.js",
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
}, },
"devDependencies": { "devDependencies": {
"@types/react": "catalog:", "@types/react": "catalog:",
@@ -234,7 +234,7 @@
"typescript": "catalog:", "typescript": "catalog:",
}, },
"optionalDependencies": { "optionalDependencies": {
"@gitbook/fontawesome-pro": "1.0.16", "@gitbook/fontawesome-pro": "1.0.15",
}, },
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
@@ -346,7 +346,7 @@
"react-dom": "catalog:", "react-dom": "catalog:",
}, },
"catalog": { "catalog": {
"@gitbook/api": "0.154.0", "@gitbook/api": "0.151.0",
"@scalar/api-client-react": "^1.3.46", "@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6", "@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6", "@tsconfig/strictest": "^2.0.6",
@@ -356,8 +356,8 @@
"bidc": "^0.0.2", "bidc": "^0.0.2",
"bun-types": "^1.1.20", "bun-types": "^1.1.20",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"react": "^19.0.1", "react": "^19.0.0",
"react-dom": "^19.0.1", "react-dom": "^19.0.0",
"tsdown": "^0.15.6", "tsdown": "^0.15.6",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
@@ -385,25 +385,25 @@
"@argos-ci/util": ["@argos-ci/util@2.3.2", "", {}, "sha512-xtNHJxpWYNst/sMNn4Jv/vkODuFsJ+APr4FBeoFUdIa+Izjl4ZFHsYA2PUyu+ygIpQCkof8yZLL9U1/VpiyyIw=="], "@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=="], "@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 +721,13 @@
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.5.10", "", { "dependencies": { "tslib": "2" } }, "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q=="], "@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.151.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-5d9+rZ2u6CKKIiVHO1Toyk+7wHtTOXmP0+sVIE3teRkceX4z5FGIIpa4XsFeKC9XeosncvehdshPaOGQtSDpTQ=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"], "@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
@@ -741,7 +741,7 @@
"@gitbook/expr": ["@gitbook/expr@workspace:packages/expr"], "@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"], "@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"],
@@ -823,10 +823,6 @@
"@internationalized/string": ["@internationalized/string@3.2.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A=="], "@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/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=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
@@ -873,23 +869,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=="], "@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=="], "@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
@@ -925,9 +921,9 @@
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="], "@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=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -1543,6 +1539,8 @@
"@types/parse-cache-control": ["@types/parse-cache-control@1.0.4", "", {}, "sha512-YA9hcvRn3oYBdLK0PdWRsw5ehrqmfdCwfz68sSAxi2OB6L7B3jJEhrqlLfMvxnWfsZcUdbR6QI6NfkY1Y4e2zg=="], "@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": ["@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=="], "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
@@ -1637,6 +1635,8 @@
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], "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=="], "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
@@ -1731,10 +1731,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": ["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=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001668", "", {}, "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="], "caniuse-lite": ["caniuse-lite@1.0.30001668", "", {}, "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="],
@@ -1883,8 +1879,6 @@
"dts-resolver": ["dts-resolver@2.1.2", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg=="], "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=="], "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
@@ -1929,8 +1923,6 @@
"es-module-lexer": ["es-module-lexer@1.4.1", "", {}, "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="], "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=="], "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=="], "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 +2025,7 @@
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], "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=="], "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 +2065,6 @@
"get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="], "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-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=="], "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
@@ -2083,7 +2073,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "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=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -2235,7 +2225,7 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "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=="], "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
@@ -2365,8 +2355,6 @@
"markdown-table": ["markdown-table@3.0.3", "", {}, "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw=="], "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=="], "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=="], "mathml-tag-names": ["mathml-tag-names@2.1.3", "", {}, "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg=="],
@@ -2489,7 +2477,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=="], "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=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -2517,7 +2505,7 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "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=="], "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 +2663,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=="], "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": ["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=="], "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 +2775,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": ["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=="], "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=="], "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
@@ -2977,7 +2961,7 @@
"url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], "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=="], "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 +3647,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=="], "@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=="], "@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=="], "@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 +3701,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=="], "@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=="], "@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=="], "@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 +4009,8 @@
"@types/jsonwebtoken/@types/node": ["@types/node@20.16.11", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw=="], "@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/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=="], "@types/ws/@types/node": ["@types/node@20.16.11", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw=="],
@@ -4089,8 +4071,6 @@
"bun-types/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], "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/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=="], "cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
@@ -4113,8 +4093,6 @@
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "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/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="],
"edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], "edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
@@ -4137,8 +4115,6 @@
"finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], "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=="], "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=="], "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 +4233,6 @@
"send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "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=="], "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=="], "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 +4777,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=="], "@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-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=="], "@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 +4979,6 @@
"bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "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/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=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
@@ -5121,8 +5067,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=="], "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/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -5131,18 +5075,6 @@
"send/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "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=="], "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=="], "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
@@ -5293,16 +5225,6 @@
"@node-minify/core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "@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-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=="], "@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 +5311,6 @@
"@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "@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/@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=="], "@scalar/draggable/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
@@ -5420,9 +5336,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/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=="], "@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": { "catalog": {
"@tsconfig/strictest": "^2.0.6", "@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6", "@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.154.0", "@gitbook/api": "0.151.0",
"@scalar/api-client-react": "^1.3.46", "@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",
@@ -49,8 +49,8 @@
"bidc": "^0.0.2", "bidc": "^0.0.2",
"bun-types": "^1.1.20", "bun-types": "^1.1.20",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"react": "^19.0.1", "react": "^19.0.0",
"react-dom": "^19.0.1", "react-dom": "^19.0.0",
"tsdown": "^0.15.6", "tsdown": "^0.15.6",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"usehooks-ts": "^3.1.1" "usehooks-ts": "^3.1.1"
+8 -370
View File
@@ -1,61 +1,24 @@
# GitBook Docs Embed (`@gitbook/embed`) # `@gitbook/embed`
Embed your GitBook docs in your product or website. Embed the GitBook Docs Assistant in your product or website.
The Docs Embed can contain two tabs:
- **Assistant**: The [GitBook Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) - an AI-powered chat interface to help users find answers
- **Docs**: A browser for navigating your documentation site
The embed is set up automatically based on your site's configuration. You can optionally customize and override the configuration with custom actions, tools, suggested questions, [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access), and more. See the [Configuration](#configuration) section for all available options.
# Usage # Usage
## As a standalone script from your docs site ## As a script from your docs site
All GitBook docs sites include a script to easily add the Docs Embed as a widget on your website. All GitBook docs site includes a script to easily embed the docs assistant as a widget on your website.
The script is served at `https://docs.company.com/~gitbook/embed/script.js`. The script is served at `https://docs.company.com/~gitbook/embed/script.js`.
You can find the embed script from your docs site settings, or you can copy the following and replace `docs.company.com` with your docs site hostname. You can find the embed script from your docs site settings, or you can copy the following and replace the `docs.company.com` by your docs site hostname.
```html ```html
<script src="https://docs.company.com/~gitbook/embed/script.js"></script> <script src="https://docs.company.com/~gitbook/embed/script.js"></script>
<script> <script>
// Initialize with Authenticated Access (optional)
window.GitBook('init',
{ siteURL: 'https://docs.company.com' },
{ visitor: { token: 'your-jwt-token' } }
);
window.GitBook('show'); window.GitBook('show');
</script> </script>
``` ```
The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods.
### Example: Configuring the widget
```javascript
GitBook('configure', {
button: {
label: 'Ask',
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
},
tabs: ['assistant', 'docs'],
actions: [
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
],
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
suggestions: ['What is GitBook?', 'How do I get started?'],
tools: [/* ... */]
});
```
See the [Configuration](#configuration) section for all available options.
## As a package from NPM ## As a package from NPM
Install the package: `npm install @gitbook/embed` and import it in your web application: Install the package: `npm install @gitbook/embed` and import it in your web application:
@@ -67,46 +30,10 @@ const gitbook = createGitBook({
siteURL: 'https://docs.company.com' siteURL: 'https://docs.company.com'
}); });
// Create an iframe and get its URL
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.src = gitbook.getFrameURL({ iframe.src = gitbook.getFrameURL();
visitor: {
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
unsignedClaims: { // Optional: custom claims for dynamic expressions
userId: '123',
plan: 'premium'
}
}
});
// Create a frame client to communicate with the iframe
const frame = gitbook.createFrame(iframe); const frame = gitbook.createFrame(iframe);
// Use the frame client methods
frame.navigateToPage('/getting-started'); // Navigate to a page in the docs tab
frame.navigateToAssistant(); // Switch to the assistant tab
frame.postUserMessage('How do I get started?');
frame.clearChat();
// Configure the embed (see Configuration section for all options)
frame.configure({
tabs: ['assistant', 'docs'],
actions: [
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
],
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
suggestions: ['What is GitBook?', 'How do I get started?'],
tools: [/* ... */]
});
// Listen to events
frame.on('close', () => {
console.log('Frame closed');
});
``` ```
## As React components ## As React components
@@ -114,298 +41,9 @@ frame.on('close', () => {
After installing the NPM package, you can import prebuilt React components: After installing the NPM package, you can import prebuilt React components:
```tsx ```tsx
import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react'; import { GitBookProvider, GitBookAssistantFrame } from '@gitbook/embed/react';
<GitBookProvider siteURL="https://docs.company.com"> <GitBookProvider siteURL="https://docs.company.com">
<GitBookFrame <GitBookAssistantFrame />
visitor={{
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
}}
tabs={['assistant', 'docs']}
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
suggestions={['What is GitBook?', 'How do I get started?']}
actions={[
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
]}
tools={[/* ... */]}
/>
</GitBookProvider> </GitBookProvider>
``` ```
You can also use the `useGitBook` hook to access the client:
```tsx
import { useGitBook } from '@gitbook/embed/react';
function MyComponent() {
const gitbook = useGitBook();
const frameURL = gitbook.getFrameURL({ visitor: { token: '...' } });
// ...
}
```
# API Reference
## Method Comparison
| Method | Standalone Script | NPM Package | React Components |
|--------|------------------|-------------|------------------|
| **Initialize** | `GitBook('init', options, frameOptions)` | `createGitBook(options)` | `<GitBookProvider siteURL="...">` |
| **Get frame URL** | ❌ (handled internally) | `client.getFrameURL(options)` | `useGitBook().getFrameURL(options)` |
| **Create frame client** | ❌ (handled internally) | `client.createFrame(iframe)` | `useGitBook().createFrame(iframe)` |
| **Show/Hide widget** | `GitBook('show')` / `GitBook('hide')` | ❌ | ❌ |
| **Open/Close window** | `GitBook('open')` / `GitBook('close')` / `GitBook('toggle')` | ❌ | ❌ |
| **Navigate to page** | `GitBook('navigateToPage', path)` | `frame.navigateToPage(path)` | Via frame client |
| **Navigate to assistant** | `GitBook('navigateToAssistant')` | `frame.navigateToAssistant()` | Via frame client |
| **Post message** | `GitBook('postUserMessage', message)` | `frame.postUserMessage(message)` | Via frame client |
| **Clear chat** | `GitBook('clearChat')` | `frame.clearChat()` | Via frame client |
| **Configure** | `GitBook('configure', settings)` | `frame.configure(settings)` | Props on `<GitBookFrame>` |
| **Event listeners** | ❌ | `frame.on(event, listener)` | Via frame client |
| **Unload** | `GitBook('unload')` | ❌ | ❌ |
## Method Signatures
### Standalone Script
- `GitBook('init', options: { siteURL: string }, frameOptions?: { visitor?: {...} })` - Initialize widget
- `GitBook('show')` - Show widget button
- `GitBook('hide')` - Hide widget button
- `GitBook('open')` - Open widget window
- `GitBook('close')` - Close widget window
- `GitBook('toggle')` - Toggle widget window
- `GitBook('navigateToPage', path: string)` - Navigate to page
- `GitBook('navigateToAssistant')` - Navigate to assistant tab
- `GitBook('postUserMessage', message: string)` - Post message to chat
- `GitBook('clearChat')` - Clear chat history
- `GitBook('configure', settings: {...})` - Configure widget
- `GitBook('unload')` - Unload widget
### NPM Package
**Client Factory:**
- `createGitBook(options: { siteURL: string })``GitBookClient`
- `client.getFrameURL(options?: { visitor?: {...} })``string`
- `client.createFrame(iframe: HTMLIFrameElement)``GitBookFrameClient`
**Frame Client:**
- `frame.navigateToPage(path: string)``void`
- `frame.navigateToAssistant()``void`
- `frame.postUserMessage(message: string)``void`
- `frame.clearChat()``void`
- `frame.configure(settings: Partial<GitBookEmbeddableConfiguration>)``void`
- `frame.on(event: string, listener: Function)``() => void` (unsubscribe)
### React Components
**Components:**
- `<GitBookProvider siteURL: string>` - Provider component
- `<GitBookFrame {...props}>` - Frame component (accepts all config options as props)
**Hooks:**
- `useGitBook()``GitBookClient` (must be used within `<GitBookProvider>`)
# Configuration
Configuration options are available across usage methods as follows:
- **Standalone script**: via `GitBook('configure', {...})`
- **NPM package**: via `frame.configure({...})`
- **React components**: via props on `<GitBookFrame>`
### `tabs`
Available in: Standalone script, NPM package, React components
Override which tabs are displayed. Defaults to your site's configuration.
- **Type**: `('assistant' | 'docs')[]`
- **Options**:
- `['assistant', 'docs']` - Show both tabs
- `['assistant']` - Show only the assistant tab
- `['docs']` - Show only the docs tab
```javascript
tabs: ['assistant', 'docs']
```
### `actions`
Available in: Standalone script, NPM package, React components
Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked.
**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
- **Type**: `GitBookEmbeddableActionDefinition[]`
- **Properties**:
- `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...)
- `label`: `string` - Button label text
- `onClick`: `() => void | Promise<void>` - Callback function when clicked
```javascript
actions: [
{
icon: 'comments',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
},
{
icon: 'rocket',
label: 'Get started',
onClick: () => {
GitBook('navigateToPage', '/getting-started');
}
}
]
```
### `greeting`
Available in: Standalone script, NPM package, React components
Welcome message displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) tab.
- **Type**: `{ title: string, subtitle: string }`
```javascript
greeting: {
title: 'Welcome!',
subtitle: 'How can I help you today?'
}
```
### `suggestions`
Available in: Standalone script, NPM package, React components
Suggested questions displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) welcome screen.
- **Type**: `string[]`
```javascript
suggestions: [
'What is GitBook?',
'How do I get started?',
'What can you do?'
]
```
### `tools`
Available in: Standalone script, NPM package, React components
Custom AI tools to extend the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant). Tools allow the assistant to execute functions and integrate with your own systems.
**Note**: In addition to custom tools, the Assistant will always have access to any [MCP servers you define](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant#extend-gitbook-assistant-with-mcp-servers) in your site's AI settings.
- **Type**: `GitBookToolDefinition[]`
- **Properties**:
- `name`: `string` - Unique tool identifier
- `description`: `string` - Description of what the tool does (used by the AI to decide when and how to use it).
- `inputSchema`: `object` - JSON schema defining the tool's input parameters
- `execute`: `(input: object) => Promise<{ output: any, summary: string }>` - Async function that executes the tool.
- `output`: The result of the tool execution, provided to the AI to continue working with. Not shown to the user.
- `summary`: The visual summary of the tool execution, shown in the user's chat window.
- `confirmation`: `{ icon?: string, label: string }` (optional) - Confirmation button shown before execution, useful for actions that require the user's express approval.
```javascript
tools: [
{
name: 'get_user_info',
description: 'Get information about the current user',
inputSchema: {
type: 'object',
properties: {
userId: {
type: 'string',
description: 'The user ID to look up'
}
},
required: ['userId']
},
execute: async (input) => {
const user = await fetch(`/api/users/${input.userId}`).then(r => r.json());
return {
output: { name: user.name, plan: user.plan },
summary: `Retrieved info for user ${user.name}`
};
}
},
{
name: 'create_ticket',
description: 'Create a support ticket',
confirmation: {
icon: 'circle-question',
label: 'Create support ticket?'
},
inputSchema: {
type: 'object',
properties: {
subject: { type: 'string' },
description: { type: 'string' }
},
required: ['subject', 'description']
},
execute: async (input) => {
const ticket = await fetch('/api/tickets', {
method: 'POST',
body: JSON.stringify(input)
}).then(r => r.json());
return {
output: { ticketId: ticket.id },
summary: `Created ticket #${ticket.id}`
};
}
}
]
```
### `visitor` (Authenticated Access)
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
[Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) options passed when creating the frame URL. Used for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) and [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access).
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
**NPM package**: Pass to `getFrameURL({ visitor: {...} })`
**React components**: Pass as the `visitor` prop on `<GitBookFrame>`
- **Type**: `{ token?: string, unsignedClaims?: Record<string, unknown> }`
- **Properties**:
- `token`: `string` (optional) - Signed JWT token for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) or [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access)
- `unsignedClaims`: `Record<string, unknown>` (optional) - Unsigned claims that can be used in dynamic expressions via `visitor.claims.unsigned.<claim-name>`
```javascript
visitor: {
token: 'your-jwt-token',
unsignedClaims: {
userId: '123',
plan: 'premium',
role: 'admin'
}
}
```
### `button`
Available in: Standalone script only
Configure the widget button for the standalone script. This option is not available when using the NPM package or React components, since they can be customized completely.
- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }`
- **Properties**:
- `label`: `string` - Button label text
- `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets.
```javascript
button: {
label: 'Ask',
icon: 'assistant'
}
```
+1 -1
View File
@@ -40,7 +40,7 @@ export function createGitBook(options: CreateGitBookOptions) {
const client: GitBookClient = { const client: GitBookClient = {
getFrameURL: (frameOptions) => { getFrameURL: (frameOptions) => {
const url = new URL(options.siteURL); const url = new URL(options.siteURL);
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`; url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed/assistant`;
if (frameOptions.visitor?.token) { if (frameOptions.visitor?.token) {
url.searchParams.set('token', frameOptions.visitor.token); url.searchParams.set('token', frameOptions.visitor.token);
@@ -64,9 +64,8 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien
const events = new Map<string, Array<(...args: any[]) => void>>(); const events = new Map<string, Array<(...args: any[]) => void>>();
const configuration: GitBookEmbeddableConfiguration = { const configuration: GitBookEmbeddableConfiguration = {
tabs: ['assistant', 'docs'], buttons: [],
actions: [], welcomeMessage: '',
greeting: { title: '', subtitle: '' },
suggestions: [], suggestions: [],
tools: [], tools: [],
}; };
+5 -15
View File
@@ -23,7 +23,7 @@ export type GitBookToolDefinition = AIToolDefinition & {
/** /**
* Custom button definition to be passed to the embeddable GitBook. * Custom button definition to be passed to the embeddable GitBook.
*/ */
export type GitBookEmbeddableActionDefinition = { export type GitBookEmbeddableButtonDefinition = {
/** /**
* Icon to be displayed in the button. * Icon to be displayed in the button.
*/ */
@@ -41,26 +41,16 @@ export type GitBookEmbeddableActionDefinition = {
}; };
/** /**
* Overall configuration for the layout of the GitBook embed. * Overall configuration for the layout of the embeddable GitBook.
*/ */
export type GitBookEmbeddableConfiguration = { export type GitBookEmbeddableConfiguration = {
/** Tabs to display in the embed (if enabled on the site). */
tabs: ('assistant' | 'docs')[];
/** Additional buttons to be displayed in the header of the GitBook embed. */
actions: GitBookEmbeddableActionDefinition[];
/** /**
* Additional buttons to be displayed in the header of the GitBook embed. * Buttons to be displayed in the header of the embeddable GitBook.
* @deprecated Use `actions` instead.
*/ */
buttons?: GitBookEmbeddableActionDefinition[]; buttons: GitBookEmbeddableButtonDefinition[];
/** Message to be displayed in the welcome page. */ /** Message to be displayed in the welcome page. */
greeting: { welcomeMessage: string;
title: string;
subtitle: string;
};
/** Suggestions of questions to be displayed in the welcome page. */ /** Suggestions of questions to be displayed in the welcome page. */
suggestions: string[]; suggestions: string[];
+4 -5
View File
@@ -17,7 +17,7 @@ export type GitBookFrameProps = {
* Render a frame with the GitBook Assistant in it. * Render a frame with the GitBook Assistant in it.
*/ */
export function GitBookFrame(props: GitBookFrameProps) { export function GitBookFrame(props: GitBookFrameProps) {
const { className, visitor, actions, greeting, suggestions, tools } = props; const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
const frameRef = useRef<HTMLIFrameElement>(null); const frameRef = useRef<HTMLIFrameElement>(null);
const gitbook = useGitBook(); const gitbook = useGitBook();
@@ -33,13 +33,12 @@ export function GitBookFrame(props: GitBookFrameProps) {
useEffect(() => { useEffect(() => {
gitbookFrame?.configure({ gitbookFrame?.configure({
tabs: ['assistant', 'docs'], buttons,
actions, welcomeMessage,
greeting,
suggestions, suggestions,
tools, tools,
}); });
}, [gitbookFrame, actions, greeting, suggestions, tools]); }, [gitbookFrame, buttons, welcomeMessage, suggestions, tools]);
return ( return (
<iframe <iframe
+28 -47
View File
@@ -31,50 +31,24 @@ type StandaloneCalls =
// Clear the chat // Clear the chat
| ['clearChat'] | ['clearChat']
// Configure the embed // Configure the embed
| ['configure', Partial<GitBookEmbeddableConfiguration & StandaloneConfiguration>] | ['configure', Partial<GitBookEmbeddableConfiguration>]
// Navigate to a page // Navigate to a page
| ['navigateToPage', string] | ['navigateToPage', string]
// Navigate to the assistant // Navigate to the assistant
| ['navigateToAssistant']; | ['navigateToAssistant'];
type StandaloneConfiguration = {
/** Configure the button to open the embed */
button: {
/** Label to be displayed in the button. */
label: string;
/** Icon to be displayed in the button. */
icon: 'assistant' | 'sparkle' | 'help' | 'book';
};
};
export type GitBookStandalone = ((...args: StandaloneCalls) => void) & { export type GitBookStandalone = ((...args: StandaloneCalls) => void) & {
q?: StandaloneCalls[]; q?: StandaloneCalls[];
}; };
let widgetIframe: HTMLIFrameElement | undefined;
let _client: GitBookClient | undefined;
let _frame: GitBookFrameClient | undefined;
let frameOptions: GetFrameURLOptions | undefined;
let frameConfiguration: GitBookEmbeddableConfiguration & StandaloneConfiguration = {
button: {
label: 'Ask',
icon: 'assistant',
},
actions: [],
greeting: { title: '', subtitle: '' },
suggestions: [],
tools: [],
tabs: ['assistant', 'docs'],
};
const widgetButton = document.createElement('button'); const widgetButton = document.createElement('button');
widgetButton.id = 'gitbook-widget-button'; widgetButton.id = 'gitbook-widget-button';
widgetButton.addEventListener('click', () => { widgetButton.addEventListener('click', () => {
GitBook('toggle'); GitBook('toggle');
}); });
widgetButton.innerHTML = ` widgetButton.innerHTML = `
<span id="gitbook-widget-button-icon" data-icon="${frameConfiguration.button.icon}"></span> <span id="gitbook-widget-button-icon"></span>
<span id="gitbook-widget-button-label">${frameConfiguration.button.label}</span> <span id="gitbook-widget-button-label">Ask</span>
`; `;
const widgetWindow = document.createElement('div'); const widgetWindow = document.createElement('div');
@@ -84,6 +58,17 @@ widgetWindow.classList.add('hidden');
document.body.appendChild(widgetButton); document.body.appendChild(widgetButton);
document.body.appendChild(widgetWindow); document.body.appendChild(widgetWindow);
let widgetIframe: HTMLIFrameElement | undefined;
let _client: GitBookClient | undefined;
let _frame: GitBookFrameClient | undefined;
let frameOptions: GetFrameURLOptions | undefined;
let frameConfiguration: GitBookEmbeddableConfiguration = {
buttons: [],
welcomeMessage: '',
suggestions: [],
tools: [],
};
function getClient() { function getClient() {
if (!_client) { if (!_client) {
throw new Error( throw new Error(
@@ -150,31 +135,27 @@ const GitBook = (...args: StandaloneCalls) => {
case 'postUserMessage': case 'postUserMessage':
getIframe().frame.postUserMessage(args[1]); getIframe().frame.postUserMessage(args[1]);
break; break;
case 'configure': { case 'configure':
const settings = args[1];
frameConfiguration = { frameConfiguration = {
...frameConfiguration, ...frameConfiguration,
...settings, ...args[1],
}; };
// Update the button label and icon
if (settings.button?.label) {
const label = widgetButton.querySelector('#gitbook-widget-button-label');
if (label) {
label.textContent = settings.button.label;
}
}
if (settings.button?.icon) {
const icon = widgetButton.querySelector('#gitbook-widget-button-icon');
if (icon) {
icon.setAttribute('data-icon', settings.button.icon);
}
}
getIframe().frame.configure({ getIframe().frame.configure({
...frameConfiguration, ...frameConfiguration,
buttons: [
...frameConfiguration.buttons,
// Always include a close button
{
icon: 'close',
label: 'Close',
onClick: () => {
GitBook('close');
},
},
],
}); });
break; break;
}
case 'clearChat': case 'clearChat':
getIframe().frame.clearChat(); getIframe().frame.clearChat();
break; break;
-12
View File
@@ -113,18 +113,6 @@
background-color: currentColor; background-color: currentColor;
} }
#gitbook-widget-button-icon[data-icon="sparkle"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/sparkle.svg?v=2&token=a463935e93");
}
#gitbook-widget-button-icon[data-icon="help"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/circle-question.svg?v=2&token=a463935e93");
}
#gitbook-widget-button-icon[data-icon="book"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/book-open.svg?v=2&token=a463935e93");
}
#gitbook-widget-button.open #gitbook-widget-button-icon { #gitbook-widget-button.open #gitbook-widget-button-icon {
mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93'); mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93');
} }
+2 -2
View File
@@ -19,7 +19,7 @@
"@gitbook/react-openapi": "workspace:*", "@gitbook/react-openapi": "workspace:*",
"@modelcontextprotocol/sdk": "1.17.5", "@modelcontextprotocol/sdk": "1.17.5",
"@opennextjs/aws": "^3.8.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-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15",
@@ -50,7 +50,7 @@
"micromark-extension-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"next": "15.4.10", "next": "15.4.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nuqs": "^2.2.3", "nuqs": "^2.2.3",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
@@ -16,7 +16,7 @@ export default async function Page(props: PageProps) {
const { context } = await getDynamicSiteContext(params); const { context } = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(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> { export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -23,8 +23,7 @@ export default async function SiteDynamicLayout({
return ( return (
<CustomizationRootLayout <CustomizationRootLayout
htmlClassName="sheet-open:gutter-stable" className="site-background"
bodyClassName="site-background"
forcedTheme={forcedTheme} forcedTheme={forcedTheme}
context={context} context={context}
> >
@@ -1,28 +1,7 @@
import type { RouteLayoutParams } from '@/app/utils';
import { EmbeddableAssistantPage } from '@/components/Embeddable'; import { EmbeddableAssistantPage } from '@/components/Embeddable';
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
import { CustomizationAIMode } from '@gitbook/api';
import { redirect } from 'next/navigation';
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export const dynamic = 'force-static'; export const dynamic = 'force-static';
export default async function Page(props: PageProps) { export default async function Page() {
const params = await props.params; return <EmbeddableAssistantPage />;
const { context } = await getEmbeddableDynamicContext(params);
// If the assistant is not enabled, redirect to the docs
if (context.customization.ai.mode !== CustomizationAIMode.Assistant) {
redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`);
}
return (
<EmbeddableAssistantPage
baseURL={context.linker.toPathInSite('~gitbook/embed/')}
siteTitle={context.site.title}
/>
);
} }
@@ -1,21 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import { getEmbeddableDynamicContext } from '@/lib/embeddable';
import { CustomizationAIMode } from '@gitbook/api';
import { redirect } from 'next/navigation';
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableDynamicContext(params);
const baseURL = context.linker.toPathInSite('~gitbook/embed/');
// If assistant is enabled, redirect to assistant, otherwise to docs
if (context.customization.ai.mode === CustomizationAIMode.Assistant) {
redirect(`${baseURL}/assistant`);
} else {
redirect(`${baseURL}/page/`);
}
}
@@ -12,7 +12,7 @@ export default async function Page(props: PageProps) {
const { context } = await getEmbeddableDynamicContext(params); const { context } = await getEmbeddableDynamicContext(params);
const pathname = getPagePathFromParams(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> { 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 { context } = await getStaticSiteContext(params);
const pathname = getPagePathFromParams(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> { export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -19,11 +19,7 @@ export default async function SiteStaticLayout({
const withTracking = shouldTrackEvents(); const withTracking = shouldTrackEvents();
return ( return (
<CustomizationRootLayout <CustomizationRootLayout className="site-background" context={context}>
htmlClassName="sheet-open:gutter-stable"
bodyClassName="site-background"
context={context}
>
<SiteLayout <SiteLayout
context={context} context={context}
withTracking={withTracking} withTracking={withTracking}
@@ -1,28 +1,7 @@
import type { RouteParams } from '@/app/utils';
import { EmbeddableAssistantPage } from '@/components/Embeddable'; import { EmbeddableAssistantPage } from '@/components/Embeddable';
import { getEmbeddableStaticContext } from '@/lib/embeddable';
import { CustomizationAIMode } from '@gitbook/api';
import { redirect } from 'next/navigation';
export const dynamic = 'force-static'; export const dynamic = 'force-static';
type PageProps = { export default async function Page() {
params: Promise<RouteParams>; return <EmbeddableAssistantPage />;
};
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableStaticContext(params);
// If the assistant is not enabled, redirect to the docs
if (context.customization.ai.mode !== CustomizationAIMode.Assistant) {
redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`);
}
return (
<EmbeddableAssistantPage
baseURL={context.linker.toPathInSite('~gitbook/embed/')}
siteTitle={context.site.title}
/>
);
} }
@@ -1,23 +0,0 @@
import type { RouteLayoutParams } from '@/app/utils';
import { getEmbeddableStaticContext } from '@/lib/embeddable';
import { CustomizationAIMode } from '@gitbook/api';
import { redirect } from 'next/navigation';
export const dynamic = 'force-static';
type PageProps = {
params: Promise<RouteLayoutParams>;
};
export default async function Page(props: PageProps) {
const params = await props.params;
const { context } = await getEmbeddableStaticContext(params);
const baseURL = context.linker.toPathInSite('~gitbook/embed/');
// If assistant is enabled, redirect to assistant, otherwise to docs
if (context.customization.ai.mode === CustomizationAIMode.Assistant) {
redirect(`${baseURL}/assistant`);
} else {
redirect(`${baseURL}/page/`);
}
}
@@ -14,7 +14,7 @@ export default async function Page(props: PageProps) {
const { context } = await getEmbeddableStaticContext(params); const { context } = await getEmbeddableStaticContext(params);
const pathname = getPagePathFromParams(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> { export async function generateMetadata(props: PageProps): Promise<Metadata> {
+8 -28
View File
@@ -3,7 +3,7 @@ import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@
import { getDynamicCustomizationSettings } from '@/lib/customization'; import { getDynamicCustomizationSettings } from '@/lib/customization';
import type { SiteAPIToken } from '@gitbook/api'; import type { SiteAPIToken } from '@gitbook/api';
import { jwtDecode } from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
import { forbidden, notFound } from 'next/navigation'; import { forbidden } from 'next/navigation';
import rison from 'rison'; import rison from 'rison';
export type RouteParamMode = 'url-host' | 'url'; export type RouteParamMode = 'url-host' | 'url';
@@ -80,27 +80,14 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) {
* Get the decoded page path from the params. * Get the decoded page path from the params.
*/ */
export function getPagePathFromParams(params: RouteParams) { export function getPagePathFromParams(params: RouteParams) {
// If decoding the param fails, return a 404 instead of crashing const decoded = decodeURIComponent(params.pagePath);
try { return decoded;
const decoded = decodeURIComponent(params.pagePath);
return decoded;
} catch (error) {
console.error(
`Returning 404 after failing to decode page path ${params.pagePath}: ${error}`
);
notFound();
}
} }
function getSiteURLFromParams(params: RouteLayoutParams) { function getSiteURLFromParams(params: RouteLayoutParams) {
try { const decoded = decodeURIComponent(params.siteURL);
const decoded = decodeURIComponent(params.siteURL); const url = new URL(`https://${decoded}`);
const url = new URL(`https://${decoded}`); return url;
return url;
} catch (error) {
console.error(`Returning 404 after failing to decode site URL ${params.siteURL}: ${error}`);
notFound();
}
} }
function getModeFromParams(mode: string): RouteParamMode { function getModeFromParams(mode: string): RouteParamMode {
@@ -115,13 +102,6 @@ function getModeFromParams(mode: string): RouteParamMode {
* Get the decoded site data from the params. * Get the decoded site data from the params.
*/ */
function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData { function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData {
try { const decoded = decodeURIComponent(params.siteData);
const decoded = decodeURIComponent(params.siteData); return rison.decode(decoded);
return rison.decode(decoded);
} catch (error) {
console.error(
`Returning 404 after failing to decode site data ${params.siteData}: ${error}`
);
notFound();
}
} }
@@ -128,10 +128,10 @@ function parseResponse<T>(
parse: (response: AIStreamResponse) => T | undefined | Promise<T | undefined> parse: (response: AIStreamResponse) => T | undefined | Promise<T | undefined>
): { ): {
stream: EventIterator<T>; stream: EventIterator<T>;
response: Promise<{ responseId: string | null }>; response: Promise<{ responseId: string }>;
} { } {
let resolveResponse: (value: { responseId: string | null }) => void; let resolveResponse: (value: { responseId: string }) => void;
const response = new Promise<{ responseId: string | null }>((resolve) => { const response = new Promise<{ responseId: string }>((resolve) => {
resolveResponse = resolve; resolveResponse = resolve;
}); });
@@ -147,7 +147,7 @@ function parseResponse<T>(
if (event.type === 'response_finish') { if (event.type === 'response_finish') {
foundResponse = true; foundResponse = true;
resolveResponse({ responseId: event.response.id ?? null }); resolveResponse({ responseId: event.responseId });
} }
} }
+2 -11
View File
@@ -15,12 +15,7 @@ import { useSearch } from '../Search/useSearch';
// Unify assistants configuration context with the assistants hook in one place // Unify assistants configuration context with the assistants hook in one place
export type AIConfig = { export type AIConfig = {
aiMode: CustomizationAIMode; aiMode: CustomizationAIMode;
suggestions?: string[];
trademark: boolean; trademark: boolean;
greeting?: {
title: string;
subtitle: string;
};
}; };
export type Assistant = Omit<GitBookAssistant, 'icon'> & { export type Assistant = Omit<GitBookAssistant, 'icon'> & {
@@ -54,11 +49,8 @@ export type Assistant = Omit<GitBookAssistant, 'icon'> & {
const AIContext = React.createContext<AIConfig | null>(null); const AIContext = React.createContext<AIConfig | null>(null);
export function AIContextProvider(props: React.PropsWithChildren<AIConfig>): React.ReactElement { export function AIContextProvider(props: React.PropsWithChildren<AIConfig>): React.ReactElement {
const { aiMode, trademark, suggestions, greeting, children } = props; const { aiMode, trademark, children } = props;
const value = React.useMemo( const value = React.useMemo(() => ({ aiMode, trademark }), [aiMode, trademark]);
() => ({ aiMode, trademark, suggestions, greeting }),
[aiMode, trademark, suggestions, greeting]
);
return <AIContext.Provider value={value}>{children}</AIContext.Provider>; return <AIContext.Provider value={value}>{children}</AIContext.Provider>;
} }
@@ -96,7 +88,6 @@ export function useAI(): AIContext {
<AIChatIcon <AIChatIcon
state={chat.loading ? 'thinking' : 'default'} state={chat.loading ? 'thinking' : 'default'}
trademark={config.trademark} trademark={config.trademark}
className="size-4"
/> />
), ),
open: (query?: string) => { open: (query?: string) => {
@@ -87,19 +87,6 @@ export type AIChatState = {
error: boolean; error: boolean;
}; };
export type AIChatEvent =
| { type: 'open' }
| { type: 'postMessage'; message: string }
| { type: 'clear' }
| { type: 'close' };
type AIChatEventData<T extends AIChatEvent['type']> = Omit<
Extract<AIChatEvent, { type: T }>,
'type'
>;
type AIChatEventListener = (input?: Omit<AIChatEvent, 'type'>) => void;
export type AIChatController = { export type AIChatController = {
/** Open the dialog */ /** Open the dialog */
open: () => void; open: () => void;
@@ -109,11 +96,6 @@ export type AIChatController = {
postMessage: (input: { message: string }) => void; postMessage: (input: { message: string }) => void;
/** Clear the conversation */ /** Clear the conversation */
clear: () => void; clear: () => void;
/** Register an event listener */
on: <T extends AIChatEvent['type']>(
event: T,
listener: (input?: AIChatEventData<T>) => void
) => () => void;
}; };
const AIChatControllerContext = React.createContext<AIChatController | null>(null); const AIChatControllerContext = React.createContext<AIChatController | null>(null);
@@ -141,17 +123,6 @@ export function useAIChatState(): AIChatState {
return state; return state;
} }
function notify(
listeners: AIChatEventListener[] | undefined,
input: Omit<AIChatEvent, 'type'>
): void {
if (!listeners) return;
// Defer event listeners to next tick so React can process state updates first
setTimeout(() => {
listeners.forEach((listener) => listener(input));
}, 0);
}
/** /**
* Provide the controller to interact with the AI chat. * Provide the controller to interact with the AI chat.
*/ */
@@ -166,9 +137,6 @@ export function AIChatProvider(props: {
const [, setSearchState] = useSearch(); const [, setSearchState] = useSearch();
const language = useLanguage(); const language = useLanguage();
// Event listeners storage
const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map());
// Open AI chat and sync with search state // Open AI chat and sync with search state
const onOpen = React.useCallback(() => { const onOpen = React.useCallback(() => {
const { initialQuery } = globalState.getState(); const { initialQuery } = globalState.getState();
@@ -181,8 +149,6 @@ export function AIChatProvider(props: {
scope: prev?.scope ?? 'default', scope: prev?.scope ?? 'default',
open: false, // Close search popover when opening chat open: false, // Close search popover when opening chat
})); }));
notify(eventsRef.current.get('open'), {});
}, [setSearchState]); }, [setSearchState]);
// Close AI chat and clear ask parameter // Close AI chat and clear ask parameter
@@ -196,8 +162,6 @@ export function AIChatProvider(props: {
scope: prev?.scope ?? 'default', scope: prev?.scope ?? 'default',
open: false, open: false,
})); }));
notify(eventsRef.current.get('close'), {});
}, [setSearchState]); }, [setSearchState]);
// Stream a message with the AI backend // Stream a message with the AI backend
@@ -296,7 +260,7 @@ export function AIChatProvider(props: {
case 'response_finish': { case 'response_finish': {
globalState.setState((state) => ({ globalState.setState((state) => ({
...state, ...state,
responseId: event.response.id ?? null, responseId: event.responseId,
// Mark as not loading when the response is finished // Mark as not loading when the response is finished
// Even if the stream might continue as we receive 'response_followup_suggestion' // Even if the stream might continue as we receive 'response_followup_suggestion'
loading: false, loading: false,
@@ -415,14 +379,8 @@ export function AIChatProvider(props: {
})); }));
} }
notify(eventsRef.current.get('postMessage'), { message: input.message });
if (query === input.message) { if (query === input.message) {
// Return early if the message is the same as the previous message // Return early if the message is the same as the previous message
globalState.setState((state) => ({
...state,
opened: true,
}));
return; return;
} }
@@ -482,34 +440,14 @@ export function AIChatProvider(props: {
})); }));
}, [setSearchState]); }, [setSearchState]);
const onEvent = React.useCallback(
<T extends AIChatEvent['type']>(
event: T,
listener: (input?: AIChatEventData<T>) => void
) => {
const listeners = eventsRef.current.get(event) || [];
listeners.push(listener as AIChatEventListener);
eventsRef.current.set(event, listeners);
return () => {
const currentListeners = eventsRef.current.get(event) || [];
eventsRef.current.set(
event,
currentListeners.filter((l) => l !== listener)
);
};
},
[]
);
const controller = React.useMemo(() => { const controller = React.useMemo(() => {
return { return {
open: onOpen, open: onOpen,
close: onClose, close: onClose,
clear: onClear, clear: onClear,
postMessage: onPostMessage, postMessage: onPostMessage,
on: onEvent,
}; };
}, [onOpen, onClose, onClear, onPostMessage, onEvent]); }, [onOpen, onClose, onClear, onPostMessage]);
return ( return (
<AIChatControllerContext.Provider value={controller}> <AIChatControllerContext.Provider value={controller}>
@@ -19,7 +19,6 @@ import {
EmbeddableFrameButtons, EmbeddableFrameButtons,
EmbeddableFrameHeader, EmbeddableFrameHeader,
EmbeddableFrameHeaderMain, EmbeddableFrameHeaderMain,
EmbeddableFrameMain,
EmbeddableFrameSubtitle, EmbeddableFrameSubtitle,
EmbeddableFrameTitle, EmbeddableFrameTitle,
} from '../Embeddable/EmbeddableFrame'; } from '../Embeddable/EmbeddableFrame';
@@ -27,7 +26,6 @@ import { useTrackEvent } from '../Insights';
import { useNow } from '../hooks'; import { useNow } from '../hooks';
import { Button } from '../primitives'; import { Button } from '../primitives';
import { ScrollContainer } from '../primitives/ScrollContainer'; import { ScrollContainer } from '../primitives/ScrollContainer';
import { SideSheet } from '../primitives/SideSheet';
import { AIChatControlButton } from './AIChatControlButton'; import { AIChatControlButton } from './AIChatControlButton';
import { AIChatIcon } from './AIChatIcon'; import { AIChatIcon } from './AIChatIcon';
import { AIChatInput } from './AIChatInput'; import { AIChatInput } from './AIChatInput';
@@ -70,54 +68,41 @@ export function AIChat() {
}, [chat.opened, trackEvent]); }, [chat.opened, trackEvent]);
return ( return (
<SideSheet <div
side="right"
open={chat.opened}
onOpenChange={(open) => {
if (open) {
chatController.open();
} else {
chatController.close();
}
}}
data-testid="ai-chat" data-testid="ai-chat"
withScrim={true}
className={tcls( 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>
<EmbeddableFrameHeader> <AIChatDynamicIcon trademark={config.trademark} />
<AIChatDynamicIcon trademark={config.trademark} /> <EmbeddableFrameHeaderMain>
<EmbeddableFrameHeaderMain> <EmbeddableFrameTitle>
<EmbeddableFrameTitle> {getAIChatName(language, config.trademark)}
{getAIChatName(language, config.trademark)} </EmbeddableFrameTitle>
</EmbeddableFrameTitle> <AIChatSubtitle chat={chat} />
<AIChatSubtitle chat={chat} /> </EmbeddableFrameHeaderMain>
</EmbeddableFrameHeaderMain> <EmbeddableFrameButtons>
<EmbeddableFrameButtons> <AIChatControlButton />
<AIChatControlButton /> <Button
<Button onClick={() => chatController.close()}
onClick={() => chatController.close()} iconOnly
iconOnly icon="close"
icon="close" label={tString(language, 'close')}
label={tString(language, 'close')} variant="blank"
variant="blank" size="default"
size="default"
/>
</EmbeddableFrameButtons>
</EmbeddableFrameHeader>
<EmbeddableFrameBody>
<AIChatBody
chatController={chatController}
chat={chat}
suggestions={config.suggestions}
/> />
</EmbeddableFrameBody> </EmbeddableFrameButtons>
</EmbeddableFrameMain> </EmbeddableFrameHeader>
<EmbeddableFrameBody>
<AIChatBody chatController={chatController} chat={chat} />
</EmbeddableFrameBody>
</EmbeddableFrame> </EmbeddableFrame>
</SideSheet> </div>
); );
} }
@@ -126,14 +111,13 @@ export function AIChat() {
*/ */
export function AIChatDynamicIcon(props: { export function AIChatDynamicIcon(props: {
trademark: boolean; trademark: boolean;
className?: string;
}) { }) {
const { trademark, className } = props; const { trademark } = props;
const chat = useAIChatState(); const chat = useAIChatState();
return ( return (
<AIChatIcon <AIChatIcon
className={tcls('size-5 text-tint', className)} className="size-5 text-tint"
trademark={trademark} trademark={trademark}
state={ state={
chat.error chat.error
@@ -201,14 +185,11 @@ export function AIChatBody(props: {
chat: AIChatState; chat: AIChatState;
welcomeMessage?: string; welcomeMessage?: string;
suggestions?: string[]; suggestions?: string[];
greeting?: {
title: string;
subtitle: string;
};
}) { }) {
const { chatController, chat, suggestions, greeting } = props; const { chatController, chat, suggestions } = props;
const { trademark } = useAI().config; const { trademark } = useAI().config;
const [input, setInput] = React.useState('');
const language = useLanguage(); const language = useLanguage();
const now = useNow(60 * 60 * 1000); // Refresh every hour for greeting const now = useNow(60 * 60 * 1000); // Refresh every hour for greeting
@@ -228,8 +209,8 @@ export function AIChatBody(props: {
className="shrink grow basis-80 animate-fade-in-slow [container-type:size]" className="shrink grow basis-80 animate-fade-in-slow [container-type:size]"
contentClassName="p-4 gutter-stable flex flex-col gap-4" contentClassName="p-4 gutter-stable flex flex-col gap-4"
orientation="vertical" orientation="vertical"
trailing={{ fade: false, button: true }} fadeEdges={['leading']}
active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`} active={`message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
> >
{isEmpty ? ( {isEmpty ? (
<div className="flex grow flex-col"> <div className="flex grow flex-col">
@@ -244,20 +225,19 @@ export function AIChatBody(props: {
className="size-8 text-primary [@container(min-height:400px)]:size-16" className="size-8 text-primary [@container(min-height:400px)]:size-16"
/> />
</div> </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 <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' }} style={{ animationDelay: '.5s' }}
data-testid="ai-chat-time-greeting" data-testid="ai-chat-time-greeting"
> >
{greeting?.title || timeGreeting} {timeGreeting}
</h5> </h5>
<p <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' }} style={{ animationDelay: '.6s' }}
> >
{greeting?.subtitle || {t(language, 'ai_chat_assistant_description')}
t(language, 'ai_chat_assistant_description')}
</p> </p>
</div> </div>
</div> </div>
@@ -278,10 +258,13 @@ export function AIChatBody(props: {
{chat.error ? <AIChatError chatController={chatController} /> : null} {chat.error ? <AIChatError chatController={chatController} /> : null}
<AIChatInput <AIChatInput
value={input}
onChange={setInput}
loading={chat.loading} loading={chat.loading}
disabled={chat.loading || chat.error} disabled={chat.loading || chat.error}
onSubmit={(value) => { onSubmit={() => {
chatController.postMessage({ message: value }); chatController.postMessage({ message: input });
setInput('');
}} }}
/> />
</div> </div>
@@ -1,9 +1,10 @@
'use client'; 'use client';
import { useLanguage } from '@/intl/client'; import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate'; import { t, tString } from '@/intl/translate';
import { Icon } from '@gitbook/icons';
import { useAIChatController, useAIChatState } from '../AI'; import { useAIChatController, useAIChatState } from '../AI';
import { Button } from '../primitives'; import { Button, DropdownMenu, DropdownMenuItem } from '../primitives';
/** /**
* Button to control the chat (clear, etc.) * Button to control the chat (clear, etc.)
@@ -14,16 +15,26 @@ export function AIChatControlButton() {
const chatController = useAIChatController(); const chatController = useAIChatController();
return chat.messages.length > 0 ? ( return chat.messages.length > 0 ? (
<Button <DropdownMenu
onClick={() => { button={
chatController.clear(); <Button
}} onClick={() => {}}
iconOnly iconOnly
icon="trash-can" icon="ellipsis"
label={t(language, 'ai_chat_clear_conversation')} label={tString(language, 'actions')}
variant="blank" variant="blank"
size="default" size="default"
className="animate-blur-in-slow" />
/> }
>
<DropdownMenuItem
onClick={() => {
chatController.clear();
}}
>
<Icon icon="broom-wide" className="size-3 shrink-0 text-tint-subtle" />
{t(language, 'ai_chat_clear_conversation')}
</DropdownMenuItem>
</DropdownMenu>
) : null; ) : null;
} }
@@ -1,3 +1,4 @@
import { tcls } from '@/lib/tailwind';
import { Icon, IconStyle } from '@gitbook/icons'; import { Icon, IconStyle } from '@gitbook/icons';
import type React from 'react'; import type React from 'react';
@@ -20,18 +21,12 @@ export function AIChatIcon({
<Icon <Icon
icon="sparkle" icon="sparkle"
{...props} {...props}
className={className} className={tcls(
style={{ className,
animation: { (state === 'thinking' || state === 'working') &&
intro: 'spin 2s forwards cubic-bezier(0.16,1,0.3,1)', 'animate-[spin_2s_infinite_forwards_cubic-bezier(0.16,1,0.3,1)]',
thinking: '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)]'
working: 'spin 2s infinite forwards cubic-bezier(0.16,1,0.3,1)', )}
done: '',
confirm: '',
default: '',
error: '',
}[state],
}}
/> />
); );
} }
@@ -55,36 +50,28 @@ export function AIChatIcon({
<path <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" 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" stroke="currentColor"
strokeWidth={state === 'intro' ? '1.2' : '1'} strokeWidth="1.2"
strokeLinejoin="round" strokeLinejoin="round"
shapeRendering="crispEdges" className={tcls(
className="transition-opacity duration-300" state === 'intro' &&
style={{ 'animate-[fadeIn_.5s_.7s_both,spin_2s_1s_forwards_cubic-bezier(.43,1.54,.64,1)]',
animation: { (state === 'working' || state === 'thinking') &&
intro: 'fadeIn .5s .5s backwards, spin 2s .5s forwards cubic-bezier(0.43,1.54,0.64,1)', 'animate-[fadeIn_.5s_.3s_both,spin_2s_1s_infinite_forwards_cubic-bezier(0.16,1,0.3,1)]',
thinking: 'spin 2s 1s infinite forwards cubic-bezier(0.16,1,0.3,1)', state === 'done' && 'animate-[fadeOut_.5s_both]',
working: 'spin 2s 1s infinite forwards cubic-bezier(0.16,1,0.3,1)', state === 'confirm' && 'animate-[fadeOut_.5s_both]',
done: '', state === 'default' && 'animate-[fadeIn_0s_both]',
confirm: '', state === 'error' && 'hidden'
default: '', )}
error: '', style={{ transformOrigin: '13px 3.5px' }}
}[state],
transitionDelay:
state === 'default' || state === 'thinking' ? '.3s' : undefined,
opacity: ['done', 'confirm', 'error'].includes(state) ? 0 : 1,
transformOrigin: '13px 3.5px',
}}
/> />
{/* Error */} {/* Error */}
<g <g
clipPath="url(#clip0_153_2034)" clipPath="url(#clip0_153_2034)"
className="transition-opacity duration-300" className={tcls(
style={{ 'text-danger-subtle',
color: 'rgb(var(--danger-9))', state === 'error' ? 'animate-[fadeIn_.5s_.3s_both]' : 'hidden'
opacity: state === 'error' ? 1 : 0, )}
transitionDelay: state === 'error' ? '.3s' : undefined,
}}
> >
<path <path
d="M13.0312 1.42059L13.0312 3.95184" d="M13.0312 1.42059L13.0312 3.95184"
@@ -107,34 +94,29 @@ export function AIChatIcon({
strokeWidth="1.2" strokeWidth="1.2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="transition-opacity duration-300" className={tcls(
style={{ state === 'done'
opacity: state === 'done' ? 1 : 0, ? 'animate-[fadeIn_.5s_.3s_both]'
transitionDelay: state === 'done' ? '.3s' : undefined, : 'animate-[fadeOut_.5s_both]',
}} state === 'intro' && 'hidden',
state === 'confirm' && 'hidden'
)}
/> />
{/* Confirm */} {/* Confirm */}
<path <path
className="transition-opacity duration-300" className={tcls(
style={{ 'fill-primary-9',
fill: 'rgb(var(--primary-9))', state === 'confirm'
opacity: state === 'confirm' ? 1 : 0, ? 'animate-[fadeIn_.5s_.3s_both,bounceSmall_1s_infinite_both]'
transitionDelay: state === 'confirm' ? '.3s' : undefined, : state === 'thinking'
animation: { ? 'animate-[fadeOut_.5s_both]'
intro: '', : 'hidden'
thinking: '', )}
working: '',
done: '',
confirm: 'bounceSmall 1s infinite both',
default: '',
error: '',
}[state],
}}
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" 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 <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" 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" stroke="currentColor"
@@ -142,17 +124,7 @@ export function AIChatIcon({
strokeWidth="1.2" strokeWidth="1.2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
style={{ className={tcls(state === 'intro' && 'animate-[fadeIn_2s_forwards]')}
animation: {
intro: 'fadeIn 2s forwards',
thinking: '',
working: '',
done: '',
confirm: '',
default: '',
error: '',
}[state],
}}
/> />
{/* Logo */} {/* Logo */}
@@ -182,65 +154,45 @@ export function AIChatIcon({
strokeWidth="1.2" strokeWidth="1.2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
style={{ className={tcls(
animation: { (state === 'thinking' || state === 'working') &&
intro: 'pathEnter 1.5s both ease-out', 'animate-[pathLoading_2s_infinite_both]',
thinking: 'pathLoading 2s infinite both', state === 'intro' && 'animate-[pathEnter_2s_both]',
working: 'pathLoading 2s infinite both', state === 'done' && 'animate-[pathEnter_1s_forwards_ease]'
done: 'pathEnter 1s forwards ease', )}
confirm: '',
default: '',
error: '',
}[state],
}}
/> />
</g> </g>
</svg> </svg>
); );
} }
interface AISearchIconProps extends React.SVGProps<SVGSVGElement> { export function AISearchIcon({
className?: string; className = 'size-4',
state?: 'default' | 'intro' | 'thinking' | 'working' | 'done' | 'error' | 'confirm'; state = 'default',
} }: Pick<AIChatIconProps, 'className' | 'state'>) {
export function AISearchIcon({ className = 'size-4', state = 'default' }: AISearchIconProps) {
return ( return (
<div <div
style={{ className={tcls(
position: 'relative', 'relative',
animation: { state === 'intro' && 'animate-[fadeIn_1s_both,orbit_1s_cubic-bezier(0.16,1,0.3,1)]',
intro: 'fadeIn 1s both, orbit 1s cubic-bezier(0.16,1,0.3,1)', state === 'thinking' || state === 'working'
thinking: ? 'animate-[fadeIn_1s_both,orbit_1s_ease-out,orbit_2s_1s_infinite_forwards_linear]'
'fadeIn 1s both, orbit 1s orbit 2s 1s infinite forwards linear ease-out,', : ''
working: '', )}
done: '',
confirm: '',
default: '',
error: '',
}[state],
}}
> >
<Icon icon="search" className={className} /> <Icon icon="search" className={className} />
<Icon <Icon
icon="sparkle" icon="sparkle"
iconStyle={IconStyle.Solid} iconStyle={IconStyle.Solid}
style={{ className={tcls(
position: 'absolute', 'absolute top-[15.7%] left-[15.6%] size-[50%]',
top: '15.7%', state === 'thinking' || state === 'working'
left: '15.6%', ? 'animate-[spin_2s_infinite_forwards_cubic-bezier(0.16,1,0.3,1)]'
width: '50%', : '',
height: '50%', state === 'intro'
animation: { ? 'animate-[spin_2s_.5s_forwards_cubic-bezier(0.16,1,0.3,1)]'
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],
}}
/> />
</div> </div>
); );
@@ -1,26 +1,38 @@
import { t, tString, useLanguage } from '@/intl/client'; import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons'; import { Icon } from '@gitbook/icons';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useAIChatState } from '../AI/useAIChat'; import { useAIChatState } from '../AI/useAIChat';
import { HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives'; import { Button, HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
import { Input } from '../primitives/Input'; import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
export function AIChatInput(props: { export function AIChatInput(props: {
value: string;
disabled?: boolean; disabled?: boolean;
/** /**
* When true, the input is disabled * When true, the input is disabled
*/ */
loading: boolean; loading: boolean;
onChange: (value: string) => void;
onSubmit: (value: string) => void; onSubmit: (value: string) => void;
}) { }) {
const { onSubmit, disabled, loading } = props; const { value, onChange, onSubmit, disabled, loading } = props;
const language = useLanguage(); const language = useLanguage();
const chat = useAIChatState(); const chat = useAIChatState();
const inputRef = useRef<HTMLTextAreaElement>(null); 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(() => { useEffect(() => {
if (chat.opened && !disabled && !loading) { if (chat.opened && !disabled && !loading) {
// Add a small delay to ensure the input is rendered before focusing // Add a small delay to ensure the input is rendered before focusing
@@ -45,33 +57,57 @@ export function AIChatInput(props: {
); );
return ( return (
<Input <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">
data-testid="ai-chat-input" <textarea
name="ai-chat-input" ref={inputRef}
multiline disabled={disabled || loading}
resize data-loading={loading}
sizing="large" data-testid="ai-chat-input"
label="Assistant chat input" className={tcls(
placeholder={tString(language, 'ai_chat_input_placeholder')} 'resize-none',
onSubmit={(val) => onSubmit(val as string)} 'focus:outline-hidden',
submitButton={{ 'focus:ring-0',
label: tString(language, 'send'), 'w-full',
}} 'px-3',
className="animate-blur-in-slow bg-tint-base/9 backdrop-blur-lg contrast-more:bg-tint-base" 'py-3',
rows={1} 'pb-12',
maxLength={2048} 'h-auto',
keyboardShortcut={ 'bg-transparent',
!disabled && !loading 'peer',
? { 'max-h-64',
keys: ['mod', 'i'], 'placeholder:text-tint/8',
className: 'bg-tint-base group-focus-within/input:hidden', 'transition-colors',
} 'disabled:bg-tint-subtle',
: undefined 'delay-300',
} 'disabled:delay-0',
disabled={disabled || loading} 'disabled:cursor-not-allowed',
aria-busy={loading} 'data-[loading=true]:cursor-progress',
ref={inputRef} 'data-[loading=true]:opacity-50'
trailing={ )}
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}> <HoverCardRoot openDelay={500}>
<HoverCard <HoverCard
className="max-w-xs bg-tint p-2 text-sm text-tint" className="max-w-xs bg-tint p-2 text-sm text-tint"
@@ -99,8 +135,7 @@ export function AIChatInput(props: {
</div> </div>
</HoverCard> </HoverCard>
<HoverCardTrigger> <HoverCardTrigger>
{/* Negative margin to compensate for Input's padding, so the badge appears flush with the cursor */} <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">
<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">
<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"> <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')} {t(language, 'ai_chat_context_badge')}
</span>{' '} </span>{' '}
@@ -111,7 +146,14 @@ export function AIChatInput(props: {
</div> </div>
</HoverCardTrigger> </HoverCardTrigger>
</HoverCardRoot> </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[]; suggestions?: string[];
}) { }) {
const language = useLanguage(); const language = useLanguage();
const { chatController, suggestions: _suggestions } = props; const {
chatController,
const suggestions = suggestions = [
_suggestions && _suggestions.length > 0 tString(language, 'ai_chat_suggested_questions_about_this_page'),
? _suggestions tString(language, 'ai_chat_suggested_questions_read_next'),
: [ tString(language, 'ai_chat_suggested_questions_example'),
tString(language, 'ai_chat_suggested_questions_about_this_page'), ],
tString(language, 'ai_chat_suggested_questions_read_next'), } = props;
tString(language, 'ai_chat_suggested_questions_example'),
];
return ( return (
<div className="flex flex-col items-start gap-2 self-start"> <div className="flex flex-col items-start gap-2 self-start">
@@ -35,68 +35,66 @@ export function AnnouncementBanner(props: {
data-nosnippet="" data-nosnippet=""
> >
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96"> <div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className={tcls(CONTAINER_STYLE)}> <div className={tcls('relative', CONTAINER_STYLE)}>
<div className="relative"> <Tag
<Tag href={contentRef?.href ?? ''}
href={contentRef?.href ?? ''} className={tcls(
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',
'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,
style.container, closeable && 'pr-12',
closeable && 'pr-12', hasLink && style.hover
hasLink && style.hover )}
)} insights={
insights={ announcement.link
announcement.link ? {
? { type: 'link_click',
type: 'link_click', link: {
link: { target: announcement.link.to,
target: announcement.link.to, position: SiteInsightsLinkPosition.Announcement,
position: SiteInsightsLinkPosition.Announcement, },
}, }
} : undefined
: undefined }
} >
> <Icon
<Icon icon={style.icon as IconName}
icon={style.icon as IconName} className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`} />
/> <div>
<div> {announcement.message}
{announcement.message} {hasLink ? (
{hasLink ? ( <div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}> {contentRef?.icon ? (
{contentRef?.icon ? ( <span className="mr-1 ml-2 *:inline">
<span className="mr-1 ml-2 *:inline"> {contentRef?.icon}
{contentRef?.icon} </span>
</span> ) : null}
) : null} {announcement.link?.title && (
{announcement.link?.title && ( <span className="mr-1">{announcement.link?.title}</span>
<span className="mr-1">{announcement.link?.title}</span> )}
)} <Icon
<Icon icon={
icon={ announcement.link?.to.kind === 'url'
announcement.link?.to.kind === 'url' ? 'arrow-up-right'
? 'arrow-up-right' : 'chevron-right'
: 'chevron-right' }
} className={tcls('mb-0.5 inline size-3')}
className={tcls('mb-0.5 inline size-3')} />
/> </div>
</div> ) : null}
) : null} </div>
</div> </Tag>
</Tag> {closeable ? (
{closeable ? ( <Button
<Button iconOnly
iconOnly icon="close"
icon="close" label={tString(language, 'close')}
label={tString(language, 'close')} variant="blank"
variant="blank" size="default"
size="default" onClick={dismissAnnouncement}
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}`}
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}
) : null}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -41,7 +41,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
aria-describedby={describedById} aria-describedby={describedById}
className={tcls( className={tcls(
'fixed', 'fixed',
'z-50', 'z-10',
'bg-tint-base', 'bg-tint-base',
'rounded-sm', 'rounded-sm',
'straight-corners:rounded-none', 'straight-corners:rounded-none',
@@ -52,9 +52,9 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'depth-flat:shadow-none', 'depth-flat:shadow-none',
'p-4', 'p-4',
'pr-8', 'pr-8',
'bottom-[max(env(safe-area-inset-bottom),1rem)]', 'bottom-4',
'right-[max(env(safe-area-inset-right),1rem)]', 'right-4',
'left-[max(env(safe-area-inset-left),4rem)]', 'left-16',
'max-w-md', 'max-w-md',
'text-balance', 'text-balance',
'sm:left-auto', 'sm:left-auto',
@@ -32,9 +32,10 @@ export function Hint({
className={tcls( className={tcls(
'hint', 'hint',
'transition-colors', 'transition-colors',
'rounded-corners:rounded-md', 'rounded-md',
hasHeading ? 'rounded-l-sm' : null,
'straight-corners:rounded-none',
'circular-corners:rounded-xl', 'circular-corners:rounded-xl',
hasHeading ? 'circular-corners:rounded-l-none rounded-corners:rounded-l-none' : '',
'overflow-hidden', 'overflow-hidden',
hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container, hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container,
@@ -156,10 +156,8 @@
/** Text input */ /** Text input */
.contentkit-textinput { .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 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 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:outline-primary focus:border-primary;
@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;
} }
/** Form */ /** Form */
@@ -36,13 +36,13 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
<div className="relative select-none"> <div className="relative select-none">
<div <div
className={tcls( className={tcls(
'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-solid theme-muted:bg-tint-base tabular-nums contrast-more:bg-primary-11', 'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-original theme-muted:bg-primary-subtle tabular-nums contrast-more:bg-primary-solid',
'font-medium text-contrast-primary-solid theme-muted:text-tint contrast-more:text-contrast-primary-11' 'font-medium text-contrast-primary-original theme-muted:text-primary contrast-more:text-contrast-primary-solid'
)} )}
> >
{index + 1} {index + 1}
</div> </div>
<div className="can-override-bg absolute top-9 bottom-2 left-3.5 w-px bg-primary-7 theme-muted:bg-tint-6" /> <div className="can-override-bg absolute top-9 bottom-2 left-3.5 w-px bg-primary-7 theme-muted:bg-primary-subtle" />
</div> </div>
<Blocks <Blocks
{...contextProps} {...contextProps}
@@ -18,36 +18,20 @@ import {
EmbeddableFrameButtons, EmbeddableFrameButtons,
EmbeddableFrameHeader, EmbeddableFrameHeader,
EmbeddableFrameHeaderMain, EmbeddableFrameHeaderMain,
EmbeddableFrameMain,
EmbeddableFrameSidebar,
EmbeddableFrameTitle, EmbeddableFrameTitle,
} from './EmbeddableFrame'; } from './EmbeddableFrame';
import { import { EmbeddableIframeButtons, useEmbeddableConfiguration } from './EmbeddableIframeAPI';
EmbeddableIframeButtons,
EmbeddableIframeTabs,
useEmbeddableConfiguration,
} from './EmbeddableIframeAPI';
type EmbeddableAIChatProps = {
baseURL: string;
siteTitle: string;
};
/** /**
* Embeddable AI chat window in an iframe. * Embeddable AI chat window in an iframe.
*/ */
export function EmbeddableAIChat(props: EmbeddableAIChatProps) { export function EmbeddableAIChat() {
const { baseURL, siteTitle } = props;
const chat = useAIChatState(); const chat = useAIChatState();
const { config } = useAI(); const { config } = useAI();
const chatController = useAIChatController(); const chatController = useAIChatController();
const configuration = useEmbeddableConfiguration(); const configuration = useEmbeddableConfiguration();
const language = useLanguage(); const language = useLanguage();
React.useEffect(() => {
chatController.open();
}, [chatController]);
// Track the view of the AI chat // Track the view of the AI chat
const trackEvent = useTrackEvent(); const trackEvent = useTrackEvent();
React.useEffect(() => { React.useEffect(() => {
@@ -62,46 +46,28 @@ export function EmbeddableAIChat(props: EmbeddableAIChatProps) {
); );
}, [trackEvent]); }, [trackEvent]);
const tabsRef = React.useRef<HTMLDivElement>(null);
return ( return (
<EmbeddableFrame> <EmbeddableFrame>
<EmbeddableFrameSidebar> <EmbeddableFrameHeader>
<EmbeddableIframeTabs <AIChatDynamicIcon trademark={config.trademark} />
ref={tabsRef} <EmbeddableFrameHeaderMain>
active="assistant" <EmbeddableFrameTitle>
baseURL={baseURL} {getAIChatName(language, config.trademark)}
siteTitle={siteTitle} </EmbeddableFrameTitle>
<AIChatSubtitle chat={chat} />
</EmbeddableFrameHeaderMain>
<EmbeddableFrameButtons>
<AIChatControlButton />
<EmbeddableIframeButtons />
</EmbeddableFrameButtons>
</EmbeddableFrameHeader>
<EmbeddableFrameBody>
<AIChatBody
chatController={chatController}
chat={chat}
suggestions={configuration.suggestions}
/> />
<EmbeddableIframeButtons /> </EmbeddableFrameBody>
</EmbeddableFrameSidebar>
<EmbeddableFrameMain>
<EmbeddableFrameHeader>
{!tabsRef.current ? (
<AIChatDynamicIcon
className="animate-blur-in-slow"
trademark={config.trademark}
/>
) : null}
<EmbeddableFrameHeaderMain>
<EmbeddableFrameTitle>
{getAIChatName(language, config.trademark)}
</EmbeddableFrameTitle>
<AIChatSubtitle chat={chat} />
</EmbeddableFrameHeaderMain>
<EmbeddableFrameButtons>
<AIChatControlButton />
</EmbeddableFrameButtons>
</EmbeddableFrameHeader>
<EmbeddableFrameBody>
<AIChatBody
chatController={chatController}
chat={chat}
suggestions={configuration.suggestions}
greeting={configuration.greeting}
/>
</EmbeddableFrameBody>
</EmbeddableFrameMain>
</EmbeddableFrame> </EmbeddableFrame>
); );
} }
@@ -1,13 +1,8 @@
import { EmbeddableAIChat } from './EmbeddableAIChat'; import { EmbeddableAIChat } from './EmbeddableAIChat';
type EmbeddableAssistantPageProps = {
baseURL: string;
siteTitle: string;
};
/** /**
* Reusable page component for the embed assistant page. * Reusable page component for the embed assistant page.
*/ */
export async function EmbeddableAssistantPage(props: EmbeddableAssistantPageProps) { export async function EmbeddableAssistantPage() {
return <EmbeddableAIChat baseURL={props.baseURL} siteTitle={props.siteTitle} />; return <EmbeddableAIChat />;
} }
@@ -1,24 +1,18 @@
import { type PagePathParams, getSitePageData } from '@/components/SitePage'; import { type PagePathParams, getSitePageData } from '@/components/SitePage';
import { PageBody } from '@/components/PageBody';
import type { GitBookSiteContext } from '@/lib/context'; import type { GitBookSiteContext } from '@/lib/context';
import { SiteInsightsDisplayContext } from '@gitbook/api'; import { SiteInsightsDisplayContext } from '@gitbook/api';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { HeaderMobileMenu } from '../Header/HeaderMobileMenu'; import { Button } from '../primitives';
import { PageBody } from '../PageBody';
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
import { TableOfContents } from '../TableOfContents';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { EmbeddableDocsPageControlButtons } from './EmbeddableDocsPageControlButtons';
import { import {
EmbeddableFrame, EmbeddableFrame,
EmbeddableFrameBody, EmbeddableFrameBody,
EmbeddableFrameButtons, EmbeddableFrameButtons,
EmbeddableFrameHeader, EmbeddableFrameHeader,
EmbeddableFrameHeaderMain, EmbeddableFrameHeaderMain,
EmbeddableFrameMain,
EmbeddableFrameSidebar,
EmbeddableFrameTitle,
} from './EmbeddableFrame'; } from './EmbeddableFrame';
import { EmbeddableIframeButtons, EmbeddableIframeTabs } from './EmbeddableIframeAPI'; import { EmbeddableIframeButtons } from './EmbeddableIframeAPI';
export const dynamic = 'force-static'; export const dynamic = 'force-static';
@@ -30,9 +24,7 @@ type EmbeddableDocsPageProps = {
/** /**
* Page component for the embed docs page. * Page component for the embed docs page.
*/ */
export async function EmbeddableDocsPage( export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
props: EmbeddableDocsPageProps & { staticRoute: boolean }
) {
const { context, pageParams } = props; const { context, pageParams } = props;
const { page, document, ancestors, withPageFeedback } = await getSitePageData({ const { page, document, ancestors, withPageFeedback } = await getSitePageData({
context, context,
@@ -41,59 +33,32 @@ export async function EmbeddableDocsPage(
return ( return (
<EmbeddableFrame className="site-background"> <EmbeddableFrame className="site-background">
<EmbeddableFrameSidebar> <EmbeddableFrameHeader>
<EmbeddableIframeTabs <EmbeddableFrameHeaderMain>
active="docs" <Button
baseURL={context.linker.toPathInSite('~gitbook/embed/')} href={context.linker.toPathInSite('~gitbook/embed/assistant')}
siteTitle={context.site.title} size="default"
/> variant="blank"
<EmbeddableIframeButtons /> icon="arrow-left"
</EmbeddableFrameSidebar> label="Back"
<EmbeddableFrameMain> />
<div className="relative flex not-hydrated:animate-blur-in-slow flex-col"> </EmbeddableFrameHeaderMain>
<EmbeddableFrameHeader> <EmbeddableFrameButtons>
<HeaderMobileMenu className="-ml-2 page-no-toc:hidden" /> <EmbeddableIframeButtons />
<EmbeddableFrameHeaderMain> </EmbeddableFrameButtons>
<EmbeddableFrameTitle>{context.site.title}</EmbeddableFrameTitle> </EmbeddableFrameHeader>
</EmbeddableFrameHeaderMain> <EmbeddableFrameBody>
<EmbeddableFrameButtons> <div className="flex-1 overflow-auto p-6">
<EmbeddableDocsPageControlButtons <PageBody
href={context.linker context={context}
.toPathForPage({ page={page}
pages: context.revision.pages, ancestors={ancestors}
page, document={document}
}) withPageFeedback={withPageFeedback}
.replace(/~gitbook\/embed\/page\/?/, '')} insightsDisplayContext={SiteInsightsDisplayContext.Embed}
/> />
</EmbeddableFrameButtons>
</EmbeddableFrameHeader>
{context.sections ? (
<SiteSectionTabs
className="-mt-2 border-tint-subtle border-b"
sections={encodeClientSiteSections(context, context.sections)}
/>
) : null}
</div> </div>
<EmbeddableFrameBody> </EmbeddableFrameBody>
<ScrollContainer
orientation="vertical"
className="not-hydrated:animate-blur-in-slow"
contentClassName="p-4"
leading={{ fade: !context.sections, button: true }}
>
<TableOfContents context={context} />
<PageBody
context={context}
page={page}
ancestors={ancestors}
document={document}
withPageFeedback={withPageFeedback}
insightsDisplayContext={SiteInsightsDisplayContext.Embed}
staticRoute={props.staticRoute}
/>
</ScrollContainer>
</EmbeddableFrameBody>
</EmbeddableFrameMain>
</EmbeddableFrame> </EmbeddableFrame>
); );
} }
@@ -1,21 +0,0 @@
'use client';
import { tString, useLanguage } from '@/intl/client';
import { Button } from '../primitives';
export function EmbeddableDocsPageControlButtons(props: { href: string }) {
const { href } = props;
const language = useLanguage();
return (
<Button
icon="arrow-up-right-from-square"
label={tString(language, 'open_in_new_tab')}
href={href}
target="_blank"
iconOnly
variant="blank"
size="default"
/>
);
}
@@ -17,7 +17,7 @@ export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameP
<div <div
{...divProps} {...divProps}
className={tcls( className={tcls(
'flex h-full grow overflow-hidden bg-radial-[circle_at_bottom] from-primary to-50% to-transparent text-sm text-tint', 'flex h-full grow flex-col overflow-hidden bg-radial-[circle_at_bottom] from-primary to-50% to-transparent text-sm text-tint',
divProps.className divProps.className
)} )}
ref={ref} ref={ref}
@@ -28,19 +28,13 @@ export const EmbeddableFrame = React.forwardRef<HTMLDivElement, EmbeddableFrameP
} }
); );
export function EmbeddableFrameMain(props: { children: React.ReactNode }) {
const { children } = props;
return <div className="flex flex-1 flex-col overflow-hidden">{children}</div>;
}
export function EmbeddableFrameHeader(props: { export function EmbeddableFrameHeader(props: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { children } = props; const { children } = props;
return ( return (
<div className="relative z-10 flex not-hydrated:animate-blur-in-slow select-none items-center gap-2 px-4 py-2.5 text-tint-strong"> <div className="relative z-10 flex animate-fade-in-slow select-none items-center gap-2 px-4 py-2 text-tint-strong">
{children} {children}
</div> </div>
); );
@@ -51,7 +45,7 @@ export function EmbeddableFrameHeaderMain(props: {
}) { }) {
const { children } = props; const { children } = props;
return <div className="flex h-8 flex-1 flex-col justify-center">{children}</div>; return <div className="flex flex-1 flex-col">{children}</div>;
} }
export function EmbeddableFrameBody(props: { export function EmbeddableFrameBody(props: {
@@ -88,21 +82,10 @@ export function EmbeddableFrameSubtitle(props: {
); );
} }
export function EmbeddableFrameSidebar(props: { children: React.ReactNode }) {
const { children } = props;
return (
<div className="flex w-13 shrink-0 origin-top not-hydrated:animate-blur-in-slow flex-col gap-2 overflow-hidden border-tint-solid/3 border-r bg-tint-solid/1 p-2 transition-all transition-discrete duration-300 empty:hidden empty:w-0 empty:px-0">
{children}
</div>
);
}
export function EmbeddableFrameButtons(props: { export function EmbeddableFrameButtons(props: {
className?: string;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { children, className } = props; const { children } = props;
return <div className={tcls('flex gap-2', className)}>{children}</div>; return <div className="-mr-2 ml-auto flex gap-2">{children}</div>;
} }
@@ -4,17 +4,15 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb
import { createChannel } from 'bidc'; import { createChannel } from 'bidc';
import React from 'react'; import React from 'react';
import { useAI, useAIChatController } from '@/components/AI'; import { useAIChatController } from '@/components/AI';
import { CustomizationAIMode } from '@gitbook/api';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { createStore, useStore } from 'zustand'; import { createStore, useStore } from 'zustand';
import { integrationsAssistantTools } from '../Integrations'; import { integrationsAssistantTools } from '../Integrations';
import { Button } from '../primitives'; import { Button } from '../primitives';
const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() => ({ const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() => ({
tabs: [], buttons: [],
actions: [], welcomeMessage: '',
greeting: { title: '', subtitle: '' },
suggestions: [], suggestions: [],
tools: [], tools: [],
})); }));
@@ -30,12 +28,6 @@ export function EmbeddableIframeAPI(props: {
const router = useRouter(); const router = useRouter();
const chatController = useAIChatController(); const chatController = useAIChatController();
React.useEffect(() => {
return chatController.on('open', () => {
router.push(`${baseURL}/assistant`);
});
}, [router, baseURL, chatController]);
React.useEffect(() => { React.useEffect(() => {
if (window.parent === window) { if (window.parent === window) {
return; return;
@@ -101,114 +93,23 @@ export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
* Display the buttons defined by the parent window. * Display the buttons defined by the parent window.
*/ */
export function EmbeddableIframeButtons() { export function EmbeddableIframeButtons() {
const { actions: configuredActions, buttons: configuredButtons = [] } = const buttons = useEmbeddableConfiguration((state) => state.buttons);
useEmbeddableConfiguration((state) => state);
const actions = configuredActions.length > 0 ? configuredActions : configuredButtons;
return ( return (
<> <>
{actions.length > 0 && ( {buttons.map((button) => (
<hr className="my-2 border-0 border-tint-subtle border-b first:hidden" />
)}
{actions.map((action, index) => (
<Button <Button
key={action.label} key={button.label}
size="default" size="default"
variant="blank" variant="blank"
icon={action?.icon ?? 'square-question'} icon={button.icon}
label={action?.label} label={button.label}
iconOnly iconOnly
className="not-hydrated:animate-blur-in-slow [&_.button-leading-icon]:size-5"
disabled={!action.onClick}
onClick={() => { onClick={() => {
action.onClick?.(); button.onClick();
}} }}
tooltipProps={{
contentProps: {
side: 'right',
},
}}
style={{ animationDelay: `${index * 100}ms` }}
/> />
))} ))}
</> </>
); );
} }
export function EmbeddableIframeTabs(props: {
ref?: React.RefObject<HTMLDivElement | null>;
active?: string;
baseURL: string;
siteTitle: string;
}) {
const { ref, active = 'assistant', baseURL, siteTitle } = props;
const { tabs: configuredTabs, actions } = useEmbeddableConfiguration();
const { assistants, config } = useAI();
const router = useRouter();
const tabs = [
config.aiMode === CustomizationAIMode.Assistant &&
assistants[0] &&
(configuredTabs.includes('assistant') || configuredTabs.length === 0)
? {
key: 'assistant',
label: assistants[0].label,
icon: assistants[0].icon,
onClick: () => {
router.push(`${baseURL}/assistant`);
},
}
: null,
configuredTabs.includes('docs') || configuredTabs.length === 0
? {
key: 'docs',
label: siteTitle,
icon: 'book-open',
onClick: () => {
router.push(`${baseURL}/page/`);
},
}
: null,
].filter((tab) => tab !== null);
// Override the active tab if it doesn't match the configured tabs
React.useEffect(() => {
const hasAssistant = tabs.find((tab) => tab.key === 'assistant');
const hasDocs = tabs.find((tab) => tab.key === 'docs');
if (!hasAssistant && !hasDocs) {
// No valid tabs, do not redirect
return;
}
if (active === 'assistant' && !hasAssistant) {
router.replace(`${baseURL}/page`);
} else if (active === 'docs' && !hasDocs) {
router.replace(`${baseURL}/assistant`);
}
}, [tabs, baseURL, router, active]);
return tabs.length > 1 || actions.length > 0 ? (
<div className="flex flex-col gap-2" ref={ref}>
{tabs.map((tab) => (
<Button
key={tab.key}
data-testid={`embed-tab-${tab.key}`}
label={tab.label}
size="default"
variant="blank"
icon={tab.icon}
active={tab.key === active}
className="not-hydrated:animate-blur-in-slow [&_.button-leading-icon]:size-5"
iconOnly
onClick={tab.onClick}
tooltipProps={{
contentProps: {
side: 'right',
},
}}
/>
))}
</div>
) : null;
}
@@ -7,10 +7,8 @@ import {
} from '@/components/SiteLayout'; } from '@/components/SiteLayout';
import type { VisitorAuthClaims } from '@/lib/adaptive'; import type { VisitorAuthClaims } from '@/lib/adaptive';
import type { GitBookSiteContext } from '@/lib/context'; import type { GitBookSiteContext } from '@/lib/context';
import { SiteInsightsTrademarkPlacement } from '@gitbook/api'; import { CustomizationAIMode } from '@gitbook/api';
import { SpaceLayoutServerContext } from '../SpaceLayout'; import { SpaceLayoutServerContext } from '../SpaceLayout';
import { TrademarkLink } from '../TableOfContents/Trademark';
import { NavigationLoader } from '../primitives/NavigationLoader';
import { EmbeddableIframeAPI } from './EmbeddableIframeAPI'; import { EmbeddableIframeAPI } from './EmbeddableIframeAPI';
type EmbeddableRootLayoutProps = { type EmbeddableRootLayoutProps = {
@@ -31,17 +29,12 @@ export async function EmbeddableRootLayout({
return ( return (
<CustomizationRootLayout context={context}> <CustomizationRootLayout context={context}>
<SiteLayoutClientContexts <SiteLayoutClientContexts
forcedTheme={ forcedTheme={context.customization.themes.default}
context.customization.themes.toggeable
? undefined
: context.customization.themes.default
}
externalLinksTarget={context.customization.externalLinks.target} externalLinksTarget={context.customization.externalLinks.target}
contextId={context.contextId} contextId={context.contextId}
> >
<AIContextProvider <AIContextProvider
aiMode={context.customization.ai.mode} aiMode={CustomizationAIMode.Assistant}
suggestions={context.customization.ai.suggestions}
trademark={context.customization.trademark.enabled} trademark={context.customization.trademark.enabled}
> >
<SpaceLayoutServerContext <SpaceLayoutServerContext
@@ -53,19 +46,9 @@ export async function EmbeddableRootLayout({
asEmbeddable: true, asEmbeddable: true,
}} }}
> >
<NavigationLoader /> <div className="fixed inset-0 flex flex-col">{children}</div>
<div className="fixed inset-0 flex flex-col">
{children}
{context.customization.trademark.enabled ? (
<TrademarkLink
className="border-tint-solid/3 border-t bg-tint-solid/1 px-4 py-2.5 text-tint/8 ring-0"
context={context}
placement={SiteInsightsTrademarkPlacement.Embed}
/>
) : null}
</div>
<EmbeddableIframeAPI <EmbeddableIframeAPI
baseURL={context.linker.toPathInSite('~gitbook/embed/')} baseURL={context.linker.toPathInSpace('~gitbook/embed/')}
/> />
</SpaceLayoutServerContext> </SpaceLayoutServerContext>
</AIContextProvider> </AIContextProvider>
@@ -29,7 +29,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
className={tcls( className={tcls(
'border-tint-subtle border-t', 'border-tint-subtle border-t',
// If the footer only contains a mode toggle, we only show it on smaller screens // If the footer only contains a mode toggle, we only show it on smaller screens
mobileOnly ? 'xl:hidden' : null mobileOnly ? '@7xl:hidden' : null
)} )}
> >
<div className="motion-safe:transition-[padding] motion-safe:duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96"> <div className="motion-safe:transition-[padding] motion-safe:duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
@@ -42,7 +42,6 @@ export function Header(props: {
`h-[${HEADER_HEIGHT_DESKTOP}px]`, `h-[${HEADER_HEIGHT_DESKTOP}px]`,
'sticky', 'sticky',
'top-0', 'top-0',
'pt-[env(safe-area-inset-top)]',
'z-30', 'z-30',
'w-full', 'w-full',
'flex-none', 'flex-none',
@@ -1,20 +1,40 @@
'use client'; 'use client';
import { Icon } from '@gitbook/icons';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { tString, useLanguage } from '@/intl/client'; import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Button, type ButtonProps } from '../primitives'; import { useScrollListener } from '../hooks/useScrollListener';
const globalClassName = 'navigation-open'; const globalClassName = 'navigation-open';
const SCROLL_DISTANCE = 320;
/** /**
* Button to show/hide the table of content on mobile. * Button to show/hide the table of content on mobile.
*/ */
export function HeaderMobileMenu(props: ButtonProps) { export function HeaderMobileMenu(props: Partial<React.ButtonHTMLAttributes<HTMLButtonElement>>) {
const language = useLanguage(); const language = useLanguage();
const pathname = usePathname(); const pathname = usePathname();
const hasScrollRef = useRef(false);
const toggleNavigation = () => {
if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) {
document.body.classList.remove(globalClassName);
} else {
document.body.classList.add(globalClassName);
window.scrollTo(0, 0);
}
};
const windowRef = useRef(typeof window === 'undefined' ? null : window);
useScrollListener(() => {
hasScrollRef.current = window.scrollY >= SCROLL_DISTANCE;
}, windowRef);
// Close the navigation when navigating to a page // Close the navigation when navigating to a page
useEffect(() => { useEffect(() => {
@@ -22,17 +42,16 @@ export function HeaderMobileMenu(props: ButtonProps) {
}, [pathname]); }, [pathname]);
return ( return (
<Button <button
icon="bars"
iconOnly
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.
{...props} {...props}
/> aria-label={tString(language, 'table_of_contents_button_label')}
onClick={toggleNavigation}
className={tcls(
'flex flex-row items-center rounded-sm straight-corners:rounded-xs px-2 py-1',
props.className
)}
>
<Icon icon="bars" className="size-4 text-inherit" />
</button>
); );
} }
@@ -43,11 +43,6 @@ export function PageAside(props: {
'opacity-0', 'opacity-0',
'xl:flex', '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', 'overflow-hidden',
@@ -107,7 +102,7 @@ export function PageAside(props: {
className={tcls( className={tcls(
'flex flex-col', 'flex flex-col',
'min-w-56 shrink-0', 'min-w-56 shrink-0',
'page-has-toc:overflow-hidden', 'overflow-hidden',
'w-full', 'w-full',
'xl:max-2xl:rounded-corners:page-api-block:rounded-md', 'xl:max-2xl:rounded-corners:page-api-block:rounded-md',
'xl:max-2xl:circular-corners:page-api-block:rounded-xl', 'xl:max-2xl:circular-corners:page-api-block:rounded-xl',
@@ -213,7 +208,7 @@ async function PageAsideFooter(props: { context: GitBookSiteContext }) {
<div <div
className={tcls( className={tcls(
'sticky bottom-0 z-10 mt-auto flex flex-col', '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', 'border-tint-subtle xl:max-2xl:page-api-block:border-t xl:max-2xl:page-api-block:p-2',
'pt-4' '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 { GitBookSiteContext } from '@/lib/context';
import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api'; import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
import React from 'react';
import { getSpaceLanguage } from '@/intl/server'; import { getSpaceLanguage } from '@/intl/server';
import { t } from '@/intl/translate'; import { t } from '@/intl/translate';
@@ -11,7 +12,6 @@ import { TrackPageViewEvent } from '../Insights';
import { PageFeedbackForm } from '../PageFeedback'; import { PageFeedbackForm } from '../PageFeedback';
import { CurrentPageProvider } from '../hooks/useCurrentPage'; import { CurrentPageProvider } from '../hooks/useCurrentPage';
import { DateRelative, SuspenseLoadedHint } from '../primitives'; import { DateRelative, SuspenseLoadedHint } from '../primitives';
import OptionalSuspense from './OptionalSuspense';
import { PageBodyBlankslate } from './PageBodyBlankslate'; import { PageBodyBlankslate } from './PageBodyBlankslate';
import { PageCover } from './PageCover'; import { PageCover } from './PageCover';
import { PageFooterNavigation } from './PageFooterNavigation'; import { PageFooterNavigation } from './PageFooterNavigation';
@@ -27,17 +27,8 @@ export function PageBody(props: {
document: JSONDocument | null; document: JSONDocument | null;
withPageFeedback: boolean; withPageFeedback: boolean;
insightsDisplayContext: SiteInsightsDisplayContext; insightsDisplayContext: SiteInsightsDisplayContext;
staticRoute: boolean;
}) { }) {
const { const { page, context, ancestors, document, withPageFeedback, insightsDisplayContext } = props;
page,
context,
ancestors,
document,
withPageFeedback,
insightsDisplayContext,
staticRoute,
} = props;
const { customization } = context; const { customization } = context;
const contentFullWidth = document ? hasFullWidthBlock(document) : false; const contentFullWidth = document ? hasFullWidthBlock(document) : false;
@@ -74,7 +65,7 @@ export function PageBody(props: {
// Allow words to break if they are too long. // Allow words to break if they are too long.
'break-anywhere', 'break-anywhere',
'@container', '@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', siteWidthWide ? 'site-width-wide' : 'site-width-default',
page.layout.tableOfContents && hasVisibleTOCItems page.layout.tableOfContents && hasVisibleTOCItems
? 'page-has-toc' ? 'page-has-toc'
@@ -93,8 +84,7 @@ export function PageBody(props: {
withRSSFeed={contentHasUpdates} withRSSFeed={contentHasUpdates}
/> />
{document && !isNodeEmpty(document) ? ( {document && !isNodeEmpty(document) ? (
<OptionalSuspense <React.Suspense
staticRoute={staticRoute}
fallback={ fallback={
<DocumentViewSkeleton <DocumentViewSkeleton
document={document} document={document}
@@ -116,7 +106,7 @@ export function PageBody(props: {
withLinkPreviews, withLinkPreviews,
}} }}
/> />
</OptionalSuspense> </React.Suspense>
) : ( ) : (
<PageBodyBlankslate page={page} context={context} /> <PageBodyBlankslate page={page} context={context} />
)} )}
@@ -46,7 +46,7 @@ export async function PageBodyBlankslate(props: {
<Card <Card
key={child.id} key={child.id}
leadingIcon={icon} leadingIcon={icon}
title={child.linkTitle || child.title} title={child.title}
href={resolved.href} href={resolved.href}
insights={{ insights={{
type: 'link_click', type: 'link_click',
@@ -62,14 +62,7 @@ export async function PageBodyBlankslate(props: {
pages: context.revision.pages, pages: context.revision.pages,
page: child, page: child,
}); });
return ( return <Card key={child.id} title={child.title} leadingIcon={icon} href={href} />;
<Card
key={child.id}
title={child.linkTitle || child.title}
leadingIcon={icon}
href={href}
/>
);
}) })
); );
@@ -43,7 +43,7 @@ export async function PageFooterNavigation(props: {
<NavigationCard <NavigationCard
icon="chevron-left" icon="chevron-left"
label={t(language, 'previous_page')} label={t(language, 'previous_page')}
title={previous.linkTitle || previous.title} title={previous.title}
href={previousHref} href={previousHref}
insights={{ insights={{
type: 'link_click', type: 'link_click',
@@ -62,7 +62,7 @@ export async function PageFooterNavigation(props: {
<NavigationCard <NavigationCard
icon="chevron-right" icon="chevron-right"
label={t(language, 'next_page')} label={t(language, 'next_page')}
title={next.linkTitle || next.title} title={next.title}
href={nextHref} href={nextHref}
insights={{ insights={{
type: 'link_click', type: 'link_click',
@@ -6,10 +6,10 @@ import React, { type ButtonHTMLAttributes } from 'react';
import { useLanguage } from '@/intl/client'; import { useLanguage } from '@/intl/client';
import { t, tString } from '@/intl/translate'; import { t, tString } from '@/intl/translate';
import { tcls } from '@/lib/tailwind'; 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; const MAX_COMMENT_LENGTH = 512;
/** /**
@@ -24,6 +24,7 @@ export function PageFeedbackForm(props: {
const trackEvent = useTrackEvent(); const trackEvent = useTrackEvent();
const inputRef = React.useRef<HTMLTextAreaElement>(null); const inputRef = React.useRef<HTMLTextAreaElement>(null);
const [rating, setRating] = React.useState<PageFeedbackRating>(); const [rating, setRating] = React.useState<PageFeedbackRating>();
const [comment, setComment] = React.useState('');
const [submitted, setSubmitted] = React.useState(false); const [submitted, setSubmitted] = React.useState(false);
const onSubmitRating = (rating: PageFeedbackRating) => { const onSubmitRating = (rating: PageFeedbackRating) => {
@@ -85,21 +86,43 @@ export function PageFeedbackForm(props: {
</ButtonGroup> </ButtonGroup>
</div> </div>
{rating ? ( {rating ? (
<Input <div className="flex flex-col gap-2">
ref={inputRef} {!submitted ? (
label={tString(languages, 'was_this_helpful_comment')} <>
multiline <textarea
submitButton ref={inputRef}
rows={3} name="comment"
name="page-feedback-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"
onSubmit={(comment) => onSubmitComment(rating, comment as string)} placeholder={tString(languages, 'was_this_helpful_comment')}
maxLength={MAX_COMMENT_LENGTH} aria-label={tString(languages, 'was_this_helpful_comment')}
minLength={MIN_COMMENT_LENGTH} onChange={(e) => setComment(e.target.value)}
disabled={submitted} value={comment}
submitMessage={tString(languages, 'was_this_helpful_thank_you')} rows={3}
className="animate-blur-in" maxLength={MAX_COMMENT_LENGTH}
resize />
/> <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} ) : null}
</div> </div>
); );
@@ -57,15 +57,13 @@ function preloadFont(fontData: FontData) {
* It takes care of setting the theme and the language. * It takes care of setting the theme and the language.
*/ */
export async function CustomizationRootLayout(props: { 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. */ /** The class name to apply to the body element. */
bodyClassName?: string; className?: string;
forcedTheme?: CustomizationThemeMode | null; forcedTheme?: CustomizationThemeMode | null;
context: GitBookAnyContext; context: GitBookAnyContext;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { htmlClassName, bodyClassName, context, forcedTheme, children } = props; const { className, context, forcedTheme, children } = props;
const customization = const customization =
'customization' in context ? context.customization : defaultCustomization(); '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 // Set the dark/light class statically to avoid flashing and make it work when JS is disabled
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark (forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
? 'dark' ? 'dark'
: '', : ''
htmlClassName
)} )}
> >
<head> <head>
@@ -182,7 +179,7 @@ export async function CustomizationRootLayout(props: {
} }
`}</style> `}</style>
</head> </head>
<body className={tcls(bodyClassName, 'sheet-open:overflow-hidden')}> <body className={className}>
<IconsProvider <IconsProvider
assetsURL={GITBOOK_ICONS_URL} assetsURL={GITBOOK_ICONS_URL}
assetsURLToken={GITBOOK_ICONS_TOKEN} assetsURLToken={GITBOOK_ICONS_TOKEN}
@@ -58,7 +58,7 @@ export function SearchContainer({
viewport, viewport,
siteSpaces, siteSpaces,
}: SearchContainerProps) { }: SearchContainerProps) {
const { assistants, config } = useAI(); const { assistants } = useAI();
const [state, setSearchState] = useSearch(); const [state, setSearchState] = useSearch();
const searchAsk = useSearchAskState(); const searchAsk = useSearchAskState();
@@ -200,8 +200,7 @@ export function SearchContainer({
siteSpaceId: siteSpace.id, siteSpaceId: siteSpace.id,
siteSpaceIds, siteSpaceIds,
scope: state?.scope ?? 'default', scope: state?.scope ?? 'default',
withAI, withAI: withAI,
suggestions: config.suggestions,
}); });
const searchValue = state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? ''; const searchValue = state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? '';
@@ -1,9 +1,13 @@
'use client'; 'use client';
import React, { useEffect, useRef } from 'react'; import React from 'react';
import { useEffect, useRef } from 'react';
import { tString, useLanguage } from '@/intl/client'; import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons'; 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 { interface SearchInputProps {
onChange: (value: string) => void; onChange: (value: string) => void;
@@ -16,11 +20,14 @@ interface SearchInputProps {
children?: React.ReactNode; 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. * Input to trigger search.
*/ */
export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>( export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
function SearchInput(props, containerRef) { function SearchInput(props, ref) {
const { const {
onChange, onChange,
onKeyDown, onKeyDown,
@@ -35,6 +42,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const language = useLanguage(); const language = useLanguage();
const buttonStyles = useClassnames(['ButtonStyles']);
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
@@ -50,45 +58,74 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
}, [isOpen, value]); }, [isOpen, value]);
return ( return (
<div className="relative flex @max-2xl:size-9.5 grow"> <div className={tcls('relative flex size-9 grow', className)}>
<Input {/* biome-ignore lint/a11y/useKeyWithClickEvents: this div needs an onClick to show the input on mobile, where it's normally hidden.
data-testid="search-input" 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. */}
name="search-input" <div
ref={inputRef} ref={ref}
containerRef={containerRef as React.RefObject<HTMLDivElement | null>} onClick={onFocus}
sizing="medium" className={tcls(
label={tString(language, withAI ? 'search_or_ask' : 'search')} // Apply button styles
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" buttonStyles,
placeholder={`${tString(language, withAI ? 'search_or_ask' : 'search')}`} variantClasses.header,
onFocus={onFocus} sizeClasses,
onKeyDown={onKeyDown} // Additional custom styles
leading={ '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',
<Icon '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',
icon="search" '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.
className="size-4 shrink-0 text-tint theme-bold:text-header-link/8" '@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) => { <Icon
onChange(event.target.value); icon="magnifying-glass"
}} className="size-4 shrink-0 animate-scale-in"
value={value} />
maxLength={512} )}
autoComplete="off" {children}
aria-autocomplete="list" <input
aria-haspopup="listbox" {...rest}
aria-expanded={value && isOpen ? 'true' : 'false'} type="text"
clearButton={{ onFocus={onFocus}
className: onKeyDown={onKeyDown}
'theme-bold:text-header-link theme-bold:hover:bg-header-link/3 text-base', onChange={(event) => onChange(event.target.value)}
}} value={value}
keyboardShortcut={{ // We only show "search or ask" if the search input actually handles both search and ask.
className: placeholder={`${tString(language, withAI ? 'search_or_ask' : 'search')}`}
'theme-bold:border-header-link/4 theme-bold:bg-header-background theme-bold:text-header-link', maxLength={512}
keys: isOpen ? ['esc'] : ['mod', 'k'], size={10}
}} data-testid="search-input"
{...rest} className={tcls(
type="text" '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> </div>
); );
} }
@@ -35,9 +35,8 @@ export function useSearchResults(props: {
siteSpaceIds: string[]; siteSpaceIds: string[];
scope: SearchScope; scope: SearchScope;
withAI: boolean; withAI: boolean;
suggestions?: string[];
}) { }) {
const { disabled, query, siteSpaceId, siteSpaceIds, scope, suggestions } = props; const { disabled, query, siteSpaceId, siteSpaceIds, scope } = props;
const trackEvent = useTrackEvent(); const trackEvent = useTrackEvent();
@@ -79,22 +78,6 @@ export function useSearchResults(props: {
const questions = new Set<string>(); const questions = new Set<string>();
const recommendedQuestions: ResultType[] = []; 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 () => { const timeout = setTimeout(async () => {
if (cancelled) { if (cancelled) {
return; return;
@@ -173,11 +156,7 @@ export function useSearchResults(props: {
return; return;
} }
const aiEnrichedResults = withAI setResultsState({ results, fetching: false, error: false });
? withAskTriggers(results, query, assistants)
: results;
setResultsState({ results: aiEnrichedResults, fetching: false, error: false });
trackEvent({ trackEvent({
type: 'search_type_query', type: 'search_type_query',
@@ -196,9 +175,16 @@ export function useSearchResults(props: {
cancelled = true; cancelled = true;
clearTimeout(timeout); 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 <AIContextProvider
aiMode={customization.ai?.mode} aiMode={customization.ai?.mode}
suggestions={context.customization.ai?.suggestions}
trademark={customization.trademark.enabled} trademark={customization.trademark.enabled}
> >
<SpaceLayout <SpaceLayout
@@ -101,7 +100,6 @@ export async function generateSiteLayoutViewport(context: GitBookSiteContext): P
width: 'device-width', width: 'device-width',
initialScale: 1, initialScale: 1,
maximumScale: 1, maximumScale: 1,
viewportFit: 'cover',
}; };
} }
@@ -53,7 +53,7 @@ export type PageMetaLinks = {
/** /**
* Fetch and render a page. * Fetch and render a page.
*/ */
export async function SitePage(props: SitePageProps & { staticRoute: boolean }) { export async function SitePage(props: SitePageProps) {
const { const {
context, context,
page, page,
@@ -99,7 +99,6 @@ export async function SitePage(props: SitePageProps & { staticRoute: boolean })
document={document} document={document}
withPageFeedback={withPageFeedback} withPageFeedback={withPageFeedback}
insightsDisplayContext={SiteInsightsDisplayContext.Site} insightsDisplayContext={SiteInsightsDisplayContext.Site}
staticRoute={props.staticRoute}
/> />
</div> </div>
<PageClientLayout pageMetaLinks={pageMetaLinks} /> <PageClientLayout pageMetaLinks={pageMetaLinks} />
@@ -297,7 +296,7 @@ async function getPageDataWithFallback(args: {
}) { }) {
const { context: baseContext, pagePathParams } = args; const { context: baseContext, pagePathParams } = args;
const { context, pageTarget } = await fetchPageData(baseContext, pagePathParams); 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) ? resolvePageMetaLinks(context, pageTarget.page.id)
: null); : 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. * Get the <title> for a page.
*/ */
@@ -40,7 +40,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
orientation="vertical" orientation="vertical"
style={{ maxHeight: `${MAX_ITEMS * 3 + 2}rem` }} style={{ maxHeight: `${MAX_ITEMS * 3 + 2}rem` }}
className="pb-4" className="pb-4"
active={`#${currentSection.id}`} active={currentSection.id}
> >
<div className="flex w-full flex-col px-2"> <div className="flex w-full flex-col px-2">
{sectionsAndGroups.map((item) => { {sectionsAndGroups.map((item) => {
@@ -18,7 +18,7 @@ import type {
} from './encodeClientSiteSections'; } from './encodeClientSiteSections';
const SCREEN_OFFSET = 16; // 1rem 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 * A set of navigational links representing site sections for multi-section sites
*/ */
@@ -33,7 +33,6 @@ export function SiteSectionTabs(props: {
children, children,
} = props; } = props;
const containerRef = React.useRef<HTMLDivElement>(null);
const currentTriggerRef = React.useRef<HTMLButtonElement | null>(null); const currentTriggerRef = React.useRef<HTMLButtonElement | null>(null);
const [offset, setOffset] = React.useState<number | null>(null); const [offset, setOffset] = React.useState<number | null>(null);
const [value, setValue] = React.useState<string | undefined>(); const [value, setValue] = React.useState<string | undefined>();
@@ -42,15 +41,12 @@ export function SiteSectionTabs(props: {
React.useEffect(() => { React.useEffect(() => {
const trigger = currentTriggerRef.current; const trigger = currentTriggerRef.current;
const container = containerRef.current; if (!value || !trigger) {
if (!value || !trigger || !container) {
return; return;
} }
const triggerWidth = trigger.getBoundingClientRect().width - SCREEN_OFFSET; const triggerWidth = trigger.getBoundingClientRect().width;
const triggerLeft = const triggerLeft = trigger.getBoundingClientRect().left;
trigger.getBoundingClientRect().left -
(window.innerWidth - container.getBoundingClientRect().width) / 2;
setOffset(triggerLeft + triggerWidth / 2); setOffset(triggerLeft + triggerWidth / 2);
}, [value]); }, [value]);
@@ -62,7 +58,6 @@ export function SiteSectionTabs(props: {
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]', 'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]',
className className
)} )}
ref={containerRef}
value={value} value={value}
onValueChange={setValue} onValueChange={setValue}
skipDelayDuration={500} skipDelayDuration={500}
@@ -76,12 +71,8 @@ export function SiteSectionTabs(props: {
? 'md:-mr-8 -mr-4 sm:-mr-6' ? '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' : 'after:contents[] after:absolute after:inset-y-2 after:right-0 after:border-transparent after:border-r after:transition-colors'
)} )}
active={`#${currentSection.id}`} active={currentSection.id}
trailing={{ trailingEdgeScrollClassName={children ? 'after:border-tint' : ''}
fade: true,
button: true,
className: children ? 'after:border-tint' : '',
}}
> >
<NavigationMenu.List <NavigationMenu.List
className={tcls( className={tcls(
@@ -150,14 +141,14 @@ export function SiteSectionTabs(props: {
{children} {children}
<div <div
className="absolute top-full left-0 z-20 flex w-full" className="fixed top-full left-0 z-20 flex w-full"
style={{ style={{
padding: `0 ${SCREEN_OFFSET}px 0 ${SCREEN_OFFSET}px`, padding: `0 ${SCREEN_OFFSET}px 0 ${SCREEN_OFFSET}px`,
}} }}
> >
<NavigationMenu.Viewport <NavigationMenu.Viewport
className={tcls( className={tcls(
'relative origin-top overflow-auto circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base shadow-lg ease-in-out', 'relative origin-top overflow-auto circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base shadow-lg transition-transform duration-250 ease-in-out',
'-mt-0.5 w-full md:w-max', '-mt-0.5 w-full md:w-max',
'max-h-[calc(100vh-8rem)] data-[state=closed]:animate-scale-out data-[state=open]:animate-scale-in', 'max-h-[calc(100vh-8rem)] data-[state=closed]:animate-scale-out data-[state=open]:animate-scale-in',
"[&:not([style*='--radix-navigation-menu-viewport-width'])]:hidden" // The viewport width is only calculated once it's triggered, and can take a while. We hide the viewport until it's ready. "[&:not([style*='--radix-navigation-menu-viewport-width'])]:hidden" // The viewport width is only calculated once it's triggered, and can take a while. We hide the viewport until it's ready.
@@ -165,7 +156,7 @@ export function SiteSectionTabs(props: {
style={{ style={{
translate: translate:
!isMobile && offset !isMobile && offset
? `clamp(0px, calc(${offset}px - var(--radix-navigation-menu-viewport-width, 0px)/2), calc(100vw - var(--radix-navigation-menu-viewport-width, 0px) - ${SCREEN_OFFSET * 3}px)) 0 0` ? `clamp(0px, calc(${offset}px - ${SCREEN_OFFSET}px - 50%), calc(100vw - var(--radix-navigation-menu-viewport-width, 0px) - ${SCREEN_OFFSET * 3}px)) 0 0`
: '0 0 0', // TranslateZ is needed to force a stacking context, fixing a rendering bug on Safari : '0 0 0', // TranslateZ is needed to force a stacking context, fixing a rendering bug on Safari
display: offset === null ? 'none' : undefined, display: offset === null ? 'none' : undefined,
}} }}
@@ -227,7 +218,7 @@ function SectionGroupTileList(props: {
{hasSections && ( {hasSections && (
<ul <ul
className={tcls( 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' : '' hasGroups ? 'bg-tint-base' : ''
)} )}
style={{ style={{
@@ -248,7 +239,7 @@ function SectionGroupTileList(props: {
{hasGroups && ( {hasGroups && (
<ul <ul
className={tcls( 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 hasSections
? 'border-tint-subtle bg-tint-subtle max-md:border-t md:border-l' ? 'border-tint-subtle bg-tint-subtle max-md:border-t md:border-l'
: '' : ''
@@ -280,7 +271,7 @@ function SectionGroupTile(props: {
const { url, icon, title, description } = child; const { url, icon, title, description } = child;
const isActive = child.id === currentSection.id; const isActive = child.id === currentSection.id;
return ( 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 <Link
href={url} href={url}
className={tcls( className={tcls(
@@ -321,7 +312,7 @@ function SectionGroupTile(props: {
const { title, icon, children } = child; const { title, icon, children } = child;
return ( 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"> <div className="mt-3 mb-2 flex gap-2.5 px-3 font-semibold text-tint-subtle text-xs uppercase tracking-wider">
{icon && ( {icon && (
<SectionIcon className="mt-0.5" isActive={false} icon={icon as IconName} /> <SectionIcon className="mt-0.5" isActive={false} icon={icon as IconName} />
@@ -331,7 +322,7 @@ function SectionGroupTile(props: {
<ul <ul
className="flex grid-flow-row flex-col gap-x-2 gap-y-1 md:grid" className="flex grid-flow-row flex-col gap-x-2 gap-y-1 md:grid"
style={{ 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) => ( {children.map((nestedChild) => (
@@ -128,7 +128,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
'lg:justify-center', 'lg:justify-center',
CONTAINER_STYLE, CONTAINER_STYLE,
'site-width-wide:max-w-screen-4xl', '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 // Ensure the footer is display below the viewport even if the content is not enough
withFooter && [ withFooter && [
@@ -141,82 +141,79 @@ export function SpaceLayout(props: SpaceLayoutProps) {
<TableOfContents <TableOfContents
context={context} context={context}
header={ header={
<div withTopHeader ? null : (
className={tcls( <div
'pr-4', className={tcls(
'flex', 'hidden',
withTopHeader ? 'lg:hidden' : '', 'pr-4',
'grow-0', 'mt-2',
'dark:shadow-light/1', 'lg:flex',
'text-base/tight', 'grow-0',
'items-center' 'dark:shadow-light/1',
)} 'text-base/tight',
> 'items-center'
<HeaderLogo context={context} /> )}
{variants.translations.length > 1 ? ( >
<TranslationsDropdown <HeaderLogo context={context} />
context={context} {variants.translations.length > 1 ? (
siteSpace={ <TranslationsDropdown
variants.translations.find( context={context}
(space) => space.id === siteSpace.id siteSpace={
) ?? siteSpace variants.translations.find(
} (space) => space.id === siteSpace.id
siteSpaces={variants.translations} ) ?? siteSpace
className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden" }
/> siteSpaces={variants.translations}
) : null} className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden"
</div> />
) : null}
</div>
)
} }
// Displays the search button and/or the space dropdown in the ToC // Displays the search button and/or the space dropdown in the ToC
// according to the header/variant settings. // according to the header/variant settings.
// E.g if there is no header, the search button will be displayed in the ToC. // E.g if there is no header, the search button will be displayed in the ToC.
innerHeader={ innerHeader={
!withTopHeader || variants.generic.length > 1 ? ( <>
<div {!withTopHeader && (
className={tcls( <div className="flex gap-2">
'my-5 sidebar-default:mt-2 flex flex-col gap-2 px-5 empty:hidden', <SearchContainer
variants.generic.length > 1 ? '' : 'max-lg:hidden' style={CustomizationSearchStyle.Subtle}
)} withVariants={variants.generic.length > 1}
> withSiteVariants={
{!withTopHeader && ( visibleSections?.list.some(
<div className="flex gap-2 max-lg:hidden"> (s) =>
<SearchContainer s.object === 'site-section' &&
style={CustomizationSearchStyle.Subtle} s.siteSpaces.length > 1
withVariants={variants.generic.length > 1} ) ?? false
withSiteVariants={ }
visibleSections?.list.some( withSections={withSections}
(s) => section={visibleSections?.current}
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}
siteSpace={siteSpace} siteSpace={siteSpace}
siteSpaces={variants.generic} siteSpaces={visibleSiteSpaces}
className="w-full px-3" className="max-lg:hidden"
viewport="desktop"
/> />
) : null} </div>
</div> )}
) : null {!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} {children}
@@ -32,8 +32,7 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
'my-2', 'my-2',
'border-tint-subtle', 'border-tint-subtle',
'sidebar-list-default:border-l', 'sidebar-list-default:border-l',
'sidebar-list-line:border-l', 'sidebar-list-line:border-l'
'break-anywhere'
)} )}
/> />
) : null ) : null
@@ -14,9 +14,9 @@ export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boole
<li className="flex flex-col"> <li className="flex flex-col">
<div <div
className={tcls( 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', '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. '-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. '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', '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-bold.tint_&]:bg-tint-subtle',
'[html.sidebar-filled.theme-muted_&]:bg-tint-base', '[html.sidebar-filled.theme-muted_&]:bg-tint-base',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base', '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
'lg:[html.sidebar-default.theme-gradient_&]:bg-gradient-primary', '[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
'lg:[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint', '[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint',
isFirst ? '-mt-4' : '' isFirst ? '-mt-6' : ''
)} )}
> >
<TOCPageIcon page={page} /> <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,61 +3,41 @@ import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
import type React from 'react'; import type React from 'react';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { SideSheet } from '../primitives/SideSheet';
import { PagesList } from './PagesList'; import { PagesList } from './PagesList';
import { TOCScrollContainer } from './TOCScroller';
import { TableOfContentsScript } from './TableOfContentsScript'; import { TableOfContentsScript } from './TableOfContentsScript';
import { Trademark } from './Trademark'; import { Trademark } from './Trademark';
import { encodeClientTableOfContents } from './encodeClientTableOfContents'; import { encodeClientTableOfContents } from './encodeClientTableOfContents';
/**
* Sidebar container, responsible for setting the right dimensions and position for the sidebar.
*/
export async function TableOfContents(props: { export async function TableOfContents(props: {
context: GitBookSiteContext; context: GitBookSiteContext;
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
className?: string;
}) { }) {
const { innerHeader, context, header, className } = props; const { innerHeader, context, header } = props;
const { customization, revision } = context; const { customization, revision } = context;
const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages); const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages);
return ( return (
<> <>
<SideSheet <aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
side="left"
data-testid="table-of-contents" data-testid="table-of-contents"
id="table-of-contents" id="table-of-contents"
toggleClass="navigation-open"
withScrim={true}
withCloseButton={true}
className={tcls( className={tcls(
'group/table-of-contents', 'group',
'text-sm', 'text-sm',
'grow-0', 'grow-0',
'shrink-0', 'shrink-0',
'basis-full',
'lg:basis-72',
'lg:page-no-toc:basis-56',
'w-72', 'relative',
'lg:max-xl:w-56', 'z-1',
'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!',
'lg:sticky', 'lg:sticky',
'lg:mr-12', 'lg:mr-12',
// 'lg:z-0!',
'lg:z-20',
// Server-side static positioning // Server-side static positioning
'lg:top-0', 'lg:top-0',
@@ -79,23 +59,29 @@ 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-top-offset"]_&]:top-(--outline-top-offset)!',
'lg:page-no-toc:[html[style*="--outline-height"]_&]:top-(--outline-height)!', '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:sidebar-filled:pr-6',
'lg:page-no-toc:pr-0', 'lg:page-no-toc:pr-0',
'hidden', 'hidden',
'navigation-open:flex!', 'navigation-open:flex!',
'lg:flex', 'lg:flex',
'lg:page-no-toc:hidden',
'xl:page-no-toc:flex',
'lg:site-header-none:page-no-toc:flex',
'flex-col', 'flex-col',
'gap-4', 'gap-4',
className
'navigation-open:border-b',
'border-tint-subtle'
)} )}
> >
{header ? header : null} {header && header}
<div // The actual sidebar, either shown with a filled bg or transparent. <div // The actual sidebar, either shown with a filled bg or transparent.
className={tcls( className={tcls(
'-ms-5', 'lg:-ms-5',
'relative flex min-h-0 grow flex-col border-tint-subtle', 'relative flex grow flex-col overflow-hidden border-tint-subtle',
'sidebar-filled:bg-tint-subtle', 'sidebar-filled:bg-tint-subtle',
'theme-muted:bg-tint-subtle', 'theme-muted:bg-tint-subtle',
@@ -103,7 +89,6 @@ export async function TableOfContents(props: {
'[html.sidebar-filled.theme-muted_&]:bg-tint-base', '[html.sidebar-filled.theme-muted_&]:bg-tint-base',
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base', '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
'[html.sidebar-filled.theme-gradient_&]:border', '[html.sidebar-filled.theme-gradient_&]:border',
'max-lg:sidebar-filled:border',
'page-no-toc:bg-transparent!', 'page-no-toc:bg-transparent!',
'page-no-toc:border-none!', 'page-no-toc:border-none!',
@@ -112,33 +97,32 @@ export async function TableOfContents(props: {
'page-has-toc:[html.sidebar-filled.circular-corners_&]:rounded-3xl' 'page-has-toc:[html.sidebar-filled.circular-corners_&]:rounded-3xl'
)} )}
> >
{innerHeader ? innerHeader : null} {innerHeader ? (
<ScrollContainer <div className="my-4 flex flex-col space-y-4 px-5 empty:hidden">
data-testid="toc-scroll-container" {innerHeader}
orientation="vertical" </div>
contentClassName="flex flex-col p-2 gutter-stable" ) : null}
active="[data-active=true]" <TOCScrollContainer // The scrollview inside the sidebar
leading={{ className={tcls(
fade: true, 'flex grow flex-col p-2 pt-4',
button: { customization.trademark.enabled && 'lg:pb-20',
className: '-mt-4', 'hide-scrollbar overflow-y-auto'
}, )}
}}
> >
<PagesList <PagesList
pages={pages} pages={pages}
isRoot={true} 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 ? (
{customization.trademark.enabled ? ( <Trademark
<Trademark context={context}
context={context} placement={SiteInsightsTrademarkPlacement.Sidebar}
placement={SiteInsightsTrademarkPlacement.Sidebar} />
/> ) : null}
) : null} </TOCScrollContainer>
</div> </div>
</SideSheet> </aside>
<TableOfContentsScript /> <TableOfContentsScript />
</> </>
); );
@@ -8,6 +8,7 @@ import { tcls } from '@/lib/tailwind';
import { useCurrentPagePath } from '../hooks'; import { useCurrentPagePath } from '../hooks';
import { Link, type LinkInsightsProps, type LinkProps } from '../primitives'; 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. * 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 { isActive, href, insights, children, onActiveClick } = props;
const anchorRef = useRef<HTMLAnchorElement>(null);
useScrollToActiveTOCItem({ anchorRef, isActive });
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
if (isActive && onActiveClick) { if (isActive && onActiveClick) {
@@ -90,7 +93,7 @@ function LinkItem(
return ( return (
<Link <Link
data-active={isActive} ref={anchorRef}
href={href} href={href}
insights={insights} insights={insights}
aria-current={isActive ? 'page' : undefined} aria-current={isActive ? 'page' : undefined}
@@ -13,17 +13,52 @@ import { Link } from '../primitives';
export function Trademark(props: { export function Trademark(props: {
context: GitBookSpaceContext; context: GitBookSpaceContext;
placement: SiteInsightsTrademarkPlacement; placement: SiteInsightsTrademarkPlacement;
className?: string;
}) { }) {
const { className, ...rest } = props;
return ( return (
<div <div
className={tcls( className={tcls(
'mt-auto circular-corners:rounded-2xl rounded-lg straight-corners:rounded-none px-2 sidebar-default:pr-4 pb-2', 'relative',
className '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!'
)} )}
> >
<TrademarkLink {...rest} /> <TrademarkLink {...props} />
</div> </div>
); );
} }
@@ -34,9 +69,8 @@ export function Trademark(props: {
export function TrademarkLink(props: { export function TrademarkLink(props: {
context: GitBookSpaceContext; context: GitBookSpaceContext;
placement: SiteInsightsTrademarkPlacement; placement: SiteInsightsTrademarkPlacement;
className?: string;
}) { }) {
const { context, placement, className } = props; const { context, placement } = props;
const { space } = context; const { space } = context;
const language = getSpaceLanguage(context); const language = getSpaceLanguage(context);
@@ -51,6 +85,9 @@ export function TrademarkLink(props: {
href={url.toString()} href={url.toString()}
className={tcls( className={tcls(
'text-sm', 'text-sm',
// 'lg:max-xl:page-no-toc:text-xs',
// 'lg:max-xl:page-no-toc:px-3',
// 'lg:max-xl:page-no-toc:py-3',
'font-semibold', 'font-semibold',
'text-tint', 'text-tint',
@@ -62,27 +99,43 @@ export function TrademarkLink(props: {
'sidebar-filled:px-3', 'sidebar-filled:px-3',
'lg:sidebar-filled:page-no-toc:px-5', '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:bg-tint',
'hover:text-tint-strong', 'hover:text-tint-strong',
'ring-1', 'ring-2',
'lg:ring-1',
'ring-inset', 'ring-inset',
'ring-tint-subtle', 'ring-tint-subtle',
'transition-colors', 'transition-colors',
'pointer-events-auto', 'pointer-events-auto'
className
)} )}
insights={{ insights={{
type: 'trademark_click', type: 'trademark_click',
placement, placement,
}} }}
> >
<Icon icon="gitbook" className={tcls('size-5', 'shrink-0')} /> <Icon
<span className={tcls('ml-3')}>{t(language, 'powered_by_gitbook')}</span> icon="gitbook"
className={tcls(
'size-5',
// 'lg:max-xl:page-no-toc:size-4',
'shrink-0'
)}
/>
<span
className={tcls(
'ml-3'
// 'lg:max-xl:page-no-toc:ml-2'
)}
>
{t(language, 'powered_by_gitbook')}
</span>
</Link> </Link>
); );
} }
@@ -72,7 +72,7 @@ export async function encodeClientTableOfContents(
result.push( result.push(
removeUndefined({ removeUndefined({
id: page.id, id: page.id,
title: page.linkTitle || page.title, title: page.title,
href, href,
emoji: page.emoji, emoji: page.emoji,
icon: page.icon, icon: page.icon,
@@ -1,3 +1,4 @@
export { TableOfContents } from './TableOfContents'; export { TableOfContents } from './TableOfContents';
export { PagesList } from './PagesList'; export { PagesList } from './PagesList';
export { TOCScrollContainer } from './TOCScroller';
export { Trademark } from './Trademark'; 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', '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', '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', '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 = [ export const ToggleableLinkItemActiveStyles = [
@@ -27,11 +27,7 @@ export function ThemeToggler() {
}; };
return ( return (
<ButtonGroup <ButtonGroup role="radiogroup" className="gap-2" combinedShape={false}>
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}
>
<ThemeButton <ThemeButton
active={mounted && theme === 'light'} active={mounted && theme === 'light'}
icon="sun-bright" icon="sun-bright"
@@ -70,7 +66,11 @@ function ThemeButton(props: {
aria-checked={active} aria-checked={active}
variant="blank" variant="blank"
size="default" 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} icon={icon}
iconOnly 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. * Style for the container to adapt between normal and full width.
*/ */
export const CONTAINER_STYLE: ClassValue = [ export const CONTAINER_STYLE: ClassValue = [
'px-4 pl-[max(env(safe-area-inset-left),1rem)] pr-[max(env(safe-area-inset-right),1rem)]', 'px-4',
'sm:px-6 sm:pl-[max(env(safe-area-inset-left),1.5rem)] sm:pr-[max(env(safe-area-inset-right),1.5rem)]', 'sm:px-6',
'md:px-8 md:pl-[max(env(safe-area-inset-left),2rem)] md:pr-[max(env(safe-area-inset-right),2rem)]', 'md:px-8',
'max-w-screen-2xl', 'max-w-screen-2xl',
'mx-auto', 'mx-auto',
]; ];
@@ -7,7 +7,7 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons'; import { Icon, type IconName } from '@gitbook/icons';
import { Link, type LinkInsightsProps } from './Link'; import { Link, type LinkInsightsProps } from './Link';
import { useClassnames } from './StyleProvider'; import { useClassnames } from './StyleProvider';
import { Tooltip, type TooltipProps } from './Tooltip'; import { Tooltip } from './Tooltip';
export type ButtonProps = { export type ButtonProps = {
href?: string; href?: string;
@@ -20,7 +20,6 @@ export type ButtonProps = {
trailing?: React.ReactNode; trailing?: React.ReactNode;
children?: React.ReactNode; children?: React.ReactNode;
active?: boolean; active?: boolean;
tooltipProps?: TooltipProps;
} & LinkInsightsProps & } & LinkInsightsProps &
React.HTMLAttributes<HTMLElement>; React.HTMLAttributes<HTMLElement>;
@@ -113,7 +112,6 @@ export const Button = React.forwardRef<
active, active,
trailing, trailing,
disabled, disabled,
tooltipProps,
...rest ...rest
}, },
ref ref
@@ -135,29 +133,18 @@ export const Button = React.forwardRef<
); );
const buttonOnlyClassNames = useClassnames(['ButtonStyles']); const buttonOnlyClassNames = useClassnames(['ButtonStyles']);
let iconElement = null;
if (icon) {
if (React.isValidElement(icon)) {
type IconElement = React.ReactElement<React.SVGProps<SVGSVGElement>>;
iconElement = React.cloneElement(icon as IconElement, {
className: tcls(
'button-leading-icon size-[1em] shrink-0',
(icon as IconElement).props.className
),
});
} else {
iconElement = (
<Icon
icon={icon as IconName}
className={tcls('button-leading-icon size-[1em] shrink-0')}
/>
);
}
}
const content = ( const content = (
<> <>
{iconElement} {icon ? (
typeof icon === 'string' ? (
<Icon
icon={icon as IconName}
className={tcls('button-leading-icon size-[1em] shrink-0')}
/>
) : (
icon
)
) : null}
{iconOnly || (!children && !label) ? null : ( {iconOnly || (!children && !label) ? null : (
<span className="button-content truncate">{children ?? label}</span> <span className="button-content truncate">{children ?? label}</span>
)} )}
@@ -197,13 +184,9 @@ export const Button = React.forwardRef<
return (children || iconOnly) && label ? ( return (children || iconOnly) && label ? (
<Tooltip <Tooltip
rootProps={{ rootProps={{ open: disabled === true ? false : undefined }}
open: disabled === true ? false : undefined,
...tooltipProps?.rootProps,
}}
label={label} label={label}
triggerProps={{ disabled, ...tooltipProps?.triggerProps }} triggerProps={{ disabled }}
contentProps={{ ...tooltipProps?.contentProps }}
> >
{button} {button}
</Tooltip> </Tooltip>
@@ -28,7 +28,7 @@ export function HoverCard(
<RadixHoverCard.Portal> <RadixHoverCard.Portal>
<RadixHoverCard.Content <RadixHoverCard.Content
side={props.side ?? 'top'} 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 <div
className={tcls( 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'; 'use client';
import { tcls } from '@/lib/tailwind'; import { type ClassValue, tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons'; import { Icon } from '@gitbook/icons';
import * as React from 'react'; import * as React from 'react';
export type KeyboardShortcutProps = {
keys: string[];
} & React.HTMLAttributes<HTMLDivElement>;
function getOperatingSystem() { function getOperatingSystem() {
const platform = navigator.platform.toLowerCase(); const platform = navigator.platform.toLowerCase();
@@ -17,7 +13,7 @@ function getOperatingSystem() {
return 'win'; return 'win';
} }
export function KeyboardShortcut(props: KeyboardShortcutProps) { export function KeyboardShortcut(props: { keys: string[]; className?: ClassValue }) {
const { keys, className } = props; const { keys, className } = props;
const [operatingSystem, setOperatingSystem] = React.useState<string | null>(null); const [operatingSystem, setOperatingSystem] = React.useState<string | null>(null);
@@ -45,7 +41,7 @@ export function KeyboardShortcut(props: KeyboardShortcutProps) {
break; break;
case 'enter': case 'enter':
element = <Icon icon="arrow-turn-down-left" className="size-[.9em]" />; element = <Icon icon="arrow-turn-down-left" className="size-[.75em]" />;
break; break;
} }
return ( return (
@@ -78,8 +78,8 @@ export function Link(props: LinkProps) {
const onClick = (event: React.MouseEvent<HTMLAnchorElement>) => { const onClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
const isExternalWithOrigin = isExternalLink(href, window.location.origin); const isExternalWithOrigin = isExternalLink(href, window.location.origin);
// Only trigger navigation context for internal links in the same window without modifier keys (i.e. open in new tab). // Only trigger navigation context for internal links without modifier keys (i.e. open in new tab).
if (!isExternal && target !== '_blank' && !event.ctrlKey && !event.metaKey) { if (!isExternal && !event.ctrlKey && !event.metaKey) {
onNavigationClick(href); onNavigationClick(href);
} }
@@ -4,7 +4,7 @@ import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import * as React from 'react'; import * as React from 'react';
import { useScrollListener } from '../hooks/useScrollListener'; 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. * A container that encapsulates a scrollable area with usability features.
@@ -17,26 +17,17 @@ export type ScrollContainerProps = {
className?: string; className?: string;
contentClassName?: 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. */ /** The direction of the scroll container. */
orientation: 'horizontal' | 'vertical'; orientation: 'horizontal' | 'vertical';
leading?: { /** Whether to fade out the edges of the container. */
/** Whether to fade out the leading edge of the container. */ fadeEdges?: ('leading' | 'trailing')[];
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;
};
/** The ID or ref of the active item to scroll to. */ /** The ID or ref of the active item to scroll to. */
active?: string | React.RefObject<HTMLElement | null>; active?: string | React.RefObject<HTMLElement | null>;
@@ -48,9 +39,10 @@ export function ScrollContainer(props: ScrollContainerProps) {
className, className,
contentClassName, contentClassName,
orientation, orientation,
fadeEdges = ['leading', 'trailing'],
active, active,
leading = { fade: true, button: true }, leadingEdgeScrollClassName,
trailing = { fade: true, button: true }, trailingEdgeScrollClassName,
...rest ...rest
} = props; } = props;
@@ -110,9 +102,7 @@ export function ScrollContainer(props: ScrollContainerProps) {
return; return;
} }
const activeItem = const activeItem =
typeof active === 'string' typeof active === 'string' ? document.getElementById(active) : active.current;
? containerRef.current?.querySelector(active)
: active.current;
if (!activeItem || !container.contains(activeItem)) { if (!activeItem || !container.contains(activeItem)) {
return; return;
} }
@@ -148,30 +138,28 @@ export function ScrollContainer(props: ScrollContainerProps) {
return ( return (
<div <div
className={tcls( className={tcls(
'group/scroll-container relative flex shrink grow', 'group/scroll-container relative flex overflow-hidden',
orientation === 'horizontal' ? 'min-w-0' : 'min-h-0',
className, className,
scrollPosition > 0 ? leading?.className : '', scrollPosition > 0 ? leadingEdgeScrollClassName : '',
scrollPosition < scrollSize ? trailing?.className : '' scrollPosition < scrollSize ? trailingEdgeScrollClassName : ''
)} )}
{...rest} {...rest}
> >
{/* Scrollable content */} {/* Scrollable content */}
<div <div
className={tcls( className={tcls(
'flex flex-1 overflow-hidden', 'flex shrink grow',
orientation === 'horizontal' ? 'min-w-0' : 'min-h-0',
orientation === 'horizontal' ? 'no-scrollbar' : 'hide-scrollbar', orientation === 'horizontal' ? 'no-scrollbar' : 'hide-scrollbar',
orientation === 'horizontal' ? 'overflow-x-scroll' : 'flex-col overflow-y-auto', orientation === 'horizontal' ? 'overflow-x-scroll' : 'flex-col overflow-y-auto',
leading.fade && scrollPosition > 0 fadeEdges.includes('leading') && scrollPosition > 0
? orientation === 'horizontal' ? orientation === 'horizontal'
? 'mask-l-from-[calc(100%-1rem)]' ? 'mask-l-from-[calc(100%-2rem)]'
: 'mask-t-from-[calc(100%-1rem)]' : 'mask-t-from-[calc(100%-2rem)]'
: '', : '',
trailing.fade && scrollPosition < scrollSize fadeEdges.includes('trailing') && scrollPosition < scrollSize
? orientation === 'horizontal' ? orientation === 'horizontal'
? 'mask-r-from-[calc(100%-1rem)]' ? 'mask-r-from-[calc(100%-2rem)]'
: 'mask-b-from-[calc(100%-1rem)]' : 'mask-b-from-[calc(100%-2rem)]'
: '', : '',
contentClassName contentClassName
)} )}
@@ -181,52 +169,44 @@ export function ScrollContainer(props: ScrollContainerProps) {
</div> </div>
{/* Scroll buttons back & forward */} {/* Scroll buttons back & forward */}
{leading.button !== false ? ( <Button
<Button icon={orientation === 'horizontal' ? 'chevron-left' : 'chevron-up'}
icon={orientation === 'horizontal' ? 'chevron-left' : 'chevron-up'} iconOnly
iconOnly size="xsmall"
size="xsmall" variant="secondary"
variant="secondary" tabIndex={-1}
tabIndex={-1} className={tcls(
onClick={scrollBack} 'bg-tint-base!',
label={tString(language, 'scroll_back')} orientation === 'horizontal'
{...(typeof leading.button === 'object' ? leading.button : {})} ? '-translate-y-1/2! top-1/2 left-0 ml-2'
className={tcls( : '-translate-x-1/2! top-0 left-1/2 mt-2',
'z-10 bg-tint-base!', 'absolute not-pointer-none:block hidden scale-0 opacity-0 transition-[scale,opacity]',
orientation === 'horizontal' scrollPosition > 0
? '-translate-y-1/2! top-1/2 left-0 ml-2' ? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
: '-translate-x-1/2! top-0 left-1/2 mt-2', : 'pointer-events-none'
'absolute z-10 not-pointer-none:block hidden scale-0 opacity-0 transition-[scale,opacity]', )}
scrollPosition > 0 onClick={scrollBack}
? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11' label={tString(language, 'scroll_back')}
: 'pointer-events-none', />
typeof leading.button === 'object' ? leading.button.className : '' <Button
)} icon={orientation === 'horizontal' ? 'chevron-right' : 'chevron-down'}
/> iconOnly
) : null} size="xsmall"
{trailing.button !== false ? ( variant="secondary"
<Button tabIndex={-1}
icon={orientation === 'horizontal' ? 'chevron-right' : 'chevron-down'} className={tcls(
iconOnly 'bg-tint-base!',
size="xsmall" orientation === 'horizontal'
variant="secondary" ? '-translate-y-1/2! top-1/2 right-0 mr-2'
tabIndex={-1} : '-translate-x-1/2! bottom-0 left-1/2 mb-2',
onClick={scrollFurther} 'absolute not-pointer-none:block hidden scale-0 transition-[scale,opacity]',
label={tString(language, 'scroll_further')} scrollPosition < scrollSize
{...(typeof trailing.button === 'object' ? trailing.button : {})} ? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11'
className={tcls( : 'pointer-events-none'
'z-10 bg-tint-base!', )}
orientation === 'horizontal' onClick={scrollFurther}
? '-translate-y-1/2! top-1/2 right-0 mr-2' label={tString(language, 'scroll_further')}
: '-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}
</div> </div>
); );
} }
@@ -234,7 +214,7 @@ export function ScrollContainer(props: ScrollContainerProps) {
/** /**
* Scroll to an element in a container. * Scroll to an element in a container.
*/ */
function scrollToElementInContainer(element: Element, container: HTMLElement) { function scrollToElementInContainer(element: HTMLElement, container: HTMLElement) {
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
@@ -249,6 +229,6 @@ function scrollToElementInContainer(element: Element, container: HTMLElement) {
(rect.left - containerRect.left) - (rect.left - containerRect.left) -
container.clientWidth / 2 + container.clientWidth / 2 +
rect.width / 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?.();
}}
/>
);
}
@@ -4,12 +4,6 @@ import { tcls } from '@/lib/tailwind';
import * as RadixTooltip from '@radix-ui/react-tooltip'; import * as RadixTooltip from '@radix-ui/react-tooltip';
import { useState } from 'react'; import { useState } from 'react';
export type TooltipProps = {
rootProps?: RadixTooltip.TooltipProps;
triggerProps?: RadixTooltip.TooltipTriggerProps;
contentProps?: RadixTooltip.TooltipContentProps;
};
export function Tooltip(props: { export function Tooltip(props: {
children: React.ReactNode; children: React.ReactNode;
label?: string | React.ReactNode; label?: string | React.ReactNode;
@@ -13,4 +13,3 @@ export * from './Popover';
export * from './LoadingStateProvider'; export * from './LoadingStateProvider';
export * from './HoverCard'; export * from './HoverCard';
export * from './DropdownMenu'; export * from './DropdownMenu';
export * from './Input';
-11
View File
@@ -55,16 +55,5 @@ export function getEmbeddableLinker(linker: GitBookLinker): GitBookLinker {
spaceBasePath: joinPath(override.spaceBasePath, '~gitbook/embed/page'), spaceBasePath: joinPath(override.spaceBasePath, '~gitbook/embed/page'),
}); });
}, },
toLinkForContent(rawURL: string): string {
const result = linker.toLinkForContent(rawURL);
// If the link is not relative or already an embed, return it as is
if (result.includes('~gitbook/embed') || !result.startsWith('/')) {
return result;
}
// If the link is relative, assume it's a section link and append the embed path
return joinPath(result, '~gitbook/embed/page');
},
}; };
} }
+4 -4
View File
@@ -129,7 +129,7 @@ export async function resolveContentRef(
const page = resolvePageResult?.page; const page = resolvePageResult?.page;
const ancestors = const ancestors =
resolvePageResult?.ancestors.map((ancestor) => ({ resolvePageResult?.ancestors.map((ancestor) => ({
label: ancestor.linkTitle || ancestor.title, label: ancestor.title,
icon: icon:
ancestor.emoji || ancestor.icon ? ( ancestor.emoji || ancestor.icon ? (
<PageIcon <PageIcon
@@ -153,9 +153,9 @@ export async function resolveContentRef(
// Compute the text to display for the link // Compute the text to display for the link
if (anchor) { if (anchor) {
text = page.linkTitle || page.title; text = page.title;
ancestors.push({ ancestors.push({
label: page.linkTitle || page.title, label: page.title,
icon: <PageIcon page={page} style={iconStyle} />, icon: <PageIcon page={page} style={iconStyle} />,
href, href,
}); });
@@ -177,7 +177,7 @@ export async function resolveContentRef(
parentPage && contentRef.page === parentPage.id && parentPage.type === 'group' parentPage && contentRef.page === parentPage.id && parentPage.type === 'group'
? parentPage ? parentPage
: page; : page;
text = pageOrGroup.linkTitle || pageOrGroup.title; text = pageOrGroup.title;
emoji = isCurrentPage ? undefined : page.emoji; emoji = isCurrentPage ? undefined : page.emoji;
icon = <PageIcon page={pageOrGroup} style={iconStyle} />; icon = <PageIcon page={pageOrGroup} style={iconStyle} />;
} }
-1
View File
@@ -612,7 +612,6 @@ function encodePathInSiteContent(rawPathname: string): {
} }
switch (pathname) { switch (pathname) {
case '~gitbook/embed':
case '~gitbook/embed/assistant': case '~gitbook/embed/assistant':
case '~gitbook/icon': case '~gitbook/icon':
return { pathname }; return { pathname };
+1 -1
View File
@@ -61,13 +61,13 @@ export function servePagesSitemap(context: GitBookSiteContext) {
const lastModified = page.updatedAt || page.createdAt; const lastModified = page.updatedAt || page.createdAt;
const url: { loc: string; priority: number; lastmod?: string } = { const url: { loc: string; priority: number; lastmod?: string } = {
priority: normalizedPriority,
loc: linker.toAbsoluteURL( loc: linker.toAbsoluteURL(
linker.toPathForPage({ linker.toPathForPage({
pages: context.revision.pages, pages: context.revision.pages,
page, page,
}) })
), ),
priority: normalizedPriority,
}; };
if (lastModified) { if (lastModified) {
+9 -13
View File
@@ -328,10 +328,10 @@ const config: Config = {
blurOut: 'blurOut 200ms ease-in both', blurOut: 'blurOut 200ms ease-in both',
'blurOut-slow': 'blurOut 500ms ease-in both', 'blurOut-slow': 'blurOut 500ms ease-in both',
enterFromLeft: 'enterFromLeft 300ms cubic-bezier(0.83, 0, 0.17, 1) both', enterFromLeft: 'enterFromLeft 250ms cubic-bezier(0.83, 0, 0.17, 1) both',
enterFromRight: 'enterFromRight 300ms cubic-bezier(0.83, 0, 0.17, 1) both', enterFromRight: 'enterFromRight 250ms cubic-bezier(0.83, 0, 0.17, 1) both',
exitToLeft: 'exitToLeft 300ms cubic-bezier(0.83, 0, 0.17, 1) both', exitToLeft: 'exitToLeft 250ms cubic-bezier(0.83, 0, 0.17, 1) both',
exitToRight: 'exitToRight 300ms 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', heightIn: 'heightIn 200ms ease both',
crawl: 'crawl 2s ease-in-out infinite', crawl: 'crawl 2s ease-in-out infinite',
@@ -474,18 +474,18 @@ const config: Config = {
}, },
enterFromRight: { enterFromRight: {
from: { opacity: '0', transform: 'translateX(50%)', display: 'none' }, 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: { enterFromLeft: {
from: { opacity: '0', transform: 'translateX(-50%)', display: 'none' }, 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: { 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' }, to: { opacity: '0', transform: 'translateX(50%)', display: 'none' },
}, },
exitToLeft: { 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' }, to: { opacity: '0', transform: 'translateX(-50%)', display: 'none' },
}, },
scaleIn: { scaleIn: {
@@ -576,11 +576,7 @@ const config: Config = {
* Variant when the Table of Content navigation is open. * Variant when the Table of Content navigation is open.
*/ */
addVariant('navigation-open', 'body.navigation-open &'); addVariant('navigation-open', 'body.navigation-open &');
addVariant('chat-open', 'body:has(.ai-chat[aria-expanded="true"]) &'); addVariant('chat-open', 'body:has(.ai-chat:not(.hidden)) &');
addVariant(
'sheet-open',
'html:has(.side-sheet[aria-modal="true"][aria-expanded="true"]) &, &:has(.side-sheet[aria-modal="true"][aria-expanded="true"])'
);
/** /**
* Variant when a header is displayed. * Variant when a header is displayed.
+3 -3
View File
@@ -17,11 +17,11 @@
"sideEffects": false, "sideEffects": false,
"version": "0.3.4", "version": "0.3.4",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/fontawesome-free": "^7.1.0" "@fortawesome/fontawesome-free": "^6.6.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@gitbook/fontawesome-pro": "1.0.16" "@gitbook/fontawesome-pro": "1.0.15"
}, },
"devDependencies": { "devDependencies": {
"bun-types": "catalog:", "bun-types": "catalog:",
+75 -355
View File
@@ -36,19 +36,9 @@ function OpenAPISchemaProperty(
context: OpenAPIClientContext; context: OpenAPIClientContext;
circularRefs: CircularRefsIds; circularRefs: CircularRefsIds;
className?: string; className?: string;
discriminator?: OpenAPIV3.DiscriminatorObject;
discriminatorValue?: string;
} & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'> } & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'>
) { ) {
const { const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props;
circularRefs: parentCircularRefs,
context,
className,
property,
discriminator,
discriminatorValue,
...rest
} = props;
const { schema } = property; const { schema } = property;
@@ -69,7 +59,7 @@ function OpenAPISchemaProperty(
const circularRefs = new Map(parentCircularRefs); const circularRefs = new Map(parentCircularRefs);
circularRefs.set(schema, id); circularRefs.set(schema, id);
const properties = getSchemaProperties(schema, discriminator, discriminatorValue); const properties = getSchemaProperties(schema);
const ancestors = new Set(circularRefs.keys()); const ancestors = new Set(circularRefs.keys());
const alternatives = getSchemaAlternatives(schema, ancestors); const alternatives = getSchemaAlternatives(schema, ancestors);
@@ -77,15 +67,26 @@ function OpenAPISchemaProperty(
const header = <OpenAPISchemaPresentation id={id} context={context} property={property} />; const header = <OpenAPISchemaPresentation id={id} context={context} property={property} />;
const content = (() => { const content = (() => {
if (alternatives?.schemas) { if (alternatives?.schemas) {
const { schemas, discriminator } = alternatives;
return ( return (
<OpenAPISchemaAlternatives <div className="openapi-schema-alternatives">
alternatives={alternatives} {schemas.map((alternativeSchema, index) => (
schema={schema} <div key={index} className="openapi-schema-alternative">
circularRefs={circularRefs} <OpenAPISchemaAlternative
context={context} schema={alternativeSchema}
parentDiscriminator={discriminator} discriminator={discriminator}
parentDiscriminatorValue={discriminatorValue} 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 id = useId();
const properties = getSchemaProperties(schema); const properties = getSchemaProperties(schema);
const description = resolveDescription(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) { if (properties?.length) {
const circularRefs = new Map(parentCircularRefs);
circularRefs.set(schema, id);
return ( return (
<> <>
{description ? ( {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. * Render a tab for an alternative schema.
* It renders directly the properties if relevant; * It renders directly the properties if relevant;
@@ -364,14 +236,11 @@ function OpenAPISchemaAlternatives(props: {
function OpenAPISchemaAlternative(props: { function OpenAPISchemaAlternative(props: {
schema: OpenAPIV3.SchemaObject; schema: OpenAPIV3.SchemaObject;
discriminator: OpenAPIV3.DiscriminatorObject | undefined; discriminator: OpenAPIV3.DiscriminatorObject | undefined;
discriminatorValue?: string;
circularRefs: CircularRefsIds; circularRefs: CircularRefsIds;
context: OpenAPIClientContext; context: OpenAPIClientContext;
}) { }) {
const { schema, discriminator, circularRefs, context } = props; const { schema, discriminator, circularRefs, context } = props;
const discriminatorValue = const properties = getSchemaProperties(schema, discriminator);
props.discriminatorValue || getDiscriminatorValue(schema, discriminator);
const properties = getSchemaProperties(schema, discriminator, discriminatorValue);
return properties?.length ? ( return properties?.length ? (
<OpenAPIDisclosure <OpenAPIDisclosure
@@ -388,8 +257,6 @@ function OpenAPISchemaAlternative(props: {
) : ( ) : (
<OpenAPISchemaProperty <OpenAPISchemaProperty
property={{ schema }} property={{ schema }}
discriminator={discriminator}
discriminatorValue={discriminatorValue}
circularRefs={circularRefs} circularRefs={circularRefs}
context={context} context={context}
/> />
@@ -515,7 +382,7 @@ export function OpenAPISchemaPresentation(props: {
<div id={id} className="openapi-schema-presentation"> <div id={id} className="openapi-schema-presentation">
<OpenAPISchemaName <OpenAPISchemaName
schema={schema} schema={schema}
type={getSchemaTitle(schema, { ignoreAlternatives: !propertyName })} type={getSchemaTitle(schema)}
propertyName={propertyName} propertyName={propertyName}
isDiscriminatorProperty={isDiscriminatorProperty} isDiscriminatorProperty={isDiscriminatorProperty}
required={required} 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. * Get the sub-properties of a schema.
*/ */
function getSchemaProperties( function getSchemaProperties(
schema: OpenAPIV3.SchemaObject, schema: OpenAPIV3.SchemaObject,
discriminator?: OpenAPIV3.DiscriminatorObject | undefined, discriminator?: OpenAPIV3.DiscriminatorObject | undefined
discriminatorValue?: string | undefined
): null | OpenAPISchemaPropertyEntry[] { ): null | OpenAPISchemaPropertyEntry[] {
// check array AND schema.items as this is sometimes null despite what the type indicates // check array AND schema.items as this is sometimes null despite what the type indicates
if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) { if (schema.type === 'array' && schema.items && !checkIsReference(schema.items)) {
const items = schema.items; const items = schema.items;
const itemProperties = getSchemaProperties(items, discriminator, discriminatorValue); const itemProperties = getSchemaProperties(items);
if (itemProperties) { if (itemProperties) {
return itemProperties.map((prop) => ({ return itemProperties.map((prop) => ({
...prop, ...prop,
@@ -665,55 +462,34 @@ function getSchemaProperties(
return [{ propertyName: 'items', schema: items }]; 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) { 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; return null;
@@ -912,90 +688,34 @@ function flattenAlternatives(
): OpenAPIV3.SchemaObject[] { ): OpenAPIV3.SchemaObject[] {
// Get the parent schema's required fields from the most recent ancestor // Get the parent schema's required fields from the most recent ancestor
const latestAncestor = Array.from(ancestors).pop(); 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)) { if (checkIsReference(schemaOrRef)) {
continue; return acc;
} }
const flattened = flattenSchema(schemaOrRef, alternativeType, ancestors, latestAncestor); if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) {
const alternatives = getSchemaAlternatives(schemaOrRef, ancestors);
if (flattened) { if (alternatives?.schemas) {
result.push(...flattened); acc.push(
} ...alternatives.schemas.map((schema) => ({
} ...schema,
required: mergeRequiredFields(schema, latestAncestor),
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;
});
}
} }
return acc;
} }
}
const required = mergeRequiredFields(schema, latestAncestor); // For direct schemas, handle required fields
const schema = {
...schemaOrRef,
required: mergeRequiredFields(schemaOrRef, latestAncestor),
};
return [ acc.push(schema);
{ return acc;
...schema, }, []);
...(required ? { required } : {}),
},
];
} }
/** /**
+125 -24
View File
@@ -37,30 +37,31 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
label: 'HTTP', label: 'HTTP',
syntax: 'http', syntax: 'http',
generate: ({ method, url: { origin, path }, headers = {}, body }: CodeSampleInput) => { generate: ({ method, url: { origin, path }, headers = {}, body }: CodeSampleInput) => {
// Process URL and headers to use consistent placeholder format
const processedPath = convertPathParametersToPlaceholders(path);
const processedHeaders = processHeadersWithPlaceholders(headers);
if (body) { if (body) {
// if we had a body add a content length header // if we had a body add a content length header
const bodyContent = body ? stringifyOpenAPI(body) : ''; const bodyContent = body ? stringifyOpenAPI(body) : '';
// handle unicode chars with a text encoder // handle unicode chars with a text encoder
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const bodyString = BodyGenerators.getHTTPBody(body, headers); const bodyString = BodyGenerators.getHTTPBody(body, processedHeaders);
if (bodyString) { if (bodyString) {
body = bodyString; body = bodyString;
} }
headers = { processedHeaders['Content-Length'] = encoder.encode(bodyContent).length.toString();
...headers,
'Content-Length': encoder.encode(bodyContent).length.toString(),
};
} }
if (!headers.hasOwnProperty('Accept')) { if (!processedHeaders.hasOwnProperty('Accept')) {
headers.Accept = '*/*'; processedHeaders.Accept = '*/*';
} }
const headerString = headers const headerString = processedHeaders
? `${Object.entries(headers) ? `${Object.entries(processedHeaders)
.map(([key, value]) => .map(([key, value]) =>
key.toLowerCase() !== 'host' ? `${key}: ${value}` : '' key.toLowerCase() !== 'host' ? `${key}: ${value}` : ''
) )
@@ -69,8 +70,8 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
const bodyString = body ? `\n${body}` : ''; const bodyString = body ? `\n${body}` : '';
const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1 const httpRequest = `${method.toUpperCase()} ${decodeURI(processedPath)} HTTP/1.1
Host: ${origin.replaceAll(/https*:\/\//g, '')} Host: ${origin.replace(/https*:\/\//g, '')}
${headerString}${bodyString}`; ${headerString}${bodyString}`;
return httpRequest; return httpRequest;
@@ -87,15 +88,23 @@ ${headerString}${bodyString}`;
lines.push(`--request ${method.toUpperCase()}`); lines.push(`--request ${method.toUpperCase()}`);
} }
lines.push(`--url '${origin}${path}'`); // Process URL and headers to use consistent placeholder format
const processedUrl = convertPathParametersToPlaceholders(origin + path);
const processedHeaders = processHeadersWithPlaceholders(headers);
lines.push(`--url '${processedUrl}'`);
if (body) { if (body) {
const bodyContent = BodyGenerators.getCurlBody(body, headers); const bodyContent = BodyGenerators.getCurlBody(body, processedHeaders);
if (bodyContent) { if (bodyContent) {
body = bodyContent.body; body = bodyContent.body;
headers = bodyContent.headers; headers = bodyContent.headers;
} else {
headers = processedHeaders;
} }
} else {
headers = processedHeaders;
} }
if (headers && Object.keys(headers).length > 0) { if (headers && Object.keys(headers).length > 0) {
@@ -122,18 +131,26 @@ ${headerString}${bodyString}`;
generate: ({ method, url: { origin, path }, headers, body }) => { generate: ({ method, url: { origin, path }, headers, body }) => {
let code = ''; let code = '';
// Process URL and headers to use consistent placeholder format
const processedUrl = convertPathParametersToPlaceholders(origin + path);
const processedHeaders = processHeadersWithPlaceholders(headers);
if (body) { if (body) {
const lines = BodyGenerators.getJavaScriptBody(body, headers); const lines = BodyGenerators.getJavaScriptBody(body, processedHeaders);
if (lines) { if (lines) {
// add the generated code to the top // add the generated code to the top
code += lines.code; code += lines.code;
body = lines.body; body = lines.body;
headers = lines.headers; headers = lines.headers;
} else {
headers = processedHeaders;
} }
} else {
headers = processedHeaders;
} }
code += `const response = await fetch('${origin}${path}', { code += `const response = await fetch('${processedUrl}', {
method: '${method.toUpperCase()}',\n`; method: '${method.toUpperCase()}',\n`;
if (headers && Object.keys(headers).length > 0) { if (headers && Object.keys(headers).length > 0) {
@@ -156,7 +173,19 @@ ${headerString}${bodyString}`;
syntax: 'python', syntax: 'python',
generate: ({ method, url: { origin, path }, headers, body }) => { generate: ({ method, url: { origin, path }, headers, body }) => {
const contentType = headers?.['Content-Type']; const contentType = headers?.['Content-Type'];
let code = `${isJSON(contentType) ? 'import json\n' : ''}import requests\n\n`; const needsJsonImport = body && isJSON(contentType) && typeof body === 'string';
let code = '';
// Import statements
if (needsJsonImport) {
code += 'import json\n';
}
code += 'import requests\n\n';
// Process headers and URL to use consistent placeholder format
const processedUrl = convertPathParametersToPlaceholders(origin + path);
const processedHeaders = processHeadersWithPlaceholders(headers);
if (body) { if (body) {
const lines = BodyGenerators.getPythonBody(body, headers); const lines = BodyGenerators.getPythonBody(body, headers);
@@ -170,16 +199,25 @@ ${headerString}${bodyString}`;
} }
code += `response = requests.${method.toLowerCase()}(\n`; code += `response = requests.${method.toLowerCase()}(\n`;
code += indent(`"${origin}${path}",\n`, 4); code += indent(`"${processedUrl}",\n`, 4);
if (headers && Object.keys(headers).length > 0) { if (processedHeaders && Object.keys(processedHeaders).length > 0) {
code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4); code += indent(`headers={\n`, 4);
Object.entries(processedHeaders).forEach(([key, value], index, array) => {
const isLast = index === array.length - 1;
code += indent(`"${key}": "${value}"${isLast ? '' : ','}\n`, 8);
});
code += indent(`},\n`, 4);
} }
if (body) { if (body) {
if (body === 'files') { if (body === 'files') {
code += indent(`files=${body}\n`, 4); code += indent(`files=${body}\n`, 4);
} else if (isJSON(contentType)) { } else if (isJSON(contentType) && isPlainObject(body)) {
// Use json parameter for dict objects
code += indent(`json=${body}\n`, 4);
} else if (isJSON(contentType) && needsJsonImport) {
// Use data=json.dumps() for JSON strings
code += indent(`data=json.dumps(${body})\n`, 4); code += indent(`data=json.dumps(${body})\n`, 4);
} else { } else {
code += indent(`data=${body}\n`, 4); code += indent(`data=${body}\n`, 4);
@@ -372,7 +410,8 @@ const BodyGenerators = {
} else if (isYAML(contentType)) { } else if (isYAML(contentType)) {
code += `yamlBody = \"\"\"\n${indent(yaml.dump(body), 4)}\"\"\"\n\n`; code += `yamlBody = \"\"\"\n${indent(yaml.dump(body), 4)}\"\"\"\n\n`;
body = 'yamlBody'; body = 'yamlBody';
} else { } else if (isJSON(contentType) && isPlainObject(body)) {
// For dict objects, return as-is to use with json= parameter
body = stringifyOpenAPI( body = stringifyOpenAPI(
body, body,
(_key, value) => { (_key, value) => {
@@ -389,9 +428,30 @@ const BodyGenerators = {
}, },
2 2
) )
.replaceAll('"$$__TRUE__$$"', 'True') .replace(/"\\$\\$__TRUE__\\$\\$"/g, 'True')
.replaceAll('"$$__FALSE__$$"', 'False') .replace(/"\\$\\$__FALSE__\\$\\$"/g, 'False')
.replaceAll('"$$__NULL__$$"', 'None'); .replace(/"\\$\\$__NULL__\\$\\$"/g, 'None');
} else {
// For everything else (including JSON strings)
body = stringifyOpenAPI(
body,
(_key, value) => {
switch (value) {
case true:
return '$$__TRUE__$$';
case false:
return '$$__FALSE__$$';
case null:
return '$$__NULL__$$';
default:
return value;
}
},
2
)
.replace(/"\\$\\$__TRUE__\\$\\$"/g, 'True')
.replace(/"\\$\\$__FALSE__\\$\\$"/g, 'False')
.replace(/"\\$\\$__NULL__\\$\\$"/g, 'None');
} }
return { body, code, headers }; return { body, code, headers };
@@ -487,3 +547,44 @@ function buildHeredoc(lines: string[]): string {
} }
return result; return result;
} }
/**
* Converts path parameters from {paramName} to YOUR_PARAM_NAME format
*/
function convertPathParametersToPlaceholders(urlPath: string): string {
return urlPath.replace(/\{([^}]+)\}/g, (match, paramName) => {
// Convert camelCase to UPPER_SNAKE_CASE
const placeholder = paramName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
return `YOUR_${placeholder}`;
});
}
/**
* Processes headers to use consistent placeholder format
*/
function processHeadersWithPlaceholders(headers?: Record<string, string>): Record<string, string> {
if (!headers) {
return {};
}
const processedHeaders: Record<string, string> = {};
Object.entries(headers).forEach(([key, value]) => {
if (key === 'Authorization' && value.includes('Bearer')) {
processedHeaders[key] = 'Bearer YOUR_API_TOKEN';
} else if (key === 'Authorization' && value.includes('Basic')) {
processedHeaders[key] = 'Basic YOUR_API_TOKEN';
} else if (value.includes('YOUR_') || value.includes('TOKEN')) {
// Already in correct format or generic token
processedHeaders[key] = value.replace(
/YOUR_SECRET_TOKEN|YOUR_TOKEN/g,
'YOUR_API_TOKEN'
);
} else {
// Regular headers - keep as-is
processedHeaders[key] = value;
}
});
return processedHeaders;
}
@@ -34,6 +34,6 @@ export function isFormData(contentType?: string): boolean {
return !!contentType && contentType.toLowerCase().includes('multipart/form-data'); 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); return typeof value === 'object' && value !== null && !Array.isArray(value);
} }

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