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