Compare commits

..

57 Commits

Author SHA1 Message Date
Zeno Kapitein 63971b9803 Update SearchContainer.tsx 2025-08-20 09:23:50 +02:00
Zeno Kapitein 64455b55e4 Add ready handshake to integration assistants 2025-08-20 09:01:54 +02:00
Taran Vohra 68308154b5 Register custom assistant (#3569)
Co-authored-by: Zeno Kapitein <zeno@gitbook.io>
2025-08-19 12:07:47 +02:00
Zeno Kapitein 36af03f4d5 Pageaside fixes (#3567) 2025-08-18 18:50:00 +02:00
Samy Pessé cbc71a56b6 API to let integrations define custom tools for Docs Assistant (#3568) 2025-08-17 10:03:54 +02:00
Zeno Kapitein 1420180220 Assistant: Prevent sending queries multiple times (#3565) 2025-08-15 08:44:31 +02:00
Zeno Kapitein e1b2cf6d81 Fix page outline scroll (#3563) 2025-08-14 17:52:56 +02:00
conico974 c0e9c49679 Optimize cache retrieval performance and reduce costs on 2C (#3542)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-14 09:14:40 +02:00
conico974 afc4bd4415 Improve image resizing logic in fetchImage function (#3553)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-13 15:35:14 +02:00
Zeno Kapitein bcfa8d8b19 Improve vertical alignment of site items and fix floating page aside (#3560) 2025-08-13 14:36:07 +02:00
Zeno Kapitein 43766d6519 Fix custom logo not rendering on the published site (#3557) 2025-08-13 14:29:49 +02:00
Greg Bergé 42c17f5c74 Improve OpenAPI parsing errors (#3555) 2025-08-13 13:38:14 +02:00
Greg Bergé d655d3eece Support "objectivec" as alias for Objective C syntax (#3556) 2025-08-13 13:37:06 +02:00
conico974 3500de95e8 Never follow redirect in the middleware (#3552)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-13 12:58:43 +02:00
Taran Vohra 7f03b94949 Fix an issue where recommended questions werent using the current siteSpace context (#3559) 2025-08-13 11:06:05 +05:30
Zeno Kapitein f0cf060191 Disable AI Search tests for now (#3561) 2025-08-12 22:12:51 +02:00
Zeno Kapitein 5a137e7b14 Always show placeholder while generating (#3554) 2025-08-12 15:43:29 +02:00
conico974 28f7fbaa8a Fix image format handling and enhance PageCoverImage component (#3527)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-08 18:30:00 +02:00
Nolann B. ff96bb5787 Support new coverDefinitionDark for cards & image type (#3547) 2025-08-08 18:08:11 +02:00
conico974 87841124e0 Add HashContext and HashProvider for managing URL hash state (#3541)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-08 17:01:21 +02:00
conico974 a650b58276 feat: add tracing to server actions (#3543)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-08 16:10:06 +02:00
Zeno Kapitein 388b20d44f Clear AI chat stream properly (#3544) 2025-08-07 19:07:25 +02:00
Claire Chabas ea7e94fc51 Fix search bar layout shit caused by the ToC (#3545) 2025-08-07 15:19:20 +02:00
Zeno Kapitein d30bcbabdb Improve Button and ButtonGroup styling (#3540) 2025-08-06 16:10:55 +02:00
Zeno Kapitein fb858a1941 Tweaks to AIChatButton and AIChatInput (#3539) 2025-08-06 16:10:05 +02:00
Samy Pessé cc2e615b9a Emit a <link rel="alternate"> for the markdown version of a page (#3528) 2025-08-06 08:55:02 +02:00
Samy Pessé a862cde6b4 Use new API to fetch the reusable content document (#3536) 2025-08-06 08:54:43 +02:00
Zeno Kapitein e2b7bec162 Add linewrap to AI actions dropdown (#3534) 2025-08-05 12:17:00 +00:00
Zeno Kapitein c1b577ecc9 Updates to RU translation (#3532) 2025-08-05 14:10:45 +02:00
Zeno Kapitein ba7ec14f76 Fix z-index of search highlight (#3533) 2025-08-05 14:08:46 +02:00
Zeno Kapitein 9f0117723c Add russian translation (#3531) 2025-08-05 12:52:30 +02:00
conico974 f72b6a20db Validate 'next-action' header format in server action requests (#3523)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-05 11:31:43 +02:00
conico974 2f798209bb Bump OpenNext to 1.6.4 (#3520)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-05 11:18:56 +02:00
conico974 eec8f16424 Increase timeout for search answer visibility check (#3530)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-08-05 11:18:12 +02:00
Samy Pessé 885264f132 Version Packages (#3506)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-05 11:03:50 +02:00
Greg Bergé f1a6dec951 Less strict OpenAPI parser (#3529)
Co-authored-by: Nolann Biron <biron.nolann@gmail.com>
2025-08-05 08:45:45 +00:00
Claire Chabas 44f41510fa Fix close buttons tooltips (#3525) 2025-08-04 15:22:59 +02:00
spastorelli f220229e5e Fix stepper title misalignment (#3524) 2025-08-04 09:34:22 +02:00
Greg Bergé 61b4cb8d7c Upgrade Argos (#3522) 2025-08-01 16:07:35 +02:00
Greg Bergé 185cdb4883 Fix scrolling to anchor positionning (#3521) 2025-08-01 15:44:27 +02:00
Zeno Kapitein 2cdba53451 Upgrade to Tailwind v4 (#3206) 2025-08-01 13:03:44 +02:00
Greg Bergé 950f6c36cf greg/rnd 6997 fix long openapi responses label (#3518) 2025-07-31 12:08:57 +00:00
Zeno Kapitein ea37977b1e Fix inconsistent state between q= and ask= and keyboard navigation (#3519) 2025-07-31 13:20:27 +02:00
Greg Bergé 81da82aed7 Increase tests max duration (#3517) 2025-07-30 18:33:56 +00:00
Greg Bergé 39a3518651 Show a HoverCard when an internal link is broken (#3516) 2025-07-30 18:09:57 +02:00
spastorelli d903273759 Strip visitor.* param when normalizing URL (#3512) 2025-07-30 17:22:09 +02:00
Taran Vohra d130532f69 Suggest questions in the current space context (#3514) 2025-07-30 20:36:40 +05:30
Zeno Kapitein 94fcd21f34 Improve ?ask= init handling (#3513) 2025-07-30 13:17:15 +02:00
conico974 1d2db4ae2b Fix cookie handling for AI assistant server actions (#3509)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-30 12:29:51 +02:00
Steven H d270b4af9b Don't track events if a special middleware header is set. (#3511) 2025-07-30 09:26:01 +01:00
Zeno Kapitein ccfbd3c1a8 Fix AI Search Query (#3510) 2025-07-30 07:44:37 +00:00
Claire Chabas 5c4923557f Temporarily remove highlighting for PowerShell and C++ in Safari to avoid page crash (#3508) 2025-07-29 20:39:26 +02:00
Zeno Kapitein b1f608b865 Implement basic URL scheme for Assistant & AI Search (#3503) 2025-07-29 16:13:44 +02:00
conico974 c7993756dd Fix regional caching of tag cache (#3504)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-07-29 13:27:56 +02:00
Nolann B. d02465823f Prevent page breaks inside blocks when printing (#3507) 2025-07-28 18:08:08 +02:00
Greg Bergé 6b9f6b5b79 Improve french translations (#3501) 2025-07-28 14:05:04 +02:00
Taran Vohra df7de8fb0c Fix an issue where links were not rendering properly in Ask AI answers (#3505) 2025-07-28 16:43:18 +05:30
199 changed files with 6268 additions and 2264 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve `Button` and `ButtonGroup` styling
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix scroll of page outline
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Support "objectivec" as alias for Objective C syntax
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Support custom AI providers
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/browser-types": minor
---
First version of the public package for typing script integrations.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fixes to `PageAside`
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Tweaks to AIChatButton and AIChatInput
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Allow integrations to provide tools to the Docs Assistant
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve vertical alignment of site items and fix floating page aside
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix search bar layout shift caused by ToC
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Emit a <link rel="alternate"> for the markdown version of a page
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Support new coverDefinitionDark for cards & image type
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Clear AI chat properly
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix bug in search highlight in GBO
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/openapi-parser": patch
"gitbook": patch
---
Improve OpenAPI parsing errors
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix Custom logo not rendering on the published site
+4 -4
View File
@@ -102,7 +102,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing v2
needs: deploy-v2-vercel
timeout-minutes: 10
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -123,7 +123,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing v2 (Cloudflare)
needs: deploy-v2-cloudflare
timeout-minutes: 10
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -144,7 +144,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
needs: deploy-v2-vercel
timeout-minutes: 10
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -165,7 +165,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing Customers v2 (Cloudflare)
needs: deploy-v2-cloudflare
timeout-minutes: 10
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
+2 -1
View File
@@ -21,7 +21,8 @@
"packages/openapi-parser/src/fixtures/**/*",
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json",
"packages/gitbook/worker-configuration.d.ts"
"packages/gitbook/worker-configuration.d.ts",
"**/*.css"
]
},
"formatter": {
+218 -70
View File
@@ -10,6 +10,17 @@
"vercel": "^39.3.0",
},
},
"packages/browser-types": {
"name": "@gitbook/browser-types",
"version": "0.3.1",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
},
"devDependencies": {
"typescript": "^5.5.3",
},
},
"packages/cache-tags": {
"name": "@gitbook/cache-tags",
"version": "0.3.1",
@@ -48,9 +59,10 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.14.0",
"version": "0.15.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/browser-types": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
@@ -60,7 +72,7 @@
"@gitbook/react-contentkit": "workspace:*",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@opennextjs/cloudflare": "^1.4.0",
"@opennextjs/cloudflare": "^1.6.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-navigation-menu": "^1.2.3",
@@ -118,8 +130,9 @@
"zustand": "^5.0.3",
},
"devDependencies": {
"@argos-ci/playwright": "^5.0.5",
"@playwright/test": "^1.51.1",
"@argos-ci/playwright": "^5.0.9",
"@playwright/test": "^1.54.2",
"@tailwindcss/postcss": "^4.1.11",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
@@ -131,14 +144,13 @@
"@types/react": "18.3.13",
"@types/react-dom": "18.3.1",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"deepmerge": "^4.3.1",
"env-cmd": "^10.1.0",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"psi": "^4.1.0",
"stylelint": "^16.16.0",
"tailwindcss": "^3.4.0",
"tailwindcss": "^4.1.11",
"ts-essentials": "^10.0.1",
"typescript": "^5.5.3",
"vercel": "^39.3.0",
@@ -146,7 +158,7 @@
},
"packages/icons": {
"name": "@gitbook/icons",
"version": "0.2.1",
"version": "0.2.2",
"bin": {
"gitbook-icons": "./bin/gitbook-icons.js",
},
@@ -166,7 +178,7 @@
},
"packages/openapi-parser": {
"name": "@gitbook/openapi-parser",
"version": "2.2.1",
"version": "3.0.0",
"dependencies": {
"@scalar/openapi-parser": "^0.18.0",
"@scalar/openapi-types": "^0.1.9",
@@ -181,7 +193,7 @@
},
"packages/react-contentkit": {
"name": "@gitbook/react-contentkit",
"version": "0.7.2",
"version": "0.7.3",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:*",
@@ -196,7 +208,7 @@
},
"packages/react-math": {
"name": "@gitbook/react-math",
"version": "0.6.0",
"version": "0.6.1",
"bin": {
"gitbook-math": "./bin/gitbook-math.js",
},
@@ -213,7 +225,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.3.3",
"version": "1.3.5",
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "^1.3.16",
@@ -246,7 +258,7 @@
"react-dom": "^19.0.0",
},
"catalog": {
"@gitbook/api": "^0.129.0",
"@gitbook/api": "^0.136.0",
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -259,15 +271,17 @@
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@argos-ci/api-client": ["@argos-ci/api-client@0.8.1", "", { "dependencies": { "debug": "^4.4.0", "openapi-fetch": "0.13.5" } }, "sha512-3IHv7ANSPNO6OwWgwULlHbP9/tFV9kQDu6+nL9jysfPkGj0GgtrOsyBb+iU931c7wSMo1OD+XNujCnRzDD968w=="],
"@argos-ci/browser": ["@argos-ci/browser@4.1.2", "", {}, "sha512-OljPFzxSNndWSwMBxKGtN3p8lSbt73z+/0CIXbBVTQOPjPMErXWjgtrFL23xqtTq5wdds3uxGv7tjVDWJbgBYg=="],
"@argos-ci/browser": ["@argos-ci/browser@4.2.0", "", {}, "sha512-xztv7hMNJyAYw2FXZDm4OCtw+jotJv0BPp5/9dljKrJ0us/2dDEEYKAvVcxzdzRpSe7VOtXSdsoFf0XCCPmxtQ=="],
"@argos-ci/core": ["@argos-ci/core@3.2.0", "", { "dependencies": { "@argos-ci/api-client": "0.8.1", "@argos-ci/util": "2.3.1", "axios": "^1.8.4", "convict": "^6.2.4", "debug": "^4.4.0", "fast-glob": "^3.3.3", "sharp": "^0.33.5", "tmp": "^0.2.3" } }, "sha512-wraH5RGFpoU7zcKV2BVi4LvY+5bWR/Ds6OVhUtq3jtd7I3qJRFVFq2IX1TfPPvBBA7wX0OqcrqgIWAQwy+vsoA=="],
"@argos-ci/core": ["@argos-ci/core@3.2.1", "", { "dependencies": { "@argos-ci/api-client": "0.8.1", "@argos-ci/util": "2.3.2", "axios": "^1.8.4", "convict": "^6.2.4", "debug": "^4.4.0", "fast-glob": "^3.3.3", "sharp": "^0.33.5", "tmp": "^0.2.3" } }, "sha512-P+tGofNLAtH0+e87M8sZc+juAtbOcnV6z2nA2MwB2OzUVVXGINJHAF2cK0ZUyXC9d8a7RL0+rQWkP4vXDA/gBw=="],
"@argos-ci/playwright": ["@argos-ci/playwright@5.0.5", "", { "dependencies": { "@argos-ci/browser": "4.1.2", "@argos-ci/core": "3.2.0", "@argos-ci/util": "2.3.1", "chalk": "^5.4.1", "debug": "^4.4.0" } }, "sha512-ZqaqptjI8rVUjD2KQzpvI1olFDiiMjgDaIyFOMWo8531cB7CMCwPXDNsFw2psUfsz3a3LP91eH2OrJopmevEkQ=="],
"@argos-ci/playwright": ["@argos-ci/playwright@5.0.9", "", { "dependencies": { "@argos-ci/browser": "4.2.0", "@argos-ci/core": "3.2.1", "@argos-ci/util": "2.3.2", "chalk": "^5.4.1", "debug": "^4.4.0" } }, "sha512-FbUPjNVm+Qer9aUU56oGNiPeynqDCh33GOYKekzjikBlxWFZfhiW4NA+sOXF0VWzGb5ni9l1U7fy6ycOTSalAA=="],
"@argos-ci/util": ["@argos-ci/util@2.3.1", "", {}, "sha512-kE61HU2480fbAnimmA4x9HK45ZJvkoqLdW5GxT5uvwhkclQykVd2S6WfGFUr3JokTXfZ5LZEEfoWgtGA316KSQ=="],
"@argos-ci/util": ["@argos-ci/util@2.3.2", "", {}, "sha512-xtNHJxpWYNst/sMNn4Jv/vkODuFsJ+APr4FBeoFUdIa+Izjl4ZFHsYA2PUyu+ygIpQCkof8yZLL9U1/VpiyyIw=="],
"@ast-grep/napi": ["@ast-grep/napi@0.35.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.35.0", "@ast-grep/napi-darwin-x64": "0.35.0", "@ast-grep/napi-linux-arm64-gnu": "0.35.0", "@ast-grep/napi-linux-arm64-musl": "0.35.0", "@ast-grep/napi-linux-x64-gnu": "0.35.0", "@ast-grep/napi-linux-x64-musl": "0.35.0", "@ast-grep/napi-win32-arm64-msvc": "0.35.0", "@ast-grep/napi-win32-ia32-msvc": "0.35.0", "@ast-grep/napi-win32-x64-msvc": "0.35.0" } }, "sha512-3ucaaSxV6fxXoqHrE/rxAvP1THnDdY5jNzGlnvx+JvnY9C/dSRKc0jlRMRz59N3El572+/yNRUUpAV1T9aBJug=="],
@@ -609,7 +623,9 @@
"@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.129.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Uh+k/BiDgdXj5a8BIlvwfEXdZNNlQMFDlGKmivUYH1gvFySqrTO6PjR/HtUOZEdNauAeWZmGoB8CLuwrVZS+YA=="],
"@gitbook/api": ["@gitbook/api@0.136.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-IxNqmXE6yUEUq0IzbenN8S/PcMfgxr4+akY8xh6V5ShI/+U37ixujJHZp2zJq6NdZOdBQoR3UQmhPnvtWrkF7g=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
"@gitbook/cache-tags": ["@gitbook/cache-tags@workspace:packages/cache-tags"],
@@ -787,9 +803,9 @@
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
"@opennextjs/aws": ["@opennextjs/aws@3.7.0", "", { "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-JXUZice+CedEQW20hnBVqzBEj+jfr4Oe2LVYSE4RNKdfHVIeYG+WJAop14TxRJ+NugKWGcJx6opf944l+ZG7XQ=="],
"@opennextjs/aws": ["@opennextjs/aws@3.7.4", "", { "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-s50dmKrgQ62GliffoI/hGGQVb3q/7ZN5VRa4jJw0ZsEGLfk25XuAulO/ySCYeE7/A23KoAYuhafnKr/h+vxOeQ=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.4.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.7.0", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-nEtxmSo6/y83Qw2TSU8rBpljaGl3NUl5GenD/c71h6YqgbP4QURSxMh7ySe7KK+vtQbilD7WePXovqL4Y6D6dw=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.6.4", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.7.4", "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-vGEHr1+Dli4BSrs+CM+b2bnXoUdY/GUFiLfdH28UTRHggLMG7cL49M39LtzIVR5D8eqzZ38PDB2CZMn1LJ2gSw=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -797,7 +813,7 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@playwright/test": ["@playwright/test@1.51.1", "", { "dependencies": { "playwright": "1.51.1" }, "bin": { "playwright": "cli.js" } }, "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q=="],
"@playwright/test": ["@playwright/test@1.54.2", "", { "dependencies": { "playwright": "1.54.2" }, "bin": { "playwright": "cli.js" } }, "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA=="],
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
@@ -1247,6 +1263,36 @@
"@tailwindcss/container-queries": ["@tailwindcss/container-queries@0.1.1", "", { "peerDependencies": { "tailwindcss": ">=3.2.0" } }, "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.11", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "postcss": "^8.4.41", "tailwindcss": "4.1.11" } }, "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA=="],
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.10.8", "", {}, "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA=="],
@@ -1301,6 +1347,8 @@
"@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="],
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
"@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="],
"@types/object-hash": ["@types/object-hash@3.0.6", "", {}, "sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w=="],
@@ -1407,6 +1455,8 @@
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@4.3.0", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.4", "@ai-sdk/react": "1.2.6", "@ai-sdk/ui-utils": "1.2.5", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-PxyQYKhWaU3LiZEpeKRaekVonZIbWdKAwgnqm0CSAxy1MFufmYEC5SM5Mc9uiK2DoHcbAL3d1jyaQ2fSDAJL8w=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
@@ -1429,7 +1479,7 @@
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
"arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -1453,8 +1503,6 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
"axios": ["axios@1.8.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw=="],
@@ -1489,8 +1537,6 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.24.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
@@ -1549,8 +1595,12 @@
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
"cloudflare": ["cloudflare@4.5.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-fPcbPKx4zF45jBvQ0z7PCdgejVAPBBCZxwqk1k7krQNfpM07Cfj97/Q6wBzvYqlWXx/zt1S9+m8vnfCe06umbQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"code-block-writer": ["code-block-writer@10.1.1", "", {}, "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw=="],
@@ -1653,7 +1703,7 @@
"detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
"detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
@@ -1697,8 +1747,6 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.36", "", {}, "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw=="],
"emoji-assets": ["emoji-assets@8.0.0", "", {}, "sha512-Y2M8Y5N6AUxKtE7AYj61+V3tU1CAcLQLX4kCO7HJEH1K+o2lgFd3ieiGSbZ8/qfz6qYxr0NUY+5Ds0WQer85og=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -1709,6 +1757,8 @@
"end-of-stream": ["end-of-stream@1.1.0", "", { "dependencies": { "once": "~1.3.0" } }, "sha512-EoulkdKF/1xa92q25PbjuDcgJ9RDHYU2Rs3SCIvs2/dSQ3BpmxneNHmA/M7fe60M3PrV7nNGTTNbkK62l6vXiQ=="],
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@@ -1823,11 +1873,13 @@
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"framer-motion": ["framer-motion@10.18.0", "", { "dependencies": { "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w=="],
@@ -1851,6 +1903,10 @@
"generic-pool": ["generic-pool@3.4.2", "", {}, "sha512-H7cUpwCQSiJmAHM4c/aFu6fUfrhWXW1ncyh8ftxEPMu6AiYkHw9K8br720TGPZJbk5eOH2bynjZD1yPvdDAmag=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
"get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
@@ -1865,7 +1921,7 @@
"glob": ["glob@11.0.1", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^4.0.1", "minimatch": "^10.0.0", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
@@ -1979,6 +2035,8 @@
"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=="],
"humanize-url": ["humanize-url@2.1.1", "", { "dependencies": { "normalize-url": "^4.5.1" } }, "sha512-V4nxsPGNE7mPjr1qDp471YfW8nhBiTRWrG/4usZlpvFU8I7gsV7Jvrrzv/snbLm5dWO3dr1ennu2YqnhTWFmYA=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
@@ -2059,7 +2117,7 @@
"jackspeak": ["jackspeak@4.0.2", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw=="],
"jiti": ["jiti@1.21.6", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="],
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
@@ -2117,6 +2175,28 @@
"leven": ["leven@4.0.0", "", {}, "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@@ -2163,7 +2243,7 @@
"lru-queue": ["lru-queue@0.1.0", "", { "dependencies": { "es5-ext": "~0.10.2" } }, "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ=="],
"magic-string": ["magic-string@0.30.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="],
@@ -2331,22 +2411,20 @@
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
"node-forge": ["node-forge@0.10.0", "", {}, "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA=="],
"node-gyp-build": ["node-gyp-build@4.8.2", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw=="],
"node-releases": ["node-releases@2.0.18", "", {}, "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g=="],
"nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
"normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
"normalize-url": ["normalize-url@4.5.1", "", {}, "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA=="],
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
@@ -2723,7 +2801,9 @@
"tailwind-shades": ["tailwind-shades@1.1.2", "", {}, "sha512-gsyyr9NtfPS1QNWV/YQMcoO5tLStTwp/So2I6jnPWmCFmqY8JEM1csBoq0nbFtuRsjYypC/Bm09pprUyLL/Zjg=="],
"tailwindcss": ["tailwindcss@3.4.13", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw=="],
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
"tar": ["tar@4.4.18", "", { "dependencies": { "chownr": "^1.1.4", "fs-minipass": "^1.2.7", "minipass": "^2.9.0", "minizlib": "^1.3.3", "mkdirp": "^0.5.5", "safe-buffer": "^5.2.1", "yallist": "^3.1.1" } }, "sha512-ZuOtqqmkV9RE1+4odd+MhBpibmCxNP6PJhH/h2OqNuotTX7/XHPZQJv2pKvWMplFH9SIZZhitehh6vBH6LO8Pg=="],
@@ -2841,8 +2921,6 @@
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"update-browserslist-db": ["update-browserslist-db@1.1.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A=="],
"update-notifier": ["update-notifier@4.1.3", "", { "dependencies": { "boxen": "^4.2.0", "chalk": "^3.0.0", "configstore": "^5.0.1", "has-yarn": "^2.1.0", "import-lazy": "^2.1.0", "is-ci": "^2.0.0", "is-installed-globally": "^0.3.1", "is-npm": "^4.0.0", "is-yarn-global": "^0.3.0", "latest-version": "^5.0.0", "pupa": "^2.0.1", "semver-diff": "^3.1.1", "xdg-basedir": "^4.0.0" } }, "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
@@ -2897,6 +2975,8 @@
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
"web-vitals": ["web-vitals@0.2.4", "", {}, "sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -2913,7 +2993,7 @@
"wrangler": ["wrangler@4.10.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.3.1", "blake3-wasm": "2.1.5", "esbuild": "0.24.2", "miniflare": "4.20250409.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.15", "workerd": "1.20250409.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250409.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-fTE4hZ79msEUt8+HEjl/8Q72haCyzPLu4PgrU3L81ysmjrMEdiYfUPqnvCkBUVtJvrDNdctTEimkufT1Y0ipNg=="],
"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=="],
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -2929,10 +3009,14 @@
"xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
@@ -3607,12 +3691,16 @@
"@formatjs/intl-localematcher/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@headlessui/tailwindcss/tailwindcss": ["tailwindcss@3.4.13", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw=="],
"@hyperjump/json-schema/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@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=="],
"@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=="],
@@ -3637,6 +3725,8 @@
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
"@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/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=="],
@@ -3647,6 +3737,8 @@
"@opennextjs/aws/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
"@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-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
"@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
@@ -3965,6 +4057,20 @@
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@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=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
"@ts-morph/common/fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
@@ -4013,6 +4119,8 @@
"@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"@vue/compiler-sfc/magic-string": ["magic-string@0.30.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw=="],
"@vue/compiler-sfc/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="],
"@vueuse/integrations/@vueuse/core": ["@vueuse/core@11.2.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.2.0", "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" } }, "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA=="],
@@ -4049,6 +4157,14 @@
"camelcase-keys/quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="],
"cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"cloudflare/@types/node": ["@types/node@18.19.121", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ=="],
"cloudflare/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=="],
"codemirror/@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=="],
"codemirror/@codemirror/commands": ["@codemirror/commands@6.7.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw=="],
@@ -4085,8 +4201,6 @@
"express/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
@@ -4155,8 +4269,6 @@
"meow/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"micro/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
"micro/content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
"micromark/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
@@ -4241,6 +4353,8 @@
"send/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"sharp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
"sharp/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
@@ -4255,14 +4369,6 @@
"sucrase/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=="],
"tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"tailwindcss/fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
"tailwindcss/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="],
"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=="],
"tar/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="],
"terminal-link/supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="],
@@ -4271,8 +4377,6 @@
"ts-node/acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="],
"ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="],
"unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"update-notifier/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
@@ -4287,10 +4391,14 @@
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"@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=="],
@@ -4331,6 +4439,8 @@
"@argos-ci/core/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"@argos-ci/core/sharp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
"@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=="],
@@ -4765,10 +4875,26 @@
"@dotenvx/dotenvx/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"@headlessui/tailwindcss/tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
"@headlessui/tailwindcss/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"@headlessui/tailwindcss/tailwindcss/fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
"@headlessui/tailwindcss/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"@headlessui/tailwindcss/tailwindcss/jiti": ["jiti@1.21.6", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="],
"@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=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"@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=="],
@@ -4783,6 +4909,8 @@
"@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"@playwright/test/playwright/playwright-core": ["playwright-core@1.54.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
@@ -4871,7 +4999,15 @@
"@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=="],
"@ts-morph/common/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@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=="],
"@tailwindcss/oxide/tar/minizlib": ["minizlib@3.0.1", "", { "dependencies": { "minipass": "^7.0.4", "rimraf": "^5.0.5" } }, "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg=="],
"@tailwindcss/oxide/tar/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"@tailwindcss/oxide/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"@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=="],
@@ -4909,6 +5045,12 @@
"bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"cliui/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"codemirror/@codemirror/autocomplete/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="],
"codemirror/@codemirror/commands/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="],
@@ -4929,8 +5071,6 @@
"gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"globby/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"google-auth-library/jws/jwa": ["jwa@2.0.0", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA=="],
"gtoken/jws/jwa": ["jwa@2.0.0", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA=="],
@@ -4969,16 +5109,6 @@
"sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"tailwindcss/chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"tailwindcss/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"tailwindcss/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="],
"wrangler/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=="],
"wrangler/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=="],
@@ -5017,10 +5147,16 @@
"wrangler/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"wrangler/sharp/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
"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=="],
"yargs/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"yargs/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@argos-ci/core/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="],
"@aws-sdk/client-dynamodb/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
@@ -5157,6 +5293,16 @@
"@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
"@headlessui/tailwindcss/tailwindcss/chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"@headlessui/tailwindcss/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@headlessui/tailwindcss/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"@headlessui/tailwindcss/tailwindcss/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@headlessui/tailwindcss/tailwindcss/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="],
"@node-minify/core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@node-minify/core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
@@ -5181,6 +5327,8 @@
"wrangler/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="],
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@argos-ci/core/sharp/@img/sharp-wasm32/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw=="],
+1 -1
View File
@@ -34,7 +34,7 @@
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.129.0"
"@gitbook/api": "^0.136.0"
}
},
"patchedDependencies": {
+1
View File
@@ -0,0 +1 @@
dist/
+3
View File
@@ -0,0 +1,3 @@
# `@gitbook/browser-types`
Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@gitbook/browser-types",
"description": "Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"version": "0.0.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:"
},
"devDependencies": {
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"files": ["dist", "README.md", "CHANGELOG.md"]
}
+99
View File
@@ -0,0 +1,99 @@
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
export type GitBookIntegrationEvent = 'load' | 'unload';
export type GitBookIntegrationEventCallback = (...args: unknown[]) => void;
export type GitBookIntegrationTool = AIToolDefinition & {
/**
* Confirmation action to be displayed to the user before executing the tool.
*/
confirmation?: {
icon?: IconName;
label: string;
};
/**
* Callback when the tool is executed.
* The input is provided by the AI assistant following the input schema of the tool.
*/
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
};
export type GitBookAssistant = {
/**
* Name of the assistant displayed in the UI.
*/
label: string;
/**
* Icon of the assistant displayed in the UI.
* Any FontAwesome icon name is supported.
* @example 'sparkle'
*/
icon: string;
/**
* Callback when the assistant is opened.
*/
open: (query?: string) => void;
/**
* Whether to display the triggers for this assistant in the UI.
* @default true
*/
ui?: boolean;
};
export type GitBookGlobal = {
/**
* Register an event listener.
*/
addEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
/**
* Remove an event listener.
*/
removeEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
/**
* Register a custom tool to be exposed to the AI assistant.
*/
registerTool: (tool: GitBookIntegrationTool) => void;
/**
* Register a custom assistant to be available on the site.
* Returns a registration object that lets the integration signal readiness
* and dispose the assistant when no longer needed.
*/
registerAssistant: (assistant: GitBookAssistant) => GitBookAssistantRegistration;
};
declare global {
interface Window {
/**
* Global `window.GitBook` object accessible by integrations.
*/
GitBook?: GitBookGlobal;
}
}
export type GitBookAssistantRegistration = ((this: void) => void) & {
/**
* Call this when the integration has finished initializing and is ready to
* receive `open` calls.
*/
ready: () => void;
/**
* Unregister the assistant.
*/
dispose: () => void;
};
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
+26
View File
@@ -1,5 +1,31 @@
# gitbook
## 0.15.0
### Minor Changes
- d130532: Suggest questions in the current space context
### Patch Changes
- 44f4151: Fix close buttons tooltips
- d903273: Strip visitor params from URL
- df7de8f: Fix an issue where links were not rendering properly in Ask AI answers
- d270b4a: All tracking is now disabled for dynamic routes such as the site preview.
- b1f608b: Implement basic URL scheme for assistant with ask
- d024658: Prevent page breaks inside blocks when printing
- 185cdb4: Fix scrolling to anchor positionning
- 5c49235: Remove highlighting in Safari for PowerShell and C++ to avoid page crash until next version with bug fix is released
- f1a6dec: Update OpenAPI parser
- 2cdba53: Upgrade to Tailwind v4
- Updated dependencies [2cdba53]
- Updated dependencies [f1a6dec]
- @gitbook/react-math@0.6.1
- @gitbook/icons@0.2.2
- @gitbook/openapi-parser@3.0.0
- @gitbook/react-contentkit@0.7.3
- @gitbook/react-openapi@1.3.5
## 0.14.1
### Patch Changes
+177 -56
View File
@@ -1,4 +1,5 @@
import {
CustomizationAIMode,
CustomizationBackground,
CustomizationCorners,
CustomizationDefaultMonospaceFont,
@@ -19,6 +20,7 @@ import {
import { getSiteAPIToken } from '../tests/utils';
import {
type Test,
type TestsCase,
allDeprecatedThemePresets,
allLocales,
@@ -34,6 +36,179 @@ import {
waitForNotFound,
} from './util';
const searchTestCases: Test[] = [
{
name: 'Search - AI Mode: None - Complete flow',
url: getCustomizationURL({
ai: {
mode: CustomizationAIMode.None,
},
}),
screenshot: false,
run: async (page) => {
const searchInput = page.getByTestId('search-input');
await searchInput.focus();
await expect(page.getByTestId('search-results')).toHaveCount(0); // No pop-up yet because there's no recommended questions.
// Fill search input, expecting search results
await searchInput.fill('gitbook');
await expect(page.getByTestId('search-results')).toBeVisible();
const pageResults = await page.getByTestId('search-page-result').all();
await expect(pageResults.length).toBeGreaterThan(2);
const pageSectionResults = await page.getByTestId('search-page-section-result').all();
await expect(pageSectionResults.length).toBeGreaterThan(2);
await expect(page.getByTestId('search-ask-question')).toHaveCount(0); // No AI search results with aiMode=None.
},
},
{
name: 'Search - AI Mode: None - Keyboard shortcut',
url: getCustomizationURL({
ai: {
mode: CustomizationAIMode.None,
},
}),
screenshot: false,
run: async (page) => {
await page.keyboard.press('ControlOrMeta+K');
await expect(page.getByTestId('search-input')).toBeFocused();
},
},
{
name: 'Search - AI Mode: None - URL query (Initial)',
url: `${getCustomizationURL({
ai: {
mode: CustomizationAIMode.None,
},
})}&q=`,
run: async (page) => {
await expect(page.getByTestId('search-results')).toHaveCount(0); // No pop-up yet because there's no recommended questions.
},
},
{
name: 'Search - AI Mode: None - URL query (Results)',
url: `${getCustomizationURL({
ai: {
mode: CustomizationAIMode.None,
},
})}&q=gitbook`,
run: async (page) => {
await expect(page.getByTestId('search-input')).toBeFocused();
await expect(page.getByTestId('search-input')).toHaveValue('gitbook');
await expect(page.getByTestId('search-results')).toBeVisible();
},
},
// TODO: Re-enable the following tests when we have fixed the AI Search timing out:
// - Search - AI Mode: Search - Complete flow
// - Search - AI Mode: Search - URL query (Initial)
{
name: 'Search - AI Mode: Search - URL query (Results)',
url: `${getCustomizationURL({
ai: {
mode: CustomizationAIMode.Search,
},
})}&q=gitbook`,
screenshot: false,
run: async (page) => {
await expect(page.getByTestId('search-input')).toBeFocused();
await expect(page.getByTestId('search-input')).toHaveValue('gitbook');
await expect(page.getByTestId('search-results')).toBeVisible();
},
},
// TODO: Re-enable the following tests when we have fixed the AI Search timing out:
// - Ask - AI Mode: Search - URL query (Ask initial)
// - Ask - AI Mode: Search - URL query (Ask results)
{
name: 'Ask - AI Mode: Assistant - Complete flow',
url: getCustomizationURL({
ai: {
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
const searchInput = page.locator('css=[data-testid="search-input"]');
// Focus search input, expecting recommended questions
await searchInput.focus();
// TODO: Re-enable this part of the test when we have fixed the AI Search timing out
// await expect(page.getByTestId('search-results')).toBeVisible();
// const recommendedQuestions = await page
// .getByTestId('search-recommended-question')
// .all();
// await expect(recommendedQuestions.length).toBeGreaterThan(2); // Expect at least 3 questions
// Fill search input, expecting AI search option
await searchInput.fill('What is gitbook?');
const aiSearchResult = page.getByTestId('search-ask-question');
await expect(aiSearchResult).toBeVisible();
await aiSearchResult.click();
await expect(page.getByTestId('ai-chat')).toBeVisible();
},
},
{
name: 'Ask - AI Mode: Assistant - Keyboard shortcut',
url: getCustomizationURL({
ai: {
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
await page.keyboard.press('ControlOrMeta+I');
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
},
},
{
name: 'Ask - AI Mode: Assistant - Button',
url: getCustomizationURL({
ai: {
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
await page.getByTestId('ai-chat-button').click();
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
},
},
{
name: 'Ask - AI Mode: Assistant - URL query (Initial)',
url: `${getCustomizationURL({
ai: {
mode: CustomizationAIMode.Assistant,
},
})}&ask=`,
screenshot: false,
run: async (page) => {
await expect(page.getByTestId('search-input')).not.toBeFocused();
await expect(page.getByTestId('search-input')).not.toHaveValue('What is GitBook?');
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
},
},
{
name: 'Ask - AI Mode: Assistant - URL query (Results)',
url: `${getCustomizationURL({
ai: {
mode: CustomizationAIMode.Assistant,
},
})}&ask=What+is+GitBook%3F`,
screenshot: false,
run: async (page) => {
await expect(page.getByTestId('search-input')).not.toBeFocused();
await expect(page.getByTestId('search-input')).not.toHaveValue('What is GitBook?');
await expect(page.getByTestId('ai-chat')).toBeVisible({
timeout: 15_000,
});
await expect(page.getByTestId('ai-chat-message').first()).toHaveText(
'What is GitBook?'
);
},
},
];
const testCases: TestsCase[] = [
{
name: 'GitBook Site (Single Variant)',
@@ -53,34 +228,7 @@ const testCases: TestsCase[] = [
);
},
},
{
name: 'Search',
url: '?q=',
screenshot: false,
run: async (page) => {
await expect(page.getByTestId('search-results')).toBeVisible();
const allItems = await page.getByTestId('search-result-item').all();
// Expect at least 3 questions
await expect(allItems.length).toBeGreaterThan(2);
},
},
{
name: 'Search Results',
url: '?q=gitbook',
run: async (page) => {
await expect(page.getByTestId('search-results')).toBeVisible();
},
},
{
name: 'AI Search',
url: '?q=What+is+GitBook%3F&ask=true',
run: async (page) => {
await expect(page.getByTestId('search-ask-answer')).toBeVisible({
timeout: 15_000,
});
},
screenshot: false,
},
...searchTestCases,
{
name: 'Not found',
url: 'content-not-found',
@@ -279,34 +427,7 @@ const testCases: TestsCase[] = [
url: '',
run: waitForCookiesDialog,
},
{
name: 'Search',
url: '?q=',
screenshot: false,
run: async (page) => {
await expect(page.getByTestId('search-results')).toBeVisible();
const allItems = await page.getByTestId('search-result-item').all();
// Expect at least 3 questions
await expect(allItems.length).toBeGreaterThan(2);
},
},
{
name: 'Search Results',
url: '?q=gitbook',
run: async (page) => {
await expect(page.getByTestId('search-results')).toBeVisible();
},
},
{
name: 'AI Search',
url: '?q=What+is+GitBook%3F&ask=true',
run: async (page) => {
await expect(page.getByTestId('search-ask-answer')).toBeVisible({
timeout: 15_000,
});
},
screenshot: false,
},
...searchTestCases,
{
name: 'Not found',
url: 'content-not-found',
@@ -2,6 +2,9 @@ import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/ini
import { DurableObject } from 'cloudflare:workers';
//Only needed to run locally, in prod we'll use the one from do.js
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
// Only needed to run locally, in prod we'll use the one from do.js
export class R2WriteBuffer extends DurableObject {
writePromise;
@@ -36,13 +36,17 @@
{
"name": "WRITE_BUFFER",
"class_name": "R2WriteBuffer"
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["R2WriteBuffer"]
"new_sqlite_classes": ["R2WriteBuffer", "DOShardedTagCache"]
}
]
},
@@ -23,6 +23,7 @@ export default class extends WorkerEntrypoint {
`gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
);
return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
redirect: 'manual',
cf: {
cacheEverything: false,
},
@@ -33,6 +34,9 @@ export default class extends WorkerEntrypoint {
modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
const nextRequest = new Request(modifiedUrl, reqOrResp);
return fetch(nextRequest, {
// We never want to follow the redirects here.
// Redirects are supposed to happen from the client.
redirect: 'manual',
cf: {
cacheEverything: false,
},
@@ -22,7 +22,11 @@
"vars": {
"STAGE": "dev",
"NEXT_PRIVATE_DEBUG_CACHE": "true",
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
// When deployed locally, we don't have access to the tag cache here,
// we should just bypass the cache to go to the server directly
"SHOULD_BYPASS_CACHE": "true",
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
"GITBOOK_URL": "http://localhost:8771"
},
"r2_buckets": [
{
+31 -12
View File
@@ -36,6 +36,11 @@ class GitbookIncrementalCache implements IncrementalCache {
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
// and go directly to the server.
return null;
}
try {
// Check local cache first if available
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
@@ -43,7 +48,13 @@ class GitbookIncrementalCache implements IncrementalCache {
const result = (await localCacheEntry.json()) as WithLastModified<
CacheValue<CacheType>
>;
return this.returnNullOn404(result);
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);
@@ -134,19 +145,27 @@ class GitbookIncrementalCache implements IncrementalCache {
}
async writeToR2(key: string, value: string): Promise<void> {
const env = getCloudflareContext().env as {
WRITE_BUFFER: DurableObjectNamespace<
Rpc.DurableObjectBranded & {
write: (key: string, value: string) => Promise<void>;
}
>;
};
const id = env.WRITE_BUFFER.idFromName(key);
try {
const env = getCloudflareContext().env as {
WRITE_BUFFER: DurableObjectNamespace<
Rpc.DurableObjectBranded & {
write: (key: string, value: string) => Promise<void>;
}
>;
};
const id = env.WRITE_BUFFER.idFromName(key);
// A stub is a client used to invoke methods on the Durable Object
const stub = env.WRITE_BUFFER.get(id);
// A stub is a client used to invoke methods on the Durable Object
const stub = env.WRITE_BUFFER.get(id);
await stub.write(key, value);
await stub.write(key, value);
} catch {
// We fallback to writing directly to R2
// it can fail locally because the limit is 1Mb per args
// It is 32Mb in production, so we should be fine
const r2 = getCloudflareContext().env[BINDING_NAME];
r2?.put(key, value);
}
}
async getCacheInstance(): Promise<Cache> {
@@ -1,4 +1,4 @@
import { getLogger } from '@/lib/logger';
import { createLogger, getLogger } from '@/lib/logger';
import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
@@ -6,8 +6,9 @@ import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-ca
const originalTagCache = doShardedTagCache({
baseShardSize: 12,
regionalCache: true,
// We can probably increase this value even further
regionalCacheTtlSec: 60,
regionalCacheTtlSec: 60 * 5 /* 5 minutes */,
// Because we invalidate the Cache API on update, we can safely set this to true
regionalCacheDangerouslyPersistMissingTags: true,
shardReplication: {
numberOfSoftReplicas: 2,
numberOfHardReplicas: 1,
@@ -20,6 +21,7 @@ const originalTagCache = doShardedTagCache({
export default {
name: 'GitbookTagCache',
mode: 'nextMode',
// We don't really use this one, as of now it is only used for soft tags
getLastRevalidated: async (tags: string[]) => {
const tagsToCheck = tags.filter(softTagFilter);
if (tagsToCheck.length === 0) {
@@ -29,12 +31,20 @@ export default {
return await originalTagCache.getLastRevalidated(tagsToCheck);
},
hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
const tagsToCheck = tags.filter(softTagFilter);
if (tagsToCheck.length === 0) {
return false; // If no tags to check, return false
}
try {
const tagsToCheck = tags.filter(softTagFilter);
if (tagsToCheck.length === 0) {
return false; // If no tags to check, return false
}
return await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
return await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
} catch (e) {
createLogger('gitbookTagCache', {}).error(
`hasBeenRevalidated - Error checking tags ${tags.join(', ')}`,
e
);
return false; // In case of error, return false
}
},
writeTags: async (tags: string[]) => {
const tagsToWrite = tags.filter(softTagFilter);
@@ -43,7 +53,7 @@ export default {
logger.warn('writeTags - No valid tags to write');
return; // If no tags to write, exit early
}
// Write only the filtered tags
await originalTagCache.writeTags(tagsToWrite);
},
} satisfies NextModeTagCache;
+11 -10
View File
@@ -1,11 +1,12 @@
{
"name": "gitbook",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/browser-types": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/icons": "workspace:*",
@@ -13,7 +14,7 @@
"@gitbook/react-contentkit": "workspace:*",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@opennextjs/cloudflare": "^1.4.0",
"@opennextjs/cloudflare": "^1.6.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-navigation-menu": "^1.2.3",
@@ -22,6 +23,7 @@
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
"@tusbar/cache-control": "^1.0.2",
"ai": "^4.2.2",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
@@ -49,7 +51,7 @@
"object-identity": "^0.1.2",
"openapi-types": "^12.1.3",
"p-map": "^7.0.3",
"@tusbar/cache-control": "^1.0.2",
"quick-lru": "^7.0.1",
"react-hotkeys-hook": "^4.4.1",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
@@ -67,12 +69,12 @@
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0",
"warn-once": "^0.1.1",
"zustand": "^5.0.3",
"quick-lru": "^7.0.1"
"zustand": "^5.0.3"
},
"devDependencies": {
"@argos-ci/playwright": "^5.0.5",
"@playwright/test": "^1.51.1",
"@argos-ci/playwright": "^5.0.9",
"@playwright/test": "^1.54.2",
"@tailwindcss/postcss": "^4.1.11",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
@@ -84,14 +86,13 @@
"@types/react": "18.3.13",
"@types/react-dom": "18.3.1",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"deepmerge": "^4.3.1",
"env-cmd": "^10.1.0",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"psi": "^4.1.0",
"stylelint": "^16.16.0",
"tailwindcss": "^3.4.0",
"tailwindcss": "^4.1.11",
"ts-essentials": "^10.0.1",
"typescript": "^5.5.3",
"vercel": "^39.3.0"
@@ -100,7 +101,7 @@
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math && wrangler types",
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math",
"dev": "env-cmd --silent -f ../../.env.local next --turbopack",
"dev": "env-cmd --silent -f ../../.env.local next",
"build": "next build",
"start": "next start",
"build:cloudflare": "opennextjs-cloudflare build",
+1 -2
View File
@@ -1,6 +1,5 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
};
@@ -5,8 +5,9 @@ import {
generateSiteLayoutMetadata,
generateSiteLayoutViewport,
} from '@/components/SiteLayout';
import { GITBOOK_DISABLE_TRACKING } from '@/lib/env';
import { getThemeFromMiddleware } from '@/lib/middleware';
import { shouldTrackEvents } from '@/lib/tracking';
import { headers } from 'next/headers';
interface SiteDynamicLayoutProps {
params: Promise<RouteLayoutParams>;
@@ -18,13 +19,14 @@ export default async function SiteDynamicLayout({
}: React.PropsWithChildren<SiteDynamicLayoutProps>) {
const { context, visitorAuthClaims } = await getDynamicSiteContext(await params);
const forcedTheme = await getThemeFromMiddleware();
const withTracking = shouldTrackEvents(await headers());
return (
<CustomizationRootLayout forcedTheme={forcedTheme} customization={context.customization}>
<SiteLayout
context={context}
forcedTheme={forcedTheme}
withTracking={!GITBOOK_DISABLE_TRACKING}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
>
{children}
@@ -5,7 +5,7 @@ import {
generateSiteLayoutMetadata,
generateSiteLayoutViewport,
} from '@/components/SiteLayout';
import { GITBOOK_DISABLE_TRACKING } from '@/lib/env';
import { shouldTrackEvents } from '@/lib/tracking';
interface SiteStaticLayoutProps {
params: Promise<RouteLayoutParams>;
@@ -16,12 +16,13 @@ export default async function SiteStaticLayout({
children,
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
const { context, visitorAuthClaims } = await getStaticSiteContext(await params);
const withTracking = shouldTrackEvents();
return (
<CustomizationRootLayout customization={context.customization}>
<SiteLayout
context={context}
withTracking={!GITBOOK_DISABLE_TRACKING}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
>
{children}
@@ -1 +1,2 @@
export * from './useAI';
export * from './useAIChat';
@@ -23,7 +23,7 @@ export function AIMessageView(
<div
key={index}
className={tcls(
'flex animate-fadeIn-slow flex-col gap-2',
'flex animate-fade-in-slow flex-col gap-2',
step.content.nodes.length > 0 ? 'has-content' : ''
)}
>
@@ -10,6 +10,7 @@ import type {
AIToolCallGetPageContent,
AIToolCallGetPages,
AIToolCallMCP,
AIToolCallOther,
AIToolCallSearch,
ContentRef,
} from '@gitbook/api';
@@ -38,7 +39,7 @@ function ToolCallSummary(props: { toolCall: AIToolCall; context: GitBookSiteCont
const { toolCall, context } = props;
return (
<div className="flex origin-left animate-scaleIn-slow items-start gap-2 text-sm text-tint-subtle">
<div className="flex origin-left animate-scale-in-slow items-start gap-2 text-sm text-tint-subtle">
<Icon
icon={getIconForToolCall(toolCall)}
className="mt-1 size-3 shrink-0 text-tint-subtle/8"
@@ -58,6 +59,8 @@ function getDescriptionForToolCall(toolCall: AIToolCall, context: GitBookSiteCon
return <DescriptionForGetPagesToolCall toolCall={toolCall} context={context} />;
case 'mcp':
return <DescriptionForMCPToolCall toolCall={toolCall} context={context} />;
case 'other':
return <DescriptionForOtherToolCall toolCall={toolCall} context={context} />;
default:
return <>{toolCall.tool}</>;
}
@@ -112,6 +115,15 @@ function DescriptionForMCPToolCall(props: {
);
}
function DescriptionForOtherToolCall(props: {
toolCall: AIToolCallOther;
context: GitBookSiteContext;
}) {
const { toolCall } = props;
return <p>{toolCall.summary.text}</p>;
}
async function DescriptionForSearchToolCall(props: {
toolCall: AIToolCallSearch;
context: GitBookSiteContext;
@@ -175,12 +187,12 @@ async function DescriptionForSearchToolCall(props: {
) : null}
</summary>
{hasResults ? (
<div className="mt-1 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all duration-500 [transition-behavior:allow-discrete] group-open:max-h-96 group-open:opacity-11">
<div className="mt-1 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all transition-discrete duration-500 group-open:max-h-96 group-open:opacity-11">
<ol className="space-y-1">
{searchResultsWithHrefs.map((result, index) => (
<li
key={`${result.pageId}-${index}`}
className="animate-fadeIn-slow"
className="animate-fade-in-slow"
style={{
animationDelay: `${index * 25}ms`,
}}
@@ -247,6 +259,8 @@ function getIconForToolCall(toolCall: AIToolCall): IconName {
return 'magnifying-glass';
case 'getPages':
return 'files';
case 'other':
return (toolCall.summary.icon as IconName) ?? 'hammer';
default:
return 'hammer';
}
@@ -1,6 +1,7 @@
'use server';
import type { GitBookBaseContext } from '@/lib/context';
import { fetchServerActionSiteContext } from '@/lib/server-actions';
import { traceErrorOnly } from '@/lib/tracing';
import {
type AIMessage,
AIMessageRole,
@@ -19,97 +20,100 @@ export async function streamRenderAIMessage(
rawStream: AsyncIterable<AIStreamResponse>,
options?: RenderAIMessageOptions
) {
const message: AIMessage = {
id: '',
role: AIMessageRole.Assistant,
steps: [],
};
const updateProcessingMessageStep = (
stepIndex: number,
callback: (step: AIMessageStep) => void
) => {
if (stepIndex > message.steps.length) {
throw new Error(
`Step index out of bounds ${stepIndex} (${message.steps.length} steps)`
);
}
if (message.steps[stepIndex]) {
message.steps = [...message.steps];
message.steps[stepIndex] = { ...message.steps[stepIndex] };
callback(message.steps[stepIndex]);
} else {
message.steps = [
...message.steps,
{
content: {
object: 'document',
data: {},
nodes: [],
},
},
];
callback(message.steps[stepIndex]);
}
};
// Fetch the full-context in the background to avoid blocking the stream.
const promiseContext = fetchServerActionSiteContext(baseContext);
return parseResponse<{
content: React.ReactNode;
event: AIStreamResponse;
}>(rawStream, async (event) => {
switch (event.type) {
/**
* The agent is processing a tool call in a new message.
*/
case 'response_tool_call': {
updateProcessingMessageStep(event.stepIndex, (step) => {
step.toolCalls ??= [];
step.toolCalls.push(event.toolCall);
});
break;
}
/**
* The agent is writing the content of a new message.
*/
case 'response_reasoning':
case 'response_document': {
updateProcessingMessageStep(event.stepIndex, (step) => {
const container = event.type === 'response_reasoning' ? 'reasoning' : 'content';
step[container] ??= {
object: 'document',
data: {},
nodes: [],
};
step[container] = {
...step[container],
nodes: [...step[container].nodes],
};
if (event.operation === 'insert') {
step[container].nodes.push(...event.blocks);
} else {
step[container].nodes.splice(
-event.blocks.length,
event.blocks.length,
...event.blocks
);
}
});
break;
}
}
return {
event,
content: (
<AIMessageView message={message} context={await promiseContext} {...options} />
),
return traceErrorOnly('AI.streamRenderAIMessage', async () => {
const message: AIMessage = {
id: '',
role: AIMessageRole.Assistant,
steps: [],
};
const updateProcessingMessageStep = (
stepIndex: number,
callback: (step: AIMessageStep) => void
) => {
if (stepIndex > message.steps.length) {
throw new Error(
`Step index out of bounds ${stepIndex} (${message.steps.length} steps)`
);
}
if (message.steps[stepIndex]) {
message.steps = [...message.steps];
message.steps[stepIndex] = { ...message.steps[stepIndex] };
callback(message.steps[stepIndex]);
} else {
message.steps = [
...message.steps,
{
content: {
object: 'document',
data: {},
nodes: [],
},
},
];
callback(message.steps[stepIndex]);
}
};
// Fetch the full-context in the background to avoid blocking the stream.
const promiseContext = fetchServerActionSiteContext(baseContext);
return parseResponse<{
content: React.ReactNode;
event: AIStreamResponse;
}>(rawStream, async (event) => {
switch (event.type) {
/**
* The agent is processing a tool call in a new message.
*/
case 'response_tool_call': {
updateProcessingMessageStep(event.stepIndex, (step) => {
step.toolCalls ??= [];
step.toolCalls.push(event.toolCall);
});
break;
}
/**
* The agent is writing the content of a new message.
*/
case 'response_reasoning':
case 'response_document': {
updateProcessingMessageStep(event.stepIndex, (step) => {
const container =
event.type === 'response_reasoning' ? 'reasoning' : 'content';
step[container] ??= {
object: 'document',
data: {},
nodes: [],
};
step[container] = {
...step[container],
nodes: [...step[container].nodes],
};
if (event.operation === 'insert') {
step[container].nodes.push(...event.blocks);
} else {
step[container].nodes.splice(
-event.blocks.length,
event.blocks.length,
...event.blocks
);
}
});
break;
}
}
return {
event,
content: (
<AIMessageView message={message} context={await promiseContext} {...options} />
),
};
});
});
}
@@ -1,7 +1,14 @@
'use server';
import { getSiteURLDataFromMiddleware } from '@/lib/middleware';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { type AIMessageContext, AIMessageRole, AIModel } from '@gitbook/api';
import { traceErrorOnly } from '@/lib/tracing';
import {
type AIMessageContext,
AIMessageRole,
AIModel,
type AIToolCallResult,
type AIToolDefinition,
} from '@gitbook/api';
import { streamRenderAIMessage } from './api';
import type { RenderAIMessageOptions } from './types';
@@ -12,32 +19,44 @@ export async function* streamAIChatResponse({
message,
messageContext,
previousResponseId,
toolCall,
tools,
options,
}: {
message: string;
message?: string;
messageContext: AIMessageContext;
previousResponseId?: string;
toolCall?: AIToolCallResult;
tools?: AIToolDefinition[];
options?: RenderAIMessageOptions;
}) {
const context = await getServerActionBaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const { stream } = await traceErrorOnly('AI.streamAIChatResponse', async () => {
const context = await getServerActionBaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const api = await context.dataFetcher.api();
const rawStream = api.orgs.streamAiResponseInSite(siteURLData.organization, siteURLData.site, {
mode: 'assistant',
input: [
const api = await context.dataFetcher.api();
const rawStream = api.orgs.streamAiResponseInSite(
siteURLData.organization,
siteURLData.site,
{
role: AIMessageRole.User,
content: message,
context: messageContext,
},
],
output: { type: 'document' },
model: AIModel.ReasoningLow,
previousResponseId,
});
input: message
? [
{
role: AIMessageRole.User,
content: message,
context: messageContext,
},
]
: [],
model: AIModel.ReasoningLow,
previousResponseId,
toolCall,
tools,
}
);
const { stream } = await streamRenderAIMessage(context, rawStream, options);
return await streamRenderAIMessage(context, rawStream, options);
});
for await (const output of stream) {
yield output;
@@ -1,6 +1,7 @@
'use server';
import { getSiteURLDataFromMiddleware } from '@/lib/middleware';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { traceErrorOnly } from '@/lib/tracing';
import { streamRenderAIMessage } from './api';
import type { RenderAIMessageOptions } from './types';
@@ -14,16 +15,18 @@ export async function* streamAIResponseById({
responseId: string;
options?: RenderAIMessageOptions;
}) {
const context = await getServerActionBaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const { stream } = await traceErrorOnly('AI.streamAIResponseById', async () => {
const context = await getServerActionBaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const api = await context.dataFetcher.api();
const rawStream = api.orgs.streamExistingAiResponseInSite(
siteURLData.organization,
siteURLData.site,
responseId
);
const { stream } = await streamRenderAIMessage(context, rawStream, options);
const api = await context.dataFetcher.api();
const rawStream = api.orgs.streamExistingAiResponseInSite(
siteURLData.organization,
siteURLData.site,
responseId
);
return await streamRenderAIMessage(context, rawStream, options);
});
for await (const output of stream) {
yield output;
@@ -0,0 +1,144 @@
'use client';
import { CustomizationAIMode } from '@gitbook/api';
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
import * as React from 'react';
import type { ReactNode } from 'react';
import { tString, useLanguage } from '@/intl/client';
import type { GitBookAssistant } from '@gitbook/browser-types';
import { useAIChatController, useAIChatState } from '.';
import { AIChatIcon, getAIChatName } from '../AIChat';
import { useIntegrationAssistants } from '../Integrations';
import { useSearch } from '../Search/useSearch';
// Unify assistants configuration context with the assistants hook in one place
export type AIConfig = {
aiMode: CustomizationAIMode;
trademark: boolean;
};
export type Assistant = Omit<GitBookAssistant, 'icon'> & {
/**
* Unique identifier for the assistant. Generated automatically using Crypto.randomUUID().
* @example '123e4567-e89b-12d3-a456-426614174000'
*/
id: string;
/**
* Display mode for the assistant. Currently, only `overlay` is supported for custom assistants.
* - `overlay`: Display the assistant in an overlay on top of the page.
* - `sidebar`: Display the assistant in a sidebar next to the page. Only supported for the GitBook Assistant.
* - `search`: Display the assistant inside the search container. Only supported for GitBook AI Search.
* @default 'overlay'
*/
mode?: 'overlay' | 'sidebar' | 'search';
/**
* Icon of the assistant displayed in the UI.
*/
icon: ReactNode;
/**
* Assistant readiness. Built-in assistants are always ready.
* Integrations set this to true via the registration's ready() callback.
*/
ready?: boolean;
};
const AIContext = React.createContext<AIConfig | null>(null);
export function AIContextProvider(props: React.PropsWithChildren<AIConfig>): React.ReactElement {
const { aiMode, trademark, children } = props;
const value = React.useMemo(() => ({ aiMode, trademark }), [aiMode, trademark]);
return <AIContext.Provider value={value}>{children}</AIContext.Provider>;
}
function useAIConfig(): AIConfig {
const ctx = React.useContext(AIContext);
if (!ctx) {
throw new Error('useAI must be used within AIContextProvider');
}
return ctx;
}
type AIContext = {
config: AIConfig;
assistants: Assistant[];
};
/**
* Unified assistants list combining the built-in GitBook Assistant (when enabled)
* with any custom assistants registered at runtime.
*/
export function useAI(): AIContext {
const config = useAIConfig();
const chat = useAIChatState();
const chatController = useAIChatController();
const language = useLanguage();
const [, setSearchState] = useSearch();
const assistants: Assistant[] = [];
if (config.aiMode === CustomizationAIMode.Assistant) {
assistants.push({
id: 'gitbook-assistant',
label: getAIChatName(config.trademark),
icon: (
<AIChatIcon
state={chat.loading ? 'thinking' : 'default'}
trademark={config.trademark}
/>
),
open: (query?: string) => {
chatController.open();
if (query) {
chatController.postMessage({ message: query });
}
},
ui: true,
mode: 'sidebar',
ready: true,
});
} else if (config.aiMode === CustomizationAIMode.Search) {
assistants.push({
id: 'gitbook-ai-search',
label: tString(language, 'ai_chat_context_badge'),
icon: (
<div className="relative">
<Icon icon="search" className="size-4" />
<Icon
icon="sparkle"
iconStyle={IconStyle.Solid}
className="absolute top-[2.5px] left-[2.6px] size-2"
/>
</div>
),
open: (query?: string) => {
if (query) {
setSearchState((prev) =>
prev ? { ...prev, query: null, ask: query, open: true } : null
);
}
},
ui: false,
mode: 'search',
ready: true,
});
}
const integrationAssistants = useIntegrationAssistants();
if (integrationAssistants.length > 0) {
assistants.push(
...integrationAssistants.map((assistant) => ({
...assistant,
icon: <Icon icon={assistant.icon as IconName} className="size-4" />,
}))
);
}
return {
config,
assistants,
};
}
+340 -123
View File
@@ -2,9 +2,16 @@
import * as zustand from 'zustand';
import { AIMessageRole } from '@gitbook/api';
import {
AIMessageRole,
type AIStreamResponseToolCallPending,
type AIToolCallResult,
} from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
import * as React from 'react';
import { useTrackEvent } from '../Insights';
import { integrationsAssistantTools } from '../Integrations';
import { useSearch } from '../Search';
import { streamAIChatResponse } from './server-actions';
import { useAIMessageContextRef } from './useAIMessageContext';
@@ -14,6 +21,21 @@ export type AIChatMessage = {
query?: string;
};
export type AIChatPendingTool = {
icon?: IconName;
label: string;
/**
* Confirm the tool call by calling this function.
*/
confirm: () => Promise<void>;
/**
* Tool call result to cancel it.
*/
cancelToolCall: AIToolCallResult;
};
export type AIChatState = {
/**
* If true, the chat is open.
@@ -30,6 +52,11 @@ export type AIChatState = {
*/
query: string | null;
/**
* The first query sent to the AI. This is appended to the URL when the AI chat is opened.
*/
initialQuery: string | null;
/**
* Messages in the session.
*/
@@ -40,6 +67,11 @@ export type AIChatState = {
*/
followUpSuggestions: string[];
/**
* Tools that are pending confirmation to be executed.
*/
pendingTools: AIChatPendingTool[];
/**
* If true, the session is in progress.
*/
@@ -56,35 +88,26 @@ export type AIChatState = {
export type AIChatController = {
/** Open the dialog */
open: () => void;
/** Close the dialog */
close: () => void;
/** Post a message to the session */
postMessage: (input: {
/** The message to post to the session. it can be markdown formatted. */
message: string;
}) => void;
postMessage: (input: { message: string }) => void;
/** Clear the conversation */
clear: () => void;
};
const globalState = zustand.create<{
state: AIChatState;
setState: (fn: (state: AIChatState) => Partial<AIChatState>) => void;
}>((set) => {
// Global state store for AI chat
const globalState = zustand.create<AIChatState>(() => {
return {
state: {
opened: false,
responseId: null,
messages: [],
query: null,
followUpSuggestions: [],
loading: false,
error: false,
},
setState: (fn) => set((state) => ({ state: { ...state.state, ...fn(state.state) } })),
opened: false,
responseId: null,
messages: [],
query: null,
followUpSuggestions: [],
pendingTools: [],
loading: false,
error: false,
initialQuery: null,
};
});
@@ -92,118 +115,312 @@ const globalState = zustand.create<{
* Get the current state of the AI chat.
*/
export function useAIChatState(): AIChatState {
const state = zustand.useStore(globalState, (state) => state.state);
const state = zustand.useStore(globalState);
return state;
}
/**
* Get the controller to interact with the AI chat.
* Integrates with search state to synchronize ?ask= parameter.
*/
export function useAIChatController(): AIChatController {
const messageContextRef = useAIMessageContextRef();
const setState = zustand.useStore(globalState, (state) => state.setState);
const trackEvent = useTrackEvent();
const [, setSearchState] = useSearch();
// Open AI chat and sync with search state
const onOpen = React.useCallback(() => {
const { initialQuery } = globalState.getState();
globalState.setState((state) => ({ ...state, opened: true }));
// Update search state to show ask mode with first message or current ask value
setSearchState((prev) => ({
ask: prev?.ask ?? initialQuery ?? '',
query: prev?.query ?? null,
global: prev?.global ?? false,
open: false, // Close search popover when opening chat
}));
}, [setSearchState]);
// Close AI chat and clear ask parameter
const onClose = React.useCallback(() => {
globalState.setState((state) => ({ ...state, opened: false }));
// Clear ask parameter but keep other search state
setSearchState((prev) => ({
ask: null,
query: prev?.query ?? null,
global: prev?.global ?? false,
open: false,
}));
}, [setSearchState]);
// Stream a message with the AI backend
const streamResponse = React.useCallback(
async (input: {
/** Text message to send to the AI backend */
message?: string;
/** Tool call to send to the AI backend */
toolCall?: AIToolCallResult;
}) => {
globalState.setState((state) => {
return {
...state,
followUpSuggestions: [],
pendingTools: [],
loading: true,
error: false,
messages: [
...state.messages,
{
role: AIMessageRole.Assistant,
content: null, // Placeholder for streaming response
},
],
};
});
// Execute a tool call
const executeToolCall = async (event: AIStreamResponseToolCallPending) => {
const integrationTools = integrationsAssistantTools.getState().tools;
const toolDef = integrationTools.find((tool) => tool.name === event.toolCall.tool);
if (!toolDef) {
throw new Error(`Tool ${event.toolCall.tool} not found`);
}
try {
const result = await toolDef.execute(event.toolCall.input);
streamResponse({
toolCall: {
tool: event.toolCall.tool,
toolCallId: event.toolCallId,
output: result.output,
summary: result.summary,
},
});
} catch (error) {
streamResponse({
toolCall: {
tool: event.toolCall.tool,
toolCallId: event.toolCallId,
output: {
error: error instanceof Error ? error.message : 'Unknown error',
},
summary: {
icon: 'bomb',
text: 'An error occurred while executing the tool',
},
},
});
}
};
let toolToExecute: AIStreamResponseToolCallPending | null = null;
try {
const integrationTools = integrationsAssistantTools.getState().tools;
const stream = await streamAIChatResponse({
message: input.message,
toolCall: input.toolCall,
messageContext: messageContextRef.current,
previousResponseId: globalState.getState().responseId ?? undefined,
tools: integrationTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
});
// Process streaming response
for await (const data of stream) {
if (!data) continue;
if (input.message && globalState.getState().query !== input.message) {
// Chat was cleared, stop processing the stream
break;
}
const event = data.event;
switch (event.type) {
case 'response_finish': {
globalState.setState((state) => ({
...state,
responseId: event.responseId,
// Mark as not loading when the response is finished
// Even if the stream might continue as we receive 'response_followup_suggestion'
loading: false,
error: false,
}));
break;
}
case 'response_followup_suggestion': {
globalState.setState((state) => ({
...state,
followUpSuggestions: [
...state.followUpSuggestions,
...event.suggestions,
],
}));
break;
}
case 'response_tool_call_pending': {
const toolDef = integrationTools.find(
(tool) => tool.name === event.toolCall.tool
);
if (!toolDef) {
throw new Error(`Tool ${event.toolCall.tool} not found`);
}
const confirmation = toolDef.confirmation;
if (confirmation) {
globalState.setState((state) => ({
...state,
pendingTools: [
...state.pendingTools,
{
icon: confirmation.icon,
label: confirmation.label,
cancelToolCall: {
tool: event.toolCall.tool,
toolCallId: event.toolCallId,
output: {
cancelled: 'User did not confirm the tool call',
},
summary: {
icon: 'forward',
text: `Skipped confirmation of "${confirmation.label}"`,
},
},
confirm: async () => {
await executeToolCall(event);
},
},
],
}));
} else {
toolToExecute = event;
}
break;
}
}
// Update the assistant message with streamed content
globalState.setState((state) => ({
...state,
messages: [
...state.messages.slice(0, -1),
{
role: AIMessageRole.Assistant,
content: data.content,
},
],
}));
}
// Execute the tool call if it doesn't require confirmation
if (toolToExecute) {
await executeToolCall(toolToExecute);
}
globalState.setState((state) => ({
...state,
loading: false,
error: false,
}));
} catch {
globalState.setState((state) => ({
...state,
loading: false,
error: true,
}));
}
},
[messageContextRef.current]
);
// Post a message to the AI chat
const onPostMessage = React.useCallback(
async (input: { message: string }) => {
const { query, messages, pendingTools } = globalState.getState();
// For first message, update the ask parameter in URL
if (messages.length === 0) {
setSearchState((prev) => ({
ask: input.message,
query: prev?.query ?? null,
global: prev?.global ?? false,
open: false,
}));
}
if (query === input.message) {
// Return early if the message is the same as the previous message
return;
}
trackEvent({ type: 'ask_question', query: input.message });
// Add user message and placeholder for AI response
globalState.setState((state) => {
return {
...state,
messages: [
...state.messages,
{
role: AIMessageRole.User,
content: input.message,
query: input.message,
},
],
query: input.message,
followUpSuggestions: [],
loading: true,
error: false,
initialQuery: state.initialQuery ?? input.message,
};
});
const pendingTool = pendingTools[0];
streamResponse({
message: input.message,
// If we had a pending tool call, we need to send it as being cancelled
// otherwise the AI will fail to process the message
...(pendingTool ? { toolCall: pendingTool.cancelToolCall } : {}),
});
},
[setSearchState, trackEvent, streamResponse]
);
// Clear the conversation and reset ask parameter
const onClear = React.useCallback(() => {
globalState.setState((state) => ({
opened: state.opened,
loading: false,
messages: [],
query: null,
followUpSuggestions: [],
pendingTools: [],
responseId: null,
error: false,
initialQuery: null,
}));
// Reset ask parameter to empty string (keeps chat open but clears content)
setSearchState((prev) => ({
ask: '',
query: prev?.query ?? null,
global: prev?.global ?? false,
open: false,
}));
}, [setSearchState]);
return React.useMemo(() => {
return {
open: () => setState((state) => ({ ...state, opened: true })),
close: () => setState((state) => ({ ...state, opened: false })),
clear: () =>
setState((state) => ({
opened: state.opened,
loading: false,
messages: [],
query: null,
followUpSuggestions: [],
responseId: null,
error: false,
})),
postMessage: async (input: { message: string }) => {
trackEvent({ type: 'ask_question', query: input.message });
setState((state) => {
return {
...state,
messages: [
...state.messages,
{
// TODO: how to handle markdown here?
// to avoid rendering as plain text
role: AIMessageRole.User,
content: input.message,
},
{
role: AIMessageRole.Assistant,
content: null,
},
],
query: input.message,
followUpSuggestions: [],
loading: true,
error: false,
};
});
try {
const stream = await streamAIChatResponse({
message: input.message,
messageContext: messageContextRef.current,
previousResponseId: globalState.getState().state.responseId ?? undefined,
});
for await (const data of stream) {
if (!data) continue;
const event = data.event;
switch (event.type) {
case 'response_finish': {
setState((state) => ({
...state,
responseId: event.responseId,
// Mark as not loading when the response is finished
// Even if the stream might continue as we receive 'response_followup_suggestion'
loading: false,
error: false,
}));
break;
}
case 'response_followup_suggestion': {
setState((state) => ({
...state,
followUpSuggestions: [
...state.followUpSuggestions,
...event.suggestions,
],
}));
break;
}
}
setState((state) => ({
...state,
messages: [
...state.messages.slice(0, -1),
{
role: AIMessageRole.Assistant,
content: data.content,
},
],
}));
}
setState((state) => ({
...state,
loading: false,
error: false,
}));
} catch {
setState((state) => ({
...state,
loading: false,
error: true,
}));
}
},
open: onOpen,
close: onClose,
clear: onClear,
postMessage: onPostMessage,
};
}, [messageContextRef, setState, trackEvent]);
}, [onOpen, onClose, onClear, onPostMessage]);
}
@@ -1,12 +1,10 @@
'use client';
import { useAIChatController } from '@/components/AI/useAIChat';
import { useAIChatState } from '@/components/AI/useAIChat';
import { useAIChatState } from '@/components/AI';
import type { Assistant } from '@/components/AI';
import { ChatGPTIcon } from '@/components/AIActions/assets/ChatGPTIcon';
import { ClaudeIcon } from '@/components/AIActions/assets/ClaudeIcon';
import { MarkdownIcon } from '@/components/AIActions/assets/MarkdownIcon';
import { getAIChatName } from '@/components/AIChat';
import { AIChatIcon } from '@/components/AIChat';
import { Button } from '@/components/primitives/Button';
import { DropdownMenuItem, useDropdownMenuClose } from '@/components/primitives/DropdownMenu';
import { tString, useLanguage } from '@/intl/client';
@@ -22,32 +20,21 @@ type AIActionType = 'button' | 'dropdown-menu-item';
/**
* Opens our AI Docs Assistant.
*/
export function OpenDocsAssistant(props: { type: AIActionType; trademark: boolean }) {
const { type, trademark } = props;
const chatController = useAIChatController();
export function OpenAIAssistant(props: { assistant: Assistant; type: AIActionType }) {
const { assistant, type } = props;
const chat = useAIChatState();
const language = useLanguage();
return (
<AIActionWrapper
type={type}
icon={
<AIChatIcon state={chat.loading ? 'thinking' : 'default'} trademark={trademark} />
}
label={tString(language, 'ai_chat_ask', getAIChatName(trademark))}
icon={assistant.icon}
label={assistant.label}
shortLabel={tString(language, 'ask')}
description={tString(language, 'ai_chat_ask_about_page', getAIChatName(trademark))}
description={tString(language, 'ai_chat_ask_about_page', assistant.label)}
disabled={chat.loading}
onClick={() => {
// Open the chat if it's not already open
if (!chat.opened) {
chatController.open();
}
// Send the "What is this page about?" message
chatController.postMessage({
message: tString(language, 'ai_chat_suggested_questions_about_this_page'),
});
assistant.open(tString(language, 'ai_chat_suggested_questions_about_this_page'));
}}
/>
);
@@ -231,13 +218,15 @@ function AIActionWrapper(props: {
}
size="xsmall"
variant="secondary"
label={shortLabel || label}
label={label ?? shortLabel}
className="bg-tint-base text-sm"
onClick={onClick}
href={href}
target={href ? '_blank' : undefined}
disabled={disabled || loading}
/>
>
{shortLabel}
</Button>
);
}
@@ -251,13 +240,13 @@ function AIActionWrapper(props: {
>
<div className="flex size-5 items-center justify-center text-tint">
{loading ? (
<Icon icon="spinner-third" className="size-4 animate-spin" />
<Icon icon="spinner-third" className="size-4 shrink-0 animate-spin" />
) : icon ? (
typeof icon === 'string' ? (
<Icon
icon={icon as IconName}
iconStyle={IconStyle.Regular}
className="size-4 fill-transparent stroke-current"
className="size-4 shrink-0 fill-transparent stroke-current"
/>
) : (
icon
@@ -268,9 +257,9 @@ function AIActionWrapper(props: {
<div className="flex flex-1 flex-col gap-0.5">
<span className="flex items-center gap-2 text-tint-strong">
<span className="truncate font-medium text-sm">{label}</span>
{href ? <Icon icon="arrow-up-right" className="size-3" /> : null}
{href ? <Icon icon="arrow-up-right" className="size-3 shrink-0" /> : null}
</span>
{description && <span className="truncate text-tint text-xs">{description}</span>}
{description && <span className="text-tint text-xs">{description}</span>}
</div>
</DropdownMenuItem>
);
@@ -2,21 +2,21 @@
import {
CopyMarkdown,
OpenDocsAssistant,
OpenAIAssistant,
OpenInLLM,
ViewAsMarkdown,
} from '@/components/AIActions/AIActions';
import { Button, ButtonGroup } from '@/components/primitives/Button';
import { DropdownMenu, DropdownMenuSeparator } from '@/components/primitives/DropdownMenu';
import { tString, useLanguage } from '@/intl/client';
import type { SiteCustomizationSettings } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { useRef } from 'react';
import { useAI } from '../AI';
interface AIActionsDropdownProps {
markdownPageUrl: string;
withAIChat?: boolean;
trademark: boolean;
className?: string;
actions: SiteCustomizationSettings['pageActions'];
}
@@ -25,9 +25,11 @@ interface AIActionsDropdownProps {
*/
export function AIActionsDropdown(props: AIActionsDropdownProps) {
const ref = useRef<HTMLDivElement>(null);
const assistants = useAI().assistants.filter((assistant) => assistant.ui === true);
const language = useLanguage();
return (
<ButtonGroup ref={ref}>
return assistants.length > 0 || props.actions.markdown || props.actions.externalAI ? (
<ButtonGroup ref={ref} className={props.className}>
<DefaultAction {...props} />
{props.actions.markdown || props.actions.externalAI ? (
<DropdownMenu
@@ -41,6 +43,7 @@ export function AIActionsDropdown(props: AIActionsDropdownProps) {
className="size-3 transition-transform group-data-[state=open]/button:rotate-180"
/>
}
label={tString(language, 'more')}
iconOnly
size="xsmall"
variant="secondary"
@@ -52,26 +55,31 @@ export function AIActionsDropdown(props: AIActionsDropdownProps) {
</DropdownMenu>
) : null}
</ButtonGroup>
);
) : null;
}
/**
* The content of the dropdown menu.
*/
function AIActionsDropdownMenuContent(props: AIActionsDropdownProps) {
const { markdownPageUrl, withAIChat, trademark, actions } = props;
const { markdownPageUrl, actions } = props;
const assistants = useAI().assistants.filter((assistant) => assistant.ui === true);
return (
<>
{withAIChat ? (
<OpenDocsAssistant trademark={trademark} type="dropdown-menu-item" />
) : null}
{assistants.map((assistant) => (
<OpenAIAssistant
key={assistant.label}
assistant={assistant}
type="dropdown-menu-item"
/>
))}
{actions.markdown ? (
<>
<DropdownMenuSeparator className="first:hidden" />
<CopyMarkdown
isDefaultAction={!withAIChat}
isDefaultAction={!assistants.length}
markdownPageUrl={markdownPageUrl}
type="dropdown-menu-item"
/>
@@ -94,16 +102,17 @@ function AIActionsDropdownMenuContent(props: AIActionsDropdownProps) {
* A default action shown as a quick-access button beside the dropdown menu
*/
function DefaultAction(props: AIActionsDropdownProps) {
const { markdownPageUrl, withAIChat, trademark, actions } = props;
const { markdownPageUrl, actions } = props;
const assistants = useAI().assistants.filter((assistant) => assistant.ui === true);
if (withAIChat) {
return <OpenDocsAssistant trademark={trademark} type="button" />;
if (assistants.length) {
return <OpenAIAssistant assistant={assistants[0]} type="button" />;
}
if (actions.markdown) {
return (
<CopyMarkdown
isDefaultAction={!withAIChat}
isDefaultAction={!assistants.length}
markdownPageUrl={markdownPageUrl}
type="button"
/>
@@ -9,7 +9,7 @@ import {
type AIChatState,
useAIChatController,
useAIChatState,
} from '../AI/useAIChat';
} from '../AI';
import { useNow } from '../hooks';
import { Button } from '../primitives';
import { DropdownMenu, DropdownMenuItem } from '../primitives/DropdownMenu';
@@ -24,7 +24,7 @@ export function AIChat(props: { trademark: boolean }) {
const chatController = useAIChatController();
useHotkeys(
'mod+j',
'mod+i',
(e) => {
e.preventDefault();
chatController.open();
@@ -93,6 +93,15 @@ export function AIChatWindow(props: {
block: 'start',
});
const timeout = setTimeout(() => {
if (lastUserMessageRef.current) {
lastUserMessageRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, 100);
// We want the chat messages to scroll underneath the input, but they should scroll past the input when scrolling all the way down.
// The best way to do this is to observe the input height and adjust the padding bottom of the scroll container accordingly.
const observer = new ResizeObserver((entries) => {
@@ -103,12 +112,16 @@ export function AIChatWindow(props: {
if (inputRef.current) {
observer.observe(inputRef.current);
}
return () => observer.disconnect();
}, [chat.opened]);
return () => {
observer.disconnect();
clearTimeout(timeout);
};
}, []);
return (
<div
className="ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl animate-present scroll-mt-36 px-4 py-4 transition-all duration-300 sm:px-6 lg:fixed lg:w-80 lg:animate-enterFromRight lg:pr-4 lg:pl-0 xl:w-96"
data-testid="ai-chat"
className="ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl animate-present scroll-mt-36 px-4 py-4 transition-all duration-300 sm:px-6 lg:fixed lg:w-80 lg:animate-enter-from-right lg:pr-4 lg:pl-0 xl:w-96"
ref={containerRef}
>
<div className="relative flex h-full grow flex-col overflow-hidden circular-corners:rounded-3xl rounded-corners:rounded-md bg-tint-base text-sm text-tint depth-subtle:shadow-lg shadow-tint ring-1 ring-tint-subtle">
@@ -150,7 +163,6 @@ export function AIChatWindow(props: {
iconOnly
icon="ellipsis"
label={tString(language, 'actions')}
className="!px-2"
variant="blank"
size="default"
/>
@@ -174,7 +186,6 @@ export function AIChatWindow(props: {
iconOnly
icon="close"
label={tString(language, 'close')}
className="!px-2"
variant="blank"
size="default"
/>
@@ -189,7 +200,7 @@ export function AIChatWindow(props: {
>
{isEmpty ? (
<div className="flex min-h-full w-full shrink-0 flex-col items-center justify-center gap-6 py-4">
<div className="flex size-32 animate-fadeIn-slow items-center justify-center rounded-full bg-tint-subtle">
<div className="flex size-32 animate-fade-in-slow items-center justify-center rounded-full bg-tint-subtle">
<AIChatIcon
state="intro"
trademark={trademark}
@@ -218,7 +229,7 @@ export function AIChatWindow(props: {
</div>
<div
ref={inputRef}
className="absolute inset-x-0 bottom-0 mr-2 flex select-none flex-col gap-4 bg-gradient-to-b from-transparent to-50% to-tint-base/9 p-4 pr-2"
className="absolute inset-x-0 bottom-0 mr-2 flex select-none flex-col gap-4 bg-linear-to-b from-transparent to-50% to-tint-base/9 p-4 pr-2"
>
{/* Display an error banner when something went wrong. */}
{chat.error ? <AIChatError chatController={chatController} /> : null}
@@ -244,7 +255,7 @@ function AIChatError(props: { chatController: AIChatController }) {
const { chatController } = props;
return (
<div className="flex animate-scaleIn flex-wrap justify-between gap-2 circular-corners:rounded-2xl rounded-corners:rounded-md bg-danger px-3 py-2 text-danger text-sm ring-1 ring-danger">
<div className="flex animate-scale-in flex-wrap justify-between gap-2 circular-corners:rounded-2xl rounded-corners:rounded-md bg-danger px-3 py-2 text-danger text-sm ring-1 ring-danger">
<div className="flex items-center gap-2">
<Icon icon="exclamation-triangle" className="size-3.5" />
<span className="flex items-center gap-1">{t(language, 'ai_chat_error')}</span>
@@ -258,7 +269,7 @@ function AIChatError(props: { chatController: AIChatController }) {
onClick={() => {
chatController.clear();
}}
className="!text-danger hover:bg-danger-5"
className="text-danger! hover:bg-danger-5"
/>
</div>
</div>
@@ -2,41 +2,37 @@
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
import { useAIChatController, useAIChatState } from '../AI/useAIChat';
import type { Assistant } from '../AI';
import { Button } from '../primitives';
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
import { getAIChatName } from './AIChat';
import { AIChatIcon } from './AIChatIcon';
/**
* Button to open/close the AI chat.
*/
export function AIChatButton(props: { trademark: boolean }) {
const { trademark } = props;
const chatController = useAIChatController();
const chat = useAIChatState();
export function AIChatButton(props: {
assistant: Assistant;
showLabel?: boolean;
}) {
const { assistant, showLabel = true } = props;
const language = useLanguage();
return (
<Button
icon={<AIChatIcon trademark={trademark} />}
iconOnly
size="default"
icon={assistant.icon}
data-testid="ai-chat-button"
iconOnly={!showLabel}
size="medium"
variant="header"
className={tcls('h-9 px-2.5')}
label={
<div className="flex items-center gap-2">
{t(language, 'ai_chat_ask', getAIChatName(trademark))}
<KeyboardShortcut keys={['mod', 'j']} className="border-tint-11 text-tint-1" />
{t(language, 'ai_chat_ask', assistant.label)}
<KeyboardShortcut keys={['mod', 'i']} className="border-tint-11 text-tint-1" />
</div>
}
onClick={() => {
if (chat.opened) {
chatController.close();
} else {
chatController.open();
}
}}
/>
onClick={() => assistant.open()}
>
{showLabel ? <span className="max-md:hidden">{t(language, 'ask')}</span> : null}
</Button>
);
}
@@ -38,14 +38,14 @@ export function AIChatInput(props: {
// This fixes inconsistent focus behaviour across browsers
const timeout = setTimeout(() => {
inputRef.current?.focus();
}, 50);
}, 150);
return () => clearTimeout(timeout);
}
}, [disabled, loading]);
useHotkeys(
'mod+j',
'mod+i',
(e) => {
e.preventDefault();
inputRef.current?.focus();
@@ -54,14 +54,15 @@ export function AIChatInput(props: {
);
return (
<div className="relative flex flex-col overflow-hidden circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint-base/9 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base">
<div className="depth-subtle:has-[textarea:focus]:-translate-y-px relative flex flex-col overflow-hidden circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint-base/9 depth-subtle:shadow-sm shadow-tint/6 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:shadow-primary-subtle has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base dark:shadow-tint-1">
<textarea
ref={inputRef}
disabled={disabled || loading}
data-loading={loading}
data-testid="ai-chat-input"
className={tcls(
'resize-none',
'focus:outline-none',
'focus:outline-hidden',
'focus:ring-0',
'w-full',
'px-3',
@@ -98,10 +99,12 @@ export function AIChatInput(props: {
}
}}
/>
<div className="absolute top-2.5 right-3 animate-[fadeIn_0.2s_0.5s_ease-in-out_both] peer-focus:hidden">
<KeyboardShortcut keys={['mod', 'j']} />
</div>
<div className="absolute inset-x-0 bottom-0 flex items-center px-2 py-2">
{!disabled ? (
<div className="absolute top-2.5 right-3 animate-[fadeIn_0.2s_0.5s_ease-in-out_both] peer-focus:hidden">
<KeyboardShortcut keys={['mod', 'i']} className="bg-tint-base" />
</div>
) : null}
<div className="absolute inset-x-0 bottom-0 flex items-center gap-2 px-2 py-2">
<Tooltip
label={
<div className="flex flex-col gap-3 p-2">
@@ -125,11 +128,11 @@ export function AIChatInput(props: {
}
arrow
>
<div className="flex cursor-help items-center gap-1 circular-corners:rounded-2xl rounded-corners:rounded-md px-2 py-1 text-tint/7 text-xs transition-all hover:bg-tint">
<span className="-ml-1 rounded-corners:rounded circular-corners:rounded-2xl bg-tint-11/7 px-1 py-0.5 font-mono font-semibold text-[0.65rem] text-contrast-tint-11 leading-none">
<div className="flex cursor-help items-center gap-1 circular-corners:rounded-2xl rounded-corners:rounded-md px-2.5 py-1.5 text-tint/7 text-xs transition-all hover:bg-tint">
<span className="-ml-1 circular-corners:rounded-2xl rounded-corners:rounded-sm bg-tint-11/7 px-1 py-0.5 font-mono font-semibold text-[0.65rem] text-contrast-tint-11 leading-none">
{t(language, 'ai_chat_context_badge')}
</span>{' '}
<span>{t(language, 'ai_chat_context_title')}</span>
<span className="leading-none">{t(language, 'ai_chat_context_title')}</span>
</div>
</Tooltip>
<Button
@@ -1,7 +1,8 @@
import { tcls } from '@/lib/tailwind';
import { AIMessageRole } from '@gitbook/api';
import type React from 'react';
import type { AIChatController, AIChatState } from '../AI/useAIChat';
import type { AIChatController, AIChatState } from '../AI';
import { AIChatToolConfirmations } from './AIChatToolConfirmations';
import { AIResponseFeedback } from './AIResponseFeedback';
import { AIChatFollowupSuggestions } from './AiChatFollowupSuggestions';
@@ -23,8 +24,9 @@ export function AIChatMessages(props: {
return (
<div
ref={isLastUserMessage ? lastUserMessageRef : undefined}
data-testid="ai-chat-message"
className={tcls(
message.content ? 'animate-fadeIn-slow' : '',
message.content ? 'animate-fade-in-slow' : '',
'shrink-0',
'last:min-h-[calc(100%-5rem)]',
'scroll-mt-36',
@@ -43,7 +45,7 @@ export function AIChatMessages(props: {
{message.content ? message.content : null}
{isLastMessage && chat.loading ? (
<div className="flex w-full animate-fadeIn-slow flex-wrap gap-2 group-has-[.has-content]/message:hidden">
<div className="flex w-full animate-fade-in-slow flex-wrap gap-2">
{Array.from({ length: 7 }).map((_, index) => (
<div
key={index}
@@ -59,13 +61,18 @@ export function AIChatMessages(props: {
{isLastMessage ? (
<>
{!chat.loading && !chat.error && chat.query && chat.responseId && (
{!chat.loading &&
!chat.error &&
chat.query &&
chat.responseId &&
chat.pendingTools.length === 0 ? (
<AIResponseFeedback
responseId={chat.responseId}
query={chat.query}
className="-ml-1 -mt-4"
/>
)}
) : null}
<AIChatToolConfirmations chat={chat} />
<AIChatFollowupSuggestions
chat={chat}
chatController={chatController}
@@ -1,5 +1,5 @@
import { tString, useLanguage } from '@/intl/client';
import type { AIChatController } from '../AI/useAIChat';
import type { AIChatController } from '../AI';
import { Button } from '../primitives';
export default function AIChatSuggestedQuestions(props: { chatController: AIChatController }) {
@@ -17,7 +17,6 @@ export default function AIChatSuggestedQuestions(props: { chatController: AIChat
{DEFAULT_SUGGESTED_QUESTIONS.map((question, index) => (
<Button
key={question}
label={question}
variant="secondary"
size="medium"
className="max-w-full animate-[present_500ms_both] whitespace-normal"
@@ -0,0 +1,36 @@
import type { AIChatState } from '../AI';
import { Button } from '../primitives';
/**
* Display buttons to confirm tool calls.
*/
export function AIChatToolConfirmations(props: {
chat: AIChatState;
}) {
const { chat } = props;
if (chat.pendingTools.length === 0) {
return null;
}
return (
<div className="flex w-full flex-wrap justify-end gap-2">
{chat.pendingTools.map((tool, index) => (
<Button
key={index}
onClick={() => {
tool.confirm();
}}
label={tool.label}
className="whitespace-normal! max-w-full animate-[present_500ms_both] text-left ring-1 ring-tint-subtle"
size="medium"
variant="primary"
icon={tool.icon}
style={{
animationDelay: `${250 + Math.min(index * 50, 150)}ms`,
}}
/>
))}
</div>
);
}
@@ -31,8 +31,8 @@ export function AIResponseFeedback(props: {
label={tString(language, 'was_this_helpful_positive_label')}
variant="blank"
className={tcls(
'animate-fadeIn overflow-hidden text-tint-subtle transition-all',
rating !== null && rating !== 1 && 'px-0 text-[0] opacity-0'
'animate-fade-in overflow-hidden text-tint-subtle transition-all',
rating !== null && rating !== 1 && 'px-0 text-[0rem] opacity-0'
)}
size="medium"
style={{ animationDuration: '.5s' }}
@@ -47,8 +47,8 @@ export function AIResponseFeedback(props: {
label={tString(language, 'was_this_helpful_negative_label')}
variant="blank"
className={tcls(
'animate-fadeIn overflow-hidden text-tint-subtle transition-all',
rating !== null && rating !== -1 && 'px-0 text-[0] opacity-0'
'animate-fade-in overflow-hidden text-tint-subtle transition-all',
rating !== null && rating !== -1 && 'px-0 text-[0rem] opacity-0'
)}
size="medium"
style={{ animationDelay: '.2s', animationDuration: '.5s' }}
@@ -59,7 +59,7 @@ export function AIResponseFeedback(props: {
/>
{rating !== null ? (
<span
className="ml-2 animate-fadeIn-slow text-tint-subtle"
className="ml-2 animate-fade-in-slow text-tint-subtle"
style={{ animationDelay: '.3s' }}
>
{t(language, 'was_this_helpful_thank_you')}
@@ -23,7 +23,7 @@ export function AIChatFollowupSuggestions(props: {
chatController.postMessage({ message: suggestion });
}}
label={suggestion}
className="!whitespace-normal max-w-full animate-[present_500ms_both] text-left ring-1 ring-tint-subtle"
className="whitespace-normal! max-w-full animate-[present_500ms_both] text-left ring-1 ring-tint-subtle"
size="medium"
variant="blank"
style={{
@@ -49,7 +49,7 @@ export async function AdCoverRendering({
'rounded-lg',
'p-4',
'overflow-hidden',
'shadow-sm'
'shadow-xs'
)}
style={{ backgroundColor: ad.backgroundColor, color: ad.textColor ?? '#ffffff' }}
href={ad.statlink}
@@ -68,7 +68,7 @@ export async function AdCoverRendering({
}}
/>
<div className={tcls('z-[2]')}>
<div className={tcls('z-2')}>
<img
alt="Large cover"
src={largeImage}
@@ -81,10 +81,10 @@ export async function AdCoverRendering({
)}
/>
</div>
<div className={tcls('z-[2]')}>
<div className={tcls('z-2')}>
<img alt={ad.company} src={ad.logo} className={tcls('max-w-36', 'max-h-12')} />
</div>
<div className={tcls('flex', 'flex-col', 'z-[2]')}>
<div className={tcls('flex', 'flex-col', 'z-2')}>
<div className={tcls('text-sm', 'font-semibold', 'mb-2')}>{ad.companyTagline}</div>
<div
className={tcls(
@@ -99,7 +99,7 @@ export async function AdCoverRendering({
{ad.description}
</div>
</div>
<div className={tcls('z-[2]')}>
<div className={tcls('z-2')}>
<span
className={tcls(
'text-sm',
@@ -119,7 +119,7 @@ export async function AdCoverRendering({
</span>
</div>
<div
className={tcls('absolute', 'inset-0', 'backdrop-blur', 'z-[1]')}
className={tcls('absolute', 'inset-0', 'backdrop-blur-sm', 'z-1')}
style={{
backgroundColor: hexToRgba(ad.backgroundColor, 0.8),
}}
@@ -4,6 +4,7 @@ import type { SiteInsightsAd, SiteInsightsAdPlacement } from '@gitbook/api';
import { headers } from 'next/headers';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { traceErrorOnly } from '@/lib/tracing';
import { AdClassicRendering } from './AdClassicRendering';
import { AdCoverRendering } from './AdCoverRendering';
import { AdPixels } from './AdPixels';
@@ -40,40 +41,42 @@ interface FetchPlaceholderAdOptions {
* and properly access user-agent and IP.
*/
export async function renderAd(options: FetchAdOptions) {
const [context, result] = await Promise.all([
getServerActionBaseContext(),
options.source === 'live' ? fetchAd(options) : getPlaceholderAd(),
]);
return traceErrorOnly('Ads.renderAd', async () => {
const [context, result] = await Promise.all([
getServerActionBaseContext(),
options.source === 'live' ? fetchAd(options) : getPlaceholderAd(),
]);
const mode = options.source === 'live' ? options.mode : 'classic';
if (!result || !result.ad.description || !result.ad.statlink) {
return null;
}
const mode = options.source === 'live' ? options.mode : 'classic';
if (!result || !result.ad.description || !result.ad.statlink) {
return null;
}
const { ad } = result;
const { ad } = result;
const insightsAd: SiteInsightsAd | null =
options.source === 'live'
? {
placement: options.placement,
zoneId: options.zoneId,
domain: 'company' in ad ? ad.company : '',
}
: null;
const insightsAd: SiteInsightsAd | null =
options.source === 'live'
? {
placement: options.placement,
zoneId: options.zoneId,
domain: 'company' in ad ? ad.company : '',
}
: null;
return {
children: (
<>
{mode === 'classic' || !('callToAction' in ad) ? (
<AdClassicRendering ad={ad} insightsAd={insightsAd} context={context} />
) : (
<AdCoverRendering ad={ad} insightsAd={insightsAd} context={context} />
)}
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
</>
),
insightsAd,
};
return {
children: (
<>
{mode === 'classic' || !('callToAction' in ad) ? (
<AdClassicRendering ad={ad} insightsAd={insightsAd} context={context} />
) : (
<AdCoverRendering ad={ad} insightsAd={insightsAd} context={context} />
)}
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
</>
),
insightsAd,
};
});
}
async function fetchAd({
@@ -7,7 +7,7 @@ import { tcls } from '@/lib/tailwind';
import { type CustomizationAnnouncement, SiteInsightsLinkPosition } from '@gitbook/api';
import { Icon, type IconName } from '@gitbook/icons';
import { CONTAINER_STYLE } from '../layout';
import { Link } from '../primitives';
import { Button, Link } from '../primitives';
import { LinkStyles } from '../primitives/styles';
import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
@@ -86,14 +86,15 @@ export function AnnouncementBanner(props: {
</div>
</Tag>
{closeable ? (
<button
className={`absolute top-0 right-4 mt-2 mr-2 rounded circular-corners:rounded-lg straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
type="button"
<Button
iconOnly
icon="close"
label={tString(language, 'close')}
variant="blank"
size="default"
onClick={dismissAnnouncement}
title={tString(language, 'close')}
>
<Icon icon="close" className="size-4" />
</button>
className={`absolute top-0 right-4 mt-2 mr-2 circular-corners:rounded-lg rounded-sm straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
/>
) : null}
</div>
</div>
@@ -130,7 +131,7 @@ const BANNER_STYLES = {
icon: 'circle-exclamation',
iconColor: 'text-warning-subtle',
close: 'hover:bg-tint-base hover:ring-warning-subtle',
link: 'links-default:text-warning links-default:hover:text-warning-strong links-default:decoration-warning/6 links-accent:decoration-warning',
link: 'links-default:text-warning hover:links-default:text-warning-strong links-default:decoration-warning/6 links-accent:decoration-warning',
},
danger: {
container: 'bg-danger decoration-danger/6 ring-danger-subtle',
@@ -138,7 +139,7 @@ const BANNER_STYLES = {
icon: 'triangle-exclamation',
iconColor: 'text-danger-subtle',
close: 'hover:bg-tint-base hover:ring-danger-subtle',
link: 'links-default:text-danger links-default:hover:text-danger-strong links-default:decoration-danger/6 links-accent:decoration-danger',
link: 'links-default:text-danger hover:links-default:text-danger-strong links-default:decoration-danger/6 links-accent:decoration-danger',
},
success: {
container: 'bg-success decoration-success/6 ring-success-subtle',
@@ -146,6 +147,6 @@ const BANNER_STYLES = {
icon: 'circle-check',
iconColor: 'text-success-subtle',
close: 'hover:bg-tint-base hover:ring-success-subtle',
link: 'links-default:text-success links-default:hover:text-success-strong links-default:decoration-success/6 links-accent:decoration-success',
link: 'links-default:text-success hover:links-default:text-success-strong links-default:decoration-success/6 links-accent:decoration-success',
},
};
@@ -2,6 +2,7 @@
import { getDataOrNull } from '@/lib/data';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { traceErrorOnly } from '@/lib/tracing';
/**
* Return true if a change-request has been updated.
@@ -11,15 +12,17 @@ export async function hasContentBeenUpdated(props: {
changeRequestId: string;
revisionId: string;
}) {
const context = await getServerActionBaseContext();
const changeRequest = await getDataOrNull(
context.dataFetcher.getChangeRequest({
spaceId: props.spaceId,
changeRequestId: props.changeRequestId,
})
);
if (!changeRequest) {
return false;
}
return changeRequest.revision !== props.revisionId;
return traceErrorOnly('AutoRefreshContent.hasContentBeenUpdated', async () => {
const context = await getServerActionBaseContext();
const changeRequest = await getDataOrNull(
context.dataFetcher.getChangeRequest({
spaceId: props.spaceId,
changeRequestId: props.changeRequestId,
})
);
if (!changeRequest) {
return false;
}
return changeRequest.revision !== props.revisionId;
});
}
@@ -1,6 +1,4 @@
'use client';
import { Icon } from '@gitbook/icons';
import * as React from 'react';
import { Button, StyledLink } from '@/components/primitives';
@@ -45,7 +43,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'fixed',
'z-10',
'bg-tint-base',
'rounded',
'rounded-sm',
'straight-corners:rounded-none',
'circular-corners:rounded-2xl',
'ring-1',
@@ -61,7 +59,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'text-balance',
'sm:left-auto',
'lg:chat-open:mr-80',
'xl:chat-open:mr-[25rem]',
'xl:chat-open:mr-100',
'transition-all',
'duration-300'
)}
@@ -75,27 +73,14 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
</StyledLink>
)}
</p>
<button
type="button"
<Button
iconOnly
icon="close"
label={tString(language, 'close')}
variant="blank"
onClick={() => setShow(false)}
aria-label={tString(language, 'close')}
className={tcls(
'absolute',
'top-3',
'right-3',
'w-6',
'h-6',
'flex',
'justify-center',
'items-center',
'rounded-sm',
'circular-corners:rounded-full',
'hover:bg-tint-hover'
)}
title={tString(language, 'close')}
>
<Icon icon="xmark" className={tcls('size-4')} />
</button>
className={tcls('absolute', 'top-2', 'right-2', 'hover:bg-tint-hover')}
/>
<div className={tcls('mt-3', 'flex', 'flex-row', 'gap-2')}>
<Button
variant="primary"
@@ -34,7 +34,7 @@ export function AnnotationPopover(props: { children: React.ReactNode; body: Reac
'bg-tint',
'ring-1',
'ring-tint',
'rounded',
'rounded-sm',
'shadow-1xs',
'shadow-tint-12/1',
'dark:shadow-tint-1/2',
@@ -53,7 +53,7 @@ export function AnnotationPopover(props: { children: React.ReactNode; body: Reac
viewBox="0 0 8 5"
className={tcls(
'relative',
'z-[2]',
'z-2',
'fill-tint-3', // Same as bg-tint
'stroke-tint-7', // Same as ring-tint
'[paint-order:stroke_fill]'
@@ -85,9 +85,9 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
key={node.key || `${node.type}-${index}`}
block={node}
style={[
'mx-auto page-width-wide:mx-0 w-full decoration-primary/6',
'mx-auto page-width-wide:mx-0 w-full decoration-primary/6 print:break-inside-avoid',
node.data && 'fullWidth' in node.data && node.data.fullWidth
? 'max-w-screen-2xl'
? 'max-w-screen-xl'
: 'max-w-3xl',
FULL_WIDTH_BLOCKS.includes(node.type) && 'page-width-wide:max-w-screen-2xl',
blockStyle,
@@ -38,11 +38,11 @@ export function Caption(
'overflow-hidden',
'after:block',
'after:absolute',
'after:-inset-[0]',
'after:-inset-0',
'after:pointer-events-none',
fit ? 'w-fit' : null,
withBorder
? 'rounded circular-corners:rounded-2xl straight-corners:rounded-none after:border-tint-subtle after:border after:rounded circular-corners:after:rounded-2xl straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none'
? '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,
],
style,
@@ -43,12 +43,12 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
</div>
<CopyCodeButton
codeId={id}
style="z-[2] mt-2 mr-2 self-start justify-self-end rounded-md bg-transparent p-1 text-tint text-xs leading-none opacity-0 ring-1 ring-tint backdrop-blur-md transition-opacity duration-75 [grid-area:2/1] hover:ring-tint-hover group-hover/codeblock:opacity-[1]"
style="z-2 mt-2 mr-2 self-start justify-self-end leading-none opacity-0 backdrop-blur-md [grid-area:2/1] group-hover/codeblock:opacity-11"
/>
<pre
className={tcls(
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base p-2 [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base',
'rounded-md straight-corners:rounded-sm shadow-sm',
'rounded-md straight-corners:rounded-xs shadow-xs',
title && 'rounded-ss-none'
)}
>
@@ -2,6 +2,7 @@
import React from 'react';
import { Button } from '@/components/primitives';
import { t, useLanguage } from '@/intl/client';
import { type ClassValue, tcls } from '@/lib/tailwind';
@@ -41,9 +42,14 @@ export function CopyCodeButton(props: { codeId: string; style: ClassValue }) {
};
return (
<button onClick={onClick} className={tcls(style, 'print:hidden')}>
<Button
size="xsmall"
variant="secondary"
onClick={onClick}
className={tcls(style, 'translate-y-0!', 'print:hidden')}
>
{t(language, copied ? 'code_copied' : 'code_copy')}
</button>
</Button>
);
}
@@ -34,6 +34,8 @@ export type RenderedInline = {
body: React.ReactNode;
};
const isSafari =
typeof navigator !== 'undefined' && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const theme = createCssVariablesTheme();
const { getSingletonHighlighter } = createSingletonShorthands(
@@ -65,8 +67,13 @@ export async function highlight(
inlines: RenderedInline[]
): Promise<HighlightLine[]> {
const langName = getBlockLang(block);
if (!langName) {
// Language not found, fallback to plain highlighting
if (!langName || (isSafari && ['powershell', 'cpp'].includes(langName))) {
// Fallback to plain highlighting if
// - language is not found
// - TEMP : language is PowerShell or C++ and browser is Safari:
// RegExp#[Symbol.search] throws TypeError when `lastIndex` isnt writable
// Fixed in upcoming Safari 18.6, remove when it'll be released - RND-7772
return plainHighlight(block, inlines);
}
@@ -129,6 +136,9 @@ const syntaxAliases: Record<string, BundledLanguage> = {
// "Parser" language does not exist in Shiki, but it's used in GitBook
// The closest language is "Blade"
parser: 'blade',
// From GitBook App we receive "objectivec" instead of "objective-c"
objectivec: 'objective-c',
};
function checkIsBundledLanguage(lang: string): lang is BundledLanguage {
@@ -51,7 +51,7 @@ function transformLengthToCSS(length: Length | undefined) {
if (length.unit === '%') {
return {
className: [
'md:flex-shrink-0',
'md:shrink-0',
COLUMN_WIDTHS[Math.round(length.value * 0.01 * (COLUMN_WIDTHS.length - 1))],
],
};
@@ -50,12 +50,12 @@ export function Details(props: {
'border-b-0',
'border-tint-subtle',
//all
'[&]:mt-[0px]',
'mt-0!',
//select first child
'[&:first-child]:mt-5',
'[&:first-child]:rounded-t-lg',
'first:mt-5!',
'first:rounded-t-lg',
//select first in group
'[:not(&)_+&]:mt-5',
'[:not(&)_+&]:mt-5!',
'[:not(&)_+&]:rounded-t-lg',
//select last in group
'[&:not(:has(+_&))]:mb-5',
@@ -30,8 +30,8 @@ export function HashLinkButton(props: {
'opacity-0',
'group-hover/hash:opacity-[0]',
'group-focus/hash:opacity-[0]',
'md:group-hover/hash:md:opacity-[1]',
'md:group-focus/hash:md:opacity-[1]',
'md:group-hover/hash:opacity-[1]',
'md:group-focus/hash:opacity-[1]',
className
)}
>
@@ -26,7 +26,7 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
'heading',
'flex',
'items-baseline',
'scroll-m-12',
'scroll-mt-(--content-scroll-margin)',
getTextAlignment(block.data.align),
hashLinkButtonWrapperStyles,
style,
@@ -44,7 +44,7 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
<div
className={tcls(
'flex-1',
'z-[1]',
'z-1',
'justify-self-start',
'max-w-full',
'break-words',
@@ -21,7 +21,7 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
'hint',
'transition-colors',
'rounded-md',
hasHeading ? 'rounded-l' : null,
hasHeading ? 'rounded-l-sm' : null,
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'overflow-hidden',
@@ -52,7 +52,7 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
{hasHeading ? (
<Block
style={tcls(
'!py-4 w-full items-start pl-3 text-[1em] *:flex-none',
'w-full items-start py-4! pl-3 text-[1em] *:flex-none',
// Heading hash styles
'flip-heading-hash pr-8',
hintStyle.header
@@ -19,7 +19,7 @@ export async function InlineButton(props: InlineProps<api.DocumentInlineButton>)
return (
// Set the leading to have some vertical space between adjacent buttons
<span className="inline-button leading-[3rem] [&:has(+.inline-button)]:mr-2">
<span className="inline-button leading-12 [&:has(+.inline-button)]:mr-2">
<Button
href={resolved.href}
label={inline.data.label}
@@ -51,7 +51,7 @@ export async function InlineImage(props: InlineProps<DocumentInlineImage>) {
}}
priority="lazy"
preload
style={[size === 'line' ? ['max-h-[1lh]', 'h-[1lh]', 'w-auto'] : null]}
style={[size === 'line' ? ['max-h-lh', 'h-lh', 'w-auto'] : null]}
inline
zoom={!isInLink}
/>
@@ -1,11 +1,10 @@
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
import { getSpaceLanguage, tString } from '@/intl/server';
import { languages } from '@/intl/translations';
import type { GitBookAnyContext } from '@/lib/context';
import { type TranslationLanguage, languages } from '@/intl/translations';
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
import { Icon } from '@gitbook/icons';
import { StyledLink } from '../../primitives';
import { HoverCard, HoverCardRoot, HoverCardTrigger, StyledLink } from '../../primitives';
import type { InlineProps } from '../Inline';
import { Inlines } from '../Inlines';
import { InlineLinkTooltip } from './InlineLinkTooltip';
@@ -19,17 +18,34 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
resolveAnchorText: false,
})
: null;
const { contentContext } = context;
if (!context.contentContext || !resolved) {
const language =
contentContext && 'customization' in contentContext
? getSpaceLanguage(contentContext.customization)
: languages.en;
if (!contentContext || !resolved) {
return (
<span title="Broken link" className="underline">
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
</span>
<HoverCardRoot>
<HoverCardTrigger>
<span className="cursor-not-allowed underline">
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
</span>
</HoverCardTrigger>
<HoverCard className="flex flex-col gap-1 p-4">
<div className="flex items-center gap-2">
<Icon icon="ban" className="size-4 text-tint-subtle" />
<h5 className="font-semibold">{tString(language, 'notfound_title')}</h5>
</div>
<p className="text-sm text-tint">{tString(language, 'notfound_link')}</p>
</HoverCard>
</HoverCardRoot>
);
}
const isExternal = inline.data.ref.kind === 'url';
@@ -61,11 +77,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
if (context.shouldRenderLinkPreviews) {
return (
<InlineLinkTooltipWrapper
inline={inline}
context={context.contentContext}
resolved={resolved}
>
<InlineLinkTooltipWrapper inline={inline} language={language} resolved={resolved}>
{content}
</InlineLinkTooltipWrapper>
);
@@ -80,15 +92,13 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
*/
function InlineLinkTooltipWrapper(props: {
inline: DocumentInlineLink;
context: GitBookAnyContext;
children: React.ReactNode;
resolved: ResolvedContentRef;
language: TranslationLanguage;
}) {
const { inline, context, resolved, children } = props;
const { inline, language, resolved, children } = props;
let breadcrumbs = resolved.ancestors ?? [];
const language =
'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
const isExternal = inline.data.ref.kind === 'url';
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
if (isExternal) {
@@ -1,9 +1,8 @@
'use client';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import * as Tooltip from '@radix-ui/react-tooltip';
import { Fragment } from 'react';
import { Button, StyledLink } from '../../primitives';
import { Button, HoverCard, HoverCardRoot, HoverCardTrigger, StyledLink } from '../../primitives';
export function InlineLinkTooltipImpl(props: {
isSamePage: boolean;
@@ -21,89 +20,76 @@ export function InlineLinkTooltipImpl(props: {
const { isSamePage, isExternal, openInNewTabLabel, target, breadcrumbs, children } = props;
return (
<Tooltip.Provider delayDuration={200}>
<Tooltip.Root>
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="z-40 w-screen max-w-md animate-present px-4 sm:w-auto">
<div className="overflow-hidden rounded-md straight-corners:rounded-none shadow-lg shadow-tint-12/4 ring-1 ring-tint-subtle dark:shadow-tint-1 ">
<div className="bg-tint-base p-4">
<div className="flex items-start gap-4">
<div className="flex flex-col">
{breadcrumbs && breadcrumbs.length > 0 ? (
<div className="mb-1 flex grow flex-wrap items-center gap-x-2 gap-y-0.5 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{breadcrumbs.map((crumb, index) => {
const Tag = crumb.href ? StyledLink : 'div';
<HoverCardRoot>
<HoverCardTrigger>{children}</HoverCardTrigger>
<HoverCard className="p-4">
<div className="flex items-start gap-4">
<div className="flex flex-col">
{breadcrumbs && breadcrumbs.length > 0 ? (
<div className="mb-1 flex grow flex-wrap items-center gap-x-2 gap-y-0.5 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{breadcrumbs.map((crumb, index) => {
const Tag = crumb.href ? StyledLink : 'div';
return (
<Fragment key={crumb.label}>
{index !== 0 ? (
<Icon
icon="chevron-right"
className="size-3 text-tint-subtle"
/>
) : null}
<Tag
className={tcls(
'flex gap-1',
crumb.href &&
'links-default:text-tint no-underline hover:underline contrast-more:underline contrast-more:decoration-current'
)}
href={crumb.href ?? '#'}
>
{crumb.icon ? (
<span className="mt-0.5 text-tint-subtle empty:hidden">
{crumb.icon}
</span>
) : null}
{crumb.label}
</Tag>
</Fragment>
);
})}
</div>
) : null}
<div
className={tcls(
'flex gap-2 leading-snug',
isExternal && 'text-sm [overflow-wrap:anywhere]'
)}
>
{target.icon ? (
<div className="mt-1 text-tint-subtle empty:hidden">
{target.icon}
</div>
return (
<Fragment key={crumb.label}>
{index !== 0 ? (
<Icon
icon="chevron-right"
className="size-3 text-tint-subtle"
/>
) : null}
<h5 className="font-semibold">{target.text}</h5>
</div>
</div>
{!isSamePage && target.href ? (
<Button
className={tcls(
'-mx-2 -my-2 ml-auto',
breadcrumbs?.length === 0
? 'place-self-center'
: null
)}
variant="blank"
href={target.href}
target="_blank"
label={openInNewTabLabel}
size="small"
icon="arrow-up-right-from-square"
iconOnly={true}
/>
) : null}
</div>
{target.subText ? (
<p className="mt-1 text-sm text-tint">{target.subText}</p>
) : null}
<Tag
className={tcls(
'flex gap-1',
crumb.href &&
'links-default:text-tint no-underline hover:underline contrast-more:underline contrast-more:decoration-current'
)}
href={crumb.href ?? '#'}
>
{crumb.icon ? (
<span className="mt-0.5 text-tint-subtle empty:hidden">
{crumb.icon}
</span>
) : null}
{crumb.label}
</Tag>
</Fragment>
);
})}
</div>
) : null}
<div
className={tcls(
'flex gap-2 leading-snug',
isExternal && 'wrap-anywhere text-sm'
)}
>
{target.icon ? (
<div className="mt-1 text-tint-subtle empty:hidden">
{target.icon}
</div>
) : null}
<h5 className="font-semibold">{target.text}</h5>
</div>
<Tooltip.Arrow className="fill-tint-1" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
</div>
{!isSamePage && target.href ? (
<Button
className={tcls(
'-mx-2 -my-2 ml-auto',
breadcrumbs?.length === 0 ? 'place-self-center' : null
)}
variant="blank"
href={target.href}
target="_blank"
label={openInNewTabLabel}
size="small"
icon="arrow-up-right-from-square"
iconOnly={true}
/>
) : null}
</div>
{target.subText ? <p className="mt-1 text-sm text-tint">{target.subText}</p> : null}
</HoverCard>
</HoverCardRoot>
);
}
@@ -1,3 +1,5 @@
@reference '../../RootLayout/globals.css';
/** Stacks */
.contentkit-stack {
@apply flex gap-4;
@@ -114,7 +116,7 @@
/** Modals */
.contentkit-modal-backdrop {
@apply fixed inset-0 z-40 px-7 pb-11 pt-4 md:pt-[min(8vw,_6rem)] flex items-start justify-center backdrop-blur-2xl bg-tint-12/4 dark:bg-tint-1/6;
@apply fixed inset-0 z-40 px-7 pb-11 pt-4 md:pt-[min(8vw,6rem)] flex items-start justify-center backdrop-blur-2xl bg-tint-12/4 dark:bg-tint-1/6;
}
.contentkit-modal {
@apply flex flex-col gap-6 relative z-50 bg-tint
@@ -1,6 +1,7 @@
'use server';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { traceErrorOnly } from '@/lib/tracing';
import type { RenderIntegrationUI } from '@gitbook/api';
import { ContentKitOutput } from '@gitbook/react-contentkit';
import { contentKitServerContext } from './contentkit';
@@ -19,20 +20,22 @@ export async function renderIntegrationUi({
};
request: RenderIntegrationUI;
}) {
const serverAction = await getServerActionBaseContext();
const output = await fetchSafeIntegrationUI(serverAction, {
integrationName: renderContext.integrationName,
request,
});
return traceErrorOnly('DocumentView.renderIntegrationUi', async () => {
const serverAction = await getServerActionBaseContext();
const output = await fetchSafeIntegrationUI(serverAction, {
integrationName: renderContext.integrationName,
request,
});
if (output.error) {
return {
error: output.error.message,
};
}
if (output.error) {
return {
error: output.error.message,
children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
output: output.data,
};
}
return {
children: <ContentKitOutput output={output.data} context={contentKitServerContext} />,
output: output.data,
};
});
}
@@ -31,13 +31,13 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
nodes={block.nodes}
ancestorBlocks={[...ancestorBlocks, block]}
blockStyle={tcls(
'min-h-[1lh]',
'min-h-lh',
// flip heading hash icon if list item is a heading
'flip-heading-hash',
// remove margin-top for the first heading in a list
'[&:is(h2)]:pt-0',
'[&:is(h3)]:pt-0',
'[&:is(h4)]:pt-0',
'[h2]:pt-0',
'[h3]:pt-0',
'[h4]:pt-0',
// Override the "mx-auto" class from UnwrappedBlocks
'mx-0'
)}
@@ -137,7 +137,7 @@ function ListItemPrefix(props: { block: DocumentBlockListItem; children: React.R
className={tcls(
textStyle.textSize,
textStyle.lineHeight,
'mr-1 flex min-h-[1lh] min-w-6 items-center justify-center text-tint'
'mr-1 flex min-h-lh min-w-6 items-center justify-center text-tint'
)}
>
{props.children}
@@ -165,7 +165,7 @@ function PseudoBefore(props: {
}) {
return (
<div
className="before:font-var before:content-[--pseudoBefore--content]"
className="before:font-var before:content-(--pseudoBefore--content)"
style={
{
'--pseudoBefore--content': `'${props.content}'`,
@@ -1,4 +1,5 @@
@import "@scalar/api-client-react/style.css";
@reference "../../RootLayout/globals.css";
html,
body {
@@ -1,3 +1,5 @@
@reference "../../RootLayout/globals.css";
/* Layout Components */
.openapi-operation,
.openapi-schemas,
@@ -32,7 +34,7 @@
.openapi-deprecated,
.openapi-stability {
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:content-none! after:!content-none;
}
.openapi-stability-alpha {
@@ -70,7 +72,7 @@
}
.openapi-markdown code {
@apply py-px px-1 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
@apply py-px px-1 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:content-none! after:!content-none;
}
.openapi-markdown pre code {
@@ -79,7 +81,7 @@
/* Markdown Base */
.openapi-markdown {
@apply prose text-tint-strong prose-p:!my-2 dark:prose-invert max-w-none whitespace-normal;
@apply prose text-tint-strong prose-p:my-2! dark:prose-invert max-w-none whitespace-normal;
}
.openapi-markdown > *:first-child {
@@ -145,7 +147,7 @@
}
.openapi-column-preview-body {
@apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header:xl:max-2xl:top-32 site-header-sections:top-32 site-header-sections:xl:max-2xl:top-44 print-mode:static;
@apply flex flex-col gap-4 sticky top-[calc(var(--toc-top-offset)+5rem)] print-mode:static;
}
.openapi-column-preview pre {
@@ -158,7 +160,7 @@
}
.openapi-schema-root-description.openapi-markdown {
@apply prose-sm text-balance mt-1.5 !text-[0.813rem] text-tint overflow-hidden !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
@apply prose-sm text-balance mt-1.5 text-[0.813rem]! text-tint overflow-hidden font-normal! select-text prose-strong:font-semibold prose-strong:text-inherit;
}
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root-description {
@@ -273,7 +275,7 @@
/* Schema Description */
.openapi-schema-description.openapi-markdown {
@apply prose-sm text-tint overflow-hidden text-pretty !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit;
@apply prose-sm text-tint overflow-hidden text-pretty font-normal! select-text prose-strong:font-semibold prose-strong:text-inherit;
}
.openapi-schema-description.openapi-markdown pre:has(code) {
@@ -302,7 +304,7 @@
.openapi-schema-pattern code,
.openapi-schema-enum-value code,
.openapi-schema-default code {
@apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint-subtle bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
@apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint-subtle bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:content-none! after:!content-none;
}
/* Authentication */
@@ -335,7 +337,7 @@
}
.openapi-securities-description.openapi-markdown {
@apply prose-sm text-tint !font-normal select-text text-pretty prose-strong:font-semibold prose-strong:text-inherit;
@apply prose-sm text-tint font-normal! select-text text-pretty prose-strong:font-semibold prose-strong:text-inherit;
}
.openapi-securities-label {
@@ -366,7 +368,7 @@
}
.openapi-requestbody-description.openapi-markdown {
@apply prose-sm text-tint !font-normal text-pretty select-text prose-strong:font-semibold prose-strong:text-inherit;
@apply prose-sm text-tint font-normal! text-pretty select-text prose-strong:font-semibold prose-strong:text-inherit;
}
/* Responses */
@@ -379,21 +381,23 @@
}
.openapi-response-tab-content {
@apply flex items-baseline truncate grow shrink max-w-max basis-[60%] mr-auto;
@apply flex items-baseline truncate grow shrink max-w-max basis-full mr-auto;
@apply text-left text-pretty relative leading-tight text-tint select-text;
}
.openapi-response-description.openapi-markdown {
@apply text-left truncate prose-sm text-sm leading-tight text-tint select-text prose-strong:font-semibold prose-strong:text-inherit;
@apply text-left prose-sm text-sm leading-tight text-tint select-text prose-strong:font-semibold prose-strong:text-inherit;
}
.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-tab-content {
@apply basis-full;
}
.openapi-disclosure-group-trigger[aria-expanded="true"]
.openapi-disclosure-group-trigger[aria-expanded="false"] {
.openapi-response-description.openapi-markdown {
@apply whitespace-normal;
@apply truncate;
@apply [&>*:not(:first-child)]:hidden *:truncate *:!p-0 *:!m-0;
}
.openapi-response-tab-content {
@apply basis-[60%]
}
}
.openapi-response-body {
@@ -465,7 +469,7 @@
}
.openapi-path-variable {
@apply p-px min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-none before:!content-none after:!content-none;
@apply p-px min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-none before:content-none! after:!content-none;
}
.openapi-path-server {
@@ -551,6 +555,10 @@
@apply text-[0.813rem] truncate text-tint font-normal;
}
.openapi-response-examples-statuscode-label {
@apply truncate;
}
.openapi-response-examples-panel,
.openapi-codesample-panel {
@apply flex-1 text-sm relative focus-visible:outline-none;
@@ -583,10 +591,6 @@ body:has(.openapi-select-popover) {
@apply shrink truncate flex items-center;
}
.openapi-select > button > span.react-aria-SelectValue span:not(.openapi-statuscode) {
@apply truncate;
}
.openapi-select > button .openapi-markdown {
@apply *:leading-none;
}
@@ -610,18 +614,18 @@ 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 hover:bg-tint-hover theme-gradient:hover:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current;
@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;
}
.openapi-select button .openapi-markdown,
.openapi-select-item .openapi-markdown {
@apply text-[0.813rem] *:truncate;
@apply text-[0.813rem] *:truncate *:!p-0 *:!m-0 [&>*:not(:first-child)]:hidden;
}
.openapi-select-item-selected,
.openapi-select-item-selected .openapi-markdown {
@apply text-primary-subtle hover:text-primary hover:bg-primary-hover;
@apply theme-muted:hover:bg-primary-active theme-gradient:hover:bg-primary-active tint:font-semibold;
@apply hover:theme-muted:bg-primary-active hover:theme-gradient:bg-primary-active tint:font-semibold;
@apply contrast-more:text-primary contrast-more:hover:text-primary-strong contrast-more:font-semibold;
}
@@ -693,7 +697,7 @@ body:has(.openapi-select-popover) {
}
.openapi-tabs-tab[aria-selected="true"] {
@apply !text-primary-subtle after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
@apply text-primary-subtle! after:absolute after:-bottom-[calc(0.375rem+1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
}
.openapi-tabs-panel {
@@ -769,7 +773,7 @@ body:has(.openapi-select-popover) {
}
.openapi-schema .openapi-schema-properties .openapi-schema {
@apply animate-fadeIn [animation-fill-mode:both];
animation: fadeIn 0.2s ease-in-out both;
}
.openapi-schemas-disclosure > .openapi-disclosure-trigger[aria-expanded="true"] > svg {
@@ -830,7 +834,8 @@ body:has(.openapi-select-popover) {
.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label span,
.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label span {
@apply block animate-fadeIn;
@apply block;
animation: fadeIn 0.2s ease-in-out both;
}
@media (hover: none) {
@@ -26,14 +26,15 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
}
const { reusableContent } = resolved;
if (!reusableContent || !reusableContent.revisionReusableContent.document) {
if (!reusableContent) {
return null;
}
const document = await getDataOrNull(
dataFetcher.getDocument({
dataFetcher.getRevisionReusableContentDocument({
spaceId: reusableContent.context.space.id,
documentId: reusableContent.revisionReusableContent.document,
revisionId: reusableContent.context.revisionId,
reusableContentId: reusableContent.revisionReusableContent.id,
})
);
@@ -24,6 +24,8 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
return '-mt-9';
case 'heading-2':
return '-mt-[calc(1.25rem+1px)]';
case 'heading-3':
return '-mt-[calc(0.50rem+1px)]';
default:
return '';
}
@@ -40,7 +42,7 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
>
{index + 1}
</div>
<div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-7 theme-muted:bg-primary-subtle" />
<div className="can-override-bg absolute top-9 bottom-2 left-3.5 w-px bg-primary-7 theme-muted:bg-primary-subtle" />
</div>
<Blocks
{...contextProps}
@@ -1,18 +1,16 @@
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import {
type ContentRef,
type DocumentTableViewCards,
SiteInsightsLinkPosition,
} from '@gitbook/api';
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { RecordColumnValue } from './RecordColumnValue';
import type { TableRecordKV, TableViewProps } from './Table';
import { RecordCardStyles } from './styles';
import { getRecordValue } from './utils';
import { getRecordCardCovers } from './utils';
export async function RecordCard(
props: TableViewProps<DocumentTableViewCards> & {
@@ -21,25 +19,21 @@ export async function RecordCard(
) {
const { view, record, context, block, isOffscreen } = props;
const coverFile = view.coverDefinition
? getRecordValue<string[]>(record[1], view.coverDefinition)?.[0]
: null;
const { dark, light } = getRecordCardCovers(record[1], view);
const targetRef = view.targetDefinition
? (record[1].values[view.targetDefinition] as ContentRef)
: null;
const [cover, target] = await Promise.all([
coverFile && context.contentContext
? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
: null,
const [lightCover, darkCover, target] = await Promise.all([
light && context.contentContext ? resolveContentRef(light, context.contentContext) : null,
dark && context.contentContext ? resolveContentRef(dark, context.contentContext) : null,
targetRef && context.contentContext
? resolveContentRef(targetRef, context.contentContext)
: null,
]);
const coverIsSquareOrPortrait =
cover?.file?.dimensions &&
cover.file?.dimensions?.width / cover.file?.dimensions?.height <= 1;
const darkCoverIsSquareOrPortrait = isSquareOrPortrait(darkCover);
const lightCoverIsSquareOrPortrait = isSquareOrPortrait(lightCover);
const body = (
<div
@@ -50,8 +44,8 @@ export async function RecordCard(
'bg-tint-base',
'w-[calc(100%+2px)]',
'h-[calc(100%+2px)]',
'inset-[-1px]',
'rounded',
'-inset-px',
'rounded-sm',
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'overflow-hidden',
@@ -63,23 +57,32 @@ export async function RecordCard(
// On mobile, check if we can display the cover responsively or not:
// - If the file has a landscape aspect ratio, we display it normally
// - If the file is square or portrait, we display it left with 40% of the card width
coverIsSquareOrPortrait
lightCoverIsSquareOrPortrait || darkCoverIsSquareOrPortrait
? [
'grid-cols-[40%,_1fr]',
'min-[432px]:grid-cols-none',
'min-[432px]:grid-rows-[auto,1fr]',
]
: 'grid-rows-[auto,1fr]'
lightCoverIsSquareOrPortrait
? 'grid-cols-[40%__1fr] min-[432px]:grid-cols-none min-[432px]:grid-rows-[auto_1fr]'
: '',
darkCoverIsSquareOrPortrait
? 'dark:grid-cols-[40%__1fr] dark:min-[432px]:grid-cols-none dark:min-[432px]:grid-rows-[auto_1fr]'
: '',
].filter(Boolean)
: 'grid-rows-[auto_1fr]'
)}
>
{cover ? (
{lightCover ? (
<Image
alt="Cover"
sources={{
light: {
src: cover.href,
size: cover.file?.dimensions,
src: lightCover.href,
size: lightCover.file?.dimensions,
},
dark: darkCover
? {
src: darkCover.href,
size: darkCover.file?.dimensions,
}
: null,
}}
sizes={[
{
@@ -92,8 +95,15 @@ export async function RecordCard(
'w-full',
'h-full',
'object-cover',
coverIsSquareOrPortrait
? ['min-[432px]:h-auto', 'min-[432px]:aspect-video']
lightCoverIsSquareOrPortrait || darkCoverIsSquareOrPortrait
? [
lightCoverIsSquareOrPortrait
? 'min-[432px]:aspect-video min-[432px]:h-auto'
: '',
darkCoverIsSquareOrPortrait
? 'dark:min-[432px]:aspect-video dark:min-[432px]:h-auto'
: '',
].filter(Boolean)
: ['h-auto', 'aspect-video']
)}
priority={isOffscreen ? 'lazy' : 'high'}
@@ -167,3 +177,16 @@ export async function RecordCard(
return <div className={tcls(RecordCardStyles)}>{body}</div>;
}
/**
* Check if a file is square or portrait.
*/
function isSquareOrPortrait(contentRef: ResolvedContentRef | null) {
const file = contentRef?.file;
if (!file || !file.dimensions) {
return false;
}
return file.dimensions?.width / file.dimensions?.height <= 1;
}
@@ -23,9 +23,9 @@ import type { TableRecordKV } from './Table';
import { type VerticalAlignment, getColumnAlignment } from './utils';
const alignmentMap: Record<'text-left' | 'text-center' | 'text-right', string> = {
'text-left': '[&_*]:text-left text-left',
'text-center': '[&_*]:text-center text-center',
'text-right': '[&_*]:text-right text-right',
'text-left': '**:text-left text-left',
'text-center': '**:text-center text-center',
'text-right': '**:text-right text-right',
};
/**
@@ -174,7 +174,6 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
<StyledLink
key={index}
href={ref.href}
target="_blank"
className="flex flex-row items-center gap-2"
insights={
ref.file
@@ -193,7 +192,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
>
{contentType === 'image' ? (
<Image
style={['max-h-[1lh]', 'h-[1lh]']}
style={['max-h-lh', 'h-lh']}
alt={ref.text}
sizes={[{ width: 24 }]}
resize={context.contentContext?.imageResizer}
@@ -314,7 +313,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
className={tcls(
'text-sm',
'whitespace-pre',
'rounded',
'rounded-sm',
'py-1',
'px-2',
'bg-primary',
@@ -329,6 +328,50 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
</Tag>
);
}
case 'image': {
const image = context.contentContext
? await resolveContentRef(value as ContentRef, context.contentContext)
: null;
if (!image) {
return null;
}
return (
<Tag className={tcls('text-base')} aria-labelledby={ariaLabelledBy}>
<StyledLink
href={image.href}
className="flex flex-row items-center gap-2"
insights={
image.file
? {
type: 'link_click',
link: {
target: value as ContentRef,
position: SiteInsightsLinkPosition.Content,
},
}
: undefined
}
>
<Image
style={['max-h-lh', 'h-lh']}
alt={image.text}
sizes={[{ width: 24 }]}
resize={context.contentContext?.imageResizer}
sources={{
light: {
src: image.href,
size: image.file?.dimensions,
},
}}
priority="lazy"
/>
{image.text}
</StyledLink>
</Tag>
);
}
default:
assertNever(definition);
}
@@ -6,7 +6,7 @@ export const RecordCardStyles = [
'shadow-1xs',
'shadow-tint-9/1',
'depth-flat:shadow-none',
'rounded',
'rounded-sm',
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'dark:shadow-transparent',
@@ -1,3 +1,5 @@
@reference "../../RootLayout/globals.css";
/* Detect whether a scrollbar exists on the table */
@keyframes detect-scroll {
from,
@@ -1,4 +1,11 @@
import type { ContentRef, DocumentTableDefinition, DocumentTableRecord } from '@gitbook/api';
import type {
ContentRef,
ContentRefFile,
ContentRefURL,
DocumentTableDefinition,
DocumentTableRecord,
DocumentTableViewCards,
} from '@gitbook/api';
import assertNever from 'assert-never';
/**
@@ -12,6 +19,57 @@ export function getRecordValue<T extends number | string | boolean | string[] |
return record.values[definitionId];
}
/**
* Get the covers for a record card.
* Returns both the light and dark covers.
* The light cover is a string or a content ref (image or files column type).
* The dark cover is a content ref (image column type).
*/
export function getRecordCardCovers(
record: DocumentTableRecord,
view: DocumentTableViewCards
): { [key in 'light' | 'dark']: ContentRefFile | ContentRefURL | null } {
return {
light: (() => {
if (!view.coverDefinition) {
return null;
}
const value = getRecordValue(record, view.coverDefinition) as
| ContentRefFile
| ContentRefURL
| string[];
if (Array.isArray(value)) {
if (value.length === 0) {
return null;
}
if (typeof value[0] === 'string') {
return { kind: 'file', file: value[0] };
}
}
return value as ContentRefFile | ContentRefURL;
})(),
dark: (() => {
if (!view.coverDefinitionDark) {
return null;
}
const value = getRecordValue(record, view.coverDefinitionDark) as
| ContentRefFile
| ContentRefURL;
if (!value) {
return null;
}
return value;
})(),
};
}
/**
* Get the text alignment for a column.
*/
@@ -144,7 +144,7 @@ export function DynamicTabs(
<div
className={tcls(
'rounded-lg',
'straight-corners:rounded-sm',
'straight-corners:rounded-xs',
'ring-1',
'ring-inset',
'ring-tint-subtle',
@@ -161,7 +161,7 @@ export function DynamicTabs(
'inline-flex',
'flex-row',
'self-stretch',
'after:flex-[1]',
'after:flex-1',
'after:bg-tint-12/1',
// if last tab is selected, apply rounded to :after element
'[&:has(button.active-tab:last-of-type):after]:rounded-bl-md'
@@ -180,7 +180,7 @@ export function DynamicTabs(
'[&:has(+_.active-tab)]:rounded-br-md',
//next from active-tab
'[.active-tab_+_&]:rounded-bl-md',
'[.active-tab+&]:rounded-bl-md',
//next from active-tab
'[.active-tab_+_:after]:rounded-br-md',
@@ -193,7 +193,7 @@ export function DynamicTabs(
'after:border-tint',
'after:top-[15%]',
'after:h-[70%]',
'after:w-[1px]',
'after:w-px',
'px-3.5',
'py-2',
@@ -212,9 +212,9 @@ export function DynamicTabs(
'active-tab',
'text-tint-strong',
'bg-transparent',
'after:[&.active-tab]:border-transparent',
'after:[:has(+_&.active-tab)]:border-transparent',
'after:[:has(&_+)]:border-transparent',
'[&.active-tab]:after:border-transparent',
'[:has(+_&.active-tab)]:after:border-transparent',
'[:has(&_+)]:after:border-transparent',
]
: null
)}
@@ -232,7 +232,7 @@ export function DynamicTabs(
'inline-block',
'text-sm',
'transition-[color]',
'font-[500]',
'font-medium',
'relative',
'max-w-full',
'truncate'
@@ -71,11 +71,7 @@ function Strikethrough(props: MarkedLeafProps<DocumentMarkStrikethrough>) {
}
function Keyboard(props: MarkedLeafProps<DocumentMarkKeyboard>) {
return (
<kbd className="rounded border px-1 font-mono shadow-[0_1px_0_0_theme(borderColor.DEFAULT)]">
{props.children}
</kbd>
);
return <kbd className="rounded-xs border border-b-2 px-1 font-mono">{props.children}</kbd>;
}
function Superscript(props: MarkedLeafProps<DocumentMarkSuperscript>) {
@@ -90,9 +86,9 @@ function Code(props: MarkedLeafProps<DocumentMarkCode>) {
return (
<code
className={tcls(
'py-[1px]',
'py-px',
'px-1.5',
'min-w-[1.625rem]',
'min-w-6.5',
'justify-center',
'items-center',
'leading-normal',
@@ -100,7 +96,7 @@ function Code(props: MarkedLeafProps<DocumentMarkCode>) {
'ring-inset',
'ring-tint',
'bg-tint',
'rounded',
'rounded-sm',
// Text size is proportional to the font-size of the parent element
'text-[.875em]',
// We ensure that the code is not making the parent bigger, especially in headings
@@ -36,16 +36,16 @@ export function Footer(props: { context: GitBookSiteContext }) {
<div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
<div
className={tcls(
'lg:!max-w-none mx-auto grid max-w-3xl site-width-wide:max-w-screen-2xl justify-between gap-12',
'mx-auto grid max-w-3xl site-width-wide:max-w-screen-2xl justify-between gap-12 lg:max-w-none!',
'grid-cols-[auto_auto]',
'lg:grid-cols-[18rem_minmax(auto,_48rem)_auto]',
'xl:grid-cols-[18rem_minmax(auto,_48rem)_14rem]',
'site-width-wide:lg:grid-cols-[18rem_minmax(auto,_80rem)_auto]',
'site-width-wide:xl:grid-cols-[18rem_minmax(auto,_80rem)_14rem]',
'page-no-toc:lg:grid-cols-[minmax(auto,_48rem)_auto]',
'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]',
'[body:has(.site-width-wide,.page-no-toc)_&]:lg:grid-cols-[minmax(auto,_90rem)_auto]',
'[body:has(.site-width-wide,.page-no-toc)_&]:xl:grid-cols-[14rem_minmax(auto,_90rem)_14rem]'
'lg:grid-cols-[18rem_minmax(auto,48rem)_auto]',
'xl:grid-cols-[18rem_minmax(auto,48rem)_14rem]',
'lg:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_auto]',
'xl:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_14rem]',
'lg:page-no-toc:grid-cols-[minmax(auto,48rem)_auto]',
'xl:page-no-toc:grid-cols-[14rem_minmax(auto,48rem)_14rem]',
'lg:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[minmax(auto,90rem)_auto]',
'xl:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[14rem_minmax(auto,90rem)_14rem]'
)}
>
{
@@ -74,8 +74,8 @@ export function Footer(props: { context: GitBookSiteContext }) {
'lg:max-h-12',
'object-contain',
'object-left',
'rounded',
'straight-corners:rounded-sm',
'rounded-sm',
'straight-corners:rounded-xs',
]}
sizes={[
{
@@ -103,7 +103,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
customization.footer.groups?.length > 0 ? (
<div
className={tcls(
'col-span-2 page-has-toc:lg:col-span-1 page-has-toc:lg:col-start-2 page-no-toc:xl:col-span-1 page-no-toc:xl:col-start-2'
'col-span-2 lg:page-has-toc:col-span-1 lg:page-has-toc:col-start-2 xl:page-no-toc:col-span-1 xl:page-no-toc:col-start-2'
)}
>
<div className="mx-auto flex max-w-3xl site-width-wide:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6">
@@ -85,7 +85,7 @@ export function Header(props: {
'text-tint-strong',
'theme-bold:text-header-link',
'hover:bg-tint-hover',
'theme-bold:hover:bg-header-link/3',
'hover:theme-bold:bg-header-link/3',
'page-no-toc:hidden'
)}
/>
@@ -110,7 +110,6 @@ export function Header(props: {
'lg:max-w-lg',
'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem)
'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]',
'md:mr-auto',
'order-last',
'md:order-[unset]',
@@ -119,7 +119,7 @@ function HeaderItemButton(
className={tcls(
{
'button-primary':
'theme-bold:bg-header-link theme-bold:text-header-background theme-bold:shadow-none theme-bold:hover:bg-header-link theme-bold:hover:text-header-background theme-bold:hover:shadow-none',
'theme-bold:bg-header-link theme-bold:text-header-background theme-bold:shadow-none hover:theme-bold:bg-header-link hover:theme-bold:text-header-background hover:theme-bold:shadow-none',
'button-secondary': '',
}[linkStyle]
)}
@@ -157,7 +157,7 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese
'links-accent:py-0.5', // Prevent underline from being cut off at the bottom
'theme-bold:text-header-link',
'theme-bold:hover:!text-header-link/7'
'hover:theme-bold:text-header-link/7!'
);
}
@@ -53,11 +53,12 @@ export async function HeaderLogo(props: HeaderLogoProps) {
'min-w-0',
'max-w-40',
'lg:max-w-64',
'site-header-none:page-no-toc:lg:max-w-56',
'lg:site-header-none:page-no-toc:max-w-56',
'max-h-10',
'lg:max-h-12',
'h-full',
'w-auto'
'w-full',
'object-contain',
'object-left'
)}
/>
) : (
@@ -47,7 +47,7 @@ export function HeaderMobileMenu(props: Partial<React.ButtonHTMLAttributes<HTMLB
aria-label={tString(language, 'table_of_contents_button_label')}
onClick={toggleNavigation}
className={tcls(
'flex flex-row items-center rounded straight-corners:rounded-sm px-2 py-1',
'flex flex-row items-center rounded-sm straight-corners:rounded-xs px-2 py-1',
props.className
)}
>
@@ -1,23 +1,95 @@
'use client';
import * as React from 'react';
import * as zustand from 'zustand';
import type {
GitBookGlobal,
GitBookIntegrationEvent,
GitBookIntegrationEventCallback,
GitBookIntegrationTool,
} from '@gitbook/browser-types';
import type { Assistant } from '../AI';
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
export const integrationsAssistantTools = zustand.createStore<{
/**
* Tools exposed to the assistant by integrations
*/
tools: GitBookIntegrationTool[];
}>(() => {
return {
tools: [],
};
});
export const integrationAssistants = zustand.createStore<Array<Assistant>>(() => []);
if (typeof window !== 'undefined') {
window.GitBook = {
events: new Map(),
const gitbookGlobal: GitBookGlobal = {
addEventListener: (event, callback) => {
const handlers = window.GitBook?.events.get(event) ?? [];
const handlers = events.get(event) ?? [];
handlers.push(callback);
window.GitBook?.events.set(event, handlers);
events.set(event, handlers);
},
removeEventListener: (event, callback) => {
const handlers = window.GitBook?.events.get(event) ?? [];
const handlers = events.get(event) ?? [];
const index = handlers.indexOf(callback);
if (index !== -1) {
handlers.splice(index, 1);
}
},
registerTool: (tool) => {
integrationsAssistantTools.setState((state) => ({
tools: [...state.tools, tool],
}));
},
registerAssistant: (assistant) => {
const id = window.crypto.randomUUID();
// Add assistant with ready=false until the integration signals readiness
integrationAssistants.setState(
(state) => [
...state,
{ ...assistant, id, ui: assistant.ui ?? true, mode: 'overlay', ready: false },
],
true
);
const ready = () => {
integrationAssistants.setState(
(state) =>
state.map((assistant) =>
assistant.id === id ? { ...assistant, ready: true } : assistant
),
true
);
};
const dispose = () => {
integrationAssistants.setState(
(state) => state.filter((assistant) => assistant.id !== id),
true
);
};
const registration = (() => dispose()) as ((this: void) => void) & {
ready: () => void;
dispose: () => void;
};
registration.ready = ready;
registration.dispose = dispose;
return registration;
},
};
window.GitBook = gitbookGlobal;
}
/**
* Get the current state of the assistants.
*/
export function useIntegrationAssistants(): Array<Assistant> {
return zustand.useStore(integrationAssistants);
}
/**
@@ -33,7 +105,6 @@ export function LoadIntegrations() {
/**
* Client function to dispatch a GitBook event.
*/
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: any[]) {
const handlers = window.GitBook?.events.get(type) || [];
handlers.forEach((handler) => handler(...args));
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: unknown[]) {
events.get(type)?.forEach((handler) => handler(...args));
}
@@ -107,7 +107,7 @@ export async function PDFPage(props: {
'p-4',
'rounded-full',
'bg-white',
'shadow-sm',
'shadow-xs',
'hover:shadow-md',
'border-slate-300',
'border'
@@ -132,7 +132,7 @@ export async function PDFPage(props: {
'p-4',
'rounded-full',
'bg-white',
'shadow-sm',
'shadow-xs',
'hover:shadow-md',
'border-slate-300',
'border'
@@ -280,7 +280,7 @@ function PrintPage(
'print:p-0',
'shadow-xl',
'print:shadow-none',
'rounded-sm',
'rounded-xs',
'bg-white',
'min-h-[29.7cm]',
'print:min-h-0',
@@ -99,7 +99,7 @@ export function PageControlButtons(props: {
'bg-yellow-100',
'border-yellow-400',
'text-yellow-800',
'shadow-sm',
'shadow-xs',
'border',
'rounded-md',
'p-4',
@@ -140,7 +140,7 @@ export function PageControlButtons(props: {
'py-2',
'bg-slate-100',
'rounded-full',
'shadow-sm',
'shadow-xs',
'border-slate-300',
'border'
)}
@@ -32,33 +32,28 @@ export function PageAside(props: {
withPageFeedback: boolean;
}) {
const { page, document, withPageFeedback, context } = props;
const { customization, site, space } = context;
const language = getSpaceLanguage(customization);
const { customization, site } = context;
const pdfHref = context.linker.toPathInSpace(
`~gitbook/pdf?${getPDFURLSearchParams({
page: page.id,
only: true,
limit: 100,
}).toString()}`
);
return (
<aside
className={tcls(
'group/aside',
'hidden',
'pt-8',
'xl:flex',
'chat-open:xl:max-3xl:hidden',
'chat-open:xl:max-3xl:opacity-0',
'xl:max-3xl:chat-open:hidden',
'xl:max-3xl:chat-open:opacity-0',
'max-w-56',
'chat-open:xl:max-3xl:*:w-56',
'chat-open:xl:max-3xl:max-w-0',
'chat-open:xl:max-3xl:ml-0',
'motion-safe:xl:transition-all motion-safe:xl:duration-300',
'motion-safe:[transition-behavior:allow-discrete]',
// Animate the width of the aside when the chat is open
'xl:max-3xl:*:chat-open:w-56',
'xl:max-3xl:chat-open:max-w-0',
'xl:max-3xl:chat-open:ml-0',
'motion-safe:xl:transition-[width,max-width,margin,opacity,display] motion-safe:xl:duration-300',
'motion-safe:transition-discrete',
'flex-col',
'basis-56',
'xl:ml-12',
'grow-0',
@@ -68,208 +63,241 @@ export function PageAside(props: {
'text-tint',
'contrast-more:text-tint-strong',
'sticky',
// Without header
'lg:top-0',
'lg:max-h-screen',
// With header
'site-header:lg:top-16',
'site-header:lg:max-h-[calc(100vh_-_4rem)]',
'lg:site-header:top-16',
'lg:site-header:max-h-[calc(100vh-4rem)]',
// With header & sections
'site-header-sections:lg:top-[6.75rem]',
'site-header-sections:lg:max-h-[calc(100vh_-_6.75rem)]',
'lg:site-header-sections:top-27',
'lg:site-header-sections:max-h-[calc(100vh-6.75rem)]',
// Client-side dynamic positioning (CSS vars applied by script)
'lg:[html[style*="--toc-top-offset"]_&]:top-(--toc-top-offset)!',
'lg:[html[style*="--toc-height"]_&]:max-h-(--toc-height)!',
// When in api page mode, we display it as an overlay on non-large resolutions
'page-api-block:xl:max-2xl:z-10',
'page-api-block:xl:max-2xl:fixed',
'page-api-block:xl:max-2xl:right-8',
'page-api-block:xl:max-2xl:w-56',
'page-api-block:xl:max-2xl:bg-tint/9',
'page-api-block:xl:max-2xl:contrast-more:bg-tint',
'page-api-block:xl:max-2xl:backdrop-blur-lg',
'page-api-block:xl:max-2xl:border',
'page-api-block:xl:max-2xl:border-tint',
'page-api-block:xl:max-2xl:hover:shadow-lg',
'page-api-block:xl:max-2xl:hover:shadow-tint-12/1',
'page-api-block:xl:max-2xl:dark:hover:shadow-tint-1/1',
'page-api-block:xl:max-2xl:rounded-md',
'page-api-block:xl:max-2xl:h-auto',
'page-api-block:xl:max-2xl:my-9',
'page-api-block:px-2',
'page-api-block:py-1.5'
'xl:max-2xl:page-api-block:z-10',
'xl:max-2xl:page-api-block:fixed',
'xl:max-2xl:page-api-block:right-8',
'xl:max-2xl:page-api-block:w-60',
'xl:max-2xl:page-api-block:max-w-60',
'xl:max-2xl:page-api-block:pb-8',
'xl:max-2xl:page-api-block:pt-10',
'xl:max-2xl:[body:has(.openapi-block):has(.page-has-ancestors)_&]:pt-6.5'
)}
>
{page.layout.outline ? (
<>
<div
className={tcls(
'hidden',
'page-api-block:xl:max-2xl:flex',
'text-xs',
'tracking-wide',
'font-semibold',
'uppercase',
'flex-row',
'items-center',
'gap-2'
)}
>
<Icon icon="block-quote" className={tcls('size-3')} />
{t(language, 'on_this_page')}
<Icon
icon="chevron-down"
className={tcls(
'size-3',
'opacity-6',
'ml-auto',
'page-api-block:xl:max-2xl:group-hover/aside:hidden'
)}
/>
</div>
<div
className={tcls(
'overflow-y-auto',
'overflow-x-visible',
'flex',
'flex-col',
'shrink',
'pb-12',
'sticky',
'lg:top:0',
'site-header:lg:top-16',
'site-header-sections:lg:top-[6.75rem]',
'gap-6',
'pt-8',
'page-api-block:xl:max-2xl:py-0',
// Hide it for api page, until hovered
'page-api-block:xl:max-2xl:hidden',
'page-api-block:xl:max-2xl:group-hover/aside:flex'
)}
>
<div
className={tcls(
'flex flex-col',
'overflow-hidden',
'w-full',
'xl:max-2xl:rounded-corners:page-api-block:rounded-md',
'xl:max-2xl:circular-corners:page-api-block:rounded-xl',
'xl:max-2xl:page-api-block:border',
'xl:max-2xl:page-api-block:border-tint',
'xl:max-2xl:page-api-block:bg-tint/9',
'xl:max-2xl:page-api-block:backdrop-blur-lg',
'xl:max-2xl:contrast-more:page-api-block:bg-tint',
'xl:max-2xl:page-api-block:hover:shadow-lg',
'xl:max-2xl:page-api-block:hover:shadow-tint-12/1',
'xl:max-2xl:dark:page-api-block:hover:shadow-tint-1/1',
'xl:max-2xl:page-api-block:not-hover:*:hidden'
)}
>
<PageAsideHeader context={context} />
{page.layout.outline ? (
<div className="overflow-y-auto border-tint xl:max-2xl:page-api-block:border-t">
{document ? (
<React.Suspense fallback={null}>
<PageAsideSections document={document} context={context} />
</React.Suspense>
) : null}
<div
className={tcls(
'flex',
'flex-col',
'gap-3',
'sidebar-list-default:px-3',
'border-t',
'first:border-none',
'border-tint-subtle',
'py-4',
'first:pt-0',
'page-api-block:xl:max-2xl:px-3',
'empty:hidden'
)}
>
{withPageFeedback ? (
<React.Suspense fallback={null}>
<PageFeedbackForm pageId={page.id} className={tcls('mt-2')} />
</React.Suspense>
) : null}
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
<div>
<a
href={urlJoin(space.gitSync.url, page.git.path)}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon={
space.gitSync.installationProvider === 'gitlab'
? 'gitlab'
: 'github'
}
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'edit_on_git', getGitSyncName(space))}
</a>
</div>
) : null}
{customization.pdf.enabled ? (
<div>
<a
href={pdfHref}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon="file-pdf"
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'pdf_download')}
</a>
</div>
) : null}
</div>
</div>
</>
) : null}
<div
className={tcls(
'sticky bottom-0 z-10 mt-auto flex flex-col bg-tint-base theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle pb-4 page-api-block:xl:max-2xl:hidden page-api-block:xl:max-2xl:pb-0 page-api-block:xl:max-2xl:group-hover/aside:flex [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'page-api-block:xl:max-2xl:bg-transparent'
)}
>
{/* Mode Switcher */}
{customization.themes.toggeable ? (
<div className="mt-4 flex items-center justify-end">
<React.Suspense fallback={null}>
<ThemeToggler />
</React.Suspense>
<PageAsideActions
page={page}
withPageFeedback={withPageFeedback}
context={context}
/>
</div>
) : null}
<Ad
zoneId={
site?.ads && site.ads.status === SiteAdsStatus.Live ? site.ads.zoneId : null
}
placement={SiteInsightsAdPlacement.Aside}
spaceId={space.id}
siteAdsStatus={site?.ads?.status ? site.ads.status : undefined}
ignore={process.env.NODE_ENV !== 'production'}
style={site?.ads ? 'mt-4' : undefined}
/>
{customization.themes.toggeable || site.ads ? (
<PageAsideFooter context={context} />
) : null}
</div>
</aside>
);
}
function PageAsideHeader(props: { context: GitBookSiteContext }) {
const { context } = props;
const language = getSpaceLanguage(context.customization);
return (
<div
className={tcls(
'hidden',
'xl:max-2xl:page-api-block:flex!',
'text-xs',
'tracking-wide',
'font-semibold',
'uppercase',
'px-2',
'py-1.5',
'flex-row',
'items-center',
'gap-2'
)}
>
<Icon icon="block-quote" className={tcls('size-3')} />
{t(language, 'on_this_page')}
<Icon icon="chevron-down" className={tcls('size-3', 'opacity-6', 'ml-auto')} />
</div>
);
}
async function PageAsideSections(props: { document: JSONDocument; context: GitBookSiteContext }) {
const { document, context } = props;
const sections = await getDocumentSections(context, document);
return sections.length > 1 ? <ScrollSectionsList sections={sections} /> : null;
return sections.length > 1 ? (
<div className="-mt-8 pt-8 pb-5 empty:hidden xl:max-2xl:page-api-block:mt-0 xl:max-2xl:page-api-block:p-2">
<ScrollSectionsList sections={sections} />
</div>
) : null;
}
function PageAsideActions(props: {
withPageFeedback: boolean;
context: GitBookSiteContext;
page: RevisionPageDocument;
}) {
const { page, withPageFeedback, context } = props;
const { customization, space } = context;
const language = getSpaceLanguage(customization);
const pdfHref = context.linker.toPathInSpace(
`~gitbook/pdf?${getPDFURLSearchParams({
page: page.id,
only: true,
limit: 100,
}).toString()}`
);
return (
<div
className={tcls(
'flex',
'flex-col',
'gap-3',
'sidebar-list-default:px-3',
'border-t',
'first:border-none',
'border-tint-subtle',
'py-5',
'first:pt-0',
'xl:max-2xl:page-api-block:p-5',
'empty:hidden'
)}
>
{withPageFeedback ? (
<React.Suspense fallback={null}>
<PageFeedbackForm pageId={page.id} />
</React.Suspense>
) : null}
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
<div>
<a
href={urlJoin(space.gitSync.url, page.git.path)}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon
icon={
space.gitSync.installationProvider === 'gitlab'
? 'gitlab'
: 'github'
}
className={tcls('size-4', 'mr-1.5')}
/>
{t(language, 'edit_on_git', getGitSyncName(space))}
</a>
</div>
) : null}
{customization.pdf.enabled ? (
<div>
<a
href={pdfHref}
className={tcls(
'flex',
'flex-row',
'items-center',
'text-sm',
'hover:text-tint-strong',
'links-accent:hover:underline',
'links-accent:hover:underline-offset-4',
'links-accent:hover:decoration-[3px]',
'links-accent:hover:decoration-primary-subtle',
'py-2'
)}
>
<Icon icon="file-pdf" className={tcls('size-4', 'mr-1.5')} />
{t(language, 'pdf_download')}
</a>
</div>
) : null}
</div>
);
}
async function PageAsideFooter(props: { context: GitBookSiteContext }) {
const { context } = props;
const { customization, site, space } = context;
return (
<div
className={tcls(
'sticky bottom-0 z-10 mt-auto flex flex-col',
'bg-tint-base theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
'border-tint-subtle xl:max-2xl:page-api-block:border-t xl:max-2xl:page-api-block:p-2',
'py-4'
)}
>
{/* Mode Switcher */}
{customization.themes.toggeable ? (
<div className="flex items-center justify-end">
<React.Suspense fallback={null}>
<ThemeToggler />
</React.Suspense>
</div>
) : null}
<Ad
zoneId={
site?.ads && site.ads.status === SiteAdsStatus.Live ? site.ads.zoneId : null
}
placement={SiteInsightsAdPlacement.Aside}
spaceId={space.id}
siteAdsStatus={site?.ads?.status ? site.ads.status : undefined}
ignore={process.env.NODE_ENV !== 'production'}
style={site?.ads ? 'mt-4' : undefined}
/>
</div>
);
}
function getGitSyncName(space: Space): string {
@@ -40,7 +40,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
});
return (
<ul className={tcls('sidebar-list-line:border-l', 'border-tint-subtle')}>
<ul className="flex flex-col border-tint-subtle sidebar-list-line:border-l">
{sections.map((section) => (
<motion.li
key={section.id}
@@ -50,6 +50,8 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
'relative',
'h-fit',
'mt-2',
'first:mt-0',
'mb-0.5',
section.depth > 1 && ['ml-3', 'my-0', 'sidebar-list-line:ml-0']
)}
>
@@ -128,7 +130,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
>
{section.tag ? (
<span
className={`-mt-0.5 openapi-method !text-xs openapi-method-${section.tag.toLowerCase()}`}
className={`-mt-0.5 openapi-method text-xs! openapi-method-${section.tag.toLowerCase()}`}
>
{section.tag}
</span>
@@ -77,7 +77,7 @@ export async function PageBodyBlankslate(props: {
'gap-4',
'grid-cols-1',
'sm:grid-cols-2',
'page-width-wide:md:grid-cols-3'
'md:page-width-wide:grid-cols-3'
)}
>
{pageElements}

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