mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-15 23:25:16 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38226b9413 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Simplify AI Chat
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix robots.txt preventing favicon from being indexed.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Show a toolbar for authenticated users with access to the current site.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Scope search across sections and variants
|
||||||
@@ -54,7 +54,7 @@ After forking this repository, you'll want to [create a branch](https://docs.git
|
|||||||
#### 3. Install dependencies and run the project locally
|
#### 3. Install dependencies and run the project locally
|
||||||
|
|
||||||
##### Prerequisites:
|
##### Prerequisites:
|
||||||
- Node.js (Version: >= 22.3)
|
- Node.js (Version: >=20.6)
|
||||||
- Use `nvm` for easy Node management
|
- Use `nvm` for easy Node management
|
||||||
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
||||||
- We use a text-based lockfile which isn't supported below 1.2.15
|
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
env:
|
env:
|
||||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||||
jobs:
|
jobs:
|
||||||
format:
|
format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
name: Build (Open Source)
|
name: Build (Open Source)
|
||||||
timeout-minutes: 6
|
timeout-minutes: 6
|
||||||
env:
|
env:
|
||||||
BUN_NPM_TOKEN: ''
|
NPM_TOKEN_READONLY: ''
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
env:
|
env:
|
||||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||||
jobs:
|
jobs:
|
||||||
deploy-v2-vercel:
|
deploy-v2-vercel:
|
||||||
name: Deploy v2 to Vercel (preview)
|
name: Deploy v2 to Vercel (preview)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
env:
|
env:
|
||||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||||
jobs:
|
jobs:
|
||||||
deploy-v2-vercel:
|
deploy-v2-vercel:
|
||||||
name: Deploy v2 to Vercel (production)
|
name: Deploy v2 to Vercel (production)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
env:
|
env:
|
||||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN_READONLY }}
|
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||||
jobs:
|
jobs:
|
||||||
deploy-v2-vercel:
|
deploy-v2-vercel:
|
||||||
name: Deploy v2 to Vercel (staging)
|
name: Deploy v2 to Vercel (staging)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUN_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ jobs:
|
|||||||
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
|
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
|
||||||
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,3 @@ yarn-error.log*
|
|||||||
|
|
||||||
# TypeScript
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.npmrc
|
|
||||||
@@ -35,7 +35,7 @@ To run a local version of this project, please follow these simple steps.
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Node.js (Version: >= 22.3)
|
- Node.js (Version: >=20.6)
|
||||||
- Use nvm for easy Node management
|
- Use nvm for easy Node management
|
||||||
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
- [Bun](https://bun.sh/) (Version: >=1.2.15)
|
||||||
- We use a text-based lockfile which isn't supported below 1.2.15
|
- We use a text-based lockfile which isn't supported below 1.2.15
|
||||||
@@ -90,7 +90,7 @@ If you are GitBook staff, you'll need our NPM token in your local environment.
|
|||||||
```
|
```
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
BUN_NPM_TOKEN=xxx
|
NPM_TOKEN_READONLY=xxx
|
||||||
```
|
```
|
||||||
|
|
||||||
and then reinstall dependencies.
|
and then reinstall dependencies.
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
[install.scopes]
|
[install.scopes]
|
||||||
"gitbook" = { token = "$BUN_NPM_TOKEN", url = "https://registry.npmjs.org" }
|
"gitbook" = { token = "$NPM_TOKEN_READONLY", url = "https://registry.npmjs.org" }
|
||||||
|
|||||||
+10
-19
@@ -3,20 +3,20 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@changesets/cli": "^2.29.7",
|
"@changesets/cli": "^2.27.12",
|
||||||
"turbo": "^2.5.8",
|
"turbo": "^2.5.0",
|
||||||
"vercel": "^39.4.2"
|
"vercel": "^39.3.0"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.2.15",
|
"packageManager": "bun@1.2.15",
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@codemirror/state": "6.4.1",
|
"@codemirror/state": "6.4.1",
|
||||||
"react": "catalog:",
|
"react": "^19.0.0",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "^19.0.0",
|
||||||
"esbuild": "0.24.2"
|
"esbuild": "0.24.2"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo run dev --concurrency 20",
|
"dev": "turbo run dev",
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
|
||||||
"typecheck": "turbo run typecheck",
|
"typecheck": "turbo run typecheck",
|
||||||
@@ -26,25 +26,16 @@
|
|||||||
"e2e": "turbo run e2e",
|
"e2e": "turbo run e2e",
|
||||||
"e2e-customers": "turbo run e2e-customers",
|
"e2e-customers": "turbo run e2e-customers",
|
||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"changeset-version": "changeset version && bun run format && bun update",
|
"changeset-version": "changeset version && bun run format",
|
||||||
"release": "turbo run release && bun run publish-all-packages",
|
"release": "turbo run release && changeset publish",
|
||||||
"publish-all-packages": "for dir in packages/*; do (cd \"$dir\" && bun publish || true); done && changeset tag",
|
|
||||||
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
|
||||||
"clean": "turbo run clean"
|
"clean": "turbo run clean"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": ["packages/*"],
|
"packages": ["packages/*"],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@gitbook/api": "0.143.2",
|
"@gitbook/api": "^0.141.0",
|
||||||
"assert-never": "^1.4.0",
|
"bidc": "^0.0.2"
|
||||||
"bidc": "^0.0.2",
|
|
||||||
"bun-types": "^1.1.20",
|
|
||||||
"classnames": "^2.5.1",
|
|
||||||
"react": "^19.0.0",
|
|
||||||
"react-dom": "^19.0.0",
|
|
||||||
"tsdown": "^0.15.6",
|
|
||||||
"typescript": "^5.5.3",
|
|
||||||
"usehooks-ts": "^3.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
# @gitbook/browser-types
|
# @gitbook/browser-types
|
||||||
|
|
||||||
## 0.1.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
|
||||||
- Updated dependencies [6142d6b]
|
|
||||||
- @gitbook/icons@0.3.3
|
|
||||||
|
|
||||||
## 0.1.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 295f03d: Republish packages
|
|
||||||
- Updated dependencies [295f03d]
|
|
||||||
- @gitbook/icons@0.3.2
|
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -8,24 +8,17 @@
|
|||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"version": "0.1.0",
|
||||||
"version": "0.1.2",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "catalog:",
|
"@gitbook/api": "catalog:",
|
||||||
"@gitbook/icons": "workspace:"
|
"@gitbook/icons": "workspace:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsdown": "catalog:",
|
"typescript": "^5.5.3"
|
||||||
"typescript": "catalog:"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown",
|
"build": "tsc",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit"
|
||||||
"dev": "bun run build -- --watch ./src"
|
|
||||||
},
|
},
|
||||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"registry": "https://registry.npmjs.org/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
# @gitbook/cache-tags
|
# @gitbook/cache-tags
|
||||||
|
|
||||||
## 0.3.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
|
||||||
|
|
||||||
## 0.3.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 295f03d: Republish packages
|
|
||||||
|
|
||||||
## 0.3.1
|
## 0.3.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -4,27 +4,22 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"version": "0.3.1",
|
||||||
"version": "0.3.3",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "catalog:",
|
"@gitbook/api": "catalog:",
|
||||||
"assert-never": "catalog:"
|
"assert-never": "^1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsdown": "catalog:",
|
"typescript": "^5.5.3"
|
||||||
"typescript": "catalog:"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown",
|
"build": "tsc",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"dev": "bun run build -- --watch ./src"
|
"dev": "tsc -w"
|
||||||
},
|
},
|
||||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"registry": "https://registry.npmjs.org/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
# @gitbook/colors
|
# @gitbook/colors
|
||||||
|
|
||||||
## 0.4.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
|
||||||
|
|
||||||
## 0.4.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 295f03d: Republish packages
|
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -4,23 +4,18 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"version": "0.4.0",
|
||||||
"version": "0.4.2",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsdown": "catalog:",
|
"typescript": "^5.5.3"
|
||||||
"typescript": "catalog:"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown",
|
"build": "tsc",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"dev": "bun run build -- --watch ./src"
|
"dev": "tsc -w"
|
||||||
},
|
},
|
||||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"registry": "https://registry.npmjs.org/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,5 @@
|
|||||||
# @gitbook/embed
|
# @gitbook/embed
|
||||||
|
|
||||||
## 0.1.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
|
||||||
- Updated dependencies [6142d6b]
|
|
||||||
- @gitbook/icons@0.3.3
|
|
||||||
|
|
||||||
## 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
|
## 0.1.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
+10
-22
@@ -5,39 +5,27 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js",
|
||||||
},
|
"standalone": "./dist/standalone/index.js",
|
||||||
"./react": {
|
"react": "./dist/react/index.js"
|
||||||
"types": "./dist/react/index.d.ts",
|
|
||||||
"default": "./dist/react/index.js"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"version": "0.1.1",
|
||||||
"version": "0.1.4",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "catalog:",
|
"@gitbook/api": "catalog:",
|
||||||
"@gitbook/icons": "workspace:",
|
"@gitbook/icons": "workspace:",
|
||||||
"bidc": "catalog:"
|
"bidc": "catalog:"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*"
|
"react": "^18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsdown": "catalog:",
|
"typescript": "^5.5.3",
|
||||||
"typescript": "catalog:",
|
"react": "^19.0.0"
|
||||||
"react": "catalog:"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run build-lib && bun run build-standalone",
|
"build": "tsc && bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||||
"build-lib": "tsdown",
|
"typecheck": "tsc --noEmit"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
"files": ["dist", "README.md", "CHANGELOG.md", "standalone"]
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"registry": "https://registry.npmjs.org/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
'use client';
|
import React from 'react';
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import type {
|
import type {
|
||||||
GetFrameURLOptions,
|
GetFrameURLOptions,
|
||||||
GitBookEmbeddableConfiguration,
|
GitBookEmbeddableConfiguration,
|
||||||
@@ -19,19 +17,19 @@ export type GitBookFrameProps = {
|
|||||||
export function GitBookFrame(props: GitBookFrameProps) {
|
export function GitBookFrame(props: GitBookFrameProps) {
|
||||||
const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
|
const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props;
|
||||||
|
|
||||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
const frameRef = React.useRef<HTMLIFrameElement>(null);
|
||||||
const gitbook = useGitBook();
|
const gitbook = useGitBook();
|
||||||
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
|
const [gitbookFrame, setGitbookFrame] = React.useState<GitBookFrameClient | null>(null);
|
||||||
|
|
||||||
const frameURL = useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
|
const frameURL = React.useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (frameRef.current) {
|
if (frameRef.current) {
|
||||||
setGitbookFrame(gitbook.createFrame(frameRef.current));
|
setGitbookFrame(gitbook.createFrame(frameRef.current));
|
||||||
}
|
}
|
||||||
}, [gitbook]);
|
}, [gitbook]);
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
gitbookFrame?.configure({
|
gitbookFrame?.configure({
|
||||||
buttons,
|
buttons,
|
||||||
welcomeMessage,
|
welcomeMessage,
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { defineConfig } from 'tsdown';
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
{
|
|
||||||
entry: 'src/index.ts',
|
|
||||||
outDir: 'dist',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entry: 'src/react/index.ts',
|
|
||||||
outDir: 'dist/react',
|
|
||||||
unbundle: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
@@ -1,25 +1,13 @@
|
|||||||
# @gitbook/emoji-codepoints
|
# @gitbook/emoji-codepoints
|
||||||
|
|
||||||
## 0.2.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
|
||||||
|
|
||||||
## 0.2.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 295f03d: Republish packages
|
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
||||||
- 57adb3e: Second release to fix publishing with changeset
|
- 57adb3e: Second release to fix publishing with changeset
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
||||||
- 5f8a8fe: Initial release
|
- 5f8a8fe: Initial release
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@gitbook/emoji-codepoints",
|
"name": "@gitbook/emoji-codepoints",
|
||||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||||
"version": "0.2.2",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"exports": "./dist/index.ts",
|
"exports": "./dist/index.ts",
|
||||||
"sideEffects": false,
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"emoji-assets": "^9.0.0"
|
"emoji-assets": "^8.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "bun ./build.ts",
|
"generate": "bun ./build.ts",
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
# @gitbook/expr
|
# @gitbook/expr
|
||||||
|
|
||||||
## 1.2.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
|
||||||
|
|
||||||
## 1.2.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 295f03d: Republish packages
|
|
||||||
|
|
||||||
## 1.2.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- a629900: Add dev script for @gitbook/expr
|
|
||||||
|
|
||||||
## 1.2.0
|
## 1.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
+11
-14
@@ -1,40 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "@gitbook/expr",
|
"name": "@gitbook/expr",
|
||||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||||
"version": "1.2.3",
|
"version": "1.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2",
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.14.0",
|
||||||
"acorn-loose": "8.4.0",
|
"acorn-loose": "8.4.0",
|
||||||
"acorn-walk": "^8.3.4",
|
"acorn-walk": "^8.3.4",
|
||||||
"assert-never": "catalog:",
|
|
||||||
"escodegen": "^2.1.0",
|
"escodegen": "^2.1.0",
|
||||||
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2"
|
"assert-never": "^1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/escodegen": "^0.0.10",
|
"bun-types": "^1.1.20",
|
||||||
|
"tsdown": "^0.15.0",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.6",
|
||||||
|
"@babel/types": "^7.26.0",
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"bun-types": "catalog:",
|
"@types/escodegen": "^0.0.10"
|
||||||
"tsdown": "catalog:"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown",
|
"build": "tsdown --project tsconfig.build.json",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"unit": "bun test",
|
"unit": "bun test",
|
||||||
"clean": "rm -rf ./dist",
|
"clean": "rm -rf ./dist"
|
||||||
"dev": "bun run build -- --watch ./src"
|
|
||||||
},
|
},
|
||||||
"files": ["dist", "README.md", "CHANGELOG.md"],
|
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"registry": "https://registry.npmjs.org/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": ["./tsconfig.json"],
|
||||||
|
"exclude": ["**/*.test.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"outDir": "dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@ import { defineConfig } from 'tsdown';
|
|||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
entry: 'src/index.ts',
|
entry: ['src/index.ts'],
|
||||||
unbundle: true,
|
dts: true,
|
||||||
|
format: ['esm'],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1,17 +1,5 @@
|
|||||||
# @gitbook/fonts
|
# @gitbook/fonts
|
||||||
|
|
||||||
## 0.1.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
|
||||||
|
|
||||||
## 0.1.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 295f03d: Republish packages
|
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -4,33 +4,28 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"version": "0.1.0",
|
||||||
"version": "0.1.2",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "catalog:"
|
"@gitbook/api": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"google-font-metadata": "^6.0.7",
|
"google-font-metadata": "^6.0.3",
|
||||||
"tsdown": "catalog:",
|
"typescript": "^5.5.3"
|
||||||
"typescript": "catalog:"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "bun ./bin/generate.js",
|
"generate": "bun ./bin/generate.js",
|
||||||
"build": "tsdown",
|
"build": "tsc --project tsconfig.build.json",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"dev": "bun run build -- --watch ./src",
|
"dev": "tsc -w",
|
||||||
"clean": "rm -rf ./dist && rm -rf ./src/data",
|
"clean": "rm -rf ./dist && rm -rf ./src/data",
|
||||||
"unit": "bun test"
|
"unit": "bun test"
|
||||||
},
|
},
|
||||||
"files": ["dist", "bin", "README.md", "CHANGELOG.md"],
|
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"registry": "https://registry.npmjs.org/"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`getDefaultFont should return correct object for Latin text 1`] = `
|
exports[`getDefaultFont should return correct object for Latin text 1`] = `
|
||||||
{
|
{
|
||||||
"font": "Inter",
|
"font": "Inter",
|
||||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
|
exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
|
||||||
{
|
{
|
||||||
"font": "Inter",
|
"font": "Inter",
|
||||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`getDefaultFont should return correct object for Greek text 1`] = `
|
exports[`getDefaultFont should return correct object for Greek text 1`] = `
|
||||||
{
|
{
|
||||||
"font": "Inter",
|
"font": "Inter",
|
||||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
|
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`getDefaultFont should handle mixed script text 1`] = `
|
exports[`getDefaultFont should handle mixed script text 1`] = `
|
||||||
{
|
{
|
||||||
"font": "Inter",
|
"font": "Inter",
|
||||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`getDefaultFont should handle different font weights: regular 1`] = `
|
exports[`getDefaultFont should handle different font weights: regular 1`] = `
|
||||||
{
|
{
|
||||||
"font": "Inter",
|
"font": "Inter",
|
||||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`getDefaultFont should handle different font weights: bold 1`] = `
|
exports[`getDefaultFont should handle different font weights: bold 1`] = `
|
||||||
{
|
{
|
||||||
"font": "Inter",
|
"font": "Inter",
|
||||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
|
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`getDefaultFont should handle different fonts: inter 1`] = `
|
exports[`getDefaultFont should handle different fonts: inter 1`] = `
|
||||||
{
|
{
|
||||||
"font": "Inter",
|
"font": "Inter",
|
||||||
"url": "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`getDefaultFont should handle different fonts: roboto 1`] = `
|
exports[`getDefaultFont should handle different fonts: roboto 1`] = `
|
||||||
{
|
{
|
||||||
"font": "Roboto",
|
"font": "Roboto",
|
||||||
"url": "https://fonts.gstatic.com/s/roboto/v49/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAz0klQm_20.woff",
|
"url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "src/**/*.test.ts"]
|
||||||
|
}
|
||||||
@@ -1,94 +1,5 @@
|
|||||||
# gitbook
|
# gitbook
|
||||||
|
|
||||||
## 0.19.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 6142d6b: Mark as sideEffects, fix all package bundles
|
|
||||||
- 75543b1: Update minimum Node version in .npmrc
|
|
||||||
- 511f1cf: Add scroll margin to Expandable
|
|
||||||
- Updated dependencies [6142d6b]
|
|
||||||
- @gitbook/emoji-codepoints@0.2.2
|
|
||||||
- @gitbook/react-contentkit@0.7.7
|
|
||||||
- @gitbook/openapi-parser@3.0.4
|
|
||||||
- @gitbook/browser-types@0.1.2
|
|
||||||
- @gitbook/react-openapi@1.5.2
|
|
||||||
- @gitbook/cache-tags@0.3.3
|
|
||||||
- @gitbook/react-math@0.6.3
|
|
||||||
- @gitbook/colors@0.4.2
|
|
||||||
- @gitbook/fonts@0.1.2
|
|
||||||
- @gitbook/icons@0.3.3
|
|
||||||
- @gitbook/expr@1.2.3
|
|
||||||
|
|
||||||
## 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
|
## 0.18.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ const testCases: TestsCase[] = [
|
|||||||
contentBaseURL: 'https://docs.gmgn.ai',
|
contentBaseURL: 'https://docs.gmgn.ai',
|
||||||
tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// name: 'docs.spicychat.ai',
|
name: 'docs.spicychat.ai',
|
||||||
// contentBaseURL: 'https://docs.spicychat.ai',
|
contentBaseURL: 'https://docs.spicychat.ai',
|
||||||
// tests: [{ name: 'Home', url: '/' }],
|
tests: [{ name: 'Home', url: '/' }],
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
name: 'docs.portainer.io',
|
name: 'docs.portainer.io',
|
||||||
contentBaseURL: 'https://docs.portainer.io',
|
contentBaseURL: 'https://docs.portainer.io',
|
||||||
|
|||||||
@@ -26,10 +26,8 @@ export default class extends WorkerEntrypoint {
|
|||||||
async fetch(request) {
|
async fetch(request) {
|
||||||
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const middlewareRequest = new Request(request.url, request);
|
|
||||||
middlewareRequest.headers.set('x-open-next-continent', request.cf?.continent || '');
|
|
||||||
// - `Request`s are handled by the Next server
|
// - `Request`s are handled by the Next server
|
||||||
const reqOrResp = await middlewareHandler(middlewareRequest, this.env, this.ctx);
|
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
|
||||||
if (reqOrResp instanceof Response) {
|
if (reqOrResp instanceof Response) {
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
const logMessage = formatLog(
|
const logMessage = formatLog(
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import type {
|
|||||||
} from '@opennextjs/aws/types/overrides.js';
|
} from '@opennextjs/aws/types/overrides.js';
|
||||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||||
|
|
||||||
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
|
||||||
|
|
||||||
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
|
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
|
||||||
|
|
||||||
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
|
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
|
||||||
@@ -20,12 +18,15 @@ export type KeyOptions = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it has an additional
|
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
|
||||||
* R2WriteBuffer Durable Object to handle writes to R2. Given how we set up cache, we often end up writing to the same key too fast.
|
* the cache operations. It also integrates both R2 and Cache API in a single class.
|
||||||
|
* Having our own, will allow us to customize it in the future if needed.
|
||||||
*/
|
*/
|
||||||
class GitbookIncrementalCache implements IncrementalCache {
|
class GitbookIncrementalCache implements IncrementalCache {
|
||||||
name = 'GitbookIncrementalCache';
|
name = 'GitbookIncrementalCache';
|
||||||
|
|
||||||
|
protected localCache: Cache | undefined;
|
||||||
|
|
||||||
async get<CacheType extends CacheEntryType = 'cache'>(
|
async get<CacheType extends CacheEntryType = 'cache'>(
|
||||||
key: string,
|
key: string,
|
||||||
cacheType?: CacheType
|
cacheType?: CacheType
|
||||||
@@ -33,6 +34,7 @@ class GitbookIncrementalCache implements IncrementalCache {
|
|||||||
const cacheKey = this.getR2Key(key, cacheType);
|
const cacheKey = this.getR2Key(key, cacheType);
|
||||||
|
|
||||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||||
|
const localCache = await this.getCacheInstance();
|
||||||
if (!r2) throw new Error('No R2 bucket');
|
if (!r2) throw new Error('No R2 bucket');
|
||||||
if (process.env.SHOULD_BYPASS_CACHE === 'true') {
|
if (process.env.SHOULD_BYPASS_CACHE === 'true') {
|
||||||
// We are in a local middleware environment, we should bypass the cache
|
// We are in a local middleware environment, we should bypass the cache
|
||||||
@@ -40,17 +42,27 @@ class GitbookIncrementalCache implements IncrementalCache {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// Check local cache first if available
|
||||||
|
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
|
||||||
|
if (localCacheEntry) {
|
||||||
|
const result = (await localCacheEntry.json()) as WithLastModified<
|
||||||
|
CacheValue<CacheType>
|
||||||
|
>;
|
||||||
|
return this.returnNullOn404({
|
||||||
|
...result,
|
||||||
|
// Because we use tag cache and also invalidate them every time,
|
||||||
|
// if we get a cache hit, we don't need to check the tag cache as we already know it's not been revalidated
|
||||||
|
// this should improve performance even further, and reduce costs
|
||||||
|
shouldBypassTagCache: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const r2Object = await r2.get(cacheKey);
|
const r2Object = await r2.get(cacheKey);
|
||||||
if (!r2Object) return null;
|
if (!r2Object) return null;
|
||||||
|
|
||||||
const json = (await r2Object.json()) as CacheValue<CacheType>;
|
|
||||||
const lastModified = r2Object.uploaded.getTime();
|
|
||||||
|
|
||||||
if (!json) return null;
|
|
||||||
|
|
||||||
return this.returnNullOn404({
|
return this.returnNullOn404({
|
||||||
value: json,
|
value: await r2Object.json(),
|
||||||
lastModified,
|
lastModified: r2Object.uploaded.getTime(),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to get from cache', e);
|
console.error('Failed to get from cache', e);
|
||||||
@@ -77,8 +89,39 @@ class GitbookIncrementalCache implements IncrementalCache {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cacheKey = this.getR2Key(key, cacheType);
|
const cacheKey = this.getR2Key(key, cacheType);
|
||||||
|
|
||||||
|
const localCache = await this.getCacheInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.writeToR2(cacheKey, JSON.stringify(value));
|
await this.writeToR2(cacheKey, JSON.stringify(value));
|
||||||
|
|
||||||
|
//TODO: Check if there is any places where we don't have tags
|
||||||
|
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
|
||||||
|
// For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
|
||||||
|
const tags = this.getTagsFromCacheEntry(value) ?? [
|
||||||
|
`build_id/${process.env.NEXT_BUILD_ID}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// We consider R2 as the source of truth, so we update the local cache
|
||||||
|
// only after a successful R2 write
|
||||||
|
await localCache.put(
|
||||||
|
this.getCacheUrlKey(cacheKey),
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
value,
|
||||||
|
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
||||||
|
// See https://developers.cloudflare.com/workers/reference/security-model/
|
||||||
|
lastModified: Date.now(),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
// Cache-Control default to 30 minutes, will be overridden by `revalidate`
|
||||||
|
// In theory we should always get the `revalidate` value
|
||||||
|
'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
|
||||||
|
'cache-tag': tags.join(','),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to set to cache', e);
|
console.error('Failed to set to cache', e);
|
||||||
}
|
}
|
||||||
@@ -88,10 +131,14 @@ class GitbookIncrementalCache implements IncrementalCache {
|
|||||||
const cacheKey = this.getR2Key(key);
|
const cacheKey = this.getR2Key(key);
|
||||||
|
|
||||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||||
|
const localCache = await this.getCacheInstance();
|
||||||
if (!r2) throw new Error('No R2 bucket');
|
if (!r2) throw new Error('No R2 bucket');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await r2.delete(cacheKey);
|
await r2.delete(cacheKey);
|
||||||
|
|
||||||
|
// Here again R2 is the source of truth, so we delete from local cache first
|
||||||
|
await localCache.delete(this.getCacheUrlKey(cacheKey));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to delete from cache', e);
|
console.error('Failed to delete from cache', e);
|
||||||
}
|
}
|
||||||
@@ -121,6 +168,12 @@ class GitbookIncrementalCache implements IncrementalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCacheInstance(): Promise<Cache> {
|
||||||
|
if (this.localCache) return this.localCache;
|
||||||
|
this.localCache = await caches.open('incremental-cache');
|
||||||
|
return this.localCache;
|
||||||
|
}
|
||||||
|
|
||||||
// Utility function to generate keys for R2/Cache API
|
// Utility function to generate keys for R2/Cache API
|
||||||
getR2Key(initialKey: string, cacheType: CacheEntryType = 'cache'): string {
|
getR2Key(initialKey: string, cacheType: CacheEntryType = 'cache'): string {
|
||||||
let key = initialKey;
|
let key = initialKey;
|
||||||
@@ -136,13 +189,28 @@ class GitbookIncrementalCache implements IncrementalCache {
|
|||||||
'/'
|
'/'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCacheUrlKey(cacheKey: string): string {
|
||||||
|
return `http://cache.local/${cacheKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagsFromCacheEntry<CacheType extends CacheEntryType>(
|
||||||
|
entry: CacheValue<CacheType>
|
||||||
|
): string[] | undefined {
|
||||||
|
if ('tags' in entry && entry.tags) {
|
||||||
|
return entry.tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
|
||||||
|
const rawTags = entry.meta.headers['x-next-cache-tags'];
|
||||||
|
if (typeof rawTags === 'string') {
|
||||||
|
return rawTags.split(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('value' in entry) {
|
||||||
|
return entry.tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRegionalCache(new GitbookIncrementalCache(), {
|
export default new GitbookIncrementalCache();
|
||||||
mode: 'long-lived',
|
|
||||||
// We can do it because we use our own logic to invalidate the cache
|
|
||||||
bypassTagCacheOnCacheHit: true,
|
|
||||||
defaultLongLivedTtlSec: 60 * 60 * 24 /* 24 hours */,
|
|
||||||
// We don't want to update the cache entry on every cache hit
|
|
||||||
shouldLazilyUpdateOnCacheHit: false,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gitbook",
|
"name": "gitbook",
|
||||||
"version": "0.19.2",
|
"version": "0.18.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "catalog:",
|
"@gitbook/api": "catalog:",
|
||||||
@@ -8,15 +8,13 @@
|
|||||||
"@gitbook/cache-tags": "workspace:*",
|
"@gitbook/cache-tags": "workspace:*",
|
||||||
"@gitbook/colors": "workspace:*",
|
"@gitbook/colors": "workspace:*",
|
||||||
"@gitbook/emoji-codepoints": "workspace:*",
|
"@gitbook/emoji-codepoints": "workspace:*",
|
||||||
"@gitbook/expr": "workspace:*",
|
|
||||||
"@gitbook/fonts": "workspace:*",
|
"@gitbook/fonts": "workspace:*",
|
||||||
"@gitbook/icons": "workspace:*",
|
"@gitbook/icons": "workspace:*",
|
||||||
"@gitbook/openapi-parser": "workspace:*",
|
"@gitbook/openapi-parser": "workspace:*",
|
||||||
"@gitbook/react-contentkit": "workspace:*",
|
"@gitbook/react-contentkit": "workspace:*",
|
||||||
"@gitbook/react-math": "workspace:*",
|
"@gitbook/react-math": "workspace:*",
|
||||||
"@gitbook/react-openapi": "workspace:*",
|
"@gitbook/react-openapi": "workspace:*",
|
||||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
"@opennextjs/cloudflare": "^1.7.1",
|
||||||
"@opennextjs/cloudflare": "^1.9.2",
|
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
@@ -28,9 +26,10 @@
|
|||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tusbar/cache-control": "^1.0.2",
|
"@tusbar/cache-control": "^1.0.2",
|
||||||
"ai": "^4.2.2",
|
"ai": "^4.2.2",
|
||||||
"assert-never": "catalog:",
|
"assert-never": "^1.2.1",
|
||||||
"bidc": "catalog:",
|
"bidc": "catalog:",
|
||||||
"classnames": "catalog:",
|
"bun-types": "^1.1.20",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"direction": "^2.0.1",
|
"direction": "^2.0.1",
|
||||||
"event-iterator": "^2.0.0",
|
"event-iterator": "^2.0.0",
|
||||||
"framer-motion": "^10.16.14",
|
"framer-motion": "^10.16.14",
|
||||||
@@ -38,8 +37,8 @@
|
|||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsontoxml": "^1.0.1",
|
"jsontoxml": "^1.0.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"katex": "^0.16.9",
|
||||||
"mathjax": "^3.2.2",
|
"mathjax": "^3.2.2",
|
||||||
"mcp-handler": "^1.0.2",
|
|
||||||
"mdast-util-from-markdown": "^2.0.2",
|
"mdast-util-from-markdown": "^2.0.2",
|
||||||
"mdast-util-frontmatter": "^2.0.1",
|
"mdast-util-frontmatter": "^2.0.1",
|
||||||
"mdast-util-gfm": "^3.1.0",
|
"mdast-util-gfm": "^3.1.0",
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
"micromark-extension-gfm": "^3.0.0",
|
"micromark-extension-gfm": "^3.0.0",
|
||||||
"motion": "^12.23.12",
|
"motion": "^12.23.12",
|
||||||
"next": "15.4.0",
|
"next": "15.4.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.2.1",
|
||||||
"nuqs": "^2.2.3",
|
"nuqs": "^2.2.3",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"object-identity": "^0.1.2",
|
"object-identity": "^0.1.2",
|
||||||
@@ -71,10 +70,12 @@
|
|||||||
"unist-util-remove": "^4.0.0",
|
"unist-util-remove": "^4.0.0",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"url-join": "^5.0.0",
|
"url-join": "^5.0.0",
|
||||||
"usehooks-ts": "catalog:",
|
"usehooks-ts": "^3.1.0",
|
||||||
"warn-once": "^0.1.1",
|
"warn-once": "^0.1.1",
|
||||||
"zod": "^3",
|
"zustand": "^5.0.3",
|
||||||
"zustand": "^5.0.3"
|
"mcp-handler": "^1.0.2",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||||
|
"zod": "^3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@argos-ci/playwright": "^5.0.9",
|
"@argos-ci/playwright": "^5.0.9",
|
||||||
@@ -90,7 +91,6 @@
|
|||||||
"@types/react": "18.3.13",
|
"@types/react": "18.3.13",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "18.3.1",
|
||||||
"@types/rison": "^0.0.9",
|
"@types/rison": "^0.0.9",
|
||||||
"bun-types": "catalog:",
|
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"stylelint": "^16.16.0",
|
"stylelint": "^16.16.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"ts-essentials": "^10.0.1",
|
"ts-essentials": "^10.0.1",
|
||||||
"typescript": "catalog:",
|
"typescript": "^5.5.3",
|
||||||
"vercel": "^39.3.0"
|
"vercel": "^39.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -120,9 +120,5 @@
|
|||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
">0.3%, chrome >= 64, edge >= 79, firefox >= 67, opera >= 51, safari >= 12 and not dead"
|
">0.3%, chrome >= 64, edge >= 79, firefox >= 67, opera >= 51, safari >= 12 and not dead"
|
||||||
],
|
]
|
||||||
"publishConfig": {
|
|
||||||
"access": "public",
|
|
||||||
"registry": "https://registry.npmjs.org/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-100
@@ -16,107 +16,9 @@ export async function GET(
|
|||||||
return new Response(
|
return new Response(
|
||||||
`
|
`
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head></head>
|
||||||
<meta name="color-scheme" content="light dark">
|
<body></body>
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<svg style="position: absolute; bottom: 6rem; right: 4rem;" width="719" height="644" viewBox="0 0 719 644" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect opacity="0.1" x="0.566895" y="208" width="529.567" height="228" rx="16" fill="#A4A7B0"/>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="141.567" y1="472" x2="4.56689" y2="472" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="416.067" y1="472" x2="165.567" y2="472" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="526.567" y1="472" x2="440.067" y2="472" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="194.567" y1="496" x2="4.56689" y2="496" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="324.067" y1="496" x2="218.567" y2="496" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="473.567" y1="496" x2="348.067" y2="496" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="231.901" y1="520" x2="4.56689" y2="520" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="291.186" y1="520" x2="255.901" y2="520" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="442.058" y1="520" x2="315.186" y2="520" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="526.567" y1="520" x2="466.058" y2="520" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="141.567" y1="544" x2="4.56689" y2="544" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="348.191" y1="544" x2="165.567" y2="544" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="184.017" y1="592" x2="4.56689" y2="592" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="312.848" y1="592" x2="208.017" y2="592" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="386.574" y1="592" x2="336.848" y2="592" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="448.401" y1="592" x2="410.574" y2="592" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="126.633" y1="616" x2="4.56689" y2="616" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="401.133" y1="616" x2="150.633" y2="616" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="526.567" y1="616" x2="425.133" y2="616" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="179.457" y1="640" x2="4.56689" y2="640" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="329.569" y1="640" x2="203.457" y2="640" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="141.567" y1="4" x2="4.56689" y2="4" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="416.067" y1="4" x2="165.567" y2="4" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="526.567" y1="4" x2="440.067" y2="4" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="194.567" y1="28" x2="4.56689" y2="28" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="324.067" y1="28" x2="218.567" y2="28" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="473.567" y1="28" x2="348.067" y2="28" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="231.901" y1="52" x2="4.56689" y2="52" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="291.186" y1="52" x2="255.901" y2="52" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="442.058" y1="52" x2="315.186" y2="52" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="526.567" y1="52" x2="466.058" y2="52" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="141.567" y1="76" x2="4.56689" y2="76" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="348.191" y1="76" x2="165.567" y2="76" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="184.017" y1="124" x2="4.56689" y2="124" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="312.848" y1="124" x2="208.017" y2="124" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="386.574" y1="124" x2="336.848" y2="124" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="448.401" y1="124" x2="410.574" y2="124" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="126.633" y1="148" x2="4.56689" y2="148" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="401.133" y1="148" x2="150.633" y2="148" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="526.567" y1="148" x2="425.133" y2="148" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="179.457" y1="172" x2="4.56689" y2="172" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="329.569" y1="172" x2="203.457" y2="172" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<rect x="563" y="208" width="156" height="256" rx="16" fill="#A4A7B0" opacity="0.05"/>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="682.214" y1="228" x2="583" y2="228" stroke="#656973" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="638.472" y1="252" x2="583" y2="252" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="681.396" y1="252" x2="654.472" y2="252" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="617.989" y1="276" x2="583" y2="276" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="699" y1="276" x2="633.989" y2="276" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="603.168" y1="300" x2="583" y2="300" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="657.873" y1="300" x2="619.168" y2="300" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="637" y1="324" x2="583" y2="324" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="688.836" y1="324" x2="653" y2="324" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="657.474" y1="372" x2="583" y2="372" stroke="#656973" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="633" y1="396" x2="583" y2="396" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="699" y1="396" x2="649" y2="396" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="585.416" y1="420" x2="583" y2="420" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="621.731" y1="420" x2="601.416" y2="420" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="699" y1="420" x2="637.731" y2="420" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
<g opacity="0.2">
|
|
||||||
<line x1="619.471" y1="444" x2="583" y2="444" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
<line x1="658.186" y1="444" x2="635.471" y2="444" stroke="#A4A7B0" stroke-width="8" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</body>
|
|
||||||
<script src="${context.linker.toAbsoluteURL(context.linker.toPathInSite('~gitbook/embed/script.js'))}"></script>
|
<script src="${context.linker.toAbsoluteURL(context.linker.toPathInSite('~gitbook/embed/script.js'))}"></script>
|
||||||
<script>
|
|
||||||
window.GitBook('configure', {
|
|
||||||
suggestions: [
|
|
||||||
'Help me get started',
|
|
||||||
'What can I ask you?',
|
|
||||||
'Show me tips and tricks',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
window.GitBook('open');
|
|
||||||
</script>
|
|
||||||
</html>
|
</html>
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type AIChatMessage = {
|
|||||||
role: AIMessageRole;
|
role: AIMessageRole;
|
||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
note?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AIChatPendingTool = {
|
export type AIChatPendingTool = {
|
||||||
@@ -391,11 +392,11 @@ export function AIChatProvider(props: {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
messages: [
|
messages: [
|
||||||
...state.messages,
|
|
||||||
{
|
{
|
||||||
role: AIMessageRole.User,
|
role: AIMessageRole.User,
|
||||||
content: input.message,
|
content: input.message,
|
||||||
query: input.message,
|
query: input.message,
|
||||||
|
note: state.messages.length > 0 ? 'Starting a new chat' : undefined,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
query: input.message,
|
query: input.message,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useLanguage } from '@/intl/client';
|
import { useLanguage } from '@/intl/client';
|
||||||
import { t } from '@/intl/translate';
|
import { t } from '@/intl/translate';
|
||||||
|
import { tcls } from '@/lib/tailwind';
|
||||||
import type { Assistant } from '../AI';
|
import type { Assistant } from '../AI';
|
||||||
import { Button } from '../primitives';
|
import { Button } from '../primitives';
|
||||||
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
|
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
|
||||||
@@ -23,7 +24,7 @@ export function AIChatButton(props: {
|
|||||||
iconOnly={!showLabel}
|
iconOnly={!showLabel}
|
||||||
size="medium"
|
size="medium"
|
||||||
variant="header"
|
variant="header"
|
||||||
className="h-9 px-2.5 max-md:[&_.button-content]:hidden"
|
className={tcls('h-9 px-2.5')}
|
||||||
label={
|
label={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{t(language, 'ai_chat_ask', assistant.label)}
|
{t(language, 'ai_chat_ask', assistant.label)}
|
||||||
@@ -37,7 +38,7 @@ export function AIChatButton(props: {
|
|||||||
}
|
}
|
||||||
onClick={() => assistant.open()}
|
onClick={() => assistant.open()}
|
||||||
>
|
>
|
||||||
{showLabel ? t(language, 'ask') : null}
|
{showLabel ? <span className="max-md:hidden">{t(language, 'ask')}</span> : null}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function AIChatInput(props: {
|
|||||||
<span className="leading-none">
|
<span className="leading-none">
|
||||||
{t(language, 'ai_chat_context_title')}
|
{t(language, 'ai_chat_context_title')}
|
||||||
</span>
|
</span>
|
||||||
<Icon icon="question-circle" className="size-3 shrink-0" />
|
<Icon icon="question-circle" className="size-3" />
|
||||||
</div>
|
</div>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
</HoverCardRoot>
|
</HoverCardRoot>
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import type React from 'react';
|
|||||||
import type { AIChatController, AIChatState } from '../AI';
|
import type { AIChatController, AIChatState } from '../AI';
|
||||||
import { AIChatToolConfirmations } from './AIChatToolConfirmations';
|
import { AIChatToolConfirmations } from './AIChatToolConfirmations';
|
||||||
import { AIResponseFeedback } from './AIResponseFeedback';
|
import { AIResponseFeedback } from './AIResponseFeedback';
|
||||||
import { AIChatFollowupSuggestions } from './AiChatFollowupSuggestions';
|
|
||||||
|
|
||||||
export function AIChatMessages(props: {
|
export function AIChatMessages(props: {
|
||||||
chat: AIChatState;
|
chat: AIChatState;
|
||||||
chatController: AIChatController;
|
chatController: AIChatController;
|
||||||
lastUserMessageRef?: React.RefObject<HTMLDivElement>;
|
lastUserMessageRef?: React.RefObject<HTMLDivElement>;
|
||||||
}) {
|
}) {
|
||||||
const { chat, chatController, lastUserMessageRef } = props;
|
const { chat, lastUserMessageRef } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -24,57 +23,63 @@ export function AIChatMessages(props: {
|
|||||||
index === chat.messages.map((m) => m.role).lastIndexOf(AIMessageRole.User);
|
index === chat.messages.map((m) => m.role).lastIndexOf(AIMessageRole.User);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
ref={isLastUserMessage ? lastUserMessageRef : undefined}
|
{message.note ? (
|
||||||
data-testid="ai-chat-message"
|
<div
|
||||||
className={tcls(
|
key={`${index}-note`}
|
||||||
message.content ? 'animate-fade-in-slow' : '',
|
className="self-center text-tint-subtle text-xs"
|
||||||
'shrink-0',
|
>
|
||||||
'last:min-h-[calc(100%-5rem)]',
|
{message.note}
|
||||||
'scroll-mt-36',
|
|
||||||
'lg:scroll-mt-0',
|
|
||||||
'flex flex-col gap-6',
|
|
||||||
'break-words',
|
|
||||||
'group/message',
|
|
||||||
message.role === AIMessageRole.User
|
|
||||||
? 'max-w-[80%] self-end circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint px-4 py-2'
|
|
||||||
: 'text-tint-strong'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
animationDelay: `${Math.min(index * 0.05, 0.5)}s`,
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
{message.content ? message.content : null}
|
|
||||||
|
|
||||||
{isLastMessage && chat.loading ? (
|
|
||||||
<div className="flex w-full animate-fade-in-slow flex-col gap-2">
|
|
||||||
{!message.content ? <HoldMessage /> : null}
|
|
||||||
<LoadingSkeleton />
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<div
|
||||||
|
ref={isLastUserMessage ? lastUserMessageRef : undefined}
|
||||||
|
data-testid="ai-chat-message"
|
||||||
|
className={tcls(
|
||||||
|
message.content ? 'animate-fade-in-slow' : '',
|
||||||
|
'shrink-0',
|
||||||
|
'last:min-h-[calc(100%-5rem)]',
|
||||||
|
'scroll-mt-36',
|
||||||
|
'lg:scroll-mt-0',
|
||||||
|
'flex flex-col gap-6',
|
||||||
|
'break-words',
|
||||||
|
'group/message',
|
||||||
|
message.role === AIMessageRole.User
|
||||||
|
? 'max-w-[80%] self-end circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint px-4 py-2'
|
||||||
|
: 'text-tint-strong'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${Math.min(index * 0.05, 0.5)}s`,
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{message.content ? message.content : null}
|
||||||
|
|
||||||
{isLastMessage ? (
|
{isLastMessage && chat.loading ? (
|
||||||
<>
|
<div className="flex w-full animate-fade-in-slow flex-col gap-2">
|
||||||
{!chat.loading &&
|
{!message.content ? <HoldMessage /> : null}
|
||||||
!chat.error &&
|
<LoadingSkeleton />
|
||||||
chat.query &&
|
</div>
|
||||||
chat.responseId &&
|
) : null}
|
||||||
chat.pendingTools.length === 0 ? (
|
|
||||||
<AIResponseFeedback
|
{isLastMessage ? (
|
||||||
responseId={chat.responseId}
|
<>
|
||||||
query={chat.query}
|
{!chat.loading &&
|
||||||
className="-ml-1 -mt-4"
|
!chat.error &&
|
||||||
/>
|
chat.query &&
|
||||||
) : null}
|
chat.responseId &&
|
||||||
<AIChatToolConfirmations chat={chat} />
|
chat.pendingTools.length === 0 ? (
|
||||||
<AIChatFollowupSuggestions
|
<AIResponseFeedback
|
||||||
chat={chat}
|
responseId={chat.responseId}
|
||||||
chatController={chatController}
|
query={chat.query}
|
||||||
/>
|
className="-ml-1 -mt-4"
|
||||||
</>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
<AIChatToolConfirmations chat={chat} />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ import type { GitBookSiteContext } from '@/lib/context';
|
|||||||
import { OpenAPIPrefillContextProvider } from '@gitbook/react-openapi';
|
import { OpenAPIPrefillContextProvider } from '@gitbook/react-openapi';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import type { AdaptiveVisitorClaims } from './types';
|
|
||||||
|
export type AdaptiveVisitorClaimsData = {
|
||||||
|
visitor: {
|
||||||
|
claims: Record<string, unknown> & { unsigned: Record<string, unknown> };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In-memory cache of visitor claim readers keyed by contextId.
|
* In-memory cache of visitor claim readers keyed by contextId.
|
||||||
*/
|
*/
|
||||||
const adaptiveVisitorReaderCache = new Map<
|
const adaptiveVisitorReaderCache = new Map<
|
||||||
string,
|
string,
|
||||||
ReturnType<typeof createResourceReader<AdaptiveVisitorClaims | null>>
|
ReturnType<typeof createResourceReader<AdaptiveVisitorClaimsData | null>>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
function createResourceReader<T>(promise: Promise<T>) {
|
function createResourceReader<T>(promise: Promise<T>) {
|
||||||
@@ -47,7 +52,7 @@ function getAdaptiveVisitorClaimsReader(url: string, contextId: string) {
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return await res.json<AdaptiveVisitorClaims>();
|
return await res.json<AdaptiveVisitorClaimsData>();
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -59,7 +64,7 @@ function getAdaptiveVisitorClaimsReader(url: string, contextId: string) {
|
|||||||
return reader;
|
return reader;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdaptiveVisitorContextValue = () => AdaptiveVisitorClaims | null;
|
export type AdaptiveVisitorContextValue = () => AdaptiveVisitorClaimsData | null;
|
||||||
|
|
||||||
const AdaptiveVisitorContext = createContext<AdaptiveVisitorContextValue>(() => null);
|
const AdaptiveVisitorContext = createContext<AdaptiveVisitorContextValue>(() => null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
export * from './types';
|
|
||||||
export * from './utils';
|
|
||||||
export * from './AdaptiveVisitorContextProvider';
|
export * from './AdaptiveVisitorContextProvider';
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export type AdaptiveVisitorClaimsData = Record<string, unknown> & {
|
|
||||||
unsigned: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AdaptiveVisitorClaims = {
|
|
||||||
visitor: {
|
|
||||||
claims: AdaptiveVisitorClaimsData;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { Variables } from '@gitbook/api';
|
|
||||||
import type { AdaptiveVisitorClaims } from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an evaluation context to evaluate expressions.
|
|
||||||
*/
|
|
||||||
export function createExpressionEvaluationContext(args: {
|
|
||||||
visitorClaims: AdaptiveVisitorClaims | null;
|
|
||||||
variables: {
|
|
||||||
space?: Variables;
|
|
||||||
page?: Variables;
|
|
||||||
};
|
|
||||||
}) {
|
|
||||||
const { visitorClaims, variables } = args;
|
|
||||||
return {
|
|
||||||
...(visitorClaims ? visitorClaims : {}),
|
|
||||||
space: {
|
|
||||||
vars: variables.space ?? {},
|
|
||||||
},
|
|
||||||
...(variables.page
|
|
||||||
? {
|
|
||||||
page: {
|
|
||||||
vars: variables.page ?? {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import { useCheckForContentUpdate } from '../AutoRefreshContent';
|
|||||||
import { useVisitorSession } from '../Insights';
|
import { useVisitorSession } from '../Insights';
|
||||||
import { useCurrentPagePath } from '../hooks';
|
import { useCurrentPagePath } from '../hooks';
|
||||||
import { DateRelative } from '../primitives';
|
import { DateRelative } from '../primitives';
|
||||||
import { HideToolbarButton } from './HideToolbarButton';
|
|
||||||
import { IframeWrapper } from './IframeWrapper';
|
import { IframeWrapper } from './IframeWrapper';
|
||||||
import { RefreshContentButton } from './RefreshContentButton';
|
import { RefreshContentButton } from './RefreshContentButton';
|
||||||
import {
|
import {
|
||||||
@@ -18,108 +17,48 @@ import {
|
|||||||
ToolbarSubtitle,
|
ToolbarSubtitle,
|
||||||
ToolbarTitle,
|
ToolbarTitle,
|
||||||
} from './Toolbar';
|
} from './Toolbar';
|
||||||
import {
|
import type { AdminToolbarClientProps } from './types';
|
||||||
type ToolbarControlsContextValue,
|
|
||||||
ToolbarControlsProvider,
|
|
||||||
} from './ToolbarControlsContext';
|
|
||||||
import type { AdminToolbarClientProps, AdminToolbarContext } from './types';
|
|
||||||
import { useToolbarVisibility } from './utils';
|
|
||||||
|
|
||||||
export function AdminToolbarClient(props: AdminToolbarClientProps) {
|
export function AdminToolbarClient(props: AdminToolbarClientProps) {
|
||||||
const { context, onPersistentClose, onSessionClose, onToggleMinify } = props;
|
const { context } = props;
|
||||||
const {
|
|
||||||
minified,
|
|
||||||
setMinified,
|
|
||||||
shouldAutoExpand,
|
|
||||||
hidden,
|
|
||||||
minimize,
|
|
||||||
closeSession,
|
|
||||||
closePersistent,
|
|
||||||
} = useToolbarVisibility({
|
|
||||||
onPersistentClose,
|
|
||||||
onSessionClose,
|
|
||||||
onToggleMinify,
|
|
||||||
});
|
|
||||||
|
|
||||||
const visitorSession = useVisitorSession();
|
const visitorSession = useVisitorSession();
|
||||||
|
|
||||||
const toolbarControls: ToolbarControlsContextValue = {
|
|
||||||
minimize,
|
|
||||||
closeSession,
|
|
||||||
closePersistent,
|
|
||||||
shouldAutoExpand,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (hidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is a change request, show the change request toolbar
|
// If there is a change request, show the change request toolbar
|
||||||
if (context.changeRequest) {
|
if (context.changeRequest) {
|
||||||
return (
|
return (
|
||||||
<ToolbarControlsWrapper value={toolbarControls}>
|
<IframeWrapper>
|
||||||
<ChangeRequestToolbar
|
<MotionConfig reducedMotion="user">
|
||||||
context={context}
|
<ChangeRequestToolbar context={context} />
|
||||||
minified={minified}
|
</MotionConfig>
|
||||||
onMinifiedChange={setMinified}
|
</IframeWrapper>
|
||||||
/>
|
|
||||||
</ToolbarControlsWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the revision is not the current revision, the user is looking at a previous version of the site, so show the revision toolbar
|
// If 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) {
|
if (context.revisionId !== context.space.revision) {
|
||||||
return (
|
return (
|
||||||
<ToolbarControlsWrapper value={toolbarControls}>
|
<IframeWrapper>
|
||||||
<RevisionToolbar
|
<MotionConfig reducedMotion="user">
|
||||||
context={context}
|
<RevisionToolbar context={context} />
|
||||||
minified={minified}
|
</MotionConfig>
|
||||||
onMinifiedChange={setMinified}
|
</IframeWrapper>
|
||||||
/>
|
|
||||||
</ToolbarControlsWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user is authenticated and part of the organization owning this site, show the authenticated user toolbar
|
// If the user is authenticated and part of the organization owning this site, show the authenticated user toolbar
|
||||||
if (visitorSession?.organizationId === context.organizationId) {
|
if (visitorSession?.organizationId === context.organizationId) {
|
||||||
return (
|
return (
|
||||||
<ToolbarControlsWrapper value={toolbarControls}>
|
<IframeWrapper>
|
||||||
<AuthenticatedUserToolbar
|
<MotionConfig reducedMotion="user">
|
||||||
context={context}
|
<AuthenticatedUserToolbar context={context} />
|
||||||
minified={minified}
|
</MotionConfig>
|
||||||
onMinifiedChange={setMinified}
|
</IframeWrapper>
|
||||||
/>
|
|
||||||
</ToolbarControlsWrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function ChangeRequestToolbar(props: AdminToolbarClientProps) {
|
||||||
* Reusable wrapper that provides tooling and containers that are used by all types of toolbar views.
|
const { context } = props;
|
||||||
*/
|
|
||||||
export function ToolbarControlsWrapper(
|
|
||||||
props: React.PropsWithChildren<{ value: ToolbarControlsContextValue | null }>
|
|
||||||
) {
|
|
||||||
const { children, value } = props;
|
|
||||||
return (
|
|
||||||
<ToolbarControlsProvider value={value}>
|
|
||||||
<IframeWrapper>
|
|
||||||
<MotionConfig reducedMotion="user">{children}</MotionConfig>
|
|
||||||
</IframeWrapper>
|
|
||||||
</ToolbarControlsProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToolbarViewProps {
|
|
||||||
context: AdminToolbarContext;
|
|
||||||
minified: boolean;
|
|
||||||
onMinifiedChange: (value: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChangeRequestToolbar(props: ToolbarViewProps) {
|
|
||||||
const { context, minified, onMinifiedChange } = props;
|
|
||||||
const { changeRequest, site } = context;
|
const { changeRequest, site } = context;
|
||||||
if (!changeRequest) {
|
if (!changeRequest) {
|
||||||
throw new Error('Change request is not set');
|
throw new Error('Change request is not set');
|
||||||
@@ -132,11 +71,11 @@ function ChangeRequestToolbar(props: ToolbarViewProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange} label="Site preview">
|
<Toolbar>
|
||||||
<ToolbarBody>
|
<ToolbarBody>
|
||||||
<ToolbarTitle
|
<ToolbarTitle
|
||||||
prefix={`Change #${changeRequest.number}:`}
|
prefix="Change request"
|
||||||
suffix={`${changeRequest.subject || 'Untitled'}`}
|
suffix={`#${changeRequest.number} ${changeRequest.subject || 'Untitled'}`}
|
||||||
/>
|
/>
|
||||||
<ToolbarSubtitle
|
<ToolbarSubtitle
|
||||||
subtitle={
|
subtitle={
|
||||||
@@ -149,13 +88,9 @@ function ChangeRequestToolbar(props: ToolbarViewProps) {
|
|||||||
|
|
||||||
<ToolbarSeparator />
|
<ToolbarSeparator />
|
||||||
|
|
||||||
<ToolbarActions>
|
<ToolbarButtonGroup>
|
||||||
{/* Refresh to retrieve latest changes */}
|
{/* Refresh to retrieve latest changes */}
|
||||||
{updated ? <RefreshContentButton refreshForUpdates={refreshForUpdates} /> : null}
|
{updated ? <RefreshContentButton refreshForUpdates={refreshForUpdates} /> : null}
|
||||||
|
|
||||||
{/* Edit in GitBook */}
|
|
||||||
<EditPageButton href={changeRequest.urls.app} siteId={site.id} />
|
|
||||||
|
|
||||||
{/* Comment in app */}
|
{/* Comment in app */}
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
title="Comment in a GitBook"
|
title="Comment in a GitBook"
|
||||||
@@ -190,13 +125,16 @@ function ChangeRequestToolbar(props: ToolbarViewProps) {
|
|||||||
})}
|
})}
|
||||||
icon="code-pull-request"
|
icon="code-pull-request"
|
||||||
/>
|
/>
|
||||||
</ToolbarActions>
|
|
||||||
|
{/* Edit in GitBook */}
|
||||||
|
<EditPageButton href={changeRequest.urls.app} siteId={site.id} />
|
||||||
|
</ToolbarButtonGroup>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RevisionToolbar(props: ToolbarViewProps) {
|
function RevisionToolbar(props: AdminToolbarClientProps) {
|
||||||
const { context, minified, onMinifiedChange } = props;
|
const { context } = props;
|
||||||
const { revision, site } = context;
|
const { revision, site } = context;
|
||||||
if (!revision) {
|
if (!revision) {
|
||||||
throw new Error('Revision is not set');
|
throw new Error('Revision is not set');
|
||||||
@@ -207,7 +145,7 @@ function RevisionToolbar(props: ToolbarViewProps) {
|
|||||||
const gitProvider = isGitHub ? 'GitHub' : 'GitLab';
|
const gitProvider = isGitHub ? 'GitHub' : 'GitLab';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange} label="Site preview">
|
<Toolbar>
|
||||||
<ToolbarBody>
|
<ToolbarBody>
|
||||||
<ToolbarTitle prefix="Site version" suffix={context.site.title} />
|
<ToolbarTitle prefix="Site version" suffix={context.site.title} />
|
||||||
<ToolbarSubtitle
|
<ToolbarSubtitle
|
||||||
@@ -219,7 +157,7 @@ function RevisionToolbar(props: ToolbarViewProps) {
|
|||||||
/>
|
/>
|
||||||
</ToolbarBody>
|
</ToolbarBody>
|
||||||
<ToolbarSeparator />
|
<ToolbarSeparator />
|
||||||
<ToolbarActions>
|
<ToolbarButtonGroup>
|
||||||
{/* Open commit in Git client */}
|
{/* Open commit in Git client */}
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
title={
|
title={
|
||||||
@@ -267,26 +205,22 @@ function RevisionToolbar(props: ToolbarViewProps) {
|
|||||||
})}
|
})}
|
||||||
icon="code-commit"
|
icon="code-commit"
|
||||||
/>
|
/>
|
||||||
</ToolbarActions>
|
</ToolbarButtonGroup>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthenticatedUserToolbar(props: ToolbarViewProps) {
|
function AuthenticatedUserToolbar(props: AdminToolbarClientProps) {
|
||||||
const { context, minified, onMinifiedChange } = props;
|
const { context } = props;
|
||||||
const { revision, space, site } = context;
|
const { revision, space, site } = context;
|
||||||
const { refreshForUpdates, updated } = useCheckForContentUpdate({
|
const { refreshForUpdates, updated } = useCheckForContentUpdate({
|
||||||
revisionId: space.revision,
|
revisionId: space.revision,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar
|
<Toolbar>
|
||||||
minified={minified}
|
|
||||||
onMinifiedChange={onMinifiedChange}
|
|
||||||
label="Only visible to your GitBook organization"
|
|
||||||
>
|
|
||||||
<ToolbarBody>
|
<ToolbarBody>
|
||||||
<ToolbarTitle suffix={context.site.title} />
|
<ToolbarTitle prefix="Site" suffix={context.site.title} />
|
||||||
<ToolbarSubtitle
|
<ToolbarSubtitle
|
||||||
subtitle={
|
subtitle={
|
||||||
<>
|
<>
|
||||||
@@ -296,14 +230,9 @@ function AuthenticatedUserToolbar(props: ToolbarViewProps) {
|
|||||||
/>
|
/>
|
||||||
</ToolbarBody>
|
</ToolbarBody>
|
||||||
<ToolbarSeparator />
|
<ToolbarSeparator />
|
||||||
<ToolbarActions>
|
<ToolbarButtonGroup>
|
||||||
{/* Refresh to retrieve latest changes */}
|
{/* Refresh to retrieve latest changes */}
|
||||||
{updated ? <RefreshContentButton refreshForUpdates={refreshForUpdates} /> : null}
|
{updated ? <RefreshContentButton refreshForUpdates={refreshForUpdates} /> : null}
|
||||||
|
|
||||||
{/* Edit in GitBook */}
|
|
||||||
<EditPageButton href={space.urls.app} siteId={site.id} />
|
|
||||||
|
|
||||||
{/* Open site in GitBook */}
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
title="Open site in GitBook"
|
title="Open site in GitBook"
|
||||||
href={getToolbarHref({
|
href={getToolbarHref({
|
||||||
@@ -311,10 +240,8 @@ function AuthenticatedUserToolbar(props: ToolbarViewProps) {
|
|||||||
siteId: site.id,
|
siteId: site.id,
|
||||||
buttonId: 'site',
|
buttonId: 'site',
|
||||||
})}
|
})}
|
||||||
icon="gears"
|
icon="gear"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Customize in GitBook */}
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
title="Customize in GitBook"
|
title="Customize in GitBook"
|
||||||
href={getToolbarHref({
|
href={getToolbarHref({
|
||||||
@@ -324,8 +251,6 @@ function AuthenticatedUserToolbar(props: ToolbarViewProps) {
|
|||||||
})}
|
})}
|
||||||
icon="palette"
|
icon="palette"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Open insights in GitBook */}
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
title="Open insights in GitBook"
|
title="Open insights in GitBook"
|
||||||
href={getToolbarHref({
|
href={getToolbarHref({
|
||||||
@@ -335,22 +260,12 @@ function AuthenticatedUserToolbar(props: ToolbarViewProps) {
|
|||||||
})}
|
})}
|
||||||
icon="chart-simple"
|
icon="chart-simple"
|
||||||
/>
|
/>
|
||||||
</ToolbarActions>
|
<EditPageButton href={space.urls.app} siteId={site.id} />
|
||||||
|
</ToolbarButtonGroup>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarActions(props: { children: React.ReactNode }) {
|
|
||||||
const { children } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolbarButtonGroup>
|
|
||||||
{children}
|
|
||||||
<HideToolbarButton />
|
|
||||||
</ToolbarButtonGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditPageButton(props: {
|
function EditPageButton(props: {
|
||||||
href: string;
|
href: string;
|
||||||
siteId: string;
|
siteId: string;
|
||||||
|
|||||||
@@ -3,23 +3,18 @@
|
|||||||
--seg-A-color: #2782c4;
|
--seg-A-color: #2782c4;
|
||||||
--seg-B-color: #43b7f2;
|
--seg-B-color: #43b7f2;
|
||||||
--seg-C-color: #8be2ff;
|
--seg-C-color: #8be2ff;
|
||||||
--trace-color: #46474c;
|
|
||||||
|
|
||||||
|
|
||||||
--T: 2s;
|
--T: 2s;
|
||||||
shape-rendering: geometricPrecision;
|
shape-rendering: geometricPrecision;
|
||||||
vector-effect: non-scaling-stroke;
|
vector-effect: non-scaling-stroke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.static .trace {
|
:global(html.dark) .svgLogo {
|
||||||
animation: none;
|
--logo-fill: var(--color-neutral-800);
|
||||||
fill: var(--logo-fill);
|
--seg-A-color: #19537d;
|
||||||
stroke: none;
|
--seg-B-color: #2782c4;
|
||||||
}
|
--seg-C-color: #43b7f2;
|
||||||
|
--trace-color: #46474c;
|
||||||
.static .seg {
|
|
||||||
animation: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base segment animation */
|
/* Base segment animation */
|
||||||
@@ -182,4 +177,4 @@
|
|||||||
fill: var(--logo-fill);
|
fill: var(--logo-fill);
|
||||||
stroke:none;
|
stroke:none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,10 @@ import { tcls } from '@/lib/tailwind';
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import styles from './AnimatedLogo.module.css';
|
import styles from './AnimatedLogo.module.css';
|
||||||
|
|
||||||
interface AnimatedLogoProps {
|
export const AnimatedLogo: React.FC = () => {
|
||||||
shouldAnimate?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AnimatedLogo: React.FC<AnimatedLogoProps> = (props) => {
|
|
||||||
const { shouldAnimate = true } = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={tcls(styles.svgLogo, shouldAnimate ? undefined : styles.static)}
|
className={styles.svgLogo}
|
||||||
width="28"
|
width="28"
|
||||||
height="28"
|
height="28"
|
||||||
viewBox="0 0 28 28"
|
viewBox="0 0 28 28"
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
|
||||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
|
||||||
import { motion } from 'motion/react';
|
|
||||||
import React, { useRef } from 'react';
|
|
||||||
import { useOnClickOutside } from 'usehooks-ts';
|
|
||||||
import { ToolbarButton, type ToolbarButtonProps } from './Toolbar';
|
|
||||||
import styles from './Toolbar.module.css';
|
|
||||||
import { useToolbarControls } from './ToolbarControlsContext';
|
|
||||||
|
|
||||||
const ARC_DURATION_SECONDS = 0.4;
|
|
||||||
const ARC_STAGGER_MS = 80;
|
|
||||||
const BASE_ROTATION_DEG = 95;
|
|
||||||
const ROTATION_STEP_DEG = 18;
|
|
||||||
|
|
||||||
interface HideToolbarButtonProps {
|
|
||||||
motionValues?: ToolbarButtonProps['motionValues'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide menu trigger. Expands a macOS Dock-like submenu with 3 labeled actions.
|
|
||||||
*/
|
|
||||||
export function HideToolbarButton(props: HideToolbarButtonProps) {
|
|
||||||
const { motionValues } = props;
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
const controls = useToolbarControls();
|
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const buttonRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleClickOutsideArcMenu = (event: Event) => {
|
|
||||||
// Don't close the arc if we are clicking on the button itself
|
|
||||||
if (buttonRef.current?.contains(event.target as Node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
useOnClickOutside(ref, handleClickOutsideArcMenu);
|
|
||||||
|
|
||||||
// Close arc menu on scroll
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
|
|
||||||
const handleScroll = () => setOpen(false);
|
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
controls?.minimize
|
|
||||||
? {
|
|
||||||
id: 'minimize',
|
|
||||||
icon: 'minus',
|
|
||||||
label: 'Minimize',
|
|
||||||
onClick: controls.minimize,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
controls?.closeSession
|
|
||||||
? {
|
|
||||||
id: 'session-close',
|
|
||||||
icon: 'xmark',
|
|
||||||
label: 'Close for one session',
|
|
||||||
onClick: controls.closeSession,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
controls?.closePersistent
|
|
||||||
? {
|
|
||||||
id: 'persistent-close',
|
|
||||||
icon: 'ban',
|
|
||||||
label: "Don't show again",
|
|
||||||
onClick: controls.closePersistent,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
].filter(Boolean) as Array<ArcMenuItem>;
|
|
||||||
|
|
||||||
const sharedMotionStyle = motionValues
|
|
||||||
? {
|
|
||||||
x: motionValues.x,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
ref={buttonRef}
|
|
||||||
title={open ? 'Hide options' : 'Hide toolbar'}
|
|
||||||
onClick={() => {
|
|
||||||
setOpen((v) => !v);
|
|
||||||
}}
|
|
||||||
motionValues={motionValues}
|
|
||||||
icon="eye-slash"
|
|
||||||
>
|
|
||||||
{/* Expanding arc menu */}
|
|
||||||
{open && (
|
|
||||||
<motion.div
|
|
||||||
className={tcls('pointer-events-none absolute inset-0', styles.arcMenu)}
|
|
||||||
style={sharedMotionStyle as React.CSSProperties | undefined}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={tcls(
|
|
||||||
'pointer-events-none absolute left-0 overflow-visible',
|
|
||||||
styles.arcMenuPath
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<ArcToolbarButton
|
|
||||||
index={index}
|
|
||||||
staggerIndex={items.length - 1 - index}
|
|
||||||
key={item.icon}
|
|
||||||
{...item}
|
|
||||||
onClick={() => {
|
|
||||||
setOpen(false);
|
|
||||||
item.onClick?.();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</ToolbarButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArcMenuItem = {
|
|
||||||
id: string;
|
|
||||||
icon: IconName;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ArcToolbarButtonProps = Pick<ArcMenuItem, 'label' | 'icon' | 'onClick'> & {
|
|
||||||
index: number;
|
|
||||||
staggerIndex?: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
className?: string;
|
|
||||||
iconClassName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ArcToolbarButton(props: ArcToolbarButtonProps) {
|
|
||||||
const {
|
|
||||||
index,
|
|
||||||
staggerIndex = index,
|
|
||||||
label,
|
|
||||||
disabled,
|
|
||||||
className,
|
|
||||||
onClick = () => {},
|
|
||||||
icon,
|
|
||||||
iconClassName,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const targetOffset = `calc(var(--start-distance) + ${index} * var(--spread-distance))`;
|
|
||||||
|
|
||||||
// Calculate rotation based on position along the arc
|
|
||||||
const calculateRotation = () => {
|
|
||||||
return BASE_ROTATION_DEG - index * ROTATION_STEP_DEG;
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemRotation = calculateRotation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pointer-events-none">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
onClick();
|
|
||||||
}}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
'--target-offset-distance': targetOffset,
|
|
||||||
'--arc-duration': `${ARC_DURATION_SECONDS}s`,
|
|
||||||
'--arc-delay': `${(staggerIndex ?? 0) * ARC_STAGGER_MS}ms`,
|
|
||||||
'--rotation-offset': `${itemRotation}deg`,
|
|
||||||
offsetPath: 'border-box',
|
|
||||||
offsetDistance: targetOffset,
|
|
||||||
offsetAnchor: '0% 40%',
|
|
||||||
offsetRotate: `auto ${itemRotation}deg`,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
className={tcls(
|
|
||||||
'group',
|
|
||||||
'absolute',
|
|
||||||
'top-0',
|
|
||||||
'left-0',
|
|
||||||
'w-40',
|
|
||||||
'opacity-0',
|
|
||||||
'pointer-events-auto',
|
|
||||||
'flex',
|
|
||||||
'items-center',
|
|
||||||
'gap-2',
|
|
||||||
styles.arcMenuItem,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={tcls(
|
|
||||||
'flex shrink-0 items-center justify-center gap-1',
|
|
||||||
'h-8 w-8 rounded-full border',
|
|
||||||
'truncate text-sm',
|
|
||||||
'cursor-pointer transition-colors',
|
|
||||||
'group-hover:-rotate-5 group-hover:scale-105',
|
|
||||||
disabled ? 'cursor-not-allowed opacity-50' : '',
|
|
||||||
'text-tint-1 dark:text-tint-12',
|
|
||||||
'bg-[linear-gradient(110deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]',
|
|
||||||
'dark:[background:linear-gradient(110deg,rgba(255,255,255,1)_0%,rgba(240,246,248,1)_100%)]',
|
|
||||||
'border border-solid dark:border-[rgba(256,_256,_256,_0.06)]'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(rgb(51, 53, 57), rgb(50, 52, 56))',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={icon as IconName}
|
|
||||||
iconStyle={IconStyle.Solid}
|
|
||||||
className={tcls('size-4 shrink-0 group-hover:scale-110', iconClassName)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={tcls(
|
|
||||||
'whitespace-nowrap rounded-lg px-3 py-1 font-normal text-sm transition-transform',
|
|
||||||
'group-hover:rotate-2 group-hover:scale-105',
|
|
||||||
'text-neutral-1 dark:text-neutral-12',
|
|
||||||
'bg-[linear-gradient(110deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
.arcMenu {
|
|
||||||
--arc-width: 505px;
|
|
||||||
--arc-height: 400px;
|
|
||||||
--arc-radius: 34%;
|
|
||||||
--start-distance: -240px;
|
|
||||||
--spread-distance: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arcMenuPath {
|
|
||||||
bottom: calc(var(--arc-height) / -2);
|
|
||||||
width: var(--arc-width);
|
|
||||||
height: var(--arc-height);
|
|
||||||
border-radius: var(--arc-radius);
|
|
||||||
transform: translate(0%, 0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arcMenuItem {
|
|
||||||
animation-name: hide-toolbar-arc-enter;
|
|
||||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
animation-fill-mode: forwards;
|
|
||||||
animation-duration: var(--arc-duration, 0.4s);
|
|
||||||
animation-delay: var(--arc-delay, 0s);
|
|
||||||
transform-origin: center left;
|
|
||||||
offset-path: border-box;
|
|
||||||
offset-anchor: 0% 0%;
|
|
||||||
offset-rotate: auto var(--rotation-offset, 0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes hide-toolbar-arc-enter {
|
|
||||||
from {
|
|
||||||
offset-distance: var(--start-distance);
|
|
||||||
transform: scale(0.5);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
offset-distance: var(--target-offset-distance);
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import {
|
import { AnimatePresence, type MotionValue, motion, useReducedMotion } from 'motion/react';
|
||||||
AnimatePresence,
|
|
||||||
type MotionValue,
|
|
||||||
motion,
|
|
||||||
useReducedMotion,
|
|
||||||
useSpring,
|
|
||||||
} from 'motion/react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AnimatedLogo } from './AnimatedLogo';
|
import { AnimatedLogo } from './AnimatedLogo';
|
||||||
import { useToolbarControls } from './ToolbarControlsContext';
|
|
||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
import { Icon, type IconName } from '@gitbook/icons';
|
||||||
import { Tooltip } from '../primitives';
|
import { Tooltip } from '../primitives';
|
||||||
import { getCopyVariants, toolbarEasings } from './transitions';
|
import { getCopyVariants, minifyButtonAnimation, toolbarEasings } from './transitions';
|
||||||
import { useMagnificationEffect } from './useMagnificationEffect';
|
import { useMagnificationEffect } from './useMagnificationEffect';
|
||||||
|
|
||||||
const DURATION_LOGO_APPEARANCE = 2000;
|
const DURATION_LOGO_APPEARANCE = 2000;
|
||||||
const DELAY_BETWEEN_LOGO_AND_CONTENT = 100;
|
const DELAY_BETWEEN_LOGO_AND_CONTENT = 100;
|
||||||
|
|
||||||
interface ToolbarProps {
|
export function Toolbar(props: { children: React.ReactNode }) {
|
||||||
label: React.ReactNode;
|
const [minified, setMinified] = React.useState(true);
|
||||||
children: React.ReactNode;
|
const [showToolbarControls, setShowToolbarControls] = React.useState(false);
|
||||||
minified: boolean;
|
|
||||||
onMinifiedChange: (value: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Toolbar(props: ToolbarProps) {
|
|
||||||
const { children, label, minified, onMinifiedChange } = props;
|
|
||||||
const controls = useToolbarControls();
|
|
||||||
const [isReady, setIsReady] = React.useState(false);
|
const [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
|
// Wait for page to be ready, then show the toolbar
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -49,41 +31,16 @@ 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(() => {
|
React.useEffect(() => {
|
||||||
if (!isReady || autoExpandTriggeredRef.current) {
|
if (isReady) {
|
||||||
return;
|
const expandAfterTimeout = setTimeout(() => {
|
||||||
|
setMinified(false);
|
||||||
|
}, DURATION_LOGO_APPEARANCE + DELAY_BETWEEN_LOGO_AND_CONTENT);
|
||||||
|
|
||||||
|
return () => clearTimeout(expandAfterTimeout);
|
||||||
}
|
}
|
||||||
|
}, [isReady]);
|
||||||
if (!shouldAutoExpand) {
|
|
||||||
// When we already know the toolbar should stay expanded (e.g. the user previously
|
|
||||||
// opened it this session) we short-circuit the auto-expand animation and immediately
|
|
||||||
// render the expanded state without replaying the logo animation.
|
|
||||||
autoExpandTriggeredRef.current = true;
|
|
||||||
setShouldAnimateLogo(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
autoExpandTriggeredRef.current = true;
|
|
||||||
|
|
||||||
// On a fresh session we let the toolbar appear in its compact form, play the logo
|
|
||||||
// animation, and only then expand the toolbar. The timeout mirrors the duration of the
|
|
||||||
// logo animation so both transitions feel connected.
|
|
||||||
const expandAfterTimeout = setTimeout(() => {
|
|
||||||
setShouldAnimateLogo(false);
|
|
||||||
onMinifiedChange(false);
|
|
||||||
}, DURATION_LOGO_APPEARANCE + DELAY_BETWEEN_LOGO_AND_CONTENT);
|
|
||||||
|
|
||||||
return () => clearTimeout(expandAfterTimeout);
|
|
||||||
}, [isReady, onMinifiedChange, shouldAutoExpand]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!minified) {
|
|
||||||
// Any manual expansion should stop the logo animation so the icon stays in its
|
|
||||||
// “settled” state once the toolbar is open.
|
|
||||||
setShouldAnimateLogo(false);
|
|
||||||
}
|
|
||||||
}, [minified]);
|
|
||||||
|
|
||||||
// Don't render anything until page is ready
|
// Don't render anything until page is ready
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
@@ -91,47 +48,61 @@ export function Toolbar(props: ToolbarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label}>
|
<motion.div
|
||||||
<motion.div className="-translate-x-1/2 fixed bottom-5 left-1/2 z-40 w-auto max-w-xl transform px-4">
|
onMouseEnter={() => setShowToolbarControls(true)}
|
||||||
<AnimatePresence mode="wait">
|
onMouseLeave={() => setShowToolbarControls(false)}
|
||||||
<motion.div
|
className="-translate-x-1/2 fixed bottom-5 left-1/2 z-40 w-auto max-w-xl transform px-4"
|
||||||
onClick={() => {
|
>
|
||||||
if (minified) {
|
<AnimatePresence mode="wait">
|
||||||
setShouldAnimateLogo(false);
|
<motion.div
|
||||||
onMinifiedChange(false);
|
onClick={() => {
|
||||||
}
|
if (minified) {
|
||||||
}}
|
setMinified((prev) => !prev);
|
||||||
layout
|
}
|
||||||
transition={toolbarEasings.spring}
|
}}
|
||||||
className={tcls(
|
layout
|
||||||
minified ? 'cursor-pointer px-2' : 'pr-2 pl-3.5',
|
transition={toolbarEasings.spring}
|
||||||
'flex',
|
className={tcls(
|
||||||
'items-center',
|
minified ? 'cursor-pointer px-2' : 'pr-2 pl-3.5',
|
||||||
'justify-center',
|
'flex',
|
||||||
'min-h-11',
|
'items-center',
|
||||||
'min-w-12',
|
'justify-center',
|
||||||
'h-12',
|
'min-h-11',
|
||||||
'py-2',
|
'min-w-12',
|
||||||
'backdrop-blur-sm',
|
'h-12',
|
||||||
'origin-center',
|
'py-2',
|
||||||
'border-[0.5px] border-neutral-5 border-solid dark:border-neutral-8',
|
'border-tint-1/3',
|
||||||
'bg-[linear-gradient(45deg,rgba(39,39,39,0.8)_100%,rgba(39,39,39,0.4)_80%)]',
|
'backdrop-blur-sm',
|
||||||
'dark:bg-[linear-gradient(45deg,rgba(39,39,39,0.5)_100%,rgba(39,39,39,0.3)_80%)]'
|
'origin-center',
|
||||||
)}
|
'bg-[linear-gradient(110deg,rgba(20,23,28,0.90)_0%,rgba(20,23,28,0.80)_100%)]',
|
||||||
style={{
|
'dark:bg-[linear-gradient(110deg,rgba(256,256,256,0.90)_0%,rgba(256,256,256,0.80)_100%)]'
|
||||||
borderRadius: '100px', // This is set on `style` so Framer Motion can correct for distortions
|
)}
|
||||||
}}
|
initial={{
|
||||||
>
|
scale: 1,
|
||||||
{/* Logo with stroke segments animation in blue-tints */}
|
opacity: 1,
|
||||||
<motion.div layout>
|
}}
|
||||||
<AnimatedLogo shouldAnimate={shouldAnimateLogo} />
|
animate={{
|
||||||
</motion.div>
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
{!minified ? children : null}
|
boxShadow: minified
|
||||||
|
? '0 4px 40px 8px rgba(0, 0, 0, .2), 0 0 0 .5px rgba(0, 0, 0, .4), inset 0 .5px 0 0 hsla(0, 0%, 100%, .15)'
|
||||||
|
: '0 4px 40px 8px rgba(0, 0, 0, .4), 0 0 0 .5px rgba(0, 0, 0, .8), inset 0 .5px 0 0 hsla(0, 0%, 100%, .3)',
|
||||||
|
}}
|
||||||
|
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>
|
</motion.div>
|
||||||
</AnimatePresence>
|
|
||||||
</motion.div>
|
{!minified ? props.children : null}
|
||||||
</Tooltip>
|
|
||||||
|
{!minified && showToolbarControls && <MinifyButton setMinified={setMinified} />}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,15 +129,12 @@ export function ToolbarButtonGroup(props: { children: React.ReactNode }) {
|
|||||||
className="flex items-center gap-1 overflow-visible pr-2 pl-4"
|
className="flex items-center gap-1 overflow-visible pr-2 pl-4"
|
||||||
>
|
>
|
||||||
{buttonChildren.map((child, index) => {
|
{buttonChildren.map((child, index) => {
|
||||||
|
const motionValues = buttonMotionValues[index];
|
||||||
const childEl = child as React.ReactElement;
|
const childEl = child as React.ReactElement;
|
||||||
const childKey = childEl.key ?? `toolbar-button-${index}`;
|
return React.cloneElement(childEl, {
|
||||||
return (
|
key: index,
|
||||||
<ToolbarButtonWrapper
|
motionValues,
|
||||||
key={childKey}
|
});
|
||||||
child={childEl}
|
|
||||||
rawMotionValues={buttonMotionValues[index]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
@@ -180,27 +148,15 @@ export interface ToolbarButtonProps extends Omit<React.HTMLProps<HTMLAnchorEleme
|
|||||||
icon: IconName;
|
icon: IconName;
|
||||||
iconClassName?: string;
|
iconClassName?: string;
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps>((props, ref) => {
|
export function ToolbarButton(props: ToolbarButtonProps) {
|
||||||
const {
|
const { title, disabled, motionValues, className, style, href, onClick, icon, iconClassName } =
|
||||||
title,
|
props;
|
||||||
disabled,
|
|
||||||
motionValues,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
href,
|
|
||||||
onClick,
|
|
||||||
icon,
|
|
||||||
iconClassName,
|
|
||||||
children,
|
|
||||||
} = props;
|
|
||||||
const reduceMotion = useReducedMotion();
|
const reduceMotion = useReducedMotion();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div variants={toolbarEasings.staggeringChild} className="relative" ref={ref}>
|
<motion.div variants={toolbarEasings.staggeringChild}>
|
||||||
{children ? children : null}
|
|
||||||
<Tooltip label={title}>
|
<Tooltip label={title}>
|
||||||
<motion.a
|
<motion.a
|
||||||
href={href}
|
href={href}
|
||||||
@@ -233,77 +189,39 @@ export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps
|
|||||||
'gap-1',
|
'gap-1',
|
||||||
'text-sm',
|
'text-sm',
|
||||||
'rounded-full',
|
'rounded-full',
|
||||||
|
'border-neutral-500',
|
||||||
|
'outline-neutral-800',
|
||||||
|
'outline-1',
|
||||||
|
'border',
|
||||||
'truncate',
|
'truncate',
|
||||||
'text-tint-1',
|
'text-tint-1',
|
||||||
'dark:text-tint-12',
|
'dark:text-tint-12',
|
||||||
'cursor-pointer',
|
'cursor-pointer',
|
||||||
'transition-colors',
|
'transition-colors',
|
||||||
'size-8',
|
'size-8',
|
||||||
|
'bg-tint-1/3',
|
||||||
|
'hover:bg-tint-1/4',
|
||||||
|
'dark:bg-tint-3',
|
||||||
|
'dark:hover:bg-tint-1',
|
||||||
disabled ? 'cursor-not-allowed opacity-50' : '',
|
disabled ? 'cursor-not-allowed opacity-50' : '',
|
||||||
'border border-[rgba(256,_256,_256,_0.06)] border-solid',
|
'shadow-1xs'
|
||||||
'bg-[linear-gradient(45deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon icon={icon} className={tcls('size-4', iconClassName)} />
|
||||||
icon={icon}
|
|
||||||
iconStyle={IconStyle.Solid}
|
|
||||||
className={tcls(
|
|
||||||
'size-4 shrink-0 group-hover:scale-110 group-hover:text-tint-3',
|
|
||||||
iconClassName
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</motion.a>
|
</motion.a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
ToolbarButton.displayName = 'ToolbarButton';
|
|
||||||
|
|
||||||
function ToolbarButtonWrapper(props: {
|
|
||||||
child: React.ReactElement;
|
|
||||||
rawMotionValues?: { scale: MotionValue<number>; x: MotionValue<number> };
|
|
||||||
}) {
|
|
||||||
const { child, rawMotionValues } = props;
|
|
||||||
|
|
||||||
// Convert the raw motion values to smooth spring easings
|
|
||||||
const springScale = useSpring(rawMotionValues?.scale.get() ?? 1, {
|
|
||||||
stiffness: 400,
|
|
||||||
damping: 30,
|
|
||||||
});
|
|
||||||
const springX = useSpring(rawMotionValues?.x.get() ?? 0, { stiffness: 400, damping: 30 });
|
|
||||||
|
|
||||||
// Sync springs with raw motion values
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!rawMotionValues) return;
|
|
||||||
|
|
||||||
const unsubScale = rawMotionValues.scale.on('change', (v) => springScale.set(v));
|
|
||||||
const unsubX = rawMotionValues.x.on('change', (v) => springX.set(v));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubScale();
|
|
||||||
unsubX();
|
|
||||||
};
|
|
||||||
}, [rawMotionValues, springScale, springX]);
|
|
||||||
|
|
||||||
const motionValues = {
|
|
||||||
scale: springScale,
|
|
||||||
x: springX,
|
|
||||||
};
|
|
||||||
|
|
||||||
return React.cloneElement(child, {
|
|
||||||
motionValues,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarSeparator() {
|
export function ToolbarSeparator() {
|
||||||
return <div className="h-5 w-px bg-tint-1/3" />;
|
return <div className="h-5 w-px bg-tint-1/3" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarTitle(props: { prefix?: string; suffix: string }) {
|
export function ToolbarTitle(props: { prefix: string; suffix: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 text-xs ">
|
<div className="flex items-center gap-1 text-xs ">
|
||||||
{props.prefix ? <ToolbarTitlePrefix title={props.prefix} /> : null}
|
<ToolbarTitlePrefix title={props.prefix} />
|
||||||
<ToolbarTitleSuffix title={props.suffix} />
|
<ToolbarTitleSuffix title={props.suffix} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -313,7 +231,7 @@ function ToolbarTitlePrefix(props: { title: string }) {
|
|||||||
return (
|
return (
|
||||||
<motion.span
|
<motion.span
|
||||||
{...getCopyVariants(0)}
|
{...getCopyVariants(0)}
|
||||||
className="truncate font-medium text-neutral-1 dark:text-neutral-12"
|
className="font-light text-neutral-7 dark:text-neutral-3"
|
||||||
>
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
@@ -324,7 +242,7 @@ function ToolbarTitleSuffix(props: { title: string }) {
|
|||||||
return (
|
return (
|
||||||
<motion.span
|
<motion.span
|
||||||
{...getCopyVariants(1)}
|
{...getCopyVariants(1)}
|
||||||
className="max-w-[20ch] truncate text-neutral-1 dark:text-neutral-12"
|
className="max-w-[24ch] truncate font-semibold text-neutral-3 dark:text-neutral-2"
|
||||||
>
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
@@ -335,9 +253,36 @@ export function ToolbarSubtitle(props: { subtitle: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<motion.span
|
<motion.span
|
||||||
{...getCopyVariants(1)}
|
{...getCopyVariants(1)}
|
||||||
className="text-neutral-1/80 text-xxs dark:text-neutral-12/80"
|
className="text-neutral-7 text-xxs dark:text-neutral-2"
|
||||||
>
|
>
|
||||||
{props.subtitle}
|
{props.subtitle}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MinifyButton(props: { setMinified: (minified: boolean) => void }) {
|
||||||
|
return (
|
||||||
|
<Tooltip label="Minify">
|
||||||
|
<motion.div
|
||||||
|
{...minifyButtonAnimation}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
}}
|
||||||
|
whileHover={{
|
||||||
|
scale: 1.05,
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
props.setMinified(true);
|
||||||
|
}}
|
||||||
|
className={tcls(
|
||||||
|
'-top-2 -right-4 absolute flex size-4 cursor-pointer items-center justify-center rounded-full border',
|
||||||
|
'border-neutral-500 bg-neutral-700 hover:border-neutral-400 hover:bg-neutral-600',
|
||||||
|
'dark:border-neutral-400 dark:bg-neutral-200 dark:hover:border-neutral-200 dark:hover:bg-neutral-100'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon="minus" className="size-2 text-neutral-1 dark:text-neutral-9" />
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export interface ToolbarControlsContextValue {
|
|
||||||
minimize: () => void;
|
|
||||||
closeSession?: () => void;
|
|
||||||
closePersistent?: () => void;
|
|
||||||
shouldAutoExpand?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ToolbarControlsContext = React.createContext<ToolbarControlsContextValue | null>(null);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Provides reusable state setters (mainly for hiding/showing the toolbar) for the toolbar controls propagated through to the children
|
|
||||||
*/
|
|
||||||
export function ToolbarControlsProvider(
|
|
||||||
props: React.PropsWithChildren<{ value: ToolbarControlsContextValue | null }>
|
|
||||||
) {
|
|
||||||
const { children, value } = props;
|
|
||||||
return (
|
|
||||||
<ToolbarControlsContext.Provider value={value}>{children}</ToolbarControlsContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useToolbarControls() {
|
|
||||||
return React.useContext(ToolbarControlsContext);
|
|
||||||
}
|
|
||||||
@@ -3,4 +3,3 @@ export * from './AdminToolbarClient';
|
|||||||
export * from './IframeWrapper';
|
export * from './IframeWrapper';
|
||||||
export * from './Toolbar';
|
export * from './Toolbar';
|
||||||
export * from './transitions';
|
export * from './transitions';
|
||||||
export * from './utils';
|
|
||||||
|
|||||||
@@ -54,7 +54,4 @@ export type AdminToolbarContext = {
|
|||||||
|
|
||||||
export interface AdminToolbarClientProps {
|
export interface AdminToolbarClientProps {
|
||||||
context: AdminToolbarContext;
|
context: AdminToolbarContext;
|
||||||
onSessionClose?: () => void;
|
|
||||||
onPersistentClose?: () => void;
|
|
||||||
onToggleMinify?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { type MotionValue, motionValue } from 'motion/react';
|
import { type MotionValue, motionValue } from 'framer-motion';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ButtonMotionValues {
|
||||||
|
scale: MotionValue<number>;
|
||||||
|
x: MotionValue<number>;
|
||||||
|
}
|
||||||
|
|
||||||
interface MagnificationConfig {
|
interface MagnificationConfig {
|
||||||
/** Size of each button in pixels - used for spacing calculations */
|
/** Size of each button in pixels - used for spacing calculations */
|
||||||
buttonSize?: number;
|
buttonSize?: number;
|
||||||
@@ -18,16 +23,21 @@ interface MagnificationConfig {
|
|||||||
|
|
||||||
const defaultConfig: Required<MagnificationConfig> = {
|
const defaultConfig: Required<MagnificationConfig> = {
|
||||||
buttonSize: 32, // Standard button size for 32px (size-8) buttons
|
buttonSize: 32, // Standard button size for 32px (size-8) buttons
|
||||||
maxScale: 1.25, // 25% scale increase - noticeable but not overwhelming
|
maxScale: 1.3, // 30% scale increase - noticeable but not overwhelming
|
||||||
influenceRadius: 80, // ~2.5 button widths of influence
|
influenceRadius: 80, // ~2.5 button widths of influence
|
||||||
spacingMultiplier: 1.3, // Creates proportional spacing to scale increase
|
spacingMultiplier: 1.3, // Creates proportional spacing to scale increase
|
||||||
scaleExponent: 2.5, // Exponential curve for dramatic close-range scaling
|
scaleExponent: 2.5, // Exponential curve for dramatic close-range scaling
|
||||||
padding: 10, // Small buffer zone around container edges
|
padding: 10, // Small buffer zone around container edges
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetMotionValues = (
|
// Helper functions for cleaner code
|
||||||
motionValues: Array<{ scale: MotionValue<number>; x: MotionValue<number> }>
|
const createMotionValues = (count: number): ButtonMotionValues[] =>
|
||||||
) => {
|
Array.from({ length: count }, () => ({
|
||||||
|
scale: motionValue(1),
|
||||||
|
x: motionValue(0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resetMotionValues = (motionValues: ButtonMotionValues[]) => {
|
||||||
motionValues.forEach(({ scale, x }) => {
|
motionValues.forEach(({ scale, x }) => {
|
||||||
scale.set(1);
|
scale.set(1);
|
||||||
x.set(0);
|
x.set(0);
|
||||||
@@ -36,17 +46,10 @@ const resetMotionValues = (
|
|||||||
|
|
||||||
const captureButtonPositions = (buttons: HTMLElement[]) => {
|
const captureButtonPositions = (buttons: HTMLElement[]) => {
|
||||||
// Reset transforms to get original positions
|
// Reset transforms to get original positions
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => (button.style.transform = ''));
|
||||||
button.style.transform = '';
|
|
||||||
});
|
|
||||||
return buttons.map((button) => {
|
return buttons.map((button) => {
|
||||||
const rect = button.getBoundingClientRect();
|
const rect = button.getBoundingClientRect();
|
||||||
return {
|
return { left: rect.left, width: rect.width };
|
||||||
left: rect.left,
|
|
||||||
width: rect.width,
|
|
||||||
top: rect.top,
|
|
||||||
height: rect.height,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,46 +59,26 @@ const calculateScale = (
|
|||||||
containerRect: DOMRect,
|
containerRect: DOMRect,
|
||||||
config: Required<MagnificationConfig>
|
config: Required<MagnificationConfig>
|
||||||
) => {
|
) => {
|
||||||
// Calculate only X-axis distance from mouse to button center
|
|
||||||
const distance = Math.abs(mouseX - buttonCenterX);
|
const distance = Math.abs(mouseX - buttonCenterX);
|
||||||
|
|
||||||
if (distance > config.influenceRadius) return 1;
|
if (distance > config.influenceRadius) return 1;
|
||||||
|
|
||||||
// Calculate distance from container edge (X-axis only)
|
// Edge influence to reduce scaling near container edges
|
||||||
const distanceFromEdgeX = Math.min(mouseX - containerRect.left, containerRect.right - mouseX);
|
const distanceFromEdge = Math.min(mouseX - containerRect.left, containerRect.right - mouseX);
|
||||||
const distanceFromEdge = distanceFromEdgeX;
|
const edgeInfluence = Math.min(distanceFromEdge / 20, 1);
|
||||||
|
|
||||||
// Define the "heart" zone - 8px from center (16x16px total area)
|
// Exponential scaling curve
|
||||||
const heartZoneRadius = 8;
|
const progress = distance / config.influenceRadius;
|
||||||
|
const exponentialProgress = (1 - progress) ** config.scaleExponent;
|
||||||
|
const baseScale = 1 + (config.maxScale - 1) * exponentialProgress;
|
||||||
|
|
||||||
let baseScale: number;
|
return 1 + (baseScale - 1) * edgeInfluence;
|
||||||
|
|
||||||
if (distance <= heartZoneRadius) {
|
|
||||||
// Within the heart zone - always at max scale
|
|
||||||
baseScale = config.maxScale;
|
|
||||||
} else {
|
|
||||||
// Outside heart zone - scale from max to 1 based on distance
|
|
||||||
const outerRadius = config.influenceRadius - heartZoneRadius;
|
|
||||||
const outerDistance = distance - heartZoneRadius;
|
|
||||||
const progress = Math.min(outerDistance / outerRadius, 1);
|
|
||||||
const exponentialProgress = (1 - progress) ** config.scaleExponent;
|
|
||||||
baseScale = 1 + (config.maxScale - 1) * exponentialProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only apply entry transition if we're very close to the edge (within 10px)
|
|
||||||
// This prevents the entry logic from interfering with normal scaling
|
|
||||||
if (distanceFromEdge < 10) {
|
|
||||||
const entryProgress = distanceFromEdge / 10;
|
|
||||||
return 1 + (baseScale - 1) * entryProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseScale;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateSpacing = (
|
const calculateSpacing = (
|
||||||
buttonIndex: number,
|
buttonIndex: number,
|
||||||
buttonEffects: Array<{ scale: number; translateX: number }>,
|
buttonEffects: Array<{ scale: number; translateX: number }>,
|
||||||
positions: Array<{ left: number; width: number; top: number; height: number }>,
|
positions: Array<{ left: number; width: number }>,
|
||||||
config: Required<MagnificationConfig>
|
config: Required<MagnificationConfig>
|
||||||
) => {
|
) => {
|
||||||
const currentPos = positions[buttonIndex];
|
const currentPos = positions[buttonIndex];
|
||||||
@@ -140,19 +123,10 @@ export function useMagnificationEffect(props: {
|
|||||||
config?: MagnificationConfig;
|
config?: MagnificationConfig;
|
||||||
}) {
|
}) {
|
||||||
const { childrenCount, containerRef, config } = props;
|
const { childrenCount, containerRef, config } = props;
|
||||||
const originalPositionsRef = React.useRef<
|
const [buttonMotionValues, setButtonMotionValues] = React.useState<ButtonMotionValues[]>([]);
|
||||||
Array<{ left: number; width: number; top: number; height: number }>
|
const originalPositionsRef = React.useRef<Array<{ left: number; width: number }>>([]);
|
||||||
>([]);
|
|
||||||
|
|
||||||
const finalConfig = React.useMemo(() => ({ ...defaultConfig, ...config }), [config]);
|
const finalConfig = { ...defaultConfig, ...config };
|
||||||
|
|
||||||
// Create basic motion values that will be consumed by springs in the components
|
|
||||||
const buttonMotionValues = React.useMemo(() => {
|
|
||||||
return Array.from({ length: childrenCount }, () => ({
|
|
||||||
scale: motionValue(1),
|
|
||||||
x: motionValue(0),
|
|
||||||
}));
|
|
||||||
}, [childrenCount]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
@@ -167,6 +141,11 @@ export function useMagnificationEffect(props: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize motion values if button count changed
|
||||||
|
if (buttonMotionValues.length !== buttons.length) {
|
||||||
|
setButtonMotionValues(createMotionValues(buttons.length));
|
||||||
|
}
|
||||||
|
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
const buttons = Array.from(
|
const buttons = Array.from(
|
||||||
container.querySelectorAll('.toolbar-button')
|
container.querySelectorAll('.toolbar-button')
|
||||||
@@ -231,7 +210,17 @@ export function useMagnificationEffect(props: {
|
|||||||
container.removeEventListener('mousemove', handleMouseMove);
|
container.removeEventListener('mousemove', handleMouseMove);
|
||||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
};
|
};
|
||||||
}, [finalConfig, containerRef, buttonMotionValues, childrenCount]);
|
}, [
|
||||||
|
finalConfig.buttonSize,
|
||||||
|
finalConfig.maxScale,
|
||||||
|
finalConfig.influenceRadius,
|
||||||
|
finalConfig.spacingMultiplier,
|
||||||
|
finalConfig.scaleExponent,
|
||||||
|
finalConfig.padding,
|
||||||
|
containerRef,
|
||||||
|
buttonMotionValues,
|
||||||
|
childrenCount,
|
||||||
|
]);
|
||||||
|
|
||||||
return { buttonMotionValues };
|
return { buttonMotionValues };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
node.data && 'fullWidth' in node.data && node.data.fullWidth
|
||||||
? 'max-w-screen-xl'
|
? 'max-w-screen-xl'
|
||||||
: 'max-w-3xl',
|
: '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,
|
blockStyle,
|
||||||
]}
|
]}
|
||||||
isEstimatedOffscreen={isOffscreen}
|
isEstimatedOffscreen={isOffscreen}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export function Caption(
|
|||||||
wrapperStyle?: ClassValue;
|
wrapperStyle?: ClassValue;
|
||||||
block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile;
|
block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile;
|
||||||
withBorder?: boolean;
|
withBorder?: boolean;
|
||||||
withFrame?: boolean;
|
|
||||||
} & DocumentContextProps
|
} & DocumentContextProps
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
@@ -34,7 +33,6 @@ export function Caption(
|
|||||||
context,
|
context,
|
||||||
fit = false,
|
fit = false,
|
||||||
withBorder = false,
|
withBorder = false,
|
||||||
withFrame = false,
|
|
||||||
wrapperStyle = [
|
wrapperStyle = [
|
||||||
'relative',
|
'relative',
|
||||||
'overflow-hidden',
|
'overflow-hidden',
|
||||||
@@ -46,7 +44,6 @@ export function Caption(
|
|||||||
withBorder
|
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'
|
? '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,
|
: null,
|
||||||
withFrame && 'p-2',
|
|
||||||
],
|
],
|
||||||
style,
|
style,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -65,14 +62,7 @@ export function Caption(
|
|||||||
return (
|
return (
|
||||||
<picture className={tcls('relative', style)}>
|
<picture className={tcls('relative', style)}>
|
||||||
<div className={tcls(wrapperStyle, 'mx-auto')}>{children}</div>
|
<div className={tcls(wrapperStyle, 'mx-auto')}>{children}</div>
|
||||||
<figcaption
|
<figcaption className={tcls('text-sm', 'text-center', 'mt-2', 'text-tint')}>
|
||||||
className={tcls(
|
|
||||||
'text-xs',
|
|
||||||
'text-center',
|
|
||||||
'text-tint',
|
|
||||||
withFrame ? 'mt-0.5 mb-2.5' : 'mt-2'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Inlines
|
<Inlines
|
||||||
nodes={captionParagraph.nodes}
|
nodes={captionParagraph.nodes}
|
||||||
document={document}
|
document={document}
|
||||||
|
|||||||
@@ -3,19 +3,16 @@
|
|||||||
import type { DocumentBlockCode } from '@gitbook/api';
|
import type { DocumentBlockCode } from '@gitbook/api';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
|
||||||
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
|
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
|
||||||
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
||||||
import { useDebounceCallback } from 'usehooks-ts';
|
import { useDebounceCallback } from 'usehooks-ts';
|
||||||
import type { BlockProps } from '../Block';
|
import type { BlockProps } from '../Block';
|
||||||
import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression';
|
|
||||||
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
import { CodeBlockRenderer } from './CodeBlockRenderer';
|
||||||
import type { HighlightLine, RenderedInline } from './highlight';
|
import type { HighlightLine, RenderedInline } from './highlight';
|
||||||
import { plainHighlight } from './plain-highlight';
|
import { plainHighlight } from './plain-highlight';
|
||||||
|
|
||||||
type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||||
inlines: RenderedInline[];
|
inlines: RenderedInline[];
|
||||||
inlineExprVariables: InlineExpressionVariables;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,21 +20,11 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
|
|||||||
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
||||||
*/
|
*/
|
||||||
export function ClientCodeBlock(props: ClientBlockProps) {
|
export function ClientCodeBlock(props: ClientBlockProps) {
|
||||||
const { block, style, inlines, inlineExprVariables } = props;
|
const { block, style, inlines } = props;
|
||||||
const blockRef = useRef<HTMLDivElement>(null);
|
const blockRef = useRef<HTMLDivElement>(null);
|
||||||
const isInViewportRef = useRef(false);
|
const isInViewportRef = useRef(false);
|
||||||
const [isInViewport, setIsInViewport] = useState(false);
|
const [isInViewport, setIsInViewport] = useState(false);
|
||||||
|
const plainLines = useMemo(() => plainHighlight(block, []), [block]);
|
||||||
const getAdaptiveVisitorClaims = useAdaptiveVisitor();
|
|
||||||
const visitorClaims = getAdaptiveVisitorClaims();
|
|
||||||
const evaluateInlineExpression = useEvaluateInlineExpression({
|
|
||||||
visitorClaims,
|
|
||||||
variables: inlineExprVariables,
|
|
||||||
});
|
|
||||||
const plainLines = useMemo(
|
|
||||||
() => plainHighlight(block, inlines, { evaluateInlineExpression }),
|
|
||||||
[block, inlines, evaluateInlineExpression]
|
|
||||||
);
|
|
||||||
const [lines, setLines] = useState<null | HighlightLine[]>(null);
|
const [lines, setLines] = useState<null | HighlightLine[]>(null);
|
||||||
const [highlighting, setHighlighting] = useState(false);
|
const [highlighting, setHighlighting] = useState(false);
|
||||||
|
|
||||||
@@ -93,7 +80,7 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
setHighlighting(true);
|
setHighlighting(true);
|
||||||
import('./highlight').then(({ highlight }) => {
|
import('./highlight').then(({ highlight }) => {
|
||||||
highlight(block, inlines, { evaluateInlineExpression }).then((lines) => {
|
highlight(block, inlines).then((lines) => {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -111,7 +98,7 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
|||||||
|
|
||||||
// Otherwise if the block is not in viewport, we reset to the plain lines
|
// Otherwise if the block is not in viewport, we reset to the plain lines
|
||||||
setLines(null);
|
setLines(null);
|
||||||
}, [isInViewport, block, inlines, evaluateInlineExpression]);
|
}, [isInViewport, block, inlines]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeBlockRenderer
|
<CodeBlockRenderer
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import type { DocumentBlockCode } from '@gitbook/api';
|
import type { DocumentBlockCode } from '@gitbook/api';
|
||||||
|
|
||||||
import { getNodeFragmentByType } from '@/lib/document';
|
import { getNodeFragmentByType } from '@/lib/document';
|
||||||
@@ -16,64 +14,32 @@ import { type RenderedInline, getInlines, highlight } from './highlight';
|
|||||||
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||||
const { block, document, style, isEstimatedOffscreen, context } = props;
|
const { block, document, style, isEstimatedOffscreen, context } = props;
|
||||||
const inlines = getInlines(block);
|
const inlines = getInlines(block);
|
||||||
|
const richInlines: RenderedInline[] = inlines.map((inline, index) => {
|
||||||
let hasInlineExpression = false;
|
const body = (() => {
|
||||||
|
const fragment = getNodeFragmentByType(inline.inline, 'annotation-body');
|
||||||
const richInlines: RenderedInline[] = inlines
|
if (!fragment) {
|
||||||
// Exclude inline expressions from rendered inline as they are rendered as code text once evaluated
|
return null;
|
||||||
// and so need to be treated as plain code tokens.
|
|
||||||
.filter((inline) => {
|
|
||||||
if (inline.inline.type === 'expression') {
|
|
||||||
hasInlineExpression = true;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return (
|
||||||
})
|
<Blocks
|
||||||
.map((inline, index) => {
|
key={index}
|
||||||
const body = (() => {
|
document={document}
|
||||||
const fragment = getNodeFragmentByType(inline.inline, 'annotation-body');
|
ancestorBlocks={[]}
|
||||||
if (!fragment) {
|
context={context}
|
||||||
return null;
|
nodes={fragment.nodes}
|
||||||
}
|
style="space-y-4"
|
||||||
return (
|
/>
|
||||||
<Blocks
|
);
|
||||||
key={index}
|
})();
|
||||||
document={document}
|
|
||||||
ancestorBlocks={[]}
|
|
||||||
context={context}
|
|
||||||
nodes={fragment.nodes}
|
|
||||||
style="space-y-4"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return { inline, body };
|
return { inline, body };
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isEstimatedOffscreen && !hasInlineExpression) {
|
if (!isEstimatedOffscreen) {
|
||||||
// In v2, we render the code block server-side
|
// In v2, we render the code block server-side
|
||||||
const lines = await highlight(block, richInlines);
|
const lines = await highlight(block, richInlines);
|
||||||
return <CodeBlockRenderer block={block} style={style} lines={lines} />;
|
return <CodeBlockRenderer block={block} style={style} lines={lines} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variables = context.contentContext
|
return <ClientCodeBlock block={block} style={style} inlines={richInlines} />;
|
||||||
? {
|
|
||||||
space: context.contentContext?.revision.variables,
|
|
||||||
page:
|
|
||||||
'page' in context.contentContext
|
|
||||||
? context.contentContext.page.variables
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Suspense fallback={null}>
|
|
||||||
<ClientCodeBlock
|
|
||||||
block={block}
|
|
||||||
style={style}
|
|
||||||
inlines={richInlines}
|
|
||||||
inlineExprVariables={variables}
|
|
||||||
/>
|
|
||||||
</React.Suspense>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
aria-busy={ariaBusy}
|
aria-busy={ariaBusy}
|
||||||
className={tcls('group/codeblock grid shrink grid-flow-col overflow-hidden', style)}
|
className={tcls('group/codeblock grid grid-flow-col', style)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
|
<div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
|
||||||
{title ? (
|
{title ? (
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { JSONDocument } from '@gitbook/api';
|
import type { DocumentBlockCode, JSONDocument } from '@gitbook/api';
|
||||||
import { useId } from 'react';
|
import { useId } from 'react';
|
||||||
|
|
||||||
import { CodeBlock } from './CodeBlock';
|
import { CodeBlock } from './CodeBlock';
|
||||||
import { convertCodeStringToBlock } from './utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plain code block with syntax highlighting.
|
* Plain code block with syntax highlighting.
|
||||||
@@ -12,7 +11,31 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
|||||||
const { code, syntax } = props;
|
const { code, syntax } = props;
|
||||||
const id = useId();
|
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 = {
|
const document: JSONDocument = {
|
||||||
object: 'document',
|
object: 'document',
|
||||||
|
|||||||
@@ -64,10 +64,7 @@ export async function preloadHighlight(block: DocumentBlockCode) {
|
|||||||
*/
|
*/
|
||||||
export async function highlight(
|
export async function highlight(
|
||||||
block: DocumentBlockCode,
|
block: DocumentBlockCode,
|
||||||
inlines: RenderedInline[],
|
inlines: RenderedInline[]
|
||||||
options?: {
|
|
||||||
evaluateInlineExpression?: (expr: string) => string;
|
|
||||||
}
|
|
||||||
): Promise<HighlightLine[]> {
|
): Promise<HighlightLine[]> {
|
||||||
const langName = getBlockLang(block);
|
const langName = getBlockLang(block);
|
||||||
|
|
||||||
@@ -77,10 +74,10 @@ export async function highlight(
|
|||||||
// - TEMP : language is PowerShell or C++ and browser is Safari:
|
// - TEMP : language is PowerShell or C++ and browser is Safari:
|
||||||
// RegExp#[Symbol.search] throws TypeError when `lastIndex` isn’t writable
|
// RegExp#[Symbol.search] throws TypeError when `lastIndex` isn’t writable
|
||||||
// Fixed in upcoming Safari 18.6, remove when it'll be released - RND-7772
|
// Fixed in upcoming Safari 18.6, remove when it'll be released - RND-7772
|
||||||
return plainHighlight(block, inlines, options);
|
return plainHighlight(block, inlines);
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = getPlainCodeBlock(block, undefined, options);
|
const code = getPlainCodeBlock(block);
|
||||||
|
|
||||||
const highlighter = await getSingletonHighlighter({
|
const highlighter = await getSingletonHighlighter({
|
||||||
langs: [langName],
|
langs: [langName],
|
||||||
@@ -258,17 +255,11 @@ function matchTokenAndInlines(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlainCodeBlock(
|
function getPlainCodeBlock(code: DocumentBlockCode, inlines?: InlineIndexed[]): string {
|
||||||
code: DocumentBlockCode,
|
|
||||||
inlines?: InlineIndexed[],
|
|
||||||
options?: {
|
|
||||||
evaluateInlineExpression?: (expr: string) => string;
|
|
||||||
}
|
|
||||||
): string {
|
|
||||||
let content = '';
|
let content = '';
|
||||||
|
|
||||||
code.nodes.forEach((node, index) => {
|
code.nodes.forEach((node, index) => {
|
||||||
const lineContent = getPlainCodeBlockLine(node, content.length, inlines, options);
|
const lineContent = getPlainCodeBlockLine(node, content.length, inlines);
|
||||||
content += lineContent;
|
content += lineContent;
|
||||||
|
|
||||||
if (index < code.nodes.length - 1) {
|
if (index < code.nodes.length - 1) {
|
||||||
@@ -282,10 +273,7 @@ function getPlainCodeBlock(
|
|||||||
function getPlainCodeBlockLine(
|
function getPlainCodeBlockLine(
|
||||||
parent: DocumentBlockCodeLine | DocumentInlineAnnotation,
|
parent: DocumentBlockCodeLine | DocumentInlineAnnotation,
|
||||||
index: number,
|
index: number,
|
||||||
inlines?: InlineIndexed[],
|
inlines?: InlineIndexed[]
|
||||||
options?: {
|
|
||||||
evaluateInlineExpression?: (expr: string) => string;
|
|
||||||
}
|
|
||||||
): string {
|
): string {
|
||||||
let content = '';
|
let content = '';
|
||||||
|
|
||||||
@@ -296,12 +284,7 @@ function getPlainCodeBlockLine(
|
|||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'annotation': {
|
case 'annotation': {
|
||||||
const start = index + content.length;
|
const start = index + content.length;
|
||||||
content += getPlainCodeBlockLine(
|
content += getPlainCodeBlockLine(node, index + content.length, inlines);
|
||||||
node,
|
|
||||||
index + content.length,
|
|
||||||
inlines,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
const end = index + content.length;
|
const end = index + content.length;
|
||||||
|
|
||||||
if (inlines) {
|
if (inlines) {
|
||||||
@@ -314,19 +297,6 @@ function getPlainCodeBlockLine(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'expression': {
|
case 'expression': {
|
||||||
const start = index + content.length;
|
|
||||||
const exprValue =
|
|
||||||
options?.evaluateInlineExpression?.(node.data.expression) ?? '';
|
|
||||||
content += exprValue;
|
|
||||||
const end = start + exprValue.length;
|
|
||||||
|
|
||||||
if (inlines) {
|
|
||||||
inlines.push({
|
|
||||||
inline: node,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -9,13 +9,9 @@ import type { HighlightLine, HighlightToken, RenderedInline } from './highlight'
|
|||||||
*/
|
*/
|
||||||
export function plainHighlight(
|
export function plainHighlight(
|
||||||
block: DocumentBlockCode,
|
block: DocumentBlockCode,
|
||||||
inlines: RenderedInline[],
|
inlines: RenderedInline[]
|
||||||
options?: {
|
|
||||||
evaluateInlineExpression?: (expr: string) => string;
|
|
||||||
}
|
|
||||||
): HighlightLine[] {
|
): HighlightLine[] {
|
||||||
const inlinesCopy = Array.from(inlines);
|
const inlinesCopy = Array.from(inlines);
|
||||||
|
|
||||||
return block.nodes.map((lineBlock) => {
|
return block.nodes.map((lineBlock) => {
|
||||||
const tokens: HighlightToken[] = lineBlock.nodes.map((node) => {
|
const tokens: HighlightToken[] = lineBlock.nodes.map((node) => {
|
||||||
if (node.object === 'text') {
|
if (node.object === 'text') {
|
||||||
@@ -24,14 +20,6 @@ export function plainHighlight(
|
|||||||
content: getNodeText(node),
|
content: getNodeText(node),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === 'expression') {
|
|
||||||
return {
|
|
||||||
type: 'plain',
|
|
||||||
content: options?.evaluateInlineExpression?.(node.data.expression) ?? '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const inline = inlinesCopy.shift();
|
const inline = inlinesCopy.shift();
|
||||||
return {
|
return {
|
||||||
type: 'annotation',
|
type: 'annotation',
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,57 +1,17 @@
|
|||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import { type DocumentBlockColumns, type Length, VerticalAlignment } from '@gitbook/api';
|
import { type DocumentBlockColumns, type Length, VerticalAlignment } from '@gitbook/api';
|
||||||
import React from 'react';
|
|
||||||
import type { BlockProps } from '../Block';
|
import type { BlockProps } from '../Block';
|
||||||
import { Blocks } from '../Blocks';
|
import { Blocks } from '../Blocks';
|
||||||
|
|
||||||
export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
||||||
const { block, style, ancestorBlocks, document, context } = props;
|
const { block, style, ancestorBlocks, document, context } = props;
|
||||||
|
|
||||||
const columnWidths = React.useMemo(() => {
|
|
||||||
const widths = block.nodes.map((block) => {
|
|
||||||
const width = block.data.width;
|
|
||||||
return width ? getFractionalWidth(width) : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalWidth = widths.reduce<number>((acc, width) => acc + width, 0);
|
|
||||||
// If not all columns widths are set, distribute the remaining widths as equally as we can
|
|
||||||
if (totalWidth < 1.0 && widths.some((width) => width === 0)) {
|
|
||||||
const unsetWidths = widths.filter((width) => width === 0);
|
|
||||||
let remainingWidth = 1.0 - totalWidth;
|
|
||||||
let unsetWidthsLength = unsetWidths.length;
|
|
||||||
widths.forEach((width, index) => {
|
|
||||||
if (width === 0) {
|
|
||||||
const calculatedWidth =
|
|
||||||
Math.round((remainingWidth / unsetWidthsLength) * COLUMN_DIVISIONS) /
|
|
||||||
COLUMN_DIVISIONS;
|
|
||||||
widths[index] = calculatedWidth; // Assign width to empty columns
|
|
||||||
unsetWidthsLength--;
|
|
||||||
remainingWidth -= calculatedWidth;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return widths;
|
|
||||||
}, [block.nodes]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
|
||||||
className={tcls(
|
{block.nodes.map((columnBlock) => {
|
||||||
'grid w-full grid-cols-1 gap-x-8 gap-y-4 md:grid-flow-col md:grid-cols-[repeat(var(--grid-slices),minmax(0,1fr))] md:grid-rows-1',
|
|
||||||
style
|
|
||||||
)}
|
|
||||||
style={{ '--grid-slices': COLUMN_DIVISIONS } as React.CSSProperties}
|
|
||||||
>
|
|
||||||
{block.nodes.map((columnBlock, index) => {
|
|
||||||
const columnWidth = columnWidths[index];
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
key={columnBlock.key}
|
key={columnBlock.key}
|
||||||
width={
|
width={columnBlock.data.width}
|
||||||
columnBlock.data.width ??
|
|
||||||
(columnWidth
|
|
||||||
? { value: columnWidth * 100, unit: '%' }
|
|
||||||
: { value: 0, unit: '%' })
|
|
||||||
}
|
|
||||||
verticalAlignment={columnBlock.data.verticalAlignment}
|
verticalAlignment={columnBlock.data.verticalAlignment}
|
||||||
>
|
>
|
||||||
<Blocks
|
<Blocks
|
||||||
@@ -71,11 +31,11 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
|||||||
|
|
||||||
export function Column(props: {
|
export function Column(props: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
width: Length;
|
width?: Length;
|
||||||
verticalAlignment?: VerticalAlignment;
|
verticalAlignment?: VerticalAlignment;
|
||||||
}) {
|
}) {
|
||||||
const { width, verticalAlignment } = props;
|
const { width, verticalAlignment } = props;
|
||||||
const { className, style } = transformLengthToCSS(width) ?? {};
|
const { className, style } = transformLengthToCSS(width);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
@@ -93,25 +53,41 @@ export function Column(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMN_DIVISIONS = 12;
|
function transformLengthToCSS(length: Length | undefined) {
|
||||||
|
if (!length) {
|
||||||
export function transformLengthToCSS(length: Length) {
|
return { className: ['md:w-full'] }; // default to full width if no length is specified
|
||||||
if (typeof length === 'number') {
|
|
||||||
return; // not implemented yet with non-percentage lengths
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof length === 'number') {
|
||||||
|
return { style: undefined }; // not implemented yet with non-percentage lengths
|
||||||
|
}
|
||||||
|
|
||||||
if (length.unit === '%') {
|
if (length.unit === '%') {
|
||||||
return {
|
return {
|
||||||
className: 'md:flex-shrink-0 md:[grid-column:var(--grid-col)]',
|
className: [
|
||||||
style: {
|
'md:shrink-0',
|
||||||
'--grid-col': `auto / span ${Math.round(length.value * 0.01 * COLUMN_DIVISIONS)}`,
|
COLUMN_WIDTHS[Math.round(length.value * 0.01 * (COLUMN_WIDTHS.length - 1))],
|
||||||
} as React.CSSProperties,
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { style: undefined }; // not implemented yet with non-percentage lengths
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFractionalWidth(length: Length) {
|
// Tailwind CSS classes for column widths.
|
||||||
if (typeof length === 'number') {
|
// The index of the array corresponds to the percentage width of the column.
|
||||||
return 0;
|
const COLUMN_WIDTHS = [
|
||||||
}
|
'md:w-0',
|
||||||
return length.value / 100;
|
'md:w-1/12',
|
||||||
}
|
'md:w-2/12',
|
||||||
|
'md:w-3/12',
|
||||||
|
'md:w-4/12',
|
||||||
|
'md:w-5/12',
|
||||||
|
'md:w-6/12',
|
||||||
|
'md:w-7/12',
|
||||||
|
'md:w-8/12',
|
||||||
|
'md:w-9/12',
|
||||||
|
'md:w-10/12',
|
||||||
|
'md:w-11/12',
|
||||||
|
'md:w-full',
|
||||||
|
];
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
|||||||
id = context.getId ? context.getId(id) : id;
|
id = context.getId ? context.getId(id) : id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Details
|
<Details id={id} open={context.mode === 'print'} className={style}>
|
||||||
id={id}
|
|
||||||
open={context.mode === 'print'}
|
|
||||||
className={tcls('scroll-mt-(--content-scroll-margin)', style)}
|
|
||||||
>
|
|
||||||
<summary
|
<summary
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'cursor-pointer',
|
'cursor-pointer',
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { Icon, type IconName } from '@gitbook/icons';
|
|||||||
|
|
||||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { getSpaceLanguage, tString } from '@/intl/server';
|
|
||||||
import { languages } from '@/intl/translations';
|
|
||||||
import { Block, type BlockProps } from './Block';
|
import { Block, type BlockProps } from './Block';
|
||||||
import { Blocks } from './Blocks';
|
import { Blocks } from './Blocks';
|
||||||
import { getBlockTextStyle } from './spacing';
|
import { getBlockTextStyle } from './spacing';
|
||||||
@@ -20,12 +18,6 @@ export function Hint({
|
|||||||
const firstLine = getBlockTextStyle(firstNode);
|
const firstLine = getBlockTextStyle(firstNode);
|
||||||
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(firstNode.type);
|
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(firstNode.type);
|
||||||
|
|
||||||
const language = contextProps.context.contentContext
|
|
||||||
? getSpaceLanguage(contextProps.context.contentContext)
|
|
||||||
: languages.en;
|
|
||||||
|
|
||||||
const label = tString(language, `hint_${block.data.style}`);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
@@ -46,8 +38,6 @@ export function Hint({
|
|||||||
|
|
||||||
style
|
style
|
||||||
)}
|
)}
|
||||||
aria-label={label}
|
|
||||||
role="note"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import type { DocumentContext } from './DocumentView';
|
|||||||
export function Images(props: BlockProps<DocumentBlockImages>) {
|
export function Images(props: BlockProps<DocumentBlockImages>) {
|
||||||
const { document, block, style, context, isEstimatedOffscreen } = props;
|
const { document, block, style, context, isEstimatedOffscreen } = props;
|
||||||
|
|
||||||
const hasMultipleImages = block.nodes.length > 1;
|
const isMultipleImages = block.nodes.length > 1;
|
||||||
const { align = 'center', withFrame } = block.data;
|
const { align = 'center' } = block.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -24,15 +24,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
|||||||
align === 'center' && 'justify-center',
|
align === 'center' && 'justify-center',
|
||||||
align === 'right' && 'justify-end',
|
align === 'right' && 'justify-end',
|
||||||
align === 'left' && 'justify-start',
|
align === 'left' && 'justify-start',
|
||||||
hasMultipleImages && ['grid', 'grid-flow-col'],
|
isMultipleImages && ['grid', 'grid-flow-col']
|
||||||
withFrame && [
|
|
||||||
'rounded-2xl',
|
|
||||||
'border',
|
|
||||||
'border-[rgb(234,235,238)]',
|
|
||||||
'dark:border-[rgb(45,50,58)]',
|
|
||||||
'relative',
|
|
||||||
'overflow-hidden',
|
|
||||||
]
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{block.nodes.map((node: any, _i: number) => (
|
{block.nodes.map((node: any, _i: number) => (
|
||||||
@@ -44,7 +36,6 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
|
|||||||
siblings={block.nodes.length}
|
siblings={block.nodes.length}
|
||||||
context={context}
|
context={context}
|
||||||
isEstimatedOffscreen={isEstimatedOffscreen}
|
isEstimatedOffscreen={isEstimatedOffscreen}
|
||||||
withFrame={withFrame}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -71,9 +62,8 @@ async function ImageBlock(props: {
|
|||||||
context: DocumentContext;
|
context: DocumentContext;
|
||||||
siblings: number;
|
siblings: number;
|
||||||
isEstimatedOffscreen: boolean;
|
isEstimatedOffscreen: boolean;
|
||||||
withFrame?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const { block, context, isEstimatedOffscreen, withFrame } = props;
|
const { block, context, isEstimatedOffscreen } = props;
|
||||||
|
|
||||||
const [src, darkSrc] = await Promise.all([
|
const [src, darkSrc] = await Promise.all([
|
||||||
context.contentContext ? resolveContentRef(block.data.ref, context.contentContext) : null,
|
context.contentContext ? resolveContentRef(block.data.ref, context.contentContext) : null,
|
||||||
@@ -87,66 +77,33 @@ async function ImageBlock(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={tcls('relative', 'overflow-hidden')}>
|
<Caption {...props} fit>
|
||||||
{/* Frame grid */}
|
<Image
|
||||||
{withFrame && (
|
alt={block.data.alt ?? ''}
|
||||||
<div
|
sizes={imageBlockSizes}
|
||||||
className={tcls(
|
resize={context.contentContext?.imageResizer}
|
||||||
'absolute',
|
sources={{
|
||||||
'-top-0.5',
|
light: {
|
||||||
'-left-0.5',
|
src: src.href,
|
||||||
'right-px',
|
size: src.file?.dimensions,
|
||||||
'bottom-px',
|
},
|
||||||
'opacity-40',
|
dark: darkSrc
|
||||||
'dark:opacity-[0.1]',
|
? {
|
||||||
'bg-[length:24px_24px,24px_24px]',
|
src: darkSrc.href,
|
||||||
'bg-[linear-gradient(to_right,_rgb(234,235,238)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(234,235,238)_1px,_transparent_1px)]',
|
size: darkSrc.file?.dimensions,
|
||||||
'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'
|
: null,
|
||||||
)}
|
}}
|
||||||
/>
|
priority={isEstimatedOffscreen ? 'lazy' : 'high'}
|
||||||
)}
|
preload
|
||||||
|
zoom
|
||||||
{/* Shadow overlay */}
|
inlineStyle={{
|
||||||
{withFrame && (
|
maxWidth: '100%',
|
||||||
<div
|
width: getImageDimension(block.data.width, undefined),
|
||||||
className={tcls(
|
height: getImageDimension(block.data.height, 'auto'),
|
||||||
'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>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Caption {...props} fit>
|
|
||||||
<Image
|
|
||||||
alt={block.data.alt ?? ''}
|
|
||||||
sizes={imageBlockSizes}
|
|
||||||
resize={context.contentContext?.imageResizer}
|
|
||||||
sources={{
|
|
||||||
light: {
|
|
||||||
src: src.href,
|
|
||||||
size: src.file?.dimensions,
|
|
||||||
},
|
|
||||||
dark: darkSrc
|
|
||||||
? {
|
|
||||||
src: darkSrc.href,
|
|
||||||
size: darkSrc.file?.dimensions,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}}
|
|
||||||
priority={isEstimatedOffscreen ? 'lazy' : 'high'}
|
|
||||||
preload
|
|
||||||
zoom
|
|
||||||
inlineStyle={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
width: getImageDimension(block.data.width, undefined),
|
|
||||||
height: getImageDimension(block.data.height, 'auto'),
|
|
||||||
}}
|
|
||||||
style={withFrame ? 'rounded-xl' : undefined}
|
|
||||||
/>
|
|
||||||
</Caption>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Annotation } from './Annotation/Annotation';
|
|||||||
import type { DocumentContextProps } from './DocumentView';
|
import type { DocumentContextProps } from './DocumentView';
|
||||||
import { Emoji } from './Emoji';
|
import { Emoji } from './Emoji';
|
||||||
import { InlineButton } from './InlineButton';
|
import { InlineButton } from './InlineButton';
|
||||||
import { InlineExpression } from './InlineExpression';
|
|
||||||
import { InlineIcon } from './InlineIcon';
|
import { InlineIcon } from './InlineIcon';
|
||||||
import { InlineImage } from './InlineImage';
|
import { InlineImage } from './InlineImage';
|
||||||
import { InlineLink } from './InlineLink';
|
import { InlineLink } from './InlineLink';
|
||||||
@@ -52,7 +51,9 @@ export function Inline<T extends DocumentInline>(props: InlineProps<T>) {
|
|||||||
case 'icon':
|
case 'icon':
|
||||||
return <InlineIcon {...contextProps} inline={inline} />;
|
return <InlineIcon {...contextProps} inline={inline} />;
|
||||||
case 'expression':
|
case 'expression':
|
||||||
return <InlineExpression {...contextProps} inline={inline} />;
|
// The GitBook API should take care of evaluating expressions.
|
||||||
|
// We should never need to render them.
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
return nullIfNever(inline);
|
return nullIfNever(inline);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import type { DocumentInlineExpression } from '@gitbook/api';
|
|
||||||
import type { InlineProps } from '../Inline';
|
|
||||||
import { InlineExpressionValue } from './InlineExpressionValue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render an inline expression.
|
|
||||||
*/
|
|
||||||
export function InlineExpression(props: InlineProps<DocumentInlineExpression>) {
|
|
||||||
const { context, inline } = props;
|
|
||||||
|
|
||||||
const { data } = inline;
|
|
||||||
|
|
||||||
const variables = context.contentContext
|
|
||||||
? {
|
|
||||||
space: context.contentContext?.revision.variables,
|
|
||||||
page:
|
|
||||||
'page' in context.contentContext
|
|
||||||
? context.contentContext.page.variables
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Suspense fallback={null}>
|
|
||||||
<InlineExpressionValue expression={data.expression} variables={variables} />
|
|
||||||
</React.Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import type { InlineExpressionVariables } from './types';
|
|
||||||
import { useEvaluateInlineExpression } from './useEvaluateInlineExpression';
|
|
||||||
|
|
||||||
export function InlineExpressionValue(props: {
|
|
||||||
expression: string;
|
|
||||||
variables: InlineExpressionVariables;
|
|
||||||
}) {
|
|
||||||
const { expression, variables } = props;
|
|
||||||
|
|
||||||
const getAdaptiveVisitorClaims = useAdaptiveVisitor();
|
|
||||||
const visitorClaims = getAdaptiveVisitorClaims();
|
|
||||||
const evaluateInlineExpression = useEvaluateInlineExpression({
|
|
||||||
visitorClaims,
|
|
||||||
variables,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = useMemo(
|
|
||||||
() => evaluateInlineExpression(expression),
|
|
||||||
[expression, evaluateInlineExpression]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <>{result}</>;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './InlineExpression';
|
|
||||||
export * from './types';
|
|
||||||
export * from './useEvaluateInlineExpression';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import type { Variables } from '@gitbook/api';
|
|
||||||
|
|
||||||
export interface InlineExpressionVariables {
|
|
||||||
space?: Variables;
|
|
||||||
page?: Variables;
|
|
||||||
}
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
type AdaptiveVisitorClaims,
|
|
||||||
createExpressionEvaluationContext,
|
|
||||||
} from '@/components/Adaptive';
|
|
||||||
import type { Variables } from '@gitbook/api';
|
|
||||||
import { ExpressionRuntime, formatExpressionResult } from '@gitbook/expr';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook that returns a callback to evaluate an inline expression with visitor data
|
|
||||||
* and space/page variables as context.
|
|
||||||
*/
|
|
||||||
export function useEvaluateInlineExpression(args: {
|
|
||||||
visitorClaims: AdaptiveVisitorClaims | null;
|
|
||||||
variables: {
|
|
||||||
space?: Variables;
|
|
||||||
page?: Variables;
|
|
||||||
};
|
|
||||||
}) {
|
|
||||||
const { visitorClaims, variables } = args;
|
|
||||||
const evaluateInlineExpression = React.useMemo(() => {
|
|
||||||
const runtime = new ExpressionRuntime();
|
|
||||||
const evaluationContext = createExpressionEvaluationContext({
|
|
||||||
visitorClaims,
|
|
||||||
variables,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (expression: string) => {
|
|
||||||
try {
|
|
||||||
return formatExpressionResult(
|
|
||||||
runtime.evaluate(expression, evaluationContext) ?? ''
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to evaluate expression:', expression, err);
|
|
||||||
return `{{${expression}}}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [variables, visitorClaims]);
|
|
||||||
|
|
||||||
return evaluateInlineExpression;
|
|
||||||
}
|
|
||||||
@@ -1,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 { Icon, type IconName } from '@gitbook/icons';
|
||||||
import type { InlineProps } from './Inline';
|
import type { InlineProps } from './Inline';
|
||||||
import { textColorToStyle } from './utils/colors';
|
|
||||||
|
|
||||||
export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
|
export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
|
||||||
const { inline } = props;
|
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 (
|
return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />;
|
||||||
<Icon
|
|
||||||
icon={icon}
|
|
||||||
className={tcls('inline size-[1em]', color ? textColorToStyle[color] : null)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function InlineImage(props: InlineProps<DocumentInlineImage>) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
alt={inline.data.alt ?? inline.data.caption ?? ''}
|
alt={inline.data.caption ?? ''}
|
||||||
sizes={sizes}
|
sizes={sizes}
|
||||||
resize={context.contentContext?.imageResizer}
|
resize={context.contentContext?.imageResizer}
|
||||||
sources={{
|
sources={{
|
||||||
|
|||||||
@@ -143,11 +143,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-column-preview {
|
.openapi-column-preview {
|
||||||
@apply flex flex-col flex-1 xl:max-2xl:pt-20 lg:py-6 sticky max-h-[calc(100vh-var(--toc-top-offset))] top-(--toc-top-offset);
|
@apply flex flex-col flex-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-column-preview-body {
|
.openapi-column-preview-body {
|
||||||
@apply flex flex-col shrink overflow-hidden gap-4 print-mode:static;
|
@apply flex flex-col gap-4 sticky top-[calc(var(--toc-top-offset)+5rem)] print-mode:static;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-column-preview pre {
|
.openapi-column-preview pre {
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
.openapi-schema-name {
|
.openapi-schema-name {
|
||||||
/* To make double click on the property name select only the 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. */
|
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 {
|
.openapi-schema-name .openapi-deprecated {
|
||||||
@@ -207,10 +207,6 @@
|
|||||||
@apply line-through opacity-9;
|
@apply line-through opacity-9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-schema-discriminator {
|
|
||||||
@apply text-primary-subtle/9 text-[0.813rem] lowercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-schema-required {
|
.openapi-schema-required {
|
||||||
@apply text-warning-subtle text-[0.813rem] lowercase;
|
@apply text-warning-subtle text-[0.813rem] lowercase;
|
||||||
}
|
}
|
||||||
@@ -317,11 +313,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-securities-oauth-flows {
|
.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-oauth-content {
|
||||||
.openapi-securities-scopes {
|
|
||||||
@apply prose *:!prose-sm *:text-tint;
|
@apply prose *:!prose-sm *:text-tint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +324,7 @@
|
|||||||
@apply text-xs;
|
@apply text-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-securities-scopes ul {
|
.openapi-securities-oauth-content ul {
|
||||||
@apply !my-0;
|
@apply !my-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,21 +510,13 @@
|
|||||||
.openapi-panel,
|
.openapi-panel,
|
||||||
.openapi-codesample,
|
.openapi-codesample,
|
||||||
.openapi-response-examples {
|
.openapi-response-examples {
|
||||||
@apply border shrink min-h-40 overflow-hidden rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
|
@apply border rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-response-examples-panel {
|
|
||||||
@apply flex flex-col shrink overflow-hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-codesample-panel {
|
|
||||||
@apply flex flex-col shrink overflow-hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-panel pre,
|
.openapi-panel pre,
|
||||||
.openapi-codesample pre,
|
.openapi-codesample pre,
|
||||||
.openapi-response-examples pre {
|
.openapi-response-examples pre {
|
||||||
@apply bg-transparent border-none rounded-none shrink shadow-none;
|
@apply bg-transparent border-none rounded-none shadow-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-panel-heading {
|
.openapi-panel-heading {
|
||||||
@@ -592,7 +579,7 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select > button {
|
.openapi-select > button {
|
||||||
@apply flex items-center font-normal cursor-pointer *:truncate gap-1.5 text-tint-strong rounded text-xs p-1.5 leading-none border border-tint-subtle bg-tint;
|
@apply flex items-center font-normal cursor-pointer *:truncate gap-1.5 text-tint-strong max-w-32 rounded text-xs p-1.5 leading-none border border-tint-subtle bg-tint;
|
||||||
@apply hover:bg-tint-hover transition-all;
|
@apply hover:bg-tint-hover transition-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +613,7 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select-item {
|
.openapi-select-item {
|
||||||
@apply text-sm flex items-center cursor-pointer px-1.5 overflow-hidden py-1 text-tint ring-0 border-none rounded !outline-none;
|
@apply text-sm flex items-center cursor-pointer px-1.5 overflow-hidden py-1 *:truncate text-tint ring-0 border-none rounded !outline-none;
|
||||||
@apply hover:bg-tint-hover hover:theme-gradient:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current;
|
@apply hover:bg-tint-hover hover:theme-gradient:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,14 +638,6 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Section Components */
|
/* Section Components */
|
||||||
.openapi-section {
|
|
||||||
@apply flex flex-col overflow-hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-section-body {
|
|
||||||
@apply flex flex-col shrink overflow-hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-section-header {
|
.openapi-section-header {
|
||||||
@apply flex flex-row items-center;
|
@apply flex flex-row items-center;
|
||||||
}
|
}
|
||||||
@@ -841,7 +820,7 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-disclosure-trigger-label {
|
.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 {
|
.openapi-disclosure-trigger-label span {
|
||||||
|
|||||||
@@ -95,12 +95,8 @@ export async function RecordCard(
|
|||||||
}}
|
}}
|
||||||
sizes={[
|
sizes={[
|
||||||
{
|
{
|
||||||
media: '(max-width: 640px)',
|
|
||||||
width: view.cardSize === 'medium' ? 245 : 376,
|
width: view.cardSize === 'medium' ? 245 : 376,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
width: view.cardSize === 'medium' ? 490 : 752,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
resize={context.contentContext?.imageResizer}
|
resize={context.contentContext?.imageResizer}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
|
|||||||
@@ -71,18 +71,16 @@ function processCoverValue(value: DocumentTableImageRecord | string[] | null | u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageValue = value as DocumentTableImageRecord | null | undefined;
|
|
||||||
|
|
||||||
// Check if it's the new schema with objectFit
|
// Check if it's the new schema with objectFit
|
||||||
if (imageValue && typeof imageValue === 'object' && 'ref' in imageValue) {
|
if (value && typeof value === 'object' && 'ref' in value && 'objectFit' in value) {
|
||||||
return {
|
return {
|
||||||
contentRef: imageValue.ref,
|
contentRef: value.ref,
|
||||||
objectFit: imageValue.objectFit,
|
objectFit: value.objectFit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's a direct ContentRef
|
// It's a direct ContentRef
|
||||||
return { contentRef: imageValue || null };
|
return { contentRef: value as ContentRefFile | ContentRefURL };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import type {
|
|||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||||
import { backgroundColorToStyle, textColorToStyle } from './utils/colors';
|
|
||||||
|
|
||||||
export function Text(props: { text: DocumentText }) {
|
export function Text(props: { text: DocumentText }) {
|
||||||
const { text } = props;
|
const { text } = props;
|
||||||
@@ -122,3 +121,51 @@ function Color(props: MarkedLeafProps<DocumentMarkColor>) {
|
|||||||
</span>
|
</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'],
|
|
||||||
};
|
|
||||||
@@ -36,9 +36,13 @@ async function FooterLink(props: { link: CustomizationContentLink; context: GitB
|
|||||||
const { link, context } = props;
|
const { link, context } = props;
|
||||||
const resolved = await resolveContentRef(link.to, context);
|
const resolved = await resolveContentRef(link.to, context);
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={resolved?.href ?? '#'}
|
href={resolved.href}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'font-normal',
|
'font-normal',
|
||||||
'text-tint',
|
'text-tint',
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ export function CurrentContentIcon(
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
light: {
|
light: {
|
||||||
src: linker.toPathInSpace('~gitbook/icon?size=large&theme=light'),
|
src: linker.toPathInSpace('~gitbook/icon?size=medium&theme=light'),
|
||||||
size: { width: 256, height: 256 },
|
size: { width: 256, height: 256 },
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
src: linker.toPathInSpace('~gitbook/icon?size=large&theme=dark'),
|
src: linker.toPathInSpace('~gitbook/icon?size=medium&theme=dark'),
|
||||||
size: { width: 256, height: 256 },
|
size: { width: 256, height: 256 },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ export function Header(props: {
|
|||||||
const { context, withTopHeader, withVariants } = props;
|
const { context, withTopHeader, withVariants } = props;
|
||||||
const { siteSpace, siteSpaces, sections, customization } = context;
|
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 (
|
return (
|
||||||
<header
|
<header
|
||||||
id="site-header"
|
id="site-header"
|
||||||
@@ -63,7 +57,7 @@ export function Header(props: {
|
|||||||
'theme-bold:shadow-tint-12/2'
|
'theme-bold:shadow-tint-12/2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="transition-[padding] duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'gap-4',
|
'gap-4',
|
||||||
@@ -130,7 +124,10 @@ export function Header(props: {
|
|||||||
withSiteVariants={
|
withSiteVariants={
|
||||||
sections?.list.some(
|
sections?.list.some(
|
||||||
(s) =>
|
(s) =>
|
||||||
s.object === 'site-section' && s.siteSpaces.length > 1
|
s.object === 'site-section' &&
|
||||||
|
s.siteSpaces.filter(
|
||||||
|
(s) => s.space.language === siteSpace.space.language
|
||||||
|
).length > 1
|
||||||
) ?? false
|
) ?? false
|
||||||
}
|
}
|
||||||
withSections={!!sections}
|
withSections={!!sections}
|
||||||
@@ -150,7 +147,7 @@ export function Header(props: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{customization.header.links.length > 0 ||
|
{customization.header.links.length > 0 ||
|
||||||
(!withSections && withVariants === 'translations') ? (
|
(!sections && withVariants === 'translations') ? (
|
||||||
<HeaderLinks>
|
<HeaderLinks>
|
||||||
{customization.header.links.length > 0 ? (
|
{customization.header.links.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
@@ -170,7 +167,7 @@ export function Header(props: {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{!withSections && withVariants === 'translations' ? (
|
{!sections && withVariants === 'translations' ? (
|
||||||
<TranslationsDropdown
|
<TranslationsDropdown
|
||||||
context={context}
|
context={context}
|
||||||
siteSpace={siteSpace}
|
siteSpace={siteSpace}
|
||||||
@@ -184,19 +181,43 @@ export function Header(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sections && withSections ? (
|
{sections ? (
|
||||||
<div className="transition-[padding] duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||||
<SiteSectionTabs sections={encodeClientSiteSections(context, sections)}>
|
<div
|
||||||
{withVariants === 'translations' ? (
|
className={tcls(
|
||||||
<div className="before:contents[] flex self-start py-2 before:mr-4 before:border-tint before:border-l">
|
'w-full',
|
||||||
<TranslationsDropdown
|
'overflow-x-auto',
|
||||||
context={context}
|
'no-scrollbar',
|
||||||
siteSpace={siteSpace}
|
'-mb-4 pb-4', // Positive padding / negative margin allows the navigation menu indicator to show in a scroll viewƒ
|
||||||
siteSpaces={siteSpaces}
|
!sections ? ['hidden', 'page-no-toc:flex'] : 'flex'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={tcls(
|
||||||
|
CONTAINER_STYLE,
|
||||||
|
'grow',
|
||||||
|
'flex',
|
||||||
|
'items-end',
|
||||||
|
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{sections.list.some((s) => s.object === 'site-section-group') || // If there's even a single group, show the tabs
|
||||||
|
sections.list.length > 1 ? ( // Otherwise, show the tabs if there's more than one section
|
||||||
|
<SiteSectionTabs
|
||||||
|
sections={encodeClientSiteSections(context, sections)}
|
||||||
/>
|
/>
|
||||||
</div>
|
) : null}
|
||||||
) : null}
|
{withVariants === 'translations' ? (
|
||||||
</SiteSectionTabs>
|
<div className="site-background before:contents[] -mr-4 sm:-mr-6 md:-mr-8 sticky inset-y-0 right-0 z-10 ml-6 flex h-full items-center py-2 pr-4 before:mr-4 before:h-full before:border-tint before:border-l sm:pr-6 md:pr-8">
|
||||||
|
<TranslationsDropdown
|
||||||
|
context={context}
|
||||||
|
siteSpace={siteSpace}
|
||||||
|
siteSpaces={siteSpaces}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export async function HeaderLink(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!link.to) {
|
if (!target || !link.to) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ export async function HeaderLink(props: {
|
|||||||
headerPreset={headerPreset}
|
headerPreset={headerPreset}
|
||||||
title={link.title}
|
title={link.title}
|
||||||
isDropdown={false}
|
isDropdown={false}
|
||||||
href={target?.href}
|
href={target.href}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export type HeaderLinkNavItemProps = {
|
|||||||
linkStyle: NonNullable<CustomizationHeaderItem['style']>;
|
linkStyle: NonNullable<CustomizationHeaderItem['style']>;
|
||||||
headerPreset: CustomizationHeaderPreset;
|
headerPreset: CustomizationHeaderPreset;
|
||||||
title: string;
|
title: string;
|
||||||
href?: string;
|
href: string;
|
||||||
isDropdown: boolean;
|
isDropdown: boolean;
|
||||||
} & DropdownButtonProps<HTMLElement>;
|
} & DropdownButtonProps<HTMLElement>;
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
|
|||||||
const { linkTarget, headerPreset, title, isDropdown, href, ...rest } = props;
|
const { linkTarget, headerPreset, title, isDropdown, href, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href ?? '#'}
|
href={href}
|
||||||
className={getHeaderLinkClassName({ headerPreset })}
|
className={getHeaderLinkClassName({ headerPreset })}
|
||||||
insights={{
|
insights={{
|
||||||
type: 'link_click',
|
type: 'link_click',
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function TranslationsDropdown(props: {
|
|||||||
siteSpaces={siteSpaces}
|
siteSpaces={siteSpaces}
|
||||||
variant="blank"
|
variant="blank"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'-mx-2 bg-transparent px-2 md:py-1 lg:max-w-64 max-md:[&_.button-content]:hidden',
|
'-mx-2 bg-transparent px-2 py-1 lg:max-w-64 max-md:[&_.button-content]:hidden',
|
||||||
hasEmojiPrefix
|
hasEmojiPrefix
|
||||||
? 'md:[&_.button-leading-icon]:hidden' // If the title starts with an emoji, don't show the icon (on desktop)
|
? 'md:[&_.button-leading-icon]:hidden' // If the title starts with an emoji, don't show the icon (on desktop)
|
||||||
: '',
|
: '',
|
||||||
|
|||||||
@@ -18,22 +18,17 @@ import {
|
|||||||
ActionViewAsPDF,
|
ActionViewAsPDF,
|
||||||
} from './PageActions';
|
} from './PageActions';
|
||||||
|
|
||||||
export type PageActionsDropdownURLs = {
|
interface PageActionsDropdownProps {
|
||||||
html: string;
|
siteTitle: string;
|
||||||
markdown: string;
|
markdownPageURL: string;
|
||||||
mcp?: string;
|
mcpURL?: string;
|
||||||
pdf?: string;
|
pdfURL?: string;
|
||||||
|
className?: string;
|
||||||
|
actions: SiteCustomizationSettings['pageActions'];
|
||||||
editOnGit?: {
|
editOnGit?: {
|
||||||
provider: GitSyncState['installationProvider'];
|
provider: GitSyncState['installationProvider'];
|
||||||
url: string;
|
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.
|
* Return the list of actions to show in the dropdown menu.
|
||||||
*/
|
*/
|
||||||
function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNode[] {
|
function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNode[] {
|
||||||
const { siteTitle, urls, actions } = props;
|
const { siteTitle, markdownPageURL, mcpURL, actions } = props;
|
||||||
const assistants = useAI().assistants.filter(
|
const assistants = useAI().assistants.filter(
|
||||||
(assistant) => assistant.ui === true && assistant.pageAction
|
(assistant) => assistant.ui === true && assistant.pageAction
|
||||||
);
|
);
|
||||||
@@ -99,45 +94,55 @@ function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNod
|
|||||||
<DropdownMenuSeparator className="first:hidden" />
|
<DropdownMenuSeparator className="first:hidden" />
|
||||||
<ActionCopyMarkdown
|
<ActionCopyMarkdown
|
||||||
isDefaultAction={!assistants.length}
|
isDefaultAction={!assistants.length}
|
||||||
markdownPageURL={urls.markdown}
|
markdownPageURL={markdownPageURL}
|
||||||
type="dropdown-menu-item"
|
type="dropdown-menu-item"
|
||||||
/>
|
/>
|
||||||
<ActionViewAsMarkdown markdownPageURL={urls.markdown} type="dropdown-menu-item" />
|
<ActionViewAsMarkdown markdownPageURL={markdownPageURL} type="dropdown-menu-item" />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : null,
|
) : null,
|
||||||
|
|
||||||
actions.externalAI ? (
|
actions.externalAI ? (
|
||||||
<React.Fragment key="externalAI">
|
<React.Fragment key="externalAI">
|
||||||
<DropdownMenuSeparator className="first:hidden" />
|
<DropdownMenuSeparator className="first:hidden" />
|
||||||
<ActionOpenInLLM provider="chatgpt" url={urls.html} type="dropdown-menu-item" />
|
<ActionOpenInLLM
|
||||||
<ActionOpenInLLM provider="claude" url={urls.html} type="dropdown-menu-item" />
|
provider="chatgpt"
|
||||||
|
url={markdownPageURL}
|
||||||
|
type="dropdown-menu-item"
|
||||||
|
/>
|
||||||
|
<ActionOpenInLLM
|
||||||
|
provider="claude"
|
||||||
|
url={markdownPageURL}
|
||||||
|
type="dropdown-menu-item"
|
||||||
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : null,
|
) : null,
|
||||||
|
|
||||||
actions.mcp && urls.mcp ? (
|
actions.mcp && mcpURL ? (
|
||||||
<React.Fragment key="mcp">
|
<React.Fragment key="mcp">
|
||||||
<DropdownMenuSeparator className="first:hidden" />
|
<DropdownMenuSeparator className="first:hidden" />
|
||||||
<ActionCopyMCPURL mcpURL={urls.mcp} type="dropdown-menu-item" />
|
<ActionCopyMCPURL mcpURL={mcpURL} type="dropdown-menu-item" />
|
||||||
<ActionOpenMCP
|
<ActionOpenMCP
|
||||||
provider="vscode"
|
provider="vscode"
|
||||||
mcpURL={urls.mcp}
|
mcpURL={mcpURL}
|
||||||
siteTitle={siteTitle}
|
siteTitle={siteTitle}
|
||||||
type="dropdown-menu-item"
|
type="dropdown-menu-item"
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : null,
|
) : null,
|
||||||
|
|
||||||
urls.editOnGit || urls.pdf ? (
|
props.editOnGit || props.pdfURL ? (
|
||||||
<React.Fragment key="editOnGit">
|
<React.Fragment key="editOnGit">
|
||||||
<DropdownMenuSeparator className="first:hidden" />
|
<DropdownMenuSeparator className="first:hidden" />
|
||||||
{urls.editOnGit ? (
|
{props.editOnGit ? (
|
||||||
<ActionOpenEditOnGit
|
<ActionOpenEditOnGit
|
||||||
type="dropdown-menu-item"
|
type="dropdown-menu-item"
|
||||||
provider={urls.editOnGit.provider}
|
provider={props.editOnGit.provider}
|
||||||
url={urls.editOnGit.url}
|
url={props.editOnGit.url}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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>
|
</React.Fragment>
|
||||||
) : null,
|
) : null,
|
||||||
].filter(Boolean);
|
].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
|
* A default action shown as a quick-access button beside the dropdown menu
|
||||||
*/
|
*/
|
||||||
function getPageDefaultAction(props: PageActionsDropdownProps) {
|
function getPageDefaultAction(props: PageActionsDropdownProps) {
|
||||||
const { urls, actions } = props;
|
const { markdownPageURL, actions } = props;
|
||||||
const assistants = useAI().assistants.filter(
|
const assistants = useAI().assistants.filter(
|
||||||
(assistant) => assistant.ui === true && assistant.pageAction
|
(assistant) => assistant.ui === true && assistant.pageAction
|
||||||
);
|
);
|
||||||
@@ -157,12 +162,12 @@ function getPageDefaultAction(props: PageActionsDropdownProps) {
|
|||||||
return <ActionOpenAssistant assistant={assistant} type="button" />;
|
return <ActionOpenAssistant assistant={assistant} type="button" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urls.editOnGit) {
|
if (props.editOnGit) {
|
||||||
return (
|
return (
|
||||||
<ActionOpenEditOnGit
|
<ActionOpenEditOnGit
|
||||||
type="button"
|
type="button"
|
||||||
provider={urls.editOnGit.provider}
|
provider={props.editOnGit.provider}
|
||||||
url={urls.editOnGit.url}
|
url={props.editOnGit.url}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -171,7 +176,7 @@ function getPageDefaultAction(props: PageActionsDropdownProps) {
|
|||||||
return (
|
return (
|
||||||
<ActionCopyMarkdown
|
<ActionCopyMarkdown
|
||||||
isDefaultAction={!assistant}
|
isDefaultAction={!assistant}
|
||||||
markdownPageURL={urls.markdown}
|
markdownPageURL={markdownPageURL}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export function PageAside(props: {
|
|||||||
<aside
|
<aside
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'group/aside',
|
'group/aside',
|
||||||
'order-last',
|
|
||||||
'hidden',
|
'hidden',
|
||||||
'pt-8',
|
'pt-8',
|
||||||
'pb-4',
|
'pb-4',
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
|
|||||||
'opacity-8',
|
'opacity-8',
|
||||||
'contrast-more:opacity-11',
|
'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-l',
|
||||||
'sidebar-list-default:border-tint',
|
'sidebar-list-default:border-tint',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ export function PageBody(props: {
|
|||||||
<main
|
<main
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'relative min-w-0 flex-1',
|
'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.
|
// Allow words to break if they are too long.
|
||||||
'break-anywhere',
|
'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',
|
siteWidthWide ? 'site-width-wide' : 'site-width-default',
|
||||||
page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
|
page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
|
||||||
)}
|
)}
|
||||||
@@ -81,10 +81,7 @@ export function PageBody(props: {
|
|||||||
blockStyle="page-api-block:ml-0"
|
blockStyle="page-api-block:ml-0"
|
||||||
context={{
|
context={{
|
||||||
mode: 'default',
|
mode: 'default',
|
||||||
contentContext: {
|
contentContext: context,
|
||||||
...context,
|
|
||||||
page,
|
|
||||||
},
|
|
||||||
withLinkPreviews,
|
withLinkPreviews,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import { type RevisionPageDocument, SiteVisibility } from '@gitbook/api';
|
|||||||
import { Icon } from '@gitbook/icons';
|
import { Icon } from '@gitbook/icons';
|
||||||
import urlJoin from 'url-join';
|
import urlJoin from 'url-join';
|
||||||
import { getPDFURLSearchParams } from '../PDF';
|
import { getPDFURLSearchParams } from '../PDF';
|
||||||
import {
|
import { PageActionsDropdown } from '../PageActions/PageActionsDropdown';
|
||||||
PageActionsDropdown,
|
|
||||||
type PageActionsDropdownURLs,
|
|
||||||
} from '../PageActions/PageActionsDropdown';
|
|
||||||
import { PageIcon } from '../PageIcon';
|
import { PageIcon } from '../PageIcon';
|
||||||
import { StyledLink } from '../primitives';
|
import { StyledLink } from '../primitives';
|
||||||
|
|
||||||
@@ -43,7 +40,35 @@ export async function PageHeader(props: {
|
|||||||
// Show page actions if *any* of the actions are enabled
|
// Show page actions if *any* of the actions are enabled
|
||||||
<PageActionsDropdown
|
<PageActionsDropdown
|
||||||
siteTitle={context.site.title}
|
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}
|
actions={context.customization.pageActions}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'float-right ml-4 xl:max-2xl:page-api-block:mr-62',
|
'float-right ml-4 xl:max-2xl:page-api-block:mr-62',
|
||||||
@@ -121,38 +146,3 @@ export async function PageHeader(props: {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the URLs for the page actions.
|
|
||||||
*/
|
|
||||||
function getPageActionsURLs(
|
|
||||||
context: GitBookSiteContext,
|
|
||||||
page: RevisionPageDocument
|
|
||||||
): PageActionsDropdownURLs {
|
|
||||||
return {
|
|
||||||
html: context.linker.toAbsoluteURL(
|
|
||||||
context.linker.toPathForPage({ pages: context.revision.pages, page })
|
|
||||||
),
|
|
||||||
markdown: `${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`,
|
|
||||||
editOnGit:
|
|
||||||
context.customization.git.showEditLink && context.space.gitSync?.url && page.git
|
|
||||||
? {
|
|
||||||
provider: context.space?.gitSync?.installationProvider,
|
|
||||||
url: urlJoin(context.space.gitSync.url, page.git.path),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
pdf: context.customization.pdf.enabled
|
|
||||||
? context.linker.toPathInSpace(
|
|
||||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
|
||||||
page: page.id,
|
|
||||||
only: true,
|
|
||||||
limit: 100,
|
|
||||||
}).toString()}`
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
mcp:
|
|
||||||
context.site.visibility !== SiteVisibility.VisitorAuth
|
|
||||||
? context.linker.toAbsoluteURL(context.linker.toPathInSpace('~gitbook/mcp'))
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type React from 'react';
|
|||||||
import { TranslateContext } from '@/intl/client';
|
import { TranslateContext } from '@/intl/client';
|
||||||
import type { TranslationLanguage } from '@/intl/translations';
|
import type { TranslationLanguage } from '@/intl/translations';
|
||||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||||
import { NavigationStatusProvider } from '../hooks';
|
import { HashProvider } from '../hooks';
|
||||||
import { LoadingStateProvider } from '../primitives/LoadingStateProvider';
|
import { LoadingStateProvider } from '../primitives/LoadingStateProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,9 +20,9 @@ export function RootLayoutClientContexts(props: {
|
|||||||
return (
|
return (
|
||||||
<TranslateContext.Provider value={language}>
|
<TranslateContext.Provider value={language}>
|
||||||
<TooltipProvider delayDuration={200}>
|
<TooltipProvider delayDuration={200}>
|
||||||
<NavigationStatusProvider>
|
<HashProvider>
|
||||||
<LoadingStateProvider>{children}</LoadingStateProvider>
|
<LoadingStateProvider>{children}</LoadingStateProvider>
|
||||||
</NavigationStatusProvider>
|
</HashProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</TranslateContext.Provider>
|
</TranslateContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -217,7 +217,6 @@
|
|||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
max-width: 8px;
|
max-width: 8px;
|
||||||
max-height: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user