mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 07:35:16 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bddd458446 | |||
| 174e7f43a2 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix missing geo data in site insights because of incorrect proxying of events.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add stable scroll gutter to search container
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix robots.txt preventing favicon from being indexed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Make search accessible
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve default site icon
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Use sitespace title instead of space title in search
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Support nested site section groups
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Show a toolbar for authenticated users with access to the current site.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add support for inline expressions rendering with visitor data on GBO side
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Update to column width sizing
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Expose "Best match" when site has translations
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Improve screen reader accessibility for hints
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-openapi": minor
|
||||
---
|
||||
|
||||
Fix OpenAPISecurities and code sample not using operation security requirements
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix crash for card cover defined without objectFit
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add scrollcontainer component
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Refactor section tabs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix OpenAPI response dropdown & vertical overflow
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
---
|
||||
|
||||
Fix OpenAPI response showing as JSON instead of YAML when it should
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Scope search across sections and variants
|
||||
@@ -37,7 +37,6 @@ jobs:
|
||||
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
|
||||
+8
-10
@@ -3,9 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"turbo": "^2.5.8",
|
||||
"vercel": "^39.4.2"
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"turbo": "^2.5.0",
|
||||
"vercel": "^39.3.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.15",
|
||||
"overrides": {
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run dev --concurrency 20",
|
||||
"dev": "turbo run dev",
|
||||
"build": "turbo run build",
|
||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||
"typecheck": "turbo run typecheck",
|
||||
@@ -26,18 +26,16 @@
|
||||
"e2e": "turbo run e2e",
|
||||
"e2e-customers": "turbo run e2e-customers",
|
||||
"changeset": "changeset",
|
||||
"changeset-version": "changeset version && bun run format && bun update",
|
||||
"release": "turbo run release && bun run publish-all-packages",
|
||||
"publish-all-packages": "for dir in packages/*; do (cd \"$dir\" && bun publish || true); done && changeset tag",
|
||||
"changeset-version": "changeset version && bun run format",
|
||||
"release": "turbo run release && changeset publish",
|
||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||
"clean": "turbo run clean"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "0.143.2",
|
||||
"bidc": "^0.0.2",
|
||||
"tsdown": "^0.15.6"
|
||||
"@gitbook/api": "^0.142.0",
|
||||
"bidc": "^0.0.2"
|
||||
}
|
||||
},
|
||||
"patchedDependencies": {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# @gitbook/browser-types
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
- Updated dependencies [295f03d]
|
||||
- @gitbook/icons@0.3.2
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:"
|
||||
@@ -20,9 +20,5 @@
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @gitbook/cache-tags
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"assert-never": "^1.2.1"
|
||||
@@ -21,9 +21,5 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
@@ -17,9 +17,5 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
# @gitbook/embed
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
- Updated dependencies [295f03d]
|
||||
- @gitbook/icons@0.3.2
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7508674: Fix bundling of packages
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./dist/react/index.d.ts",
|
||||
"default": "./dist/react/index.js"
|
||||
"default": "./dist/index.js",
|
||||
"standalone": "./dist/standalone/index.js",
|
||||
"react": "./dist/react/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:",
|
||||
@@ -22,21 +20,12 @@
|
||||
"react": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "^5.5.3",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run build-lib && bun run build-standalone",
|
||||
"build-lib": "tsdown",
|
||||
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"clean": "rm -rf ./dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run build -- --watch ./src"
|
||||
"build": "tsc && bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
"files": ["dist", "README.md", "CHANGELOG.md", "standalone"]
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ['src/index.ts', 'src/react/index.ts'],
|
||||
dts: true,
|
||||
format: ['esm'],
|
||||
},
|
||||
]);
|
||||
@@ -1,19 +1,13 @@
|
||||
# @gitbook/emoji-codepoints
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 57adb3e: Second release to fix publishing with changeset
|
||||
- 57adb3e: Second release to fix publishing with changeset
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5f8a8fe: Initial release
|
||||
- 5f8a8fe: Initial release
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/emoji-codepoints",
|
||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"exports": "./dist/index.ts",
|
||||
"dependencies": {},
|
||||
@@ -11,9 +11,5 @@
|
||||
"scripts": {
|
||||
"generate": "bun ./build.ts",
|
||||
"clean": "rm -rf ./dist"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
# @gitbook/expr
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a629900: Add dev script for @gitbook/expr
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/expr",
|
||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -21,22 +21,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.1.20",
|
||||
"tsdown": "catalog:",
|
||||
"tsdown": "^0.15.0",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@babel/types": "^7.26.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/escodegen": "^0.0.10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"build": "tsdown --project tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"clean": "rm -rf ./dist",
|
||||
"dev": "bun run build -- --watch ./src"
|
||||
"clean": "rm -rf ./dist"
|
||||
},
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": ["./tsconfig.json"],
|
||||
"exclude": ["**/*.test.ts"],
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
# @gitbook/fonts
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:"
|
||||
},
|
||||
@@ -27,9 +27,5 @@
|
||||
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,5 @@
|
||||
# gitbook
|
||||
|
||||
## 0.19.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
- Updated dependencies [bdde392]
|
||||
- Updated dependencies [295f03d]
|
||||
- @gitbook/react-contentkit@0.7.6
|
||||
- @gitbook/browser-types@0.1.1
|
||||
- @gitbook/cache-tags@0.3.2
|
||||
- @gitbook/colors@0.4.1
|
||||
- @gitbook/emoji-codepoints@0.2.1
|
||||
- @gitbook/expr@1.2.2
|
||||
- @gitbook/fonts@0.1.1
|
||||
- @gitbook/icons@0.3.2
|
||||
- @gitbook/openapi-parser@3.0.3
|
||||
- @gitbook/react-math@0.6.2
|
||||
- @gitbook/react-openapi@1.5.1
|
||||
|
||||
## 0.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ed7d47d: Show a toolbar for authenticated users with access to the current site.
|
||||
- 50c1be3: Move inline expression evaluation from API to GBO
|
||||
- 56e46ce: Add Italian translation
|
||||
- b932e4e: Scope search across sections and variants
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 162cfe4: Hide sections if only one is present
|
||||
- 1456251: Unify section display condition
|
||||
- 9344431: Fix missing geo data in site insights because of incorrect proxying of events.
|
||||
- eea8f1e: Enhance OpenAPI security scopes handling
|
||||
- 2b8a2d2: Add stable scroll gutter to search container
|
||||
- b498521: Fix robots.txt preventing favicon from being indexed.
|
||||
- ceb32b1: Redo search scope UI
|
||||
- 8e99871: Highlight discriminator properties in oneOf, allOf, anyOf objects
|
||||
- 2fc2127: Make search accessible
|
||||
- f3e4041: Adapt OpenAPI code samples to prefill API key using visitor data
|
||||
- 6815dd1: Fix markdownPageURL for PageActions
|
||||
- c06b3dd: Improve default site icon
|
||||
- cba583a: Use sitespace title instead of space title in search
|
||||
- e434442: Support nested site section groups
|
||||
- 469b332: Add support for inline expressions rendering with visitor data on GBO side
|
||||
- 0ef5dc8: Update to column width sizing
|
||||
- 1e4e54e: Fix rounded outline items
|
||||
- 379d486: Expose "Best match" when site has translations
|
||||
- c21693f: Improve screen reader accessibility for hints
|
||||
- badf3a9: Add alt text to inline image
|
||||
- c23d4ef: Fix crash for card cover defined without objectFit
|
||||
- d74f8a7: Improve wide page width layout
|
||||
- b8388e5: Add scrollcontainer component
|
||||
- b85eccd: Refactor section tabs
|
||||
- 1e53f46: Fix button content truncate
|
||||
- 7a504b7: Fix OpenAPI response dropdown & vertical overflow
|
||||
- a4c3399: Fix OpenAPI alternatives not showing
|
||||
- Updated dependencies [a629900]
|
||||
- Updated dependencies [eea8f1e]
|
||||
- Updated dependencies [8e99871]
|
||||
- Updated dependencies [f3e4041]
|
||||
- Updated dependencies [319a1e5]
|
||||
- Updated dependencies [529f940]
|
||||
- Updated dependencies [754cc11]
|
||||
- Updated dependencies [d7948e3]
|
||||
- Updated dependencies [a4c3399]
|
||||
- @gitbook/expr@1.2.1
|
||||
- @gitbook/react-openapi@1.5.0
|
||||
|
||||
## 0.18.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.19.1",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
@@ -16,7 +16,7 @@
|
||||
"@gitbook/react-math": "workspace:*",
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"@opennextjs/cloudflare": "^1.9.2",
|
||||
"@opennextjs/cloudflare": "^1.8.3",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
@@ -51,7 +51,7 @@
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.12",
|
||||
"next": "15.4.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
"object-identity": "^0.1.2",
|
||||
@@ -121,9 +121,5 @@
|
||||
},
|
||||
"browserslist": [
|
||||
">0.3%, chrome >= 64, edge >= 79, firefox >= 67, opera >= 51, safari >= 12 and not dead"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
import { useLanguage } from '@/intl/client';
|
||||
import { t } from '@/intl/translate';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { Assistant } from '../AI';
|
||||
import { Button } from '../primitives';
|
||||
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
|
||||
@@ -23,7 +24,7 @@ export function AIChatButton(props: {
|
||||
iconOnly={!showLabel}
|
||||
size="medium"
|
||||
variant="header"
|
||||
className="h-9 px-2.5 max-md:[&_.button-content]:hidden"
|
||||
className={tcls('h-9 px-2.5')}
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
{t(language, 'ai_chat_ask', assistant.label)}
|
||||
@@ -37,7 +38,7 @@ export function AIChatButton(props: {
|
||||
}
|
||||
onClick={() => assistant.open()}
|
||||
>
|
||||
{showLabel ? t(language, 'ask') : null}
|
||||
{showLabel ? <span className="max-md:hidden">{t(language, 'ask')}</span> : null}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { MotionConfig } from 'motion/react';
|
||||
import React from 'react';
|
||||
import { useCheckForContentUpdate } from '../AutoRefreshContent';
|
||||
import { useVisitorSession } from '../Insights';
|
||||
import { useCurrentPagePath } from '../hooks';
|
||||
import { DateRelative } from '../primitives';
|
||||
import { DateRelative, Tooltip } from '../primitives';
|
||||
import { HideToolbarButton } from './HideToolbarButton';
|
||||
import { IframeWrapper } from './IframeWrapper';
|
||||
import { RefreshContentButton } from './RefreshContentButton';
|
||||
@@ -18,98 +19,113 @@ import {
|
||||
ToolbarSubtitle,
|
||||
ToolbarTitle,
|
||||
} from './Toolbar';
|
||||
import {
|
||||
type ToolbarControlsContextValue,
|
||||
ToolbarControlsProvider,
|
||||
} from './ToolbarControlsContext';
|
||||
import { ToolbarControlsProvider } from './ToolbarControlsContext';
|
||||
import type { AdminToolbarClientProps, AdminToolbarContext } from './types';
|
||||
import { useToolbarVisibility } from './utils';
|
||||
|
||||
export function AdminToolbarClient(props: AdminToolbarClientProps) {
|
||||
const { context, onPersistentClose, onSessionClose, onToggleMinify } = props;
|
||||
const {
|
||||
minified,
|
||||
setMinified,
|
||||
shouldAutoExpand,
|
||||
hidden,
|
||||
minimize,
|
||||
closeSession,
|
||||
closePersistent,
|
||||
} = useToolbarVisibility({
|
||||
onPersistentClose,
|
||||
onSessionClose,
|
||||
onToggleMinify,
|
||||
});
|
||||
|
||||
const [minified, setMinified] = React.useState(true);
|
||||
const visitorSession = useVisitorSession();
|
||||
const [sessionClosed, setSessionClosed] = React.useState(false);
|
||||
const [shouldHide, setShouldHide] = React.useState(false);
|
||||
|
||||
const toolbarControls: ToolbarControlsContextValue = {
|
||||
minimize,
|
||||
closeSession,
|
||||
closePersistent,
|
||||
shouldAutoExpand,
|
||||
};
|
||||
React.useEffect(() => {
|
||||
const STORAGE_KEY = 'gitbook_toolbar_closed';
|
||||
|
||||
if (hidden) {
|
||||
try {
|
||||
const hidden = !!localStorage.getItem(STORAGE_KEY);
|
||||
setShouldHide(hidden);
|
||||
} catch {
|
||||
setShouldHide(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSessionClose = React.useCallback(() => {
|
||||
setSessionClosed(true);
|
||||
onSessionClose?.();
|
||||
}, [onSessionClose]);
|
||||
|
||||
const handlePersistentClose = React.useCallback(() => {
|
||||
try {
|
||||
localStorage.setItem('gitbook_toolbar_closed', '1');
|
||||
} catch {
|
||||
console.error('Failed to close toolbar using local storage');
|
||||
}
|
||||
setSessionClosed(true);
|
||||
onPersistentClose?.();
|
||||
}, [onPersistentClose]);
|
||||
|
||||
const handleMinifiedChange = React.useCallback(
|
||||
(value: boolean) => {
|
||||
setMinified(value);
|
||||
onToggleMinify?.();
|
||||
},
|
||||
[onToggleMinify]
|
||||
);
|
||||
|
||||
const toolbarControls = React.useMemo(
|
||||
() => ({
|
||||
minimize: () => handleMinifiedChange(true),
|
||||
closeSession: handleSessionClose,
|
||||
closePersistent: handlePersistentClose,
|
||||
}),
|
||||
[handleMinifiedChange, handleSessionClose, handlePersistentClose]
|
||||
);
|
||||
|
||||
if (shouldHide || sessionClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there is a change request, show the change request toolbar
|
||||
if (context.changeRequest) {
|
||||
return (
|
||||
<ToolbarControlsWrapper value={toolbarControls}>
|
||||
<ChangeRequestToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={setMinified}
|
||||
/>
|
||||
</ToolbarControlsWrapper>
|
||||
<ToolbarControlsProvider value={toolbarControls}>
|
||||
<IframeWrapper>
|
||||
<MotionConfig reducedMotion="user">
|
||||
<ChangeRequestToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={handleMinifiedChange}
|
||||
/>
|
||||
</MotionConfig>
|
||||
</IframeWrapper>
|
||||
</ToolbarControlsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// If the revision is not the current revision, the user is looking at a previous version of the site, so show the revision toolbar
|
||||
if (context.revisionId !== context.space.revision) {
|
||||
return (
|
||||
<ToolbarControlsWrapper value={toolbarControls}>
|
||||
<RevisionToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={setMinified}
|
||||
/>
|
||||
</ToolbarControlsWrapper>
|
||||
<ToolbarControlsProvider value={toolbarControls}>
|
||||
<IframeWrapper>
|
||||
<MotionConfig reducedMotion="user">
|
||||
<RevisionToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={handleMinifiedChange}
|
||||
/>
|
||||
</MotionConfig>
|
||||
</IframeWrapper>
|
||||
</ToolbarControlsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// If the user is authenticated and part of the organization owning this site, show the authenticated user toolbar
|
||||
if (visitorSession?.organizationId === context.organizationId) {
|
||||
return (
|
||||
<ToolbarControlsWrapper value={toolbarControls}>
|
||||
<AuthenticatedUserToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={setMinified}
|
||||
/>
|
||||
</ToolbarControlsWrapper>
|
||||
<ToolbarControlsProvider value={toolbarControls}>
|
||||
<IframeWrapper>
|
||||
<MotionConfig reducedMotion="user">
|
||||
<AuthenticatedUserToolbar
|
||||
context={context}
|
||||
minified={minified}
|
||||
onMinifiedChange={handleMinifiedChange}
|
||||
/>
|
||||
</MotionConfig>
|
||||
</IframeWrapper>
|
||||
</ToolbarControlsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable wrapper that provides tooling and containers that are used by all types of toolbar views.
|
||||
*/
|
||||
export function ToolbarControlsWrapper(
|
||||
props: React.PropsWithChildren<{ value: ToolbarControlsContextValue | null }>
|
||||
) {
|
||||
const { children, value } = props;
|
||||
return (
|
||||
<ToolbarControlsProvider value={value}>
|
||||
<IframeWrapper>
|
||||
<MotionConfig reducedMotion="user">{children}</MotionConfig>
|
||||
</IframeWrapper>
|
||||
</ToolbarControlsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToolbarViewProps {
|
||||
@@ -132,7 +148,7 @@ function ChangeRequestToolbar(props: ToolbarViewProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange} label="Site preview">
|
||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange}>
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle
|
||||
prefix={`Change #${changeRequest.number}:`}
|
||||
@@ -207,17 +223,19 @@ function RevisionToolbar(props: ToolbarViewProps) {
|
||||
const gitProvider = isGitHub ? 'GitHub' : 'GitLab';
|
||||
|
||||
return (
|
||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange} label="Site preview">
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle prefix="Site version" suffix={context.site.title} />
|
||||
<ToolbarSubtitle
|
||||
subtitle={
|
||||
<>
|
||||
Created <DateRelative value={revision.createdAt} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ToolbarBody>
|
||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange}>
|
||||
<Tooltip label="Site preview">
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle prefix="Site version" suffix={context.site.title} />
|
||||
<ToolbarSubtitle
|
||||
subtitle={
|
||||
<>
|
||||
Created <DateRelative value={revision.createdAt} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ToolbarBody>
|
||||
</Tooltip>
|
||||
<ToolbarSeparator />
|
||||
<ToolbarActions>
|
||||
{/* Open commit in Git client */}
|
||||
@@ -280,21 +298,19 @@ function AuthenticatedUserToolbar(props: ToolbarViewProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
minified={minified}
|
||||
onMinifiedChange={onMinifiedChange}
|
||||
label="Only visible to your GitBook organization"
|
||||
>
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle suffix={context.site.title} />
|
||||
<ToolbarSubtitle
|
||||
subtitle={
|
||||
<>
|
||||
Updated <DateRelative value={revision.createdAt} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ToolbarBody>
|
||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange}>
|
||||
<Tooltip label="Site preview">
|
||||
<ToolbarBody>
|
||||
<ToolbarTitle suffix={context.site.title} />
|
||||
<ToolbarSubtitle
|
||||
subtitle={
|
||||
<>
|
||||
Updated <DateRelative value={revision.createdAt} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ToolbarBody>
|
||||
</Tooltip>
|
||||
<ToolbarSeparator />
|
||||
<ToolbarActions>
|
||||
{/* Refresh to retrieve latest changes */}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.svgLogo {
|
||||
--logo-fill: var(--color-neutral-100);
|
||||
--logo-fill: var(--color-neutral-900);
|
||||
--seg-A-color: #2782c4;
|
||||
--seg-B-color: #43b7f2;
|
||||
--seg-C-color: #8be2ff;
|
||||
--trace-color: #46474c;
|
||||
--trace-color: #dedfe3;
|
||||
|
||||
|
||||
--T: 2s;
|
||||
@@ -11,15 +11,9 @@
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.static .trace {
|
||||
animation: none;
|
||||
fill: var(--logo-fill);
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.static .seg {
|
||||
animation: none;
|
||||
opacity: 0;
|
||||
:global(html.dark) .svgLogo {
|
||||
--logo-fill: var(--color-neutral-100);
|
||||
--trace-color: #46474c;
|
||||
}
|
||||
|
||||
/* Base segment animation */
|
||||
@@ -182,4 +176,4 @@
|
||||
fill: var(--logo-fill);
|
||||
stroke:none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,10 @@ import { tcls } from '@/lib/tailwind';
|
||||
import type React from 'react';
|
||||
import styles from './AnimatedLogo.module.css';
|
||||
|
||||
interface AnimatedLogoProps {
|
||||
shouldAnimate?: boolean;
|
||||
}
|
||||
|
||||
export const AnimatedLogo: React.FC<AnimatedLogoProps> = (props) => {
|
||||
const { shouldAnimate = true } = props;
|
||||
|
||||
export const AnimatedLogo: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
className={tcls(styles.svgLogo, shouldAnimate ? undefined : styles.static)}
|
||||
className={styles.svgLogo}
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
|
||||
@@ -29,7 +29,7 @@ export function HideToolbarButton(props: HideToolbarButtonProps) {
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClickOutsideArcMenu = (event: Event) => {
|
||||
// Don't close the arc if we are clicking on the button itself
|
||||
// Don't close the arc if we are clicking clicking on the button itself
|
||||
if (buttonRef.current?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
@@ -47,32 +47,36 @@ export function HideToolbarButton(props: HideToolbarButtonProps) {
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [open]);
|
||||
|
||||
const items = [
|
||||
controls?.minimize
|
||||
? {
|
||||
id: 'minimize',
|
||||
icon: 'minus',
|
||||
label: 'Minimize',
|
||||
onClick: controls.minimize,
|
||||
}
|
||||
: null,
|
||||
controls?.closeSession
|
||||
? {
|
||||
id: 'session-close',
|
||||
icon: 'xmark',
|
||||
label: 'Close for one session',
|
||||
onClick: controls.closeSession,
|
||||
}
|
||||
: null,
|
||||
controls?.closePersistent
|
||||
? {
|
||||
id: 'persistent-close',
|
||||
icon: 'ban',
|
||||
label: "Don't show again",
|
||||
onClick: controls.closePersistent,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as Array<ArcMenuItem>;
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
[
|
||||
controls?.minimize
|
||||
? {
|
||||
id: 'minimize',
|
||||
icon: 'minus',
|
||||
label: 'Minimize',
|
||||
onClick: controls.minimize,
|
||||
}
|
||||
: null,
|
||||
controls?.closeSession
|
||||
? {
|
||||
id: 'session-close',
|
||||
icon: 'xmark',
|
||||
label: 'Close for one session',
|
||||
onClick: controls.closeSession,
|
||||
}
|
||||
: null,
|
||||
controls?.closePersistent
|
||||
? {
|
||||
id: 'persistent-close',
|
||||
icon: 'ban',
|
||||
label: "Don't show again",
|
||||
onClick: controls.closePersistent,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as Array<ArcMenuItem>,
|
||||
[controls]
|
||||
);
|
||||
|
||||
const sharedMotionStyle = motionValues
|
||||
? {
|
||||
@@ -204,10 +208,12 @@ export function ArcToolbarButton(props: ArcToolbarButtonProps) {
|
||||
'text-tint-1 dark:text-tint-12',
|
||||
'bg-[linear-gradient(110deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]',
|
||||
'dark:[background:linear-gradient(110deg,rgba(255,255,255,1)_0%,rgba(240,246,248,1)_100%)]',
|
||||
'border border-solid dark:border-[rgba(256,_256,_256,_0.06)]'
|
||||
'border border-solid dark:border-[rgb(255_255_255_/_40%)]'
|
||||
)}
|
||||
style={{
|
||||
background: 'linear-gradient(rgb(51, 53, 57), rgb(50, 52, 56))',
|
||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
boxShadow: 'rgba(255, 255, 255, 0.15) 0px 1px 1px 0px inset',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from 'motion/react';
|
||||
import React from 'react';
|
||||
import { AnimatedLogo } from './AnimatedLogo';
|
||||
import { useToolbarControls } from './ToolbarControlsContext';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
@@ -20,20 +19,14 @@ const DURATION_LOGO_APPEARANCE = 2000;
|
||||
const DELAY_BETWEEN_LOGO_AND_CONTENT = 100;
|
||||
|
||||
interface ToolbarProps {
|
||||
label: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
minified: boolean;
|
||||
onMinifiedChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function Toolbar(props: ToolbarProps) {
|
||||
const { children, label, minified, onMinifiedChange } = props;
|
||||
const controls = useToolbarControls();
|
||||
const { children, minified, onMinifiedChange } = props;
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
const autoExpandTriggeredRef = React.useRef(false);
|
||||
|
||||
const shouldAutoExpand = Boolean(controls?.shouldAutoExpand);
|
||||
const [shouldAnimateLogo, setShouldAnimateLogo] = React.useState(shouldAutoExpand);
|
||||
|
||||
// Wait for page to be ready, then show the toolbar
|
||||
React.useEffect(() => {
|
||||
@@ -49,41 +42,18 @@ export function Toolbar(props: ToolbarProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// After toolbar appears, wait, then show the full content
|
||||
// After toolbar appears, wait then show the full content
|
||||
React.useEffect(() => {
|
||||
if (!isReady || autoExpandTriggeredRef.current) {
|
||||
if (!isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldAutoExpand) {
|
||||
// When we already know the toolbar should stay expanded (e.g. the user previously
|
||||
// opened it this session) we short-circuit the auto-expand animation and immediately
|
||||
// render the expanded state without replaying the logo animation.
|
||||
autoExpandTriggeredRef.current = true;
|
||||
setShouldAnimateLogo(false);
|
||||
return;
|
||||
}
|
||||
|
||||
autoExpandTriggeredRef.current = true;
|
||||
|
||||
// On a fresh session we let the toolbar appear in its compact form, play the logo
|
||||
// animation, and only then expand the toolbar. The timeout mirrors the duration of the
|
||||
// logo animation so both transitions feel connected.
|
||||
const expandAfterTimeout = setTimeout(() => {
|
||||
setShouldAnimateLogo(false);
|
||||
onMinifiedChange(false);
|
||||
}, DURATION_LOGO_APPEARANCE + DELAY_BETWEEN_LOGO_AND_CONTENT);
|
||||
|
||||
return () => clearTimeout(expandAfterTimeout);
|
||||
}, [isReady, onMinifiedChange, shouldAutoExpand]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!minified) {
|
||||
// Any manual expansion should stop the logo animation so the icon stays in its
|
||||
// “settled” state once the toolbar is open.
|
||||
setShouldAnimateLogo(false);
|
||||
}
|
||||
}, [minified]);
|
||||
}, [isReady, onMinifiedChange]);
|
||||
|
||||
// Don't render anything until page is ready
|
||||
if (!isReady) {
|
||||
@@ -91,47 +61,51 @@ export function Toolbar(props: ToolbarProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<motion.div className="-translate-x-1/2 fixed bottom-5 left-1/2 z-40 w-auto max-w-xl transform px-4">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
onClick={() => {
|
||||
if (minified) {
|
||||
setShouldAnimateLogo(false);
|
||||
onMinifiedChange(false);
|
||||
}
|
||||
}}
|
||||
layout
|
||||
transition={toolbarEasings.spring}
|
||||
className={tcls(
|
||||
minified ? 'cursor-pointer px-2' : 'pr-2 pl-3.5',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'min-h-11',
|
||||
'min-w-12',
|
||||
'h-12',
|
||||
'py-2',
|
||||
'backdrop-blur-sm',
|
||||
'origin-center',
|
||||
'border-[0.5px] border-neutral-5 border-solid dark:border-neutral-8',
|
||||
'bg-[linear-gradient(45deg,rgba(39,39,39,0.8)_100%,rgba(39,39,39,0.4)_80%)]',
|
||||
'dark:bg-[linear-gradient(45deg,rgba(39,39,39,0.5)_100%,rgba(39,39,39,0.3)_80%)]'
|
||||
)}
|
||||
style={{
|
||||
borderRadius: '100px', // This is set on `style` so Framer Motion can correct for distortions
|
||||
}}
|
||||
>
|
||||
{/* Logo with stroke segments animation in blue-tints */}
|
||||
<motion.div layout>
|
||||
<AnimatedLogo shouldAnimate={shouldAnimateLogo} />
|
||||
</motion.div>
|
||||
|
||||
{!minified ? children : null}
|
||||
<motion.div className="-translate-x-1/2 fixed bottom-5 left-1/2 z-40 w-auto max-w-xl transform px-4">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
onClick={() => {
|
||||
if (minified) {
|
||||
onMinifiedChange(false);
|
||||
}
|
||||
}}
|
||||
layout
|
||||
transition={toolbarEasings.spring}
|
||||
className={tcls(
|
||||
minified ? 'cursor-pointer px-2' : 'pr-2 pl-3.5',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'min-h-11',
|
||||
'min-w-12',
|
||||
'h-12',
|
||||
'py-2',
|
||||
'backdrop-blur-sm',
|
||||
'origin-center',
|
||||
'border-[0.5px] border-neutral-5 border-solid dark:border-neutral-8',
|
||||
'bg-[linear-gradient(45deg,rgba(255,255,255,0)_0%,rgba(255,255,255,0.2)_100%)]'
|
||||
)}
|
||||
initial={{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
}}
|
||||
animate={{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
}}
|
||||
style={{
|
||||
borderRadius: '100px', // This is set on `style` so Framer Motion can correct for distortions
|
||||
}}
|
||||
>
|
||||
{/* Logo with stroke segments animation in blue-tints */}
|
||||
<motion.div layout>
|
||||
<AnimatedLogo />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
|
||||
{!minified ? children : null}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,6 +190,9 @@ export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps
|
||||
transformOrigin: 'bottom center',
|
||||
zIndex: motionValues?.scale ? 10 : 'auto',
|
||||
...style,
|
||||
background: 'linear-gradient(rgb(51, 53, 57), rgb(50, 52, 56))',
|
||||
boxShadow: 'rgba(255, 255, 255, 0.15) 0px 1px 1px 0px inset',
|
||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
}
|
||||
}
|
||||
transition={{
|
||||
@@ -239,9 +216,7 @@ export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps
|
||||
'cursor-pointer',
|
||||
'transition-colors',
|
||||
'size-8',
|
||||
disabled ? 'cursor-not-allowed opacity-50' : '',
|
||||
'border border-[rgba(256,_256,_256,_0.06)] border-solid',
|
||||
'bg-[linear-gradient(45deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]'
|
||||
disabled ? 'cursor-not-allowed opacity-50' : ''
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
@@ -311,10 +286,7 @@ export function ToolbarTitle(props: { prefix?: string; suffix: string }) {
|
||||
|
||||
function ToolbarTitlePrefix(props: { title: string }) {
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(0)}
|
||||
className="truncate font-medium text-neutral-1 dark:text-neutral-12"
|
||||
>
|
||||
<motion.span {...getCopyVariants(0)} className="truncate font-medium text-neutral-12">
|
||||
{props.title}
|
||||
</motion.span>
|
||||
);
|
||||
@@ -322,10 +294,7 @@ function ToolbarTitlePrefix(props: { title: string }) {
|
||||
|
||||
function ToolbarTitleSuffix(props: { title: string }) {
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(1)}
|
||||
className="max-w-[20ch] truncate text-neutral-1 dark:text-neutral-12"
|
||||
>
|
||||
<motion.span {...getCopyVariants(1)} className="max-w-[20ch] truncate text-neutral-12">
|
||||
{props.title}
|
||||
</motion.span>
|
||||
);
|
||||
@@ -333,10 +302,7 @@ function ToolbarTitleSuffix(props: { title: string }) {
|
||||
|
||||
export function ToolbarSubtitle(props: { subtitle: React.ReactNode }) {
|
||||
return (
|
||||
<motion.span
|
||||
{...getCopyVariants(1)}
|
||||
className="text-neutral-1/80 text-xxs dark:text-neutral-12/80"
|
||||
>
|
||||
<motion.span {...getCopyVariants(1)} className="text-neutral-12/90 text-xxs">
|
||||
{props.subtitle}
|
||||
</motion.span>
|
||||
);
|
||||
|
||||
@@ -5,21 +5,15 @@ export interface ToolbarControlsContextValue {
|
||||
minimize: () => void;
|
||||
closeSession?: () => void;
|
||||
closePersistent?: () => void;
|
||||
shouldAutoExpand?: boolean;
|
||||
}
|
||||
|
||||
const ToolbarControlsContext = React.createContext<ToolbarControlsContextValue | null>(null);
|
||||
|
||||
/*
|
||||
* Provides reusable state setters (mainly for hiding/showing the toolbar) for the toolbar controls propagated through to the children
|
||||
*/
|
||||
export function ToolbarControlsProvider(
|
||||
props: React.PropsWithChildren<{ value: ToolbarControlsContextValue | null }>
|
||||
) {
|
||||
const { children, value } = props;
|
||||
return (
|
||||
<ToolbarControlsContext.Provider value={value}>{children}</ToolbarControlsContext.Provider>
|
||||
);
|
||||
return <ToolbarControlsContext.Provider value={value}>{children}</ToolbarControlsContext.Provider>;
|
||||
}
|
||||
|
||||
export function useToolbarControls() {
|
||||
|
||||
@@ -3,4 +3,3 @@ export * from './AdminToolbarClient';
|
||||
export * from './IframeWrapper';
|
||||
export * from './Toolbar';
|
||||
export * from './transitions';
|
||||
export * from './utils';
|
||||
|
||||
@@ -212,7 +212,7 @@ export function useMagnificationEffect(props: {
|
||||
);
|
||||
});
|
||||
|
||||
// Update motion values
|
||||
// Update motion values - springs in components will animate to these values
|
||||
buttonEffects.forEach((effect, index) => {
|
||||
const motionValue = buttonMotionValues[index];
|
||||
if (motionValue) {
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
getLocalStorageItem,
|
||||
getSessionStorageItem,
|
||||
removeSessionStorageItem,
|
||||
setLocalStorageItem,
|
||||
setSessionStorageItem,
|
||||
} from '@/lib/browser/local-storage';
|
||||
|
||||
const STORAGE_KEY = 'gitbook_toolbar_closed';
|
||||
const SESSION_STORAGE_KEY = 'gitbook_toolbar_session_closed';
|
||||
const SESSION_MINIFIED_KEY = 'gitbook_toolbar_minified';
|
||||
|
||||
type SessionHideReason = 'session' | 'persistent';
|
||||
|
||||
/**
|
||||
* Read the current session hide state. We store whether the toolbar was hidden “for session” or
|
||||
* “persistently” so we can distinguish between a temporary dismissal and a user preference.
|
||||
*/
|
||||
export const getSessionHideState = (): { hidden: boolean; reason?: SessionHideReason } => {
|
||||
const value = getSessionStorageItem<SessionHideReason | null>(SESSION_STORAGE_KEY, null);
|
||||
if (value === 'session' || value === 'persistent') {
|
||||
return { hidden: true, reason: value };
|
||||
}
|
||||
return { hidden: false, reason: undefined };
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist the session hide state. Passing `false` clears the stored preference entirely.
|
||||
*/
|
||||
export const setSessionHidden = (value: boolean, reason?: SessionHideReason) => {
|
||||
if (value && reason) {
|
||||
setSessionStorageItem(SESSION_STORAGE_KEY, reason);
|
||||
} else {
|
||||
removeSessionStorageItem(SESSION_STORAGE_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the last minified state from session storage. If no value was ever stored we return
|
||||
* `undefined`, which signals that the toolbar should auto-expand once the logo animation finishes.
|
||||
*/
|
||||
export const getStoredMinified = (): boolean | undefined => {
|
||||
const stored = getSessionStorageItem<boolean | null>(SESSION_MINIFIED_KEY, null);
|
||||
if (stored === null || stored === undefined) {
|
||||
removeSessionStorageItem(SESSION_MINIFIED_KEY);
|
||||
return undefined;
|
||||
}
|
||||
return stored;
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist the current minified state for the ongoing session.
|
||||
*/
|
||||
export const setStoredMinified = (value: boolean) => {
|
||||
setSessionStorageItem(SESSION_MINIFIED_KEY, value);
|
||||
};
|
||||
|
||||
interface UseToolbarVisibilityOptions {
|
||||
onPersistentClose?: () => void;
|
||||
onSessionClose?: () => void;
|
||||
onToggleMinify?: () => void;
|
||||
}
|
||||
|
||||
export function useToolbarVisibility(options: UseToolbarVisibilityOptions = {}) {
|
||||
const { onPersistentClose, onSessionClose, onToggleMinify } = options;
|
||||
|
||||
const [initialStoredMinified] = React.useState<boolean | undefined>(() =>
|
||||
typeof window !== 'undefined' ? getStoredMinified() : undefined
|
||||
);
|
||||
const shouldAutoExpand = initialStoredMinified === undefined;
|
||||
|
||||
const [minified, setMinifiedState] = React.useState<boolean>(() =>
|
||||
initialStoredMinified !== undefined ? initialStoredMinified : true
|
||||
);
|
||||
|
||||
const [persistentHidden, setPersistentHidden] = React.useState(() =>
|
||||
getLocalStorageItem(STORAGE_KEY, false)
|
||||
);
|
||||
|
||||
const [sessionReason, setSessionReason] = React.useState<SessionHideReason | undefined>(() => {
|
||||
const state = getSessionHideState();
|
||||
return state.hidden ? state.reason : undefined;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleStorage = () => {
|
||||
setPersistentHidden(getLocalStorageItem(STORAGE_KEY, false));
|
||||
const state = getSessionHideState();
|
||||
setSessionReason(state.hidden ? state.reason : undefined);
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorage);
|
||||
return () => window.removeEventListener('storage', handleStorage);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (persistentHidden) {
|
||||
if (sessionReason !== 'persistent') {
|
||||
// Sync the session flag so we honour the persistent preference and avoid flashes when
|
||||
// opening new tabs in the same browsing session.
|
||||
setSessionHidden(true, 'persistent');
|
||||
setSessionReason('persistent');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionReason === 'persistent') {
|
||||
// If the persistent preference was cleared we also clear the session flag so the toolbar
|
||||
// can reappear immediately without requiring a full reload.
|
||||
setSessionHidden(false);
|
||||
setSessionReason(undefined);
|
||||
}
|
||||
}, [persistentHidden, sessionReason]);
|
||||
|
||||
const setMinified = (value: boolean) => {
|
||||
setMinifiedState(value);
|
||||
setStoredMinified(value);
|
||||
onToggleMinify?.();
|
||||
};
|
||||
|
||||
const minimize = () => setMinified(true);
|
||||
|
||||
const closeSession = () => {
|
||||
setSessionHidden(true, 'session');
|
||||
setSessionReason('session');
|
||||
onSessionClose?.();
|
||||
};
|
||||
|
||||
const closePersistent = () => {
|
||||
setLocalStorageItem(STORAGE_KEY, true);
|
||||
setPersistentHidden(true);
|
||||
setSessionHidden(true, 'persistent');
|
||||
setSessionReason('persistent');
|
||||
onPersistentClose?.();
|
||||
};
|
||||
|
||||
const hidden = persistentHidden || sessionReason === 'session';
|
||||
|
||||
return {
|
||||
minified,
|
||||
setMinified,
|
||||
shouldAutoExpand,
|
||||
hidden,
|
||||
minimize,
|
||||
closeSession,
|
||||
closePersistent,
|
||||
};
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
|
||||
node.data && 'fullWidth' in node.data && node.data.fullWidth
|
||||
? 'max-w-screen-xl'
|
||||
: 'max-w-3xl',
|
||||
FULL_WIDTH_BLOCKS.includes(node.type) && 'page-width-wide:max-w-full',
|
||||
FULL_WIDTH_BLOCKS.includes(node.type) && 'page-width-wide:max-w-screen-2xl',
|
||||
blockStyle,
|
||||
]}
|
||||
isEstimatedOffscreen={isOffscreen}
|
||||
|
||||
@@ -24,7 +24,6 @@ export function Caption(
|
||||
wrapperStyle?: ClassValue;
|
||||
block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile;
|
||||
withBorder?: boolean;
|
||||
withFrame?: boolean;
|
||||
} & DocumentContextProps
|
||||
) {
|
||||
const {
|
||||
@@ -34,7 +33,6 @@ export function Caption(
|
||||
context,
|
||||
fit = false,
|
||||
withBorder = false,
|
||||
withFrame = false,
|
||||
wrapperStyle = [
|
||||
'relative',
|
||||
'overflow-hidden',
|
||||
@@ -46,7 +44,6 @@ export function Caption(
|
||||
withBorder
|
||||
? 'rounded-corners:rounded-sm circular-corners:rounded-2xl after:border-tint-subtle after:border after:rounded circular-corners:after:rounded-2xl rounded-corners:after:rounded-sm dark:after:mix-blend-plus-lighter after:pointer-events-none'
|
||||
: null,
|
||||
withFrame && 'p-2',
|
||||
],
|
||||
style,
|
||||
} = props;
|
||||
@@ -65,14 +62,7 @@ export function Caption(
|
||||
return (
|
||||
<picture className={tcls('relative', style)}>
|
||||
<div className={tcls(wrapperStyle, 'mx-auto')}>{children}</div>
|
||||
<figcaption
|
||||
className={tcls(
|
||||
'text-xs',
|
||||
'text-center',
|
||||
'text-tint',
|
||||
withFrame ? 'mt-0.5 mb-2.5' : 'mt-2'
|
||||
)}
|
||||
>
|
||||
<figcaption className={tcls('text-sm', 'text-center', 'mt-2', 'text-tint')}>
|
||||
<Inlines
|
||||
nodes={captionParagraph.nodes}
|
||||
document={document}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { JSONDocument } from '@gitbook/api';
|
||||
import type { DocumentBlockCode, JSONDocument } from '@gitbook/api';
|
||||
import { useId } from 'react';
|
||||
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
import { convertCodeStringToBlock } from './utils';
|
||||
|
||||
/**
|
||||
* Plain code block with syntax highlighting.
|
||||
@@ -12,7 +11,31 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
||||
const { code, syntax } = props;
|
||||
const id = useId();
|
||||
|
||||
const block = convertCodeStringToBlock({ key: id, code, syntax });
|
||||
const block: DocumentBlockCode = {
|
||||
key: id,
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: {
|
||||
syntax,
|
||||
},
|
||||
nodes: code.split('\n').map((line) => ({
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: line,
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
|
||||
const document: JSONDocument = {
|
||||
object: 'document',
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { convertCodeStringToBlock } from './utils';
|
||||
|
||||
describe('convertCodeStringToBlock', () => {
|
||||
it('converts plain code string without placeholders', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test1',
|
||||
code: 'console.log("hello");',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test1',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'console.log("hello");', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('converts a single placeholder into an expression node', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test2',
|
||||
code: 'const config = { API_KEY: "$$__X-GITBOOK-PREFILL[visitor.claims.apiKey ?? "YOUR_API_KEY"]__$$" }',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test2',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{ object: 'leaf', text: 'const config = { API_KEY: "', marks: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.apiKey ?? "YOUR_API_KEY"' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: '" }', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple placeholders in one line', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test3',
|
||||
code: 'let a = $$__X-GITBOOK-PREFILL[valA]__$$, b = $$__X-GITBOOK-PREFILL[valB]__$$;',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test3',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'let a = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'valA' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ', b = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'valB' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ';', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple placeholders across different lines', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test4',
|
||||
code: [
|
||||
'const name = "$$__X-GITBOOK-PREFILL[visitor.claims.userName]__$$";',
|
||||
'const age = $$__X-GITBOOK-PREFILL[visitor.claims.userAge]__$$;',
|
||||
'console.log(name, age);',
|
||||
].join('\n'),
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test4',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'const name = "', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.userName' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: '";', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'const age = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.userAge' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ';', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{ object: 'leaf', text: 'console.log(name, age);', marks: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty code block for empty string', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test5',
|
||||
code: '',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test5',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import type { DocumentBlockCode, DocumentBlockCodeLine } from '@gitbook/api';
|
||||
|
||||
const PREFILL_WITH_EXPR_REGEX = /\$\$__X-GITBOOK-PREFILL\[(.+?)\]__\$\$/g;
|
||||
|
||||
/**
|
||||
* Convert a raw code string into a `DocumentBlockCode` object representation.
|
||||
*
|
||||
* Any placeholder of the form `$$__X-GITBOOK-PREFILL[<expression>]__$$` inside the code
|
||||
* string is transformed into a DocumentInlineExpression node with its `data.expression` set to the
|
||||
* extracted `<expression>`.
|
||||
*/
|
||||
export function convertCodeStringToBlock(args: {
|
||||
key: string;
|
||||
code: string;
|
||||
syntax: string;
|
||||
}): DocumentBlockCode {
|
||||
const { key, code, syntax } = args;
|
||||
const lines = code.split('\n').map<DocumentBlockCodeLine>((line) => {
|
||||
const nodes: DocumentBlockCodeLine['nodes'] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of line.matchAll(PREFILL_WITH_EXPR_REGEX)) {
|
||||
const [placeholder, expression] = match;
|
||||
const start = match.index ?? 0;
|
||||
|
||||
if (start > lastIndex) {
|
||||
nodes.push({
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: line.slice(lastIndex, start), marks: [] }],
|
||||
});
|
||||
}
|
||||
|
||||
if (expression) {
|
||||
nodes.push({
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression },
|
||||
isVoid: true,
|
||||
});
|
||||
}
|
||||
|
||||
lastIndex = start + placeholder.length;
|
||||
}
|
||||
|
||||
if (lastIndex < line.length) {
|
||||
nodes.push({
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: line.slice(lastIndex), marks: [] }],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
key,
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax },
|
||||
nodes: lines,
|
||||
};
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import type { DocumentContext } from './DocumentView';
|
||||
export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
const { document, block, style, context, isEstimatedOffscreen } = props;
|
||||
|
||||
const hasMultipleImages = block.nodes.length > 1;
|
||||
const { align = 'center', withFrame } = block.data;
|
||||
const isMultipleImages = block.nodes.length > 1;
|
||||
const { align = 'center' } = block.data;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -24,15 +24,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
align === 'center' && 'justify-center',
|
||||
align === 'right' && 'justify-end',
|
||||
align === 'left' && 'justify-start',
|
||||
hasMultipleImages && ['grid', 'grid-flow-col'],
|
||||
withFrame && [
|
||||
'rounded-2xl',
|
||||
'border',
|
||||
'border-[rgb(234,235,238)]',
|
||||
'dark:border-[rgb(45,50,58)]',
|
||||
'relative',
|
||||
'overflow-hidden',
|
||||
]
|
||||
isMultipleImages && ['grid', 'grid-flow-col']
|
||||
)}
|
||||
>
|
||||
{block.nodes.map((node: any, _i: number) => (
|
||||
@@ -44,7 +36,6 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||
siblings={block.nodes.length}
|
||||
context={context}
|
||||
isEstimatedOffscreen={isEstimatedOffscreen}
|
||||
withFrame={withFrame}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -71,9 +62,8 @@ async function ImageBlock(props: {
|
||||
context: DocumentContext;
|
||||
siblings: number;
|
||||
isEstimatedOffscreen: boolean;
|
||||
withFrame?: boolean;
|
||||
}) {
|
||||
const { block, context, isEstimatedOffscreen, withFrame } = props;
|
||||
const { block, context, isEstimatedOffscreen } = props;
|
||||
|
||||
const [src, darkSrc] = await Promise.all([
|
||||
context.contentContext ? resolveContentRef(block.data.ref, context.contentContext) : null,
|
||||
@@ -87,66 +77,33 @@ async function ImageBlock(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={tcls('relative', 'overflow-hidden')}>
|
||||
{/* Frame grid */}
|
||||
{withFrame && (
|
||||
<div
|
||||
className={tcls(
|
||||
'absolute',
|
||||
'-top-0.5',
|
||||
'-left-0.5',
|
||||
'right-px',
|
||||
'bottom-px',
|
||||
'opacity-40',
|
||||
'dark:opacity-[0.1]',
|
||||
'bg-[length:24px_24px,24px_24px]',
|
||||
'bg-[linear-gradient(to_right,_rgb(234,235,238)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(234,235,238)_1px,_transparent_1px)]',
|
||||
'dark:bg-[linear-gradient(to_right,_rgb(122,128,139)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(122,128,139)_1px,_transparent_1px)]',
|
||||
'bg-repeat'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Shadow overlay */}
|
||||
{withFrame && (
|
||||
<div
|
||||
className={tcls(
|
||||
'pointer-events-none absolute inset-0 rounded-2xl',
|
||||
'shadow-[inset_0_0_10px_10px_rgba(255,255,255,0.9)]',
|
||||
'dark:shadow-[inset_0_0_10px_10px_rgb(29,29,29)]'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Caption {...props} fit>
|
||||
<Image
|
||||
alt={block.data.alt ?? ''}
|
||||
sizes={imageBlockSizes}
|
||||
resize={context.contentContext?.imageResizer}
|
||||
sources={{
|
||||
light: {
|
||||
src: src.href,
|
||||
size: src.file?.dimensions,
|
||||
},
|
||||
dark: darkSrc
|
||||
? {
|
||||
src: darkSrc.href,
|
||||
size: darkSrc.file?.dimensions,
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
priority={isEstimatedOffscreen ? 'lazy' : 'high'}
|
||||
preload
|
||||
zoom
|
||||
inlineStyle={{
|
||||
maxWidth: '100%',
|
||||
width: getImageDimension(block.data.width, undefined),
|
||||
height: getImageDimension(block.data.height, 'auto'),
|
||||
}}
|
||||
style={withFrame ? 'rounded-xl' : undefined}
|
||||
/>
|
||||
</Caption>
|
||||
</div>
|
||||
<Caption {...props} fit>
|
||||
<Image
|
||||
alt={block.data.alt ?? ''}
|
||||
sizes={imageBlockSizes}
|
||||
resize={context.contentContext?.imageResizer}
|
||||
sources={{
|
||||
light: {
|
||||
src: src.href,
|
||||
size: src.file?.dimensions,
|
||||
},
|
||||
dark: darkSrc
|
||||
? {
|
||||
src: darkSrc.href,
|
||||
size: darkSrc.file?.dimensions,
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
priority={isEstimatedOffscreen ? 'lazy' : 'high'}
|
||||
preload
|
||||
zoom
|
||||
inlineStyle={{
|
||||
maxWidth: '100%',
|
||||
width: getImageDimension(block.data.width, undefined),
|
||||
height: getImageDimension(block.data.height, 'auto'),
|
||||
}}
|
||||
/>
|
||||
</Caption>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import type { DocumentInlineIcon, DocumentMarkColor } from '@gitbook/api';
|
||||
import type { DocumentInlineIcon } from '@gitbook/api';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { textColorToStyle } from './utils/colors';
|
||||
|
||||
export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
|
||||
const { inline } = props;
|
||||
const icon = inline.data.icon as IconName;
|
||||
// @ts-expect-error remove this comment once API is updated
|
||||
const color = inline.data.color
|
||||
? // @ts-expect-error remove "as DocumentMarkColor['data']['text']" once API is updated
|
||||
(inline.data.color as DocumentMarkColor['data']['text'])
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Icon
|
||||
icon={icon}
|
||||
className={tcls('inline size-[1em]', color ? textColorToStyle[color] : null)}
|
||||
/>
|
||||
);
|
||||
return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function InlineImage(props: InlineProps<DocumentInlineImage>) {
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
alt={inline.data.alt ?? inline.data.caption ?? ''}
|
||||
alt={inline.data.caption ?? ''}
|
||||
sizes={sizes}
|
||||
resize={context.contentContext?.imageResizer}
|
||||
sources={{
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
.openapi-schema-name {
|
||||
/* To make double click on the property name select only the name,
|
||||
we disable selection on the parent and re-enable it on the children. */
|
||||
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5 items-center;
|
||||
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5;
|
||||
}
|
||||
|
||||
.openapi-schema-name .openapi-deprecated {
|
||||
@@ -207,10 +207,6 @@
|
||||
@apply line-through opacity-9;
|
||||
}
|
||||
|
||||
.openapi-schema-discriminator {
|
||||
@apply text-primary-subtle/9 text-[0.813rem] lowercase;
|
||||
}
|
||||
|
||||
.openapi-schema-required {
|
||||
@apply text-warning-subtle text-[0.813rem] lowercase;
|
||||
}
|
||||
@@ -317,11 +313,10 @@
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-flows {
|
||||
@apply flex flex-col gap-3;
|
||||
@apply flex flex-col gap-2 divide-y divide-tint-subtle;
|
||||
}
|
||||
|
||||
.openapi-securities-oauth-content,
|
||||
.openapi-securities-scopes {
|
||||
.openapi-securities-oauth-content {
|
||||
@apply prose *:!prose-sm *:text-tint;
|
||||
}
|
||||
|
||||
@@ -329,7 +324,7 @@
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
.openapi-securities-scopes ul {
|
||||
.openapi-securities-oauth-content ul {
|
||||
@apply !my-0;
|
||||
}
|
||||
|
||||
@@ -841,7 +836,7 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger-label {
|
||||
@apply absolute right-3 mr-px px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
|
||||
@apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger-label span {
|
||||
|
||||
@@ -12,8 +12,7 @@ import type {
|
||||
} from '@gitbook/api';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { backgroundColorToStyle, textColorToStyle } from './utils/colors';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
export function Text(props: { text: DocumentText }) {
|
||||
const { text } = props;
|
||||
@@ -122,3 +121,51 @@ function Color(props: MarkedLeafProps<DocumentMarkColor>) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO replace by DocumentMarkColor['data']['text'] and DocumentMarkColor['data']['background']
|
||||
* once the API is updated.
|
||||
*/
|
||||
type DocumentMarkColorValue =
|
||||
| 'default'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'red'
|
||||
| 'orange'
|
||||
| 'yellow'
|
||||
| 'purple'
|
||||
| '$primary'
|
||||
| '$info'
|
||||
| '$success'
|
||||
| '$warning'
|
||||
| '$danger';
|
||||
|
||||
const textColorToStyle: { [color in DocumentMarkColorValue]: ClassValue } = {
|
||||
default: [],
|
||||
blue: ['text-blue-500'],
|
||||
red: ['text-red-500'],
|
||||
green: ['text-green-500'],
|
||||
yellow: ['text-yellow-600'],
|
||||
purple: ['text-purple-500'],
|
||||
orange: ['text-orange-500'],
|
||||
$primary: ['text-primary'],
|
||||
$info: ['text-info'],
|
||||
$success: ['text-success'],
|
||||
$warning: ['text-warning'],
|
||||
$danger: ['text-danger'],
|
||||
};
|
||||
|
||||
const backgroundColorToStyle: { [color in DocumentMarkColorValue]: ClassValue } = {
|
||||
default: [],
|
||||
blue: ['bg-mark-blue'],
|
||||
red: ['bg-mark-red'],
|
||||
green: ['bg-mark-green'],
|
||||
yellow: ['bg-mark-yellow'],
|
||||
purple: ['bg-mark-purple'],
|
||||
orange: ['bg-mark-orange'],
|
||||
$primary: ['bg-primary'],
|
||||
$info: ['bg-info'],
|
||||
$success: ['bg-success'],
|
||||
$warning: ['bg-warning'],
|
||||
$danger: ['bg-danger'],
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { ClassValue } from '@/lib/tailwind';
|
||||
import type { DocumentMarkColor } from '@gitbook/api';
|
||||
|
||||
export const textColorToStyle: { [color in DocumentMarkColor['data']['text']]: ClassValue } = {
|
||||
default: [],
|
||||
blue: ['text-blue-500'],
|
||||
red: ['text-red-500'],
|
||||
green: ['text-green-500'],
|
||||
yellow: ['text-yellow-600'],
|
||||
purple: ['text-purple-500'],
|
||||
orange: ['text-orange-500'],
|
||||
$primary: ['text-primary'],
|
||||
$info: ['text-info'],
|
||||
$success: ['text-success'],
|
||||
$warning: ['text-warning'],
|
||||
$danger: ['text-danger'],
|
||||
};
|
||||
|
||||
export const backgroundColorToStyle: {
|
||||
[color in DocumentMarkColor['data']['background']]: ClassValue;
|
||||
} = {
|
||||
default: [],
|
||||
blue: ['bg-mark-blue'],
|
||||
red: ['bg-mark-red'],
|
||||
green: ['bg-mark-green'],
|
||||
yellow: ['bg-mark-yellow'],
|
||||
purple: ['bg-mark-purple'],
|
||||
orange: ['bg-mark-orange'],
|
||||
$primary: ['bg-primary'],
|
||||
$info: ['bg-info'],
|
||||
$success: ['bg-success'],
|
||||
$warning: ['bg-warning'],
|
||||
$danger: ['bg-danger'],
|
||||
};
|
||||
@@ -23,12 +23,6 @@ export function Header(props: {
|
||||
const { context, withTopHeader, withVariants } = props;
|
||||
const { siteSpace, siteSpaces, sections, customization } = context;
|
||||
|
||||
const withSections = Boolean(
|
||||
sections &&
|
||||
(sections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group
|
||||
sections.list.some((s) => s.object === 'site-section-group'))
|
||||
);
|
||||
|
||||
return (
|
||||
<header
|
||||
id="site-header"
|
||||
@@ -150,7 +144,7 @@ export function Header(props: {
|
||||
</div>
|
||||
|
||||
{customization.header.links.length > 0 ||
|
||||
(!withSections && withVariants === 'translations') ? (
|
||||
(!sections && withVariants === 'translations') ? (
|
||||
<HeaderLinks>
|
||||
{customization.header.links.length > 0 ? (
|
||||
<>
|
||||
@@ -170,7 +164,7 @@ export function Header(props: {
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{!withSections && withVariants === 'translations' ? (
|
||||
{!sections && withVariants === 'translations' ? (
|
||||
<TranslationsDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
@@ -184,7 +178,7 @@ export function Header(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sections && withSections ? (
|
||||
{sections ? (
|
||||
<div className="transition-[padding] duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
<SiteSectionTabs sections={encodeClientSiteSections(context, sections)}>
|
||||
{withVariants === 'translations' ? (
|
||||
|
||||
@@ -18,22 +18,17 @@ import {
|
||||
ActionViewAsPDF,
|
||||
} from './PageActions';
|
||||
|
||||
export type PageActionsDropdownURLs = {
|
||||
html: string;
|
||||
markdown: string;
|
||||
mcp?: string;
|
||||
pdf?: string;
|
||||
interface PageActionsDropdownProps {
|
||||
siteTitle: string;
|
||||
markdownPageURL: string;
|
||||
mcpURL?: string;
|
||||
pdfURL?: string;
|
||||
className?: string;
|
||||
actions: SiteCustomizationSettings['pageActions'];
|
||||
editOnGit?: {
|
||||
provider: GitSyncState['installationProvider'];
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
|
||||
interface PageActionsDropdownProps {
|
||||
siteTitle: string;
|
||||
urls: PageActionsDropdownURLs;
|
||||
className?: string;
|
||||
actions: SiteCustomizationSettings['pageActions'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,7 +75,7 @@ export function PageActionsDropdown(props: PageActionsDropdownProps) {
|
||||
* Return the list of actions to show in the dropdown menu.
|
||||
*/
|
||||
function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNode[] {
|
||||
const { siteTitle, urls, actions } = props;
|
||||
const { siteTitle, markdownPageURL, mcpURL, actions } = props;
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
@@ -99,45 +94,55 @@ function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNod
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionCopyMarkdown
|
||||
isDefaultAction={!assistants.length}
|
||||
markdownPageURL={urls.markdown}
|
||||
markdownPageURL={markdownPageURL}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
<ActionViewAsMarkdown markdownPageURL={urls.markdown} type="dropdown-menu-item" />
|
||||
<ActionViewAsMarkdown markdownPageURL={markdownPageURL} type="dropdown-menu-item" />
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
actions.externalAI ? (
|
||||
<React.Fragment key="externalAI">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionOpenInLLM provider="chatgpt" url={urls.html} type="dropdown-menu-item" />
|
||||
<ActionOpenInLLM provider="claude" url={urls.html} type="dropdown-menu-item" />
|
||||
<ActionOpenInLLM
|
||||
provider="chatgpt"
|
||||
url={markdownPageURL}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
<ActionOpenInLLM
|
||||
provider="claude"
|
||||
url={markdownPageURL}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
actions.mcp && urls.mcp ? (
|
||||
actions.mcp && mcpURL ? (
|
||||
<React.Fragment key="mcp">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
<ActionCopyMCPURL mcpURL={urls.mcp} type="dropdown-menu-item" />
|
||||
<ActionCopyMCPURL mcpURL={mcpURL} type="dropdown-menu-item" />
|
||||
<ActionOpenMCP
|
||||
provider="vscode"
|
||||
mcpURL={urls.mcp}
|
||||
mcpURL={mcpURL}
|
||||
siteTitle={siteTitle}
|
||||
type="dropdown-menu-item"
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
|
||||
urls.editOnGit || urls.pdf ? (
|
||||
props.editOnGit || props.pdfURL ? (
|
||||
<React.Fragment key="editOnGit">
|
||||
<DropdownMenuSeparator className="first:hidden" />
|
||||
{urls.editOnGit ? (
|
||||
{props.editOnGit ? (
|
||||
<ActionOpenEditOnGit
|
||||
type="dropdown-menu-item"
|
||||
provider={urls.editOnGit.provider}
|
||||
url={urls.editOnGit.url}
|
||||
provider={props.editOnGit.provider}
|
||||
url={props.editOnGit.url}
|
||||
/>
|
||||
) : null}
|
||||
{urls.pdf ? <ActionViewAsPDF url={urls.pdf} type="dropdown-menu-item" /> : null}
|
||||
{props.pdfURL ? (
|
||||
<ActionViewAsPDF url={props.pdfURL} type="dropdown-menu-item" />
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
].filter(Boolean);
|
||||
@@ -147,7 +152,7 @@ function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNod
|
||||
* A default action shown as a quick-access button beside the dropdown menu
|
||||
*/
|
||||
function getPageDefaultAction(props: PageActionsDropdownProps) {
|
||||
const { urls, actions } = props;
|
||||
const { markdownPageURL, actions } = props;
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
@@ -157,12 +162,12 @@ function getPageDefaultAction(props: PageActionsDropdownProps) {
|
||||
return <ActionOpenAssistant assistant={assistant} type="button" />;
|
||||
}
|
||||
|
||||
if (urls.editOnGit) {
|
||||
if (props.editOnGit) {
|
||||
return (
|
||||
<ActionOpenEditOnGit
|
||||
type="button"
|
||||
provider={urls.editOnGit.provider}
|
||||
url={urls.editOnGit.url}
|
||||
provider={props.editOnGit.provider}
|
||||
url={props.editOnGit.url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -171,7 +176,7 @@ function getPageDefaultAction(props: PageActionsDropdownProps) {
|
||||
return (
|
||||
<ActionCopyMarkdown
|
||||
isDefaultAction={!assistant}
|
||||
markdownPageURL={urls.markdown}
|
||||
markdownPageURL={markdownPageURL}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,6 @@ export function PageAside(props: {
|
||||
<aside
|
||||
className={tcls(
|
||||
'group/aside',
|
||||
'order-last',
|
||||
'hidden',
|
||||
'pt-8',
|
||||
'pb-4',
|
||||
|
||||
@@ -110,7 +110,7 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
|
||||
'opacity-8',
|
||||
'contrast-more:opacity-11',
|
||||
|
||||
'sidebar-list-default:rounded-l-none!',
|
||||
'sidebar-list-default:rounded-l-none',
|
||||
'sidebar-list-default:border-l',
|
||||
'sidebar-list-default:border-tint',
|
||||
],
|
||||
|
||||
@@ -51,10 +51,10 @@ export function PageBody(props: {
|
||||
<main
|
||||
className={tcls(
|
||||
'relative min-w-0 flex-1',
|
||||
'max-w-screen-2xl py-8',
|
||||
'mx-auto max-w-screen-2xl py-8',
|
||||
// Allow words to break if they are too long.
|
||||
'break-anywhere',
|
||||
pageWidthWide ? 'page-width-wide 3xl:px-8' : 'page-width-default',
|
||||
pageWidthWide ? 'page-width-wide 2xl:px-8' : 'page-width-default',
|
||||
siteWidthWide ? 'site-width-wide' : 'site-width-default',
|
||||
page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
|
||||
)}
|
||||
|
||||
@@ -5,10 +5,7 @@ import { type RevisionPageDocument, SiteVisibility } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import urlJoin from 'url-join';
|
||||
import { getPDFURLSearchParams } from '../PDF';
|
||||
import {
|
||||
PageActionsDropdown,
|
||||
type PageActionsDropdownURLs,
|
||||
} from '../PageActions/PageActionsDropdown';
|
||||
import { PageActionsDropdown } from '../PageActions/PageActionsDropdown';
|
||||
import { PageIcon } from '../PageIcon';
|
||||
import { StyledLink } from '../primitives';
|
||||
|
||||
@@ -43,7 +40,35 @@ export async function PageHeader(props: {
|
||||
// Show page actions if *any* of the actions are enabled
|
||||
<PageActionsDropdown
|
||||
siteTitle={context.site.title}
|
||||
urls={getPageActionsURLs(context, page)}
|
||||
markdownPageURL={`${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`}
|
||||
editOnGit={
|
||||
context.customization.git.showEditLink &&
|
||||
context.space.gitSync?.url &&
|
||||
page.git
|
||||
? {
|
||||
provider: context.space?.gitSync?.installationProvider,
|
||||
url: urlJoin(context.space.gitSync.url, page.git.path),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pdfURL={
|
||||
context.customization.pdf.enabled
|
||||
? context.linker.toPathInSpace(
|
||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
||||
page: page.id,
|
||||
only: true,
|
||||
limit: 100,
|
||||
}).toString()}`
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
mcpURL={
|
||||
context.site.visibility !== SiteVisibility.VisitorAuth
|
||||
? context.linker.toAbsoluteURL(
|
||||
context.linker.toPathInSpace('~gitbook/mcp')
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
actions={context.customization.pageActions}
|
||||
className={tcls(
|
||||
'float-right ml-4 xl:max-2xl:page-api-block:mr-62',
|
||||
@@ -121,38 +146,3 @@ export async function PageHeader(props: {
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URLs for the page actions.
|
||||
*/
|
||||
function getPageActionsURLs(
|
||||
context: GitBookSiteContext,
|
||||
page: RevisionPageDocument
|
||||
): PageActionsDropdownURLs {
|
||||
return {
|
||||
html: context.linker.toAbsoluteURL(
|
||||
context.linker.toPathForPage({ pages: context.revision.pages, page })
|
||||
),
|
||||
markdown: `${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`,
|
||||
editOnGit:
|
||||
context.customization.git.showEditLink && context.space.gitSync?.url && page.git
|
||||
? {
|
||||
provider: context.space?.gitSync?.installationProvider,
|
||||
url: urlJoin(context.space.gitSync.url, page.git.path),
|
||||
}
|
||||
: undefined,
|
||||
pdf: context.customization.pdf.enabled
|
||||
? context.linker.toPathInSpace(
|
||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
||||
page: page.id,
|
||||
only: true,
|
||||
limit: 100,
|
||||
}).toString()}`
|
||||
)
|
||||
: undefined,
|
||||
mcp:
|
||||
context.site.visibility !== SiteVisibility.VisitorAuth
|
||||
? context.linker.toAbsoluteURL(context.linker.toPathInSpace('~gitbook/mcp'))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
}
|
||||
rootProps={{
|
||||
open: Boolean(visible && (state?.open ?? false)),
|
||||
modal: false,
|
||||
modal: isMobile,
|
||||
}}
|
||||
contentProps={{
|
||||
onOpenAutoFocus: (event) => event.preventDefault(),
|
||||
|
||||
@@ -72,7 +72,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
||||
'has-[input:focus]:-translate-y-px h-9 grow cursor-pointer px-2.5 has-[input:focus]:bg-tint-base has-[input:focus]:depth-subtle:shadow-lg has-[input:focus]:depth-subtle:shadow-primary-subtle has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-primary-hover md:cursor-text',
|
||||
'theme-bold:border-header-link/3 has-[input:focus-visible]:theme-bold:border-header-link/5 has-[input:focus-visible]:theme-bold:bg-header-link/3 has-[input:focus-visible]:theme-bold:ring-header-link/5',
|
||||
'theme-bold:before:absolute theme-bold:before:inset-0 theme-bold:before:bg-header-background/7 theme-bold:before:backdrop-blur-xl ', // Special overlay to make the transparent colors of theme-bold visible.
|
||||
'relative z-30 max-w-none shrink grow justify-start max-md:absolute max-md:right-0',
|
||||
'relative z-30 shrink grow justify-start max-md:absolute max-md:right-0',
|
||||
isOpen ? 'max-md:w-56' : 'max-md:w-[38px]'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import type { SiteSection } from '@gitbook/api';
|
||||
import { Button, Checkbox, DropdownMenu, DropdownMenuItem } from '../primitives';
|
||||
import { SegmentedControl, SegmentedControlItem } from '../primitives/SegmentedControl';
|
||||
import { useSearch } from './useSearch';
|
||||
|
||||
@@ -25,106 +24,80 @@ export function SearchScopeToggle(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Whether to include all variants in the search
|
||||
const scopeIsExtended = ['extended', 'all'].includes(state.scope);
|
||||
|
||||
return (
|
||||
<>
|
||||
{withSections ? (
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<SegmentedControl className="grow animate-scale-in">
|
||||
<SegmentedControl className="animate-scale-in">
|
||||
{/* `Default` scope = current section's current variant + best match in other sections */}
|
||||
<SegmentedControlItem
|
||||
active={
|
||||
withSiteVariants
|
||||
? state.scope === 'default'
|
||||
: ['default', 'all'].includes(state.scope)
|
||||
}
|
||||
label={
|
||||
withSiteVariants
|
||||
? tString(language, 'search_scope_default')
|
||||
: tString(language, 'search_scope_all')
|
||||
}
|
||||
className={withSiteVariants ? '@max-md:basis-full' : ''}
|
||||
icon={withSiteVariants ? 'bullseye-arrow' : 'infinity'}
|
||||
onClick={() => setSearchState({ ...state, scope: 'default' })}
|
||||
/>
|
||||
|
||||
{/* `Current` scope = current section's current variant (with further variant scope selection if necessary) */}
|
||||
<SegmentedControlItem
|
||||
active={state.scope === 'current' || state.scope === 'extended'}
|
||||
icon={section?.icon ?? 'crosshairs'}
|
||||
label={tString(language, 'search_scope_current', section?.title)}
|
||||
onClick={() => setSearchState({ ...state, scope: 'current' })}
|
||||
/>
|
||||
|
||||
{/* `All` scope = all content on the site. Only visible if site has variants, otherwise it's the same as default */}
|
||||
{withSiteVariants ? (
|
||||
<SegmentedControlItem
|
||||
active={['default', 'all'].includes(state.scope)}
|
||||
active={state.scope === 'all'}
|
||||
label={tString(language, 'search_scope_all')}
|
||||
icon="infinity"
|
||||
onClick={() =>
|
||||
setSearchState({
|
||||
...state,
|
||||
scope: scopeIsExtended ? 'all' : 'default',
|
||||
})
|
||||
}
|
||||
onClick={() => setSearchState({ ...state, scope: 'all' })}
|
||||
/>
|
||||
<SegmentedControlItem
|
||||
active={['current', 'extended'].includes(state.scope)}
|
||||
icon={section?.icon}
|
||||
label={tString(language, 'search_scope_current', section?.title)}
|
||||
onClick={() =>
|
||||
setSearchState({
|
||||
...state,
|
||||
scope: scopeIsExtended ? 'extended' : 'current',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SegmentedControl>
|
||||
{withSiteVariants ? (
|
||||
<DropdownMenu
|
||||
button={
|
||||
<Button
|
||||
icon="ellipsis"
|
||||
iconOnly
|
||||
size="default"
|
||||
variant="blank"
|
||||
label={tString(language, 'more')}
|
||||
className="shrink-0"
|
||||
active={scopeIsExtended}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setSearchState({
|
||||
...state,
|
||||
scope: (() => {
|
||||
switch (state.scope) {
|
||||
case 'extended':
|
||||
return 'current';
|
||||
case 'current':
|
||||
return 'extended';
|
||||
case 'all':
|
||||
return 'default';
|
||||
default:
|
||||
return 'all';
|
||||
}
|
||||
})(),
|
||||
})
|
||||
}
|
||||
active={scopeIsExtended}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox checked={scopeIsExtended} />
|
||||
<div className="flex flex-col items-start">
|
||||
Include all variants
|
||||
<div className="text-tint-subtle text-xs">
|
||||
Search content in every language/version
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
) : withVariants ? (
|
||||
<SegmentedControl className="mb-2 animate-scale-in">
|
||||
</SegmentedControl>
|
||||
) : null}
|
||||
{withVariants &&
|
||||
(!withSections || state.scope === 'current' || state.scope === 'extended') ? (
|
||||
<SegmentedControl className="animate-scale-in">
|
||||
{/* `Current` scope = current section's current variant. `Default` on sites without sections. */}
|
||||
<SegmentedControlItem
|
||||
size={withSections ? 'small' : 'medium'}
|
||||
active={!scopeIsExtended}
|
||||
active={
|
||||
withSections
|
||||
? state.scope === 'current'
|
||||
: ['default', 'current'].includes(state.scope)
|
||||
}
|
||||
className="py-1"
|
||||
label={tString(language, 'search_scope_current', spaceTitle)}
|
||||
onClick={() =>
|
||||
setSearchState({
|
||||
...state,
|
||||
scope: 'default',
|
||||
scope: withSections ? 'current' : 'default',
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
{/* `Extended` scope = all variants of the current section. `All` on sites without sections. */}
|
||||
<SegmentedControlItem
|
||||
size={withSections ? 'small' : 'medium'}
|
||||
active={scopeIsExtended}
|
||||
active={
|
||||
withSections
|
||||
? state.scope === 'extended'
|
||||
: ['extended', 'all'].includes(state.scope)
|
||||
}
|
||||
className="py-1"
|
||||
icon="infinity"
|
||||
label={tString(language, 'search_scope_all')}
|
||||
onClick={() => setSearchState({ ...state, scope: 'all' })}
|
||||
label={tString(language, 'search_scope_extended')}
|
||||
onClick={() =>
|
||||
setSearchState({ ...state, scope: withSections ? 'extended' : 'all' })
|
||||
}
|
||||
/>
|
||||
</SegmentedControl>
|
||||
) : null}
|
||||
|
||||
@@ -22,17 +22,12 @@ export function SiteLayoutClientContexts(props: {
|
||||
useClearRouterCache(contextId);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
disableTransitionOnChange
|
||||
enableSystem
|
||||
forcedTheme={forcedTheme}
|
||||
>
|
||||
<NuqsAdapter>
|
||||
<NuqsAdapter>
|
||||
<ThemeProvider attribute="class" enableSystem forcedTheme={forcedTheme}>
|
||||
<LinkSettingsContext.Provider value={{ externalLinksTarget }}>
|
||||
<SearchContextProvider>{children}</SearchContextProvider>
|
||||
</LinkSettingsContext.Provider>
|
||||
</NuqsAdapter>
|
||||
</ThemeProvider>
|
||||
</ThemeProvider>
|
||||
</NuqsAdapter>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,42 +43,38 @@ export async function SitePage(props: SitePageProps) {
|
||||
|
||||
return (
|
||||
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
|
||||
{/* Using `contents` makes the children of this div according to its parent — which keeps them in a single flex row with the TOC by default.
|
||||
If there's a page cover, we use `flex flex-col` to lay out the PageCover above the PageBody + PageAside instead. */}
|
||||
<div className={withFullPageCover && page.cover ? 'flex grow flex-col' : 'contents'}>
|
||||
{withFullPageCover && page.cover ? (
|
||||
<PageCover as="full" page={page} cover={page.cover} context={context} />
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={tcls(
|
||||
withFullPageCover && page.cover ? 'flex grow flex-row' : 'contents',
|
||||
withSections
|
||||
? '[--content-scroll-margin:calc(var(--spacing)*27)]'
|
||||
: '[--content-scroll-margin:calc(var(--spacing)*16)]'
|
||||
)}
|
||||
>
|
||||
<PageAside
|
||||
page={page}
|
||||
document={document}
|
||||
withHeaderOffset={headerOffset}
|
||||
withFullPageCover={withFullPageCover}
|
||||
withPageFeedback={withPageFeedback}
|
||||
context={context}
|
||||
/>
|
||||
<PageBody
|
||||
context={context}
|
||||
page={page}
|
||||
ancestors={ancestors}
|
||||
document={document}
|
||||
withPageFeedback={withPageFeedback}
|
||||
insightsDisplayContext={SiteInsightsDisplayContext.Site}
|
||||
/>
|
||||
</div>
|
||||
<React.Suspense fallback={null}>
|
||||
<PageClientLayout />
|
||||
</React.Suspense>
|
||||
{withFullPageCover && page.cover ? (
|
||||
<PageCover as="full" page={page} cover={page.cover} context={context} />
|
||||
) : null}
|
||||
{/* We use a flex row reverse to render the aside first because the page is streamed. */}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex grow flex-row-reverse justify-end',
|
||||
withSections
|
||||
? '[--content-scroll-margin:calc(var(--spacing)*27)]'
|
||||
: '[--content-scroll-margin:calc(var(--spacing)*16)]'
|
||||
)}
|
||||
>
|
||||
<PageAside
|
||||
page={page}
|
||||
document={document}
|
||||
withHeaderOffset={headerOffset}
|
||||
withFullPageCover={withFullPageCover}
|
||||
withPageFeedback={withPageFeedback}
|
||||
context={context}
|
||||
/>
|
||||
<PageBody
|
||||
context={context}
|
||||
page={page}
|
||||
ancestors={ancestors}
|
||||
document={document}
|
||||
withPageFeedback={withPageFeedback}
|
||||
insightsDisplayContext={SiteInsightsDisplayContext.Site}
|
||||
/>
|
||||
</div>
|
||||
<React.Suspense fallback={null}>
|
||||
<PageClientLayout />
|
||||
</React.Suspense>
|
||||
</PageContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,6 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
'flex',
|
||||
'flex-col',
|
||||
'lg:flex-row',
|
||||
'lg:justify-center',
|
||||
CONTAINER_STYLE,
|
||||
'site-width-wide:max-w-full',
|
||||
|
||||
@@ -224,7 +223,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{children}
|
||||
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -59,11 +59,6 @@ export const NavigationStatusProvider: React.FC<React.PropsWithChildren> = ({ ch
|
||||
}, []);
|
||||
|
||||
const onNavigationClick = React.useCallback((href: string) => {
|
||||
// We need to skip it for search like params (i.e. ?ask= or ?q=) because they don't really trigger a navigation
|
||||
// Search may trigger a navigation whenn clicking on the ask ai for example, this is not something we want to track here
|
||||
if (href.startsWith('?') || href.startsWith('#')) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(
|
||||
href,
|
||||
typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
||||
|
||||
@@ -145,9 +145,7 @@ export const Button = React.forwardRef<
|
||||
icon
|
||||
)
|
||||
) : null}
|
||||
{iconOnly || (!children && !label) ? null : (
|
||||
<span className="button-content truncate">{children ?? label}</span>
|
||||
)}
|
||||
{iconOnly ? null : (children ?? label)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export function DropdownMenu(props: {
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
align={align}
|
||||
side={side}
|
||||
className="z-50 animate-scale-in border-tint pt-2"
|
||||
className="z-40 animate-scale-in border-tint pt-2"
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
@@ -214,7 +214,7 @@ export function DropdownSubMenu(props: { children: React.ReactNode; label: React
|
||||
<RadixDropdownMenu.SubContent
|
||||
hideWhenDetached
|
||||
collisionPadding={8}
|
||||
className="z-50 animate-present"
|
||||
className="z-40 animate-present"
|
||||
>
|
||||
<div className="flex max-h-96 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle sm:min-w-52 sm:max-w-80">
|
||||
{children}
|
||||
|
||||
@@ -80,8 +80,7 @@ export const Link = React.forwardRef(function Link(
|
||||
|
||||
const onClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
const isExternalWithOrigin = isExternalLink(href, window.location.origin);
|
||||
// Only trigger navigation context for internal links without modifier keys (i.e. open in new tab).
|
||||
if (!isExternal && !event.ctrlKey && !event.metaKey) {
|
||||
if (!isExternal) {
|
||||
onNavigationClick(href);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export function SegmentedControl(props: { children: React.ReactNode; className?:
|
||||
role="toolbar"
|
||||
aria-orientation="horizontal"
|
||||
className={tcls(
|
||||
'flex flex-wrap gap-1 circular-corners:rounded-3xl rounded-corners:rounded-lg bg-tint p-1',
|
||||
'mb-2 flex flex-wrap gap-1 circular-corners:rounded-3xl rounded-corners:rounded-lg bg-tint p-1',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -35,7 +35,6 @@ export const ButtonStyles = [
|
||||
'grow-0',
|
||||
'shrink-0',
|
||||
'truncate',
|
||||
'max-w-full',
|
||||
|
||||
'disabled:cursor-not-allowed',
|
||||
'disabled:translate-y-0!',
|
||||
|
||||
@@ -17,9 +17,9 @@ export const de = {
|
||||
search_section_result_content: 'Abschnitt mit Inhalt ${1}',
|
||||
search_section_result_default: 'Abschnitt',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'Überall',
|
||||
search_scope_include_all_title: 'Alle Varianten einschließen',
|
||||
search_scope_include_all_description: 'Inhalt in jeder Sprache/Version durchsuchen',
|
||||
search_scope_extended: 'Alle Inhalte',
|
||||
search_scope_default: 'Beste Match',
|
||||
search_scope_all: 'Alles',
|
||||
ask: 'Fragen',
|
||||
search_ask: 'Fragen "${1}"',
|
||||
search_ask_description: 'Finden Sie die Antwort mit ${1}',
|
||||
|
||||
@@ -17,9 +17,9 @@ export const en = {
|
||||
search_section_result_content: 'Section with content ${1}',
|
||||
search_section_result_default: 'Section',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'Everywhere',
|
||||
search_scope_include_all_title: 'Include all variants',
|
||||
search_scope_include_all_description: 'Search content in every language/version',
|
||||
search_scope_extended: 'All content',
|
||||
search_scope_default: 'Best match',
|
||||
search_scope_all: 'Everything',
|
||||
ask: 'Ask',
|
||||
search_ask: 'Ask "${1}"',
|
||||
search_ask_description: 'Find the answer with ${1}',
|
||||
|
||||
@@ -19,9 +19,9 @@ export const es: TranslationLanguage = {
|
||||
search_section_result_content: 'Sección con contenido ${1}',
|
||||
search_section_result_default: 'Sección',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'En todas partes',
|
||||
search_scope_include_all_title: 'Incluir todas las variantes',
|
||||
search_scope_include_all_description: 'Buscar contenido en todos los idiomas/versiones',
|
||||
search_scope_extended: 'Todo el contenido',
|
||||
search_scope_default: 'Mejor match',
|
||||
search_scope_all: 'Todo',
|
||||
ask: 'Preguntar',
|
||||
search_ask: 'Preguntar "${1}"',
|
||||
search_ask_description: 'Encuentra la respuesta con ${1}',
|
||||
|
||||
@@ -17,9 +17,9 @@ export const fr = {
|
||||
search_section_result_content: 'Section avec contenu ${1}',
|
||||
search_section_result_default: 'Section',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'Partout',
|
||||
search_scope_include_all_title: 'Inclure toutes les variantes',
|
||||
search_scope_include_all_description: 'Rechercher le contenu dans toutes les langues/versions',
|
||||
search_scope_extended: 'Tous les contenus',
|
||||
search_scope_default: 'Meilleur match',
|
||||
search_scope_all: 'Tout',
|
||||
ask: 'Poser une question',
|
||||
search_ask: 'Demander « ${1} »',
|
||||
search_ask_description: 'Trouvez la réponse avec ${1}',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { de } from './de';
|
||||
import { en } from './en';
|
||||
import { es } from './es';
|
||||
import { fr } from './fr';
|
||||
import { it } from './it';
|
||||
import { ja } from './ja';
|
||||
import { nl } from './nl';
|
||||
import { no } from './no';
|
||||
@@ -16,9 +15,8 @@ export * from './types';
|
||||
export const languages = {
|
||||
de,
|
||||
en,
|
||||
es,
|
||||
fr,
|
||||
it,
|
||||
es,
|
||||
zh,
|
||||
ja,
|
||||
nl,
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import type { TranslationLanguage } from './types';
|
||||
|
||||
export const it: TranslationLanguage = {
|
||||
locale: 'it',
|
||||
powered_by_gitbook: 'Offerto da GitBook',
|
||||
sponsored_via_gitbook: 'Sponsorizzato tramite GitBook',
|
||||
switch_to_dark_theme: 'Passa al tema scuro',
|
||||
switch_to_light_theme: 'Passa al tema chiaro',
|
||||
switch_to_system_theme: 'Passa al tema di sistema',
|
||||
search: 'Cerca',
|
||||
search_or_ask: 'Chiedi o cerca',
|
||||
search_input_placeholder: 'Cerca contenuti',
|
||||
search_ask_input_placeholder: 'Cerca contenuti o fai una domanda',
|
||||
search_no_results_for: 'Nessun risultato per "${1}".',
|
||||
search_no_results: 'Nessun risultato',
|
||||
search_results_count: '${1} risultati',
|
||||
search_page_result_title: 'Pagina con titolo ${1}',
|
||||
search_section_result_title: 'Sezione con titolo ${1}',
|
||||
search_section_result_content: 'Sezione con contenuto ${1}',
|
||||
search_section_result_default: 'Sezione',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'Ovunque',
|
||||
search_scope_include_all_title: 'Includi tutte le varianti',
|
||||
search_scope_include_all_description: 'Cerca contenuti in ogni lingua/versione',
|
||||
ask: 'Chiedi',
|
||||
search_ask: 'Chiedi "${1}"',
|
||||
search_ask_description: 'Trova la risposta con ${1}',
|
||||
search_ask_sources: 'Fonti',
|
||||
search_ask_sources_no_answer: 'Pagine correlate',
|
||||
search_ask_no_answer:
|
||||
'Non è stato possibile trovare una risposta alla tua domanda. Prova a riformularla o a essere più specifico.',
|
||||
search_ask_error: 'Qualcosa è andato storto. Riprova più tardi.',
|
||||
on_this_page: 'In questa pagina',
|
||||
next_page: 'Successivo',
|
||||
previous_page: 'Precedente',
|
||||
page_last_modified: 'Ultimo aggiornamento ${1}',
|
||||
was_this_helpful: 'È stato utile?',
|
||||
was_this_helpful_positive: 'Sì, lo è stato!',
|
||||
was_this_helpful_neutral: 'Non sono sicuro',
|
||||
was_this_helpful_negative: 'No',
|
||||
was_this_helpful_thank_you: 'Grazie!',
|
||||
was_this_helpful_comment: "Vuoi aggiungere qualcos'altro?",
|
||||
was_this_helpful_positive_label: 'Utile',
|
||||
was_this_helpful_negative_label: 'Non utile',
|
||||
submit: 'Invia',
|
||||
annotation_button_label: 'Apri annotazione',
|
||||
code_copied: 'Copiato!',
|
||||
code_copy: 'Copia',
|
||||
table_of_contents_button_label: 'Apri indice dei contenuti',
|
||||
cookies_title: 'Cookie',
|
||||
cookies_prompt:
|
||||
'Questo sito utilizza i cookie per fornire il servizio e analizzare il traffico. Navigando su questo sito accetti la ${1}.',
|
||||
cookies_prompt_privacy: 'informativa sulla privacy',
|
||||
cookies_accept: 'Accetta',
|
||||
cookies_reject: 'Rifiuta',
|
||||
close: 'Chiudi',
|
||||
view: 'Visualizza',
|
||||
edit_on_git: 'Modifica su ${1}',
|
||||
edit: 'Modifica',
|
||||
notfound_title: 'Pagina non trovata',
|
||||
notfound_link: 'Questo link punta a una pagina che è stata rimossa o non esiste più.',
|
||||
notfound: 'La pagina che cerchi non esiste.',
|
||||
unexpected_error_title: 'Si è verificato un errore',
|
||||
unexpected_error: 'Spiacenti, si è verificato un errore imprevisto. Riprova più tardi.',
|
||||
unexpected_error_retry: 'Riprova',
|
||||
pdf_download: 'Esporta in PDF',
|
||||
pdf_goback: 'Torna al contenuto',
|
||||
pdf_print: 'Stampa o salva come PDF',
|
||||
pdf_page_of: '${1} di ${2}',
|
||||
pdf_mode_only_page: 'Solo questa pagina',
|
||||
pdf_mode_all: 'Tutte le pagine',
|
||||
pdf_limit_reached:
|
||||
'Impossibile generare il PDF per ${1} pagine, la generazione si è fermata a ${2}.',
|
||||
pdf_limit_reached_continue: 'Estendi con altre ${1} pagine.',
|
||||
more: 'Altro',
|
||||
link_tooltip_external_link: 'Link esterno a',
|
||||
link_tooltip_email: "Invia un'email a",
|
||||
link_tooltip_page_anchor: 'Vai alla sezione',
|
||||
open_in_new_tab: 'Apri in una nuova scheda',
|
||||
ai_chat_assistant_name: 'Assistente GitBook',
|
||||
ai_chat_assistant_name_unbranded: 'Assistente AI',
|
||||
ai_chat_assistant_description: 'Sono qui per aiutarti con la documentazione.',
|
||||
ai_chat_assistant_greeting_morning: 'Buongiorno',
|
||||
ai_chat_assistant_greeting_afternoon: 'Buon pomeriggio',
|
||||
ai_chat_assistant_greeting_evening: 'Buonasera',
|
||||
ai_chat_assistant_greeting_night: 'Buonanotte',
|
||||
ai_chat_clear_conversation: 'Cancella conversazione',
|
||||
ai_chat_thinking: 'Sto pensando...',
|
||||
ai_chat_working: 'Sto lavorando...',
|
||||
ai_chat_context_badge: 'IA',
|
||||
ai_chat_error: 'Qualcosa è andato storto.',
|
||||
ai_chat_hold_message_1: 'Ci sto pensando…',
|
||||
ai_chat_hold_message_2: 'Un momento…',
|
||||
ai_chat_context_title: 'Basato sul tuo contesto',
|
||||
ai_chat_context_description:
|
||||
"L'assistente usa l'IA e il tuo contesto per generare risposte ed eseguire azioni.",
|
||||
ai_chat_context_pages_youve_read: 'Pagine che hai letto',
|
||||
ai_chat_context_info_provided_by_the_site: 'Informazioni fornite dal sito',
|
||||
ai_chat_context_previous_messages: 'Messaggi precedenti',
|
||||
ai_chat_context_disclaimer: "Le risposte dell'IA possono contenere errori.",
|
||||
ai_chat_input_placeholder: 'Chiedi, cerca o spiega...',
|
||||
send: 'Invia',
|
||||
actions: 'Azioni',
|
||||
ai_chat_suggested_questions_title: 'Domande suggerite',
|
||||
ai_chat_suggested_questions_about_this_page: 'Di cosa parla questa pagina?',
|
||||
ai_chat_suggested_questions_read_next: 'Cosa dovrei leggere dopo?',
|
||||
ai_chat_suggested_questions_example: 'Puoi fare un esempio?',
|
||||
searched_for: 'Ricerca per ${1}',
|
||||
ai_chat_tools_listed_pages: 'Ha esplorato la documentazione',
|
||||
ai_chat_tools_read_page: 'Ha letto la pagina ${1}',
|
||||
ai_chat_tools_mcp_tool: 'Ha chiamato ${1}',
|
||||
ai_chat_ask: 'Chiedi a ${1}',
|
||||
ai_chat_ask_about_page: 'Chiedi a ${1} di questa pagina',
|
||||
copy_for_llms: 'Copia per LLM',
|
||||
copy_page_markdown: 'Copia la pagina come Markdown per LLM',
|
||||
copy_page: 'Copia pagina',
|
||||
view_page_markdown: 'Visualizza come Markdown',
|
||||
view_page_plaintext: 'Visualizza questa pagina come testo semplice',
|
||||
open_in: 'Apri in ${1}',
|
||||
open_in_llms_pre_prompt: 'Leggi ${1} e rispondi alle domande sul contenuto.',
|
||||
connect_mcp_to: 'Connetti a ${1}',
|
||||
install_mcp_on: 'Installa il server MCP su ${1}',
|
||||
connect_with_mcp: 'Connettiti con MCP',
|
||||
copy_mcp_url: "Copia l'URL del server MCP",
|
||||
press_to_confirm: 'Premi ${1} per confermare',
|
||||
tool_call_skipped: 'Saltato "${1}"',
|
||||
scroll_back: 'Scorri indietro',
|
||||
scroll_further: 'Scorri ancora',
|
||||
hint_info: 'Informazioni',
|
||||
hint_warning: 'Avviso',
|
||||
hint_danger: 'Pericolo',
|
||||
hint_success: 'Successo',
|
||||
};
|
||||
@@ -19,9 +19,9 @@ export const ja: TranslationLanguage = {
|
||||
search_section_result_content: 'コンテンツ「${1}」のセクション',
|
||||
search_section_result_default: 'セクション',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'どこでも',
|
||||
search_scope_include_all_title: 'すべてのバリエーションを含める',
|
||||
search_scope_include_all_description: 'すべての言語/バージョンでコンテンツを検索',
|
||||
search_scope_extended: '全てのコンテンツ',
|
||||
search_scope_default: '最適なマッチ',
|
||||
search_scope_all: 'すべて',
|
||||
ask: '質問する',
|
||||
search_ask: '"${1}" を質問する',
|
||||
search_ask_description: '${1}で答えを見つける',
|
||||
|
||||
@@ -19,9 +19,9 @@ export const nl: TranslationLanguage = {
|
||||
search_section_result_content: 'Sectie met inhoud ${1}',
|
||||
search_section_result_default: 'Sectie',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'Overal',
|
||||
search_scope_include_all_title: 'Alle varianten opnemen',
|
||||
search_scope_include_all_description: 'Zoek inhoud in elke taal/versie',
|
||||
search_scope_extended: 'Alle inhoud',
|
||||
search_scope_default: 'Beste match',
|
||||
search_scope_all: 'Alles',
|
||||
ask: 'Vragen',
|
||||
search_ask: 'Vraag "${1}"',
|
||||
search_ask_description: 'Vind het antwoord met ${1}',
|
||||
|
||||
@@ -19,9 +19,9 @@ export const no: TranslationLanguage = {
|
||||
search_section_result_content: 'Seksjon med innhold ${1}',
|
||||
search_section_result_default: 'Seksjon',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'Overalt',
|
||||
search_scope_include_all_title: 'Inkluder alle varianter',
|
||||
search_scope_include_all_description: 'Søk innhold på alle språk/versjoner',
|
||||
search_scope_extended: 'Alt innhold',
|
||||
search_scope_default: 'Beste treff',
|
||||
search_scope_all: 'Alt',
|
||||
ask: 'Spør',
|
||||
search_ask: 'Spør "${1}"',
|
||||
search_ask_description: 'Finn svaret med ${1}',
|
||||
|
||||
@@ -17,9 +17,9 @@ export const pt_br = {
|
||||
search_section_result_content: 'Seção com conteúdo ${1}',
|
||||
search_section_result_default: 'Seção',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'Em todos os lugares',
|
||||
search_scope_include_all_title: 'Incluir todas as variantes',
|
||||
search_scope_include_all_description: 'Buscar conteúdo em todos os idiomas/versões',
|
||||
search_scope_extended: 'Todo o conteúdo',
|
||||
search_scope_default: 'Melhor match',
|
||||
search_scope_all: 'Tudo',
|
||||
ask: 'Perguntar',
|
||||
search_ask: 'Perguntar "${1}"',
|
||||
search_ask_description: 'Encontre a resposta com ${1}',
|
||||
|
||||
@@ -17,9 +17,9 @@ export const ru = {
|
||||
search_section_result_content: 'Раздел с контентом ${1}',
|
||||
search_section_result_default: 'Раздел',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: 'Везде',
|
||||
search_scope_include_all_title: 'Включить все варианты',
|
||||
search_scope_include_all_description: 'Искать контент на всех языках/версиях',
|
||||
search_scope_extended: 'Все материалы',
|
||||
search_scope_default: 'Лучший матч',
|
||||
search_scope_all: 'Всё',
|
||||
ask: 'Спросить',
|
||||
search_ask: 'Спросить "${1}"',
|
||||
search_ask_description: 'Найти ответ с помощью ${1}',
|
||||
|
||||
@@ -19,9 +19,9 @@ export const zh: TranslationLanguage = {
|
||||
search_section_result_content: '内容为"${1}"的章节',
|
||||
search_section_result_default: '章节',
|
||||
search_scope_current: '${1}',
|
||||
search_scope_all: '任何地方',
|
||||
search_scope_include_all_title: '包含所有变体',
|
||||
search_scope_include_all_description: '在所有语言/版本中搜索内容',
|
||||
search_scope_extended: '所有内容',
|
||||
search_scope_default: '最佳匹配',
|
||||
search_scope_all: '全部',
|
||||
ask: '询问',
|
||||
search_ask: '询问"${1}"',
|
||||
search_ask_description: '利用${1}找到答案',
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { checkIsSecurityError } from './security-error';
|
||||
|
||||
/**
|
||||
* Reusable function to read from any kind of Storage
|
||||
* Get an item from local storage safely.
|
||||
*/
|
||||
function readStorage<T>(storage: Storage | undefined, key: string, defaultValue: T): T {
|
||||
export function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
||||
try {
|
||||
if (storage && 'getItem' in storage) {
|
||||
const stored = storage.getItem(key);
|
||||
return stored !== null ? (JSON.parse(stored) as T) : defaultValue;
|
||||
if (typeof localStorage !== 'undefined' && localStorage && 'getItem' in localStorage) {
|
||||
const stored = localStorage.getItem(key);
|
||||
return stored ? (JSON.parse(stored) as T) : defaultValue;
|
||||
}
|
||||
return defaultValue;
|
||||
} catch (error) {
|
||||
@@ -18,74 +18,18 @@ function readStorage<T>(storage: Storage | undefined, key: string, defaultValue:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable function to write to any kind of Storage
|
||||
*/
|
||||
function writeStorage(storage: Storage | undefined, key: string, value: unknown) {
|
||||
try {
|
||||
if (storage && 'setItem' in storage) {
|
||||
storage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
} catch (error) {
|
||||
if (checkIsSecurityError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function removeStorage(storage: Storage | undefined, key: string) {
|
||||
try {
|
||||
if (storage && 'removeItem' in storage) {
|
||||
storage.removeItem(key);
|
||||
}
|
||||
} catch (error) {
|
||||
if (checkIsSecurityError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from local storage safely.
|
||||
*/
|
||||
export function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
||||
return readStorage(
|
||||
typeof localStorage !== 'undefined' ? localStorage : undefined,
|
||||
key,
|
||||
defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an item in local storage safely.
|
||||
*/
|
||||
export function setLocalStorageItem(key: string, value: unknown) {
|
||||
writeStorage(typeof localStorage !== 'undefined' ? localStorage : undefined, key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from session storage safely.
|
||||
*/
|
||||
export function getSessionStorageItem<T>(key: string, defaultValue: T): T {
|
||||
return readStorage(
|
||||
typeof sessionStorage !== 'undefined' ? sessionStorage : undefined,
|
||||
key,
|
||||
defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an item in session storage safely.
|
||||
*/
|
||||
export function setSessionStorageItem(key: string, value: unknown) {
|
||||
writeStorage(typeof sessionStorage !== 'undefined' ? sessionStorage : undefined, key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from session storage safely.
|
||||
*/
|
||||
export function removeSessionStorageItem(key: string) {
|
||||
removeStorage(typeof sessionStorage !== 'undefined' ? sessionStorage : undefined, key);
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined' && localStorage && 'setItem' in localStorage) {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
} catch (error) {
|
||||
if (checkIsSecurityError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ const getRevisionPageDocument = cache(
|
||||
params.revisionId,
|
||||
params.pageId,
|
||||
{
|
||||
evaluated: 'deterministic-only',
|
||||
evaluated: true,
|
||||
},
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
@@ -377,7 +377,7 @@ const getRevisionReusableContentDocument = cache(
|
||||
params.revisionId,
|
||||
params.reusableContentId,
|
||||
{
|
||||
evaluated: 'deterministic-only',
|
||||
evaluated: true,
|
||||
},
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { GitBookAnyContext } from '@/lib/context';
|
||||
import type { DocumentBlock, JSONDocument } from '@gitbook/api';
|
||||
|
||||
import { getDataOrNull } from './data';
|
||||
import { getNodeText } from './document';
|
||||
import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock';
|
||||
import { resolveOpenAPISchemasBlock } from './openapi/resolveOpenAPISchemasBlock';
|
||||
import { resolveContentRef } from './references';
|
||||
|
||||
export interface DocumentSection {
|
||||
id: string;
|
||||
@@ -30,11 +28,10 @@ export async function getDocumentSections(
|
||||
*/
|
||||
async function getSectionsFromNodes(
|
||||
nodes: DocumentBlock[],
|
||||
context: GitBookAnyContext,
|
||||
initialDepth = 0
|
||||
context: GitBookAnyContext
|
||||
): Promise<DocumentSection[]> {
|
||||
const sections: DocumentSection[] = [];
|
||||
let depth = initialDepth;
|
||||
let depth = 0;
|
||||
|
||||
for (const block of nodes) {
|
||||
switch (block.type) {
|
||||
@@ -67,9 +64,7 @@ async function getSectionsFromNodes(
|
||||
}
|
||||
case 'stepper': {
|
||||
const stepNodes = await Promise.all(
|
||||
block.nodes.map(async (step) =>
|
||||
getSectionsFromNodes(step.nodes, context, depth)
|
||||
)
|
||||
block.nodes.map(async (step) => getSectionsFromNodes(step.nodes, context))
|
||||
);
|
||||
for (const stepSections of stepNodes) {
|
||||
sections.push(...stepSections);
|
||||
@@ -121,39 +116,6 @@ async function getSectionsFromNodes(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 'reusable-content': {
|
||||
const dataFetcher = block.meta?.token
|
||||
? context.dataFetcher.withToken({ apiToken: block.meta.token })
|
||||
: context.dataFetcher;
|
||||
|
||||
const resolved = await resolveContentRef(block.data.ref, {
|
||||
...context,
|
||||
dataFetcher,
|
||||
});
|
||||
if (!resolved) {
|
||||
continue;
|
||||
}
|
||||
const { reusableContent } = resolved;
|
||||
if (!reusableContent) {
|
||||
continue;
|
||||
}
|
||||
const document = await getDataOrNull(
|
||||
dataFetcher.getRevisionReusableContentDocument({
|
||||
spaceId: reusableContent.context.space.id,
|
||||
revisionId: reusableContent.context.revisionId,
|
||||
reusableContentId: reusableContent.revisionReusableContent.id,
|
||||
})
|
||||
);
|
||||
if (!document) {
|
||||
continue;
|
||||
}
|
||||
const reusableContentSections = await getSectionsFromNodes(
|
||||
document.nodes,
|
||||
reusableContent.context,
|
||||
depth
|
||||
);
|
||||
sections.push(...reusableContentSections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ export type ResponseCookie = {
|
||||
sameSite: boolean | 'lax' | 'strict' | 'none' | undefined;
|
||||
secure: boolean;
|
||||
maxAge: number;
|
||||
path: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
@@ -455,7 +454,7 @@ export function serveVisitorClaimsDataRequest(request: NextRequest, siteRequestU
|
||||
});
|
||||
|
||||
if (!visitorToken && !Object.keys(unsignedClaims).length) {
|
||||
return NextResponse.json({ visitor: { claims: { unsigned: {} } } });
|
||||
return NextResponse.json({});
|
||||
}
|
||||
|
||||
const visitorClaims = {
|
||||
|
||||
@@ -296,41 +296,16 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(stableSiteURLData));
|
||||
|
||||
// Preview of customization/theme
|
||||
const customizationCookie = request.cookies.get(MiddlewareHeaders.Customization);
|
||||
const customization =
|
||||
siteRequestURL.searchParams.get('customization') ??
|
||||
(customizationCookie ? decodeURIComponent(customizationCookie.value) : undefined);
|
||||
const customization = siteRequestURL.searchParams.get('customization');
|
||||
if (customization && validateSerializedCustomization(customization)) {
|
||||
routeType = 'dynamic';
|
||||
// We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel.
|
||||
requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
|
||||
cookies.push({
|
||||
name: MiddlewareHeaders.Customization,
|
||||
value: encodeURIComponent(customization),
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 10 * 60, // 10 minutes
|
||||
path: '/url/preview', // Only send the cookie to preview routes
|
||||
},
|
||||
});
|
||||
}
|
||||
const theme =
|
||||
siteRequestURL.searchParams.get('theme') ??
|
||||
request.cookies.get(MiddlewareHeaders.Theme)?.value;
|
||||
const theme = siteRequestURL.searchParams.get('theme');
|
||||
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
|
||||
routeType = 'dynamic';
|
||||
requestHeaders.set(MiddlewareHeaders.Theme, theme);
|
||||
cookies.push({
|
||||
name: MiddlewareHeaders.Theme,
|
||||
value: theme,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 10 * 60, // 10 minutes
|
||||
path: '/url/preview', // Only send the cookie to preview routes
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// We support forcing dynamic routes by setting a `gitbook-dynamic-route` cookie
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @gitbook/icons
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"default": "./src/style.css"
|
||||
}
|
||||
},
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0"
|
||||
@@ -44,9 +44,5 @@
|
||||
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @gitbook/openapi-parser
|
||||
|
||||
## 3.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 295f03d: Republish packages
|
||||
|
||||
## 3.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@scalar/json-magic": "^0.3.1",
|
||||
@@ -30,9 +30,5 @@
|
||||
"dev": "bun run build -- --watch",
|
||||
"clean": "rm -rf ./dist"
|
||||
},
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user