mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75b52eac28 | |||
| ad4d07dc06 | |||
| 75086740f8 | |||
| eea8f1e00f | |||
| 4f9abfb6f2 | |||
| 252afceab0 | |||
| bef191660a | |||
| 145625142d | |||
| f70a2aa310 | |||
| 56e46ce8ab | |||
| 754cc11e0b | |||
| 3551d147e9 | |||
| 162cfe4163 | |||
| 6815dd197e | |||
| d74f8a7098 | |||
| 1e53f46521 | |||
| 1a6216df4f | |||
| a629900324 | |||
| a4c33997fe | |||
| badf3a92f6 | |||
| e0fb9ac3ca | |||
| f3e40410c4 | |||
| 8e99871004 | |||
| 1e4e54e752 | |||
| 50c1be3447 | |||
| 319a1e536c | |||
| 50cb0c6e15 | |||
| ceb32b1a2e | |||
| f8c54b4d34 | |||
| c06ad70f8a | |||
| 0ef5dc8538 | |||
| 28adc9b5f1 | |||
| 912d1693f5 | |||
| c23d4ef331 | |||
| c21693fe50 | |||
| c06b3dd60d | |||
| f173e014db | |||
| 1b458c436e | |||
| b42b0b3910 | |||
| 2fc21272ff | |||
| 379d486488 | |||
| cba583a31c | |||
| 7a504b7cc5 | |||
| 8ec5fd0986 | |||
| 529f940a45 | |||
| 8a3c15911f | |||
| d7948e34b2 | |||
| 9f42211993 | |||
| 65e62307bf | |||
| 469b332da2 | |||
| e434442f1e | |||
| 9344431bfc | |||
| 622ecbec1d | |||
| fa1382b289 | |||
| b8388e5a2a | |||
| 2bce12798f | |||
| f89e889d18 | |||
| 2b8a2d24b6 | |||
| b85eccdff5 | |||
| d924e655c0 | |||
| b932e4e2e9 | |||
| ca4a3e8c9a | |||
| b498521196 | |||
| 8a8cf2025d | |||
| ed7d47d9b8 | |||
| b5494cc04b |
@@ -5,7 +5,7 @@
|
||||
"name": "gitbook",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"turbo": "^2.5.0",
|
||||
"vercel": "^39.3.0",
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"packages/embed": {
|
||||
"name": "@gitbook/embed",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:",
|
||||
@@ -49,6 +49,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"react": "^19.0.0",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -64,7 +65,7 @@
|
||||
},
|
||||
"packages/expr": {
|
||||
"name": "@gitbook/expr",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-loose": "8.4.0",
|
||||
@@ -79,7 +80,7 @@
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"bun-types": "^1.1.20",
|
||||
"tsdown": "^0.15.0",
|
||||
"tsdown": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/fonts": {
|
||||
@@ -95,13 +96,14 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.17.1",
|
||||
"version": "0.19.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
@@ -109,7 +111,7 @@
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"@opennextjs/cloudflare": "^1.7.1",
|
||||
"@opennextjs/cloudflare": "^1.8.3",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
@@ -142,8 +144,9 @@
|
||||
"memoizee": "^0.4.17",
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.12",
|
||||
"next": "15.4.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
"object-identity": "^0.1.2",
|
||||
@@ -197,7 +200,7 @@
|
||||
},
|
||||
"packages/icons": {
|
||||
"name": "@gitbook/icons",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"bin": {
|
||||
"gitbook-icons": "./bin/gitbook-icons.js",
|
||||
},
|
||||
@@ -233,7 +236,7 @@
|
||||
},
|
||||
"packages/react-contentkit": {
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.5",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
@@ -265,7 +268,7 @@
|
||||
},
|
||||
"packages/react-openapi": {
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.0",
|
||||
"dependencies": {
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
@@ -293,7 +296,6 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
@@ -302,8 +304,9 @@
|
||||
"react-dom": "^19.0.0",
|
||||
},
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.141.0",
|
||||
"@gitbook/api": "0.143.2",
|
||||
"bidc": "^0.0.2",
|
||||
"tsdown": "^0.15.6",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
|
||||
@@ -482,39 +485,39 @@
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
|
||||
|
||||
"@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.8", "", { "dependencies": { "@changesets/config": "^3.0.5", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.2", "@changesets/should-skip-package": "^0.1.1", "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-qjMUj4DYQ1Z6qHawsn7S71SujrExJ+nceyKKyI9iB+M5p9lCL55afuEd6uLBPRpLGWQwkwvWegDHtwHJb1UjpA=="],
|
||||
"@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.13", "", { "dependencies": { "@changesets/config": "^3.1.1", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg=="],
|
||||
|
||||
"@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.5", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.2", "@changesets/should-skip-package": "^0.1.1", "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-IgvBWLNKZd6k4t72MBTBK3nkygi0j3t3zdC1zrfusYo0KpdsvnDjrMM9vPnTCLCMlfNs55jRL4gIMybxa64FCQ=="],
|
||||
"@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="],
|
||||
|
||||
"@changesets/changelog-git": ["@changesets/changelog-git@0.2.0", "", { "dependencies": { "@changesets/types": "^6.0.0" } }, "sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ=="],
|
||||
"@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="],
|
||||
|
||||
"@changesets/cli": ["@changesets/cli@2.27.12", "", { "dependencies": { "@changesets/apply-release-plan": "^7.0.8", "@changesets/assemble-release-plan": "^6.0.5", "@changesets/changelog-git": "^0.2.0", "@changesets/config": "^3.0.5", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.2", "@changesets/get-release-plan": "^4.0.6", "@changesets/git": "^3.0.2", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.1", "@changesets/read": "^0.6.2", "@changesets/should-skip-package": "^0.1.1", "@changesets/types": "^6.0.0", "@changesets/write": "^0.3.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "ci-info": "^3.7.0", "enquirer": "^2.4.1", "external-editor": "^3.1.0", "fs-extra": "^7.0.1", "mri": "^1.2.0", "p-limit": "^2.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-9o3fOfHYOvBnyEn0mcahB7wzaA3P4bGJf8PNqGit5PKaMEFdsRixik+txkrJWd2VX+O6wRFXpxQL8j/1ANKE9g=="],
|
||||
"@changesets/cli": ["@changesets/cli@2.29.7", "", { "dependencies": { "@changesets/apply-release-plan": "^7.0.13", "@changesets/assemble-release-plan": "^6.0.9", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.1", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/get-release-plan": "^4.0.13", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.5", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.0", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "ci-info": "^3.7.0", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "p-limit": "^2.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ=="],
|
||||
|
||||
"@changesets/config": ["@changesets/config@3.0.5", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.2", "@changesets/logger": "^0.1.1", "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-QyXLSSd10GquX7hY0Mt4yQFMEeqnO5z/XLpbIr4PAkNNoQNKwDyiSrx4yd749WddusH1v3OSiA0NRAYmH/APpQ=="],
|
||||
"@changesets/config": ["@changesets/config@3.1.1", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/logger": "^0.1.1", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA=="],
|
||||
|
||||
"@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="],
|
||||
|
||||
"@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.2", "", { "dependencies": { "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-sgcHRkiBY9i4zWYBwlVyAjEM9sAzs4wYVwJUdnbDLnVG3QwAaia1Mk5P8M7kraTOZN+vBET7n8KyB0YXCbFRLQ=="],
|
||||
"@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="],
|
||||
|
||||
"@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.6", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.5", "@changesets/config": "^3.0.5", "@changesets/pre": "^2.0.1", "@changesets/read": "^0.6.2", "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-FHRwBkY7Eili04Y5YMOZb0ezQzKikTka4wL753vfUA5COSebt7KThqiuCN9BewE4/qFGgF/5t3AuzXx1/UAY4w=="],
|
||||
"@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.13", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", "@changesets/config": "^3.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.5", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg=="],
|
||||
|
||||
"@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="],
|
||||
|
||||
"@changesets/git": ["@changesets/git@3.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ=="],
|
||||
"@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="],
|
||||
|
||||
"@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="],
|
||||
|
||||
"@changesets/parse": ["@changesets/parse@0.4.0", "", { "dependencies": { "@changesets/types": "^6.0.0", "js-yaml": "^3.13.1" } }, "sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw=="],
|
||||
"@changesets/parse": ["@changesets/parse@0.4.1", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^3.13.1" } }, "sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q=="],
|
||||
|
||||
"@changesets/pre": ["@changesets/pre@2.0.1", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-vvBJ/If4jKM4tPz9JdY2kGOgWmCowUYOi5Ycv8dyLnEE8FgpYYUo1mgJZxcdtGGP3aG8rAQulGLyyXGSLkIMTQ=="],
|
||||
"@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="],
|
||||
|
||||
"@changesets/read": ["@changesets/read@0.6.2", "", { "dependencies": { "@changesets/git": "^3.0.2", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.0", "@changesets/types": "^6.0.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-wjfQpJvryY3zD61p8jR87mJdyx2FIhEcdXhKUqkja87toMrP/3jtg/Yg29upN+N4Ckf525/uvV7a4tzBlpk6gg=="],
|
||||
"@changesets/read": ["@changesets/read@0.6.5", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.1", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg=="],
|
||||
|
||||
"@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.1", "", { "dependencies": { "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-H9LjLbF6mMHLtJIc/eHR9Na+MifJ3VxtgP/Y+XLn4BF7tDTEN1HNYtH6QMcjP1uxp9sjaFYmW8xqloaCi/ckTg=="],
|
||||
"@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="],
|
||||
|
||||
"@changesets/types": ["@changesets/types@6.0.0", "", {}, "sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ=="],
|
||||
"@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="],
|
||||
|
||||
"@changesets/write": ["@changesets/write@0.3.2", "", { "dependencies": { "@changesets/types": "^6.0.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", "prettier": "^2.7.1" } }, "sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw=="],
|
||||
"@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
|
||||
|
||||
@@ -674,7 +677,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.141.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-KViAAUUStITbNGqDydUjZhxGdG4lJrS3ll8eQI5Tvs0j+Wvb4x1lxGvnm/LgtrhW+FoEgrFuRKZWVga1bI1Qmg=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.143.2", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-ZIQwIgX+artl0o20ftnzeJyM9icwQ6pFeM55AG/lEdeFbMuUGxTfSnZ90+8Aou8l/3ELR3/tOkU2SAvyKc+ATw=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
@@ -760,6 +763,8 @@
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="],
|
||||
|
||||
"@inquirer/external-editor": ["@inquirer/external-editor@1.0.2", "", { "dependencies": { "chardet": "^2.1.0", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ=="],
|
||||
|
||||
"@internationalized/date": ["@internationalized/date@3.7.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ=="],
|
||||
|
||||
"@internationalized/message": ["@internationalized/message@3.1.6", "", { "dependencies": { "@swc/helpers": "^0.5.0", "intl-messageformat": "^10.1.0" } }, "sha512-JxbK3iAcTIeNr1p0WIFg/wQJjIzJt9l/2KNY/48vXV7GRGZSv3zMxJsce008fZclk2cDC8y0Ig3odceHO7EfNQ=="],
|
||||
@@ -780,7 +785,7 @@
|
||||
|
||||
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
@@ -812,7 +817,7 @@
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.18.0", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-JvKyB6YwS3quM+88JPR0axeRgvdDu3Pv6mdZUy+w4qVkCzGgumb9bXG/TmtDRQv+671yaofVfXSQmFLlWU5qPQ=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.4", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.0" } }, "sha512-+ZEtJPp8EF8h4kN6rLQECRor00H7jtDgBVtttIUoxuDkXLiQMaSBqju3LV/IEsMvqVG5pviUvR4jYhIA1xNm8w=="],
|
||||
"@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.0-canary.57", "", {}, "sha512-gDNgJA7YnuUWQ2Ei6oeMiuSSWWICyYGK0AbWQnOfub1LIyYPNXMSyvyLTkGwVyIixbLMUFdzjTgU+ZxYHil7Mg=="],
|
||||
|
||||
@@ -866,15 +871,13 @@
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@opennextjs/aws": ["@opennextjs/aws@3.7.6", "", { "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-l4UGkmaZaAjWER+PuZ/zNziMnrbf6oA9B1RUDKcyKsX+hEES1b7h6kLgpo4a4maf01M+k0yJUZQyF4sf+vI+Iw=="],
|
||||
"@opennextjs/aws": ["@opennextjs/aws@3.7.7", "", { "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-rkBpNfBsq3XvbBh+VcNI9dvs0UkuuJH5I7SvVdPx12LudiErShL/9RoA7iF9tpkfRizcwGRIhsedrHKmcs9kqw=="],
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.7.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.7.6", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "wrangler": "^4.24.4" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-g3d8LSpGGJzuCLuLYHAs7nGWFKK57qSOgxhpn3wwk6t/tXxsneXA3D5nbf6zLsAQQo8b6A8LDcT732U3rUXUeA=="],
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.8.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.7.7", "cloudflare": "^4.4.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "wrangler": "^4.24.4" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-ftyZwi2Mh6gtUE2HuHfYfQ/GsJ0vkS5KrPmFEd3beBSCvDRUURDR0o3qSdF3DqS5WJIEloWwgkjxyFed3Mp12w=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.87.0", "", {}, "sha512-ky2Hqi2q/uGX36UfY79zxMbUqiNIl1RyKKVJfFenG70lbn+/fcaKBVTbhmUwn8a2wPyv2gNtDQxuDytbKX9giQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.87.0", "", {}, "sha512-ipZFWVGE9fADBVXXWJWY/cxpysc41Gt5upKDeb32F6WMgFyO7XETUMVq8UuREKCih+Km5E6p2VhEvf6Fuhey6g=="],
|
||||
"@oxc-project/types": ["@oxc-project/types@0.94.0", "", {}, "sha512-+UgQT/4o59cZfH6Cp7G0hwmqEQ0wE+AdIwhikdwnhWI9Dp8CgSY081+Q3O67/wq3VJu8mgUEB93J9EHHn70fOw=="],
|
||||
|
||||
"@phosphor-icons/core": ["@phosphor-icons/core@2.1.1", "", {}, "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ=="],
|
||||
|
||||
@@ -1176,35 +1179,35 @@
|
||||
|
||||
"@replit/codemirror-css-color-picker": ["@replit/codemirror-css-color-picker@6.3.0", "", { "peerDependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-19biDANghUm7Fz7L1SNMIhK48tagaWuCOHj4oPPxc7hxPGkTVY2lU/jVZ8tsbTKQPVG7BO2CBDzs7CBwb20t4A=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.37", "", { "os": "android", "cpu": "arm64" }, "sha512-Pdr3USGBdoYzcygfJTSATHd7x476vVF3rnQ6SuUAh4YjhgGoNaI/ZycQ0RsonptwwU5NmQRWxfWv+aUPL6JlJg=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.42", "", { "os": "android", "cpu": "arm64" }, "sha512-W5ZKF3TP3bOWuBfotAGp+UGjxOkGV7jRmIRbBA7NFjggx7Oi6vOmGDqpHEIX7kDCiry1cnIsWQaxNvWbMdkvzQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.37", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iDdmatSgbWhTYOq51G2CkJXwFayiuQpv/ywG7Bv3wKqy31L7d0LltUhWqAdfCl7eBG3gybfUm/iEXiTldH3jYA=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.42", "", { "os": "darwin", "cpu": "arm64" }, "sha512-abw/wtgJA8OCgaTlL+xJxnN/Z01BwV1rfzIp5Hh9x+IIO6xOBfPsQ0nzi0+rWx3TyZ9FZXyC7bbC+5NpQ9EaXQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.37", "", { "os": "darwin", "cpu": "x64" }, "sha512-LQPpi3YJDtIprj6mwMbVM1gLM4BV2m9oqe9h3Y1UwAd20xs+imnzWJqWFpm4Hw9SiFmefIf3q4EPx2k6Nj2K7A=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.42", "", { "os": "darwin", "cpu": "x64" }, "sha512-Y/UrZIRVr8CvXVEB88t6PeC46r1K9/QdPEo2ASE/b/KBEyXIx+QbM6kv9QfQVWU2Atly2+SVsQzxQsIvuk3lZQ=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.37", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9JnfSWfYd/YrZOu4Sj3rb2THBrCj70nJB/2FOSdg0O9ZoRrdTeB8b7Futo6N7HLWZM5uqqnJBX6VTpA0RZD+ow=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.42", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zRM0oOk7BZiy6DoWBvdV4hyEg+j6+WcBZIMHVirMEZRu8hd18kZdJkg+bjVMfCEhwpWeFUfBfZ1qcaZ5UdYzlQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.37", "", { "os": "linux", "cpu": "arm" }, "sha512-eEmQTpvefEtHxc0vg5sOnWCqBcGQB/SIDlPkkzKR9ESKq9BsjQfHxssJWuNMyQ+rpr9CYaogddyQtZ9GHkp8vA=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.42", "", { "os": "linux", "cpu": "arm" }, "sha512-6RjFaC52QNwo7ilU8C5H7swbGlgfTkG9pudXwzr3VYyT18s0C9gLg3mvc7OMPIGqNxnQ0M5lU8j6aQCk2DTRVg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.37", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ekv4OjDzQUl0X9kHM7M23N9hVRiYCYr89neLBNITCp7P4IHs1f6SNZiCIvvBVy6NIFzO1w9LZJGEeJYK5cQBVQ=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.42", "", { "os": "linux", "cpu": "arm64" }, "sha512-LMYHM5Sf6ROq+VUwHMDVX2IAuEsWTv4SnlFEedBnMGpvRuQ14lCmD4m5Q8sjyAQCgyha9oghdGoK8AEg1sXZKg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.37", "", { "os": "linux", "cpu": "arm64" }, "sha512-z8Aa5Kar5mhh0RVZEL+zKJwNz1cgcDISmwUMcTk0w986T8JZJOJCfJ/u9e8pqUTIJjxdM8SZq9/24nMgMlx5ng=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.42", "", { "os": "linux", "cpu": "arm64" }, "sha512-/bNTYb9aKNhzdbPn3O4MK2aLv55AlrkUKPE4KNfBYjkoZUfDr4jWp7gsSlvTc5A/99V1RCm9axvt616ZzeXGyA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.37", "", { "os": "linux", "cpu": "x64" }, "sha512-e+fNseKhfE/socjOw6VrQcXrbNKfi2V/KZ+ssuLnmeaYNGuJWqPhvML56oYhGb3IgROEEc61lzr3Riy5BIqoMA=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.42", "", { "os": "linux", "cpu": "x64" }, "sha512-n/SLa4h342oyeGykZdch7Y3GNCNliRPL4k5wkeZ/5eQZs+c6/ZG1SHCJQoy7bZcmxiMyaXs9HoFmv1PEKrZgWg=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.37", "", { "os": "linux", "cpu": "x64" }, "sha512-dPZfB396PMIasd19X0ikpdCvjK/7SaJFO8y5/TxnozJEy70vOf4GESe/oKcsJPav/MSTWBYsHjJSO6vX0oAW8g=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.42", "", { "os": "linux", "cpu": "x64" }, "sha512-4PSd46sFzqpLHSGdaSViAb1mk55sCUMpJg+X8ittXaVocQsV3QLG/uydSH8RyL0ngHX5fy3D70LcCzlB15AgHw=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.37", "", { "os": "none", "cpu": "arm64" }, "sha512-rFjLXoHpRqxJqkSBXHuyt6bhyiIFnvLD9X2iPmCYlfpEkdTbrY1AXg4ZbF8UMO5LM7DAAZm/7vPYPO1TKTA7Sg=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.42", "", { "os": "none", "cpu": "arm64" }, "sha512-BmWoeJJyeZXmZBcfoxG6J9+rl2G7eO47qdTkAzEegj4n3aC6CBIHOuDcbE8BvhZaEjQR0nh0nJrtEDlt65Q7Sw=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.37", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.3" }, "cpu": "none" }, "sha512-oQAe3lMaBGX6q0GSic0l3Obmd6/rX8R6eHLnRC8kyy/CvPLiCMV82MPGT8fxpPTo/ULFGrupSu2nV1zmOFBt/w=="],
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.42", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.6" }, "cpu": "none" }, "sha512-2Ft32F7uiDTrGZUKws6CLNTlvTWHC33l4vpXrzUucf9rYtUThAdPCOt89Pmn13tNX6AulxjGEP2R0nZjTSW3eQ=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.37", "", { "os": "win32", "cpu": "arm64" }, "sha512-ucO6CiZhpkNRiVAk7ybvA9pZaMreCtfHej3BtJcBL5S3aYmp4h0g6TvaXLD5YRJx5sXobp/9A//xU4wPMul3Bg=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.42", "", { "os": "win32", "cpu": "arm64" }, "sha512-hC1kShXW/z221eG+WzQMN06KepvPbMBknF0iGR3VMYJLOe9gwnSTfGxFT5hf8XrPv7CEZqTWRd0GQpkSHRbGsw=="],
|
||||
|
||||
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.37", "", { "os": "win32", "cpu": "ia32" }, "sha512-Ya9DBWJe1EGHwil7ielI8CdE0ELCg6KyDvDQqIFllnTJEYJ1Rb74DK6mvlZo273qz6Mw8WrMm26urfDeZhCc3Q=="],
|
||||
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.42", "", { "os": "win32", "cpu": "ia32" }, "sha512-AICBYromawouGjj+GS33369E8Vwhy6UwhQEhQ5evfS8jPCsyVvoICJatbDGDGH01dwtVGLD5eDFzPicUOVpe4g=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.37", "", { "os": "win32", "cpu": "x64" }, "sha512-r+RI+wMReoTIF/uXqQWJcD8xGWXzCzUyGdpLmQ8FC+MCyPHlkjEsFRv8OFIYI6HhiGAmbfWVYEGf+aeLJzkHGw=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.42", "", { "os": "win32", "cpu": "x64" }, "sha512-XpZ0M+tjoEiSc9c+uZR7FCnOI0uxDRNs1elGOMjeB0pUP1QmvVbZGYNsyLbLoP4u7e3VQN8rie1OQ8/mB6rcJg=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.37", "", {}, "sha512-0taU1HpxFzrukvWIhLRI4YssJX2wOW5q1MxPXWztltsQ13TE51/larZIwhFdpyk7+K43TH7x6GJ8oEqAo+vDbA=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.42", "", {}, "sha512-N7pQzk9CyE7q0bBN/q0J8s6Db279r5kUZc6d7/wWRe9/zXqC52HQovVyu6iXPIDY4BEzzgbVLhVFXrOuGJ22ZQ=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
|
||||
|
||||
@@ -1428,7 +1431,7 @@
|
||||
|
||||
"@tusbar/cache-control": ["@tusbar/cache-control@1.0.2", "", {}, "sha512-PXfjYTYBVvMPYCLDWj+xIOA9ITFbbhWCHzLcqUCJ5TPGm4JO4cxpGb7x3K8Q1K1ADgNgfBxLsDcTMVRydtZB9A=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
@@ -1488,7 +1491,7 @@
|
||||
|
||||
"@unhead/schema": ["@unhead/schema@1.11.14", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-V9W9u5tF1/+TiLqxu+Qvh1ShoMDkPEwHoEo4DKdDG6ko7YlbzFfDxV6el9JwCren45U/4Vy/4Xi7j8OH02wsiA=="],
|
||||
|
||||
"@vercel/build-utils": ["@vercel/build-utils@9.0.1", "", {}, "sha512-pG/izEqA0AGyqQj6QBfGoTOKU9FPG18sYw9qpncEq00uA+J4Ly4e8ssNbENsXtnXqkMjeoS3c5ncR3jT0bOyiA=="],
|
||||
"@vercel/build-utils": ["@vercel/build-utils@9.1.0", "", {}, "sha512-ccknvdKH6LDB9ZzZaX8a8cOvFbI441APLHvKrunJE/wezY0skmfuEUK1qnfPApXMs4FMWzZQj2LO9qpzfgBPsQ=="],
|
||||
|
||||
"@vercel/error-utils": ["@vercel/error-utils@2.0.3", "", {}, "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ=="],
|
||||
|
||||
@@ -1496,29 +1499,29 @@
|
||||
|
||||
"@vercel/gatsby-plugin-vercel-analytics": ["@vercel/gatsby-plugin-vercel-analytics@1.0.11", "", { "dependencies": { "web-vitals": "0.2.4" } }, "sha512-iTEA0vY6RBPuEzkwUTVzSHDATo1aF6bdLLspI68mQ/BTbi5UQEGjpjyzdKOVcSYApDtFU6M6vypZ1t4vIEnHvw=="],
|
||||
|
||||
"@vercel/gatsby-plugin-vercel-builder": ["@vercel/gatsby-plugin-vercel-builder@2.0.63", "", { "dependencies": { "@sinclair/typebox": "0.25.24", "@vercel/build-utils": "9.0.1", "@vercel/routing-utils": "5.0.0", "esbuild": "0.14.47", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "sha512-xZmZ6XOBycOR+Peq/WFBc8UkdrvSPXseCPhPLBK2MTz6y/EdD4KbpnHckYo0H85JUMsJLlAspAbRNKnJeVkvsQ=="],
|
||||
"@vercel/gatsby-plugin-vercel-builder": ["@vercel/gatsby-plugin-vercel-builder@2.0.65", "", { "dependencies": { "@sinclair/typebox": "0.25.24", "@vercel/build-utils": "9.1.0", "@vercel/routing-utils": "5.0.1", "esbuild": "0.14.47", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "sha512-MQX56fuL4WHDhT/fvKy9FMJigOymTAcCqw8rteF1wpRBAGhapSJkhT34I4mkfRRMFk1kIV7ijwuX+w1mpRrLjA=="],
|
||||
|
||||
"@vercel/go": ["@vercel/go@3.2.1", "", {}, "sha512-ezjmuUvLigH9V4egEaX0SZ+phILx8lb+Zkp1iTqKI+yl/ibPAtVo5o+dLSRAXU9U01LBmaLu3O8Oxd/JpWYCOw=="],
|
||||
|
||||
"@vercel/hydrogen": ["@vercel/hydrogen@1.0.11", "", { "dependencies": { "@vercel/static-config": "3.0.0", "ts-morph": "12.0.0" } }, "sha512-nkSQ0LC7rFRdfkTUGm9pIbAfRb2Aat05u8ouN0FoUl7/I/YVgd0G6iRBN9bOMFUIiBiaKB4KqaZEFzVfUHpwYw=="],
|
||||
|
||||
"@vercel/next": ["@vercel/next@4.4.2", "", { "dependencies": { "@vercel/nft": "0.27.10" } }, "sha512-bW/huCPGE2lRK7oUkqmwHiWpNcaWkyxJbLrsMlCF9JK6+iz5tj7EzUYng9KJzQMRMsVI7aieeA35VZqMwpYmHw=="],
|
||||
"@vercel/next": ["@vercel/next@4.4.4", "", { "dependencies": { "@vercel/nft": "0.27.10" } }, "sha512-/xMzlOMY8UHzCehRZzx8TIdzVRCu3O2O+Gb7R8uRX0/ci9cLIjJvi0WfLyR06Ny4fMqMzzUuRADp5ezfJjaO1Q=="],
|
||||
|
||||
"@vercel/nft": ["@vercel/nft@0.27.10", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0-rc.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg=="],
|
||||
|
||||
"@vercel/node": ["@vercel/node@5.0.2", "", { "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "16.18.11", "@vercel/build-utils": "9.0.1", "@vercel/error-utils": "2.0.3", "@vercel/nft": "0.27.10", "@vercel/static-config": "3.0.0", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", "es-module-lexer": "1.4.1", "esbuild": "0.14.47", "etag": "1.8.1", "node-fetch": "2.6.9", "path-to-regexp": "6.2.1", "ts-morph": "12.0.0", "ts-node": "10.9.1", "typescript": "4.9.5", "undici": "5.28.4" } }, "sha512-UcUVBC6i4j3WPxLA5GYnSvRd/E1fpqJ5dnMZMLromGCTzIXaw+Uzj7bsSSQ2Y9yPtwnWWrcEDmF3IumSTcdr/w=="],
|
||||
"@vercel/node": ["@vercel/node@5.0.4", "", { "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "16.18.11", "@vercel/build-utils": "9.1.0", "@vercel/error-utils": "2.0.3", "@vercel/nft": "0.27.10", "@vercel/static-config": "3.0.0", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", "es-module-lexer": "1.4.1", "esbuild": "0.14.47", "etag": "1.8.1", "node-fetch": "2.6.9", "path-to-regexp": "6.2.1", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0", "ts-node": "10.9.1", "typescript": "4.9.5", "undici": "5.28.4" } }, "sha512-AXpTFDzomabvi/FmxDDTwmnuqRBDfy2i0nzjKwVPM3ch94EucPbiAk3+18iZOX/A+o2mBO4jKc1DmB0ifQF2Rw=="],
|
||||
|
||||
"@vercel/python": ["@vercel/python@4.7.0", "", {}, "sha512-mkHmzYYZBLFLdvSdgrnBl1Qc1+LI5YIafSNJOj8oW4YU8vvALLMbwgZp42pZnyXW0e/3uHcesiRp4P0jSB0wyg=="],
|
||||
"@vercel/python": ["@vercel/python@4.7.1", "", {}, "sha512-H4g/5e8unII4oQ+KN5IUvTZSzHmj+lLYDkAK15QGYgAxBtE/mHUvEZpPPo7DPUDIyfq8ybWB1bmk7H5kEahubQ=="],
|
||||
|
||||
"@vercel/redwood": ["@vercel/redwood@2.1.12", "", { "dependencies": { "@vercel/nft": "0.27.10", "@vercel/routing-utils": "5.0.0", "@vercel/static-config": "3.0.0", "semver": "6.3.1", "ts-morph": "12.0.0" } }, "sha512-9CLwF8QKmxWlt1CMxoVD5gxTfwnojo1UlFONRSoUfjpNC+nTHEMCADVyawmDkANXs7Olx4QQSwJqsaWT8A9Jgg=="],
|
||||
"@vercel/redwood": ["@vercel/redwood@2.1.13", "", { "dependencies": { "@vercel/nft": "0.27.10", "@vercel/routing-utils": "5.0.1", "@vercel/static-config": "3.0.0", "semver": "6.3.1", "ts-morph": "12.0.0" } }, "sha512-e+4odfP2akWQq3WQ8mBkjqqwUcOvjhYmAhfg66IqTdIG15tIY6EOTMx/DhqXlvSDCyBbZPcqHb4/Xe662yPiEw=="],
|
||||
|
||||
"@vercel/remix-builder": ["@vercel/remix-builder@5.0.2", "", { "dependencies": { "@vercel/error-utils": "2.0.3", "@vercel/nft": "0.27.10", "@vercel/static-config": "3.0.0", "ts-morph": "12.0.0" } }, "sha512-VkdTOGdE/iiG476xQLmqzIrwlXe3oabiZzNCronJe8wgtWfF4+0jBExhLkv1KS92v1kOfAXPWXqPqs2MktE8ZQ=="],
|
||||
"@vercel/remix-builder": ["@vercel/remix-builder@5.1.1", "", { "dependencies": { "@vercel/error-utils": "2.0.3", "@vercel/nft": "0.27.10", "@vercel/static-config": "3.0.0", "ts-morph": "12.0.0" } }, "sha512-OP1f6GI8MdylL4aUrX6n7OkN93jqmkWyLzQMeQMapVOXKvRFj05STZ4SQ/kNJkXdh3rEzjJWuCsJ6bklTHkJ7Q=="],
|
||||
|
||||
"@vercel/routing-utils": ["@vercel/routing-utils@5.0.0", "", { "dependencies": { "path-to-regexp": "6.1.0" }, "optionalDependencies": { "ajv": "^6.0.0" } }, "sha512-llvozDbkGDSelbgigAt9IwCQS8boP4rNHfy3rpJf0DqSn6UDlkFX270NwIQruyXN9KHktHC9qOof6Ik2+bT88A=="],
|
||||
"@vercel/routing-utils": ["@vercel/routing-utils@5.0.1", "", { "dependencies": { "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0" }, "optionalDependencies": { "ajv": "^6.12.3" } }, "sha512-CH8sulzI8VNySWyJP+536fEX+oBnRuIVpw79jrn/0JwgCl7xb6E2JkKrMBT/mUCkZXh4vZZIOt23/QiIRK9Dyw=="],
|
||||
|
||||
"@vercel/ruby": ["@vercel/ruby@2.1.0", "", {}, "sha512-UZYwlSEEfVnfzTmgkD+kxex9/gkZGt7unOWNyWFN7V/ZnZSsGBUgv6hXLnwejdRi3EztgRQEBd1kUKlXdIeC0Q=="],
|
||||
"@vercel/ruby": ["@vercel/ruby@2.2.0", "", {}, "sha512-FJF9gKVNHAljGOgV6zS5ou2N7ZgjOqMMtcPA5lsJEUI5/AZzVDWCmtcowTP80wEtHuupkd7d7M399FA082kXYQ=="],
|
||||
|
||||
"@vercel/static-build": ["@vercel/static-build@2.5.41", "", { "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", "@vercel/gatsby-plugin-vercel-builder": "2.0.63", "@vercel/static-config": "3.0.0", "ts-morph": "12.0.0" } }, "sha512-cnKgFE6+xS00OCGLuURBLvWe2yIW9MU0ILKhFx3m3mpx8HfGYO2LN3b8Li8xYrJtr+rRUdGU3ITtXXveTClkug=="],
|
||||
"@vercel/static-build": ["@vercel/static-build@2.5.43", "", { "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", "@vercel/gatsby-plugin-vercel-builder": "2.0.65", "@vercel/static-config": "3.0.0", "ts-morph": "12.0.0" } }, "sha512-r6Pi/yC1nUCuq6V7xDxfMKDkwla4qnqpJVohd7cTsWRDKlRzHJJX/YaDp/6yKrDaNH9UY6cBhj9ryL8QJWY63w=="],
|
||||
|
||||
"@vercel/static-config": ["@vercel/static-config@3.0.0", "", { "dependencies": { "ajv": "8.6.3", "json-schema-to-ts": "1.6.4", "ts-morph": "12.0.0" } }, "sha512-2qtvcBJ1bGY0dYGYh3iM7yGKkk971FujLEDXzuW5wcZsPr1GSEjO/w2iSr3qve6nDDtBImsGoDEnus5FI4+fIw=="],
|
||||
|
||||
@@ -1632,7 +1635,7 @@
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="],
|
||||
"birpc": ["birpc@2.6.1", "", {}, "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
@@ -1680,7 +1683,7 @@
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="],
|
||||
"chardet": ["chardet@2.1.0", "", {}, "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.0", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA=="],
|
||||
|
||||
@@ -1768,7 +1771,7 @@
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="],
|
||||
|
||||
@@ -1922,8 +1925,6 @@
|
||||
|
||||
"extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="],
|
||||
|
||||
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
@@ -2102,13 +2103,13 @@
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"human-id": ["human-id@1.0.2", "", {}, "sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw=="],
|
||||
"human-id": ["human-id@4.1.2", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg=="],
|
||||
|
||||
"human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="],
|
||||
|
||||
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
"iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
@@ -2290,7 +2291,7 @@
|
||||
|
||||
"lru-queue": ["lru-queue@0.1.0", "", { "dependencies": { "es5-ext": "~0.10.2" } }, "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||
|
||||
"make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="],
|
||||
|
||||
@@ -2432,6 +2433,12 @@
|
||||
|
||||
"mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="],
|
||||
|
||||
"motion": ["motion@12.23.12", "", { "dependencies": { "framer-motion": "^12.23.12", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.23.12", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw=="],
|
||||
|
||||
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
@@ -2446,7 +2453,7 @@
|
||||
|
||||
"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.2.1", "", { "peerDependencies": { "next": "*", "react": "*", "react-dom": "*" } }, "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A=="],
|
||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
||||
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
|
||||
|
||||
@@ -2498,8 +2505,6 @@
|
||||
|
||||
"os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="],
|
||||
|
||||
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
|
||||
|
||||
"outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="],
|
||||
|
||||
"p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="],
|
||||
@@ -2544,13 +2549,15 @@
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.2.1", "", {}, "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="],
|
||||
|
||||
"path-to-regexp-updated": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
||||
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
@@ -2684,9 +2691,9 @@
|
||||
|
||||
"rison": ["rison@0.1.1", "", {}, "sha512-8C+/PKKTaAYE2quDtOUwny/eQpNn9YGby7T80wntbVWSGvw0aUT9M0YgLdLkUgIQzQwaB1ZTr80rwLVKyohHig=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-beta.37", "", { "dependencies": { "@oxc-project/runtime": "=0.87.0", "@oxc-project/types": "=0.87.0", "@rolldown/pluginutils": "1.0.0-beta.37", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.37", "@rolldown/binding-darwin-arm64": "1.0.0-beta.37", "@rolldown/binding-darwin-x64": "1.0.0-beta.37", "@rolldown/binding-freebsd-x64": "1.0.0-beta.37", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.37", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.37", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.37", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.37", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.37", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.37", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.37", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.37", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.37", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.37" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-KiTU6z1kHGaLvqaYjgsrv2LshHqNBn74waRZivlK8WbfN1obZeScVkQPKYunB66E/mxZWv/zyZlCv3xF2t0WOQ=="],
|
||||
"rolldown": ["rolldown@1.0.0-beta.42", "", { "dependencies": { "@oxc-project/types": "=0.94.0", "@rolldown/pluginutils": "1.0.0-beta.42", "ansis": "=4.2.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.42", "@rolldown/binding-darwin-arm64": "1.0.0-beta.42", "@rolldown/binding-darwin-x64": "1.0.0-beta.42", "@rolldown/binding-freebsd-x64": "1.0.0-beta.42", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.42", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.42", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.42", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.42", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.42", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.42", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.42", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.42", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.42", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.42" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-xaPcckj+BbJhYLsv8gOqezc8EdMcKKe/gk8v47B0KPvgABDrQ0qmNPAiT/gh9n9Foe0bUkEv2qzj42uU5q1WRg=="],
|
||||
|
||||
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.16.2", "", { "dependencies": { "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/types": "^7.28.4", "ast-kit": "^2.1.2", "birpc": "^2.5.0", "debug": "^4.4.1", "dts-resolver": "^2.1.2", "get-tsconfig": "^4.10.1" }, "peerDependencies": { "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "~3.0.3" }, "optionalPeers": ["@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-WV1J5OqhWL8O8Nd5VAVQghmNcjyQvcGI5w51VszjO9k73avbXpz9X86KLYcKTWXXH6L0f+93JzZxiiSrcYhDLw=="],
|
||||
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.16.11", "", { "dependencies": { "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/types": "^7.28.4", "ast-kit": "^2.1.2", "birpc": "^2.6.1", "debug": "^4.4.3", "dts-resolver": "^2.1.2", "get-tsconfig": "^4.10.1", "magic-string": "^0.30.19" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "~3.1.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-9IQDaPvPqTx3RjG2eQCK5GYZITo203BxKunGI80AGYicu1ySFTUyugicAaTZWRzFWh9DSnzkgNeMNbDWBbSs0w=="],
|
||||
|
||||
"rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="],
|
||||
|
||||
@@ -2702,7 +2709,7 @@
|
||||
|
||||
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
||||
|
||||
"semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"send": ["send@1.1.0", "", { "dependencies": { "debug": "^4.3.5", "destroy": "^1.2.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^0.5.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA=="],
|
||||
|
||||
@@ -2830,7 +2837,7 @@
|
||||
|
||||
"tippy.js": ["tippy.js@6.3.7", "", { "dependencies": { "@popperjs/core": "^2.9.0" } }, "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ=="],
|
||||
|
||||
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
|
||||
"tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
@@ -2858,23 +2865,23 @@
|
||||
|
||||
"ts-tqdm": ["ts-tqdm@0.8.6", "", {}, "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ=="],
|
||||
|
||||
"tsdown": ["tsdown@0.15.0", "", { "dependencies": { "ansis": "^4.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.1", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", "rolldown": "latest", "rolldown-plugin-dts": "^0.16.1", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig": "^7.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-xbz8tvdlB2wspocZWcJQhZNDLJC+sKV+sTIpiRgfUiQCUxLFGnUZUPc9KoKp3nfMeXQqTyMi6FM9/SasSj+csQ=="],
|
||||
"tsdown": ["tsdown@0.15.6", "", { "dependencies": { "ansis": "^4.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.3", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", "rolldown": "latest", "rolldown-plugin-dts": "^0.16.8", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig": "^7.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-W6++O3JeV9gm3JY6P/vLiC7zzTcJbZhQxXb+p3AvRMpDOPBIg82yXULyZCcwjsihY/bFG+Qw37HkezZbP7fzUg=="],
|
||||
|
||||
"tslib": ["tslib@2.7.0", "", {}, "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="],
|
||||
|
||||
"turbo": ["turbo@2.5.0", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.0", "turbo-darwin-arm64": "2.5.0", "turbo-linux-64": "2.5.0", "turbo-linux-arm64": "2.5.0", "turbo-windows-64": "2.5.0", "turbo-windows-arm64": "2.5.0" }, "bin": { "turbo": "bin/turbo" } }, "sha512-PvSRruOsitjy6qdqwIIyolv99+fEn57gP6gn4zhsHTEcCYgXPhv6BAxzAjleS8XKpo+Y582vTTA9nuqYDmbRuA=="],
|
||||
"turbo": ["turbo@2.5.8", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.8", "turbo-darwin-arm64": "2.5.8", "turbo-linux-64": "2.5.8", "turbo-linux-arm64": "2.5.8", "turbo-windows-64": "2.5.8", "turbo-windows-arm64": "2.5.8" }, "bin": { "turbo": "bin/turbo" } }, "sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w=="],
|
||||
|
||||
"turbo-darwin-64": ["turbo-darwin-64@2.5.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fP1hhI9zY8hv0idym3hAaXdPi80TLovmGmgZFocVAykFtOxF+GlfIgM/l4iLAV9ObIO4SUXPVWHeBZQQ+Hpjag=="],
|
||||
"turbo-darwin-64": ["turbo-darwin-64@2.5.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ=="],
|
||||
|
||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p9sYq7kXH7qeJwIQE86cOWv/xNqvow846l6c/qWc26Ib1ci5W7V0sI5thsrP3eH+VA0d+SHalTKg5SQXgNQBWA=="],
|
||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-f1H/tQC9px7+hmXn6Kx/w8Jd/FneIUnvLlcI/7RGHunxfOkKJKvsoiNzySkoHQ8uq1pJnhJ0xNGTlYM48ZaJOQ=="],
|
||||
|
||||
"turbo-linux-64": ["turbo-linux-64@2.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1iEln2GWiF3iPPPS1HQJT6ZCFXynJPd89gs9SkggH2EJsj3eRUSVMmMC8y6d7bBbhBFsiGGazwFIYrI12zs6uQ=="],
|
||||
"turbo-linux-64": ["turbo-linux-64@2.5.8", "", { "os": "linux", "cpu": "x64" }, "sha512-hMyvc7w7yadBlZBGl/bnR6O+dJTx3XkTeyTTH4zEjERO6ChEs0SrN8jTFj1lueNXKIHh1SnALmy6VctKMGnWfw=="],
|
||||
|
||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-bKBcbvuQHmsX116KcxHJuAcppiiBOfivOObh2O5aXNER6mce7YDDQJy00xQQNp1DhEfcSV2uOsvb3O3nN2cbcA=="],
|
||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.5.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-LQELGa7bAqV2f+3rTMRPnj5G/OHAe2U+0N9BwsZvfMvHSUbsQ3bBMWdSQaYNicok7wOZcHjz2TkESn1hYK6xIQ=="],
|
||||
|
||||
"turbo-windows-64": ["turbo-windows-64@2.5.0", "", { "os": "win32", "cpu": "x64" }, "sha512-9BCo8oQ7BO7J0K913Czbc3tw8QwLqn2nTe4E47k6aVYkM12ASTScweXPTuaPFP5iYXAT6z5Dsniw704Ixa5eGg=="],
|
||||
"turbo-windows-64": ["turbo-windows-64@2.5.8", "", { "os": "win32", "cpu": "x64" }, "sha512-3YdcaW34TrN1AWwqgYL9gUqmZsMT4T7g8Y5Azz+uwwEJW+4sgcJkIi9pYFyU4ZBSjBvkfuPZkGgfStir5BBDJQ=="],
|
||||
|
||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OUHCV+ueXa3UzfZ4co/ueIHgeq9B2K48pZwIxKSm5VaLVuv8M13MhM7unukW09g++dpdrrE1w4IOVgxKZ0/exg=="],
|
||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ=="],
|
||||
|
||||
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
|
||||
|
||||
@@ -2944,7 +2951,7 @@
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vercel": ["vercel@39.3.0", "", { "dependencies": { "@vercel/build-utils": "9.0.1", "@vercel/fun": "1.1.2", "@vercel/go": "3.2.1", "@vercel/hydrogen": "1.0.11", "@vercel/next": "4.4.2", "@vercel/node": "5.0.2", "@vercel/python": "4.7.0", "@vercel/redwood": "2.1.12", "@vercel/remix-builder": "5.0.2", "@vercel/ruby": "2.1.0", "@vercel/static-build": "2.5.41", "chokidar": "4.0.0" }, "bin": { "vc": "dist/vc.js", "vercel": "dist/vc.js" } }, "sha512-VyGaH5tnVVsgACcbU4PCRyQBDg/SST7/HQBaIXNnmOW7Ngjcn04wamjPgYAdFUGroiTm+ZpmNbCO1DQXzNeTjQ=="],
|
||||
"vercel": ["vercel@39.4.2", "", { "dependencies": { "@vercel/build-utils": "9.1.0", "@vercel/fun": "1.1.2", "@vercel/go": "3.2.1", "@vercel/hydrogen": "1.0.11", "@vercel/next": "4.4.4", "@vercel/node": "5.0.4", "@vercel/python": "4.7.1", "@vercel/redwood": "2.1.13", "@vercel/remix-builder": "5.1.1", "@vercel/ruby": "2.2.0", "@vercel/static-build": "2.5.43", "chokidar": "4.0.0" }, "bin": { "vc": "dist/vc.js", "vercel": "dist/vc.js" } }, "sha512-A3ilkwJ83xLwAYAI733hHthJ4DO0zXjQOvCWS9QYklWQTBEj0RllyRkrfGd2jypgNDZuAbDS/iFMsV+GuuaTHw=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
@@ -3038,8 +3045,6 @@
|
||||
|
||||
"@argos-ci/core/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
||||
|
||||
"@argos-ci/core/tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="],
|
||||
|
||||
"@argos-ci/playwright/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
@@ -3628,10 +3633,16 @@
|
||||
|
||||
"@aws-sdk/xml-builder/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@babel/code-frame/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
|
||||
|
||||
"@babel/highlight/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.7", "", {}, "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg=="],
|
||||
|
||||
"@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||
|
||||
"@babel/highlight/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
|
||||
|
||||
"@changesets/logger/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
|
||||
|
||||
"@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
|
||||
|
||||
"@codemirror/lang-html/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="],
|
||||
@@ -3708,14 +3719,10 @@
|
||||
|
||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/source-map/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="],
|
||||
|
||||
"@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@lezer/css/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="],
|
||||
|
||||
"@lezer/highlight/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="],
|
||||
@@ -3744,6 +3751,8 @@
|
||||
|
||||
"@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"@mapbox/node-pre-gyp/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||
|
||||
"@mapbox/node-pre-gyp/tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
@@ -3762,8 +3771,6 @@
|
||||
|
||||
"@opennextjs/aws/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
"@opennextjs/aws/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
|
||||
|
||||
"@playwright/test/playwright": ["playwright@1.54.2", "", { "dependencies": { "playwright-core": "1.54.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -4162,6 +4169,8 @@
|
||||
|
||||
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@tailwindcss/node/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@tailwindcss/oxide/tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||
@@ -4304,14 +4313,14 @@
|
||||
|
||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"google-font-metadata/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"https-proxy-agent/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"intl-messageformat/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"lowlight/highlight.js": ["highlight.js@11.9.0", "", {}, "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="],
|
||||
@@ -4352,6 +4361,10 @@
|
||||
|
||||
"minizlib/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="],
|
||||
|
||||
"motion/framer-motion": ["framer-motion@12.23.12", "", { "dependencies": { "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg=="],
|
||||
|
||||
"motion/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"p-filter/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="],
|
||||
@@ -4366,8 +4379,6 @@
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@3.1.2", "", {}, "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow=="],
|
||||
|
||||
"postcss-load-config/yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
|
||||
@@ -4396,6 +4407,8 @@
|
||||
|
||||
"rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"rolldown/ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
|
||||
|
||||
"rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
@@ -4408,16 +4421,12 @@
|
||||
|
||||
"send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"stylelint/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"stylelint/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"stylelint/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"sucrase/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="],
|
||||
@@ -4434,8 +4443,6 @@
|
||||
|
||||
"tsdown/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"tsdown/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"wrangler/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
@@ -4454,6 +4461,10 @@
|
||||
|
||||
"youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"@ampproject/remapping/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@ampproject/remapping/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@argos-ci/core/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"@argos-ci/core/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
@@ -4494,6 +4505,8 @@
|
||||
|
||||
"@argos-ci/core/sharp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
||||
|
||||
"@argos-ci/core/sharp/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||
|
||||
"@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="],
|
||||
|
||||
"@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="],
|
||||
@@ -4920,6 +4933,8 @@
|
||||
|
||||
"@codemirror/lang-json/@codemirror/language/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
@@ -4938,6 +4953,8 @@
|
||||
|
||||
"@headlessui/tailwindcss/tailwindcss/jiti": ["jiti@1.21.6", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="],
|
||||
|
||||
"@headlessui/tailwindcss/tailwindcss/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
|
||||
|
||||
"@headlessui/tailwindcss/tailwindcss/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="],
|
||||
|
||||
"@headlessui/tailwindcss/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
@@ -4948,6 +4965,10 @@
|
||||
|
||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"@jridgewell/source-map/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@jridgewell/source-map/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@mapbox/node-pre-gyp/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"@mapbox/node-pre-gyp/tar/minizlib": ["minizlib@3.0.1", "", { "dependencies": { "minipass": "^7.0.4", "rimraf": "^5.0.5" } }, "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg=="],
|
||||
@@ -5172,6 +5193,8 @@
|
||||
|
||||
"@smithy/util-endpoints/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw=="],
|
||||
|
||||
"@tailwindcss/node/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@tailwindcss/oxide/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
@@ -5184,8 +5207,6 @@
|
||||
|
||||
"@tailwindcss/postcss/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@tailwindcss/postcss/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"@types/jsonwebtoken/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
|
||||
@@ -5208,8 +5229,6 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@vueuse/integrations/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@11.2.0", "", {}, "sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
@@ -5244,8 +5263,12 @@
|
||||
|
||||
"micro/raw-body/http-errors": ["http-errors@1.7.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", "setprototypeof": "1.1.1", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" } }, "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw=="],
|
||||
|
||||
"micro/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"next/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="],
|
||||
|
||||
"next/postcss/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
|
||||
|
||||
"path-match/http-errors/inherits": ["inherits@2.0.1", "", {}, "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA=="],
|
||||
|
||||
"path-match/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
|
||||
@@ -5266,6 +5289,8 @@
|
||||
|
||||
"stylelint/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"sucrase/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"sucrase/@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
@@ -5314,6 +5339,8 @@
|
||||
|
||||
"wrangler/sharp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
||||
|
||||
"wrangler/sharp/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
@@ -5480,6 +5507,8 @@
|
||||
|
||||
"@scalar/api-client/vue/@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||
|
||||
"@scalar/api-client/vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@scalar/api-client/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"@scalar/components/@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
@@ -5488,30 +5517,44 @@
|
||||
|
||||
"@scalar/components/vue/@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||
|
||||
"@scalar/components/vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@scalar/components/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"@scalar/draggable/vue/@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||
|
||||
"@scalar/draggable/vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@scalar/draggable/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"@scalar/icons/vue/@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||
|
||||
"@scalar/icons/vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@scalar/icons/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"@scalar/use-codemirror/vue/@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||
|
||||
"@scalar/use-codemirror/vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@scalar/use-codemirror/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"@scalar/use-hooks/vue/@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||
|
||||
"@scalar/use-hooks/vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@scalar/use-hooks/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"@scalar/use-toasts/vue/@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||
|
||||
"@scalar/use-toasts/vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@scalar/use-toasts/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"@scalar/use-tooltip/vue/@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||
|
||||
"@scalar/use-tooltip/vue/@vue/compiler-sfc/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@scalar/use-tooltip/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="],
|
||||
@@ -5580,51 +5623,51 @@
|
||||
|
||||
"@scalar/api-client/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/api-client/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
"@scalar/api-client/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@scalar/api-client/vue/@vue/compiler-sfc/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"@scalar/api-client/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@scalar/components/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/components/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
"@scalar/components/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@scalar/components/vue/@vue/compiler-sfc/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"@scalar/components/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@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/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
"@scalar/draggable/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@scalar/draggable/vue/@vue/compiler-sfc/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"@scalar/draggable/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@scalar/icons/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/icons/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
"@scalar/icons/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@scalar/icons/vue/@vue/compiler-sfc/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"@scalar/icons/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@scalar/use-codemirror/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/use-codemirror/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
"@scalar/use-codemirror/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@scalar/use-codemirror/vue/@vue/compiler-sfc/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"@scalar/use-codemirror/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@scalar/use-hooks/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/use-hooks/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
"@scalar/use-hooks/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@scalar/use-hooks/vue/@vue/compiler-sfc/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"@scalar/use-hooks/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@scalar/use-toasts/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/use-toasts/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
"@scalar/use-toasts/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@scalar/use-toasts/vue/@vue/compiler-sfc/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"@scalar/use-toasts/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@scalar/use-tooltip/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/use-tooltip/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
"@scalar/use-tooltip/vue/@vue/compiler-sfc/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@scalar/use-tooltip/vue/@vue/compiler-sfc/postcss/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"@scalar/use-tooltip/vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"@smithy/core/@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=="],
|
||||
|
||||
|
||||
+10
-8
@@ -3,9 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"turbo": "^2.5.0",
|
||||
"vercel": "^39.3.0"
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"turbo": "^2.5.8",
|
||||
"vercel": "^39.4.2"
|
||||
},
|
||||
"packageManager": "bun@1.2.15",
|
||||
"overrides": {
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
"dev": "turbo run dev --concurrency 20",
|
||||
"build": "turbo run build",
|
||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||
"typecheck": "turbo run typecheck",
|
||||
@@ -26,16 +26,18 @@
|
||||
"e2e": "turbo run e2e",
|
||||
"e2e-customers": "turbo run e2e-customers",
|
||||
"changeset": "changeset",
|
||||
"changeset-version": "changeset version && bun run format",
|
||||
"release": "turbo run release && changeset publish",
|
||||
"changeset-version": "changeset version && bun run format && bun update",
|
||||
"release": "turbo run release && bun run publish-all-packages",
|
||||
"publish-all-packages": "for dir in packages/*; do (cd \"$dir\" && bun publish || true); done && changeset tag",
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||
"clean": "turbo run clean"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.141.0",
|
||||
"bidc": "^0.0.2"
|
||||
"@gitbook/api": "0.143.2",
|
||||
"bidc": "^0.0.2",
|
||||
"tsdown": "^0.15.6"
|
||||
}
|
||||
},
|
||||
"patchedDependencies": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/embed
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7508674: Fix bundling of packages
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js",
|
||||
"standalone": "./dist/standalone/index.js",
|
||||
"react": "./dist/react/index.js"
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./dist/react/index.d.ts",
|
||||
"default": "./dist/react/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:",
|
||||
@@ -20,12 +22,16 @@
|
||||
"react": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "^5.5.3",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"build": "bun run build-lib && bun run build-standalone",
|
||||
"build-lib": "tsdown",
|
||||
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"clean": "rm -rf ./dist",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md", "standalone"]
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ['src/index.ts', 'src/react/index.ts'],
|
||||
dts: true,
|
||||
format: ['esm'],
|
||||
},
|
||||
]);
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/expr
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a629900: Add dev script for @gitbook/expr
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/expr",
|
||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -21,17 +21,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.1.20",
|
||||
"tsdown": "^0.15.0",
|
||||
"tsdown": "catalog:",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@babel/types": "^7.26.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/escodegen": "^0.0.10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown --project tsconfig.build.json",
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"clean": "rm -rf ./dist"
|
||||
"clean": "rm -rf ./dist",
|
||||
"dev": "bun run build -- --watch"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": ["./tsconfig.json"],
|
||||
"exclude": ["**/*.test.ts"],
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,55 @@
|
||||
# gitbook
|
||||
|
||||
## 0.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ed7d47d: Show a toolbar for authenticated users with access to the current site.
|
||||
- 50c1be3: Move inline expression evaluation from API to GBO
|
||||
- 56e46ce: Add Italian translation
|
||||
- b932e4e: Scope search across sections and variants
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 162cfe4: Hide sections if only one is present
|
||||
- 1456251: Unify section display condition
|
||||
- 9344431: Fix missing geo data in site insights because of incorrect proxying of events.
|
||||
- eea8f1e: Enhance OpenAPI security scopes handling
|
||||
- 2b8a2d2: Add stable scroll gutter to search container
|
||||
- b498521: Fix robots.txt preventing favicon from being indexed.
|
||||
- ceb32b1: Redo search scope UI
|
||||
- 8e99871: Highlight discriminator properties in oneOf, allOf, anyOf objects
|
||||
- 2fc2127: Make search accessible
|
||||
- f3e4041: Adapt OpenAPI code samples to prefill API key using visitor data
|
||||
- 6815dd1: Fix markdownPageURL for PageActions
|
||||
- c06b3dd: Improve default site icon
|
||||
- cba583a: Use sitespace title instead of space title in search
|
||||
- e434442: Support nested site section groups
|
||||
- 469b332: Add support for inline expressions rendering with visitor data on GBO side
|
||||
- 0ef5dc8: Update to column width sizing
|
||||
- 1e4e54e: Fix rounded outline items
|
||||
- 379d486: Expose "Best match" when site has translations
|
||||
- c21693f: Improve screen reader accessibility for hints
|
||||
- badf3a9: Add alt text to inline image
|
||||
- c23d4ef: Fix crash for card cover defined without objectFit
|
||||
- d74f8a7: Improve wide page width layout
|
||||
- b8388e5: Add scrollcontainer component
|
||||
- b85eccd: Refactor section tabs
|
||||
- 1e53f46: Fix button content truncate
|
||||
- 7a504b7: Fix OpenAPI response dropdown & vertical overflow
|
||||
- a4c3399: Fix OpenAPI alternatives not showing
|
||||
- Updated dependencies [a629900]
|
||||
- Updated dependencies [eea8f1e]
|
||||
- Updated dependencies [8e99871]
|
||||
- Updated dependencies [f3e4041]
|
||||
- Updated dependencies [319a1e5]
|
||||
- Updated dependencies [529f940]
|
||||
- Updated dependencies [754cc11]
|
||||
- Updated dependencies [d7948e3]
|
||||
- Updated dependencies [a4c3399]
|
||||
- @gitbook/expr@1.2.1
|
||||
- @gitbook/react-openapi@1.5.0
|
||||
|
||||
## 0.18.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -62,11 +62,11 @@ const testCases: TestsCase[] = [
|
||||
contentBaseURL: 'https://docs.gmgn.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
{
|
||||
name: 'docs.spicychat.ai',
|
||||
contentBaseURL: 'https://docs.spicychat.ai',
|
||||
tests: [{ name: 'Home', url: '/' }],
|
||||
},
|
||||
// {
|
||||
// name: 'docs.spicychat.ai',
|
||||
// contentBaseURL: 'https://docs.spicychat.ai',
|
||||
// tests: [{ name: 'Home', url: '/' }],
|
||||
// },
|
||||
{
|
||||
name: 'docs.portainer.io',
|
||||
contentBaseURL: 'https://docs.portainer.io',
|
||||
|
||||
@@ -26,8 +26,10 @@ export default class extends WorkerEntrypoint {
|
||||
async fetch(request) {
|
||||
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||
const startTime = Date.now();
|
||||
const middlewareRequest = new Request(request.url, request);
|
||||
middlewareRequest.headers.set('x-open-next-continent', request.cf?.continent || '');
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
|
||||
const reqOrResp = await middlewareHandler(middlewareRequest, this.env, this.ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
const duration = Date.now() - startTime;
|
||||
const logMessage = formatLog(
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
|
||||
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
|
||||
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
|
||||
|
||||
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
|
||||
@@ -18,15 +20,12 @@ export type KeyOptions = {
|
||||
|
||||
/**
|
||||
*
|
||||
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
|
||||
* the cache operations. It also integrates both R2 and Cache API in a single class.
|
||||
* Having our own, will allow us to customize it in the future if needed.
|
||||
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it has an additional
|
||||
* R2WriteBuffer Durable Object to handle writes to R2. Given how we set up cache, we often end up writing to the same key too fast.
|
||||
*/
|
||||
class GitbookIncrementalCache implements IncrementalCache {
|
||||
name = 'GitbookIncrementalCache';
|
||||
|
||||
protected localCache: Cache | undefined;
|
||||
|
||||
async get<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
cacheType?: CacheType
|
||||
@@ -34,7 +33,6 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
const cacheKey = this.getR2Key(key, cacheType);
|
||||
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
const localCache = await this.getCacheInstance();
|
||||
if (!r2) throw new Error('No R2 bucket');
|
||||
if (process.env.SHOULD_BYPASS_CACHE === 'true') {
|
||||
// We are in a local middleware environment, we should bypass the cache
|
||||
@@ -42,27 +40,17 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// Check local cache first if available
|
||||
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
|
||||
if (localCacheEntry) {
|
||||
const result = (await localCacheEntry.json()) as WithLastModified<
|
||||
CacheValue<CacheType>
|
||||
>;
|
||||
return this.returnNullOn404({
|
||||
...result,
|
||||
// Because we use tag cache and also invalidate them every time,
|
||||
// if we get a cache hit, we don't need to check the tag cache as we already know it's not been revalidated
|
||||
// this should improve performance even further, and reduce costs
|
||||
shouldBypassTagCache: true,
|
||||
});
|
||||
}
|
||||
|
||||
const r2Object = await r2.get(cacheKey);
|
||||
if (!r2Object) return null;
|
||||
|
||||
const json = (await r2Object.json()) as CacheValue<CacheType>;
|
||||
const lastModified = r2Object.uploaded.getTime();
|
||||
|
||||
if (!json) return null;
|
||||
|
||||
return this.returnNullOn404({
|
||||
value: await r2Object.json(),
|
||||
lastModified: r2Object.uploaded.getTime(),
|
||||
value: json,
|
||||
lastModified,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to get from cache', e);
|
||||
@@ -89,39 +77,8 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
): Promise<void> {
|
||||
const cacheKey = this.getR2Key(key, cacheType);
|
||||
|
||||
const localCache = await this.getCacheInstance();
|
||||
|
||||
try {
|
||||
await this.writeToR2(cacheKey, JSON.stringify(value));
|
||||
|
||||
//TODO: Check if there is any places where we don't have tags
|
||||
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
|
||||
// For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
|
||||
const tags = this.getTagsFromCacheEntry(value) ?? [
|
||||
`build_id/${process.env.NEXT_BUILD_ID}`,
|
||||
];
|
||||
|
||||
// We consider R2 as the source of truth, so we update the local cache
|
||||
// only after a successful R2 write
|
||||
await localCache.put(
|
||||
this.getCacheUrlKey(cacheKey),
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
value,
|
||||
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
||||
// See https://developers.cloudflare.com/workers/reference/security-model/
|
||||
lastModified: Date.now(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
// Cache-Control default to 30 minutes, will be overridden by `revalidate`
|
||||
// In theory we should always get the `revalidate` value
|
||||
'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
|
||||
'cache-tag': tags.join(','),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to set to cache', e);
|
||||
}
|
||||
@@ -131,14 +88,10 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
const cacheKey = this.getR2Key(key);
|
||||
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
const localCache = await this.getCacheInstance();
|
||||
if (!r2) throw new Error('No R2 bucket');
|
||||
|
||||
try {
|
||||
await r2.delete(cacheKey);
|
||||
|
||||
// Here again R2 is the source of truth, so we delete from local cache first
|
||||
await localCache.delete(this.getCacheUrlKey(cacheKey));
|
||||
} catch (e) {
|
||||
console.error('Failed to delete from cache', e);
|
||||
}
|
||||
@@ -168,12 +121,6 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
}
|
||||
}
|
||||
|
||||
async getCacheInstance(): Promise<Cache> {
|
||||
if (this.localCache) return this.localCache;
|
||||
this.localCache = await caches.open('incremental-cache');
|
||||
return this.localCache;
|
||||
}
|
||||
|
||||
// Utility function to generate keys for R2/Cache API
|
||||
getR2Key(initialKey: string, cacheType: CacheEntryType = 'cache'): string {
|
||||
let key = initialKey;
|
||||
@@ -189,28 +136,13 @@ class GitbookIncrementalCache implements IncrementalCache {
|
||||
'/'
|
||||
);
|
||||
}
|
||||
|
||||
getCacheUrlKey(cacheKey: string): string {
|
||||
return `http://cache.local/${cacheKey}`;
|
||||
}
|
||||
|
||||
getTagsFromCacheEntry<CacheType extends CacheEntryType>(
|
||||
entry: CacheValue<CacheType>
|
||||
): string[] | undefined {
|
||||
if ('tags' in entry && entry.tags) {
|
||||
return entry.tags;
|
||||
}
|
||||
|
||||
if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
|
||||
const rawTags = entry.meta.headers['x-next-cache-tags'];
|
||||
if (typeof rawTags === 'string') {
|
||||
return rawTags.split(',');
|
||||
}
|
||||
}
|
||||
if ('value' in entry) {
|
||||
return entry.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new GitbookIncrementalCache();
|
||||
export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
mode: 'long-lived',
|
||||
// We can do it because we use our own logic to invalidate the cache
|
||||
bypassTagCacheOnCacheHit: true,
|
||||
defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */,
|
||||
// We don't want to update the cache entry on every cache hit
|
||||
shouldLazilyUpdateOnCacheHit: false,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
@@ -14,7 +15,8 @@
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@opennextjs/cloudflare": "^1.7.1",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"@opennextjs/cloudflare": "^1.8.3",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
@@ -39,6 +41,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"katex": "^0.16.9",
|
||||
"mathjax": "^3.2.2",
|
||||
"mcp-handler": "^1.0.2",
|
||||
"mdast-util-from-markdown": "^2.0.2",
|
||||
"mdast-util-frontmatter": "^2.0.1",
|
||||
"mdast-util-gfm": "^3.1.0",
|
||||
@@ -46,8 +49,9 @@
|
||||
"memoizee": "^0.4.17",
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.12",
|
||||
"next": "15.4.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
"object-identity": "^0.1.2",
|
||||
@@ -71,10 +75,8 @@
|
||||
"url-join": "^5.0.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"warn-once": "^0.1.1",
|
||||
"zustand": "^5.0.3",
|
||||
"mcp-handler": "^1.0.2",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"zod": "^3"
|
||||
"zod": "^3",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^5.0.9",
|
||||
|
||||
+100
-2
@@ -16,9 +16,107 @@ export async function GET(
|
||||
return new Response(
|
||||
`
|
||||
<html>
|
||||
<head></head>
|
||||
<body></body>
|
||||
<head>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
</head>
|
||||
<body>
|
||||
<svg style="position: absolute; bottom: 6rem; right: 4rem;" width="719" height="644" viewBox="0 0 719 644" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.1" x="0.566895" y="208" width="529.567" height="228" rx="16" fill="#A4A7B0"/>
|
||||
<g opacity="0.2">
|
||||
<line x1="141.567" y1="472" x2="4.56689" y2="472" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="416.067" y1="472" x2="165.567" y2="472" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="526.567" y1="472" x2="440.067" y2="472" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="194.567" y1="496" x2="4.56689" y2="496" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="324.067" y1="496" x2="218.567" y2="496" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="473.567" y1="496" x2="348.067" y2="496" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="231.901" y1="520" x2="4.56689" y2="520" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="291.186" y1="520" x2="255.901" y2="520" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="442.058" y1="520" x2="315.186" y2="520" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="526.567" y1="520" x2="466.058" y2="520" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="141.567" y1="544" x2="4.56689" y2="544" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="348.191" y1="544" x2="165.567" y2="544" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="184.017" y1="592" x2="4.56689" y2="592" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="312.848" y1="592" x2="208.017" y2="592" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="386.574" y1="592" x2="336.848" y2="592" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="448.401" y1="592" x2="410.574" y2="592" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="126.633" y1="616" x2="4.56689" y2="616" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="401.133" y1="616" x2="150.633" y2="616" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="526.567" y1="616" x2="425.133" y2="616" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="179.457" y1="640" x2="4.56689" y2="640" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="329.569" y1="640" x2="203.457" y2="640" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="141.567" y1="4" x2="4.56689" y2="4" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="416.067" y1="4" x2="165.567" y2="4" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="526.567" y1="4" x2="440.067" y2="4" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="194.567" y1="28" x2="4.56689" y2="28" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="324.067" y1="28" x2="218.567" y2="28" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="473.567" y1="28" x2="348.067" y2="28" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="231.901" y1="52" x2="4.56689" y2="52" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="291.186" y1="52" x2="255.901" y2="52" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="442.058" y1="52" x2="315.186" y2="52" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="526.567" y1="52" x2="466.058" y2="52" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="141.567" y1="76" x2="4.56689" y2="76" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="348.191" y1="76" x2="165.567" y2="76" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="184.017" y1="124" x2="4.56689" y2="124" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="312.848" y1="124" x2="208.017" y2="124" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="386.574" y1="124" x2="336.848" y2="124" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="448.401" y1="124" x2="410.574" y2="124" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="126.633" y1="148" x2="4.56689" y2="148" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="401.133" y1="148" x2="150.633" y2="148" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="526.567" y1="148" x2="425.133" y2="148" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="179.457" y1="172" x2="4.56689" y2="172" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="329.569" y1="172" x2="203.457" y2="172" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<rect x="563" y="208" width="156" height="256" rx="16" fill="#A4A7B0" opacity="0.05"/>
|
||||
<g opacity="0.2">
|
||||
<line x1="682.214" y1="228" x2="583" y2="228" stroke="#656973" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="638.472" y1="252" x2="583" y2="252" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="681.396" y1="252" x2="654.472" y2="252" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="617.989" y1="276" x2="583" y2="276" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="699" y1="276" x2="633.989" y2="276" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="603.168" y1="300" x2="583" y2="300" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="657.873" y1="300" x2="619.168" y2="300" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="637" y1="324" x2="583" y2="324" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="688.836" y1="324" x2="653" y2="324" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="657.474" y1="372" x2="583" y2="372" stroke="#656973" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="633" y1="396" x2="583" y2="396" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="699" y1="396" x2="649" y2="396" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="585.416" y1="420" x2="583" y2="420" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="621.731" y1="420" x2="601.416" y2="420" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="699" y1="420" x2="637.731" y2="420" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
<g opacity="0.2">
|
||||
<line x1="619.471" y1="444" x2="583" y2="444" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="658.186" y1="444" x2="635.471" y2="444" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
</body>
|
||||
<script src="${context.linker.toAbsoluteURL(context.linker.toPathInSite('~gitbook/embed/script.js'))}"></script>
|
||||
<script>
|
||||
window.GitBook('configure', {
|
||||
suggestions: [
|
||||
'Help me get started',
|
||||
'What can I ask you?',
|
||||
'Show me tips and tricks',
|
||||
],
|
||||
});
|
||||
window.GitBook('open');
|
||||
</script>
|
||||
</html>
|
||||
`,
|
||||
{
|
||||
|
||||
@@ -137,7 +137,7 @@ export function useAI(): AIContext {
|
||||
setSearchState((prev) => ({
|
||||
ask: null, // Reset ask as we assume the assistant will handle it
|
||||
query: prev?.query ?? null,
|
||||
global: prev?.global ?? false,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false,
|
||||
}));
|
||||
assistant.open(query);
|
||||
|
||||
@@ -146,7 +146,7 @@ export function AIChatProvider(props: {
|
||||
setSearchState((prev) => ({
|
||||
ask: prev?.ask ?? initialQuery ?? '',
|
||||
query: prev?.query ?? null,
|
||||
global: prev?.global ?? false,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false, // Close search popover when opening chat
|
||||
}));
|
||||
}, [setSearchState]);
|
||||
@@ -159,7 +159,7 @@ export function AIChatProvider(props: {
|
||||
setSearchState((prev) => ({
|
||||
ask: null,
|
||||
query: prev?.query ?? null,
|
||||
global: prev?.global ?? false,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false,
|
||||
}));
|
||||
}, [setSearchState]);
|
||||
@@ -374,7 +374,7 @@ export function AIChatProvider(props: {
|
||||
setSearchState((prev) => ({
|
||||
ask: input.message,
|
||||
query: prev?.query ?? null,
|
||||
global: prev?.global ?? false,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false,
|
||||
}));
|
||||
}
|
||||
@@ -435,7 +435,7 @@ export function AIChatProvider(props: {
|
||||
setSearchState((prev) => ({
|
||||
ask: '',
|
||||
query: prev?.query ?? null,
|
||||
global: prev?.global ?? false,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false,
|
||||
}));
|
||||
}, [setSearchState]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
import { useLanguage } from '@/intl/client';
|
||||
import { t } from '@/intl/translate';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { Assistant } from '../AI';
|
||||
import { Button } from '../primitives';
|
||||
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
|
||||
@@ -24,7 +23,7 @@ export function AIChatButton(props: {
|
||||
iconOnly={!showLabel}
|
||||
size="medium"
|
||||
variant="header"
|
||||
className={tcls('h-9 px-2.5')}
|
||||
className="h-9 px-2.5 max-md:[&_.button-content]:hidden"
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
{t(language, 'ai_chat_ask', assistant.label)}
|
||||
@@ -38,7 +37,7 @@ export function AIChatButton(props: {
|
||||
}
|
||||
onClick={() => assistant.open()}
|
||||
>
|
||||
{showLabel ? <span className="max-md:hidden">{t(language, 'ask')}</span> : null}
|
||||
{showLabel ? t(language, 'ask') : null}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export function AIChatInput(props: {
|
||||
<span className="leading-none">
|
||||
{t(language, 'ai_chat_context_title')}
|
||||
</span>
|
||||
<Icon icon="question-circle" className="size-3" />
|
||||
<Icon icon="question-circle" className="size-3 shrink-0" />
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
</HoverCardRoot>
|
||||
|
||||
@@ -4,19 +4,14 @@ import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { OpenAPIPrefillContextProvider } from '@gitbook/react-openapi';
|
||||
import * as React from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type AdaptiveVisitorClaimsData = {
|
||||
visitor: {
|
||||
claims: Record<string, unknown> & { unsigned: Record<string, unknown> };
|
||||
};
|
||||
};
|
||||
import type { AdaptiveVisitorClaims } from './types';
|
||||
|
||||
/**
|
||||
* In-memory cache of visitor claim readers keyed by contextId.
|
||||
*/
|
||||
const adaptiveVisitorReaderCache = new Map<
|
||||
string,
|
||||
ReturnType<typeof createResourceReader<AdaptiveVisitorClaimsData | null>>
|
||||
ReturnType<typeof createResourceReader<AdaptiveVisitorClaims | null>>
|
||||
>();
|
||||
|
||||
function createResourceReader<T>(promise: Promise<T>) {
|
||||
@@ -52,7 +47,7 @@ function getAdaptiveVisitorClaimsReader(url: string, contextId: string) {
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
return await res.json<AdaptiveVisitorClaimsData>();
|
||||
return await res.json<AdaptiveVisitorClaims>();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -64,7 +59,7 @@ function getAdaptiveVisitorClaimsReader(url: string, contextId: string) {
|
||||
return reader;
|
||||
}
|
||||
|
||||
export type AdaptiveVisitorContextValue = () => AdaptiveVisitorClaimsData | null;
|
||||
export type AdaptiveVisitorContextValue = () => AdaptiveVisitorClaims | null;
|
||||
|
||||
const AdaptiveVisitorContext = createContext<AdaptiveVisitorContextValue>(() => null);
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './AdaptiveVisitorContextProvider';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export type AdaptiveVisitorClaimsData = Record<string, unknown> & {
|
||||
unsigned: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type AdaptiveVisitorClaims = {
|
||||
visitor: {
|
||||
claims: AdaptiveVisitorClaimsData;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Variables } from '@gitbook/api';
|
||||
import type { AdaptiveVisitorClaims } from './types';
|
||||
|
||||
/**
|
||||
* Return an evaluation context to evaluate expressions.
|
||||
*/
|
||||
export function createExpressionEvaluationContext(args: {
|
||||
visitorClaims: AdaptiveVisitorClaims | null;
|
||||
variables: {
|
||||
space?: Variables;
|
||||
page?: Variables;
|
||||
};
|
||||
}) {
|
||||
const { visitorClaims, variables } = args;
|
||||
return {
|
||||
...(visitorClaims ? visitorClaims : {}),
|
||||
space: {
|
||||
vars: variables.space ?? {},
|
||||
},
|
||||
...(variables.page
|
||||
? {
|
||||
page: {
|
||||
vars: variables.page ?? {},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
@@ -1,143 +1,63 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
import { AdminToolbarClient } from './AdminToolbarClient';
|
||||
import type { AdminToolbarContext } from './types';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { DateRelative } from '../primitives';
|
||||
import { IframeWrapper } from './IframeWrapper';
|
||||
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
||||
import { Toolbar, ToolbarBody, ToolbarButton, ToolbarButtonGroups } from './Toolbar';
|
||||
|
||||
interface AdminToolbarProps {
|
||||
export interface AdminToolbarProps {
|
||||
context: GitBookSiteContext;
|
||||
}
|
||||
|
||||
function ToolbarLayout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'fixed',
|
||||
'bottom-5',
|
||||
'left-1/2',
|
||||
'z-50',
|
||||
'transform',
|
||||
'-translate-x-1/2',
|
||||
'rounded-full',
|
||||
|
||||
'bg-tint-12/9',
|
||||
'dark:bg-tint-1/9',
|
||||
|
||||
'shadow-lg',
|
||||
'min-h-10',
|
||||
'min-w-40',
|
||||
'p-2',
|
||||
'max-w-md',
|
||||
'border-tint-12/1',
|
||||
'backdrop-blur-md'
|
||||
)}
|
||||
>
|
||||
<React.Suspense fallback={null}>{props.children}</React.Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbar with information for the content admin when previewing a revision or change-request.
|
||||
* Server component that determines what type of toolbar to show and passes data to client component
|
||||
*/
|
||||
export async function AdminToolbar(props: AdminToolbarProps) {
|
||||
const { context } = props;
|
||||
|
||||
if (context.changeRequest) {
|
||||
return (
|
||||
<IframeWrapper>
|
||||
<ChangeRequestToolbar context={context} />
|
||||
</IframeWrapper>
|
||||
);
|
||||
}
|
||||
// Create a minimal context to avoid serializing and passing too many data to the client
|
||||
const minimalContext: AdminToolbarContext = {
|
||||
organizationId: context.organizationId,
|
||||
revisionId: context.revisionId,
|
||||
space: {
|
||||
id: context.space.id,
|
||||
revision: context.space.revision,
|
||||
urls: {
|
||||
app: context.space.urls.app,
|
||||
},
|
||||
},
|
||||
changeRequest: context.changeRequest
|
||||
? {
|
||||
id: context.changeRequest.id,
|
||||
number: context.changeRequest.number,
|
||||
subject: context.changeRequest.subject,
|
||||
revision: context.changeRequest.revision,
|
||||
updatedAt: context.changeRequest.updatedAt,
|
||||
createdBy: {
|
||||
displayName: context.changeRequest.createdBy.displayName,
|
||||
},
|
||||
urls: {
|
||||
app: context.changeRequest.urls.app,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
revision: {
|
||||
createdAt: context.revision.createdAt,
|
||||
urls: {
|
||||
app: context.revision.urls.app,
|
||||
},
|
||||
git: context.revision.git
|
||||
? {
|
||||
url: context.revision.git.url,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
site: {
|
||||
id: context.site.id,
|
||||
title: context.site.title,
|
||||
urls: {
|
||||
app: context.site.urls.app,
|
||||
published: context.site.urls.published,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (context.revisionId !== context.space.revision) {
|
||||
return (
|
||||
<IframeWrapper>
|
||||
<RevisionToolbar context={context} />
|
||||
</IframeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function ChangeRequestToolbar(props: { context: GitBookSiteContext }) {
|
||||
const { context } = props;
|
||||
const { space, changeRequest } = context;
|
||||
|
||||
if (!changeRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarLayout>
|
||||
<Toolbar>
|
||||
<ToolbarButton title="Open in application" href={changeRequest.urls.app}>
|
||||
<Icon icon="code-branch" className="size-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarBody>
|
||||
<p>
|
||||
#{changeRequest.number}: {changeRequest.subject ?? 'No subject'}
|
||||
</p>
|
||||
<p className="text-tint-2 text-xs dark:text-tint-11">
|
||||
Change request updated <DateRelative value={changeRequest.updatedAt} />
|
||||
</p>
|
||||
</ToolbarBody>
|
||||
<ToolbarButtonGroups>
|
||||
<ToolbarButton title="Open in application" href={changeRequest.urls.app}>
|
||||
<Icon icon="arrow-up-right-from-square" className="size-4" />
|
||||
</ToolbarButton>
|
||||
<RefreshChangeRequestButton
|
||||
spaceId={space.id}
|
||||
changeRequestId={changeRequest.id}
|
||||
revisionId={changeRequest.revision}
|
||||
updatedAt={new Date(changeRequest.updatedAt).getTime()}
|
||||
/>
|
||||
</ToolbarButtonGroups>
|
||||
</Toolbar>
|
||||
</ToolbarLayout>
|
||||
);
|
||||
}
|
||||
|
||||
async function RevisionToolbar(props: { context: GitBookSiteContext }) {
|
||||
const { context } = props;
|
||||
const { revision } = context;
|
||||
|
||||
return (
|
||||
<ToolbarLayout>
|
||||
<Toolbar>
|
||||
<ToolbarButton title="Open in application" href={revision.urls.app}>
|
||||
<Icon icon="code-commit" className="size-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarBody>
|
||||
<p>
|
||||
Revision created <DateRelative value={revision.createdAt} />
|
||||
</p>
|
||||
{revision.git ? (
|
||||
<p className="text-tint-2 text-xs dark:text-tint-11">
|
||||
{revision.git.message}
|
||||
</p>
|
||||
) : null}
|
||||
</ToolbarBody>
|
||||
<ToolbarButtonGroups>
|
||||
<ToolbarButton title="Open in application" href={revision.urls.app}>
|
||||
<Icon icon="arrow-up-right-from-square" className="size-4" />
|
||||
</ToolbarButton>
|
||||
{revision.git?.url ? (
|
||||
<ToolbarButton title="Open git commit" href={revision.git.url}>
|
||||
<Icon
|
||||
icon={revision.git.url.includes('github.com') ? 'github' : 'gitlab'}
|
||||
className="size-4"
|
||||
/>
|
||||
</ToolbarButton>
|
||||
) : null}
|
||||
</ToolbarButtonGroups>
|
||||
</Toolbar>
|
||||
</ToolbarLayout>
|
||||
);
|
||||
return <AdminToolbarClient context={minimalContext} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
'use client';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { MotionConfig } from 'motion/react';
|
||||
import { useCheckForContentUpdate } from '../AutoRefreshContent';
|
||||
import { useVisitorSession } from '../Insights';
|
||||
import { useCurrentPagePath } from '../hooks';
|
||||
import { DateRelative } from '../primitives';
|
||||
import { HideToolbarButton } from './HideToolbarButton';
|
||||
import { IframeWrapper } from './IframeWrapper';
|
||||
import { RefreshContentButton } from './RefreshContentButton';
|
||||
import {
|
||||
Toolbar,
|
||||
ToolbarBody,
|
||||
ToolbarButton,
|
||||
ToolbarButtonGroup,
|
||||
type ToolbarButtonProps,
|
||||
ToolbarSeparator,
|
||||
ToolbarSubtitle,
|
||||
ToolbarTitle,
|
||||
} from './Toolbar';
|
||||
import {
|
||||
type ToolbarControlsContextValue,
|
||||
ToolbarControlsProvider,
|
||||
} from './ToolbarControlsContext';
|
||||
import type { AdminToolbarClientProps, AdminToolbarContext } from './types';
|
||||
import { useToolbarVisibility } from './utils';
|
||||
|
||||
export function AdminToolbarClient(props: AdminToolbarClientProps) {
|
||||
const { context, onPersistentClose, onSessionClose, onToggleMinify } = props;
|
||||
const {
|
||||
minified,
|
||||
setMinified,
|
||||
shouldAutoExpand,
|
||||
hidden,
|
||||
minimize,
|
||||
closeSession,
|
||||
closePersistent,
|
||||
} = useToolbarVisibility({
|
||||
onPersistentClose,
|
||||
onSessionClose,
|
||||
onToggleMinify,
|
||||
});
|
||||
|
||||
const visitorSession = useVisitorSession();
|
||||
|
||||
const toolbarControls: ToolbarControlsContextValue = {
|
||||
minimize,
|
||||
closeSession,
|
||||
closePersistent,
|
||||
shouldAutoExpand,
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there is a change request, show the change request toolbar
|
||||
if (context.changeRequest) {
|
||||
return (
|
||||
<ToolbarControlsWrapper value={toolbarControls}>
|
||||
<ChangeRequestToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={setMinified}
|
||||
/>
|
||||
</ToolbarControlsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// If the revision is not the current revision, the user is looking at a previous version of the site, so show the revision toolbar
|
||||
if (context.revisionId !== context.space.revision) {
|
||||
return (
|
||||
<ToolbarControlsWrapper value={toolbarControls}>
|
||||
<RevisionToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={setMinified}
|
||||
/>
|
||||
</ToolbarControlsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// If the user is authenticated and part of the organization owning this site, show the authenticated user toolbar
|
||||
if (visitorSession?.organizationId === context.organizationId) {
|
||||
return (
|
||||
<ToolbarControlsWrapper value={toolbarControls}>
|
||||
<AuthenticatedUserToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={setMinified}
|
||||
/>
|
||||
</ToolbarControlsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable wrapper that provides tooling and containers that are used by all types of toolbar views.
|
||||
*/
|
||||
export function ToolbarControlsWrapper(
|
||||
props: React.PropsWithChildren<{ value: ToolbarControlsContextValue | null }>
|
||||
) {
|
||||
const { children, value } = props;
|
||||
return (
|
||||
<ToolbarControlsProvider value={value}>
|
||||
<IframeWrapper>
|
||||
<MotionConfig reducedMotion="user">{children}</MotionConfig>
|
||||
</IframeWrapper>
|
||||
</ToolbarControlsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToolbarViewProps {
|
||||
context: AdminToolbarContext;
|
||||
minified: boolean;
|
||||
onMinifiedChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
function ChangeRequestToolbar(props: ToolbarViewProps) {
|
||||
const { context, minified, onMinifiedChange } = props;
|
||||
const { changeRequest, site } = context;
|
||||
if (!changeRequest) {
|
||||
throw new Error('Change request is not set');
|
||||
}
|
||||
|
||||
const author = changeRequest.createdBy.displayName;
|
||||
|
||||
const { refreshForUpdates, updated } = useCheckForContentUpdate({
|
||||
revisionId: changeRequest.revision,
|
||||
});
|
||||
|
||||
return (
|
||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange} label="Site preview">
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle
|
||||
prefix={`Change #${changeRequest.number}:`}
|
||||
suffix={`${changeRequest.subject || 'Untitled'}`}
|
||||
/>
|
||||
<ToolbarSubtitle
|
||||
subtitle={
|
||||
<>
|
||||
<DateRelative value={changeRequest.updatedAt} /> by {author}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ToolbarBody>
|
||||
|
||||
<ToolbarSeparator />
|
||||
|
||||
<ToolbarActions>
|
||||
{/* Refresh to retrieve latest changes */}
|
||||
{updated ? <RefreshContentButton refreshForUpdates={refreshForUpdates} /> : null}
|
||||
|
||||
{/* Edit in GitBook */}
|
||||
<EditPageButton href={changeRequest.urls.app} siteId={site.id} />
|
||||
|
||||
{/* Comment in app */}
|
||||
<ToolbarButton
|
||||
title="Comment in a GitBook"
|
||||
href={getToolbarHref({
|
||||
href: `${changeRequest.urls.app}~/comments`,
|
||||
siteId: site.id,
|
||||
buttonId: 'comment',
|
||||
})}
|
||||
icon="comment"
|
||||
/>
|
||||
|
||||
{/* Open production site */}
|
||||
{site.urls.published ? (
|
||||
<ToolbarButton
|
||||
title="Open production site"
|
||||
href={getToolbarHref({
|
||||
href: site.urls.published,
|
||||
siteId: site.id,
|
||||
buttonId: 'production-site',
|
||||
})}
|
||||
icon="globe"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Open CR in GitBook */}
|
||||
<ToolbarButton
|
||||
title="View change request in GitBook"
|
||||
href={getToolbarHref({
|
||||
href: changeRequest.urls.app,
|
||||
siteId: site.id,
|
||||
buttonId: 'change-request',
|
||||
})}
|
||||
icon="code-pull-request"
|
||||
/>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionToolbar(props: ToolbarViewProps) {
|
||||
const { context, minified, onMinifiedChange } = props;
|
||||
const { revision, site } = context;
|
||||
if (!revision) {
|
||||
throw new Error('Revision is not set');
|
||||
}
|
||||
|
||||
const gitURL = revision.git?.url;
|
||||
const isGitHub = gitURL?.includes('github.com');
|
||||
const gitProvider = isGitHub ? 'GitHub' : 'GitLab';
|
||||
|
||||
return (
|
||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange} label="Site preview">
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle prefix="Site version" suffix={context.site.title} />
|
||||
<ToolbarSubtitle
|
||||
subtitle={
|
||||
<>
|
||||
Created <DateRelative value={revision.createdAt} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ToolbarBody>
|
||||
<ToolbarSeparator />
|
||||
<ToolbarActions>
|
||||
{/* Open commit in Git client */}
|
||||
<ToolbarButton
|
||||
title={
|
||||
gitURL ? (
|
||||
`Open commit in ${gitProvider}`
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
Setup GitSync to edit using Git{' '}
|
||||
<div className="flex items-center gap-1 text-neutral-8 text-xs hover:text-neutral-6 hover:underline dark:text-neutral-3">
|
||||
<a
|
||||
href="https://gitbook.com/docs/getting-started/git-sync"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className=""
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
<Icon icon="arrow-up-right" className="size-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
href={gitURL}
|
||||
disabled={!gitURL}
|
||||
icon={gitURL ? (isGitHub ? 'github' : 'gitlab') : 'github'}
|
||||
/>
|
||||
{site.urls.published ? (
|
||||
<ToolbarButton
|
||||
title="Open production site"
|
||||
href={getToolbarHref({
|
||||
href: site.urls.published,
|
||||
siteId: site.id,
|
||||
buttonId: 'production-site',
|
||||
})}
|
||||
icon="globe"
|
||||
/>
|
||||
) : null}
|
||||
<ToolbarButton
|
||||
title="View this revision in GitBook"
|
||||
href={getToolbarHref({
|
||||
href: revision.urls.app,
|
||||
siteId: site.id,
|
||||
buttonId: 'revision',
|
||||
})}
|
||||
icon="code-commit"
|
||||
/>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticatedUserToolbar(props: ToolbarViewProps) {
|
||||
const { context, minified, onMinifiedChange } = props;
|
||||
const { revision, space, site } = context;
|
||||
const { refreshForUpdates, updated } = useCheckForContentUpdate({
|
||||
revisionId: space.revision,
|
||||
});
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
minified={minified}
|
||||
onMinifiedChange={onMinifiedChange}
|
||||
label="Only visible to your GitBook organization"
|
||||
>
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle suffix={context.site.title} />
|
||||
<ToolbarSubtitle
|
||||
subtitle={
|
||||
<>
|
||||
Updated <DateRelative value={revision.createdAt} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ToolbarBody>
|
||||
<ToolbarSeparator />
|
||||
<ToolbarActions>
|
||||
{/* Refresh to retrieve latest changes */}
|
||||
{updated ? <RefreshContentButton refreshForUpdates={refreshForUpdates} /> : null}
|
||||
|
||||
{/* Edit in GitBook */}
|
||||
<EditPageButton href={space.urls.app} siteId={site.id} />
|
||||
|
||||
{/* Open site in GitBook */}
|
||||
<ToolbarButton
|
||||
title="Open site in GitBook"
|
||||
href={getToolbarHref({
|
||||
href: site.urls.app,
|
||||
siteId: site.id,
|
||||
buttonId: 'site',
|
||||
})}
|
||||
icon="gears"
|
||||
/>
|
||||
|
||||
{/* Customize in GitBook */}
|
||||
<ToolbarButton
|
||||
title="Customize in GitBook"
|
||||
href={getToolbarHref({
|
||||
href: `${site.urls.app}/customization/general`,
|
||||
siteId: site.id,
|
||||
buttonId: 'customize',
|
||||
})}
|
||||
icon="palette"
|
||||
/>
|
||||
|
||||
{/* Open insights in GitBook */}
|
||||
<ToolbarButton
|
||||
title="Open insights in GitBook"
|
||||
href={getToolbarHref({
|
||||
href: `${site.urls.app}/insights`,
|
||||
siteId: site.id,
|
||||
buttonId: 'insights',
|
||||
})}
|
||||
icon="chart-simple"
|
||||
/>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarActions(props: { children: React.ReactNode }) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<ToolbarButtonGroup>
|
||||
{children}
|
||||
<HideToolbarButton />
|
||||
</ToolbarButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function EditPageButton(props: {
|
||||
href: string;
|
||||
siteId: string;
|
||||
motionValues?: ToolbarButtonProps['motionValues'];
|
||||
}) {
|
||||
const { href, motionValues, siteId } = props;
|
||||
const pagePath = useCurrentPagePath();
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
title="Edit in GitBook"
|
||||
href={getToolbarHref({
|
||||
href: `${href}${pagePath.startsWith('/') ? pagePath.slice(1) : pagePath}`,
|
||||
siteId,
|
||||
buttonId: 'edit',
|
||||
})}
|
||||
icon="pencil"
|
||||
motionValues={motionValues}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append utm parameters to a URL to track usage of the toolbar.
|
||||
*/
|
||||
function getToolbarHref({
|
||||
href,
|
||||
siteId,
|
||||
buttonId,
|
||||
}: { href: string; siteId: string; buttonId: string }) {
|
||||
const url = new URL(href);
|
||||
url.searchParams.set('utm_source', 'content');
|
||||
url.searchParams.set('utm_medium', 'toolbar');
|
||||
url.searchParams.set('utm_campaign', siteId);
|
||||
url.searchParams.set('utm_content', buttonId);
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
.svgLogo {
|
||||
--logo-fill: var(--color-neutral-100);
|
||||
--seg-A-color: #2782c4;
|
||||
--seg-B-color: #43b7f2;
|
||||
--seg-C-color: #8be2ff;
|
||||
--trace-color: #46474c;
|
||||
|
||||
|
||||
--T: 2s;
|
||||
shape-rendering: geometricPrecision;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.static .trace {
|
||||
animation: none;
|
||||
fill: var(--logo-fill);
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.static .seg {
|
||||
animation: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Base segment animation */
|
||||
.seg {
|
||||
opacity: 0;
|
||||
animation: segFade var(--T) ease both 0.06s, segAMove var(--T) linear,
|
||||
segALen var(--T) linear;
|
||||
animation-delay: 0.06s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.segA {
|
||||
animation-name: segFade, segAMove, segALen;
|
||||
}
|
||||
|
||||
.segB {
|
||||
animation-name: segFade, segBMove, segBLen;
|
||||
}
|
||||
|
||||
.segC {
|
||||
animation-name: segFade, segCMove, segCLen;
|
||||
}
|
||||
|
||||
.trace {
|
||||
animation: traceFill var(--T) ease both 0.06s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes traceFill {
|
||||
0% {
|
||||
fill: transparent;
|
||||
stroke:var(--trace-color);
|
||||
}
|
||||
95% {
|
||||
fill: transparent;
|
||||
stroke:var(--trace-color);
|
||||
}
|
||||
100% {
|
||||
fill: var(--logo-fill);
|
||||
stroke:none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Segment A animation */
|
||||
@keyframes segAMove {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke: var(--seg-A-color);
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: -0.5;
|
||||
stroke: var(--seg-A-color);
|
||||
}
|
||||
95% {
|
||||
stroke-dashoffset: -1;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes segALen {
|
||||
0% {
|
||||
stroke-dasharray: 0.18 0.82;
|
||||
}
|
||||
95% {
|
||||
stroke-dasharray: 0.22 0.78;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Segment B animation */
|
||||
@keyframes segBMove {
|
||||
0% {
|
||||
stroke-dashoffset: -0.18;
|
||||
stroke: var(--seg-B-color);
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: -0.72;
|
||||
stroke: var(--seg-B-color);
|
||||
}
|
||||
95% {
|
||||
stroke-dashoffset: -1.18;
|
||||
stroke: var(--seg-B-color);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes segBLen {
|
||||
0%,
|
||||
50%,
|
||||
95% {
|
||||
stroke-dasharray: 0.2 0.8;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Segment C animation */
|
||||
@keyframes segCMove {
|
||||
0% {
|
||||
stroke-dashoffset: -0.38;
|
||||
stroke: var(--seg-C-color);
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: -0.92;
|
||||
stroke: var(--seg-C-color);
|
||||
}
|
||||
95% {
|
||||
stroke-dashoffset: -1.38;
|
||||
stroke: var(--seg-C-color);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes segCLen {
|
||||
0% {
|
||||
stroke-dasharray: 0.22 0.78;
|
||||
}
|
||||
95% {
|
||||
stroke-dasharray: 0.18 0.82;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Segment fade animation - stays visible at end */
|
||||
@keyframes segFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.seg {
|
||||
animation: none;
|
||||
opacity: 0;
|
||||
}
|
||||
.trace {
|
||||
animation: none;
|
||||
fill: var(--logo-fill);
|
||||
stroke:none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type React from 'react';
|
||||
import styles from './AnimatedLogo.module.css';
|
||||
|
||||
interface AnimatedLogoProps {
|
||||
shouldAnimate?: boolean;
|
||||
}
|
||||
|
||||
export const AnimatedLogo: React.FC<AnimatedLogoProps> = (props) => {
|
||||
const { shouldAnimate = true } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={tcls(styles.svgLogo, shouldAnimate ? undefined : styles.static)}
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Background trace */}
|
||||
<path
|
||||
d={dPath}
|
||||
transform="translate(14,14) scale(0.4)"
|
||||
strokeWidth={3}
|
||||
pathLength={1}
|
||||
strokeLinecap="round"
|
||||
className={styles.trace}
|
||||
/>
|
||||
{/* Three individual paths to create the 'chasing' segmented stroke effect */}
|
||||
<path
|
||||
className={tcls(styles.seg, styles.segA)}
|
||||
d={dPath}
|
||||
transform="translate(14,14) scale(0.4)"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
pathLength={1}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="0.18 0.82"
|
||||
strokeDashoffset={0}
|
||||
/>
|
||||
<path
|
||||
className={tcls(styles.seg, styles.segB)}
|
||||
d={dPath}
|
||||
transform="translate(14,14) scale(0.4)"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
pathLength={1}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="0.20 0.80"
|
||||
strokeDashoffset={-0.18}
|
||||
/>
|
||||
<path
|
||||
className={tcls(styles.seg, styles.segC)}
|
||||
d={dPath}
|
||||
transform="translate(14,14) scale(0.4)"
|
||||
fill="none"
|
||||
strokeWidth={3}
|
||||
pathLength={1}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="0.22 0.78"
|
||||
strokeDashoffset={-0.38}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const dPath =
|
||||
'M-5.07306 1.64898C-1.92626 3.46518 -0.352865 4.37328 1.37504 4.37477C3.10303 4.37628 4.67794 3.47098 7.82794 1.66027C7.82794 1.66027 27.9071 -9.88183 27.9071 -9.88183C28.8136 -10.4029 29.3724 -11.3687 29.3724 -12.4143C29.3724 -13.4598 28.8136 -14.4257 27.9071 -14.9467C27.9071 -14.9467 7.82063 -26.4931 7.82063 -26.4931C4.67414 -28.3018 3.10083 -29.2062 1.37453 -29.2055C-0.351665 -29.2048 -1.92427 -28.2992 -5.06947 -26.488C-5.06947 -26.488 -22.3372 -16.5441 -22.3372 -16.5441C-22.4651 -16.4704 -22.5291 -16.4336 -22.5888 -16.3986C-28.4872 -12.9457 -32.1315 -6.64173 -32.1802 0.192975C-32.1807 0.262075 -32.1807 0.335975 -32.1807 0.483575C-32.1807 0.631075 -32.1807 0.704875 -32.1802 0.773875C-32.1316 7.60087 -28.4955 13.899 -22.6075 17.3547C-22.548 17.3897 -22.4841 17.4266 -22.3564 17.5003C-22.3564 17.5003 -11.5399 23.7454 -11.5399 23.7454C-5.23716 27.3844 -2.08587 29.2039 1.37484 29.2051C4.83554 29.2062 7.98813 27.3889 14.2932 23.7541C14.2932 23.7541 25.7115 17.1718 25.7115 17.1718C28.8686 15.3518 30.4471 14.4418 31.3139 12.9416C32.1807 11.4414 32.1807 9.61938 32.1807 5.97517C32.1807 5.97517 32.1807 -1.06463 32.1807 -1.06463C32.1807 -2.07553 31.6332 -3.00723 30.75 -3.49913C29.8953 -3.97513 28.8536 -3.96802 28.0054 -3.48053C28.0054 -3.48053 4.59224 9.97808 4.59224 9.97808C3.02144 10.8811 2.23593 11.3326 1.37404 11.3329C0.512135 11.3331 -0.273565 10.8821 -1.84506 9.98007C-1.84506 9.98007 -17.6916 0.883775 -17.6916 0.883775C-18.4854 0.428075 -18.8823 0.200275 -19.2011 0.159175C-19.9279 0.065375 -20.6267 0.472475 -20.9036 1.15108C-21.025 1.44858 -21.0225 1.90628 -21.0176 2.82148C-21.014 3.49528 -21.0122 3.83218 -20.9492 4.14208C-20.8082 4.83607 -20.4431 5.46448 -19.91 5.93068C-19.672 6.13888 -19.3802 6.30727 -18.7966 6.64407C-18.7966 6.64407 -1.85397 16.4227 -1.85397 16.4227C-0.278465 17.332 0.509335 17.7867 1.37434 17.7869C2.23934 17.7872 3.02734 17.3329 4.60333 16.4245C4.60333 16.4245 25.3699 4.45377 25.3699 4.45377C25.9083 4.14348 26.1774 3.98828 26.3792 4.10488C26.5811 4.22148 26.5811 4.53218 26.5811 5.15357C26.5811 5.15357 26.5811 8.34667 26.5811 8.34667C26.5811 9.25768 26.5811 9.71317 26.3643 10.0883C26.1476 10.4634 25.753 10.6908 24.9637 11.1458C24.9637 11.1458 7.83524 21.0193 7.83524 21.0193C4.68194 22.837 3.10533 23.7458 1.37463 23.7451C-0.356065 23.7443 -1.93186 22.834 -5.08347 21.0134C-5.08347 21.0134 -21.1086 11.7563 -21.1086 11.7563C-21.1595 11.7269 -21.1849 11.7122 -21.2087 11.6984C-24.5687 9.73487 -26.642 6.14287 -26.6614 2.25128C-26.6616 2.22378 -26.6616 2.19438 -26.6616 2.13567C-26.6616 2.13567 -26.6616 -0.795425 -26.6616 -0.795425C-26.6616 -2.94372 -25.5174 -4.92953 -23.6587 -6.00682C-22.0163 -6.95883 -19.9905 -6.96072 -18.3463 -6.01183C-18.3463 -6.01183 -5.07306 1.64898 -5.07306 1.64898Z';
|
||||
@@ -0,0 +1,232 @@
|
||||
'use client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
import { motion } from 'motion/react';
|
||||
import React, { useRef } from 'react';
|
||||
import { useOnClickOutside } from 'usehooks-ts';
|
||||
import { ToolbarButton, type ToolbarButtonProps } from './Toolbar';
|
||||
import styles from './Toolbar.module.css';
|
||||
import { useToolbarControls } from './ToolbarControlsContext';
|
||||
|
||||
const ARC_DURATION_SECONDS = 0.4;
|
||||
const ARC_STAGGER_MS = 80;
|
||||
const BASE_ROTATION_DEG = 95;
|
||||
const ROTATION_STEP_DEG = 18;
|
||||
|
||||
interface HideToolbarButtonProps {
|
||||
motionValues?: ToolbarButtonProps['motionValues'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide menu trigger. Expands a macOS Dock-like submenu with 3 labeled actions.
|
||||
*/
|
||||
export function HideToolbarButton(props: HideToolbarButtonProps) {
|
||||
const { motionValues } = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const controls = useToolbarControls();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClickOutsideArcMenu = (event: Event) => {
|
||||
// Don't close the arc if we are clicking on the button itself
|
||||
if (buttonRef.current?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
useOnClickOutside(ref, handleClickOutsideArcMenu);
|
||||
|
||||
// Close arc menu on scroll
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const handleScroll = () => setOpen(false);
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [open]);
|
||||
|
||||
const items = [
|
||||
controls?.minimize
|
||||
? {
|
||||
id: 'minimize',
|
||||
icon: 'minus',
|
||||
label: 'Minimize',
|
||||
onClick: controls.minimize,
|
||||
}
|
||||
: null,
|
||||
controls?.closeSession
|
||||
? {
|
||||
id: 'session-close',
|
||||
icon: 'xmark',
|
||||
label: 'Close for one session',
|
||||
onClick: controls.closeSession,
|
||||
}
|
||||
: null,
|
||||
controls?.closePersistent
|
||||
? {
|
||||
id: 'persistent-close',
|
||||
icon: 'ban',
|
||||
label: "Don't show again",
|
||||
onClick: controls.closePersistent,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as Array<ArcMenuItem>;
|
||||
|
||||
const sharedMotionStyle = motionValues
|
||||
? {
|
||||
x: motionValues.x,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
ref={buttonRef}
|
||||
title={open ? 'Hide options' : 'Hide toolbar'}
|
||||
onClick={() => {
|
||||
setOpen((v) => !v);
|
||||
}}
|
||||
motionValues={motionValues}
|
||||
icon="eye-slash"
|
||||
>
|
||||
{/* Expanding arc menu */}
|
||||
{open && (
|
||||
<motion.div
|
||||
className={tcls('pointer-events-none absolute inset-0', styles.arcMenu)}
|
||||
style={sharedMotionStyle as React.CSSProperties | undefined}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'pointer-events-none absolute left-0 overflow-visible',
|
||||
styles.arcMenuPath
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<ArcToolbarButton
|
||||
index={index}
|
||||
staggerIndex={items.length - 1 - index}
|
||||
key={item.icon}
|
||||
{...item}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
item.onClick?.();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
||||
|
||||
type ArcMenuItem = {
|
||||
id: string;
|
||||
icon: IconName;
|
||||
label: string;
|
||||
description: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type ArcToolbarButtonProps = Pick<ArcMenuItem, 'label' | 'icon' | 'onClick'> & {
|
||||
index: number;
|
||||
staggerIndex?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
export function ArcToolbarButton(props: ArcToolbarButtonProps) {
|
||||
const {
|
||||
index,
|
||||
staggerIndex = index,
|
||||
label,
|
||||
disabled,
|
||||
className,
|
||||
onClick = () => {},
|
||||
icon,
|
||||
iconClassName,
|
||||
} = props;
|
||||
|
||||
const targetOffset = `calc(var(--start-distance) + ${index} * var(--spread-distance))`;
|
||||
|
||||
// Calculate rotation based on position along the arc
|
||||
const calculateRotation = () => {
|
||||
return BASE_ROTATION_DEG - index * ROTATION_STEP_DEG;
|
||||
};
|
||||
|
||||
const itemRotation = calculateRotation();
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClick();
|
||||
}}
|
||||
style={
|
||||
{
|
||||
'--target-offset-distance': targetOffset,
|
||||
'--arc-duration': `${ARC_DURATION_SECONDS}s`,
|
||||
'--arc-delay': `${(staggerIndex ?? 0) * ARC_STAGGER_MS}ms`,
|
||||
'--rotation-offset': `${itemRotation}deg`,
|
||||
offsetPath: 'border-box',
|
||||
offsetDistance: targetOffset,
|
||||
offsetAnchor: '0% 40%',
|
||||
offsetRotate: `auto ${itemRotation}deg`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={tcls(
|
||||
'group',
|
||||
'absolute',
|
||||
'top-0',
|
||||
'left-0',
|
||||
'w-40',
|
||||
'opacity-0',
|
||||
'pointer-events-auto',
|
||||
'flex',
|
||||
'items-center',
|
||||
'gap-2',
|
||||
styles.arcMenuItem,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'flex shrink-0 items-center justify-center gap-1',
|
||||
'h-8 w-8 rounded-full border',
|
||||
'truncate text-sm',
|
||||
'cursor-pointer transition-colors',
|
||||
'group-hover:-rotate-5 group-hover:scale-105',
|
||||
disabled ? 'cursor-not-allowed opacity-50' : '',
|
||||
'text-tint-1 dark:text-tint-12',
|
||||
'bg-[linear-gradient(110deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]',
|
||||
'dark:[background:linear-gradient(110deg,rgba(255,255,255,1)_0%,rgba(240,246,248,1)_100%)]',
|
||||
'border border-solid dark:border-[rgba(256,_256,_256,_0.06)]'
|
||||
)}
|
||||
style={{
|
||||
background: 'linear-gradient(rgb(51, 53, 57), rgb(50, 52, 56))',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={icon as IconName}
|
||||
iconStyle={IconStyle.Solid}
|
||||
className={tcls('size-4 shrink-0 group-hover:scale-110', iconClassName)}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={tcls(
|
||||
'whitespace-nowrap rounded-lg px-3 py-1 font-normal text-sm transition-transform',
|
||||
'group-hover:rotate-2 group-hover:scale-105',
|
||||
'text-neutral-1 dark:text-neutral-12',
|
||||
'bg-[linear-gradient(110deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'use client';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { useCheckForContentUpdate } from '@/components/AutoRefreshContent';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { ToolbarButton } from './Toolbar';
|
||||
|
||||
// We don't show the button if the content has been updated 30s ago or less.
|
||||
const minInterval = 1000 * 30; // 5 minutes
|
||||
|
||||
/**
|
||||
* Button to refresh the page if the content has been updated.
|
||||
*/
|
||||
export function RefreshChangeRequestButton(props: {
|
||||
spaceId: string;
|
||||
changeRequestId: string;
|
||||
revisionId: string;
|
||||
updatedAt: number;
|
||||
}) {
|
||||
const { updatedAt } = props;
|
||||
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const checkForUpdates = useCheckForContentUpdate(props);
|
||||
|
||||
const refresh = React.useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await checkForUpdates();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
}
|
||||
}, [checkForUpdates]);
|
||||
|
||||
// Show the button if the content has been updated more than 30s ago.
|
||||
React.useEffect(() => {
|
||||
if (updatedAt < Date.now() - minInterval) {
|
||||
setVisible(true);
|
||||
}
|
||||
}, [updatedAt]);
|
||||
|
||||
// 30sec after being hidden, we show the button again
|
||||
React.useEffect(() => {
|
||||
if (!visible) {
|
||||
const timeout = setTimeout(() => {
|
||||
setVisible(true);
|
||||
}, minInterval);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
title="Refresh"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
<Icon icon="rotate" className={tcls('size-4', loading ? 'animate-spin' : null)} />
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
|
||||
import { ToolbarButton, type ToolbarButtonProps } from './Toolbar';
|
||||
|
||||
/**
|
||||
* Button to refresh the page if the content has been updated.
|
||||
*/
|
||||
export function RefreshContentButton(props: {
|
||||
refreshForUpdates: () => Promise<void>;
|
||||
motionValues?: ToolbarButtonProps['motionValues'];
|
||||
}) {
|
||||
const { refreshForUpdates, motionValues } = props;
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const refresh = React.useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await refreshForUpdates();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [refreshForUpdates]);
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
title="Refresh for latest changes"
|
||||
onClick={(event) => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
refresh();
|
||||
}}
|
||||
disabled={loading}
|
||||
motionValues={motionValues}
|
||||
icon="rotate"
|
||||
iconClassName={loading ? 'animate-spin' : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
.arcMenu {
|
||||
--arc-width: 505px;
|
||||
--arc-height: 400px;
|
||||
--arc-radius: 34%;
|
||||
--start-distance: -240px;
|
||||
--spread-distance: 45px;
|
||||
}
|
||||
|
||||
.arcMenuPath {
|
||||
bottom: calc(var(--arc-height) / -2);
|
||||
width: var(--arc-width);
|
||||
height: var(--arc-height);
|
||||
border-radius: var(--arc-radius);
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.arcMenuItem {
|
||||
animation-name: hide-toolbar-arc-enter;
|
||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
animation-fill-mode: forwards;
|
||||
animation-duration: var(--arc-duration, 0.4s);
|
||||
animation-delay: var(--arc-delay, 0s);
|
||||
transform-origin: center left;
|
||||
offset-path: border-box;
|
||||
offset-anchor: 0% 0%;
|
||||
offset-rotate: auto var(--rotation-offset, 0deg);
|
||||
}
|
||||
|
||||
@keyframes hide-toolbar-arc-enter {
|
||||
from {
|
||||
offset-distance: var(--start-distance);
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
offset-distance: var(--target-offset-distance);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,343 @@
|
||||
'use client';
|
||||
|
||||
import type * as React from 'react';
|
||||
import {
|
||||
AnimatePresence,
|
||||
type MotionValue,
|
||||
motion,
|
||||
useReducedMotion,
|
||||
useSpring,
|
||||
} from 'motion/react';
|
||||
import React from 'react';
|
||||
import { AnimatedLogo } from './AnimatedLogo';
|
||||
import { useToolbarControls } from './ToolbarControlsContext';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
import { Tooltip } from '../primitives';
|
||||
import { getCopyVariants, toolbarEasings } from './transitions';
|
||||
import { useMagnificationEffect } from './useMagnificationEffect';
|
||||
|
||||
export function Toolbar(props: { children: React.ReactNode }) {
|
||||
const { children } = props;
|
||||
const DURATION_LOGO_APPEARANCE = 2000;
|
||||
const DELAY_BETWEEN_LOGO_AND_CONTENT = 100;
|
||||
|
||||
interface ToolbarProps {
|
||||
label: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
minified: boolean;
|
||||
onMinifiedChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function Toolbar(props: ToolbarProps) {
|
||||
const { children, label, minified, onMinifiedChange } = props;
|
||||
const controls = useToolbarControls();
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
const autoExpandTriggeredRef = React.useRef(false);
|
||||
|
||||
const shouldAutoExpand = Boolean(controls?.shouldAutoExpand);
|
||||
const [shouldAnimateLogo, setShouldAnimateLogo] = React.useState(shouldAutoExpand);
|
||||
|
||||
// Wait for page to be ready, then show the toolbar
|
||||
React.useEffect(() => {
|
||||
const handleLoad = () => {
|
||||
setIsReady(true);
|
||||
};
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
handleLoad();
|
||||
} else {
|
||||
window.addEventListener('load', handleLoad);
|
||||
return () => window.removeEventListener('load', handleLoad);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// After toolbar appears, wait, then show the full content
|
||||
React.useEffect(() => {
|
||||
if (!isReady || autoExpandTriggeredRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldAutoExpand) {
|
||||
// When we already know the toolbar should stay expanded (e.g. the user previously
|
||||
// opened it this session) we short-circuit the auto-expand animation and immediately
|
||||
// render the expanded state without replaying the logo animation.
|
||||
autoExpandTriggeredRef.current = true;
|
||||
setShouldAnimateLogo(false);
|
||||
return;
|
||||
}
|
||||
|
||||
autoExpandTriggeredRef.current = true;
|
||||
|
||||
// On a fresh session we let the toolbar appear in its compact form, play the logo
|
||||
// animation, and only then expand the toolbar. The timeout mirrors the duration of the
|
||||
// logo animation so both transitions feel connected.
|
||||
const expandAfterTimeout = setTimeout(() => {
|
||||
setShouldAnimateLogo(false);
|
||||
onMinifiedChange(false);
|
||||
}, DURATION_LOGO_APPEARANCE + DELAY_BETWEEN_LOGO_AND_CONTENT);
|
||||
|
||||
return () => clearTimeout(expandAfterTimeout);
|
||||
}, [isReady, onMinifiedChange, shouldAutoExpand]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!minified) {
|
||||
// Any manual expansion should stop the logo animation so the icon stays in its
|
||||
// “settled” state once the toolbar is open.
|
||||
setShouldAnimateLogo(false);
|
||||
}
|
||||
}, [minified]);
|
||||
|
||||
// Don't render anything until page is ready
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'gap-4',
|
||||
'text-sm',
|
||||
'px-4',
|
||||
'py-1',
|
||||
'rounded-full',
|
||||
'truncate',
|
||||
'text-tint-1',
|
||||
'dark:text-tint-12'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<Tooltip label={label}>
|
||||
<motion.div className="-translate-x-1/2 fixed bottom-5 left-1/2 z-40 w-auto max-w-xl transform px-4">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
onClick={() => {
|
||||
if (minified) {
|
||||
setShouldAnimateLogo(false);
|
||||
onMinifiedChange(false);
|
||||
}
|
||||
}}
|
||||
layout
|
||||
transition={toolbarEasings.spring}
|
||||
className={tcls(
|
||||
minified ? 'cursor-pointer px-2' : 'pr-2 pl-3.5',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'min-h-11',
|
||||
'min-w-12',
|
||||
'h-12',
|
||||
'py-2',
|
||||
'backdrop-blur-sm',
|
||||
'origin-center',
|
||||
'border-[0.5px] border-neutral-5 border-solid dark:border-neutral-8',
|
||||
'bg-[linear-gradient(45deg,rgba(39,39,39,0.8)_100%,rgba(39,39,39,0.4)_80%)]',
|
||||
'dark:bg-[linear-gradient(45deg,rgba(39,39,39,0.5)_100%,rgba(39,39,39,0.3)_80%)]'
|
||||
)}
|
||||
style={{
|
||||
borderRadius: '100px', // This is set on `style` so Framer Motion can correct for distortions
|
||||
}}
|
||||
>
|
||||
{/* Logo with stroke segments animation in blue-tints */}
|
||||
<motion.div layout>
|
||||
<AnimatedLogo shouldAnimate={shouldAnimateLogo} />
|
||||
</motion.div>
|
||||
|
||||
{!minified ? children : null}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarBody(props: { children: React.ReactNode }) {
|
||||
return <div className="flex flex-col gap-1">{props.children}</div>;
|
||||
return <div className="flex flex-col items-start px-3">{props.children}</div>;
|
||||
}
|
||||
|
||||
export function ToolbarButtonGroups(props: { children: React.ReactNode }) {
|
||||
return <div className="flex flex-row gap-2">{props.children}</div>;
|
||||
}
|
||||
export function ToolbarButtonGroup(props: { children: React.ReactNode }) {
|
||||
const { children } = props;
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const buttonChildren = React.Children.toArray(children).filter((child) => !!child);
|
||||
const { buttonMotionValues } = useMagnificationEffect({
|
||||
childrenCount: buttonChildren.length,
|
||||
containerRef,
|
||||
});
|
||||
|
||||
export function ToolbarButton(props: React.HTMLProps<HTMLAnchorElement>) {
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'size-11',
|
||||
'gap-1',
|
||||
'text-sm',
|
||||
'rounded-full',
|
||||
'truncate',
|
||||
'text-tint-1', // Equal to text-tint-strong in dark mode
|
||||
'dark:text-tint-12', // Equal to text-tint-strong in dark mode
|
||||
'hover:bg-tint-12',
|
||||
'dark:hover:bg-tint-1',
|
||||
'hover:shadow-lg',
|
||||
'cursor-pointer'
|
||||
)}
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
variants={toolbarEasings.parent}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="flex items-center gap-1 overflow-visible pr-2 pl-4"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
{buttonChildren.map((child, index) => {
|
||||
const childEl = child as React.ReactElement;
|
||||
const childKey = childEl.key ?? `toolbar-button-${index}`;
|
||||
return (
|
||||
<ToolbarButtonWrapper
|
||||
key={childKey}
|
||||
child={childEl}
|
||||
rawMotionValues={buttonMotionValues[index]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ToolbarButtonProps extends Omit<React.HTMLProps<HTMLAnchorElement>, 'title'> {
|
||||
motionValues?: {
|
||||
scale: MotionValue<number>;
|
||||
x: MotionValue<number>;
|
||||
};
|
||||
icon: IconName;
|
||||
iconClassName?: string;
|
||||
title?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps>((props, ref) => {
|
||||
const {
|
||||
title,
|
||||
disabled,
|
||||
motionValues,
|
||||
className,
|
||||
style,
|
||||
href,
|
||||
onClick,
|
||||
icon,
|
||||
iconClassName,
|
||||
children,
|
||||
} = props;
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<motion.div variants={toolbarEasings.staggeringChild} className="relative" ref={ref}>
|
||||
{children ? children : null}
|
||||
<Tooltip label={title}>
|
||||
<motion.a
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={
|
||||
reduceMotion
|
||||
? undefined
|
||||
: {
|
||||
scale: motionValues?.scale,
|
||||
x: motionValues?.x,
|
||||
transformOrigin: 'bottom center',
|
||||
zIndex: motionValues?.scale ? 10 : 'auto',
|
||||
...style,
|
||||
}
|
||||
}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
className={tcls(
|
||||
'toolbar-button',
|
||||
className,
|
||||
'flex',
|
||||
'relative',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'gap-1',
|
||||
'text-sm',
|
||||
'rounded-full',
|
||||
'truncate',
|
||||
'text-tint-1',
|
||||
'dark:text-tint-12',
|
||||
'cursor-pointer',
|
||||
'transition-colors',
|
||||
'size-8',
|
||||
disabled ? 'cursor-not-allowed opacity-50' : '',
|
||||
'border border-[rgba(256,_256,_256,_0.06)] border-solid',
|
||||
'bg-[linear-gradient(45deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={icon}
|
||||
iconStyle={IconStyle.Solid}
|
||||
className={tcls(
|
||||
'size-4 shrink-0 group-hover:scale-110 group-hover:text-tint-3',
|
||||
iconClassName
|
||||
)}
|
||||
/>
|
||||
</motion.a>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
||||
ToolbarButton.displayName = 'ToolbarButton';
|
||||
|
||||
function ToolbarButtonWrapper(props: {
|
||||
child: React.ReactElement;
|
||||
rawMotionValues?: { scale: MotionValue<number>; x: MotionValue<number> };
|
||||
}) {
|
||||
const { child, rawMotionValues } = props;
|
||||
|
||||
// Convert the raw motion values to smooth spring easings
|
||||
const springScale = useSpring(rawMotionValues?.scale.get() ?? 1, {
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
});
|
||||
const springX = useSpring(rawMotionValues?.x.get() ?? 0, { stiffness: 400, damping: 30 });
|
||||
|
||||
// Sync springs with raw motion values
|
||||
React.useEffect(() => {
|
||||
if (!rawMotionValues) return;
|
||||
|
||||
const unsubScale = rawMotionValues.scale.on('change', (v) => springScale.set(v));
|
||||
const unsubX = rawMotionValues.x.on('change', (v) => springX.set(v));
|
||||
|
||||
return () => {
|
||||
unsubScale();
|
||||
unsubX();
|
||||
};
|
||||
}, [rawMotionValues, springScale, springX]);
|
||||
|
||||
const motionValues = {
|
||||
scale: springScale,
|
||||
x: springX,
|
||||
};
|
||||
|
||||
return React.cloneElement(child, {
|
||||
motionValues,
|
||||
});
|
||||
}
|
||||
|
||||
export function ToolbarSeparator() {
|
||||
return <div className="h-5 w-px bg-tint-1/3" />;
|
||||
}
|
||||
|
||||
export function ToolbarTitle(props: { prefix?: string; suffix: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-xs ">
|
||||
{props.prefix ? <ToolbarTitlePrefix title={props.prefix} /> : null}
|
||||
<ToolbarTitleSuffix title={props.suffix} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarTitlePrefix(props: { title: string }) {
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(0)}
|
||||
className="truncate font-medium text-neutral-1 dark:text-neutral-12"
|
||||
>
|
||||
{props.title}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarTitleSuffix(props: { title: string }) {
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(1)}
|
||||
className="max-w-[20ch] truncate text-neutral-1 dark:text-neutral-12"
|
||||
>
|
||||
{props.title}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarSubtitle(props: { subtitle: React.ReactNode }) {
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(1)}
|
||||
className="text-neutral-1/80 text-xxs dark:text-neutral-12/80"
|
||||
>
|
||||
{props.subtitle}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
|
||||
export interface ToolbarControlsContextValue {
|
||||
minimize: () => void;
|
||||
closeSession?: () => void;
|
||||
closePersistent?: () => void;
|
||||
shouldAutoExpand?: boolean;
|
||||
}
|
||||
|
||||
const ToolbarControlsContext = React.createContext<ToolbarControlsContextValue | null>(null);
|
||||
|
||||
/*
|
||||
* Provides reusable state setters (mainly for hiding/showing the toolbar) for the toolbar controls propagated through to the children
|
||||
*/
|
||||
export function ToolbarControlsProvider(
|
||||
props: React.PropsWithChildren<{ value: ToolbarControlsContextValue | null }>
|
||||
) {
|
||||
const { children, value } = props;
|
||||
return (
|
||||
<ToolbarControlsContext.Provider value={value}>{children}</ToolbarControlsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useToolbarControls() {
|
||||
return React.useContext(ToolbarControlsContext);
|
||||
}
|
||||
@@ -1,2 +1,6 @@
|
||||
export * from './AdminToolbar';
|
||||
export * from './AdminToolbarClient';
|
||||
export * from './IframeWrapper';
|
||||
export * from './Toolbar';
|
||||
export * from './transitions';
|
||||
export * from './utils';
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Transition } from 'motion/react';
|
||||
|
||||
// Slight bounce but minimal overshoot esp. at the minimized state.
|
||||
const spring = {
|
||||
type: 'spring' as const,
|
||||
stiffness: 130,
|
||||
damping: 19,
|
||||
mass: 1,
|
||||
};
|
||||
|
||||
const parent = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
delayChildren: 0.4,
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const staggeringChild = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
scale: 0.7,
|
||||
},
|
||||
show: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
type: 'spring',
|
||||
} as Transition,
|
||||
},
|
||||
};
|
||||
|
||||
export const minifyButtonAnimation = {
|
||||
initial: {
|
||||
scale: 0.5,
|
||||
opacity: 0,
|
||||
},
|
||||
animate: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
},
|
||||
exit: {
|
||||
scale: 0.5,
|
||||
opacity: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const getCopyVariants = (position: number) => {
|
||||
return {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
x: -10,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
},
|
||||
transition: {
|
||||
duration: 0.1,
|
||||
delay: position / 10 + 0.3,
|
||||
} as Transition,
|
||||
};
|
||||
};
|
||||
|
||||
export const toolbarEasings = {
|
||||
spring,
|
||||
parent,
|
||||
staggeringChild,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Minimal types containing only the fields needed for AdminToolbar to restrict what gets serialized
|
||||
*/
|
||||
|
||||
export type MinimalChangeRequest = {
|
||||
id: string;
|
||||
number: number;
|
||||
subject: string | null;
|
||||
revision: string;
|
||||
updatedAt: string;
|
||||
createdBy: {
|
||||
displayName: string;
|
||||
};
|
||||
urls: {
|
||||
app: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type MinimalRevision = {
|
||||
createdAt: string;
|
||||
urls: {
|
||||
app: string;
|
||||
};
|
||||
git?: {
|
||||
url: string | undefined;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type MinimalSpace = {
|
||||
id: string;
|
||||
revision: string;
|
||||
urls: {
|
||||
app: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type MinimalSite = {
|
||||
id: string;
|
||||
title: string;
|
||||
urls: {
|
||||
app: string;
|
||||
published: string | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export type AdminToolbarContext = {
|
||||
organizationId: string;
|
||||
revisionId: string;
|
||||
space: MinimalSpace;
|
||||
changeRequest: MinimalChangeRequest | null;
|
||||
revision: MinimalRevision;
|
||||
site: MinimalSite;
|
||||
};
|
||||
|
||||
export interface AdminToolbarClientProps {
|
||||
context: AdminToolbarContext;
|
||||
onSessionClose?: () => void;
|
||||
onPersistentClose?: () => void;
|
||||
onToggleMinify?: () => void;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { type MotionValue, motionValue } from 'motion/react';
|
||||
import React from 'react';
|
||||
|
||||
interface MagnificationConfig {
|
||||
/** Size of each button in pixels - used for spacing calculations */
|
||||
buttonSize?: number;
|
||||
/** Maximum scale factor for buttons when mouse is directly over them */
|
||||
maxScale?: number;
|
||||
/** Distance in pixels from mouse where buttons start scaling */
|
||||
influenceRadius?: number;
|
||||
/** Multiplier for spacing between buttons - higher values create more space */
|
||||
spacingMultiplier?: number;
|
||||
/** Controls scaling curve - higher values make scaling more dramatic near mouse */
|
||||
scaleExponent?: number;
|
||||
/** Padding around container for mouse detection in pixels */
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
const defaultConfig: Required<MagnificationConfig> = {
|
||||
buttonSize: 32, // Standard button size for 32px (size-8) buttons
|
||||
maxScale: 1.25, // 25% scale increase - noticeable but not overwhelming
|
||||
influenceRadius: 80, // ~2.5 button widths of influence
|
||||
spacingMultiplier: 1.3, // Creates proportional spacing to scale increase
|
||||
scaleExponent: 2.5, // Exponential curve for dramatic close-range scaling
|
||||
padding: 10, // Small buffer zone around container edges
|
||||
};
|
||||
|
||||
const resetMotionValues = (
|
||||
motionValues: Array<{ scale: MotionValue<number>; x: MotionValue<number> }>
|
||||
) => {
|
||||
motionValues.forEach(({ scale, x }) => {
|
||||
scale.set(1);
|
||||
x.set(0);
|
||||
});
|
||||
};
|
||||
|
||||
const captureButtonPositions = (buttons: HTMLElement[]) => {
|
||||
// Reset transforms to get original positions
|
||||
buttons.forEach((button) => {
|
||||
button.style.transform = '';
|
||||
});
|
||||
return buttons.map((button) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
top: rect.top,
|
||||
height: rect.height,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const calculateScale = (
|
||||
mouseX: number,
|
||||
buttonCenterX: number,
|
||||
containerRect: DOMRect,
|
||||
config: Required<MagnificationConfig>
|
||||
) => {
|
||||
// Calculate only X-axis distance from mouse to button center
|
||||
const distance = Math.abs(mouseX - buttonCenterX);
|
||||
|
||||
if (distance > config.influenceRadius) return 1;
|
||||
|
||||
// Calculate distance from container edge (X-axis only)
|
||||
const distanceFromEdgeX = Math.min(mouseX - containerRect.left, containerRect.right - mouseX);
|
||||
const distanceFromEdge = distanceFromEdgeX;
|
||||
|
||||
// Define the "heart" zone - 8px from center (16x16px total area)
|
||||
const heartZoneRadius = 8;
|
||||
|
||||
let baseScale: number;
|
||||
|
||||
if (distance <= heartZoneRadius) {
|
||||
// Within the heart zone - always at max scale
|
||||
baseScale = config.maxScale;
|
||||
} else {
|
||||
// Outside heart zone - scale from max to 1 based on distance
|
||||
const outerRadius = config.influenceRadius - heartZoneRadius;
|
||||
const outerDistance = distance - heartZoneRadius;
|
||||
const progress = Math.min(outerDistance / outerRadius, 1);
|
||||
const exponentialProgress = (1 - progress) ** config.scaleExponent;
|
||||
baseScale = 1 + (config.maxScale - 1) * exponentialProgress;
|
||||
}
|
||||
|
||||
// Only apply entry transition if we're very close to the edge (within 10px)
|
||||
// This prevents the entry logic from interfering with normal scaling
|
||||
if (distanceFromEdge < 10) {
|
||||
const entryProgress = distanceFromEdge / 10;
|
||||
return 1 + (baseScale - 1) * entryProgress;
|
||||
}
|
||||
|
||||
return baseScale;
|
||||
};
|
||||
|
||||
const calculateSpacing = (
|
||||
buttonIndex: number,
|
||||
buttonEffects: Array<{ scale: number; translateX: number }>,
|
||||
positions: Array<{ left: number; width: number; top: number; height: number }>,
|
||||
config: Required<MagnificationConfig>
|
||||
) => {
|
||||
const currentPos = positions[buttonIndex];
|
||||
if (!currentPos) return 0;
|
||||
|
||||
const buttonCenterX = currentPos.left + currentPos.width / 2;
|
||||
let leftPush = 0;
|
||||
let rightPush = 0;
|
||||
|
||||
buttonEffects.forEach((otherEffect, otherIndex) => {
|
||||
if (otherIndex === buttonIndex) return;
|
||||
|
||||
const otherPos = positions[otherIndex];
|
||||
if (!otherPos) return;
|
||||
|
||||
const otherCenterX = otherPos.left + otherPos.width / 2;
|
||||
const distanceBetween = Math.abs(buttonCenterX - otherCenterX);
|
||||
|
||||
// Only apply influence if close enough
|
||||
if (distanceBetween < config.influenceRadius * 1.5) {
|
||||
const influenceStrength = Math.max(
|
||||
0,
|
||||
1 - distanceBetween / (config.influenceRadius * 1.5)
|
||||
);
|
||||
const extraSpace = (otherEffect.scale - 1) * config.buttonSize;
|
||||
const pushAmount = (extraSpace / 2) * config.spacingMultiplier * influenceStrength;
|
||||
|
||||
if (otherCenterX < buttonCenterX) {
|
||||
rightPush += pushAmount;
|
||||
} else {
|
||||
leftPush += pushAmount;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return rightPush - leftPush;
|
||||
};
|
||||
|
||||
export function useMagnificationEffect(props: {
|
||||
childrenCount: number;
|
||||
containerRef: React.RefObject<HTMLElement>;
|
||||
config?: MagnificationConfig;
|
||||
}) {
|
||||
const { childrenCount, containerRef, config } = props;
|
||||
const originalPositionsRef = React.useRef<
|
||||
Array<{ left: number; width: number; top: number; height: number }>
|
||||
>([]);
|
||||
|
||||
const finalConfig = React.useMemo(() => ({ ...defaultConfig, ...config }), [config]);
|
||||
|
||||
// Create basic motion values that will be consumed by springs in the components
|
||||
const buttonMotionValues = React.useMemo(() => {
|
||||
return Array.from({ length: childrenCount }, () => ({
|
||||
scale: motionValue(1),
|
||||
x: motionValue(0),
|
||||
}));
|
||||
}, [childrenCount]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const buttons = Array.from(container.querySelectorAll('.toolbar-button')) as HTMLElement[];
|
||||
|
||||
if (buttons.length !== childrenCount) {
|
||||
console.error(
|
||||
`Button count (${buttons.length}) does not match children count (${childrenCount})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const buttons = Array.from(
|
||||
container.querySelectorAll('.toolbar-button')
|
||||
) as HTMLElement[];
|
||||
|
||||
// Capture positions if needed
|
||||
if (originalPositionsRef.current.length !== buttons.length) {
|
||||
originalPositionsRef.current = captureButtonPositions(buttons);
|
||||
}
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const { clientX: mouseX, clientY: mouseY } = event;
|
||||
|
||||
// Check if mouse is in range
|
||||
const isInRange =
|
||||
mouseX >= containerRect.left - finalConfig.padding &&
|
||||
mouseX <= containerRect.right + finalConfig.padding &&
|
||||
mouseY >= containerRect.top - finalConfig.padding &&
|
||||
mouseY <= containerRect.bottom + finalConfig.padding;
|
||||
|
||||
if (!isInRange) {
|
||||
resetMotionValues(buttonMotionValues);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate scales for all buttons
|
||||
const buttonEffects = buttons.map((_, index) => {
|
||||
const pos = originalPositionsRef.current[index];
|
||||
if (!pos) return { scale: 1, translateX: 0 };
|
||||
|
||||
const buttonCenterX = pos.left + pos.width / 2;
|
||||
const scale = calculateScale(mouseX, buttonCenterX, containerRect, finalConfig);
|
||||
return { scale, translateX: 0 };
|
||||
});
|
||||
|
||||
// Calculate spacing for all buttons
|
||||
buttonEffects.forEach((effect, index) => {
|
||||
effect.translateX = calculateSpacing(
|
||||
index,
|
||||
buttonEffects,
|
||||
originalPositionsRef.current,
|
||||
finalConfig
|
||||
);
|
||||
});
|
||||
|
||||
// Update motion values
|
||||
buttonEffects.forEach((effect, index) => {
|
||||
const motionValue = buttonMotionValues[index];
|
||||
if (motionValue) {
|
||||
motionValue.scale.set(effect.scale);
|
||||
motionValue.x.set(effect.translateX);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => resetMotionValues(buttonMotionValues);
|
||||
|
||||
container.addEventListener('mousemove', handleMouseMove);
|
||||
container.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('mousemove', handleMouseMove);
|
||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}, [finalConfig, containerRef, buttonMotionValues, childrenCount]);
|
||||
|
||||
return { buttonMotionValues };
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
getLocalStorageItem,
|
||||
getSessionStorageItem,
|
||||
removeSessionStorageItem,
|
||||
setLocalStorageItem,
|
||||
setSessionStorageItem,
|
||||
} from '@/lib/browser/local-storage';
|
||||
|
||||
const STORAGE_KEY = 'gitbook_toolbar_closed';
|
||||
const SESSION_STORAGE_KEY = 'gitbook_toolbar_session_closed';
|
||||
const SESSION_MINIFIED_KEY = 'gitbook_toolbar_minified';
|
||||
|
||||
type SessionHideReason = 'session' | 'persistent';
|
||||
|
||||
/**
|
||||
* Read the current session hide state. We store whether the toolbar was hidden “for session” or
|
||||
* “persistently” so we can distinguish between a temporary dismissal and a user preference.
|
||||
*/
|
||||
export const getSessionHideState = (): { hidden: boolean; reason?: SessionHideReason } => {
|
||||
const value = getSessionStorageItem<SessionHideReason | null>(SESSION_STORAGE_KEY, null);
|
||||
if (value === 'session' || value === 'persistent') {
|
||||
return { hidden: true, reason: value };
|
||||
}
|
||||
return { hidden: false, reason: undefined };
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist the session hide state. Passing `false` clears the stored preference entirely.
|
||||
*/
|
||||
export const setSessionHidden = (value: boolean, reason?: SessionHideReason) => {
|
||||
if (value && reason) {
|
||||
setSessionStorageItem(SESSION_STORAGE_KEY, reason);
|
||||
} else {
|
||||
removeSessionStorageItem(SESSION_STORAGE_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the last minified state from session storage. If no value was ever stored we return
|
||||
* `undefined`, which signals that the toolbar should auto-expand once the logo animation finishes.
|
||||
*/
|
||||
export const getStoredMinified = (): boolean | undefined => {
|
||||
const stored = getSessionStorageItem<boolean | null>(SESSION_MINIFIED_KEY, null);
|
||||
if (stored === null || stored === undefined) {
|
||||
removeSessionStorageItem(SESSION_MINIFIED_KEY);
|
||||
return undefined;
|
||||
}
|
||||
return stored;
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist the current minified state for the ongoing session.
|
||||
*/
|
||||
export const setStoredMinified = (value: boolean) => {
|
||||
setSessionStorageItem(SESSION_MINIFIED_KEY, value);
|
||||
};
|
||||
|
||||
interface UseToolbarVisibilityOptions {
|
||||
onPersistentClose?: () => void;
|
||||
onSessionClose?: () => void;
|
||||
onToggleMinify?: () => void;
|
||||
}
|
||||
|
||||
export function useToolbarVisibility(options: UseToolbarVisibilityOptions = {}) {
|
||||
const { onPersistentClose, onSessionClose, onToggleMinify } = options;
|
||||
|
||||
const [initialStoredMinified] = React.useState<boolean | undefined>(() =>
|
||||
typeof window !== 'undefined' ? getStoredMinified() : undefined
|
||||
);
|
||||
const shouldAutoExpand = initialStoredMinified === undefined;
|
||||
|
||||
const [minified, setMinifiedState] = React.useState<boolean>(() =>
|
||||
initialStoredMinified !== undefined ? initialStoredMinified : true
|
||||
);
|
||||
|
||||
const [persistentHidden, setPersistentHidden] = React.useState(() =>
|
||||
getLocalStorageItem(STORAGE_KEY, false)
|
||||
);
|
||||
|
||||
const [sessionReason, setSessionReason] = React.useState<SessionHideReason | undefined>(() => {
|
||||
const state = getSessionHideState();
|
||||
return state.hidden ? state.reason : undefined;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleStorage = () => {
|
||||
setPersistentHidden(getLocalStorageItem(STORAGE_KEY, false));
|
||||
const state = getSessionHideState();
|
||||
setSessionReason(state.hidden ? state.reason : undefined);
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorage);
|
||||
return () => window.removeEventListener('storage', handleStorage);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (persistentHidden) {
|
||||
if (sessionReason !== 'persistent') {
|
||||
// Sync the session flag so we honour the persistent preference and avoid flashes when
|
||||
// opening new tabs in the same browsing session.
|
||||
setSessionHidden(true, 'persistent');
|
||||
setSessionReason('persistent');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionReason === 'persistent') {
|
||||
// If the persistent preference was cleared we also clear the session flag so the toolbar
|
||||
// can reappear immediately without requiring a full reload.
|
||||
setSessionHidden(false);
|
||||
setSessionReason(undefined);
|
||||
}
|
||||
}, [persistentHidden, sessionReason]);
|
||||
|
||||
const setMinified = (value: boolean) => {
|
||||
setMinifiedState(value);
|
||||
setStoredMinified(value);
|
||||
onToggleMinify?.();
|
||||
};
|
||||
|
||||
const minimize = () => setMinified(true);
|
||||
|
||||
const closeSession = () => {
|
||||
setSessionHidden(true, 'session');
|
||||
setSessionReason('session');
|
||||
onSessionClose?.();
|
||||
};
|
||||
|
||||
const closePersistent = () => {
|
||||
setLocalStorageItem(STORAGE_KEY, true);
|
||||
setPersistentHidden(true);
|
||||
setSessionHidden(true, 'persistent');
|
||||
setSessionReason('persistent');
|
||||
onPersistentClose?.();
|
||||
};
|
||||
|
||||
const hidden = persistentHidden || sessionReason === 'session';
|
||||
|
||||
return {
|
||||
minified,
|
||||
setMinified,
|
||||
shouldAutoExpand,
|
||||
hidden,
|
||||
minimize,
|
||||
closeSession,
|
||||
closePersistent,
|
||||
};
|
||||
}
|
||||
@@ -1,28 +1,43 @@
|
||||
'use server';
|
||||
|
||||
import { getDataOrNull } from '@/lib/data';
|
||||
import { getSiteURLDataFromMiddleware } from '@/lib/middleware';
|
||||
import { getServerActionBaseContext } from '@/lib/server-actions';
|
||||
import { traceErrorOnly } from '@/lib/tracing';
|
||||
|
||||
/**
|
||||
* Return true if a change-request has been updated.
|
||||
* Return true if the content has been updated.
|
||||
* It compares the revision ID displayed on the client side with the latest revision ID from the server/cache.
|
||||
*/
|
||||
export async function hasContentBeenUpdated(props: {
|
||||
spaceId: string;
|
||||
changeRequestId: string;
|
||||
revisionId: string;
|
||||
}) {
|
||||
return traceErrorOnly('AutoRefreshContent.hasContentBeenUpdated', async () => {
|
||||
const context = await getServerActionBaseContext();
|
||||
const changeRequest = await getDataOrNull(
|
||||
context.dataFetcher.getChangeRequest({
|
||||
spaceId: props.spaceId,
|
||||
changeRequestId: props.changeRequestId,
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
if (siteURLData.changeRequest) {
|
||||
const changeRequest = await getDataOrNull(
|
||||
context.dataFetcher.getChangeRequest({
|
||||
spaceId: siteURLData.space,
|
||||
changeRequestId: siteURLData.changeRequest,
|
||||
})
|
||||
);
|
||||
if (!changeRequest) {
|
||||
return false;
|
||||
}
|
||||
return changeRequest.revision !== props.revisionId;
|
||||
}
|
||||
|
||||
const space = await getDataOrNull(
|
||||
context.dataFetcher.getSpace({
|
||||
spaceId: siteURLData.space,
|
||||
shareKey: siteURLData.shareKey,
|
||||
})
|
||||
);
|
||||
if (!changeRequest) {
|
||||
if (!space) {
|
||||
return false;
|
||||
}
|
||||
return changeRequest.revision !== props.revisionId;
|
||||
return space.revision !== props.revisionId;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,20 +5,71 @@ import React from 'react';
|
||||
import { hasContentBeenUpdated } from './server-actions';
|
||||
|
||||
/**
|
||||
* Return a callback to check if a change request has been updated and to refresh the page if it has.
|
||||
* Return a callback to check if a content has been updated and to refresh the page if it has.
|
||||
*/
|
||||
export function useCheckForContentUpdate(props: {
|
||||
spaceId: string;
|
||||
changeRequestId: string;
|
||||
/**
|
||||
* ID of the revision of the content currently being displayed.
|
||||
*/
|
||||
revisionId: string;
|
||||
/**
|
||||
* How long after page load to check for updates.
|
||||
* @default 5min
|
||||
*/
|
||||
checkAfterLoad?: number;
|
||||
}) {
|
||||
const { spaceId, changeRequestId, revisionId } = props;
|
||||
|
||||
return React.useCallback(async () => {
|
||||
const updated = await hasContentBeenUpdated({ spaceId, changeRequestId, revisionId });
|
||||
const { revisionId, checkAfterLoad = 5 * 60 * 1000 } = props;
|
||||
const [updated, setUpdated] = React.useState(false);
|
||||
|
||||
const checkForUpdates = React.useCallback(async () => {
|
||||
if (updated) {
|
||||
// No need to check for updates if we already know the content has been updated
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = await hasContentBeenUpdated({ revisionId });
|
||||
setUpdated(result);
|
||||
return result;
|
||||
}, [revisionId, updated]);
|
||||
|
||||
const refreshForUpdates = React.useCallback(async () => {
|
||||
if (await checkForUpdates()) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, [spaceId, changeRequestId, revisionId]);
|
||||
}, [checkForUpdates]);
|
||||
|
||||
// Check for updates after the page has loaded
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// We check only if the tab is visible; otherwise we'll check when the tab is focus again
|
||||
checkForUpdates();
|
||||
}
|
||||
}, checkAfterLoad);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [checkForUpdates, checkAfterLoad]);
|
||||
|
||||
// Check for updates when the tab is becoming visible
|
||||
React.useEffect(() => {
|
||||
if (updated) {
|
||||
// No need to check for updates if we already know the content has been updated
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkForUpdates();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', callback);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', callback);
|
||||
};
|
||||
}, [checkForUpdates, updated]);
|
||||
|
||||
return { refreshForUpdates, updated };
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
|
||||
node.data && 'fullWidth' in node.data && node.data.fullWidth
|
||||
? 'max-w-screen-xl'
|
||||
: 'max-w-3xl',
|
||||
FULL_WIDTH_BLOCKS.includes(node.type) && 'page-width-wide:max-w-screen-2xl',
|
||||
FULL_WIDTH_BLOCKS.includes(node.type) && 'page-width-wide:max-w-full',
|
||||
blockStyle,
|
||||
]}
|
||||
isEstimatedOffscreen={isOffscreen}
|
||||
|
||||
@@ -24,6 +24,7 @@ export function Caption(
|
||||
wrapperStyle?: ClassValue;
|
||||
block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile;
|
||||
withBorder?: boolean;
|
||||
withFrame?: boolean;
|
||||
} & DocumentContextProps
|
||||
) {
|
||||
const {
|
||||
@@ -33,6 +34,7 @@ export function Caption(
|
||||
context,
|
||||
fit = false,
|
||||
withBorder = false,
|
||||
withFrame = false,
|
||||
wrapperStyle = [
|
||||
'relative',
|
||||
'overflow-hidden',
|
||||
@@ -44,6 +46,7 @@ export function Caption(
|
||||
withBorder
|
||||
? 'rounded-corners:rounded-sm circular-corners:rounded-2xl after:border-tint-subtle after:border after:rounded circular-corners:after:rounded-2xl rounded-corners:after:rounded-sm dark:after:mix-blend-plus-lighter after:pointer-events-none'
|
||||
: null,
|
||||
withFrame && 'p-2',
|
||||
],
|
||||
style,
|
||||
} = props;
|
||||
@@ -62,7 +65,14 @@ export function Caption(
|
||||
return (
|
||||
<picture className={tcls('relative', style)}>
|
||||
<div className={tcls(wrapperStyle, 'mx-auto')}>{children}</div>
|
||||
<figcaption className={tcls('text-sm', 'text-center', 'mt-2', 'text-tint')}>
|
||||
<figcaption
|
||||
className={tcls(
|
||||
'text-xs',
|
||||
'text-center',
|
||||
'text-tint',
|
||||
withFrame ? 'mt-0.5 mb-2.5' : 'mt-2'
|
||||
)}
|
||||
>
|
||||
<Inlines
|
||||
nodes={captionParagraph.nodes}
|
||||
document={document}
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
import type { DocumentBlockCode } from '@gitbook/api';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
||||
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
|
||||
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
||||
import { useDebounceCallback } from 'usehooks-ts';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression';
|
||||
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
||||
import type { HighlightLine, RenderedInline } from './highlight';
|
||||
import { plainHighlight } from './plain-highlight';
|
||||
|
||||
type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||
inlines: RenderedInline[];
|
||||
inlineExprVariables: InlineExpressionVariables;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -20,11 +23,21 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
|
||||
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
||||
*/
|
||||
export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
const { block, style, inlines } = props;
|
||||
const { block, style, inlines, inlineExprVariables } = props;
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
const isInViewportRef = useRef(false);
|
||||
const [isInViewport, setIsInViewport] = useState(false);
|
||||
const plainLines = useMemo(() => plainHighlight(block, []), [block]);
|
||||
|
||||
const getAdaptiveVisitorClaims = useAdaptiveVisitor();
|
||||
const visitorClaims = getAdaptiveVisitorClaims();
|
||||
const evaluateInlineExpression = useEvaluateInlineExpression({
|
||||
visitorClaims,
|
||||
variables: inlineExprVariables,
|
||||
});
|
||||
const plainLines = useMemo(
|
||||
() => plainHighlight(block, inlines, { evaluateInlineExpression }),
|
||||
[block, inlines, evaluateInlineExpression]
|
||||
);
|
||||
const [lines, setLines] = useState<null | HighlightLine[]>(null);
|
||||
const [highlighting, setHighlighting] = useState(false);
|
||||
|
||||
@@ -80,7 +93,7 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
if (typeof window !== 'undefined') {
|
||||
setHighlighting(true);
|
||||
import('./highlight').then(({ highlight }) => {
|
||||
highlight(block, inlines).then((lines) => {
|
||||
highlight(block, inlines, { evaluateInlineExpression }).then((lines) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
@@ -98,7 +111,7 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
|
||||
// Otherwise if the block is not in viewport, we reset to the plain lines
|
||||
setLines(null);
|
||||
}, [isInViewport, block, inlines]);
|
||||
}, [isInViewport, block, inlines, evaluateInlineExpression]);
|
||||
|
||||
return (
|
||||
<CodeBlockRenderer
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { DocumentBlockCode } from '@gitbook/api';
|
||||
|
||||
import { getNodeFragmentByType } from '@/lib/document';
|
||||
@@ -14,32 +16,64 @@ import { type RenderedInline, getInlines, highlight } from './highlight';
|
||||
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||
const { block, document, style, isEstimatedOffscreen, context } = props;
|
||||
const inlines = getInlines(block);
|
||||
const richInlines: RenderedInline[] = inlines.map((inline, index) => {
|
||||
const body = (() => {
|
||||
const fragment = getNodeFragmentByType(inline.inline, 'annotation-body');
|
||||
if (!fragment) {
|
||||
return null;
|
||||
|
||||
let hasInlineExpression = false;
|
||||
|
||||
const richInlines: RenderedInline[] = inlines
|
||||
// Exclude inline expressions from rendered inline as they are rendered as code text once evaluated
|
||||
// and so need to be treated as plain code tokens.
|
||||
.filter((inline) => {
|
||||
if (inline.inline.type === 'expression') {
|
||||
hasInlineExpression = true;
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<Blocks
|
||||
key={index}
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
context={context}
|
||||
nodes={fragment.nodes}
|
||||
style="space-y-4"
|
||||
/>
|
||||
);
|
||||
})();
|
||||
return true;
|
||||
})
|
||||
.map((inline, index) => {
|
||||
const body = (() => {
|
||||
const fragment = getNodeFragmentByType(inline.inline, 'annotation-body');
|
||||
if (!fragment) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Blocks
|
||||
key={index}
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
context={context}
|
||||
nodes={fragment.nodes}
|
||||
style="space-y-4"
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
return { inline, body };
|
||||
});
|
||||
return { inline, body };
|
||||
});
|
||||
|
||||
if (!isEstimatedOffscreen) {
|
||||
if (!isEstimatedOffscreen && !hasInlineExpression) {
|
||||
// In v2, we render the code block server-side
|
||||
const lines = await highlight(block, richInlines);
|
||||
return <CodeBlockRenderer block={block} style={style} lines={lines} />;
|
||||
}
|
||||
|
||||
return <ClientCodeBlock block={block} style={style} inlines={richInlines} />;
|
||||
const variables = context.contentContext
|
||||
? {
|
||||
space: context.contentContext?.revision.variables,
|
||||
page:
|
||||
'page' in context.contentContext
|
||||
? context.contentContext.page.variables
|
||||
: undefined,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
<ClientCodeBlock
|
||||
block={block}
|
||||
style={style}
|
||||
inlines={richInlines}
|
||||
inlineExprVariables={variables}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
<div
|
||||
ref={ref}
|
||||
aria-busy={ariaBusy}
|
||||
className={tcls('group/codeblock grid grid-flow-col', style)}
|
||||
className={tcls('group/codeblock grid shrink grid-flow-col overflow-hidden', style)}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
|
||||
{title ? (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DocumentBlockCode, JSONDocument } from '@gitbook/api';
|
||||
import type { JSONDocument } from '@gitbook/api';
|
||||
import { useId } from 'react';
|
||||
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
import { convertCodeStringToBlock } from './utils';
|
||||
|
||||
/**
|
||||
* Plain code block with syntax highlighting.
|
||||
@@ -11,31 +12,7 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
||||
const { code, syntax } = props;
|
||||
const id = useId();
|
||||
|
||||
const block: DocumentBlockCode = {
|
||||
key: id,
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: {
|
||||
syntax,
|
||||
},
|
||||
nodes: code.split('\n').map((line) => ({
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: line,
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
const block = convertCodeStringToBlock({ key: id, code, syntax });
|
||||
|
||||
const document: JSONDocument = {
|
||||
object: 'document',
|
||||
|
||||
@@ -64,7 +64,10 @@ export async function preloadHighlight(block: DocumentBlockCode) {
|
||||
*/
|
||||
export async function highlight(
|
||||
block: DocumentBlockCode,
|
||||
inlines: RenderedInline[]
|
||||
inlines: RenderedInline[],
|
||||
options?: {
|
||||
evaluateInlineExpression?: (expr: string) => string;
|
||||
}
|
||||
): Promise<HighlightLine[]> {
|
||||
const langName = getBlockLang(block);
|
||||
|
||||
@@ -74,10 +77,10 @@ export async function highlight(
|
||||
// - TEMP : language is PowerShell or C++ and browser is Safari:
|
||||
// RegExp#[Symbol.search] throws TypeError when `lastIndex` isn’t writable
|
||||
// Fixed in upcoming Safari 18.6, remove when it'll be released - RND-7772
|
||||
return plainHighlight(block, inlines);
|
||||
return plainHighlight(block, inlines, options);
|
||||
}
|
||||
|
||||
const code = getPlainCodeBlock(block);
|
||||
const code = getPlainCodeBlock(block, undefined, options);
|
||||
|
||||
const highlighter = await getSingletonHighlighter({
|
||||
langs: [langName],
|
||||
@@ -255,11 +258,17 @@ function matchTokenAndInlines(
|
||||
return result;
|
||||
}
|
||||
|
||||
function getPlainCodeBlock(code: DocumentBlockCode, inlines?: InlineIndexed[]): string {
|
||||
function getPlainCodeBlock(
|
||||
code: DocumentBlockCode,
|
||||
inlines?: InlineIndexed[],
|
||||
options?: {
|
||||
evaluateInlineExpression?: (expr: string) => string;
|
||||
}
|
||||
): string {
|
||||
let content = '';
|
||||
|
||||
code.nodes.forEach((node, index) => {
|
||||
const lineContent = getPlainCodeBlockLine(node, content.length, inlines);
|
||||
const lineContent = getPlainCodeBlockLine(node, content.length, inlines, options);
|
||||
content += lineContent;
|
||||
|
||||
if (index < code.nodes.length - 1) {
|
||||
@@ -273,7 +282,10 @@ function getPlainCodeBlock(code: DocumentBlockCode, inlines?: InlineIndexed[]):
|
||||
function getPlainCodeBlockLine(
|
||||
parent: DocumentBlockCodeLine | DocumentInlineAnnotation,
|
||||
index: number,
|
||||
inlines?: InlineIndexed[]
|
||||
inlines?: InlineIndexed[],
|
||||
options?: {
|
||||
evaluateInlineExpression?: (expr: string) => string;
|
||||
}
|
||||
): string {
|
||||
let content = '';
|
||||
|
||||
@@ -284,7 +296,12 @@ function getPlainCodeBlockLine(
|
||||
switch (node.type) {
|
||||
case 'annotation': {
|
||||
const start = index + content.length;
|
||||
content += getPlainCodeBlockLine(node, index + content.length, inlines);
|
||||
content += getPlainCodeBlockLine(
|
||||
node,
|
||||
index + content.length,
|
||||
inlines,
|
||||
options
|
||||
);
|
||||
const end = index + content.length;
|
||||
|
||||
if (inlines) {
|
||||
@@ -297,6 +314,19 @@ function getPlainCodeBlockLine(
|
||||
break;
|
||||
}
|
||||
case 'expression': {
|
||||
const start = index + content.length;
|
||||
const exprValue =
|
||||
options?.evaluateInlineExpression?.(node.data.expression) ?? '';
|
||||
content += exprValue;
|
||||
const end = start + exprValue.length;
|
||||
|
||||
if (inlines) {
|
||||
inlines.push({
|
||||
inline: node,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -9,9 +9,13 @@ import type { HighlightLine, HighlightToken, RenderedInline } from './highlight'
|
||||
*/
|
||||
export function plainHighlight(
|
||||
block: DocumentBlockCode,
|
||||
inlines: RenderedInline[]
|
||||
inlines: RenderedInline[],
|
||||
options?: {
|
||||
evaluateInlineExpression?: (expr: string) => string;
|
||||
}
|
||||
): HighlightLine[] {
|
||||
const inlinesCopy = Array.from(inlines);
|
||||
|
||||
return block.nodes.map((lineBlock) => {
|
||||
const tokens: HighlightToken[] = lineBlock.nodes.map((node) => {
|
||||
if (node.object === 'text') {
|
||||
@@ -20,6 +24,14 @@ export function plainHighlight(
|
||||
content: getNodeText(node),
|
||||
};
|
||||
}
|
||||
|
||||
if (node.type === 'expression') {
|
||||
return {
|
||||
type: 'plain',
|
||||
content: options?.evaluateInlineExpression?.(node.data.expression) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const inline = inlinesCopy.shift();
|
||||
return {
|
||||
type: 'annotation',
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { convertCodeStringToBlock } from './utils';
|
||||
|
||||
describe('convertCodeStringToBlock', () => {
|
||||
it('converts plain code string without placeholders', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test1',
|
||||
code: 'console.log("hello");',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test1',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'console.log("hello");', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('converts a single placeholder into an expression node', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test2',
|
||||
code: 'const config = { API_KEY: "$$__X-GITBOOK-PREFILL[visitor.claims.apiKey ?? "YOUR_API_KEY"]__$$" }',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test2',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{ object: 'leaf', text: 'const config = { API_KEY: "', marks: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.apiKey ?? "YOUR_API_KEY"' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: '" }', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple placeholders in one line', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test3',
|
||||
code: 'let a = $$__X-GITBOOK-PREFILL[valA]__$$, b = $$__X-GITBOOK-PREFILL[valB]__$$;',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test3',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'let a = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'valA' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ', b = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'valB' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ';', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple placeholders across different lines', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test4',
|
||||
code: [
|
||||
'const name = "$$__X-GITBOOK-PREFILL[visitor.claims.userName]__$$";',
|
||||
'const age = $$__X-GITBOOK-PREFILL[visitor.claims.userAge]__$$;',
|
||||
'console.log(name, age);',
|
||||
].join('\n'),
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test4',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'const name = "', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.userName' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: '";', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'const age = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.userAge' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ';', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{ object: 'leaf', text: 'console.log(name, age);', marks: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty code block for empty string', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test5',
|
||||
code: '',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test5',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { DocumentBlockCode, DocumentBlockCodeLine } from '@gitbook/api';
|
||||
|
||||
const PREFILL_WITH_EXPR_REGEX = /\$\$__X-GITBOOK-PREFILL\[(.+?)\]__\$\$/g;
|
||||
|
||||
/**
|
||||
* Convert a raw code string into a `DocumentBlockCode` object representation.
|
||||
*
|
||||
* Any placeholder of the form `$$__X-GITBOOK-PREFILL[<expression>]__$$` inside the code
|
||||
* string is transformed into a DocumentInlineExpression node with its `data.expression` set to the
|
||||
* extracted `<expression>`.
|
||||
*/
|
||||
export function convertCodeStringToBlock(args: {
|
||||
key: string;
|
||||
code: string;
|
||||
syntax: string;
|
||||
}): DocumentBlockCode {
|
||||
const { key, code, syntax } = args;
|
||||
const lines = code.split('\n').map<DocumentBlockCodeLine>((line) => {
|
||||
const nodes: DocumentBlockCodeLine['nodes'] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of line.matchAll(PREFILL_WITH_EXPR_REGEX)) {
|
||||
const [placeholder, expression] = match;
|
||||
const start = match.index ?? 0;
|
||||
|
||||
if (start > lastIndex) {
|
||||
nodes.push({
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: line.slice(lastIndex, start), marks: [] }],
|
||||
});
|
||||
}
|
||||
|
||||
if (expression) {
|
||||
nodes.push({
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression },
|
||||
isVoid: true,
|
||||
});
|
||||
}
|
||||
|
||||
lastIndex = start + placeholder.length;
|
||||
}
|
||||
|
||||
if (lastIndex < line.length) {
|
||||
nodes.push({
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: line.slice(lastIndex), marks: [] }],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
key,
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax },
|
||||
nodes: lines,
|
||||
};
|
||||
}
|
||||
@@ -1,17 +1,57 @@
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { type DocumentBlockColumns, type Length, VerticalAlignment } from '@gitbook/api';
|
||||
import React from 'react';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
|
||||
export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
||||
const { block, style, ancestorBlocks, document, context } = props;
|
||||
|
||||
const columnWidths = React.useMemo(() => {
|
||||
const widths = block.nodes.map((block) => {
|
||||
const width = block.data.width;
|
||||
return width ? getFractionalWidth(width) : 0;
|
||||
});
|
||||
|
||||
const totalWidth = widths.reduce<number>((acc, width) => acc + width, 0);
|
||||
// If not all columns widths are set, distribute the remaining widths as equally as we can
|
||||
if (totalWidth < 1.0 && widths.some((width) => width === 0)) {
|
||||
const unsetWidths = widths.filter((width) => width === 0);
|
||||
let remainingWidth = 1.0 - totalWidth;
|
||||
let unsetWidthsLength = unsetWidths.length;
|
||||
widths.forEach((width, index) => {
|
||||
if (width === 0) {
|
||||
const calculatedWidth =
|
||||
Math.round((remainingWidth / unsetWidthsLength) * COLUMN_DIVISIONS) /
|
||||
COLUMN_DIVISIONS;
|
||||
widths[index] = calculatedWidth; // Assign width to empty columns
|
||||
unsetWidthsLength--;
|
||||
remainingWidth -= calculatedWidth;
|
||||
}
|
||||
});
|
||||
}
|
||||
return widths;
|
||||
}, [block.nodes]);
|
||||
|
||||
return (
|
||||
<div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
|
||||
{block.nodes.map((columnBlock) => {
|
||||
<div
|
||||
className={tcls(
|
||||
'grid w-full grid-cols-1 gap-x-8 gap-y-4 md:grid-flow-col md:grid-cols-[repeat(var(--grid-slices),minmax(0,1fr))] md:grid-rows-1',
|
||||
style
|
||||
)}
|
||||
style={{ '--grid-slices': COLUMN_DIVISIONS } as React.CSSProperties}
|
||||
>
|
||||
{block.nodes.map((columnBlock, index) => {
|
||||
const columnWidth = columnWidths[index];
|
||||
return (
|
||||
<Column
|
||||
key={columnBlock.key}
|
||||
width={columnBlock.data.width}
|
||||
width={
|
||||
columnBlock.data.width ??
|
||||
(columnWidth
|
||||
? { value: columnWidth * 100, unit: '%' }
|
||||
: { value: 0, unit: '%' })
|
||||
}
|
||||
verticalAlignment={columnBlock.data.verticalAlignment}
|
||||
>
|
||||
<Blocks
|
||||
@@ -31,11 +71,11 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
||||
|
||||
export function Column(props: {
|
||||
children?: React.ReactNode;
|
||||
width?: Length;
|
||||
width: Length;
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
}) {
|
||||
const { width, verticalAlignment } = props;
|
||||
const { className, style } = transformLengthToCSS(width);
|
||||
const { className, style } = transformLengthToCSS(width) ?? {};
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
@@ -53,41 +93,25 @@ export function Column(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function transformLengthToCSS(length: Length | undefined) {
|
||||
if (!length) {
|
||||
return { className: ['md:w-full'] }; // default to full width if no length is specified
|
||||
}
|
||||
const COLUMN_DIVISIONS = 12;
|
||||
|
||||
export function transformLengthToCSS(length: Length) {
|
||||
if (typeof length === 'number') {
|
||||
return { style: undefined }; // not implemented yet with non-percentage lengths
|
||||
return; // not implemented yet with non-percentage lengths
|
||||
}
|
||||
|
||||
if (length.unit === '%') {
|
||||
return {
|
||||
className: [
|
||||
'md:shrink-0',
|
||||
COLUMN_WIDTHS[Math.round(length.value * 0.01 * (COLUMN_WIDTHS.length - 1))],
|
||||
],
|
||||
className: 'md:flex-shrink-0 md:[grid-column:var(--grid-col)]',
|
||||
style: {
|
||||
'--grid-col': `auto / span ${Math.round(length.value * 0.01 * COLUMN_DIVISIONS)}`,
|
||||
} as React.CSSProperties,
|
||||
};
|
||||
}
|
||||
|
||||
return { style: undefined }; // not implemented yet with non-percentage lengths
|
||||
}
|
||||
|
||||
// Tailwind CSS classes for column widths.
|
||||
// The index of the array corresponds to the percentage width of the column.
|
||||
const COLUMN_WIDTHS = [
|
||||
'md:w-0',
|
||||
'md:w-1/12',
|
||||
'md:w-2/12',
|
||||
'md:w-3/12',
|
||||
'md:w-4/12',
|
||||
'md:w-5/12',
|
||||
'md:w-6/12',
|
||||
'md:w-7/12',
|
||||
'md:w-8/12',
|
||||
'md:w-9/12',
|
||||
'md:w-10/12',
|
||||
'md:w-11/12',
|
||||
'md:w-full',
|
||||
];
|
||||
function getFractionalWidth(length: Length) {
|
||||
if (typeof length === 'number') {
|
||||
return 0;
|
||||
}
|
||||
return length.value / 100;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Icon, type IconName } from '@gitbook/icons';
|
||||
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getSpaceLanguage, tString } from '@/intl/server';
|
||||
import { languages } from '@/intl/translations';
|
||||
import { Block, type BlockProps } from './Block';
|
||||
import { Blocks } from './Blocks';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
@@ -18,6 +20,12 @@ export function Hint({
|
||||
const firstLine = getBlockTextStyle(firstNode);
|
||||
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(firstNode.type);
|
||||
|
||||
const language = contextProps.context.contentContext
|
||||
? getSpaceLanguage(contextProps.context.contentContext)
|
||||
: languages.en;
|
||||
|
||||
const label = tString(language, `hint_${block.data.style}`);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
@@ -38,6 +46,8 @@ export function Hint({
|
||||
|
||||
style
|
||||
)}
|
||||
aria-label={label}
|
||||
role="note"
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -11,8 +11,8 @@ import type { DocumentContext } from './DocumentView';
|
||||
export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
const { document, block, style, context, isEstimatedOffscreen } = props;
|
||||
|
||||
const isMultipleImages = block.nodes.length > 1;
|
||||
const { align = 'center' } = block.data;
|
||||
const hasMultipleImages = block.nodes.length > 1;
|
||||
const { align = 'center', withFrame } = block.data;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -24,7 +24,15 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
align === 'center' && 'justify-center',
|
||||
align === 'right' && 'justify-end',
|
||||
align === 'left' && 'justify-start',
|
||||
isMultipleImages && ['grid', 'grid-flow-col']
|
||||
hasMultipleImages && ['grid', 'grid-flow-col'],
|
||||
withFrame && [
|
||||
'rounded-2xl',
|
||||
'border',
|
||||
'border-[rgb(234,235,238)]',
|
||||
'dark:border-[rgb(45,50,58)]',
|
||||
'relative',
|
||||
'overflow-hidden',
|
||||
]
|
||||
)}
|
||||
>
|
||||
{block.nodes.map((node: any, _i: number) => (
|
||||
@@ -36,6 +44,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
siblings={block.nodes.length}
|
||||
context={context}
|
||||
isEstimatedOffscreen={isEstimatedOffscreen}
|
||||
withFrame={withFrame}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -62,8 +71,9 @@ async function ImageBlock(props: {
|
||||
context: DocumentContext;
|
||||
siblings: number;
|
||||
isEstimatedOffscreen: boolean;
|
||||
withFrame?: boolean;
|
||||
}) {
|
||||
const { block, context, isEstimatedOffscreen } = props;
|
||||
const { block, context, isEstimatedOffscreen, withFrame } = props;
|
||||
|
||||
const [src, darkSrc] = await Promise.all([
|
||||
context.contentContext ? resolveContentRef(block.data.ref, context.contentContext) : null,
|
||||
@@ -77,33 +87,66 @@ async function ImageBlock(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<Caption {...props} fit>
|
||||
<Image
|
||||
alt={block.data.alt ?? ''}
|
||||
sizes={imageBlockSizes}
|
||||
resize={context.contentContext?.imageResizer}
|
||||
sources={{
|
||||
light: {
|
||||
src: src.href,
|
||||
size: src.file?.dimensions,
|
||||
},
|
||||
dark: darkSrc
|
||||
? {
|
||||
src: darkSrc.href,
|
||||
size: darkSrc.file?.dimensions,
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
priority={isEstimatedOffscreen ? 'lazy' : 'high'}
|
||||
preload
|
||||
zoom
|
||||
inlineStyle={{
|
||||
maxWidth: '100%',
|
||||
width: getImageDimension(block.data.width, undefined),
|
||||
height: getImageDimension(block.data.height, 'auto'),
|
||||
}}
|
||||
/>
|
||||
</Caption>
|
||||
<div className={tcls('relative', 'overflow-hidden')}>
|
||||
{/* Frame grid */}
|
||||
{withFrame && (
|
||||
<div
|
||||
className={tcls(
|
||||
'absolute',
|
||||
'-top-0.5',
|
||||
'-left-0.5',
|
||||
'right-px',
|
||||
'bottom-px',
|
||||
'opacity-40',
|
||||
'dark:opacity-[0.1]',
|
||||
'bg-[length:24px_24px,24px_24px]',
|
||||
'bg-[linear-gradient(to_right,_rgb(234,235,238)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(234,235,238)_1px,_transparent_1px)]',
|
||||
'dark:bg-[linear-gradient(to_right,_rgb(122,128,139)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(122,128,139)_1px,_transparent_1px)]',
|
||||
'bg-repeat'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shadow overlay */}
|
||||
{withFrame && (
|
||||
<div
|
||||
className={tcls(
|
||||
'pointer-events-none absolute inset-0 rounded-2xl',
|
||||
'shadow-[inset_0_0_10px_10px_rgba(255,255,255,0.9)]',
|
||||
'dark:shadow-[inset_0_0_10px_10px_rgb(29,29,29)]'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Caption {...props} fit>
|
||||
<Image
|
||||
alt={block.data.alt ?? ''}
|
||||
sizes={imageBlockSizes}
|
||||
resize={context.contentContext?.imageResizer}
|
||||
sources={{
|
||||
light: {
|
||||
src: src.href,
|
||||
size: src.file?.dimensions,
|
||||
},
|
||||
dark: darkSrc
|
||||
? {
|
||||
src: darkSrc.href,
|
||||
size: darkSrc.file?.dimensions,
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
priority={isEstimatedOffscreen ? 'lazy' : 'high'}
|
||||
preload
|
||||
zoom
|
||||
inlineStyle={{
|
||||
maxWidth: '100%',
|
||||
width: getImageDimension(block.data.width, undefined),
|
||||
height: getImageDimension(block.data.height, 'auto'),
|
||||
}}
|
||||
style={withFrame ? 'rounded-xl' : undefined}
|
||||
/>
|
||||
</Caption>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Annotation } from './Annotation/Annotation';
|
||||
import type { DocumentContextProps } from './DocumentView';
|
||||
import { Emoji } from './Emoji';
|
||||
import { InlineButton } from './InlineButton';
|
||||
import { InlineExpression } from './InlineExpression';
|
||||
import { InlineIcon } from './InlineIcon';
|
||||
import { InlineImage } from './InlineImage';
|
||||
import { InlineLink } from './InlineLink';
|
||||
@@ -51,9 +52,7 @@ export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
|
||||
case 'icon':
|
||||
return <InlineIcon {...contextProps} inline={inline} />;
|
||||
case 'expression':
|
||||
// The GitBook API should take care of evaluating expressions.
|
||||
// We should never need to render them.
|
||||
return null;
|
||||
return <InlineExpression {...contextProps} inline={inline} />;
|
||||
default:
|
||||
return nullIfNever(inline);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { DocumentInlineExpression } from '@gitbook/api';
|
||||
import type { InlineProps } from '../Inline';
|
||||
import { InlineExpressionValue } from './InlineExpressionValue';
|
||||
|
||||
/**
|
||||
* Render an inline expression.
|
||||
*/
|
||||
export function InlineExpression(props: InlineProps<DocumentInlineExpression>) {
|
||||
const { context, inline } = props;
|
||||
|
||||
const { data } = inline;
|
||||
|
||||
const variables = context.contentContext
|
||||
? {
|
||||
space: context.contentContext?.revision.variables,
|
||||
page:
|
||||
'page' in context.contentContext
|
||||
? context.contentContext.page.variables
|
||||
: undefined,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
<InlineExpressionValue expression={data.expression} variables={variables} />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
||||
import { useMemo } from 'react';
|
||||
import type { InlineExpressionVariables } from './types';
|
||||
import { useEvaluateInlineExpression } from './useEvaluateInlineExpression';
|
||||
|
||||
export function InlineExpressionValue(props: {
|
||||
expression: string;
|
||||
variables: InlineExpressionVariables;
|
||||
}) {
|
||||
const { expression, variables } = props;
|
||||
|
||||
const getAdaptiveVisitorClaims = useAdaptiveVisitor();
|
||||
const visitorClaims = getAdaptiveVisitorClaims();
|
||||
const evaluateInlineExpression = useEvaluateInlineExpression({
|
||||
visitorClaims,
|
||||
variables,
|
||||
});
|
||||
|
||||
const result = useMemo(
|
||||
() => evaluateInlineExpression(expression),
|
||||
[expression, evaluateInlineExpression]
|
||||
);
|
||||
|
||||
return <>{result}</>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './InlineExpression';
|
||||
export * from './types';
|
||||
export * from './useEvaluateInlineExpression';
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Variables } from '@gitbook/api';
|
||||
|
||||
export interface InlineExpressionVariables {
|
||||
space?: Variables;
|
||||
page?: Variables;
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
type AdaptiveVisitorClaims,
|
||||
createExpressionEvaluationContext,
|
||||
} from '@/components/Adaptive';
|
||||
import type { Variables } from '@gitbook/api';
|
||||
import { ExpressionRuntime, formatExpressionResult } from '@gitbook/expr';
|
||||
|
||||
/**
|
||||
* Hook that returns a callback to evaluate an inline expression with visitor data
|
||||
* and space/page variables as context.
|
||||
*/
|
||||
export function useEvaluateInlineExpression(args: {
|
||||
visitorClaims: AdaptiveVisitorClaims | null;
|
||||
variables: {
|
||||
space?: Variables;
|
||||
page?: Variables;
|
||||
};
|
||||
}) {
|
||||
const { visitorClaims, variables } = args;
|
||||
const evaluateInlineExpression = React.useMemo(() => {
|
||||
const runtime = new ExpressionRuntime();
|
||||
const evaluationContext = createExpressionEvaluationContext({
|
||||
visitorClaims,
|
||||
variables,
|
||||
});
|
||||
|
||||
return (expression: string) => {
|
||||
try {
|
||||
return formatExpressionResult(
|
||||
runtime.evaluate(expression, evaluationContext) ?? ''
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to evaluate expression:', expression, err);
|
||||
return `{{${expression}}}`;
|
||||
}
|
||||
};
|
||||
}, [variables, visitorClaims]);
|
||||
|
||||
return evaluateInlineExpression;
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
import type { DocumentInlineIcon } from '@gitbook/api';
|
||||
import type { DocumentInlineIcon, DocumentMarkColor } from '@gitbook/api';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { textColorToStyle } from './utils/colors';
|
||||
|
||||
export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
|
||||
const { inline } = props;
|
||||
const icon = inline.data.icon as IconName;
|
||||
// @ts-expect-error remove this comment once API is updated
|
||||
const color = inline.data.color
|
||||
? // @ts-expect-error remove "as DocumentMarkColor['data']['text']" once API is updated
|
||||
(inline.data.color as DocumentMarkColor['data']['text'])
|
||||
: undefined;
|
||||
|
||||
return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />;
|
||||
return (
|
||||
<Icon
|
||||
icon={icon}
|
||||
className={tcls('inline size-[1em]', color ? textColorToStyle[color] : null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function InlineImage(props: InlineProps<DocumentInlineImage>) {
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
alt={inline.data.caption ?? ''}
|
||||
alt={inline.data.alt ?? inline.data.caption ?? ''}
|
||||
sizes={sizes}
|
||||
resize={context.contentContext?.imageResizer}
|
||||
sources={{
|
||||
|
||||
@@ -143,11 +143,11 @@
|
||||
}
|
||||
|
||||
.openapi-column-preview {
|
||||
@apply flex flex-col flex-1;
|
||||
@apply flex flex-col flex-1 xl:max-2xl:pt-20 lg:py-6 sticky max-h-[calc(100vh-var(--toc-top-offset))] top-(--toc-top-offset);
|
||||
}
|
||||
|
||||
.openapi-column-preview-body {
|
||||
@apply flex flex-col gap-4 sticky top-[calc(var(--toc-top-offset)+5rem)] print-mode:static;
|
||||
@apply flex flex-col shrink overflow-hidden gap-4 print-mode:static;
|
||||
}
|
||||
|
||||
.openapi-column-preview pre {
|
||||
@@ -192,7 +192,7 @@
|
||||
.openapi-schema-name {
|
||||
/* To make double click on the property name select only the name,
|
||||
we disable selection on the parent and re-enable it on the children. */
|
||||
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5;
|
||||
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5 items-center;
|
||||
}
|
||||
|
||||
.openapi-schema-name .openapi-deprecated {
|
||||
@@ -207,6 +207,10 @@
|
||||
@apply line-through opacity-9;
|
||||
}
|
||||
|
||||
.openapi-schema-discriminator {
|
||||
@apply text-primary-subtle/9 text-[0.813rem] lowercase;
|
||||
}
|
||||
|
||||
.openapi-schema-required {
|
||||
@apply text-warning-subtle text-[0.813rem] lowercase;
|
||||
}
|
||||
@@ -313,10 +317,11 @@
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-flows {
|
||||
@apply flex flex-col gap-2 divide-y divide-tint-subtle;
|
||||
@apply flex flex-col gap-3;
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-content {
|
||||
.openapi-securities-oauth-content,
|
||||
.openapi-securities-scopes {
|
||||
@apply prose *:!prose-sm *:text-tint;
|
||||
}
|
||||
|
||||
@@ -324,7 +329,7 @@
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-content ul {
|
||||
.openapi-securities-scopes ul {
|
||||
@apply !my-0;
|
||||
}
|
||||
|
||||
@@ -510,13 +515,21 @@
|
||||
.openapi-panel,
|
||||
.openapi-codesample,
|
||||
.openapi-response-examples {
|
||||
@apply border rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
|
||||
@apply border shrink min-h-40 overflow-hidden rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
|
||||
}
|
||||
|
||||
.openapi-response-examples-panel {
|
||||
@apply flex flex-col shrink overflow-hidden;
|
||||
}
|
||||
|
||||
.openapi-codesample-panel {
|
||||
@apply flex flex-col shrink overflow-hidden;
|
||||
}
|
||||
|
||||
.openapi-panel pre,
|
||||
.openapi-codesample pre,
|
||||
.openapi-response-examples pre {
|
||||
@apply bg-transparent border-none rounded-none shadow-none;
|
||||
@apply bg-transparent border-none rounded-none shrink shadow-none;
|
||||
}
|
||||
|
||||
.openapi-panel-heading {
|
||||
@@ -579,7 +592,7 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-select > button {
|
||||
@apply flex items-center font-normal cursor-pointer *:truncate gap-1.5 text-tint-strong max-w-32 rounded text-xs p-1.5 leading-none border border-tint-subtle bg-tint;
|
||||
@apply flex items-center font-normal cursor-pointer *:truncate gap-1.5 text-tint-strong rounded text-xs p-1.5 leading-none border border-tint-subtle bg-tint;
|
||||
@apply hover:bg-tint-hover transition-all;
|
||||
}
|
||||
|
||||
@@ -613,7 +626,7 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-select-item {
|
||||
@apply text-sm flex items-center cursor-pointer px-1.5 overflow-hidden py-1 *:truncate text-tint ring-0 border-none rounded !outline-none;
|
||||
@apply text-sm flex items-center cursor-pointer px-1.5 overflow-hidden py-1 text-tint ring-0 border-none rounded !outline-none;
|
||||
@apply hover:bg-tint-hover hover:theme-gradient:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current;
|
||||
}
|
||||
|
||||
@@ -638,6 +651,14 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
/* Section Components */
|
||||
.openapi-section {
|
||||
@apply flex flex-col overflow-hidden;
|
||||
}
|
||||
|
||||
.openapi-section-body {
|
||||
@apply flex flex-col shrink overflow-hidden;
|
||||
}
|
||||
|
||||
.openapi-section-header {
|
||||
@apply flex flex-row items-center;
|
||||
}
|
||||
@@ -820,7 +841,7 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger-label {
|
||||
@apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
|
||||
@apply absolute right-3 mr-px px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger-label span {
|
||||
|
||||
@@ -95,8 +95,12 @@ export async function RecordCard(
|
||||
}}
|
||||
sizes={[
|
||||
{
|
||||
media: '(max-width: 640px)',
|
||||
width: view.cardSize === 'medium' ? 245 : 376,
|
||||
},
|
||||
{
|
||||
width: view.cardSize === 'medium' ? 490 : 752,
|
||||
},
|
||||
]}
|
||||
resize={context.contentContext?.imageResizer}
|
||||
className={tcls(
|
||||
|
||||
@@ -71,16 +71,18 @@ function processCoverValue(value: DocumentTableImageRecord | string[] | null | u
|
||||
}
|
||||
}
|
||||
|
||||
const imageValue = value as DocumentTableImageRecord | null | undefined;
|
||||
|
||||
// Check if it's the new schema with objectFit
|
||||
if (value && typeof value === 'object' && 'ref' in value && 'objectFit' in value) {
|
||||
if (imageValue && typeof imageValue === 'object' && 'ref' in imageValue) {
|
||||
return {
|
||||
contentRef: value.ref,
|
||||
objectFit: value.objectFit,
|
||||
contentRef: imageValue.ref,
|
||||
objectFit: imageValue.objectFit,
|
||||
};
|
||||
}
|
||||
|
||||
// It's a direct ContentRef
|
||||
return { contentRef: value as ContentRefFile | ContentRefURL };
|
||||
return { contentRef: imageValue || null };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,8 @@ import type {
|
||||
} from '@gitbook/api';
|
||||
import React from 'react';
|
||||
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { backgroundColorToStyle, textColorToStyle } from './utils/colors';
|
||||
|
||||
export function Text(props: { text: DocumentText }) {
|
||||
const { text } = props;
|
||||
@@ -121,51 +122,3 @@ function Color(props: MarkedLeafProps<DocumentMarkColor>) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO replace by DocumentMarkColor['data']['text'] and DocumentMarkColor['data']['background']
|
||||
* once the API is updated.
|
||||
*/
|
||||
type DocumentMarkColorValue =
|
||||
| 'default'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'red'
|
||||
| 'orange'
|
||||
| 'yellow'
|
||||
| 'purple'
|
||||
| '$primary'
|
||||
| '$info'
|
||||
| '$success'
|
||||
| '$warning'
|
||||
| '$danger';
|
||||
|
||||
const textColorToStyle: { [color in DocumentMarkColorValue]: ClassValue } = {
|
||||
default: [],
|
||||
blue: ['text-blue-500'],
|
||||
red: ['text-red-500'],
|
||||
green: ['text-green-500'],
|
||||
yellow: ['text-yellow-600'],
|
||||
purple: ['text-purple-500'],
|
||||
orange: ['text-orange-500'],
|
||||
$primary: ['text-primary'],
|
||||
$info: ['text-info'],
|
||||
$success: ['text-success'],
|
||||
$warning: ['text-warning'],
|
||||
$danger: ['text-danger'],
|
||||
};
|
||||
|
||||
const backgroundColorToStyle: { [color in DocumentMarkColorValue]: ClassValue } = {
|
||||
default: [],
|
||||
blue: ['bg-mark-blue'],
|
||||
red: ['bg-mark-red'],
|
||||
green: ['bg-mark-green'],
|
||||
yellow: ['bg-mark-yellow'],
|
||||
purple: ['bg-mark-purple'],
|
||||
orange: ['bg-mark-orange'],
|
||||
$primary: ['bg-primary'],
|
||||
$info: ['bg-info'],
|
||||
$success: ['bg-success'],
|
||||
$warning: ['bg-warning'],
|
||||
$danger: ['bg-danger'],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ClassValue } from '@/lib/tailwind';
|
||||
import type { DocumentMarkColor } from '@gitbook/api';
|
||||
|
||||
export const textColorToStyle: { [color in DocumentMarkColor['data']['text']]: ClassValue } = {
|
||||
default: [],
|
||||
blue: ['text-blue-500'],
|
||||
red: ['text-red-500'],
|
||||
green: ['text-green-500'],
|
||||
yellow: ['text-yellow-600'],
|
||||
purple: ['text-purple-500'],
|
||||
orange: ['text-orange-500'],
|
||||
$primary: ['text-primary'],
|
||||
$info: ['text-info'],
|
||||
$success: ['text-success'],
|
||||
$warning: ['text-warning'],
|
||||
$danger: ['text-danger'],
|
||||
};
|
||||
|
||||
export const backgroundColorToStyle: {
|
||||
[color in DocumentMarkColor['data']['background']]: ClassValue;
|
||||
} = {
|
||||
default: [],
|
||||
blue: ['bg-mark-blue'],
|
||||
red: ['bg-mark-red'],
|
||||
green: ['bg-mark-green'],
|
||||
yellow: ['bg-mark-yellow'],
|
||||
purple: ['bg-mark-purple'],
|
||||
orange: ['bg-mark-orange'],
|
||||
$primary: ['bg-primary'],
|
||||
$info: ['bg-info'],
|
||||
$success: ['bg-success'],
|
||||
$warning: ['bg-warning'],
|
||||
$danger: ['bg-danger'],
|
||||
};
|
||||
@@ -36,13 +36,9 @@ async function FooterLink(props: { link: CustomizationContentLink; context: GitB
|
||||
const { link, context } = props;
|
||||
const resolved = await resolveContentRef(link.to, context);
|
||||
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={resolved.href}
|
||||
href={resolved?.href ?? '#'}
|
||||
className={tcls(
|
||||
'font-normal',
|
||||
'text-tint',
|
||||
|
||||
@@ -42,11 +42,11 @@ export function CurrentContentIcon(
|
||||
}
|
||||
: {
|
||||
light: {
|
||||
src: linker.toPathInSpace('~gitbook/icon?size=medium&theme=light'),
|
||||
src: linker.toPathInSpace('~gitbook/icon?size=large&theme=light'),
|
||||
size: { width: 256, height: 256 },
|
||||
},
|
||||
dark: {
|
||||
src: linker.toPathInSpace('~gitbook/icon?size=medium&theme=dark'),
|
||||
src: linker.toPathInSpace('~gitbook/icon?size=large&theme=dark'),
|
||||
size: { width: 256, height: 256 },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ export function Header(props: {
|
||||
const { context, withTopHeader, withVariants } = props;
|
||||
const { siteSpace, siteSpaces, sections, customization } = context;
|
||||
|
||||
const withSections = Boolean(
|
||||
sections &&
|
||||
(sections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group
|
||||
sections.list.some((s) => s.object === 'site-section-group'))
|
||||
);
|
||||
|
||||
return (
|
||||
<header
|
||||
id="site-header"
|
||||
@@ -57,7 +63,7 @@ export function Header(props: {
|
||||
'theme-bold:shadow-tint-12/2'
|
||||
)}
|
||||
>
|
||||
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
<div className="transition-[padding] duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
<div
|
||||
className={tcls(
|
||||
'gap-4',
|
||||
@@ -120,15 +126,31 @@ export function Header(props: {
|
||||
>
|
||||
<SearchContainer
|
||||
style={customization.styling.search}
|
||||
isMultiVariants={siteSpaces.length > 1}
|
||||
withVariants={withVariants === 'generic'}
|
||||
withSiteVariants={
|
||||
sections?.list.some(
|
||||
(s) =>
|
||||
s.object === 'site-section' && s.siteSpaces.length > 1
|
||||
) ?? false
|
||||
}
|
||||
withSections={!!sections}
|
||||
section={
|
||||
sections
|
||||
? // Client-encode to avoid a serialisation issue that was causing the language selector to disappear
|
||||
encodeClientSiteSections(context, sections).current
|
||||
: undefined
|
||||
}
|
||||
spaceTitle={siteSpace.title}
|
||||
siteSpaceId={siteSpace.id}
|
||||
siteSpaceIds={siteSpaces
|
||||
.filter((s) => s.space.language === siteSpace.space.language)
|
||||
.map((s) => s.id)}
|
||||
viewport={!withTopHeader ? 'mobile' : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{customization.header.links.length > 0 ||
|
||||
(!sections && withVariants === 'translations') ? (
|
||||
(!withSections && withVariants === 'translations') ? (
|
||||
<HeaderLinks>
|
||||
{customization.header.links.length > 0 ? (
|
||||
<>
|
||||
@@ -148,7 +170,7 @@ export function Header(props: {
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{!sections && withVariants === 'translations' ? (
|
||||
{!withSections && withVariants === 'translations' ? (
|
||||
<TranslationsDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
@@ -162,43 +184,19 @@ export function Header(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sections ? (
|
||||
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
<div
|
||||
className={tcls(
|
||||
'w-full',
|
||||
'overflow-x-auto',
|
||||
'no-scrollbar',
|
||||
'-mb-4 pb-4', // Positive padding / negative margin allows the navigation menu indicator to show in a scroll viewƒ
|
||||
!sections ? ['hidden', 'page-no-toc:flex'] : 'flex'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
CONTAINER_STYLE,
|
||||
'grow',
|
||||
'flex',
|
||||
'items-end',
|
||||
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]'
|
||||
)}
|
||||
>
|
||||
{sections.list.some((s) => s.object === 'site-section-group') || // If there's even a single group, show the tabs
|
||||
sections.list.length > 1 ? ( // Otherwise, show the tabs if there's more than one section
|
||||
<SiteSectionTabs
|
||||
sections={encodeClientSiteSections(context, sections)}
|
||||
{sections && withSections ? (
|
||||
<div className="transition-[padding] duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
<SiteSectionTabs sections={encodeClientSiteSections(context, sections)}>
|
||||
{withVariants === 'translations' ? (
|
||||
<div className="before:contents[] flex self-start py-2 before:mr-4 before:border-tint before:border-l">
|
||||
<TranslationsDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
/>
|
||||
) : null}
|
||||
{withVariants === 'translations' ? (
|
||||
<div className="site-background before:contents[] -mr-4 sm:-mr-6 md:-mr-8 sticky inset-y-0 right-0 z-10 ml-6 flex h-full items-center py-2 pr-4 before:mr-4 before:h-full before:border-tint before:border-l sm:pr-6 md:pr-8">
|
||||
<TranslationsDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</SiteSectionTabs>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function HeaderLink(props: {
|
||||
);
|
||||
}
|
||||
|
||||
if (!target || !link.to) {
|
||||
if (!link.to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function HeaderLink(props: {
|
||||
headerPreset={headerPreset}
|
||||
title={link.title}
|
||||
isDropdown={false}
|
||||
href={target.href}
|
||||
href={target?.href}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export type HeaderLinkNavItemProps = {
|
||||
linkStyle: NonNullable<CustomizationHeaderItem['style']>;
|
||||
headerPreset: CustomizationHeaderPreset;
|
||||
title: string;
|
||||
href: string;
|
||||
href?: string;
|
||||
isDropdown: boolean;
|
||||
} & DropdownButtonProps<HTMLElement>;
|
||||
|
||||
@@ -165,7 +165,7 @@ function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
|
||||
const { linkTarget, headerPreset, title, isDropdown, href, ...rest } = props;
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
href={href ?? '#'}
|
||||
className={getHeaderLinkClassName({ headerPreset })}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
|
||||
@@ -76,7 +76,7 @@ export function TranslationsDropdown(props: {
|
||||
siteSpaces={siteSpaces}
|
||||
variant="blank"
|
||||
className={tcls(
|
||||
'-mx-2 bg-transparent px-2 py-1 lg:max-w-64 max-md:[&_.button-content]:hidden',
|
||||
'-mx-2 bg-transparent px-2 md:py-1 lg:max-w-64 max-md:[&_.button-content]:hidden',
|
||||
hasEmojiPrefix
|
||||
? 'md:[&_.button-leading-icon]:hidden' // If the title starts with an emoji, don't show the icon (on desktop)
|
||||
: '',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useDebounceCallback, useEventCallback } from 'usehooks-ts';
|
||||
import { getAllBrowserCookiesMap } from '@/lib/browser';
|
||||
import { type CurrentContentContext, useCurrentContent } from '../hooks';
|
||||
import { getSession } from './sessions';
|
||||
import { getVisitorId } from './visitorId';
|
||||
import { type SessionResponse, useVisitorSession } from './visitorId';
|
||||
|
||||
export type InsightsEventName = api.SiteInsightsEvent['type'];
|
||||
|
||||
@@ -55,12 +55,6 @@ interface InsightsProviderProps {
|
||||
/** If true, the events will be sent to the server. */
|
||||
enabled: boolean;
|
||||
|
||||
/** If true, the visitor cookie tracking will be used */
|
||||
visitorCookieTrackingEnabled: boolean;
|
||||
|
||||
/** The application URL. */
|
||||
appURL: string;
|
||||
|
||||
/** The url of the endpoint to send events to */
|
||||
eventUrl: string;
|
||||
|
||||
@@ -72,10 +66,10 @@ interface InsightsProviderProps {
|
||||
* Wrap the content of the app with the InsightsProvider to track events.
|
||||
*/
|
||||
export function InsightsProvider(props: InsightsProviderProps) {
|
||||
const { enabled, children, visitorCookieTrackingEnabled, eventUrl, appURL } = props;
|
||||
const { enabled, children, eventUrl } = props;
|
||||
|
||||
const visitorSession = useVisitorSession();
|
||||
const currentContent = useCurrentContent();
|
||||
const visitorIdRef = React.useRef<string | null>(null);
|
||||
const eventsRef = React.useRef<{
|
||||
[pathname: string]:
|
||||
| {
|
||||
@@ -92,9 +86,8 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
*/
|
||||
const flushEventsSync = useEventCallback(() => {
|
||||
const session = getSession();
|
||||
const visitorId = visitorIdRef.current;
|
||||
if (!visitorId) {
|
||||
throw new Error('Visitor ID should be set before flushing events');
|
||||
if (!visitorSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allEvents: api.SiteInsightsEvent[] = [];
|
||||
@@ -114,7 +107,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
events: eventsForPathname.events,
|
||||
context: currentContent,
|
||||
pageContext: eventsForPathname.pageContext,
|
||||
visitorId,
|
||||
visitorSession,
|
||||
sessionId: session.id,
|
||||
})
|
||||
);
|
||||
@@ -126,25 +119,25 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
};
|
||||
}
|
||||
|
||||
if (allEvents.length > 0) {
|
||||
if (enabled) {
|
||||
sendEvents({
|
||||
eventUrl,
|
||||
events: allEvents,
|
||||
});
|
||||
} else {
|
||||
}
|
||||
if (allEvents.length > 0 && enabled) {
|
||||
sendEvents({
|
||||
eventUrl,
|
||||
events: allEvents,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const flushBatchedEvents = useDebounceCallback(async () => {
|
||||
const visitorId =
|
||||
visitorIdRef.current ?? (await getVisitorId(appURL, visitorCookieTrackingEnabled));
|
||||
visitorIdRef.current = visitorId;
|
||||
|
||||
const flushBatchedEvents = useDebounceCallback(() => {
|
||||
flushEventsSync();
|
||||
}, 1500);
|
||||
|
||||
// Flush pending events once the visitor session has been fetched
|
||||
React.useEffect(() => {
|
||||
if (visitorSession) {
|
||||
flushEventsSync();
|
||||
}
|
||||
}, [visitorSession, flushEventsSync]);
|
||||
|
||||
const trackEvent: TrackEventCallback = useEventCallback(
|
||||
(
|
||||
event: TrackEventInput<InsightsEventName>,
|
||||
@@ -169,7 +162,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
if (eventsRef.current[pathname].pageContext !== undefined) {
|
||||
// If the pageId is set, we know that the page_view event has been tracked
|
||||
// and we can flush the events
|
||||
if (options?.immediate && visitorIdRef.current) {
|
||||
if (options?.immediate) {
|
||||
flushBatchedEvents.cancel();
|
||||
flushEventsSync();
|
||||
} else {
|
||||
@@ -180,18 +173,14 @@ export function InsightsProvider(props: InsightsProviderProps) {
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the visitor ID and store it in a ref.
|
||||
* When the page is unloaded, flush all events.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
getVisitorId(appURL, visitorCookieTrackingEnabled).then((visitorId) => {
|
||||
visitorIdRef.current = visitorId;
|
||||
// When the page is unloaded, flush all events, but only if the visitor ID is set
|
||||
window.addEventListener('beforeunload', flushEventsSync);
|
||||
});
|
||||
window.addEventListener('beforeunload', flushEventsSync);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', flushEventsSync);
|
||||
};
|
||||
}, [flushEventsSync, visitorCookieTrackingEnabled, appURL]);
|
||||
}, [flushEventsSync]);
|
||||
|
||||
return (
|
||||
<InsightsContext.Provider value={trackEvent}>
|
||||
@@ -245,12 +234,12 @@ function transformEvents(input: {
|
||||
events: TrackEventInput<InsightsEventName>[];
|
||||
context: CurrentContentContext;
|
||||
pageContext: InsightsEventPageContext;
|
||||
visitorId: string;
|
||||
visitorSession: SessionResponse;
|
||||
sessionId: string;
|
||||
}): api.SiteInsightsEvent[] {
|
||||
const session: api.SiteInsightsEventSession = {
|
||||
sessionId: input.sessionId,
|
||||
visitorId: input.visitorId,
|
||||
visitorId: input.visitorSession.deviceId,
|
||||
userAgent: window.navigator.userAgent,
|
||||
language: window.navigator.language,
|
||||
cookies: getAllBrowserCookiesMap(),
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getBrowserCookie } from '@/lib/browser';
|
||||
|
||||
import { isCookiesTrackingDisabled } from './cookies';
|
||||
import { generateRandomId } from './utils';
|
||||
|
||||
const VISITORID_COOKIE = '__session';
|
||||
|
||||
let visitorId: string | null = null;
|
||||
let pendingVisitorId: Promise<string> | null = null;
|
||||
|
||||
/**
|
||||
* Return the current visitor identifier.
|
||||
*/
|
||||
export async function getVisitorId(
|
||||
appURL: string,
|
||||
visitorCookieTrackingEnabled: boolean
|
||||
): Promise<string> {
|
||||
if (!visitorId) {
|
||||
if (!pendingVisitorId) {
|
||||
pendingVisitorId = fetchVisitorID(appURL, visitorCookieTrackingEnabled).finally(() => {
|
||||
pendingVisitorId = null;
|
||||
});
|
||||
}
|
||||
|
||||
visitorId = await pendingVisitorId;
|
||||
}
|
||||
|
||||
return visitorId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Propose a visitor identifier to the GitBook.com server and get the devideId back.
|
||||
*/
|
||||
async function fetchVisitorID(
|
||||
appURL: string,
|
||||
visitorCookieTrackingEnabled: boolean
|
||||
): Promise<string> {
|
||||
const withoutCookies = isCookiesTrackingDisabled();
|
||||
|
||||
if (withoutCookies || !visitorCookieTrackingEnabled) {
|
||||
return generateRandomId();
|
||||
}
|
||||
|
||||
const existingTrackingCookie = getBrowserCookie(VISITORID_COOKIE);
|
||||
|
||||
if (existingTrackingCookie) {
|
||||
// If the cookie already exists, we'll just use that. Avoids a server request.
|
||||
return existingTrackingCookie;
|
||||
}
|
||||
// No tracking deviceId set, we'll need to consolidate with the server.
|
||||
const proposed = generateRandomId();
|
||||
|
||||
const url = new URL(appURL);
|
||||
url.pathname = '/__session/2/';
|
||||
url.searchParams.set('proposed', proposed);
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'GET', // Use GET to play nicely with SameSite cookies.
|
||||
credentials: 'include', // Make sure to send/receive cookies.
|
||||
cache: 'no-cache',
|
||||
mode: 'cors', // Need to use cors as we are on a different domain.
|
||||
});
|
||||
|
||||
const { deviceId } = (await resp.json()) as { deviceId: string };
|
||||
return deviceId;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch visitor session ID', error);
|
||||
return proposed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import { createStore, useStore } from 'zustand';
|
||||
|
||||
import { getBrowserCookie } from '@/lib/browser';
|
||||
|
||||
import React from 'react';
|
||||
import { isCookiesTrackingDisabled } from './cookies';
|
||||
import { generateRandomId } from './utils';
|
||||
|
||||
const VISITORID_COOKIE = '__session';
|
||||
|
||||
type SessionVisitorResponse = {
|
||||
deviceId: string;
|
||||
userId?: undefined;
|
||||
organizationId?: undefined;
|
||||
};
|
||||
|
||||
type SessionUserResponse = SessionVisitorResponse & {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export type SessionResponse = SessionVisitorResponse | SessionUserResponse;
|
||||
|
||||
const visitorSessionStore = createStore<{
|
||||
session: SessionResponse | null;
|
||||
pendingSession: Promise<SessionResponse> | null;
|
||||
}>(() => ({
|
||||
session: null,
|
||||
pendingSession: null,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Fetch, synchronize the visitor sesion with GitBook.
|
||||
*/
|
||||
export function VisitorSessionProvider(
|
||||
props: React.PropsWithChildren<{
|
||||
appURL: string;
|
||||
visitorCookieTrackingEnabled: boolean;
|
||||
}>
|
||||
) {
|
||||
const { appURL, visitorCookieTrackingEnabled, children } = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
const state = visitorSessionStore.getState();
|
||||
if (state.pendingSession || state.session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingSession = fetchSession({ appURL, visitorCookieTrackingEnabled });
|
||||
visitorSessionStore.setState({ pendingSession, session: null });
|
||||
pendingSession.then((session) => {
|
||||
visitorSessionStore.setState({ pendingSession: null, session });
|
||||
});
|
||||
}, [appURL, visitorCookieTrackingEnabled]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the current visitor session.
|
||||
*/
|
||||
export function useVisitorSession() {
|
||||
return useStore(visitorSessionStore, (state) => state.session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Propose a visitor identifier to the GitBook.com server and get the devideId back.
|
||||
*/
|
||||
async function fetchSession({
|
||||
appURL,
|
||||
visitorCookieTrackingEnabled,
|
||||
}: {
|
||||
appURL: string;
|
||||
visitorCookieTrackingEnabled: boolean;
|
||||
}): Promise<SessionResponse> {
|
||||
const withoutCookies = isCookiesTrackingDisabled();
|
||||
|
||||
if (withoutCookies || !visitorCookieTrackingEnabled) {
|
||||
return {
|
||||
deviceId: generateRandomId(),
|
||||
};
|
||||
}
|
||||
|
||||
const { existing, proposedId } = getSessionCookie();
|
||||
|
||||
if (existing) {
|
||||
// If the cookie already exists, we'll just use that. Avoids a server request.
|
||||
return existing;
|
||||
}
|
||||
|
||||
const url = new URL(appURL);
|
||||
url.pathname = '/__session/2/';
|
||||
url.searchParams.set('proposed', proposedId);
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'GET', // Use GET to play nicely with SameSite cookies.
|
||||
credentials: 'include', // Make sure to send/receive cookies.
|
||||
cache: 'no-cache',
|
||||
mode: 'cors', // Need to use cors as we are on a different domain.
|
||||
});
|
||||
|
||||
const session = (await resp.json()) as SessionResponse;
|
||||
return session;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch visitor session ID', error);
|
||||
return {
|
||||
deviceId: proposedId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionCookie(): { existing: SessionResponse | null; proposedId: string } {
|
||||
const proposed = generateRandomId();
|
||||
const value = getBrowserCookie(VISITORID_COOKIE);
|
||||
if (!(value && typeof value === 'string')) {
|
||||
return { existing: null, proposedId: proposed };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value) as SessionResponse;
|
||||
return { existing: parsed, proposedId: proposed };
|
||||
} catch {
|
||||
// Ignore legacy __session cookie format
|
||||
// and we'll renew it with the server using the previous one as a proposed ID
|
||||
return { existing: null, proposedId: proposed };
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,22 @@ import {
|
||||
ActionViewAsPDF,
|
||||
} from './PageActions';
|
||||
|
||||
interface PageActionsDropdownProps {
|
||||
siteTitle: string;
|
||||
markdownPageURL: string;
|
||||
mcpURL?: string;
|
||||
pdfURL?: string;
|
||||
className?: string;
|
||||
actions: SiteCustomizationSettings['pageActions'];
|
||||
export type PageActionsDropdownURLs = {
|
||||
html: string;
|
||||
markdown: string;
|
||||
mcp?: string;
|
||||
pdf?: string;
|
||||
editOnGit?: {
|
||||
provider: GitSyncState['installationProvider'];
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
|
||||
interface PageActionsDropdownProps {
|
||||
siteTitle: string;
|
||||
urls: PageActionsDropdownURLs;
|
||||
className?: string;
|
||||
actions: SiteCustomizationSettings['pageActions'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +80,7 @@ export function PageActionsDropdown(props: PageActionsDropdownProps) {
|
||||
* Return the list of actions to show in the dropdown menu.
|
||||
*/
|
||||
function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNode[] {
|
||||
const { siteTitle, markdownPageURL, mcpURL, actions } = props;
|
||||
const { siteTitle, urls, actions } = props;
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
@@ -94,55 +99,45 @@ function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNod
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionCopyMarkdown
|
||||
isDefaultAction={!assistants.length}
|
||||
markdownPageURL={markdownPageURL}
|
||||
markdownPageURL={urls.markdown}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
<ActionViewAsMarkdown markdownPageURL={markdownPageURL} type="dropdown-menu-item" />
|
||||
<ActionViewAsMarkdown markdownPageURL={urls.markdown} type="dropdown-menu-item" />
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
actions.externalAI ? (
|
||||
<React.Fragment key="externalAI">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionOpenInLLM
|
||||
provider="chatgpt"
|
||||
url={markdownPageURL}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
<ActionOpenInLLM
|
||||
provider="claude"
|
||||
url={markdownPageURL}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
<ActionOpenInLLM provider="chatgpt" url={urls.html} type="dropdown-menu-item" />
|
||||
<ActionOpenInLLM provider="claude" url={urls.html} type="dropdown-menu-item" />
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
actions.mcp && mcpURL ? (
|
||||
actions.mcp && urls.mcp ? (
|
||||
<React.Fragment key="mcp">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionCopyMCPURL mcpURL={mcpURL} type="dropdown-menu-item" />
|
||||
<ActionCopyMCPURL mcpURL={urls.mcp} type="dropdown-menu-item" />
|
||||
<ActionOpenMCP
|
||||
provider="vscode"
|
||||
mcpURL={mcpURL}
|
||||
mcpURL={urls.mcp}
|
||||
siteTitle={siteTitle}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
props.editOnGit || props.pdfURL ? (
|
||||
urls.editOnGit || urls.pdf ? (
|
||||
<React.Fragment key="editOnGit">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
{props.editOnGit ? (
|
||||
{urls.editOnGit ? (
|
||||
<ActionOpenEditOnGit
|
||||
type="dropdown-menu-item"
|
||||
provider={props.editOnGit.provider}
|
||||
url={props.editOnGit.url}
|
||||
provider={urls.editOnGit.provider}
|
||||
url={urls.editOnGit.url}
|
||||
/>
|
||||
) : null}
|
||||
{props.pdfURL ? (
|
||||
<ActionViewAsPDF url={props.pdfURL} type="dropdown-menu-item" />
|
||||
) : null}
|
||||
{urls.pdf ? <ActionViewAsPDF url={urls.pdf} type="dropdown-menu-item" /> : null}
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
].filter(Boolean);
|
||||
@@ -152,7 +147,7 @@ function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNod
|
||||
* A default action shown as a quick-access button beside the dropdown menu
|
||||
*/
|
||||
function getPageDefaultAction(props: PageActionsDropdownProps) {
|
||||
const { markdownPageURL, actions } = props;
|
||||
const { urls, actions } = props;
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
@@ -162,12 +157,12 @@ function getPageDefaultAction(props: PageActionsDropdownProps) {
|
||||
return <ActionOpenAssistant assistant={assistant} type="button" />;
|
||||
}
|
||||
|
||||
if (props.editOnGit) {
|
||||
if (urls.editOnGit) {
|
||||
return (
|
||||
<ActionOpenEditOnGit
|
||||
type="button"
|
||||
provider={props.editOnGit.provider}
|
||||
url={props.editOnGit.url}
|
||||
provider={urls.editOnGit.provider}
|
||||
url={urls.editOnGit.url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -176,7 +171,7 @@ function getPageDefaultAction(props: PageActionsDropdownProps) {
|
||||
return (
|
||||
<ActionCopyMarkdown
|
||||
isDefaultAction={!assistant}
|
||||
markdownPageURL={markdownPageURL}
|
||||
markdownPageURL={urls.markdown}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ export function PageAside(props: {
|
||||
<aside
|
||||
className={tcls(
|
||||
'group/aside',
|
||||
'order-last',
|
||||
'hidden',
|
||||
'pt-8',
|
||||
'pb-4',
|
||||
|
||||
@@ -110,7 +110,7 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
|
||||
'opacity-8',
|
||||
'contrast-more:opacity-11',
|
||||
|
||||
'sidebar-list-default:rounded-l-none',
|
||||
'sidebar-list-default:rounded-l-none!',
|
||||
'sidebar-list-default:border-l',
|
||||
'sidebar-list-default:border-tint',
|
||||
],
|
||||
|
||||
@@ -51,10 +51,10 @@ export function PageBody(props: {
|
||||
<main
|
||||
className={tcls(
|
||||
'relative min-w-0 flex-1',
|
||||
'mx-auto max-w-screen-2xl py-8',
|
||||
'max-w-screen-2xl py-8',
|
||||
// Allow words to break if they are too long.
|
||||
'break-anywhere',
|
||||
pageWidthWide ? 'page-width-wide 2xl:px-8' : 'page-width-default',
|
||||
pageWidthWide ? 'page-width-wide 3xl:px-8' : 'page-width-default',
|
||||
siteWidthWide ? 'site-width-wide' : 'site-width-default',
|
||||
page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
|
||||
)}
|
||||
@@ -81,7 +81,10 @@ export function PageBody(props: {
|
||||
blockStyle="page-api-block:ml-0"
|
||||
context={{
|
||||
mode: 'default',
|
||||
contentContext: context,
|
||||
contentContext: {
|
||||
...context,
|
||||
page,
|
||||
},
|
||||
withLinkPreviews,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,10 @@ import { type RevisionPageDocument, SiteVisibility } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import urlJoin from 'url-join';
|
||||
import { getPDFURLSearchParams } from '../PDF';
|
||||
import { PageActionsDropdown } from '../PageActions/PageActionsDropdown';
|
||||
import {
|
||||
PageActionsDropdown,
|
||||
type PageActionsDropdownURLs,
|
||||
} from '../PageActions/PageActionsDropdown';
|
||||
import { PageIcon } from '../PageIcon';
|
||||
import { StyledLink } from '../primitives';
|
||||
|
||||
@@ -40,35 +43,7 @@ export async function PageHeader(props: {
|
||||
// Show page actions if *any* of the actions are enabled
|
||||
<PageActionsDropdown
|
||||
siteTitle={context.site.title}
|
||||
markdownPageURL={`${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`}
|
||||
editOnGit={
|
||||
context.customization.git.showEditLink &&
|
||||
context.space.gitSync?.url &&
|
||||
page.git
|
||||
? {
|
||||
provider: context.space?.gitSync?.installationProvider,
|
||||
url: urlJoin(context.space.gitSync.url, page.git.path),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pdfURL={
|
||||
context.customization.pdf.enabled
|
||||
? context.linker.toPathInSpace(
|
||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
||||
page: page.id,
|
||||
only: true,
|
||||
limit: 100,
|
||||
}).toString()}`
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
mcpURL={
|
||||
context.site.visibility !== SiteVisibility.VisitorAuth
|
||||
? context.linker.toAbsoluteURL(
|
||||
context.linker.toPathInSpace('~gitbook/mcp')
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
urls={getPageActionsURLs(context, page)}
|
||||
actions={context.customization.pageActions}
|
||||
className={tcls(
|
||||
'float-right ml-4 xl:max-2xl:page-api-block:mr-62',
|
||||
@@ -146,3 +121,38 @@ export async function PageHeader(props: {
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URLs for the page actions.
|
||||
*/
|
||||
function getPageActionsURLs(
|
||||
context: GitBookSiteContext,
|
||||
page: RevisionPageDocument
|
||||
): PageActionsDropdownURLs {
|
||||
return {
|
||||
html: context.linker.toAbsoluteURL(
|
||||
context.linker.toPathForPage({ pages: context.revision.pages, page })
|
||||
),
|
||||
markdown: `${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`,
|
||||
editOnGit:
|
||||
context.customization.git.showEditLink && context.space.gitSync?.url && page.git
|
||||
? {
|
||||
provider: context.space?.gitSync?.installationProvider,
|
||||
url: urlJoin(context.space.gitSync.url, page.git.path),
|
||||
}
|
||||
: undefined,
|
||||
pdf: context.customization.pdf.enabled
|
||||
? context.linker.toPathInSpace(
|
||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
||||
page: page.id,
|
||||
only: true,
|
||||
limit: 100,
|
||||
}).toString()}`
|
||||
)
|
||||
: undefined,
|
||||
mcp:
|
||||
context.site.visibility !== SiteVisibility.VisitorAuth
|
||||
? context.linker.toAbsoluteURL(context.linker.toPathInSpace('~gitbook/mcp'))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type React from 'react';
|
||||
import { TranslateContext } from '@/intl/client';
|
||||
import type { TranslationLanguage } from '@/intl/translations';
|
||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||
import { HashProvider } from '../hooks';
|
||||
import { NavigationStatusProvider } from '../hooks';
|
||||
import { LoadingStateProvider } from '../primitives/LoadingStateProvider';
|
||||
|
||||
/**
|
||||
@@ -20,9 +20,9 @@ export function RootLayoutClientContexts(props: {
|
||||
return (
|
||||
<TranslateContext.Provider value={language}>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<HashProvider>
|
||||
<NavigationStatusProvider>
|
||||
<LoadingStateProvider>{children}</LoadingStateProvider>
|
||||
</HashProvider>
|
||||
</NavigationStatusProvider>
|
||||
</TooltipProvider>
|
||||
</TranslateContext.Provider>
|
||||
);
|
||||
|
||||
@@ -217,6 +217,7 @@
|
||||
::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
max-width: 8px;
|
||||
max-height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { CustomizationSearchStyle } from '@gitbook/api';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { CustomizationSearchStyle, type SiteSection } from '@gitbook/api';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@@ -16,11 +17,31 @@ import { SearchInput } from './SearchInput';
|
||||
import { SearchResults, type SearchResultsRef } from './SearchResults';
|
||||
import { SearchScopeToggle } from './SearchScopeToggle';
|
||||
import { useSearch } from './useSearch';
|
||||
import { useSearchResults } from './useSearchResults';
|
||||
import { useSearchResultsCursor } from './useSearchResultsCursor';
|
||||
|
||||
interface SearchContainerProps {
|
||||
/** The current site space id. */
|
||||
siteSpaceId: string;
|
||||
|
||||
/** The title of the current space. */
|
||||
spaceTitle: string;
|
||||
isMultiVariants: boolean;
|
||||
|
||||
/** The ids of all spaces in the current section. */
|
||||
siteSpaceIds: string[];
|
||||
|
||||
/** Whether there are sections on the site. */
|
||||
withSections: boolean;
|
||||
|
||||
/** The current section, displayed in search scope toggle. */
|
||||
section?: Pick<SiteSection, 'title' | 'icon'>;
|
||||
|
||||
/** Whether the current section has variants. */
|
||||
withVariants: boolean;
|
||||
|
||||
/** Whether any section on the site has variants. */
|
||||
withSiteVariants: boolean;
|
||||
|
||||
style: CustomizationSearchStyle;
|
||||
className?: string;
|
||||
viewport?: 'desktop' | 'mobile';
|
||||
@@ -30,7 +51,18 @@ interface SearchContainerProps {
|
||||
* Client component to render the search input and results.
|
||||
*/
|
||||
export function SearchContainer(props: SearchContainerProps) {
|
||||
const { siteSpaceId, spaceTitle, isMultiVariants, style, className, viewport } = props;
|
||||
const {
|
||||
siteSpaceId,
|
||||
spaceTitle,
|
||||
section,
|
||||
withVariants,
|
||||
withSiteVariants,
|
||||
withSections,
|
||||
style,
|
||||
className,
|
||||
viewport,
|
||||
siteSpaceIds,
|
||||
} = props;
|
||||
|
||||
const { assistants } = useAI();
|
||||
|
||||
@@ -108,7 +140,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
}
|
||||
setSearchState((prev) => ({
|
||||
ask: withAI ? (prev?.ask ?? null) : null,
|
||||
global: prev?.global ?? false,
|
||||
scope: prev?.scope ?? 'default',
|
||||
query: prev?.query ?? (withSearchAI || !withAI ? prev?.ask : null) ?? '',
|
||||
open: true,
|
||||
}));
|
||||
@@ -131,24 +163,11 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
resultsRef.current?.moveUp();
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
resultsRef.current?.moveDown();
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
resultsRef.current?.select();
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (value: string) => {
|
||||
setSearchState((prev) => ({
|
||||
ask: withAI && !withSearchAI ? (prev?.ask ?? null) : null, // When typing, we reset ask to get back to normal search (unless non-search assistants are defined)
|
||||
query: value,
|
||||
global: prev?.global ?? false,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: true,
|
||||
}));
|
||||
};
|
||||
@@ -157,10 +176,38 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
const normalizedQuery = state?.query?.trim() ?? '';
|
||||
const normalizedAsk = state?.ask?.trim() ?? '';
|
||||
|
||||
const showAsk = withSearchAI && normalizedAsk; // withSearchAI && normalizedAsk;
|
||||
const showAsk = withSearchAI && normalizedAsk;
|
||||
|
||||
const visible = viewport === 'desktop' ? !isMobile : viewport === 'mobile' ? isMobile : true;
|
||||
|
||||
const searchResultsId = `search-results-${React.useId()}`;
|
||||
const { results, fetching } = useSearchResults({
|
||||
disabled: !(state?.query || withAI),
|
||||
query: normalizedQuery,
|
||||
siteSpaceId,
|
||||
siteSpaceIds,
|
||||
scope: state?.scope ?? 'default',
|
||||
withAI: withAI,
|
||||
});
|
||||
const searchValue = state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? '';
|
||||
|
||||
const { cursor, moveBy: moveCursorBy } = useSearchResultsCursor({
|
||||
query: normalizedQuery,
|
||||
results,
|
||||
});
|
||||
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
moveCursorBy(-1);
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
moveCursorBy(1);
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
resultsRef.current?.select();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchAskProvider value={searchAsk}>
|
||||
<Popover
|
||||
@@ -168,15 +215,23 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
// Only show content if there's a query or Ask is enabled
|
||||
state?.query || withAI ? (
|
||||
<React.Suspense fallback={null}>
|
||||
{isMultiVariants && !showAsk ? (
|
||||
<SearchScopeToggle spaceTitle={spaceTitle} />
|
||||
{(withVariants || withSections) && !showAsk ? (
|
||||
<SearchScopeToggle
|
||||
section={section}
|
||||
spaceTitle={spaceTitle}
|
||||
withVariants={withVariants}
|
||||
withSiteVariants={withSiteVariants}
|
||||
withSections={withSections}
|
||||
/>
|
||||
) : null}
|
||||
{state !== null && !showAsk ? (
|
||||
<SearchResults
|
||||
ref={resultsRef}
|
||||
query={normalizedQuery}
|
||||
global={state?.global ?? false}
|
||||
siteSpaceId={siteSpaceId}
|
||||
id={searchResultsId}
|
||||
fetching={fetching}
|
||||
results={results}
|
||||
cursor={cursor}
|
||||
/>
|
||||
) : null}
|
||||
{showAsk ? <SearchAskAnswer query={normalizedAsk} /> : null}
|
||||
@@ -184,22 +239,21 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
) : null
|
||||
}
|
||||
rootProps={{
|
||||
open: visible && (state?.open ?? false),
|
||||
onOpenChange: (open) => {
|
||||
open ? onOpen() : onClose();
|
||||
},
|
||||
modal: isMobile,
|
||||
open: Boolean(visible && (state?.open ?? false)),
|
||||
modal: false,
|
||||
}}
|
||||
contentProps={{
|
||||
onOpenAutoFocus: (event) => event.preventDefault(),
|
||||
align: 'start',
|
||||
className:
|
||||
'bg-tint-base has-[.empty]:hidden gutter-stable scroll-py-2 w-128 p-2 pr-1 max-h-[min(32rem,var(--radix-popover-content-available-height))] max-w-[min(var(--radix-popover-content-available-width),32rem)]',
|
||||
'@container overflow-y-scroll bg-tint-base has-[.empty]:hidden scroll-py-2 w-128 p-2 max-h-[min(32rem,var(--radix-popover-content-available-height))] max-w-[min(var(--radix-popover-content-available-width),32rem)]',
|
||||
onInteractOutside: (event) => {
|
||||
// Don't close if clicking on the search input itself
|
||||
if (searchInputRef.current?.contains(event.target as Node)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
},
|
||||
sideOffset: 8,
|
||||
collisionPadding: {
|
||||
@@ -216,14 +270,23 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
>
|
||||
<SearchInput
|
||||
ref={searchInputRef}
|
||||
value={state?.query ?? (withSearchAI || !withAI ? state?.ask : null) ?? ''}
|
||||
value={searchValue}
|
||||
onFocus={onOpen}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
withAI={withSearchAI}
|
||||
isOpen={state?.open ?? false}
|
||||
className={className}
|
||||
/>
|
||||
aria-controls={searchResultsId}
|
||||
aria-activedescendant={
|
||||
cursor !== null ? `${searchResultsId}-${cursor}` : undefined
|
||||
}
|
||||
>
|
||||
<LiveResultsAnnouncer
|
||||
count={results.length}
|
||||
showing={Boolean(searchValue) && !fetching}
|
||||
/>
|
||||
</SearchInput>
|
||||
</Popover>
|
||||
{assistants
|
||||
.filter((assistant) => assistant.ui === true)
|
||||
@@ -241,3 +304,21 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
</SearchAskProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Screen reader announcement for search results.
|
||||
* Without it there is no feedback for screen reader users when a search returns no results.
|
||||
*/
|
||||
function LiveResultsAnnouncer(props: { count: number; showing: boolean }) {
|
||||
const { count, showing } = props;
|
||||
const language = useLanguage();
|
||||
return (
|
||||
<div className="sr-only" aria-live="assertive" role="alert" aria-relevant="all">
|
||||
{showing
|
||||
? count > 0
|
||||
? t(language, 'search_results_count', count)
|
||||
: t(language, 'search_no_results')
|
||||
: ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface SearchInputProps {
|
||||
withAI: boolean;
|
||||
isOpen: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Size classes for medium size button
|
||||
@@ -26,7 +27,17 @@ const sizeClasses = ['text-sm', 'px-3.5', 'py-1.5', 'md:circular-corners:px-4'];
|
||||
*/
|
||||
export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
||||
function SearchInput(props, ref) {
|
||||
const { onChange, onKeyDown, onFocus, value, withAI, isOpen, className } = props;
|
||||
const {
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onFocus,
|
||||
value,
|
||||
withAI,
|
||||
isOpen,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const language = useLanguage();
|
||||
@@ -61,7 +72,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
||||
'has-[input:focus]:-translate-y-px h-9 grow 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 md:cursor-text',
|
||||
'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.
|
||||
'relative z-30 shrink grow justify-start max-md:absolute max-md:right-0',
|
||||
'relative z-30 max-w-none shrink grow justify-start max-md:absolute max-md:right-0',
|
||||
isOpen ? 'max-md:w-56' : 'max-md:w-[38px]'
|
||||
)}
|
||||
>
|
||||
@@ -84,8 +95,9 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
||||
className="size-4 shrink-0 animate-scale-in"
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
<input
|
||||
{...rest}
|
||||
type="text"
|
||||
onFocus={onFocus}
|
||||
onKeyDown={onKeyDown}
|
||||
@@ -100,6 +112,12 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
||||
'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-md:opacity-0'
|
||||
)}
|
||||
role="combobox"
|
||||
autoComplete="off"
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={value && isOpen ? 'true' : 'false'}
|
||||
// Forward
|
||||
ref={inputRef}
|
||||
/>
|
||||
{!isOpen ? <Shortcut /> : null}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
|
||||
},
|
||||
ref: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
const { query, item, active } = props;
|
||||
const { query, item, active, ...rest } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
const breadcrumbs =
|
||||
@@ -41,6 +41,8 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
|
||||
spaceId: item.spaceId,
|
||||
},
|
||||
}}
|
||||
aria-label={tString(language, 'search_page_result_title', item.title)}
|
||||
{...rest}
|
||||
>
|
||||
{breadcrumbs.length > 0 ? (
|
||||
<div
|
||||
|
||||
@@ -14,7 +14,7 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
|
||||
},
|
||||
ref: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
const { question, recommended = false, active, assistant } = props;
|
||||
const { question, recommended = false, active, assistant, ...rest } = props;
|
||||
const language = useLanguage();
|
||||
const getLinkProp = useSearchLink();
|
||||
|
||||
@@ -38,6 +38,7 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
|
||||
active={active}
|
||||
leadingIcon={recommended ? 'search' : assistant.icon}
|
||||
className={recommended ? 'pr-1.5' : ''}
|
||||
{...rest}
|
||||
>
|
||||
{recommended ? (
|
||||
question
|
||||
|
||||
@@ -51,6 +51,7 @@ export const SearchResultItem = React.forwardRef(function SearchResultItem(
|
||||
: null,
|
||||
className
|
||||
)}
|
||||
role="option"
|
||||
{...rest}
|
||||
>
|
||||
<div className="size-4 shrink-0 text-tint-subtle">
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { readStreamableValue } from 'ai/rsc';
|
||||
import assertNever from 'assert-never';
|
||||
import React from 'react';
|
||||
|
||||
import type { Assistant } from '@/components/AI';
|
||||
import { type Assistant, useAI } from '@/components/AI';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { useAI } from '../AI';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
|
||||
import { Loading } from '../primitives';
|
||||
import { SearchPageResultItem } from './SearchPageResultItem';
|
||||
import { SearchQuestionResultItem } from './SearchQuestionResultItem';
|
||||
import { SearchSectionResultItem } from './SearchSectionResultItem';
|
||||
import {
|
||||
type OrderedComputedResult,
|
||||
searchAllSiteContent,
|
||||
searchSiteSpaceContent,
|
||||
streamRecommendedQuestions,
|
||||
} from './server-actions';
|
||||
import type { OrderedComputedResult } from './server-actions';
|
||||
|
||||
export interface SearchResultsRef {
|
||||
moveUp(): void;
|
||||
moveDown(): void;
|
||||
select(): void;
|
||||
}
|
||||
|
||||
@@ -32,14 +22,6 @@ type ResultType =
|
||||
| { type: 'question'; id: string; query: string; assistant: Assistant }
|
||||
| { type: 'recommended-question'; id: string; question: string };
|
||||
|
||||
/**
|
||||
* We cache the recommended questions globally to avoid calling the API multiple times
|
||||
* when re-opening the search modal. The cache is per space, so that we can
|
||||
* have different recommended questions for different spaces of the same site.
|
||||
* It should not be used outside of an useEffect.
|
||||
*/
|
||||
const cachedRecommendedQuestions: Map<string, ResultType[]> = new Map();
|
||||
|
||||
/**
|
||||
* Fetch the results of the keyboard navigable elements to display for a query:
|
||||
* - Recommended questions if no query is provided.
|
||||
@@ -49,134 +31,20 @@ const cachedRecommendedQuestions: Map<string, ResultType[]> = new Map();
|
||||
export const SearchResults = React.forwardRef(function SearchResults(
|
||||
props: {
|
||||
children?: React.ReactNode;
|
||||
id: string;
|
||||
query: string;
|
||||
global: boolean;
|
||||
siteSpaceId: string;
|
||||
results: ResultType[];
|
||||
fetching: boolean;
|
||||
cursor: number | null;
|
||||
},
|
||||
ref: React.Ref<SearchResultsRef>
|
||||
) {
|
||||
const { children, query, global, siteSpaceId } = props;
|
||||
const { children, id, query, results, fetching, cursor } = props;
|
||||
|
||||
const language = useLanguage();
|
||||
const trackEvent = useTrackEvent();
|
||||
const [resultsState, setResultsState] = React.useState<{
|
||||
results: ResultType[];
|
||||
fetching: boolean;
|
||||
}>({ results: [], fetching: true });
|
||||
const [cursor, setCursor] = React.useState<number | null>(null);
|
||||
|
||||
const refs = React.useRef<(null | HTMLAnchorElement)[]>([]);
|
||||
|
||||
const { assistants } = useAI();
|
||||
const withAI = assistants.length > 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!query) {
|
||||
if (!withAI) {
|
||||
setResultsState({ results: [], fetching: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedRecommendedQuestions.has(siteSpaceId)) {
|
||||
const results = cachedRecommendedQuestions.get(siteSpaceId);
|
||||
assert(
|
||||
results,
|
||||
`Cached recommended questions should be set for site-space ${siteSpaceId}`
|
||||
);
|
||||
setResultsState({ results, fetching: false });
|
||||
return;
|
||||
}
|
||||
|
||||
setResultsState({ results: [], fetching: false });
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
// We currently have a bug where the same question can be returned multiple times.
|
||||
// This is a workaround to avoid that.
|
||||
const questions = new Set<string>();
|
||||
const recommendedQuestions: ResultType[] = [];
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await streamRecommendedQuestions({ siteSpaceId });
|
||||
for await (const entry of readStreamableValue(response.stream)) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { question } = entry;
|
||||
if (questions.has(question)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
questions.add(question);
|
||||
recommendedQuestions.push({
|
||||
type: 'recommended-question',
|
||||
id: question,
|
||||
question,
|
||||
});
|
||||
cachedRecommendedQuestions.set(siteSpaceId, recommendedQuestions);
|
||||
|
||||
if (!cancelled) {
|
||||
setResultsState({ results: [...recommendedQuestions], fetching: false });
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
setResultsState((prev) => ({ results: prev.results, fetching: true }));
|
||||
let cancelled = false;
|
||||
const timeout = setTimeout(async () => {
|
||||
const results = await (global
|
||||
? searchAllSiteContent(query)
|
||||
: searchSiteSpaceContent(query));
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
setResultsState({ results: [], fetching: false });
|
||||
return;
|
||||
}
|
||||
|
||||
setResultsState({ results, fetching: false });
|
||||
|
||||
trackEvent({
|
||||
type: 'search_type_query',
|
||||
query,
|
||||
});
|
||||
}, 350);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [query, global, trackEvent, withAI, siteSpaceId]);
|
||||
|
||||
const results: ResultType[] = React.useMemo(() => {
|
||||
if (!withAI) {
|
||||
return resultsState.results;
|
||||
}
|
||||
return withAskTriggers(resultsState.results, query, assistants);
|
||||
}, [resultsState.results, query, withAI]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!query) {
|
||||
// Reset the cursor when there's no query
|
||||
setCursor(null);
|
||||
} else if (results.length > 0) {
|
||||
// Auto-focus the first result
|
||||
setCursor(0);
|
||||
}
|
||||
}, [results, query]);
|
||||
|
||||
// Scroll to the active result.
|
||||
React.useEffect(() => {
|
||||
if (cursor === null || !refs.current[cursor]) {
|
||||
@@ -189,19 +57,6 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
});
|
||||
}, [cursor]);
|
||||
|
||||
const moveBy = React.useCallback(
|
||||
(delta: number) => {
|
||||
setCursor((prev) => {
|
||||
if (prev === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.max(Math.min(prev + delta, results.length - 1), 0);
|
||||
});
|
||||
},
|
||||
[results]
|
||||
);
|
||||
|
||||
const select = React.useCallback(() => {
|
||||
if (cursor === null || !refs.current[cursor]) {
|
||||
return;
|
||||
@@ -213,18 +68,14 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
React.useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
moveUp: () => {
|
||||
moveBy(-1);
|
||||
},
|
||||
moveDown: () => {
|
||||
moveBy(1);
|
||||
},
|
||||
select,
|
||||
}),
|
||||
[moveBy, select]
|
||||
[select]
|
||||
);
|
||||
|
||||
if (resultsState.fetching) {
|
||||
const { assistants } = useAI();
|
||||
|
||||
if (fetching) {
|
||||
return (
|
||||
<div className={tcls('flex', 'items-center', 'justify-center', 'py-8', 'h-full')}>
|
||||
<Loading className={tcls('w-6', 'text-tint/6')} />
|
||||
@@ -258,8 +109,19 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div data-testid="search-results" className="flex flex-col gap-y-1">
|
||||
<div
|
||||
data-testid="search-results"
|
||||
className="flex flex-col gap-y-1"
|
||||
id={id}
|
||||
role="listbox"
|
||||
aria-live="polite"
|
||||
>
|
||||
{results.map((item, index) => {
|
||||
const resultItemProps = {
|
||||
'aria-posinset': index + 1,
|
||||
'aria-setsize': results.length,
|
||||
id: `${id}-${index}`,
|
||||
};
|
||||
switch (item.type) {
|
||||
case 'page': {
|
||||
return (
|
||||
@@ -271,6 +133,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
query={query}
|
||||
item={item}
|
||||
active={index === cursor}
|
||||
{...resultItemProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -284,6 +147,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
question={query}
|
||||
active={index === cursor}
|
||||
assistant={item.assistant}
|
||||
{...resultItemProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -298,6 +162,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
active={index === cursor}
|
||||
assistant={assistants[0]!}
|
||||
recommended
|
||||
{...resultItemProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -311,6 +176,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
query={query}
|
||||
item={item}
|
||||
active={index === cursor}
|
||||
{...resultItemProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -325,28 +191,3 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Add a "Ask <question>" item at the top of the results list.
|
||||
*/
|
||||
function withAskTriggers(
|
||||
results: ResultType[],
|
||||
query: string,
|
||||
assistants: Assistant[]
|
||||
): ResultType[] {
|
||||
const without = results.filter((result) => result.type !== 'question');
|
||||
|
||||
if (query.length === 0) {
|
||||
return without;
|
||||
}
|
||||
|
||||
return [
|
||||
...assistants.map((assistant, index) => ({
|
||||
type: 'question' as const,
|
||||
id: `question-${index}`,
|
||||
query,
|
||||
assistant,
|
||||
})),
|
||||
...(without ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import { Button } from '../primitives';
|
||||
import type { SiteSection } from '@gitbook/api';
|
||||
import { Button, Checkbox, DropdownMenu, DropdownMenuItem } from '../primitives';
|
||||
import { SegmentedControl, SegmentedControlItem } from '../primitives/SegmentedControl';
|
||||
import { useSearch } from './useSearch';
|
||||
|
||||
/**
|
||||
* Toolbar to toggle between search modes (global or scoped to a space).
|
||||
* Only visible when the space is in a collection.
|
||||
*/
|
||||
export function SearchScopeToggle(props: { spaceTitle: string }) {
|
||||
const { spaceTitle } = props;
|
||||
export function SearchScopeToggle(props: {
|
||||
spaceTitle: string;
|
||||
section?: Pick<SiteSection, 'title' | 'icon'>;
|
||||
withVariants: boolean;
|
||||
withSiteVariants: boolean;
|
||||
withSections: boolean;
|
||||
}) {
|
||||
const { spaceTitle, section, withVariants, withSections, withSiteVariants } = props;
|
||||
const [state, setSearchState] = useSearch();
|
||||
const language = useLanguage();
|
||||
|
||||
@@ -15,38 +25,109 @@ export function SearchScopeToggle(props: { spaceTitle: string }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Whether to include all variants in the search
|
||||
const scopeIsExtended = ['extended', 'all'].includes(state.scope);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-orientation="horizontal"
|
||||
className="mb-2 flex flex-row flex-wrap gap-1 circular-corners:rounded-3xl rounded-corners:rounded-lg bg-tint-subtle p-1"
|
||||
>
|
||||
<Button
|
||||
variant="blank"
|
||||
size="medium"
|
||||
className="shrink grow justify-center whitespace-normal"
|
||||
active={!state.global}
|
||||
label={tString(language, 'search_scope_space', spaceTitle)}
|
||||
onClick={() => {
|
||||
setSearchState({
|
||||
...state,
|
||||
global: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="blank"
|
||||
size="medium"
|
||||
className="shrink grow justify-center whitespace-normal"
|
||||
active={state.global}
|
||||
label={tString(language, 'search_scope_all')}
|
||||
onClick={() => {
|
||||
setSearchState({
|
||||
...state,
|
||||
global: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
{withSections ? (
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<SegmentedControl className="grow animate-scale-in">
|
||||
<SegmentedControlItem
|
||||
active={['default', 'all'].includes(state.scope)}
|
||||
label={tString(language, 'search_scope_all')}
|
||||
icon="infinity"
|
||||
onClick={() =>
|
||||
setSearchState({
|
||||
...state,
|
||||
scope: scopeIsExtended ? 'all' : 'default',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SegmentedControlItem
|
||||
active={['current', 'extended'].includes(state.scope)}
|
||||
icon={section?.icon}
|
||||
label={tString(language, 'search_scope_current', section?.title)}
|
||||
onClick={() =>
|
||||
setSearchState({
|
||||
...state,
|
||||
scope: scopeIsExtended ? 'extended' : 'current',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SegmentedControl>
|
||||
{withSiteVariants ? (
|
||||
<DropdownMenu
|
||||
button={
|
||||
<Button
|
||||
icon="ellipsis"
|
||||
iconOnly
|
||||
size="default"
|
||||
variant="blank"
|
||||
label={tString(language, 'more')}
|
||||
className="shrink-0"
|
||||
active={scopeIsExtended}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setSearchState({
|
||||
...state,
|
||||
scope: (() => {
|
||||
switch (state.scope) {
|
||||
case 'extended':
|
||||
return 'current';
|
||||
case 'current':
|
||||
return 'extended';
|
||||
case 'all':
|
||||
return 'default';
|
||||
default:
|
||||
return 'all';
|
||||
}
|
||||
})(),
|
||||
})
|
||||
}
|
||||
active={scopeIsExtended}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox checked={scopeIsExtended} />
|
||||
<div className="flex flex-col items-start">
|
||||
Include all variants
|
||||
<div className="text-tint-subtle text-xs">
|
||||
Search content in every language/version
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
) : withVariants ? (
|
||||
<SegmentedControl className="mb-2 animate-scale-in">
|
||||
<SegmentedControlItem
|
||||
size={withSections ? 'small' : 'medium'}
|
||||
active={!scopeIsExtended}
|
||||
className="py-1"
|
||||
label={tString(language, 'search_scope_current', spaceTitle)}
|
||||
onClick={() =>
|
||||
setSearchState({
|
||||
...state,
|
||||
scope: 'default',
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<SegmentedControlItem
|
||||
size={withSections ? 'small' : 'medium'}
|
||||
active={scopeIsExtended}
|
||||
className="py-1"
|
||||
icon="infinity"
|
||||
label={tString(language, 'search_scope_all')}
|
||||
onClick={() => setSearchState({ ...state, scope: 'all' })}
|
||||
/>
|
||||
</SegmentedControl>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
|
||||
},
|
||||
ref: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
const { query, item, active } = props;
|
||||
const { query, item, active, ...rest } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
return (
|
||||
@@ -32,6 +32,18 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
|
||||
spaceId: item.spaceId,
|
||||
},
|
||||
}}
|
||||
aria-label={
|
||||
item.title
|
||||
? tString(language, 'search_section_result_title', item.title)
|
||||
: item.body
|
||||
? tString(
|
||||
language,
|
||||
'search_section_result_content',
|
||||
getAbbreviatedBody(item.body, query)
|
||||
)
|
||||
: tString(language, 'search_section_result_default')
|
||||
}
|
||||
{...rest}
|
||||
>
|
||||
<div className="grow border-tint-subtle border-l-2 pl-4">
|
||||
{item.title ? (
|
||||
@@ -51,7 +63,12 @@ function highlightQueryInBody(body: string, query: string) {
|
||||
// Ensure the query to be highlighted is visible in the body.
|
||||
return (
|
||||
<p className="wrap-anywhere relative line-clamp-3 text-sm">
|
||||
<HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 10)}`} />
|
||||
<HighlightQuery query={query} text={idx < 20 ? body : `…${body.slice(idx - 10)}`} />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function getAbbreviatedBody(body: string, query: string) {
|
||||
const idx = body.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
|
||||
return idx < 20 ? body.slice(0, 100) : `…${body.slice(idx - 10, idx + query.length + 30)}…`;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
SearchSpaceResult,
|
||||
SiteSection,
|
||||
SiteSectionGroup,
|
||||
SiteSpace,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { createStreamableValue } from 'ai/rsc';
|
||||
@@ -81,18 +82,33 @@ export async function searchAllSiteContent(query: string): Promise<OrderedComput
|
||||
/**
|
||||
* Server action to search content in a space.
|
||||
*/
|
||||
export async function searchSiteSpaceContent(query: string): Promise<OrderedComputedResult[]> {
|
||||
export async function searchCurrentSiteSpaceContent(
|
||||
query: string,
|
||||
siteSpaceId: string
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
return traceErrorOnly('Search.searchSiteSpaceContent', async () => {
|
||||
const context = await getServerActionBaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
return await searchSiteContent(context, {
|
||||
query,
|
||||
// If we have a siteSectionId that means its a sections site use `current` mode
|
||||
// which searches in the current space + all default spaces of sections
|
||||
scope: siteURLData.siteSection
|
||||
? { mode: 'current', siteSpaceId: siteURLData.siteSpace }
|
||||
: { mode: 'specific', siteSpaceIds: [siteURLData.siteSpace] },
|
||||
scope: { mode: 'current', siteSpaceId },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Server action to search content in a specific space.
|
||||
*/
|
||||
export async function searchSpecificSiteSpaceContent(
|
||||
query: string,
|
||||
siteSpaceIds: string[]
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
return traceErrorOnly('Search.searchSiteSpaceContent', async () => {
|
||||
const context = await getServerActionBaseContext();
|
||||
|
||||
return await searchSiteContent(context, {
|
||||
query,
|
||||
scope: { mode: 'specific', siteSpaceIds },
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -281,6 +297,7 @@ async function searchSiteContent(
|
||||
transformSitePageResult(context, {
|
||||
pageItem,
|
||||
spaceItem,
|
||||
siteSpace: found?.siteSpace,
|
||||
space: found?.siteSpace.space,
|
||||
spaceURL: found?.siteSpace.urls.published,
|
||||
siteSection: siteSection ?? undefined,
|
||||
@@ -369,12 +386,13 @@ async function transformSitePageResult(
|
||||
pageItem: SearchPageResult;
|
||||
spaceItem: SearchSpaceResult;
|
||||
space?: Space;
|
||||
siteSpace?: SiteSpace;
|
||||
spaceURL?: string;
|
||||
siteSection?: SiteSection;
|
||||
siteSectionGroup?: SiteSectionGroup;
|
||||
}
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
const { pageItem, spaceItem, space, spaceURL, siteSection, siteSectionGroup } = args;
|
||||
const { pageItem, spaceItem, spaceURL, siteSection, siteSectionGroup, siteSpace } = args;
|
||||
const { linker } = context;
|
||||
|
||||
const page: ComputedPageResult = {
|
||||
@@ -395,9 +413,9 @@ async function transformSitePageResult(
|
||||
icon: siteSection?.icon as IconName,
|
||||
label: siteSection.title,
|
||||
},
|
||||
(!siteSection || siteSection?.siteSpaces.length > 1) && space
|
||||
(!siteSection || siteSection?.siteSpaces.length > 1) && siteSpace
|
||||
? {
|
||||
label: space?.title,
|
||||
label: siteSpace.title,
|
||||
}
|
||||
: undefined,
|
||||
].filter((item) => item !== undefined),
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { parseAsBoolean, parseAsString, useQueryStates } from 'nuqs';
|
||||
import { parseAsBoolean, parseAsString, parseAsStringLiteral, useQueryStates } from 'nuqs';
|
||||
import React from 'react';
|
||||
import type { LinkProps } from '../primitives';
|
||||
|
||||
export type SearchScope =
|
||||
/** Search all content on the site */
|
||||
| 'all'
|
||||
/** Search the current section's variant + matched/default variant for other sections */
|
||||
| 'default'
|
||||
/** Search all variants of the current section */
|
||||
| 'extended'
|
||||
/** Search only the current section's current variant */
|
||||
| 'current';
|
||||
|
||||
export interface SearchState {
|
||||
// URL-backed state
|
||||
query: string | null;
|
||||
ask: string | null;
|
||||
global: boolean;
|
||||
scope: SearchScope;
|
||||
|
||||
// Local UI state
|
||||
open: boolean;
|
||||
@@ -18,7 +28,8 @@ export interface SearchState {
|
||||
const keyMap = {
|
||||
q: parseAsString,
|
||||
ask: parseAsString,
|
||||
global: parseAsBoolean,
|
||||
scope: parseAsStringLiteral(['all', 'default', 'extended', 'current']).withDefault('default'),
|
||||
global: parseAsBoolean, // Legacy support for global=true
|
||||
};
|
||||
|
||||
export type UpdateSearchState = (
|
||||
@@ -41,14 +52,21 @@ export function SearchContextProvider(props: React.PropsWithChildren): React.Rea
|
||||
history: 'replace',
|
||||
});
|
||||
|
||||
// Handle legacy ask=true format by converting it to the new format
|
||||
React.useEffect(() => {
|
||||
// Handle legacy ask=true format by converting it to the new format
|
||||
if (rawState?.ask === 'true' && rawState?.q) {
|
||||
// Convert legacy format: q=query&ask=true -> ask=query&q=null
|
||||
setRawState({
|
||||
q: null,
|
||||
ask: rawState.q,
|
||||
global: rawState.global,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle legacy global=true
|
||||
if (rawState?.global === true) {
|
||||
setRawState({
|
||||
scope: 'all',
|
||||
global: null, // Remove the legacy parameter
|
||||
});
|
||||
}
|
||||
}, [rawState, setRawState]);
|
||||
@@ -65,7 +83,7 @@ export function SearchContextProvider(props: React.PropsWithChildren): React.Rea
|
||||
return {
|
||||
query: rawState.q,
|
||||
ask: rawState.ask,
|
||||
global: !!rawState.global,
|
||||
scope: rawState.scope,
|
||||
open,
|
||||
};
|
||||
}, [rawState, open]);
|
||||
@@ -85,14 +103,14 @@ export function SearchContextProvider(props: React.PropsWithChildren): React.Rea
|
||||
|
||||
if (update === null) {
|
||||
setIsOpen(false);
|
||||
return setRawState({ q: null, ask: null, global: null });
|
||||
return setRawState({ q: null, ask: null, scope: 'default' });
|
||||
}
|
||||
|
||||
setIsOpen(update.open);
|
||||
return setRawState({
|
||||
q: update.query,
|
||||
ask: update.ask,
|
||||
global: update.global ? true : null,
|
||||
scope: update.scope,
|
||||
});
|
||||
},
|
||||
[setRawState]
|
||||
@@ -126,7 +144,7 @@ export function useSearchLink(): (
|
||||
const searchParams = new URLSearchParams();
|
||||
params.query ? searchParams.set('q', params.query) : searchParams.delete('q');
|
||||
params.ask ? searchParams.set('ask', params.ask) : searchParams.delete('ask');
|
||||
params.global ? searchParams.set('global', 'true') : searchParams.delete('global');
|
||||
params.scope ? searchParams.set('scope', params.scope) : searchParams.delete('scope');
|
||||
return {
|
||||
href: `?${searchParams.toString()}`,
|
||||
prefetch: false,
|
||||
@@ -137,7 +155,7 @@ export function useSearchLink(): (
|
||||
...prev,
|
||||
query: params.query !== undefined ? params.query : null,
|
||||
ask: params.ask !== undefined ? params.ask : null,
|
||||
global: params.global !== undefined ? params.global : false,
|
||||
scope: params.scope !== undefined ? params.scope : 'default',
|
||||
open: params.open !== undefined ? params.open : false,
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { readStreamableValue } from 'ai/rsc';
|
||||
import React from 'react';
|
||||
|
||||
import { assert } from 'ts-essentials';
|
||||
|
||||
import {
|
||||
type OrderedComputedResult,
|
||||
searchAllSiteContent,
|
||||
searchCurrentSiteSpaceContent,
|
||||
searchSpecificSiteSpaceContent,
|
||||
streamRecommendedQuestions,
|
||||
} from './server-actions';
|
||||
|
||||
import { type Assistant, useAI } from '@/components/AI';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import type { SearchScope } from './useSearch';
|
||||
|
||||
export type ResultType =
|
||||
| OrderedComputedResult
|
||||
| { type: 'question'; id: string; query: string; assistant: Assistant }
|
||||
| { type: 'recommended-question'; id: string; question: string };
|
||||
|
||||
/**
|
||||
* We cache the recommended questions globally to avoid calling the API multiple times
|
||||
* when re-opening the search modal. The cache is per space, so that we can
|
||||
* have different recommended questions for different spaces of the same site.
|
||||
* It should not be used outside of an useEffect.
|
||||
*/
|
||||
const cachedRecommendedQuestions: Map<string, ResultType[]> = new Map();
|
||||
|
||||
export function useSearchResults(props: {
|
||||
disabled: boolean;
|
||||
query: string;
|
||||
siteSpaceId: string;
|
||||
siteSpaceIds: string[];
|
||||
scope: SearchScope;
|
||||
withAI: boolean;
|
||||
}) {
|
||||
const { disabled, query, siteSpaceId, siteSpaceIds, scope } = props;
|
||||
|
||||
const trackEvent = useTrackEvent();
|
||||
|
||||
const [resultsState, setResultsState] = React.useState<{
|
||||
results: ResultType[];
|
||||
fetching: boolean;
|
||||
}>({ results: [], fetching: false });
|
||||
|
||||
const { assistants } = useAI();
|
||||
const withAI = assistants.length > 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (!query) {
|
||||
if (!withAI) {
|
||||
setResultsState({ results: [], fetching: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedRecommendedQuestions.has(siteSpaceId)) {
|
||||
const results = cachedRecommendedQuestions.get(siteSpaceId);
|
||||
assert(
|
||||
results,
|
||||
`Cached recommended questions should be set for site-space ${siteSpaceId}`
|
||||
);
|
||||
setResultsState({ results, fetching: false });
|
||||
return;
|
||||
}
|
||||
|
||||
setResultsState({ results: [], fetching: false });
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
// We currently have a bug where the same question can be returned multiple times.
|
||||
// This is a workaround to avoid that.
|
||||
const questions = new Set<string>();
|
||||
const recommendedQuestions: ResultType[] = [];
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await streamRecommendedQuestions({ siteSpaceId });
|
||||
for await (const entry of readStreamableValue(response.stream)) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { question } = entry;
|
||||
if (questions.has(question)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
questions.add(question);
|
||||
recommendedQuestions.push({
|
||||
type: 'recommended-question',
|
||||
id: question,
|
||||
question,
|
||||
});
|
||||
cachedRecommendedQuestions.set(siteSpaceId, recommendedQuestions);
|
||||
|
||||
if (!cancelled) {
|
||||
setResultsState({ results: [...recommendedQuestions], fetching: false });
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
setResultsState((prev) => ({ results: prev.results, fetching: true }));
|
||||
let cancelled = false;
|
||||
const timeout = setTimeout(async () => {
|
||||
const results = await (() => {
|
||||
if (scope === 'all') {
|
||||
// Search all content on the site
|
||||
return searchAllSiteContent(query);
|
||||
}
|
||||
if (scope === 'default') {
|
||||
// Search the current section's variant + matched/default variant for other sections
|
||||
return searchCurrentSiteSpaceContent(query, siteSpaceId);
|
||||
}
|
||||
if (scope === 'extended') {
|
||||
// Search all variants of the current section
|
||||
return searchSpecificSiteSpaceContent(query, siteSpaceIds);
|
||||
}
|
||||
if (scope === 'current') {
|
||||
// Search only the current section's current variant
|
||||
return searchSpecificSiteSpaceContent(query, [siteSpaceId]);
|
||||
}
|
||||
throw new Error(`Unhandled search scope: ${scope}`);
|
||||
})();
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
setResultsState({ results: [], fetching: false });
|
||||
return;
|
||||
}
|
||||
|
||||
setResultsState({ results, fetching: false });
|
||||
|
||||
trackEvent({
|
||||
type: 'search_type_query',
|
||||
query,
|
||||
});
|
||||
}, 350);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [query, scope, trackEvent, withAI, siteSpaceId, siteSpaceIds, disabled]);
|
||||
|
||||
const aiEnrichedResults: ResultType[] = React.useMemo(() => {
|
||||
if (!withAI) {
|
||||
return resultsState.results;
|
||||
}
|
||||
return withAskTriggers(resultsState.results, query, assistants);
|
||||
}, [resultsState.results, query, withAI]);
|
||||
|
||||
return { ...resultsState, results: aiEnrichedResults };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "Ask <question>" item at the top of the results list.
|
||||
*/
|
||||
function withAskTriggers(
|
||||
results: ResultType[],
|
||||
query: string,
|
||||
assistants: Assistant[]
|
||||
): ResultType[] {
|
||||
const without = results.filter((result) => result.type !== 'question');
|
||||
|
||||
if (query.length === 0) {
|
||||
return without;
|
||||
}
|
||||
|
||||
return [
|
||||
...assistants.map((assistant, index) => ({
|
||||
type: 'question' as const,
|
||||
id: `question-${index}`,
|
||||
query,
|
||||
assistant,
|
||||
})),
|
||||
...(without ?? []),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import type { ResultType } from './useSearchResults';
|
||||
|
||||
export function useSearchResultsCursor(props: { query: string; results: ResultType[] }) {
|
||||
const [cursor, setCursor] = React.useState<number | null>(null);
|
||||
const { query, results } = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!query) {
|
||||
// Reset the cursor when there's no query
|
||||
setCursor(null);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (results.length > 0) {
|
||||
// Auto-focus the first result
|
||||
setCursor(0);
|
||||
}
|
||||
}, [results]);
|
||||
|
||||
const moveBy = React.useCallback(
|
||||
(delta: number) => {
|
||||
setCursor((prev) => {
|
||||
if (prev === null) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(Math.min(prev + delta, results.length - 1), 0);
|
||||
});
|
||||
},
|
||||
[results]
|
||||
);
|
||||
|
||||
return {
|
||||
cursor,
|
||||
setCursor,
|
||||
moveBy,
|
||||
};
|
||||
}
|
||||
@@ -108,6 +108,8 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
|
||||
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
|
||||
|
||||
const faviconSize = 48;
|
||||
const appIconSize = 180;
|
||||
|
||||
const icons = await Promise.all(
|
||||
[
|
||||
{
|
||||
@@ -140,12 +142,44 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
|
||||
}))
|
||||
);
|
||||
|
||||
const appIcons = await Promise.all(
|
||||
[
|
||||
{
|
||||
url: customIcon?.light
|
||||
? getResizedImageURL(imageResizer, customIcon.light, {
|
||||
width: appIconSize,
|
||||
height: appIconSize,
|
||||
})
|
||||
: linker.toAbsoluteURL(
|
||||
linker.toPathInSpace('~gitbook/icon?size=medium&theme=light&border=false')
|
||||
),
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: light)',
|
||||
},
|
||||
{
|
||||
url: customIcon?.dark
|
||||
? getResizedImageURL(imageResizer, customIcon.dark, {
|
||||
width: appIconSize,
|
||||
height: appIconSize,
|
||||
})
|
||||
: linker.toAbsoluteURL(
|
||||
linker.toPathInSpace('~gitbook/icon?size=medium&theme=dark&border=false')
|
||||
),
|
||||
type: 'image/png',
|
||||
media: '(prefers-color-scheme: dark)',
|
||||
},
|
||||
].map(async (icon) => ({
|
||||
...icon,
|
||||
url: await icon.url,
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
title: site.title,
|
||||
generator: `GitBook (${buildVersion()})`,
|
||||
icons: {
|
||||
icon: icons,
|
||||
apple: icons,
|
||||
apple: appIcons,
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
|
||||
@@ -22,12 +22,17 @@ export function SiteLayoutClientContexts(props: {
|
||||
useClearRouterCache(contextId);
|
||||
|
||||
return (
|
||||
<NuqsAdapter>
|
||||
<ThemeProvider attribute="class" enableSystem forcedTheme={forcedTheme}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
disableTransitionOnChange
|
||||
enableSystem
|
||||
forcedTheme={forcedTheme}
|
||||
>
|
||||
<NuqsAdapter>
|
||||
<LinkSettingsContext.Provider value={{ externalLinksTarget }}>
|
||||
<SearchContextProvider>{children}</SearchContextProvider>
|
||||
</LinkSettingsContext.Provider>
|
||||
</ThemeProvider>
|
||||
</NuqsAdapter>
|
||||
</NuqsAdapter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,38 +43,42 @@ export async function SitePage(props: SitePageProps) {
|
||||
|
||||
return (
|
||||
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
|
||||
{withFullPageCover && page.cover ? (
|
||||
<PageCover as="full" page={page} cover={page.cover} context={context} />
|
||||
) : null}
|
||||
{/* We use a flex row reverse to render the aside first because the page is streamed. */}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex grow flex-row-reverse justify-end',
|
||||
withSections
|
||||
? '[--content-scroll-margin:calc(var(--spacing)*27)]'
|
||||
: '[--content-scroll-margin:calc(var(--spacing)*16)]'
|
||||
)}
|
||||
>
|
||||
<PageAside
|
||||
page={page}
|
||||
document={document}
|
||||
withHeaderOffset={headerOffset}
|
||||
withFullPageCover={withFullPageCover}
|
||||
withPageFeedback={withPageFeedback}
|
||||
context={context}
|
||||
/>
|
||||
<PageBody
|
||||
context={context}
|
||||
page={page}
|
||||
ancestors={ancestors}
|
||||
document={document}
|
||||
withPageFeedback={withPageFeedback}
|
||||
insightsDisplayContext={SiteInsightsDisplayContext.Site}
|
||||
/>
|
||||
{/* Using `contents` makes the children of this div according to its parent — which keeps them in a single flex row with the TOC by default.
|
||||
If there's a page cover, we use `flex flex-col` to lay out the PageCover above the PageBody + PageAside instead. */}
|
||||
<div className={withFullPageCover && page.cover ? 'flex grow flex-col' : 'contents'}>
|
||||
{withFullPageCover && page.cover ? (
|
||||
<PageCover as="full" page={page} cover={page.cover} context={context} />
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={tcls(
|
||||
withFullPageCover && page.cover ? 'flex grow flex-row' : 'contents',
|
||||
withSections
|
||||
? '[--content-scroll-margin:calc(var(--spacing)*27)]'
|
||||
: '[--content-scroll-margin:calc(var(--spacing)*16)]'
|
||||
)}
|
||||
>
|
||||
<PageAside
|
||||
page={page}
|
||||
document={document}
|
||||
withHeaderOffset={headerOffset}
|
||||
withFullPageCover={withFullPageCover}
|
||||
withPageFeedback={withPageFeedback}
|
||||
context={context}
|
||||
/>
|
||||
<PageBody
|
||||
context={context}
|
||||
page={page}
|
||||
ancestors={ancestors}
|
||||
document={document}
|
||||
withPageFeedback={withPageFeedback}
|
||||
insightsDisplayContext={SiteInsightsDisplayContext.Site}
|
||||
/>
|
||||
</div>
|
||||
<React.Suspense fallback={null}>
|
||||
<PageClientLayout />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
<React.Suspense fallback={null}>
|
||||
<PageClientLayout />
|
||||
</React.Suspense>
|
||||
</PageContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { TOCScrollContainer, useScrollToActiveTOCItem } from '../TableOfContents/TOCScroller';
|
||||
import { useIsMounted, useToggleAnimation } from '../hooks';
|
||||
import { findSectionInGroup } from '@/lib/utils';
|
||||
import { useToggleAnimation } from '../hooks';
|
||||
import { Link } from '../primitives';
|
||||
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||
import { SectionIcon } from './SectionIcon';
|
||||
import type {
|
||||
ClientSiteSection,
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
ClientSiteSections,
|
||||
} from './encodeClientSiteSections';
|
||||
|
||||
const MAX_ITEMS = 5; // If there are more sections than this, they'll be shown below the fold in a scrollview.
|
||||
const MAX_ITEMS = 6; // If there are more sections than this, they'll be shown below the fold in a scrollview.
|
||||
|
||||
/**
|
||||
* A list of items representing site sections for multi-section sites
|
||||
@@ -36,30 +36,34 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
|
||||
className
|
||||
)}
|
||||
>
|
||||
<TOCScrollContainer
|
||||
<ScrollContainer
|
||||
orientation="vertical"
|
||||
style={{ maxHeight: `${MAX_ITEMS * 3 + 2}rem` }}
|
||||
className="overflow-y-auto px-2 pb-4"
|
||||
className="pb-4"
|
||||
activeId={currentSection.id}
|
||||
>
|
||||
{sectionsAndGroups.map((item) => {
|
||||
if (item.object === 'site-section-group') {
|
||||
<div className="flex w-full flex-col px-2">
|
||||
{sectionsAndGroups.map((item) => {
|
||||
if (item.object === 'site-section-group') {
|
||||
return (
|
||||
<SiteSectionGroupItem
|
||||
key={item.id}
|
||||
group={item}
|
||||
currentSection={currentSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SiteSectionGroupItem
|
||||
<SiteSectionListItem
|
||||
section={item}
|
||||
isActive={item.id === currentSection.id}
|
||||
key={item.id}
|
||||
group={item}
|
||||
currentSection={currentSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SiteSectionListItem
|
||||
section={item}
|
||||
isActive={item.id === currentSection.id}
|
||||
key={item.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TOCScrollContainer>
|
||||
})}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</nav>
|
||||
)
|
||||
);
|
||||
@@ -69,20 +73,15 @@ export function SiteSectionListItem(props: {
|
||||
section: ClientSiteSection;
|
||||
isActive: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const { section, isActive, className, ...otherProps } = props;
|
||||
|
||||
const isMounted = useIsMounted();
|
||||
React.useEffect(() => {}, [isMounted]); // This updates the useScrollToActiveTOCItem hook once we're mounted, so we can actually scroll to the this item
|
||||
|
||||
const anchorRef = React.createRef<HTMLAnchorElement>();
|
||||
useScrollToActiveTOCItem({ anchorRef, isActive });
|
||||
const { section, isActive, className, style, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={anchorRef}
|
||||
href={section.url}
|
||||
aria-current={isActive && 'page'}
|
||||
id={section.id}
|
||||
className={tcls(
|
||||
'group/section-link',
|
||||
'flex',
|
||||
@@ -104,6 +103,7 @@ export function SiteSectionListItem(props: {
|
||||
: null,
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
{...otherProps}
|
||||
>
|
||||
<div
|
||||
@@ -130,11 +130,12 @@ export function SiteSectionListItem(props: {
|
||||
export function SiteSectionGroupItem(props: {
|
||||
group: ClientSiteSectionGroup;
|
||||
currentSection: ClientSiteSection;
|
||||
level?: number;
|
||||
}) {
|
||||
const { group, currentSection } = props;
|
||||
const { group, currentSection, level = 0 } = props;
|
||||
|
||||
const hasDescendants = group.sections.length > 0;
|
||||
const isActiveGroup = group.sections.some((section) => section.id === currentSection.id);
|
||||
const hasDescendants = group.children.length > 0;
|
||||
const isActiveGroup = Boolean(findSectionInGroup(group, currentSection.id));
|
||||
const shouldOpen = hasDescendants && isActiveGroup;
|
||||
const [isOpen, setIsOpen] = React.useState(shouldOpen);
|
||||
|
||||
@@ -164,7 +165,7 @@ export function SiteSectionGroupItem(props: {
|
||||
className={tcls(
|
||||
'flex size-8 shrink-0 items-center justify-center rounded-md straight-corners:rounded-none bg-tint-subtle text-lg text-tint leading-none shadow-tint shadow-xs ring-1 ring-tint-subtle transition-transform group-hover/section-link:scale-110 group-hover/section-link:ring-tint-hover group-active/section-link:scale-90 group-active/section-link:shadow-none contrast-more:text-tint-strong dark:shadow-none',
|
||||
isActiveGroup
|
||||
? 'bg-primary tint:bg-primary-solid text-primary tint:text-contrast-primary-solid shadow-md shadow-primary ring-primary group-hover/section-link:ring-primary-hover, contrast-more:text-primary-strong contrast-more:ring-2 contrast-more:ring-primary'
|
||||
? 'bg-primary text-primary shadow-md shadow-primary ring-primary group-hover/section-link:ring-primary-hover, contrast-more:text-primary-strong contrast-more:ring-2 contrast-more:ring-primary'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
@@ -219,14 +220,26 @@ export function SiteSectionGroupItem(props: {
|
||||
</button>
|
||||
{hasDescendants ? (
|
||||
<Descendants isVisible={isOpen}>
|
||||
{group.sections.map((section) => (
|
||||
<SiteSectionListItem
|
||||
section={section}
|
||||
isActive={section.id === currentSection.id}
|
||||
key={section.id}
|
||||
className="pl-5"
|
||||
/>
|
||||
))}
|
||||
{group.children.map((child) => {
|
||||
if (child.object === 'site-section') {
|
||||
return (
|
||||
<SiteSectionListItem
|
||||
section={child}
|
||||
isActive={child.id === currentSection.id}
|
||||
key={child.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SiteSectionGroupItem
|
||||
group={child}
|
||||
currentSection={currentSection}
|
||||
key={child.id}
|
||||
level={level + 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Descendants>
|
||||
) : null}
|
||||
</>
|
||||
@@ -242,7 +255,7 @@ function Descendants(props: {
|
||||
return (
|
||||
<motion.div
|
||||
ref={scope}
|
||||
className={isVisible ? undefined : '[&_ul>li]:opacity-1'}
|
||||
className={isVisible ? 'pl-3' : 'pl-3 [&_ul>li]:opacity-1'}
|
||||
initial={isVisible ? show : hide}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,113 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
|
||||
import React from 'react';
|
||||
|
||||
import { Link } from '@/components/primitives';
|
||||
import { Button, DropdownChevron, Link } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { findSectionInGroup } from '@/lib/utils';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||
import { SectionIcon } from './SectionIcon';
|
||||
import type { ClientSiteSection, ClientSiteSections } from './encodeClientSiteSections';
|
||||
import type {
|
||||
ClientSiteSection,
|
||||
ClientSiteSectionGroup,
|
||||
ClientSiteSections,
|
||||
} from './encodeClientSiteSections';
|
||||
|
||||
const VIEWPORT_ITEM_WIDTH = 240; /* width of the tile (w-60) */
|
||||
const MIN_ITEMS_FOR_COLS = 4; /* number of items to switch to 2 columns */
|
||||
const SCREEN_OFFSET = 16; // 1rem
|
||||
const MAX_ITEMS_PER_COLUMN = 5; // number of items per column
|
||||
/**
|
||||
* A set of navigational links representing site sections for multi-section sites
|
||||
*/
|
||||
export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
|
||||
export function SiteSectionTabs(props: {
|
||||
sections: ClientSiteSections;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const {
|
||||
sections: { list: sectionsAndGroups, current: currentSection },
|
||||
sections: { list: structure, current: currentSection },
|
||||
className,
|
||||
children,
|
||||
} = props;
|
||||
const [value, setValue] = React.useState<string | null>();
|
||||
|
||||
const currentTriggerRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const [offset, setOffset] = React.useState<number | null>(null);
|
||||
const menuContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [value, setValue] = React.useState<string | undefined>();
|
||||
|
||||
const onNodeUpdate = (trigger: HTMLButtonElement | null, itemValue: string, size = 0) => {
|
||||
const padding = 16;
|
||||
const margin = -12; // Offsetting the menu container's negative margin
|
||||
const windowWidth = document.documentElement.clientWidth;
|
||||
const windowBuffer = 16; // constrain to within the window with some buffer on the left and right we don't want the menu to enter
|
||||
const viewportWidth =
|
||||
size < MIN_ITEMS_FOR_COLS
|
||||
? VIEWPORT_ITEM_WIDTH + padding
|
||||
: VIEWPORT_ITEM_WIDTH * 2 + padding;
|
||||
const minOffset = 0 - (menuContainerRef.current?.offsetLeft ?? 0) + margin;
|
||||
const maxOffset = minOffset + windowWidth - viewportWidth;
|
||||
const isMobile = useIsMobile(768);
|
||||
|
||||
if (windowWidth < 768) {
|
||||
// if the screen is small don't offset the menu
|
||||
setOffset(minOffset + windowBuffer);
|
||||
} else if (trigger && value === itemValue) {
|
||||
const position = minOffset + trigger?.getBoundingClientRect().left;
|
||||
setOffset(
|
||||
Math.min(maxOffset - windowBuffer, Math.max(minOffset + windowBuffer, position))
|
||||
);
|
||||
} else if (!value) {
|
||||
setOffset(null);
|
||||
React.useEffect(() => {
|
||||
const trigger = currentTriggerRef.current;
|
||||
if (!value || !trigger) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return sectionsAndGroups.length > 0 ? (
|
||||
const triggerWidth = trigger.getBoundingClientRect().width;
|
||||
const triggerLeft = trigger.getBoundingClientRect().left;
|
||||
setOffset(triggerLeft + triggerWidth / 2);
|
||||
}, [value]);
|
||||
|
||||
return structure.length > 0 ? (
|
||||
<NavigationMenu.Root
|
||||
aria-label="Sections"
|
||||
id="sections"
|
||||
className={tcls(
|
||||
CONTAINER_STYLE,
|
||||
'relative z-10 flex w-full flex-nowrap items-end',
|
||||
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]',
|
||||
className
|
||||
)}
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
className="z-10 flex w-full flex-nowrap items-center"
|
||||
skipDelayDuration={500}
|
||||
>
|
||||
<div
|
||||
ref={menuContainerRef}
|
||||
className="-mx-3"
|
||||
// className="-mb-4 pb-4" /* Positive padding / negative margin allows the navigation menu indicator to show in a scroll view */
|
||||
<ScrollContainer
|
||||
orientation="horizontal"
|
||||
className={tcls(
|
||||
'grow',
|
||||
'md:-ml-8 -ml-4 sm:-ml-6',
|
||||
!children ? 'md:-mr-8 -mr-4 sm:-mr-6' : ''
|
||||
)}
|
||||
activeId={currentSection.id}
|
||||
>
|
||||
<NavigationMenu.List className="center m-0 flex list-none gap-2 bg-transparent">
|
||||
{sectionsAndGroups.map((sectionOrGroup) => {
|
||||
const { id, title, icon } = sectionOrGroup;
|
||||
const isGroup = sectionOrGroup.object === 'site-section-group';
|
||||
<NavigationMenu.List
|
||||
className={tcls(
|
||||
'-mx-3 flex grow gap-2 bg-transparent',
|
||||
'pl-4 sm:pl-6 md:pl-8',
|
||||
!children ? 'pr-4 sm:pr-6 md:pr-8' : ''
|
||||
)}
|
||||
aria-label="Sections"
|
||||
id="sections"
|
||||
>
|
||||
{structure.map((structureItem) => {
|
||||
const { id, title, icon } = structureItem;
|
||||
const isGroup = structureItem.object === 'site-section-group';
|
||||
const isActiveGroup =
|
||||
isGroup &&
|
||||
Boolean(
|
||||
sectionOrGroup.sections.find((s) => s.id === currentSection.id)
|
||||
);
|
||||
Boolean(findSectionInGroup(structureItem, currentSection.id));
|
||||
const isActive = isActiveGroup || id === currentSection.id;
|
||||
return (
|
||||
<NavigationMenu.Item key={id} value={id}>
|
||||
{isGroup ? (
|
||||
sectionOrGroup.sections.length > 0 ? (
|
||||
<>
|
||||
<NavigationMenu.Trigger
|
||||
ref={(node) =>
|
||||
onNodeUpdate(
|
||||
node,
|
||||
id,
|
||||
sectionOrGroup.sections.length
|
||||
)
|
||||
<NavigationMenu.Item key={id} value={id} id={id}>
|
||||
{isGroup && structureItem.children.length > 0 ? (
|
||||
<>
|
||||
<NavigationMenu.Trigger
|
||||
asChild
|
||||
ref={value === id ? currentTriggerRef : undefined}
|
||||
onClick={(e) => {
|
||||
// Prevent clicking the trigger from closing when the viewport is open
|
||||
if (value === id) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
asChild
|
||||
onClick={(e) => {
|
||||
if (value) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SectionGroupTab
|
||||
isActive={isActive}
|
||||
title={title}
|
||||
icon={icon as IconName}
|
||||
/>
|
||||
</NavigationMenu.Trigger>
|
||||
<NavigationMenu.Content className="absolute top-0 left-0 z-20 w-full motion-safe:data-[motion=from-end]:animate-enter-from-right motion-safe:data-[motion=from-start]:animate-enter-from-left motion-safe:data-[motion=to-end]:animate-exit-to-right motion-safe:data-[motion=to-start]:animate-exit-to-left md:w-max">
|
||||
<SectionGroupTileList
|
||||
sections={sectionOrGroup.sections}
|
||||
currentSection={currentSection}
|
||||
/>
|
||||
</NavigationMenu.Content>
|
||||
</>
|
||||
) : null
|
||||
}}
|
||||
>
|
||||
<SectionTab
|
||||
isActive={isActive}
|
||||
title={title}
|
||||
icon={icon as IconName}
|
||||
/>
|
||||
</NavigationMenu.Trigger>
|
||||
<NavigationMenu.Content>
|
||||
<SectionGroupTileList
|
||||
items={structureItem.children}
|
||||
currentSection={currentSection}
|
||||
/>
|
||||
</NavigationMenu.Content>
|
||||
</>
|
||||
) : (
|
||||
<NavigationMenu.Link asChild>
|
||||
<SectionTab
|
||||
url={sectionOrGroup.url}
|
||||
url={
|
||||
structureItem.object === 'site-section'
|
||||
? structureItem.url
|
||||
: undefined
|
||||
}
|
||||
isActive={isActive}
|
||||
title={title}
|
||||
icon={icon ? (icon as IconName) : undefined}
|
||||
@@ -117,26 +132,30 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
|
||||
</NavigationMenu.Item>
|
||||
);
|
||||
})}
|
||||
<NavigationMenu.Indicator
|
||||
className="fixed top-full z-50 flex h-3 items-end justify-center duration-150 motion-safe:transition-[width,transform] motion-safe:data-[state=hidden]:animate-fade-out motion-safe:data-[state=visible]:animate-fade-in"
|
||||
aria-hidden
|
||||
>
|
||||
<div className="relative top-1/2 size-3 rotate-45 rounded-tl-sm border-tint-subtle border-t border-l bg-tint-base" />
|
||||
</NavigationMenu.Indicator>
|
||||
</NavigationMenu.List>
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
|
||||
{children}
|
||||
|
||||
<div
|
||||
className="absolute top-full flex transition-transform duration-200 ease-in-out"
|
||||
className="fixed top-full left-0 z-20 flex w-full"
|
||||
style={{
|
||||
display: offset === null ? 'none' : undefined,
|
||||
transform: offset ? `translateX(${offset}px) translateZ(0)` : 'translateZ(0)', // TranslateZ is needed to force a stacking context, fixing a rendering bug on Safari
|
||||
padding: `0 ${SCREEN_OFFSET}px 0 ${SCREEN_OFFSET}px`,
|
||||
}}
|
||||
>
|
||||
<NavigationMenu.Viewport
|
||||
className="relative mt-3 h-(--radix-navigation-menu-viewport-height) w-[calc(100vw-2rem)] origin-[top_center] overflow-hidden rounded-lg straight-corners:rounded-xs bg-tint-base depth-flat:shadow-none shadow-lg shadow-tint-10/6 ring-1 ring-tint-subtle duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,height,transform] motion-safe:data-[state=closed]:animate-scale-out motion-safe:data-[state=open]:animate-scale-in md:mx-0 md:w-(--radix-navigation-menu-viewport-width) dark:shadow-tint-1/6"
|
||||
className={tcls(
|
||||
'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.
|
||||
)}
|
||||
style={{
|
||||
translate:
|
||||
undefined /* don't move this to a Tailwind class as Radix renders viewport incorrectly for a few frames */,
|
||||
!isMobile && offset
|
||||
? `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,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -145,131 +164,172 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A tab representing a section
|
||||
* A tab representing a section or section group
|
||||
*/
|
||||
const SectionTab = React.forwardRef(function SectionTab(
|
||||
props: { isActive: boolean; title: string; icon?: IconName; url: string },
|
||||
props: { isActive: boolean; title: string; icon?: IconName; url?: string },
|
||||
ref: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
const { isActive, title, icon, url, ...rest } = props;
|
||||
const isGroup = url === undefined;
|
||||
return (
|
||||
<Link
|
||||
<Button
|
||||
ref={ref}
|
||||
size="medium"
|
||||
variant="blank"
|
||||
{...rest}
|
||||
icon={icon ? <SectionIcon isActive={isActive} icon={icon} /> : null}
|
||||
label={title}
|
||||
trailing={isGroup ? <DropdownChevron /> : null}
|
||||
active={isActive}
|
||||
className={tcls(
|
||||
'group relative my-2 flex select-none items-center justify-between circular-corners:rounded-full rounded-corners:rounded-xs px-3 py-1',
|
||||
'group/dropdown relative my-2 overflow-visible px-3 py-1',
|
||||
isActive
|
||||
? 'text-primary-subtle'
|
||||
: 'text-tint hover:bg-tint-hover hover:text-tint-strong'
|
||||
? 'after:contents-[] after:-bottom-2 bg-transparent text-primary-subtle after:absolute after:inset-x-3 after:h-0.5 after:bg-primary-9'
|
||||
: ''
|
||||
)}
|
||||
href={url}
|
||||
>
|
||||
<span className="flex w-full items-center gap-2 truncate">
|
||||
{icon ? <SectionIcon isActive={isActive} icon={icon} /> : null}
|
||||
{title}
|
||||
</span>
|
||||
{isActive ? <ActiveTabIndicator /> : null}
|
||||
</Link>
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* A tab representing a section group
|
||||
*/
|
||||
const SectionGroupTab = React.forwardRef(function SectionGroupTab(
|
||||
props: { isActive: boolean; title: string; icon?: IconName },
|
||||
ref: React.Ref<HTMLButtonElement>
|
||||
) {
|
||||
const { isActive, title, icon, ...rest } = props;
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={tcls(
|
||||
'group relative my-2 flex select-none items-center justify-between circular-corners:rounded-full rounded-sm straight-corners:rounded-none px-3 py-1 transition-colors hover:cursor-default',
|
||||
isActive
|
||||
? 'text-primary-subtle'
|
||||
: 'text-tint hover:bg-tint-hover hover:text-tint-strong'
|
||||
)}
|
||||
>
|
||||
<span className="flex w-full items-center gap-2 truncate">
|
||||
{icon ? <SectionIcon isActive={isActive} icon={icon as IconName} /> : null}
|
||||
{title}
|
||||
</span>
|
||||
{isActive ? <ActiveTabIndicator /> : null}
|
||||
<Icon
|
||||
aria-hidden
|
||||
icon="chevron-down"
|
||||
className="ms-1 size-3 shrink-0 opacity-6 transition-all group-data-[state=open]:rotate-180"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Horizontal line indicating the active tab
|
||||
*/
|
||||
function ActiveTabIndicator() {
|
||||
return (
|
||||
<span className="-bottom-2 absolute inset-x-3 h-0.5 bg-primary-9 contrast-more:bg-primary-11" />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of section tiles grouped in the dropdown for a section group
|
||||
*/
|
||||
function SectionGroupTileList(props: {
|
||||
sections: ClientSiteSection[];
|
||||
items: (ClientSiteSection | ClientSiteSectionGroup)[];
|
||||
currentSection: ClientSiteSection;
|
||||
}) {
|
||||
const { sections, currentSection } = props;
|
||||
const { items, currentSection } = props;
|
||||
|
||||
// Separate non-grouped sections from grouped sections
|
||||
const sections = items.filter((item) => item.object === 'site-section');
|
||||
const groups = items.filter((item) => item.object === 'site-section-group');
|
||||
|
||||
const hasSections = sections.length > 0;
|
||||
const hasGroups = groups.length > 0;
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={tcls(
|
||||
'grid w-full gap-1 p-2 sm:grid-cols-1 md:w-max',
|
||||
sections.length < MIN_ITEMS_FOR_COLS ? 'md:grid-cols-1' : 'md:grid-cols-2'
|
||||
<div className="flex flex-col md:flex-row">
|
||||
{/* Non-grouped sections */}
|
||||
{hasSections && (
|
||||
<ul
|
||||
className={tcls(
|
||||
'flex grid-flow-row flex-col gap-x-2 gap-y-1 p-2 md:grid',
|
||||
hasGroups ? 'bg-tint-base' : ''
|
||||
)}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${Math.ceil(sections.length / MAX_ITEMS_PER_COLUMN)}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{sections.map((section) => (
|
||||
<SectionGroupTile
|
||||
key={section.id}
|
||||
child={section}
|
||||
currentSection={currentSection}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
>
|
||||
{sections.map((section) => (
|
||||
<SectionGroupTile
|
||||
key={section.id}
|
||||
section={section}
|
||||
isActive={section.id === currentSection.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Grouped sections */}
|
||||
{hasGroups && (
|
||||
<ul
|
||||
className={tcls(
|
||||
'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'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<SectionGroupTile
|
||||
key={group.id}
|
||||
child={group}
|
||||
currentSection={currentSection}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A section tile shown in the dropdown for a section group
|
||||
*/
|
||||
function SectionGroupTile(props: { section: ClientSiteSection; isActive: boolean }) {
|
||||
const { section, isActive } = props;
|
||||
const { url, icon, title } = section;
|
||||
function SectionGroupTile(props: {
|
||||
child: ClientSiteSection | ClientSiteSectionGroup;
|
||||
currentSection: ClientSiteSection;
|
||||
}) {
|
||||
const { child, currentSection } = props;
|
||||
|
||||
if (child.object === 'site-section') {
|
||||
const { url, icon, title, description } = child;
|
||||
const isActive = child.id === currentSection.id;
|
||||
return (
|
||||
<li className="group/section-tile flex w-full shrink grow md:w-68">
|
||||
<Link
|
||||
href={url}
|
||||
className={tcls(
|
||||
'grow circular-corners:rounded-2xl rounded-corners:rounded-lg px-3 py-2 transition-colors',
|
||||
isActive
|
||||
? 'bg-primary-active text-primary-strong'
|
||||
: 'text-tint-strong hover:bg-tint-hover'
|
||||
)}
|
||||
>
|
||||
<div className="mb-auto flex grow items-center gap-2">
|
||||
{icon && (
|
||||
<div
|
||||
className={tcls(
|
||||
'-ml-1 self-start circular-corners:rounded-2xl rounded-corners:rounded-lg p-2 transition-colors',
|
||||
isActive
|
||||
? 'bg-primary-base text-primary-subtle'
|
||||
: 'bg-tint text-tint-strong group-hover/section-tile:bg-tint-base'
|
||||
)}
|
||||
>
|
||||
<SectionIcon isActive={isActive} icon={icon as IconName} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
{title}
|
||||
{description && (
|
||||
<p className={isActive ? 'text-primary' : 'text-tint'}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle nested section group
|
||||
const { title, icon, children } = child;
|
||||
|
||||
return (
|
||||
<li className="flex w-full md:w-60">
|
||||
<Link
|
||||
href={url}
|
||||
className={tcls(
|
||||
'flex w-full select-none flex-col gap-1 rounded-sm straight-corners:rounded-none px-3 py-2 transition-colors hover:bg-tint-hover',
|
||||
isActive ? 'text-primary' : 'text-tint-strong'
|
||||
<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} />
|
||||
)}
|
||||
{title}
|
||||
</div>
|
||||
<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, 1fr))`,
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full gap-2">
|
||||
{icon ? (
|
||||
<SectionIcon
|
||||
className="mt-[3px]"
|
||||
isActive={false}
|
||||
icon={icon as IconName}
|
||||
/>
|
||||
) : null}
|
||||
{title}
|
||||
</div>
|
||||
{section.description ? (
|
||||
<p className="text-tint-subtle">{section.description}</p>
|
||||
) : null}
|
||||
</Link>
|
||||
{children.map((nestedChild) => (
|
||||
<SectionGroupTile
|
||||
key={nestedChild.id}
|
||||
child={nestedChild}
|
||||
currentSection={currentSection}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export type ClientSiteSection = Pick<
|
||||
};
|
||||
|
||||
export type ClientSiteSectionGroup = Pick<SiteSectionGroup, 'id' | 'title' | 'icon' | 'object'> & {
|
||||
sections: ClientSiteSection[];
|
||||
children: (ClientSiteSection | ClientSiteSectionGroup)[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -30,10 +30,10 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
|
||||
for (const item of list) {
|
||||
switch (item.object) {
|
||||
case 'site-section-group': {
|
||||
const sections = item.sections.map((section) => encodeSection(context, section));
|
||||
const children = encodeChildren(context, item.children);
|
||||
|
||||
// Skip empty groups
|
||||
if (sections.length === 0) {
|
||||
if (children.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
object: item.object,
|
||||
sections,
|
||||
children,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -61,6 +61,43 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
|
||||
};
|
||||
}
|
||||
|
||||
function encodeChildren(
|
||||
context: GitBookSiteContext,
|
||||
children: (SiteSection | SiteSectionGroup)[]
|
||||
): (ClientSiteSection | ClientSiteSectionGroup)[] {
|
||||
const clientChildren: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
switch (child.object) {
|
||||
case 'site-section': {
|
||||
clientChildren.push(encodeSection(context, child));
|
||||
break;
|
||||
}
|
||||
case 'site-section-group': {
|
||||
const nestedChildren = encodeChildren(context, child.children);
|
||||
|
||||
// Skip empty groups
|
||||
if (nestedChildren.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
clientChildren.push({
|
||||
id: child.id,
|
||||
title: child.title,
|
||||
icon: child.icon,
|
||||
object: child.object,
|
||||
children: nestedChildren,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertNever(child, 'Unknown site section object type');
|
||||
}
|
||||
}
|
||||
|
||||
return clientChildren;
|
||||
}
|
||||
|
||||
function encodeSection(context: GitBookSiteContext, section: SiteSection) {
|
||||
return {
|
||||
id: section.id,
|
||||
|
||||
@@ -21,10 +21,11 @@ import { AIChat } from '../AIChat';
|
||||
import { AdaptiveVisitorContextProvider } from '../Adaptive';
|
||||
import { Announcement } from '../Announcement';
|
||||
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
|
||||
import { InsightsProvider } from '../Insights';
|
||||
import { InsightsProvider, VisitorSessionProvider } from '../Insights';
|
||||
import { SearchContainer } from '../Search';
|
||||
import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
|
||||
import { CurrentContentProvider } from '../hooks';
|
||||
import { NavigationLoader } from '../primitives/NavigationLoader';
|
||||
import { SpaceLayoutContextProvider } from './SpaceLayoutContext';
|
||||
|
||||
type SpaceLayoutProps = {
|
||||
@@ -78,16 +79,16 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
|
||||
revisionId={context.revisionId}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
>
|
||||
<InsightsProvider
|
||||
enabled={withTracking}
|
||||
<VisitorSessionProvider
|
||||
appURL={GITBOOK_APP_URL}
|
||||
eventUrl={eventUrl.toString()}
|
||||
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
|
||||
>
|
||||
<AIChatProvider renderMessageOptions={aiChatRenderMessageOptions}>
|
||||
{children}
|
||||
</AIChatProvider>
|
||||
</InsightsProvider>
|
||||
<InsightsProvider enabled={withTracking} eventUrl={eventUrl.toString()}>
|
||||
<AIChatProvider renderMessageOptions={aiChatRenderMessageOptions}>
|
||||
{children}
|
||||
</AIChatProvider>
|
||||
</InsightsProvider>
|
||||
</VisitorSessionProvider>
|
||||
</CurrentContentProvider>
|
||||
</AdaptiveVisitorContextProvider>
|
||||
</SpaceLayoutContextProvider>
|
||||
@@ -125,6 +126,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
<SpaceLayoutServerContext {...props}>
|
||||
<Announcement context={context} />
|
||||
<Header withTopHeader={withTopHeader} withVariants={withVariants} context={context} />
|
||||
<NavigationLoader />
|
||||
{customization.ai?.mode === CustomizationAIMode.Assistant ? (
|
||||
<AIChat trademark={customization.trademark.enabled} />
|
||||
) : null}
|
||||
@@ -135,6 +137,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
'flex',
|
||||
'flex-col',
|
||||
'lg:flex-row',
|
||||
'lg:justify-center',
|
||||
CONTAINER_STYLE,
|
||||
'site-width-wide:max-w-full',
|
||||
|
||||
@@ -180,9 +183,25 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
<div className="flex gap-2">
|
||||
<SearchContainer
|
||||
style={CustomizationSearchStyle.Subtle}
|
||||
isMultiVariants={siteSpaces.length > 1}
|
||||
withVariants={withVariants === 'generic'}
|
||||
withSiteVariants={
|
||||
sections?.list.some(
|
||||
(s) =>
|
||||
s.object === 'site-section' &&
|
||||
s.siteSpaces.length > 1
|
||||
) ?? false
|
||||
}
|
||||
withSections={withSections}
|
||||
section={sections?.current}
|
||||
spaceTitle={siteSpace.title}
|
||||
siteSpaceId={siteSpace.id}
|
||||
siteSpaceIds={siteSpaces
|
||||
.filter(
|
||||
(s) =>
|
||||
s.space.language ===
|
||||
siteSpace.space.language
|
||||
)
|
||||
.map((s) => s.id)}
|
||||
className="max-lg:hidden"
|
||||
viewport="desktop"
|
||||
/>
|
||||
@@ -205,7 +224,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
export const HashContext = React.createContext<{
|
||||
export const NavigationStatusContext = React.createContext<{
|
||||
hash: string | null;
|
||||
/**
|
||||
* Updates the hash value from the URL provided here.
|
||||
* It will then be used by the `useHash` hook.
|
||||
* Updates the navigation state from the URL provided here.
|
||||
* URL can be relative or absolute.
|
||||
*/
|
||||
updateHashFromUrl: (href: string) => void;
|
||||
onNavigationClick: (href: string) => void;
|
||||
/**
|
||||
* Indicates if a link has been clicked recently.
|
||||
* Becomes true after a click and resets to false when pathname changes.
|
||||
* It is debounced to avoid flickering on fast navigations.
|
||||
* Debounce time is 400ms (= doherty threshold for responsiveness).
|
||||
*/
|
||||
isNavigating: boolean;
|
||||
}>({
|
||||
hash: null,
|
||||
updateHashFromUrl: () => {},
|
||||
onNavigationClick: () => {},
|
||||
isNavigating: false,
|
||||
});
|
||||
|
||||
function getHash(): string | null {
|
||||
@@ -21,20 +30,67 @@ function getHash(): string | null {
|
||||
return window.location.hash.slice(1);
|
||||
}
|
||||
|
||||
export const HashProvider: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
|
||||
export const NavigationStatusProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const [hash, setHash] = React.useState<string | null>(getHash);
|
||||
const updateHashFromUrl = React.useCallback((href: string) => {
|
||||
const [isNavigating, setIsNavigating] = React.useState(false);
|
||||
const timeoutRef = React.useRef<number | null>(null);
|
||||
const pathname = usePathname();
|
||||
const pathnameRef = React.useRef(pathname);
|
||||
|
||||
// Reset isNavigating when pathname changes
|
||||
React.useEffect(() => {
|
||||
if (pathnameRef.current !== pathname) {
|
||||
setIsNavigating(false);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
pathnameRef.current = pathname;
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onNavigationClick = React.useCallback((href: string) => {
|
||||
// We need to skip it for search like params (i.e. ?ask= or ?q=) because they don't really trigger a navigation
|
||||
// Search may trigger a navigation whenn clicking on the ask ai for example, this is not something we want to track here
|
||||
if (href.startsWith('?') || href.startsWith('#')) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(
|
||||
href,
|
||||
typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
||||
);
|
||||
setHash(url.hash.slice(1));
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
if (pathnameRef.current !== url.pathname) {
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setIsNavigating(true);
|
||||
timeoutRef.current = null;
|
||||
return;
|
||||
}, 400); // 400ms timeout - doherty threshold for responsiveness
|
||||
}
|
||||
}, []);
|
||||
|
||||
const memoizedValue = React.useMemo(
|
||||
() => ({ hash, updateHashFromUrl }),
|
||||
[hash, updateHashFromUrl]
|
||||
() => ({ hash, onNavigationClick, isNavigating }),
|
||||
[hash, onNavigationClick, isNavigating]
|
||||
);
|
||||
return (
|
||||
<NavigationStatusContext.Provider value={memoizedValue}>
|
||||
{children}
|
||||
</NavigationStatusContext.Provider>
|
||||
);
|
||||
return <HashContext.Provider value={memoizedValue}>{children}</HashContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -45,8 +101,16 @@ export const HashProvider: React.FC<React.PropsWithChildren<{}>> = ({ children }
|
||||
* Since we have a single Link component that handles all links, we can use a context to share the hash.
|
||||
*/
|
||||
export function useHash() {
|
||||
// const params = useParams();
|
||||
const { hash } = React.useContext(HashContext);
|
||||
const { hash } = React.useContext(NavigationStatusContext);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the current navigation state.
|
||||
* @returns True if a navigation has been triggered recently. False otherwise, it also resets to false when the navigation is complete.
|
||||
*/
|
||||
export function useIsNavigating() {
|
||||
const { isNavigating: hasBeenClicked } = React.useContext(NavigationStatusContext);
|
||||
return hasBeenClicked;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user