mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 14:00:28 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 872d36b64f | |||
| e1ff17e655 | |||
| f3010bd28a | |||
| 8ff1e3b619 | |||
| d7596bf454 | |||
| aea5eb10ae | |||
| 1165a81cf5 | |||
| 61d1a0192e | |||
| f9a2977621 | |||
| 24f601d594 | |||
| 7b34537bbe | |||
| 145f385fdb | |||
| 360aa1c6d6 | |||
| ccc0975240 | |||
| 791135040a | |||
| 12c9d765ff | |||
| 45207288a1 | |||
| 177afa5828 | |||
| d51b79edbb | |||
| a2720ac49d | |||
| 5f6853d812 | |||
| 17dd382dc3 | |||
| 98e42cfe79 | |||
| c890e01004 | |||
| 659be551bb | |||
| 78a632b7fe | |||
| 4927e964b7 | |||
| 262a9b198b | |||
| 4f3588240c | |||
| abbae3ec4d | |||
| 1b8609ac60 | |||
| 193d591e9f | |||
| 1edc5d622a | |||
| 61b8507996 | |||
| 6f368b5cf3 | |||
| 9201e2cf52 | |||
| 2e0d706d43 | |||
| 7fefe4919c | |||
| 729921f338 | |||
| 1839ea2678 | |||
| f08dd29364 | |||
| ca2dfa3065 | |||
| 10ded437f0 | |||
| d1fdc13441 | |||
| a8fca0e033 |
+1
-1
@@ -34,7 +34,7 @@
|
|||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": ["packages/*"],
|
"packages": ["packages/*"],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@gitbook/api": "^0.136.0",
|
"@gitbook/api": "^0.140.0",
|
||||||
"bidc": "^0.0.2"
|
"bidc": "^0.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
@@ -121,17 +121,6 @@ export function getCacheTag(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the cache tag for a given URL.
|
|
||||||
*/
|
|
||||||
export function getCacheTagForURL(url: string | URL) {
|
|
||||||
const parsedURL = url instanceof URL ? url : new URL(url);
|
|
||||||
return getCacheTag({
|
|
||||||
tag: 'url',
|
|
||||||
hostname: parsedURL.hostname,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the tags for a computed content source.
|
* Get the tags for a computed content source.
|
||||||
*/
|
*/
|
||||||
@@ -182,7 +171,7 @@ export function getComputedContentSourceCacheTags(
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Do not throw for unknown dependency types
|
// Do not throw for unknown dependency types
|
||||||
// as it might mean we are lacking behind the API version
|
// as it might mean we are lagging behind the API version
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -198,9 +187,8 @@ export function getComputedContentSourceCacheTags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We invalidate the computed content when a new version of the integration is deployed.
|
// We invalidate the computed content when a new version of the integration is deployed.
|
||||||
|
|
||||||
if (source.type.startsWith('integration:')) {
|
if (source.type.startsWith('integration:')) {
|
||||||
const integration = source.type.split(':')[1];
|
const integration = source.type.split(':')[1]!;
|
||||||
tags.push(
|
tags.push(
|
||||||
getCacheTag({
|
getCacheTag({
|
||||||
tag: 'integration',
|
tag: 'integration',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# @gitbook/colors
|
# @gitbook/colors
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 17dd382: Add `original` background color step
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 193d591: Fix return type for `colorContrast`
|
||||||
|
|
||||||
## 0.3.3
|
## 0.3.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type RGBColor = [number, number, number];
|
|||||||
type OKLABColor = { L: number; A: number; B: number };
|
type OKLABColor = { L: number; A: number; B: number };
|
||||||
type OKLCHColor = { L: number; C: number; H: number };
|
type OKLCHColor = { L: number; C: number; H: number };
|
||||||
|
|
||||||
const D65 = [95.047, 100.0, 108.883]; // Reference white (D65)
|
const D65 = [95.047, 100.0, 108.883] as const; // Reference white (D65)
|
||||||
|
|
||||||
export enum ColorCategory {
|
export enum ColorCategory {
|
||||||
backgrounds = 'backgrounds',
|
backgrounds = 'backgrounds',
|
||||||
@@ -19,7 +19,7 @@ export enum ColorCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ColorSubScale = {
|
type ColorSubScale = {
|
||||||
[key: string]: number;
|
[key: string]: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,6 +56,8 @@ export const scale: Record<ColorCategory, ColorSubScale> = {
|
|||||||
solid: 9,
|
solid: 9,
|
||||||
/** Hovered solid backgrounds */
|
/** Hovered solid backgrounds */
|
||||||
'solid-hover': 10,
|
'solid-hover': 10,
|
||||||
|
/** Original color */
|
||||||
|
original: 'original',
|
||||||
},
|
},
|
||||||
[ColorCategory.text]: {
|
[ColorCategory.text]: {
|
||||||
/** Very low-contrast text
|
/** Very low-contrast text
|
||||||
@@ -211,8 +213,8 @@ export function colorScale(
|
|||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
for (let index = 0; index < mapping.length; index++) {
|
for (let index = 0; index < mapping.length; index++) {
|
||||||
const targetL =
|
const step = mapping[index]!;
|
||||||
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
|
const targetL = foregroundColor.L * step + backgroundColor.L * (1 - step);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
index === 8 &&
|
index === 8 &&
|
||||||
@@ -295,7 +297,7 @@ export function rgbArrayToHex(rgb: RGBColor): string {
|
|||||||
|
|
||||||
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
|
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
|
||||||
const rgb = end.map((channel, index) => {
|
const rgb = end.map((channel, index) => {
|
||||||
return Math.round(channel + percentage * (start[index] - channel));
|
return Math.round(channel + percentage * (start[index]! - channel));
|
||||||
});
|
});
|
||||||
|
|
||||||
return rgbArrayToHex(rgb as RGBColor);
|
return rgbArrayToHex(rgb as RGBColor);
|
||||||
@@ -392,14 +394,14 @@ export function xyzToLab65(xyz: [number, number, number]): {
|
|||||||
B: number;
|
B: number;
|
||||||
} {
|
} {
|
||||||
const [x, y, z] = xyz.map((v, i) => {
|
const [x, y, z] = xyz.map((v, i) => {
|
||||||
const scaled = v / D65[i];
|
const scaled = v / D65[i]!;
|
||||||
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
|
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
L: 116 * y - 16,
|
L: 116 * y! - 16,
|
||||||
A: 500 * (x - y),
|
A: 500 * (x! - y!),
|
||||||
B: 200 * (y - z),
|
B: 200 * (y! - z!),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +421,10 @@ export function dpsContrast(a: RGBColor, b: RGBColor) {
|
|||||||
return contrast < 7.5 ? 0 : contrast;
|
return contrast < 7.5 ? 0 : contrast;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorContrast(background: string, foreground: string[] = [LIGHT_BASE, DARK_BASE]) {
|
export function colorContrast(
|
||||||
|
background: string,
|
||||||
|
foreground: string[] = [LIGHT_BASE, DARK_BASE]
|
||||||
|
): string {
|
||||||
const bg = hexToRgbArray(background);
|
const bg = hexToRgbArray(background);
|
||||||
|
|
||||||
const best: { color?: RGBColor; contrast: number } = {
|
const best: { color?: RGBColor; contrast: number } = {
|
||||||
@@ -436,5 +441,5 @@ export function colorContrast(background: string, foreground: string[] = [LIGHT_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return best.color ? rgbArrayToHex(best.color) : foreground[0];
|
return best.color ? rgbArrayToHex(best.color) : foreground[0] || LIGHT_BASE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @gitbook/embed
|
# @gitbook/embed
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 6f368b5: Fix embed assistant window width on small screens
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"react": "./dist/react/index.js"
|
"react": "./dist/react/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "catalog:",
|
"@gitbook/api": "catalog:",
|
||||||
"@gitbook/icons": "workspace:",
|
"@gitbook/icons": "workspace:",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
--gitbook-widget-top: 1rem;
|
--gitbook-widget-top: 1rem;
|
||||||
--gitbook-widget-bottom: 1rem;
|
--gitbook-widget-bottom: 1rem;
|
||||||
--gitbook-widget-right: 1rem;
|
--gitbook-widget-right: 1rem;
|
||||||
|
--gitbook-widget-left: 1rem;
|
||||||
|
|
||||||
--gitbook-widget-button-height: 46px;
|
--gitbook-widget-button-height: 46px;
|
||||||
|
|
||||||
@@ -117,7 +118,7 @@
|
|||||||
bottom: var(--gitbook-widget-window-bottom);
|
bottom: var(--gitbook-widget-window-bottom);
|
||||||
right: var(--gitbook-widget-right);
|
right: var(--gitbook-widget-right);
|
||||||
z-index: 9998;
|
z-index: 9998;
|
||||||
width: var(--gitbook-widget-window-width);
|
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
|
||||||
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
|
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
|
||||||
background-color: var(--gitbook-widget-background-solid);
|
background-color: var(--gitbook-widget-background-solid);
|
||||||
border: 1px solid var(--gitbook-widget-border-color);
|
border: 1px solid var(--gitbook-widget-border-color);
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# @gitbook/expr
|
# @gitbook/expr
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- e1ff17e: Fix bundling of gitbook/expr package
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 8ff1e3b: Add support for every/some array methods
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
### Major Changes
|
### Major Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@gitbook/expr",
|
"name": "@gitbook/expr",
|
||||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
@@ -12,23 +12,26 @@
|
|||||||
},
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eval-estree-expression": "^2.0.3",
|
"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",
|
||||||
|
"escodegen": "^2.1.0",
|
||||||
"assert-never": "^1.2.1"
|
"assert-never": "^1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "^1.1.20",
|
"bun-types": "^1.1.20",
|
||||||
|
"tsdown": "^0.15.0",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.6",
|
||||||
"@babel/types": "^7.26.0",
|
"@babel/types": "^7.26.0",
|
||||||
"@types/json-schema": "^7.0.15"
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"@types/escodegen": "^0.0.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --project tsconfig.build.json",
|
"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"
|
||||||
},
|
},
|
||||||
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
|
"files": ["dist", "README.md", "CHANGELOG.md"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ describe('autocomplete', () => {
|
|||||||
'visitor.claims.hello.length',
|
'visitor.claims.hello.length',
|
||||||
'visitor.claims.hello.at',
|
'visitor.claims.hello.at',
|
||||||
'visitor.claims.hello.includes',
|
'visitor.claims.hello.includes',
|
||||||
|
'visitor.claims.hello.some',
|
||||||
|
'visitor.claims.hello.every',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -268,6 +270,8 @@ describe('autocomplete', () => {
|
|||||||
'visitor.claims.hello.length',
|
'visitor.claims.hello.length',
|
||||||
'visitor.claims.hello.at',
|
'visitor.claims.hello.at',
|
||||||
'visitor.claims.hello.includes',
|
'visitor.claims.hello.includes',
|
||||||
|
'visitor.claims.hello.some',
|
||||||
|
'visitor.claims.hello.every',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,11 +42,29 @@ describe('ExpressionRuntime', () => {
|
|||||||
},
|
},
|
||||||
expectedResult: true,
|
expectedResult: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: 'array method',
|
||||||
|
condition: 'reviews.every(review => !!review.status)',
|
||||||
|
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
||||||
|
expectedResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'array every',
|
||||||
|
condition: 'reviews.every(review => review.status === "approved")',
|
||||||
|
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
|
||||||
|
expectedResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: 'array map',
|
||||||
|
condition: '[1, 2, 3].map(n => n * x)',
|
||||||
|
inputs: { x: 2 },
|
||||||
|
expectedResult: [2, 4, 6],
|
||||||
|
},
|
||||||
])(
|
])(
|
||||||
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
|
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
|
||||||
({ condition, inputs, expectedResult }) => {
|
({ condition, inputs, expectedResult }) => {
|
||||||
expect(runtime.evaluate(condition, inputs)).toBe(expectedResult);
|
expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
|
||||||
expect(runtime.safeEvaluate(condition, inputs).value).toBe(expectedResult);
|
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export * from './errors';
|
export * from './errors';
|
||||||
export * from './input-values';
|
export * from './input-values';
|
||||||
export * from './input-values';
|
|
||||||
export * from './runtime';
|
export * from './runtime';
|
||||||
export * from './symbols';
|
export * from './symbols';
|
||||||
export * from './template';
|
export * from './template';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
tokenizer,
|
tokenizer,
|
||||||
} from 'acorn';
|
} from 'acorn';
|
||||||
import { parse as parseLoose } from 'acorn-loose';
|
import { parse as parseLoose } from 'acorn-loose';
|
||||||
|
import escodegen from 'escodegen';
|
||||||
import { evaluate } from 'eval-estree-expression';
|
import { evaluate } from 'eval-estree-expression';
|
||||||
|
|
||||||
import { AutoComplete } from './autocomplete';
|
import { AutoComplete } from './autocomplete';
|
||||||
@@ -49,6 +50,7 @@ export class ExpressionRuntime {
|
|||||||
return evaluate.sync<Expression>(parsed.result, inputs, {
|
return evaluate.sync<Expression>(parsed.result, inputs, {
|
||||||
functions: true,
|
functions: true,
|
||||||
withMembers: true,
|
withMembers: true,
|
||||||
|
generate: escodegen.generate,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error instanceof Error
|
throw error instanceof Error
|
||||||
|
|||||||
@@ -338,6 +338,8 @@ describe('ExpressionRuntime', () => {
|
|||||||
'visitor.claims.hello.length',
|
'visitor.claims.hello.length',
|
||||||
'visitor.claims.hello.at',
|
'visitor.claims.hello.at',
|
||||||
'visitor.claims.hello.includes',
|
'visitor.claims.hello.includes',
|
||||||
|
'visitor.claims.hello.some',
|
||||||
|
'visitor.claims.hello.every',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -259,6 +259,60 @@ const StandardLibrary: Partial<
|
|||||||
'true if the value searchElement is found within the array (or the part of the array indicated by the index fromIndex, if specified).',
|
'true if the value searchElement is found within the array (or the part of the array indicated by the index fromIndex, if specified).',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
SymbolFunction({
|
||||||
|
name: 'some',
|
||||||
|
description:
|
||||||
|
'Tests whether at least one element in the array passes the test implemented by the provided function.',
|
||||||
|
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some',
|
||||||
|
args: [
|
||||||
|
SymbolFunction({
|
||||||
|
name: 'callback',
|
||||||
|
description: 'A function that tests each element of the array.',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
...arraySymbolDef.items,
|
||||||
|
name: 'element',
|
||||||
|
description: 'The current element being processed in the array.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
returns: SymbolBoolean({
|
||||||
|
description:
|
||||||
|
'true if the callback function returns a truthy value for at least one element in the array.',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
returns: SymbolBoolean({
|
||||||
|
description:
|
||||||
|
'true if the callback function returns a truthy value for at least one element in the array.',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
SymbolFunction({
|
||||||
|
name: 'every',
|
||||||
|
description:
|
||||||
|
'Tests whether all elements in the array pass the test implemented by the provided function.',
|
||||||
|
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every',
|
||||||
|
args: [
|
||||||
|
SymbolFunction({
|
||||||
|
name: 'callback',
|
||||||
|
description: 'A function that tests each element of the array.',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
...arraySymbolDef.items,
|
||||||
|
name: 'element',
|
||||||
|
description: 'The current element being processed in the array.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
returns: SymbolBoolean({
|
||||||
|
description:
|
||||||
|
'true if the callback function returns a truthy value for all elements in the array.',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
returns: SymbolBoolean({
|
||||||
|
description:
|
||||||
|
'true if the callback function returns a truthy value for all elements in the array.',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'tsdown';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
dts: true,
|
||||||
|
format: ['esm'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
+1
-1
@@ -14,7 +14,7 @@ declare module 'eval-estree-expression' {
|
|||||||
/**
|
/**
|
||||||
* Enable support for function statements and expressions by enabling the functions option AND by passing the .generate() function from the escodegen library. Default: undefined
|
* Enable support for function statements and expressions by enabling the functions option AND by passing the .generate() function from the escodegen library. Default: undefined
|
||||||
*/
|
*/
|
||||||
generate?: boolean;
|
generate?: boolean | ((node: any) => string);
|
||||||
/**
|
/**
|
||||||
* Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true
|
* Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function getBestUnicodeRange(text: string, ranges: Record<string, string>): stri
|
|||||||
|
|
||||||
const body = token.slice(2); // drop "U+"
|
const body = token.slice(2); // drop "U+"
|
||||||
const [startHex, endHex] = body.split('-');
|
const [startHex, endHex] = body.split('-');
|
||||||
const start = Number.parseInt(startHex, 16);
|
const start = Number.parseInt(startHex!, 16);
|
||||||
const end = endHex ? Number.parseInt(endHex, 16) : start;
|
const end = endHex ? Number.parseInt(endHex, 16) : start;
|
||||||
|
|
||||||
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
|
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
|
||||||
@@ -92,7 +92,7 @@ function getBestUnicodeRange(text: string, ranges: Record<string, string>): stri
|
|||||||
|
|
||||||
for (const [label, rangesArr] of Object.entries(parsed)) {
|
for (const [label, rangesArr] of Object.entries(parsed)) {
|
||||||
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
|
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
|
||||||
hits[label]++;
|
hits[label]!++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "es2022",
|
"target": "es2022",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|||||||
@@ -1,5 +1,55 @@
|
|||||||
# gitbook
|
# gitbook
|
||||||
|
|
||||||
|
## 0.17.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 24f601d: Small optim in resolveTryItPrefillForOperation
|
||||||
|
- aea5eb1: Persist language choice across sections if possible
|
||||||
|
- 1165a81: Language selector edge cases
|
||||||
|
- f9a2977: Better handling for external link "mailto:" in Hovered Card in GBO
|
||||||
|
- Updated dependencies [24f601d]
|
||||||
|
- @gitbook/react-openapi@1.4.1
|
||||||
|
|
||||||
|
## 0.17.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 12c9d76: Adapt OpenAPI blocks to eval adaptive exprs & prefill TryIt config
|
||||||
|
- 7911350: Add language selector to site header
|
||||||
|
- 659be55: Track insight event when embedded assistant is displayed.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2e0d706: Fix corner radius of active section on "Line" sidebar style
|
||||||
|
- 78a632b: Fix edit on GitHub button doesn't take you to GitHub
|
||||||
|
- 1edc5d6: Add "hold message" to Assistant
|
||||||
|
- 4520728: Support bold in headings
|
||||||
|
- 61b8507: OpenAPI: Make responses without objects clickable
|
||||||
|
- d1fdc13: Remove ligatures from Lato font
|
||||||
|
- a8fca0e: Fix custom assistant keyboard shortcut
|
||||||
|
- 262a9b1: Fix embed script and assets being cached for too long
|
||||||
|
- c890e01: Fix order in robots.txt preventing indexation of images by Google.
|
||||||
|
- 1839ea2: Fix content min-height with sections
|
||||||
|
- 9201e2c: Adds vertical align to column block
|
||||||
|
- 193d591: Use space language as source of truth for UI locale
|
||||||
|
- f08dd29: Fix Search results are not clickable on sites without header
|
||||||
|
- 17dd382: Add `original` background color step
|
||||||
|
- 4f35882: Fix event ask_question not being tracked
|
||||||
|
- d51b79e: Fix Search bar is broken on site with sections when header is disabled
|
||||||
|
- 6f368b5: Fix embed assistant window width on small screens
|
||||||
|
- Updated dependencies [193d591]
|
||||||
|
- Updated dependencies [12c9d76]
|
||||||
|
- Updated dependencies [4927e96]
|
||||||
|
- Updated dependencies [61b8507]
|
||||||
|
- Updated dependencies [7fefe49]
|
||||||
|
- Updated dependencies [360aa1c]
|
||||||
|
- Updated dependencies [98e42cf]
|
||||||
|
- Updated dependencies [17dd382]
|
||||||
|
- @gitbook/colors@0.4.0
|
||||||
|
- @gitbook/react-openapi@1.4.0
|
||||||
|
- @gitbook/openapi-parser@3.0.2
|
||||||
|
|
||||||
## 0.16.0
|
## 0.16.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const args = process.argv.slice(2);
|
|||||||
const versionId = args[0];
|
const versionId = args[0];
|
||||||
|
|
||||||
// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
|
// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
|
||||||
const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
const previewHostname = `${versionId?.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
||||||
|
|
||||||
let updatedFile = file.replace(
|
let updatedFile = file.replace(
|
||||||
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
|
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "gitbook",
|
"name": "gitbook",
|
||||||
"version": "0.16.0",
|
"version": "0.17.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "catalog:",
|
"@gitbook/api": "catalog:",
|
||||||
|
"@gitbook/browser-types": "workspace:*",
|
||||||
"@gitbook/cache-tags": "workspace:*",
|
"@gitbook/cache-tags": "workspace:*",
|
||||||
"@gitbook/colors": "workspace:*",
|
"@gitbook/colors": "workspace:*",
|
||||||
"@gitbook/browser-types": "workspace:*",
|
|
||||||
"@gitbook/emoji-codepoints": "workspace:*",
|
"@gitbook/emoji-codepoints": "workspace:*",
|
||||||
"@gitbook/fonts": "workspace:*",
|
"@gitbook/fonts": "workspace:*",
|
||||||
"@gitbook/icons": "workspace:*",
|
"@gitbook/icons": "workspace:*",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"@gitbook/react-contentkit": "workspace:*",
|
"@gitbook/react-contentkit": "workspace:*",
|
||||||
"@gitbook/react-math": "workspace:*",
|
"@gitbook/react-math": "workspace:*",
|
||||||
"@gitbook/react-openapi": "workspace:*",
|
"@gitbook/react-openapi": "workspace:*",
|
||||||
"@opennextjs/cloudflare": "^1.6.4",
|
"@opennextjs/cloudflare": "^1.7.1",
|
||||||
"@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",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"@tusbar/cache-control": "^1.0.2",
|
"@tusbar/cache-control": "^1.0.2",
|
||||||
"ai": "^4.2.2",
|
"ai": "^4.2.2",
|
||||||
"assert-never": "^1.2.1",
|
"assert-never": "^1.2.1",
|
||||||
|
"bidc": "catalog:",
|
||||||
"bun-types": "^1.1.20",
|
"bun-types": "^1.1.20",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"direction": "^2.0.1",
|
"direction": "^2.0.1",
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
"memoizee": "^0.4.17",
|
"memoizee": "^0.4.17",
|
||||||
"micromark-extension-frontmatter": "^2.0.0",
|
"micromark-extension-frontmatter": "^2.0.0",
|
||||||
"micromark-extension-gfm": "^3.0.0",
|
"micromark-extension-gfm": "^3.0.0",
|
||||||
"next": "^15.5.0",
|
"next": "15.3.5",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nuqs": "^2.2.3",
|
"nuqs": "^2.2.3",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
@@ -70,8 +71,7 @@
|
|||||||
"url-join": "^5.0.0",
|
"url-join": "^5.0.0",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0",
|
||||||
"warn-once": "^0.1.1",
|
"warn-once": "^0.1.1",
|
||||||
"zustand": "^5.0.3",
|
"zustand": "^5.0.3"
|
||||||
"bidc": "catalog:"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@argos-ci/playwright": "^5.0.9",
|
"@argos-ci/playwright": "^5.0.9",
|
||||||
@@ -104,7 +104,8 @@
|
|||||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math",
|
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math",
|
||||||
"dev": "env-cmd --silent -f ../../.env.local next",
|
"dev": "env-cmd --silent -f ../../.env.local next",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"build:local": "GITBOOK_URL=http://localhost:3000 next build",
|
||||||
|
"start": "GITBOOK_URL=http://localhost:3000 next start",
|
||||||
"build:cloudflare": "opennextjs-cloudflare build",
|
"build:cloudflare": "opennextjs-cloudflare build",
|
||||||
"dev:cloudflare": "wrangler dev --port 8771 --env preview",
|
"dev:cloudflare": "wrangler dev --port 8771 --env preview",
|
||||||
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ export default async function SiteDynamicLayout({
|
|||||||
const withTracking = shouldTrackEvents(await headers());
|
const withTracking = shouldTrackEvents(await headers());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomizationRootLayout forcedTheme={forcedTheme} customization={context.customization}>
|
<CustomizationRootLayout forcedTheme={forcedTheme} context={context}>
|
||||||
<SiteLayout
|
<SiteLayout
|
||||||
context={context}
|
context={context}
|
||||||
forcedTheme={forcedTheme}
|
forcedTheme={forcedTheme}
|
||||||
|
|||||||
+13
-2
@@ -5,6 +5,8 @@ import {
|
|||||||
generateEmbeddableViewport,
|
generateEmbeddableViewport,
|
||||||
} from '@/components/Embeddable';
|
} from '@/components/Embeddable';
|
||||||
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||||
|
import { shouldTrackEvents } from '@/lib/tracking';
|
||||||
|
import { headers } from 'next/headers';
|
||||||
|
|
||||||
interface SiteStaticLayoutProps {
|
interface SiteStaticLayoutProps {
|
||||||
params: Promise<RouteLayoutParams>;
|
params: Promise<RouteLayoutParams>;
|
||||||
@@ -14,9 +16,18 @@ export default async function RootLayout({
|
|||||||
params,
|
params,
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
|
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
|
||||||
const { context } = await getEmbeddableStaticContext(await params);
|
const { context, visitorAuthClaims } = await getEmbeddableStaticContext(await params);
|
||||||
|
const withTracking = shouldTrackEvents(await headers());
|
||||||
|
|
||||||
return <EmbeddableRootLayout context={context}>{children}</EmbeddableRootLayout>;
|
return (
|
||||||
|
<EmbeddableRootLayout
|
||||||
|
context={context}
|
||||||
|
withTracking={withTracking}
|
||||||
|
visitorAuthClaims={visitorAuthClaims}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</EmbeddableRootLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateViewport({ params }: SiteStaticLayoutProps) {
|
export async function generateViewport({ params }: SiteStaticLayoutProps) {
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ export default async function SiteStaticLayout({
|
|||||||
const withTracking = shouldTrackEvents();
|
const withTracking = shouldTrackEvents();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomizationRootLayout customization={context.customization}>
|
<CustomizationRootLayout context={context}>
|
||||||
<SiteLayout
|
<SiteLayout
|
||||||
context={context}
|
context={context}
|
||||||
withTracking={withTracking}
|
withTracking={withTracking}
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils';
|
||||||
|
import { serveLLMsFullTxt } from '@/routes/llms-full';
|
||||||
|
|
||||||
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: NextRequest,
|
||||||
|
{ params }: { params: Promise<RouteLayoutParams & { page: string }> }
|
||||||
|
) {
|
||||||
|
const awaitedParams = await params;
|
||||||
|
const page = Number(awaitedParams.page);
|
||||||
|
// If page is not a number, not an integer, or less than 0, return an error
|
||||||
|
if (Number.isNaN(page) || !Number.isInteger(page) || page < 0) {
|
||||||
|
return new Response('Invalid page', { status: 400 });
|
||||||
|
}
|
||||||
|
const { context } = await getStaticSiteContext(awaitedParams);
|
||||||
|
return serveLLMsFullTxt(context, page);
|
||||||
|
}
|
||||||
+12
-2
@@ -5,6 +5,7 @@ import {
|
|||||||
generateEmbeddableViewport,
|
generateEmbeddableViewport,
|
||||||
} from '@/components/Embeddable';
|
} from '@/components/Embeddable';
|
||||||
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||||
|
import { shouldTrackEvents } from '@/lib/tracking';
|
||||||
|
|
||||||
interface SiteStaticLayoutProps {
|
interface SiteStaticLayoutProps {
|
||||||
params: Promise<RouteLayoutParams>;
|
params: Promise<RouteLayoutParams>;
|
||||||
@@ -14,9 +15,18 @@ export default async function RootLayout({
|
|||||||
params,
|
params,
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
|
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
|
||||||
const { context } = await getEmbeddableStaticContext(await params);
|
const { context, visitorAuthClaims } = await getEmbeddableStaticContext(await params);
|
||||||
|
const withTracking = shouldTrackEvents();
|
||||||
|
|
||||||
return <EmbeddableRootLayout context={context}>{children}</EmbeddableRootLayout>;
|
return (
|
||||||
|
<EmbeddableRootLayout
|
||||||
|
context={context}
|
||||||
|
withTracking={withTracking}
|
||||||
|
visitorAuthClaims={visitorAuthClaims}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</EmbeddableRootLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateViewport({ params }: SiteStaticLayoutProps) {
|
export async function generateViewport({ params }: SiteStaticLayoutProps) {
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,6 @@
|
|||||||
import type { RouteLayoutParams } from '@/app/utils';
|
import type { RouteLayoutParams } from '@/app/utils';
|
||||||
import { getAssetURL } from '@/lib/assets';
|
import { getAssetURL } from '@/lib/assets';
|
||||||
|
import { buildVersion } from '@/lib/build';
|
||||||
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||||
import type { CreateGitBookOptions } from '@gitbook/embed';
|
import type { CreateGitBookOptions } from '@gitbook/embed';
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
@@ -14,7 +15,6 @@ export async function GET(
|
|||||||
{ params }: { params: Promise<RouteLayoutParams> }
|
{ params }: { params: Promise<RouteLayoutParams> }
|
||||||
) {
|
) {
|
||||||
const { context } = await getEmbeddableStaticContext(await params);
|
const { context } = await getEmbeddableStaticContext(await params);
|
||||||
const maxAge = 7 * 24 * 60 * 60;
|
|
||||||
const initOptions: CreateGitBookOptions = {
|
const initOptions: CreateGitBookOptions = {
|
||||||
siteURL: context.linker.toAbsoluteURL(context.linker.toPathInSite('')),
|
siteURL: context.linker.toAbsoluteURL(context.linker.toPathInSite('')),
|
||||||
};
|
};
|
||||||
@@ -45,13 +45,13 @@ export async function GET(
|
|||||||
const load = function () {
|
const load = function () {
|
||||||
const style = document.createElement('link');
|
const style = document.createElement('link');
|
||||||
style.rel = 'stylesheet';
|
style.rel = 'stylesheet';
|
||||||
style.href = ${JSON.stringify(getAssetURL('embed/index.css'))};
|
style.href = ${JSON.stringify(getAssetURL(`embed/index.css?v=${buildVersion()}`))};
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
const script = d.createElement('script');
|
const script = d.createElement('script');
|
||||||
script.type = 'text/javascript';
|
script.type = 'text/javascript';
|
||||||
script.async = true;
|
script.async = true;
|
||||||
script.src = ${JSON.stringify(getAssetURL('embed/index.js'))};
|
script.src = ${JSON.stringify(getAssetURL(`embed/index.js?v=${buildVersion()}`))};
|
||||||
|
|
||||||
var latestScript = d.getElementsByTagName('script')[0];
|
var latestScript = d.getElementsByTagName('script')[0];
|
||||||
latestScript.parentNode.insertBefore(script, latestScript);
|
latestScript.parentNode.insertBefore(script, latestScript);
|
||||||
@@ -70,7 +70,7 @@ export async function GET(
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/javascript',
|
'Content-Type': 'application/javascript',
|
||||||
'Cache-Control': `public, max-age=${maxAge}, immutable`,
|
'Cache-Control': 'public, max-age=86400, stale-while-revalidate=604800',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './useAI';
|
export * from './useAI';
|
||||||
export * from './useAIChat';
|
export * from './useAIChat';
|
||||||
|
export type { RenderAIMessageOptions } from './server-actions';
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function DescriptionForPageContentToolCall(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { toolCall, context } = props;
|
const { toolCall, context } = props;
|
||||||
|
|
||||||
const language = getSpaceLanguage(context.customization);
|
const language = getSpaceLanguage(context);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
@@ -102,7 +102,7 @@ function DescriptionForMCPToolCall(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { toolCall, context } = props;
|
const { toolCall, context } = props;
|
||||||
|
|
||||||
const language = getSpaceLanguage(context.customization);
|
const language = getSpaceLanguage(context);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
@@ -130,7 +130,7 @@ async function DescriptionForSearchToolCall(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { toolCall, context } = props;
|
const { toolCall, context } = props;
|
||||||
|
|
||||||
const language = getSpaceLanguage(context.customization);
|
const language = getSpaceLanguage(context);
|
||||||
|
|
||||||
// Resolve all hrefs for search results in parallel
|
// Resolve all hrefs for search results in parallel
|
||||||
const searchResultsWithHrefs = await Promise.all(
|
const searchResultsWithHrefs = await Promise.all(
|
||||||
@@ -187,7 +187,7 @@ async function DescriptionForSearchToolCall(props: {
|
|||||||
) : null}
|
) : null}
|
||||||
</summary>
|
</summary>
|
||||||
{hasResults ? (
|
{hasResults ? (
|
||||||
<div className="mt-1 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all transition-discrete duration-500 group-open:max-h-96 group-open:opacity-11">
|
<div className="hide-scrollbar mt-1 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all transition-discrete duration-500 group-open:max-h-96 group-open:opacity-11">
|
||||||
<ol className="space-y-1">
|
<ol className="space-y-1">
|
||||||
{searchResultsWithHrefs.map((result, index) => (
|
{searchResultsWithHrefs.map((result, index) => (
|
||||||
<li
|
<li
|
||||||
@@ -241,7 +241,7 @@ function DescriptionForGetPagesToolCall(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { toolCall, context } = props;
|
const { toolCall, context } = props;
|
||||||
|
|
||||||
const language = getSpaceLanguage(context.customization);
|
const language = getSpaceLanguage(context);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export async function streamRenderAIMessage(
|
|||||||
|
|
||||||
if (message.steps[stepIndex]) {
|
if (message.steps[stepIndex]) {
|
||||||
message.steps = [...message.steps];
|
message.steps = [...message.steps];
|
||||||
|
// @ts-expect-error
|
||||||
message.steps[stepIndex] = { ...message.steps[stepIndex] };
|
message.steps[stepIndex] = { ...message.steps[stepIndex] };
|
||||||
|
// @ts-expect-error
|
||||||
callback(message.steps[stepIndex]);
|
callback(message.steps[stepIndex]);
|
||||||
} else {
|
} else {
|
||||||
message.steps = [
|
message.steps = [
|
||||||
@@ -52,6 +54,7 @@ export async function streamRenderAIMessage(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
// @ts-expect-error
|
||||||
callback(message.steps[stepIndex]);
|
callback(message.steps[stepIndex]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
EmbeddableFrameSubtitle,
|
EmbeddableFrameSubtitle,
|
||||||
EmbeddableFrameTitle,
|
EmbeddableFrameTitle,
|
||||||
} from '../Embeddable/EmbeddableFrame';
|
} from '../Embeddable/EmbeddableFrame';
|
||||||
|
import { useTrackEvent } from '../Insights';
|
||||||
import { useNow } from '../hooks';
|
import { useNow } from '../hooks';
|
||||||
import { Button } from '../primitives';
|
import { Button } from '../primitives';
|
||||||
import { AIChatControlButton } from './AIChatControlButton';
|
import { AIChatControlButton } from './AIChatControlButton';
|
||||||
@@ -46,17 +47,6 @@ export function AIChat(props: { trademark: boolean }) {
|
|||||||
}
|
}
|
||||||
}, [chat.opened]);
|
}, [chat.opened]);
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'mod+i',
|
|
||||||
(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
chatController.open();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enableOnFormTags: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'esc',
|
'esc',
|
||||||
() => {
|
() => {
|
||||||
@@ -65,6 +55,16 @@ export function AIChat(props: { trademark: boolean }) {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track the view of the AI chat
|
||||||
|
const trackEvent = useTrackEvent();
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (chat.opened) {
|
||||||
|
trackEvent({
|
||||||
|
type: 'ask_view',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [chat.opened, trackEvent]);
|
||||||
|
|
||||||
if (!chat.opened) {
|
if (!chat.opened) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -224,7 +224,7 @@ export function AIChatBody(props: {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className="flex grow scroll-pt-4 flex-col gap-4 overflow-y-auto p-4"
|
className="gutter-stable flex grow scroll-pt-4 flex-col gap-4 overflow-y-auto p-4"
|
||||||
style={{
|
style={{
|
||||||
paddingBottom: `${inputHeight}px`,
|
paddingBottom: `${inputHeight}px`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
|
|||||||
export function AIChatButton(props: {
|
export function AIChatButton(props: {
|
||||||
assistant: Assistant;
|
assistant: Assistant;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
|
withShortcut?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { assistant, showLabel = true } = props;
|
const { assistant, showLabel = true, withShortcut = true } = props;
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +28,12 @@ export function AIChatButton(props: {
|
|||||||
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)}
|
||||||
<KeyboardShortcut keys={['mod', 'i']} className="border-tint-11 text-tint-1" />
|
{withShortcut ? (
|
||||||
|
<KeyboardShortcut
|
||||||
|
keys={['mod', 'i']}
|
||||||
|
className="border-tint-11 text-tint-1"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClick={() => assistant.open()}
|
onClick={() => assistant.open()}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useLanguage } from '@/intl/client';
|
||||||
|
import { tString } from '@/intl/translate';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import { AIMessageRole } from '@gitbook/api';
|
import { AIMessageRole } from '@gitbook/api';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
@@ -46,17 +48,9 @@ export function AIChatMessages(props: {
|
|||||||
{message.content ? message.content : null}
|
{message.content ? message.content : null}
|
||||||
|
|
||||||
{isLastMessage && chat.loading ? (
|
{isLastMessage && chat.loading ? (
|
||||||
<div className="flex w-full animate-fade-in-slow flex-wrap gap-2">
|
<div className="flex w-full animate-fade-in-slow flex-col gap-2">
|
||||||
{Array.from({ length: 7 }).map((_, index) => (
|
{!message.content ? <HoldMessage /> : null}
|
||||||
<div
|
<LoadingSkeleton />
|
||||||
key={index}
|
|
||||||
className="h-4 animate-[fadeIn_500ms_ease_both,pulse_1.5s_infinite] circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint-4"
|
|
||||||
style={{
|
|
||||||
width: `calc(${(index % 4) * 20 + 10}% - 4px)`,
|
|
||||||
animationDelay: `${index * 0.1}s`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -86,3 +80,55 @@ export function AIChatMessages(props: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HoldMessage() {
|
||||||
|
const language = useLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="animate-[heightIn_500ms_4500ms_ease_both] py-2 text-tint-subtle">
|
||||||
|
{tString(language, 'ai_chat_hold_message_1')
|
||||||
|
.split(' ')
|
||||||
|
.map((word, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="animate-fade-in-slow"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${5000 + index * 200}ms`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{word}{' '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{tString(language, 'ai_chat_hold_message_2')
|
||||||
|
.split(' ')
|
||||||
|
.map((word, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="animate-fade-in-slow"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${10000 + index * 200}ms`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{word}{' '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Array.from({ length: 7 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="h-4 animate-[fadeIn_500ms_ease_both,pulse_1.5s_infinite] circular-corners:rounded-2xl rounded-corners:rounded-md bg-tint-4"
|
||||||
|
style={{
|
||||||
|
width: `calc(${(index % 4) * 20 + 10}% - 4px)`,
|
||||||
|
animationDelay: `${index * 0.1}s`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
|
import { OpenAPIPrefillContextProvider } from '@gitbook/react-openapi';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
export type AdaptiveVisitorClaimsData = {
|
||||||
|
visitor: {
|
||||||
|
claims: Record<string, unknown> & { unsigned: Record<string, unknown> };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory cache of visitor claim readers keyed by contextId.
|
||||||
|
*/
|
||||||
|
const adaptiveVisitorReaderCache = new Map<
|
||||||
|
string,
|
||||||
|
ReturnType<typeof createResourceReader<AdaptiveVisitorClaimsData | null>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
function createResourceReader<T>(promise: Promise<T>) {
|
||||||
|
let result: T | null | undefined;
|
||||||
|
|
||||||
|
const suspender = (async () => {
|
||||||
|
try {
|
||||||
|
result = await promise;
|
||||||
|
} catch {
|
||||||
|
result = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
read() {
|
||||||
|
if (result === undefined) {
|
||||||
|
throw suspender;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an adaptive visitor claims cached reader for a given endpoint URL and contextId.
|
||||||
|
*/
|
||||||
|
function getAdaptiveVisitorClaimsReader(url: string, contextId: string) {
|
||||||
|
let reader = adaptiveVisitorReaderCache.get(contextId);
|
||||||
|
if (!reader) {
|
||||||
|
const promise = (async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await res.json<AdaptiveVisitorClaimsData>();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
reader = createResourceReader(promise);
|
||||||
|
adaptiveVisitorReaderCache.set(contextId, reader);
|
||||||
|
}
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdaptiveVisitorContextValue = () => AdaptiveVisitorClaimsData | null;
|
||||||
|
|
||||||
|
const AdaptiveVisitorContext = createContext<AdaptiveVisitorContextValue>(() => null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide context to adapt site based on visitor claims.
|
||||||
|
*/
|
||||||
|
export function AdaptiveVisitorContextProvider(
|
||||||
|
props: React.PropsWithChildren<{
|
||||||
|
visitorClaimsURL: string;
|
||||||
|
contextId: GitBookSiteContext['contextId'] | undefined;
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const { visitorClaimsURL, contextId, children } = props;
|
||||||
|
|
||||||
|
const getAdaptiveVisitorClaims = React.useCallback(() => {
|
||||||
|
if (!contextId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getAdaptiveVisitorClaimsReader(visitorClaimsURL, contextId).read();
|
||||||
|
}, [visitorClaimsURL, contextId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdaptiveVisitorContext.Provider value={getAdaptiveVisitorClaims}>
|
||||||
|
<OpenAPIPrefillContextProvider getPrefillInputContextData={getAdaptiveVisitorClaims}>
|
||||||
|
{children}
|
||||||
|
</OpenAPIPrefillContextProvider>
|
||||||
|
</AdaptiveVisitorContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that returns a suspensable getter for adaptive visitor claims data.
|
||||||
|
*/
|
||||||
|
export function useAdaptiveVisitor(): AdaptiveVisitorContextValue {
|
||||||
|
return useContext(AdaptiveVisitorContext);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './AdaptiveVisitorContextProvider';
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import { Icon } from '@gitbook/icons';
|
import { Icon } from '@gitbook/icons';
|
||||||
import { headers } from 'next/headers';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { DateRelative } from '../primitives';
|
import { DateRelative } from '../primitives';
|
||||||
|
import { IframeWrapper } from './IframeWrapper';
|
||||||
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
||||||
import { Toolbar, ToolbarBody, ToolbarButton, ToolbarButtonGroups } from './Toolbar';
|
import { Toolbar, ToolbarBody, ToolbarButton, ToolbarButtonGroups } from './Toolbar';
|
||||||
|
|
||||||
@@ -47,19 +46,21 @@ function ToolbarLayout(props: { children: React.ReactNode }) {
|
|||||||
*/
|
*/
|
||||||
export async function AdminToolbar(props: AdminToolbarProps) {
|
export async function AdminToolbar(props: AdminToolbarProps) {
|
||||||
const { context } = props;
|
const { context } = props;
|
||||||
const mode = (await headers()).get('x-gitbook-mode');
|
|
||||||
|
|
||||||
if (mode === 'multi-id') {
|
|
||||||
// We don't show the admin toolbar in multi-id mode, as it's used for previewing in the dashboard.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.changeRequest) {
|
if (context.changeRequest) {
|
||||||
return <ChangeRequestToolbar context={context} />;
|
return (
|
||||||
|
<IframeWrapper>
|
||||||
|
<ChangeRequestToolbar context={context} />
|
||||||
|
</IframeWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.revisionId !== context.space.revision) {
|
if (context.revisionId !== context.space.revision) {
|
||||||
return <RevisionToolbar context={context} />;
|
return (
|
||||||
|
<IframeWrapper>
|
||||||
|
<RevisionToolbar context={context} />
|
||||||
|
</IframeWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface IframeWrapperProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client component that detects if we're in an iframe and conditionally renders children
|
||||||
|
*/
|
||||||
|
export function IframeWrapper({ children }: IframeWrapperProps) {
|
||||||
|
const [isInIframe, setIsInIframe] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Check if we're running inside an iframe
|
||||||
|
const inIframe = window !== window.parent;
|
||||||
|
setIsInIframe(inIframe);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Don't render children if we're in an iframe (GitBook app preview)
|
||||||
|
if (isInIframe) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './AdminToolbar';
|
export * from './AdminToolbar';
|
||||||
|
export * from './IframeWrapper';
|
||||||
|
|||||||
@@ -34,69 +34,67 @@ export function AnnouncementBanner(props: {
|
|||||||
className="theme-bold:bg-header-background pt-4 pb-2"
|
className="theme-bold:bg-header-background pt-4 pb-2"
|
||||||
data-nosnippet=""
|
data-nosnippet=""
|
||||||
>
|
>
|
||||||
<div className="scroll-nojump">
|
<div className="transition-all 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 className={tcls('relative', CONTAINER_STYLE)}>
|
||||||
<div className={tcls('relative', CONTAINER_STYLE)}>
|
<Tag
|
||||||
<Tag
|
href={contentRef?.href ?? ''}
|
||||||
href={contentRef?.href ?? ''}
|
className={tcls(
|
||||||
className={tcls(
|
'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
|
||||||
'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
|
style.container,
|
||||||
style.container,
|
closeable && 'pr-12',
|
||||||
closeable && 'pr-12',
|
hasLink && style.hover
|
||||||
hasLink && style.hover
|
)}
|
||||||
)}
|
insights={
|
||||||
insights={
|
announcement.link
|
||||||
announcement.link
|
? {
|
||||||
? {
|
type: 'link_click',
|
||||||
type: 'link_click',
|
link: {
|
||||||
link: {
|
target: announcement.link.to,
|
||||||
target: announcement.link.to,
|
position: SiteInsightsLinkPosition.Announcement,
|
||||||
position: SiteInsightsLinkPosition.Announcement,
|
},
|
||||||
},
|
}
|
||||||
}
|
: undefined
|
||||||
: undefined
|
}
|
||||||
}
|
>
|
||||||
>
|
<Icon
|
||||||
<Icon
|
icon={style.icon as IconName}
|
||||||
icon={style.icon as IconName}
|
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
|
||||||
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
|
/>
|
||||||
/>
|
<div>
|
||||||
<div>
|
{announcement.message}
|
||||||
{announcement.message}
|
{hasLink ? (
|
||||||
{hasLink ? (
|
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
|
||||||
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
|
{contentRef?.icon ? (
|
||||||
{contentRef?.icon ? (
|
<span className="mr-1 ml-2 *:inline">
|
||||||
<span className="mr-1 ml-2 *:inline">
|
{contentRef?.icon}
|
||||||
{contentRef?.icon}
|
</span>
|
||||||
</span>
|
) : null}
|
||||||
) : null}
|
{announcement.link?.title && (
|
||||||
{announcement.link?.title && (
|
<span className="mr-1">{announcement.link?.title}</span>
|
||||||
<span className="mr-1">{announcement.link?.title}</span>
|
)}
|
||||||
)}
|
<Icon
|
||||||
<Icon
|
icon={
|
||||||
icon={
|
announcement.link?.to.kind === 'url'
|
||||||
announcement.link?.to.kind === 'url'
|
? 'arrow-up-right'
|
||||||
? 'arrow-up-right'
|
: 'chevron-right'
|
||||||
: 'chevron-right'
|
}
|
||||||
}
|
className={tcls('mb-0.5 inline size-3')}
|
||||||
className={tcls('mb-0.5 inline size-3')}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
</Tag>
|
||||||
</Tag>
|
{closeable ? (
|
||||||
{closeable ? (
|
<Button
|
||||||
<Button
|
iconOnly
|
||||||
iconOnly
|
icon="close"
|
||||||
icon="close"
|
label={tString(language, 'close')}
|
||||||
label={tString(language, 'close')}
|
variant="blank"
|
||||||
variant="blank"
|
size="default"
|
||||||
size="default"
|
onClick={dismissAnnouncement}
|
||||||
onClick={dismissAnnouncement}
|
className={`absolute top-0 right-4 mt-2 mr-2 circular-corners:rounded-lg rounded-sm straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
|
||||||
className={`absolute top-0 right-4 mt-2 mr-2 circular-corners:rounded-lg rounded-sm straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
|
/>
|
||||||
/>
|
) : null}
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export async function highlight(
|
|||||||
currentIndex += 1; // for the \n
|
currentIndex += 1; // for the \n
|
||||||
|
|
||||||
return {
|
return {
|
||||||
highlighted: Boolean(lineBlock.data.highlighted),
|
highlighted: Boolean(lineBlock?.data.highlighted),
|
||||||
tokens: result,
|
tokens: result,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import type { DocumentBlockColumns, Length } from '@gitbook/api';
|
import { type DocumentBlockColumns, type Length, VerticalAlignment } from '@gitbook/api';
|
||||||
import type { BlockProps } from '../Block';
|
import type { BlockProps } from '../Block';
|
||||||
import { Blocks } from '../Blocks';
|
import { Blocks } from '../Blocks';
|
||||||
|
|
||||||
@@ -8,10 +8,12 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
|||||||
return (
|
return (
|
||||||
<div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
|
<div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
|
||||||
{block.nodes.map((columnBlock) => {
|
{block.nodes.map((columnBlock) => {
|
||||||
const width = columnBlock.data.width;
|
|
||||||
const { className, style } = transformLengthToCSS(width);
|
|
||||||
return (
|
return (
|
||||||
<Column key={columnBlock.key} className={className} style={style}>
|
<Column
|
||||||
|
key={columnBlock.key}
|
||||||
|
width={columnBlock.data.width}
|
||||||
|
verticalAlignment={columnBlock.data.verticalAlignment}
|
||||||
|
>
|
||||||
<Blocks
|
<Blocks
|
||||||
key={columnBlock.key}
|
key={columnBlock.key}
|
||||||
nodes={columnBlock.nodes}
|
nodes={columnBlock.nodes}
|
||||||
@@ -29,11 +31,23 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
|||||||
|
|
||||||
export function Column(props: {
|
export function Column(props: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: ClassValue;
|
width?: Length;
|
||||||
style?: React.CSSProperties;
|
verticalAlignment?: VerticalAlignment;
|
||||||
}) {
|
}) {
|
||||||
|
const { width, verticalAlignment } = props;
|
||||||
|
const { className, style } = transformLengthToCSS(width);
|
||||||
return (
|
return (
|
||||||
<div className={tcls('flex-col', props.className)} style={props.style}>
|
<div
|
||||||
|
className={tcls(
|
||||||
|
'flex flex-col',
|
||||||
|
(verticalAlignment === VerticalAlignment.Top || !verticalAlignment) &&
|
||||||
|
'justify-start',
|
||||||
|
verticalAlignment === VerticalAlignment.Middle && 'justify-center',
|
||||||
|
verticalAlignment === VerticalAlignment.Bottom && 'justify-end',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ import { Block, type BlockProps } from './Block';
|
|||||||
import { Blocks } from './Blocks';
|
import { Blocks } from './Blocks';
|
||||||
import { getBlockTextStyle } from './spacing';
|
import { getBlockTextStyle } from './spacing';
|
||||||
|
|
||||||
export function Hint(props: BlockProps<DocumentBlockHint>) {
|
export function Hint({
|
||||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
block,
|
||||||
|
style,
|
||||||
|
ancestorBlocks,
|
||||||
|
...contextProps
|
||||||
|
}: BlockProps<DocumentBlockHint>) {
|
||||||
const hintStyle = HINT_STYLES[block.data.style] ?? HINT_STYLES.info;
|
const hintStyle = HINT_STYLES[block.data.style] ?? HINT_STYLES.info;
|
||||||
const firstLine = getBlockTextStyle(block.nodes[0]);
|
const firstNode = block.nodes[0]!;
|
||||||
|
const firstLine = getBlockTextStyle(firstNode);
|
||||||
const firstNode = block.nodes[0];
|
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(firstNode.type);
|
||||||
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(block.nodes[0].type);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
|||||||
: null;
|
: null;
|
||||||
const { contentContext } = context;
|
const { contentContext } = context;
|
||||||
|
|
||||||
const language =
|
const language = contentContext ? getSpaceLanguage(contentContext) : languages.en;
|
||||||
contentContext && 'customization' in contentContext
|
|
||||||
? getSpaceLanguage(contentContext.customization)
|
|
||||||
: languages.en;
|
|
||||||
|
|
||||||
if (!contentContext || !resolved) {
|
if (!contentContext || !resolved) {
|
||||||
return (
|
return (
|
||||||
@@ -49,6 +46,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const isExternal = inline.data.ref.kind === 'url';
|
const isExternal = inline.data.ref.kind === 'url';
|
||||||
|
const isMailto = resolved.href.startsWith('mailto:');
|
||||||
const content = (
|
const content = (
|
||||||
<StyledLink
|
<StyledLink
|
||||||
href={resolved.href}
|
href={resolved.href}
|
||||||
@@ -66,7 +64,12 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
|||||||
nodes={inline.nodes}
|
nodes={inline.nodes}
|
||||||
ancestorInlines={[...ancestorInlines, inline]}
|
ancestorInlines={[...ancestorInlines, inline]}
|
||||||
/>
|
/>
|
||||||
{isExternal ? (
|
{isMailto ? (
|
||||||
|
<Icon
|
||||||
|
icon="envelope"
|
||||||
|
className="ml-1 inline size-3 links-accent:text-tint-subtle"
|
||||||
|
/>
|
||||||
|
) : isExternal ? (
|
||||||
<Icon
|
<Icon
|
||||||
icon="arrow-up-right"
|
icon="arrow-up-right"
|
||||||
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
||||||
@@ -99,16 +102,24 @@ function InlineLinkTooltipWrapper(props: {
|
|||||||
const { inline, language, resolved, children } = props;
|
const { inline, language, resolved, children } = props;
|
||||||
|
|
||||||
let breadcrumbs = resolved.ancestors ?? [];
|
let breadcrumbs = resolved.ancestors ?? [];
|
||||||
|
const isMailto = resolved.href.startsWith('mailto:');
|
||||||
const isExternal = inline.data.ref.kind === 'url';
|
const isExternal = inline.data.ref.kind === 'url';
|
||||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||||
if (isExternal) {
|
|
||||||
|
if (isMailto) {
|
||||||
|
resolved.text = resolved.text.split('mailto:')[1] ?? resolved.text;
|
||||||
|
breadcrumbs = [
|
||||||
|
{
|
||||||
|
label: tString(language, 'link_tooltip_email'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else if (isExternal) {
|
||||||
breadcrumbs = [
|
breadcrumbs = [
|
||||||
{
|
{
|
||||||
label: tString(language, 'link_tooltip_external_link'),
|
label: tString(language, 'link_tooltip_external_link'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
} else if (isSamePage) {
|
||||||
if (isSamePage) {
|
|
||||||
breadcrumbs = [
|
breadcrumbs = [
|
||||||
{
|
{
|
||||||
label: tString(language, 'link_tooltip_page_anchor'),
|
label: tString(language, 'link_tooltip_page_anchor'),
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contentkit-button-style-primary {
|
.contentkit-button-style-primary {
|
||||||
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover ring-0 contrast-more:ring-1;
|
@apply bg-primary-original text-contrast-primary-original hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover ring-0 contrast-more:ring-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentkit-button-style-secondary {
|
.contentkit-button-style-secondary {
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
|||||||
|
|
||||||
const parent = ancestorBlocks[ancestorBlocks.length - 1];
|
const parent = ancestorBlocks[ancestorBlocks.length - 1];
|
||||||
assert(
|
assert(
|
||||||
(parent && parent.type === 'list-ordered') ||
|
parent?.type === 'list-ordered' ||
|
||||||
parent.type === 'list-unordered' ||
|
parent?.type === 'list-unordered' ||
|
||||||
parent.type === 'list-tasks',
|
parent?.type === 'list-tasks',
|
||||||
'Invalid parent list type'
|
'Invalid parent list type'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -112,11 +112,11 @@ function getListItemDepth(input: {
|
|||||||
|
|
||||||
for (let i = ancestorBlocks.length - 1; i >= 0; i--) {
|
for (let i = ancestorBlocks.length - 1; i >= 0; i--) {
|
||||||
const block = ancestorBlocks[i];
|
const block = ancestorBlocks[i];
|
||||||
if (block.type === type) {
|
if (block?.type === type) {
|
||||||
depth = depth + 1;
|
depth = depth + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (block.type === 'list-item') {
|
if (block?.type === 'list-item') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Heading } from '../Heading';
|
|||||||
|
|
||||||
import './scalar.css';
|
import './scalar.css';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import { DEFAULT_LOCALE, getCustomizationLocale } from '@/intl/server';
|
import { DEFAULT_LOCALE, getSpaceLocale } from '@/intl/server';
|
||||||
import type { GitBookAnyContext } from '@/lib/context';
|
import type { GitBookAnyContext } from '@/lib/context';
|
||||||
import type {
|
import type {
|
||||||
AnyOpenAPIOperationsBlock,
|
AnyOpenAPIOperationsBlock,
|
||||||
@@ -30,10 +30,7 @@ export function getOpenAPIContext(args: {
|
|||||||
const { props, specUrl, context } = args;
|
const { props, specUrl, context } = args;
|
||||||
const { block } = props;
|
const { block } = props;
|
||||||
|
|
||||||
const customization = context && 'customization' in context ? context.customization : null;
|
const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE;
|
||||||
const customizationLocale = customization
|
|
||||||
? getCustomizationLocale(customization)
|
|
||||||
: DEFAULT_LOCALE;
|
|
||||||
const locale = checkIsValidLocale(customizationLocale) ? customizationLocale : DEFAULT_LOCALE;
|
const locale = checkIsValidLocale(customizationLocale) ? customizationLocale : DEFAULT_LOCALE;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -272,9 +272,8 @@ body {
|
|||||||
}
|
}
|
||||||
.scalar-activate-button {
|
.scalar-activate-button {
|
||||||
@apply flex gap-2 items-center;
|
@apply flex gap-2 items-center;
|
||||||
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none circular-corners:rounded-full circular-corners:px-3 place-self-start;
|
@apply bg-primary-original text-contrast-primary-original hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none circular-corners:rounded-full circular-corners:px-3 place-self-start;
|
||||||
@apply ring-1 ring-tint hover:ring-tint-hover;
|
@apply shadow-sm shadow-primary dark:shadow-primary-1 hover:shadow-md active:shadow-none;
|
||||||
@apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
|
|
||||||
@apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
|
@apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
|
||||||
@apply hover:scale-105 active:scale-100 transition-all;
|
@apply hover:scale-105 active:scale-100 transition-all;
|
||||||
@apply grow-0 shrink-0 truncate;
|
@apply grow-0 shrink-0 truncate;
|
||||||
|
|||||||
@@ -565,7 +565,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-example-empty {
|
.openapi-example-empty {
|
||||||
@apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center;
|
@apply relative text-tint min-h-20 flex flex-col justify-center items-center text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Common Elements */
|
/* Common Elements */
|
||||||
@@ -746,6 +746,10 @@ body:has(.openapi-select-popover) {
|
|||||||
@apply px-3 transition-all;
|
@apply px-3 transition-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-label {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
.openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-icon > svg {
|
.openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-icon > svg {
|
||||||
@apply rotate-90;
|
@apply rotate-90;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
|
|||||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||||
|
|
||||||
const ancestor = ancestorBlocks[ancestorBlocks.length - 1];
|
const ancestor = ancestorBlocks[ancestorBlocks.length - 1];
|
||||||
assert(ancestor.type === 'stepper', 'Ancestor block must be a stepper');
|
assert(ancestor?.type === 'stepper', 'Ancestor block must be a stepper');
|
||||||
|
|
||||||
const index = ancestor.nodes.indexOf(block);
|
const index = ancestor.nodes.indexOf(block);
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
|
|||||||
<div className="relative select-none">
|
<div className="relative select-none">
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-solid theme-muted:bg-primary-subtle tabular-nums',
|
'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-original theme-muted:bg-primary-subtle tabular-nums contrast-more:bg-primary-solid',
|
||||||
'font-medium text-contrast-primary-solid theme-muted:text-primary'
|
'font-medium text-contrast-primary-original theme-muted:text-primary contrast-more:text-contrast-primary-solid'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Image } from '@/components/utils';
|
|||||||
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
|
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import {
|
import {
|
||||||
|
CardsImageObjectFit,
|
||||||
type ContentRef,
|
type ContentRef,
|
||||||
type DocumentTableViewCards,
|
type DocumentTableViewCards,
|
||||||
SiteInsightsLinkPosition,
|
SiteInsightsLinkPosition,
|
||||||
@@ -25,8 +26,12 @@ export async function RecordCard(
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const [lightCover, darkCover, target] = await Promise.all([
|
const [lightCover, darkCover, target] = await Promise.all([
|
||||||
light && context.contentContext ? resolveContentRef(light, context.contentContext) : null,
|
light.contentRef && context.contentContext
|
||||||
dark && context.contentContext ? resolveContentRef(dark, context.contentContext) : null,
|
? resolveContentRef(light.contentRef, context.contentContext)
|
||||||
|
: null,
|
||||||
|
dark.contentRef && context.contentContext
|
||||||
|
? resolveContentRef(dark.contentRef, context.contentContext)
|
||||||
|
: null,
|
||||||
targetRef && context.contentContext
|
targetRef && context.contentContext
|
||||||
? resolveContentRef(targetRef, context.contentContext)
|
? resolveContentRef(targetRef, context.contentContext)
|
||||||
: null,
|
: null,
|
||||||
@@ -35,6 +40,10 @@ export async function RecordCard(
|
|||||||
const darkCoverIsSquareOrPortrait = isSquareOrPortrait(darkCover);
|
const darkCoverIsSquareOrPortrait = isSquareOrPortrait(darkCover);
|
||||||
const lightCoverIsSquareOrPortrait = isSquareOrPortrait(lightCover);
|
const lightCoverIsSquareOrPortrait = isSquareOrPortrait(lightCover);
|
||||||
|
|
||||||
|
const darkObjectFit = `dark:${getObjectFitClass(dark.objectFit)}`;
|
||||||
|
const lightObjectFit = getObjectFitClass(light.objectFit);
|
||||||
|
const objectFits = `${lightObjectFit} ${darkObjectFit}`;
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
@@ -94,7 +103,7 @@ export async function RecordCard(
|
|||||||
'min-w-0',
|
'min-w-0',
|
||||||
'w-full',
|
'w-full',
|
||||||
'h-full',
|
'h-full',
|
||||||
'object-cover',
|
'bg-tint-subtle',
|
||||||
lightCoverIsSquareOrPortrait || darkCoverIsSquareOrPortrait
|
lightCoverIsSquareOrPortrait || darkCoverIsSquareOrPortrait
|
||||||
? [
|
? [
|
||||||
lightCoverIsSquareOrPortrait
|
lightCoverIsSquareOrPortrait
|
||||||
@@ -104,7 +113,8 @@ export async function RecordCard(
|
|||||||
? 'dark:min-[432px]:aspect-video dark:min-[432px]:h-auto'
|
? 'dark:min-[432px]:aspect-video dark:min-[432px]:h-auto'
|
||||||
: '',
|
: '',
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
: ['h-auto', 'aspect-video']
|
: ['h-auto', 'aspect-video'],
|
||||||
|
objectFits
|
||||||
)}
|
)}
|
||||||
priority={isOffscreen ? 'lazy' : 'high'}
|
priority={isOffscreen ? 'lazy' : 'high'}
|
||||||
preload
|
preload
|
||||||
@@ -190,3 +200,23 @@ function isSquareOrPortrait(contentRef: ResolvedContentRef | null) {
|
|||||||
|
|
||||||
return file.dimensions?.width / file.dimensions?.height <= 1;
|
return file.dimensions?.width / file.dimensions?.height <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the CSS class for object-fit based on the objectFit value.
|
||||||
|
*/
|
||||||
|
function getObjectFitClass(objectFit: CardsImageObjectFit | undefined): string {
|
||||||
|
if (!objectFit) {
|
||||||
|
return 'object-cover';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (objectFit) {
|
||||||
|
case CardsImageObjectFit.Contain:
|
||||||
|
return 'object-contain';
|
||||||
|
case CardsImageObjectFit.Fill:
|
||||||
|
return 'object-fill';
|
||||||
|
case CardsImageObjectFit.Cover:
|
||||||
|
return 'object-cover';
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported object fit: ${objectFit}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ import { getSimplifiedContentType } from '@/lib/files';
|
|||||||
import { resolveContentRef } from '@/lib/references';
|
import { resolveContentRef } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import { filterOutNullable } from '@/lib/typescript';
|
import { filterOutNullable } from '@/lib/typescript';
|
||||||
|
|
||||||
import type { BlockProps } from '../Block';
|
import type { BlockProps } from '../Block';
|
||||||
import { Blocks } from '../Blocks';
|
import { Blocks } from '../Blocks';
|
||||||
import { FileIcon } from '../FileIcon';
|
import { FileIcon } from '../FileIcon';
|
||||||
import type { TableRecordKV } from './Table';
|
import type { TableRecordKV } from './Table';
|
||||||
import { type VerticalAlignment, getColumnAlignment } from './utils';
|
import {
|
||||||
|
type VerticalAlignment,
|
||||||
|
getColumnAlignment,
|
||||||
|
isContentRef,
|
||||||
|
isDocumentTableImageRecord,
|
||||||
|
isStringArray,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
const alignmentMap: Record<'text-left' | 'text-center' | 'text-right', string> = {
|
const alignmentMap: Record<'text-left' | 'text-center' | 'text-right', string> = {
|
||||||
'text-left': '**:text-left text-left',
|
'text-left': '**:text-left text-left',
|
||||||
@@ -58,18 +63,28 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Because definition and value depends on column, we have to check typing in each case at runtime.
|
||||||
|
// Validation should have been done at the API level, but we can't know typing based on `definition.type`.
|
||||||
|
// OpenAPI types cannot really handle discriminated unions based on a dynamic key.
|
||||||
switch (definition.type) {
|
switch (definition.type) {
|
||||||
case 'checkbox':
|
case 'checkbox': {
|
||||||
|
if (value === null || typeof value !== 'boolean') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className={tcls('w-5', 'h-5')}
|
className={tcls('w-5', 'h-5')}
|
||||||
checked={value as boolean}
|
checked={value}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
aria-labelledby={ariaLabelledBy}
|
aria-labelledby={ariaLabelledBy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case 'rating': {
|
case 'rating': {
|
||||||
const rating = value as number;
|
if (typeof value !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rating = value;
|
||||||
const max = definition.max;
|
const max = definition.max;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,15 +123,21 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'number':
|
case 'number': {
|
||||||
|
if (typeof value !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
className={tcls('text-base', 'tabular-nums', 'tracking-tighter')}
|
className={tcls('text-base', 'tabular-nums', 'tracking-tighter')}
|
||||||
aria-labelledby={ariaLabelledBy}
|
aria-labelledby={ariaLabelledBy}
|
||||||
>{`${value}`}</Tag>
|
>{`${value}`}</Tag>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case 'text': {
|
case 'text': {
|
||||||
// @ts-ignore
|
if (typeof value !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const fragment = getNodeFragmentByName(block, value);
|
const fragment = getNodeFragmentByName(block, value);
|
||||||
if (!fragment) {
|
if (!fragment) {
|
||||||
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
|
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
|
||||||
@@ -149,8 +170,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'files': {
|
case 'files': {
|
||||||
|
if (!isStringArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const files = await Promise.all(
|
const files = await Promise.all(
|
||||||
(value as string[]).map((fileId) =>
|
value.map((fileId) =>
|
||||||
context.contentContext
|
context.contentContext
|
||||||
? resolveContentRef(
|
? resolveContentRef(
|
||||||
{
|
{
|
||||||
@@ -221,10 +245,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'content-ref': {
|
case 'content-ref': {
|
||||||
const contentRef = value ? (value as ContentRef) : null;
|
if (value === null || !isContentRef(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const resolved =
|
const resolved =
|
||||||
contentRef && context.contentContext
|
value && context.contentContext
|
||||||
? await resolveContentRef(contentRef, context.contentContext, {
|
? await resolveContentRef(value, context.contentContext, {
|
||||||
resolveAnchorText: true,
|
resolveAnchorText: true,
|
||||||
iconStyle: ['mr-2', 'text-tint-subtle'],
|
iconStyle: ['mr-2', 'text-tint-subtle'],
|
||||||
})
|
})
|
||||||
@@ -239,11 +265,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
<StyledLink
|
<StyledLink
|
||||||
href={resolved.href}
|
href={resolved.href}
|
||||||
insights={
|
insights={
|
||||||
contentRef
|
value
|
||||||
? {
|
? {
|
||||||
type: 'link_click',
|
type: 'link_click',
|
||||||
link: {
|
link: {
|
||||||
target: contentRef,
|
target: value,
|
||||||
position: SiteInsightsLinkPosition.Content,
|
position: SiteInsightsLinkPosition.Content,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -257,8 +283,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'users': {
|
case 'users': {
|
||||||
|
if (!isStringArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const resolved = await Promise.all(
|
const resolved = await Promise.all(
|
||||||
(value as string[]).map(async (userId) => {
|
value.map(async (userId) => {
|
||||||
const contentRef: ContentRefUser = {
|
const contentRef: ContentRefUser = {
|
||||||
kind: 'user',
|
kind: 'user',
|
||||||
user: userId,
|
user: userId,
|
||||||
@@ -295,10 +324,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'select': {
|
case 'select': {
|
||||||
|
if (!isStringArray(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Tag aria-labelledby={ariaLabelledBy}>
|
<Tag aria-labelledby={ariaLabelledBy}>
|
||||||
<span className={tcls('inline-flex', 'gap-2', 'flex-wrap')}>
|
<span className={tcls('inline-flex', 'gap-2', 'flex-wrap')}>
|
||||||
{(value as string[]).map((selectId) => {
|
{value.map((selectId) => {
|
||||||
const option = definition.options.find(
|
const option = definition.options.find(
|
||||||
(option) => option.value === selectId
|
(option) => option.value === selectId
|
||||||
);
|
);
|
||||||
@@ -329,8 +361,15 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'image': {
|
case 'image': {
|
||||||
|
if (!isDocumentTableImageRecord(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const image = context.contentContext
|
const image = context.contentContext
|
||||||
? await resolveContentRef(value as ContentRef, context.contentContext)
|
? await resolveContentRef(
|
||||||
|
'ref' in value ? value.ref : value,
|
||||||
|
context.contentContext
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function RecordRow(
|
|||||||
autoSizedColumns,
|
autoSizedColumns,
|
||||||
fixedColumns,
|
fixedColumns,
|
||||||
});
|
});
|
||||||
|
// @ts-expect-error
|
||||||
const verticalAlignment = getColumnVerticalAlignment(block.data.definition[column]);
|
const verticalAlignment = getColumnVerticalAlignment(block.data.definition[column]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
|||||||
/* Only show the header when configured and not empty */
|
/* Only show the header when configured and not empty */
|
||||||
const withHeader =
|
const withHeader =
|
||||||
!view.hideHeader &&
|
!view.hideHeader &&
|
||||||
view.columns.some((columnId) => block.data.definition[columnId].title.trim().length > 0);
|
view.columns.some(
|
||||||
|
(columnId) => (block.data.definition[columnId]?.title.trim().length ?? 0) > 0
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={tcls(style, styles.tableWrapper)}>
|
<div className={tcls(style, styles.tableWrapper)}>
|
||||||
@@ -43,28 +45,31 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div role="row" className={tcls('flex', 'w-full')}>
|
<div role="row" className={tcls('flex', 'w-full')}>
|
||||||
{view.columns.map((column) => (
|
{view.columns.map((column) => {
|
||||||
<div
|
const definition = block.data.definition[column]!;
|
||||||
key={column}
|
return (
|
||||||
role="columnheader"
|
<div
|
||||||
className={tcls(
|
key={column}
|
||||||
styles.columnHeader,
|
role="columnheader"
|
||||||
getColumnAlignment(block.data.definition[column])
|
className={tcls(
|
||||||
)}
|
styles.columnHeader,
|
||||||
style={{
|
getColumnAlignment(definition)
|
||||||
width: getColumnWidth({
|
)}
|
||||||
column,
|
style={{
|
||||||
columnWidths,
|
width: getColumnWidth({
|
||||||
autoSizedColumns,
|
column,
|
||||||
fixedColumns,
|
columnWidths,
|
||||||
}),
|
autoSizedColumns,
|
||||||
minWidth: columnWidths?.[column] || '100px',
|
fixedColumns,
|
||||||
}}
|
}),
|
||||||
title={block.data.definition[column].title}
|
minWidth: columnWidths?.[column] || '100px',
|
||||||
>
|
}}
|
||||||
{block.data.definition[column].title}
|
title={definition.title}
|
||||||
</div>
|
>
|
||||||
))}
|
{definition.title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
|
CardsImageObjectFit,
|
||||||
ContentRef,
|
ContentRef,
|
||||||
ContentRefFile,
|
ContentRefFile,
|
||||||
ContentRefURL,
|
ContentRefURL,
|
||||||
DocumentTableDefinition,
|
DocumentTableDefinition,
|
||||||
|
DocumentTableImageRecord,
|
||||||
DocumentTableRecord,
|
DocumentTableRecord,
|
||||||
DocumentTableViewCards,
|
DocumentTableViewCards,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
@@ -21,53 +23,64 @@ export function getRecordValue<T extends number | string | boolean | string[] |
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the covers for a record card.
|
* Get the covers for a record card.
|
||||||
* Returns both the light and dark covers.
|
* Returns both the light and dark covers with their content refs and optional object fit.
|
||||||
* The light cover is a string or a content ref (image or files column type).
|
* The light cover is a string or a content ref (image or files column type).
|
||||||
* The dark cover is a content ref (image column type).
|
* The dark cover is a content ref (image column type).
|
||||||
*/
|
*/
|
||||||
export function getRecordCardCovers(
|
export function getRecordCardCovers(
|
||||||
record: DocumentTableRecord,
|
record: DocumentTableRecord,
|
||||||
view: DocumentTableViewCards
|
view: DocumentTableViewCards
|
||||||
): { [key in 'light' | 'dark']: ContentRefFile | ContentRefURL | null } {
|
): {
|
||||||
return {
|
[key in 'light' | 'dark']: {
|
||||||
light: (() => {
|
contentRef: ContentRefFile | ContentRefURL | null;
|
||||||
if (!view.coverDefinition) {
|
objectFit?: CardsImageObjectFit;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = getRecordValue(record, view.coverDefinition) as
|
|
||||||
| ContentRefFile
|
|
||||||
| ContentRefURL
|
|
||||||
| string[];
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (value.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value[0] === 'string') {
|
|
||||||
return { kind: 'file', file: value[0] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value as ContentRefFile | ContentRefURL;
|
|
||||||
})(),
|
|
||||||
dark: (() => {
|
|
||||||
if (!view.coverDefinitionDark) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = getRecordValue(record, view.coverDefinitionDark) as
|
|
||||||
| ContentRefFile
|
|
||||||
| ContentRefURL;
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
})(),
|
|
||||||
};
|
};
|
||||||
|
} {
|
||||||
|
const lightValue = view.coverDefinition
|
||||||
|
? (getRecordValue(record, view.coverDefinition) as DocumentTableImageRecord | string[])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const darkValue = view.coverDefinitionDark
|
||||||
|
? (getRecordValue(record, view.coverDefinitionDark) as DocumentTableImageRecord)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
light: processCoverValue(lightValue),
|
||||||
|
dark: processCoverValue(darkValue),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a cover value and return the content ref and object fit.
|
||||||
|
*/
|
||||||
|
function processCoverValue(value: DocumentTableImageRecord | string[] | null | undefined): {
|
||||||
|
contentRef: ContentRefFile | ContentRefURL | null;
|
||||||
|
objectFit?: CardsImageObjectFit;
|
||||||
|
} {
|
||||||
|
if (!value) {
|
||||||
|
return { contentRef: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 0) {
|
||||||
|
return { contentRef: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value[0] === 'string') {
|
||||||
|
return { contentRef: { kind: 'file', file: value[0] } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's the new schema with objectFit
|
||||||
|
if (value && typeof value === 'object' && 'ref' in value && 'objectFit' in value) {
|
||||||
|
return {
|
||||||
|
contentRef: value.ref,
|
||||||
|
objectFit: value.objectFit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's a direct ContentRef
|
||||||
|
return { contentRef: value as ContentRefFile | ContentRefURL };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,3 +123,35 @@ export function getColumnVerticalAlignment(column: DocumentTableDefinition): Ver
|
|||||||
|
|
||||||
return 'self-center';
|
return 'self-center';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value is a ContentRef.
|
||||||
|
* @param ref The value to check.
|
||||||
|
* @returns True if the value is a ContentRef, false otherwise.
|
||||||
|
*/
|
||||||
|
export function isContentRef(ref?: DocumentTableRecord['values'][string]): ref is ContentRef {
|
||||||
|
return Boolean(ref && typeof ref === 'object' && 'kind' in ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value is an array of strings.
|
||||||
|
* @param value The value to check.
|
||||||
|
* @returns True if the value is an array of strings, false otherwise.
|
||||||
|
*/
|
||||||
|
export function isStringArray(value?: DocumentTableRecord['values'][string]): value is string[] {
|
||||||
|
return Array.isArray(value) && value.every((v) => typeof v === 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value is a DocumentTableImageRecord.
|
||||||
|
* @param value The value to check.
|
||||||
|
* @returns True if the value is a DocumentTableImageRecord, false otherwise.
|
||||||
|
*/
|
||||||
|
export function isDocumentTableImageRecord(
|
||||||
|
value?: DocumentTableRecord['values'][string]
|
||||||
|
): value is DocumentTableImageRecord {
|
||||||
|
if (isContentRef(value) && (value.kind === 'file' || value.kind === 'url')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Boolean(value && typeof value === 'object' && 'ref' in value && isContentRef(value.ref));
|
||||||
|
}
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export function DynamicTabs(
|
|||||||
'max-w-full',
|
'max-w-full',
|
||||||
'truncate',
|
'truncate',
|
||||||
|
|
||||||
active.id === tab.id
|
active?.id === tab.id
|
||||||
? [
|
? [
|
||||||
'shrink-0',
|
'shrink-0',
|
||||||
'active-tab',
|
'active-tab',
|
||||||
@@ -222,7 +222,7 @@ export function DynamicTabs(
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={active.id === tab.id}
|
aria-selected={active?.id === tab.id}
|
||||||
aria-controls={getTabPanelId(tab.id)}
|
aria-controls={getTabPanelId(tab.id)}
|
||||||
id={getTabButtonId(tab.id)}
|
id={getTabButtonId(tab.id)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -255,7 +255,7 @@ export function DynamicTabs(
|
|||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
id={getTabPanelId(tab.id)}
|
id={getTabPanelId(tab.id)}
|
||||||
aria-labelledby={getTabButtonId(tab.id)}
|
aria-labelledby={getTabButtonId(tab.id)}
|
||||||
className={tcls('p-4', tab.id !== active.id ? 'hidden' : null)}
|
className={tcls('p-4', tab.id !== active?.id ? 'hidden' : null)}
|
||||||
>
|
>
|
||||||
{tabsBody[index]}
|
{tabsBody[index]}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ interface MarkedLeafProps<Mark extends DocumentTextMark> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Bold(props: MarkedLeafProps<DocumentMarkBold>) {
|
function Bold(props: MarkedLeafProps<DocumentMarkBold>) {
|
||||||
return <strong className={tcls('font-bold')}>{props.children}</strong>;
|
return <strong className="font-bold">{props.children}</strong>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Italic(props: MarkedLeafProps<DocumentMarkItalic>) {
|
function Italic(props: MarkedLeafProps<DocumentMarkItalic>) {
|
||||||
return <i className={tcls('font-italic')}>{props.children}</i>;
|
return <i className="font-italic">{props.children}</i>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Strikethrough(props: MarkedLeafProps<DocumentMarkStrikethrough>) {
|
function Strikethrough(props: MarkedLeafProps<DocumentMarkStrikethrough>) {
|
||||||
return <s className={tcls('line-through')}>{props.children}</s>;
|
return <s className="line-through">{props.children}</s>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Keyboard(props: MarkedLeafProps<DocumentMarkKeyboard>) {
|
function Keyboard(props: MarkedLeafProps<DocumentMarkKeyboard>) {
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export function getBlockTextStyle(block: DocumentBlock): {
|
|||||||
case 'list-ordered':
|
case 'list-ordered':
|
||||||
case 'list-tasks':
|
case 'list-tasks':
|
||||||
case 'list-unordered':
|
case 'list-unordered':
|
||||||
return getBlockTextStyle(block.nodes[0]);
|
return getBlockTextStyle(block.nodes[0]!);
|
||||||
case 'list-item':
|
case 'list-item':
|
||||||
return getBlockTextStyle(block.nodes[0]);
|
return getBlockTextStyle(block.nodes[0]!);
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
textSize: 'text-base',
|
textSize: 'text-base',
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function isBlockOffscreen(
|
|||||||
|
|
||||||
const allAncestors = [document, ...ancestorBlocks];
|
const allAncestors = [document, ...ancestorBlocks];
|
||||||
for (let index = allAncestors.length - 1; index >= 0; index--) {
|
for (let index = allAncestors.length - 1; index >= 0; index--) {
|
||||||
const parent = allAncestors[index];
|
const parent = allAncestors[index]!;
|
||||||
const offset = getBlockOffset({ block: current, parent });
|
const offset = getBlockOffset({ block: current, parent });
|
||||||
|
|
||||||
if (offset > screenHeight) {
|
if (offset > screenHeight) {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
AIChatDynamicIcon,
|
AIChatDynamicIcon,
|
||||||
AIChatSubtitle,
|
AIChatSubtitle,
|
||||||
} from '@/components/AIChat';
|
} from '@/components/AIChat';
|
||||||
|
import * as api from '@gitbook/api';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTrackEvent } from '../Insights';
|
||||||
import {
|
import {
|
||||||
EmbeddableFrame,
|
EmbeddableFrame,
|
||||||
EmbeddableFrameBody,
|
EmbeddableFrameBody,
|
||||||
@@ -28,6 +31,20 @@ export function EmbeddableAIChat(props: {
|
|||||||
const chatController = useAIChatController();
|
const chatController = useAIChatController();
|
||||||
const configuration = useEmbeddableConfiguration();
|
const configuration = useEmbeddableConfiguration();
|
||||||
|
|
||||||
|
// Track the view of the AI chat
|
||||||
|
const trackEvent = useTrackEvent();
|
||||||
|
React.useEffect(() => {
|
||||||
|
trackEvent(
|
||||||
|
{
|
||||||
|
type: 'ask_view',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageId: null,
|
||||||
|
displayContext: api.SiteInsightsDisplayContext.Embed,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [trackEvent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmbeddableFrame>
|
<EmbeddableFrame>
|
||||||
<EmbeddableFrameHeader>
|
<EmbeddableFrameHeader>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type PagePathParams, getSitePageData } from '@/components/SitePage';
|
|||||||
|
|
||||||
import { PageBody } from '@/components/PageBody';
|
import { PageBody } from '@/components/PageBody';
|
||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
|
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Button } from '../primitives';
|
import { Button } from '../primitives';
|
||||||
import {
|
import {
|
||||||
@@ -54,6 +55,7 @@ export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
|
|||||||
ancestors={ancestors}
|
ancestors={ancestors}
|
||||||
document={document}
|
document={document}
|
||||||
withPageFeedback={withPageFeedback}
|
withPageFeedback={withPageFeedback}
|
||||||
|
insightsDisplayContext={SiteInsightsDisplayContext.Embed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EmbeddableFrameBody>
|
</EmbeddableFrameBody>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { AIChatProvider, AIContextProvider } from '@/components/AI';
|
import { AIContextProvider } from '@/components/AI';
|
||||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||||
import {
|
import {
|
||||||
SiteLayoutClientContexts,
|
SiteLayoutClientContexts,
|
||||||
generateSiteLayoutMetadata,
|
generateSiteLayoutMetadata,
|
||||||
generateSiteLayoutViewport,
|
generateSiteLayoutViewport,
|
||||||
} from '@/components/SiteLayout';
|
} from '@/components/SiteLayout';
|
||||||
|
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import { CustomizationAIMode } from '@gitbook/api';
|
import { CustomizationAIMode } from '@gitbook/api';
|
||||||
|
import { SpaceLayoutServerContext } from '../SpaceLayout';
|
||||||
import { EmbeddableIframeAPI } from './EmbeddableIframeAPI';
|
import { EmbeddableIframeAPI } from './EmbeddableIframeAPI';
|
||||||
|
|
||||||
type EmbeddableRootLayoutProps = {
|
type EmbeddableRootLayoutProps = {
|
||||||
context: GitBookSiteContext;
|
context: GitBookSiteContext;
|
||||||
|
withTracking: boolean;
|
||||||
|
visitorAuthClaims: VisitorAuthClaims;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,10 +22,12 @@ type EmbeddableRootLayoutProps = {
|
|||||||
*/
|
*/
|
||||||
export async function EmbeddableRootLayout({
|
export async function EmbeddableRootLayout({
|
||||||
context,
|
context,
|
||||||
|
withTracking,
|
||||||
|
visitorAuthClaims,
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<EmbeddableRootLayoutProps>) {
|
}: React.PropsWithChildren<EmbeddableRootLayoutProps>) {
|
||||||
return (
|
return (
|
||||||
<CustomizationRootLayout customization={context.customization}>
|
<CustomizationRootLayout context={context}>
|
||||||
<SiteLayoutClientContexts
|
<SiteLayoutClientContexts
|
||||||
forcedTheme={context.customization.themes.default}
|
forcedTheme={context.customization.themes.default}
|
||||||
externalLinksTarget={context.customization.externalLinks.target}
|
externalLinksTarget={context.customization.externalLinks.target}
|
||||||
@@ -31,8 +37,11 @@ export async function EmbeddableRootLayout({
|
|||||||
aiMode={CustomizationAIMode.Assistant}
|
aiMode={CustomizationAIMode.Assistant}
|
||||||
trademark={context.customization.trademark.enabled}
|
trademark={context.customization.trademark.enabled}
|
||||||
>
|
>
|
||||||
<AIChatProvider
|
<SpaceLayoutServerContext
|
||||||
renderMessageOptions={{
|
context={context}
|
||||||
|
withTracking={withTracking}
|
||||||
|
visitorAuthClaims={visitorAuthClaims}
|
||||||
|
aiChatRenderMessageOptions={{
|
||||||
withLinkPreviews: false,
|
withLinkPreviews: false,
|
||||||
asEmbeddable: true,
|
asEmbeddable: true,
|
||||||
}}
|
}}
|
||||||
@@ -41,17 +50,17 @@ export async function EmbeddableRootLayout({
|
|||||||
<EmbeddableIframeAPI
|
<EmbeddableIframeAPI
|
||||||
baseURL={context.linker.toPathInSpace('~gitbook/embed/')}
|
baseURL={context.linker.toPathInSpace('~gitbook/embed/')}
|
||||||
/>
|
/>
|
||||||
</AIChatProvider>
|
</SpaceLayoutServerContext>
|
||||||
</AIContextProvider>
|
</AIContextProvider>
|
||||||
</SiteLayoutClientContexts>
|
</SiteLayoutClientContexts>
|
||||||
</CustomizationRootLayout>
|
</CustomizationRootLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateEmbeddableViewport({ context }: EmbeddableRootLayoutProps) {
|
export async function generateEmbeddableViewport({ context }: { context: GitBookSiteContext }) {
|
||||||
return generateSiteLayoutViewport(context);
|
return generateSiteLayoutViewport(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateEmbeddableMetadata({ context }: EmbeddableRootLayoutProps) {
|
export async function generateEmbeddableMetadata({ context }: { context: GitBookSiteContext }) {
|
||||||
return generateSiteLayoutMetadata(context);
|
return generateSiteLayoutMetadata(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,111 +32,109 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
|||||||
mobileOnly ? 'xl:hidden' : null
|
mobileOnly ? 'xl:hidden' : null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="scroll-nojump">
|
<div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
|
||||||
<div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
|
<div
|
||||||
<div
|
className={tcls(
|
||||||
className={tcls(
|
'mx-auto grid max-w-3xl site-width-wide:max-w-screen-2xl justify-between gap-12 lg:max-w-none!',
|
||||||
'mx-auto grid max-w-3xl site-width-wide:max-w-screen-2xl justify-between gap-12 lg:max-w-none!',
|
'grid-cols-[auto_auto]',
|
||||||
'grid-cols-[auto_auto]',
|
'lg:grid-cols-[18rem_minmax(auto,48rem)_auto]',
|
||||||
'lg:grid-cols-[18rem_minmax(auto,48rem)_auto]',
|
'xl:grid-cols-[18rem_minmax(auto,48rem)_14rem]',
|
||||||
'xl:grid-cols-[18rem_minmax(auto,48rem)_14rem]',
|
'lg:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_auto]',
|
||||||
'lg:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_auto]',
|
'xl:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_14rem]',
|
||||||
'xl:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_14rem]',
|
'lg:page-no-toc:grid-cols-[minmax(auto,48rem)_auto]',
|
||||||
'lg:page-no-toc:grid-cols-[minmax(auto,48rem)_auto]',
|
'xl:page-no-toc:grid-cols-[14rem_minmax(auto,48rem)_14rem]',
|
||||||
'xl:page-no-toc:grid-cols-[14rem_minmax(auto,48rem)_14rem]',
|
'lg:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[minmax(auto,90rem)_auto]',
|
||||||
'lg:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[minmax(auto,90rem)_auto]',
|
'xl:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[14rem_minmax(auto,90rem)_14rem]'
|
||||||
'xl:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[14rem_minmax(auto,90rem)_14rem]'
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{
|
||||||
{
|
// Footer Logo
|
||||||
// Footer Logo
|
customization.footer.logo ? (
|
||||||
customization.footer.logo ? (
|
<div className="col-start-1 row-start-1">
|
||||||
<div className="col-start-1 row-start-1">
|
<Image
|
||||||
<Image
|
alt="Logo"
|
||||||
alt="Logo"
|
resize={context.imageResizer}
|
||||||
resize={context.imageResizer}
|
sources={{
|
||||||
sources={{
|
light: {
|
||||||
light: {
|
src: customization.footer.logo.light,
|
||||||
src: customization.footer.logo.light,
|
},
|
||||||
},
|
dark: customization.footer.logo.dark
|
||||||
dark: customization.footer.logo.dark
|
? {
|
||||||
? {
|
src: customization.footer.logo.dark,
|
||||||
src: customization.footer.logo.dark,
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
}}
|
||||||
}}
|
priority="lazy"
|
||||||
priority="lazy"
|
style={[
|
||||||
style={[
|
'w-auto',
|
||||||
'w-auto',
|
'max-w-40',
|
||||||
'max-w-40',
|
'lg:max-w-64',
|
||||||
'lg:max-w-64',
|
'max-h-10',
|
||||||
'max-h-10',
|
'lg:max-h-12',
|
||||||
'lg:max-h-12',
|
'object-contain',
|
||||||
'object-contain',
|
'object-left',
|
||||||
'object-left',
|
'rounded-sm',
|
||||||
'rounded-sm',
|
'straight-corners:rounded-xs',
|
||||||
'straight-corners:rounded-xs',
|
]}
|
||||||
]}
|
sizes={[
|
||||||
sizes={[
|
{
|
||||||
{
|
width: 320,
|
||||||
width: 320,
|
},
|
||||||
},
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
) : null
|
||||||
) : null
|
}
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
// Theme Toggle
|
// Theme Toggle
|
||||||
customization.themes.toggeable ? (
|
customization.themes.toggeable ? (
|
||||||
<div className="-col-start-2 row-start-1 flex items-start justify-end xl:hidden">
|
<div className="-col-start-2 row-start-1 flex items-start justify-end xl:hidden">
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
<ThemeToggler />
|
<ThemeToggler />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Navigation groups (split into equal columns)
|
// Navigation groups (split into equal columns)
|
||||||
customization.footer.groups?.length > 0 ? (
|
customization.footer.groups?.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'col-span-2 lg:page-has-toc:col-span-1 lg:page-has-toc:col-start-2 xl:page-no-toc:col-span-1 xl:page-no-toc:col-start-2'
|
'col-span-2 lg:page-has-toc:col-span-1 lg:page-has-toc:col-start-2 xl:page-no-toc:col-span-1 xl:page-no-toc:col-start-2'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mx-auto flex max-w-3xl site-width-wide:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6">
|
||||||
|
{partition(customization.footer.groups, FOOTER_COLUMNS).map(
|
||||||
|
(column, columnIndex) => (
|
||||||
|
<div
|
||||||
|
key={columnIndex}
|
||||||
|
className="flex flex-1 grow flex-col gap-10"
|
||||||
|
>
|
||||||
|
{column.map((group, groupIndex) => (
|
||||||
|
<FooterLinksGroup
|
||||||
|
key={groupIndex}
|
||||||
|
group={group}
|
||||||
|
context={context}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<div className="mx-auto flex max-w-3xl site-width-wide:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6">
|
|
||||||
{partition(customization.footer.groups, FOOTER_COLUMNS).map(
|
|
||||||
(column, columnIndex) => (
|
|
||||||
<div
|
|
||||||
key={columnIndex}
|
|
||||||
className="flex flex-1 grow flex-col gap-10"
|
|
||||||
>
|
|
||||||
{column.map((group, groupIndex) => (
|
|
||||||
<FooterLinksGroup
|
|
||||||
key={groupIndex}
|
|
||||||
group={group}
|
|
||||||
context={context}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : null
|
</div>
|
||||||
}
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Legal
|
// Legal
|
||||||
customization.footer.copyright ? (
|
customization.footer.copyright ? (
|
||||||
<div className="order-last col-span-full flex w-full grow flex-col items-center gap-2 text-center text-tint text-xs">
|
<div className="order-last col-span-full flex w-full grow flex-col items-center gap-2 text-center text-tint text-xs">
|
||||||
<p>{customization.footer.copyright}</p>
|
<p>{customization.footer.copyright}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import type { GitBookSiteContext } from '@/lib/context';
|
|||||||
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '@/components/layout';
|
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '@/components/layout';
|
||||||
import { getSpaceLanguage, t } from '@/intl/server';
|
import { getSpaceLanguage, t } from '@/intl/server';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
import { SearchContainer } from '../Search';
|
||||||
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
|
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
|
||||||
import { HeaderLink } from './HeaderLink';
|
import { HeaderLink } from './HeaderLink';
|
||||||
import { HeaderLinkMore } from './HeaderLinkMore';
|
import { HeaderLinkMore } from './HeaderLinkMore';
|
||||||
import { HeaderLinks } from './HeaderLinks';
|
import { HeaderLinks } from './HeaderLinks';
|
||||||
import { HeaderLogo } from './HeaderLogo';
|
import { HeaderLogo } from './HeaderLogo';
|
||||||
import { HeaderMobileMenu } from './HeaderMobileMenu';
|
import { HeaderMobileMenu } from './HeaderMobileMenu';
|
||||||
import { SpacesDropdown } from './SpacesDropdown';
|
import { TranslationsDropdown } from './SpacesDropdown';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the header for the space.
|
* Render the header for the space.
|
||||||
@@ -17,9 +18,9 @@ import { SpacesDropdown } from './SpacesDropdown';
|
|||||||
export function Header(props: {
|
export function Header(props: {
|
||||||
context: GitBookSiteContext;
|
context: GitBookSiteContext;
|
||||||
withTopHeader?: boolean;
|
withTopHeader?: boolean;
|
||||||
search?: React.ReactNode;
|
withVariants?: 'generic' | 'translations';
|
||||||
}) {
|
}) {
|
||||||
const { context, withTopHeader, search } = props;
|
const { context, withTopHeader, withVariants } = props;
|
||||||
const { siteSpace, siteSpaces, sections, customization } = context;
|
const { siteSpace, siteSpaces, sections, customization } = context;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,135 +57,146 @@ export function Header(props: {
|
|||||||
'theme-bold:shadow-tint-12/2'
|
'theme-bold:shadow-tint-12/2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="scroll-nojump">
|
<div className="transition-all 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
|
||||||
|
className={tcls(
|
||||||
|
'gap-4',
|
||||||
|
'lg:gap-6',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'justify-between',
|
||||||
|
'w-full',
|
||||||
|
'py-3',
|
||||||
|
'min-h-16',
|
||||||
|
'sm:h-16',
|
||||||
|
CONTAINER_STYLE
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'gap-4',
|
'flex max-w-full lg:basis-72',
|
||||||
'lg:gap-6',
|
'min-w-0 shrink items-center justify-start gap-2 lg:gap-4'
|
||||||
'flex',
|
|
||||||
'items-center',
|
|
||||||
'justify-between',
|
|
||||||
'w-full',
|
|
||||||
'py-3',
|
|
||||||
'min-h-16',
|
|
||||||
'sm:h-16',
|
|
||||||
CONTAINER_STYLE
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<HeaderMobileMenu
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'flex max-w-full lg:basis-72',
|
'-ml-2',
|
||||||
'min-w-0 shrink items-center justify-start gap-2 lg:gap-4'
|
'text-tint-strong',
|
||||||
|
'theme-bold:text-header-link',
|
||||||
|
'hover:bg-tint-hover',
|
||||||
|
'hover:theme-bold:bg-header-link/3',
|
||||||
|
withVariants === 'generic'
|
||||||
|
? 'xl:hidden'
|
||||||
|
: 'page-no-toc:hidden lg:hidden'
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
<HeaderMobileMenu
|
<HeaderLogo context={context} />
|
||||||
className={tcls(
|
|
||||||
'lg:hidden',
|
|
||||||
'-ml-2',
|
|
||||||
'text-tint-strong',
|
|
||||||
'theme-bold:text-header-link',
|
|
||||||
'hover:bg-tint-hover',
|
|
||||||
'hover:theme-bold:bg-header-link/3',
|
|
||||||
'page-no-toc:hidden'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<HeaderLogo context={context} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={tcls(
|
|
||||||
'flex',
|
|
||||||
'grow-0',
|
|
||||||
'shrink-0',
|
|
||||||
'md:basis-56',
|
|
||||||
'justify-self-end',
|
|
||||||
'items-center',
|
|
||||||
'gap-2',
|
|
||||||
'search' in customization.styling &&
|
|
||||||
customization.styling.search === 'prominent'
|
|
||||||
? [
|
|
||||||
'md:grow-[0.8]',
|
|
||||||
'lg:basis-40',
|
|
||||||
'md:max-w-[40%]',
|
|
||||||
'lg:max-w-lg',
|
|
||||||
'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem)
|
|
||||||
'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
|
|
||||||
'md:mr-auto',
|
|
||||||
'order-last',
|
|
||||||
'md:order-[unset]',
|
|
||||||
]
|
|
||||||
: ['order-last']
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{search}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{customization.header.links.length > 0 && (
|
|
||||||
<HeaderLinks>
|
|
||||||
{customization.header.links.map((link) => {
|
|
||||||
return (
|
|
||||||
<HeaderLink
|
|
||||||
key={link.title}
|
|
||||||
link={link}
|
|
||||||
context={context}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<HeaderLinkMore
|
|
||||||
label={t(getSpaceLanguage(customization), 'more')}
|
|
||||||
links={customization.header.links}
|
|
||||||
context={context}
|
|
||||||
/>
|
|
||||||
</HeaderLinks>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={tcls(
|
||||||
|
'flex',
|
||||||
|
'grow-0',
|
||||||
|
'shrink-0',
|
||||||
|
'md:basis-56',
|
||||||
|
'justify-self-end',
|
||||||
|
'items-center',
|
||||||
|
'gap-2',
|
||||||
|
'search' in customization.styling &&
|
||||||
|
customization.styling.search === 'prominent'
|
||||||
|
? [
|
||||||
|
'md:grow-[0.8]',
|
||||||
|
'lg:basis-40',
|
||||||
|
'md:max-w-[40%]',
|
||||||
|
'lg:max-w-lg',
|
||||||
|
'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem)
|
||||||
|
'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
|
||||||
|
'md:mr-auto',
|
||||||
|
'order-last',
|
||||||
|
'md:order-[unset]',
|
||||||
|
]
|
||||||
|
: ['order-last']
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SearchContainer
|
||||||
|
style={customization.styling.search}
|
||||||
|
isMultiVariants={siteSpaces.length > 1}
|
||||||
|
spaceTitle={siteSpace.title}
|
||||||
|
siteSpaceId={siteSpace.id}
|
||||||
|
viewport={!withTopHeader ? 'mobile' : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{customization.header.links.length > 0 ||
|
||||||
|
(!sections && withVariants === 'translations') ? (
|
||||||
|
<HeaderLinks>
|
||||||
|
{customization.header.links.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{customization.header.links.map((link) => {
|
||||||
|
return (
|
||||||
|
<HeaderLink
|
||||||
|
key={link.title}
|
||||||
|
link={link}
|
||||||
|
context={context}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<HeaderLinkMore
|
||||||
|
label={t(getSpaceLanguage(context), 'more')}
|
||||||
|
links={customization.header.links}
|
||||||
|
context={context}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{!sections && withVariants === 'translations' ? (
|
||||||
|
<TranslationsDropdown
|
||||||
|
context={context}
|
||||||
|
siteSpace={siteSpace}
|
||||||
|
siteSpaces={siteSpaces}
|
||||||
|
className="flex! theme-bold:text-header-link hover:theme-bold:bg-header-link/3"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</HeaderLinks>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sections || siteSpaces.length > 1 ? (
|
{sections ? (
|
||||||
<div className="scroll-nojump">
|
<div className="transition-all 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
|
||||||
|
className={tcls(
|
||||||
|
'w-full',
|
||||||
|
'overflow-x-auto',
|
||||||
|
'no-scrollbar',
|
||||||
|
'-mb-4 pb-4', // Positive padding / negative margin allows the navigation menu indicator to show in a scroll viewƒ
|
||||||
|
!sections ? ['hidden', 'page-no-toc:flex'] : 'flex'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'w-full',
|
CONTAINER_STYLE,
|
||||||
'overflow-x-auto',
|
'grow',
|
||||||
'no-scrollbar',
|
'flex',
|
||||||
'-mb-4 pb-4', // Positive padding / negative margin allows the navigation menu indicator to show in a scroll viewƒ
|
'items-end',
|
||||||
!sections ? ['hidden', 'page-no-toc:flex'] : 'flex'
|
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
{sections.list.some((s) => s.object === 'site-section-group') || // If there's even a single group, show the tabs
|
||||||
className={tcls(
|
sections.list.length > 1 ? ( // Otherwise, show the tabs if there's more than one section
|
||||||
CONTAINER_STYLE,
|
<SiteSectionTabs
|
||||||
'grow',
|
sections={encodeClientSiteSections(context, sections)}
|
||||||
'flex',
|
/>
|
||||||
'items-end',
|
) : null}
|
||||||
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]'
|
{withVariants === 'translations' ? (
|
||||||
)}
|
<div className="site-background before:contents[] -mr-4 sm:-mr-6 md:-mr-8 sticky inset-y-0 right-0 z-10 ml-6 flex h-full items-center py-2 pr-4 before:mr-4 before:h-full before:border-tint before:border-l sm:pr-6 md:pr-8">
|
||||||
>
|
<TranslationsDropdown
|
||||||
{siteSpaces.length > 1 && (
|
context={context}
|
||||||
<div
|
siteSpace={siteSpace}
|
||||||
id="variants"
|
siteSpaces={siteSpaces}
|
||||||
className="my-2 mr-5 grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
|
/>
|
||||||
>
|
</div>
|
||||||
<SpacesDropdown
|
) : null}
|
||||||
context={context}
|
|
||||||
siteSpace={siteSpace}
|
|
||||||
siteSpaces={siteSpaces}
|
|
||||||
className="w-full grow py-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{sections &&
|
|
||||||
(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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ function HeaderItemButton(
|
|||||||
|
|
||||||
function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPreset }) {
|
function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPreset }) {
|
||||||
return tcls(
|
return tcls(
|
||||||
'flex items-center',
|
'flex items-center gap-1',
|
||||||
'shrink',
|
'shrink',
|
||||||
'contrast-more:underline',
|
'contrast-more:underline',
|
||||||
'truncate',
|
'truncate',
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import type { SiteSpace } from '@gitbook/api';
|
import type { SiteSpace } from '@gitbook/api';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import { getSiteSpaceURL } from '@/lib/sites';
|
import { getSiteSpaceURL } from '@/lib/sites';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
import { Button, type ButtonProps } from '../primitives';
|
||||||
import { DropdownChevron, DropdownMenu } from '../primitives/DropdownMenu';
|
import { DropdownChevron, DropdownMenu } from '../primitives/DropdownMenu';
|
||||||
import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
|
import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
|
||||||
|
|
||||||
|
// Memoized regex for checking if a string starts with an emoji
|
||||||
|
const EMOJI_REGEX = /^\p{Emoji}/u;
|
||||||
|
|
||||||
|
function startsWithEmoji(text: string): boolean {
|
||||||
|
return EMOJI_REGEX.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
export function SpacesDropdown(props: {
|
export function SpacesDropdown(props: {
|
||||||
context: GitBookSiteContext;
|
context: GitBookSiteContext;
|
||||||
siteSpace: SiteSpace;
|
siteSpace: SiteSpace;
|
||||||
siteSpaces: SiteSpace[];
|
siteSpaces: SiteSpace[];
|
||||||
className?: string;
|
className?: string;
|
||||||
|
variant?: ButtonProps['variant'];
|
||||||
|
icon?: ButtonProps['icon'];
|
||||||
}) {
|
}) {
|
||||||
const { context, siteSpace, siteSpaces, className } = props;
|
const { context, siteSpace, siteSpaces, className, variant = 'secondary', icon } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
@@ -21,48 +32,16 @@ export function SpacesDropdown(props: {
|
|||||||
'group-focus-within/dropdown:group-hover/dropdown:visible' // When the dropdown is already open, it should remain visible when hovered
|
'group-focus-within/dropdown:group-hover/dropdown:visible' // When the dropdown is already open, it should remain visible when hovered
|
||||||
)}
|
)}
|
||||||
button={
|
button={
|
||||||
<div
|
<Button
|
||||||
|
icon={icon}
|
||||||
data-testid="space-dropdown-button"
|
data-testid="space-dropdown-button"
|
||||||
className={tcls(
|
size="medium"
|
||||||
'flex',
|
variant={variant}
|
||||||
'flex-row',
|
trailing={<DropdownChevron />}
|
||||||
'items-center',
|
className={tcls('bg-tint-base', className)}
|
||||||
'transition-all',
|
|
||||||
'hover:cursor-pointer',
|
|
||||||
|
|
||||||
'px-3',
|
|
||||||
'py-2',
|
|
||||||
'gap-2',
|
|
||||||
|
|
||||||
'rounded-md',
|
|
||||||
'straight-corners:rounded-none',
|
|
||||||
|
|
||||||
'bg-tint-base',
|
|
||||||
|
|
||||||
'text-sm',
|
|
||||||
'text-tint',
|
|
||||||
'hover:text-tint-strong',
|
|
||||||
'data-[state=open]:text-tint-strong',
|
|
||||||
|
|
||||||
'ring-1',
|
|
||||||
'ring-tint-subtle',
|
|
||||||
'hover:ring-tint-hover',
|
|
||||||
'data-[state=open]:ring-tint-hover',
|
|
||||||
|
|
||||||
'contrast-more:bg-tint-base',
|
|
||||||
'contrast-more:ring-1',
|
|
||||||
'contrast-more:hover:ring-2',
|
|
||||||
'contrast-more:data-[state=open]:ring-2',
|
|
||||||
'contrast-more:ring-tint',
|
|
||||||
'contrast-more:hover:ring-tint-hover',
|
|
||||||
'contrast-more:data-[state=open]:ring-tint-hover',
|
|
||||||
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<span className={tcls('truncate', 'grow')}>{siteSpace.title}</span>
|
<span className="button-content">{siteSpace.title}</span>
|
||||||
<DropdownChevron />
|
</Button>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SpacesDropdownMenuItems
|
<SpacesDropdownMenuItems
|
||||||
@@ -77,3 +56,32 @@ export function SpacesDropdown(props: {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function TranslationsDropdown(props: {
|
||||||
|
context: GitBookSiteContext;
|
||||||
|
siteSpace: SiteSpace;
|
||||||
|
siteSpaces: SiteSpace[];
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const { context, siteSpace, siteSpaces, className } = props;
|
||||||
|
|
||||||
|
// Memoize the emoji check to avoid repeated regex execution
|
||||||
|
const hasEmojiPrefix = useMemo(() => startsWithEmoji(siteSpace.title), [siteSpace.title]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpacesDropdown
|
||||||
|
icon="globe"
|
||||||
|
context={context}
|
||||||
|
siteSpace={siteSpace}
|
||||||
|
siteSpaces={siteSpaces}
|
||||||
|
variant="blank"
|
||||||
|
className={tcls(
|
||||||
|
'-mx-2 bg-transparent px-2 py-1 lg:max-w-64 max-md:[&_.button-content]:hidden',
|
||||||
|
hasEmojiPrefix
|
||||||
|
? 'md:[&_.button-leading-icon]:hidden' // If the title starts with an emoji, don't show the icon (on desktop)
|
||||||
|
: '',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type InsightsEventName = api.SiteInsightsEvent['type'];
|
|||||||
* Context for an event on a page.
|
* Context for an event on a page.
|
||||||
*/
|
*/
|
||||||
export interface InsightsEventPageContext {
|
export interface InsightsEventPageContext {
|
||||||
|
displayContext: api.SiteInsightsDisplayContext;
|
||||||
pageId: string | null;
|
pageId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +47,9 @@ type TrackEventCallback = <EventName extends InsightsEventName>(
|
|||||||
options?: InsightsEventOptions
|
options?: InsightsEventOptions
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
const InsightsContext = React.createContext<TrackEventCallback>(() => {});
|
const InsightsContext = React.createContext<TrackEventCallback>(() => {
|
||||||
|
console.error('useTrackEvent must be used within an InsightsProvider');
|
||||||
|
});
|
||||||
|
|
||||||
interface InsightsProviderProps {
|
interface InsightsProviderProps {
|
||||||
/** If true, the events will be sent to the server. */
|
/** If true, the events will be sent to the server. */
|
||||||
@@ -263,6 +266,7 @@ function transformEvents(input: {
|
|||||||
siteShareKey: input.context.siteShareKey ?? null,
|
siteShareKey: input.context.siteShareKey ?? null,
|
||||||
revision: input.context.revisionId,
|
revision: input.context.revisionId,
|
||||||
page: input.pageContext.pageId,
|
page: input.pageContext.pageId,
|
||||||
|
displayContext: input.pageContext.displayContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
return input.events.map((partialEvent) => {
|
return input.events.map((partialEvent) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import type { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { useCurrentPage } from '../hooks';
|
import { useCurrentPage } from '../hooks';
|
||||||
@@ -8,7 +9,10 @@ import { useTrackEvent } from './InsightsProvider';
|
|||||||
/**
|
/**
|
||||||
* Track a page view event.
|
* Track a page view event.
|
||||||
*/
|
*/
|
||||||
export function TrackPageViewEvent() {
|
export function TrackPageViewEvent(props: {
|
||||||
|
displayContext: SiteInsightsDisplayContext;
|
||||||
|
}) {
|
||||||
|
const { displayContext } = props;
|
||||||
const page = useCurrentPage();
|
const page = useCurrentPage();
|
||||||
const trackEvent = useTrackEvent();
|
const trackEvent = useTrackEvent();
|
||||||
|
|
||||||
@@ -19,9 +23,10 @@ export function TrackPageViewEvent() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
pageId: page?.pageId ?? null,
|
pageId: page?.pageId ?? null,
|
||||||
|
displayContext,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [page, trackEvent]);
|
}, [page, trackEvent, displayContext]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export async function PDFPage(props: {
|
|||||||
|
|
||||||
const customization =
|
const customization =
|
||||||
'customization' in baseContext ? baseContext.customization : defaultCustomization();
|
'customization' in baseContext ? baseContext.customization : defaultCustomization();
|
||||||
const language = getSpaceLanguage(customization);
|
const language = getSpaceLanguage(baseContext);
|
||||||
|
|
||||||
// Compute the pages to render
|
// Compute the pages to render
|
||||||
const { pages, total } = selectPages(baseContext.revision.pages, pdfParams);
|
const { pages, total } = selectPages(baseContext.revision.pages, pdfParams);
|
||||||
@@ -149,8 +149,7 @@ export async function PDFPage(props: {
|
|||||||
trademark={
|
trademark={
|
||||||
customization.trademark.enabled ? (
|
customization.trademark.enabled ? (
|
||||||
<TrademarkLink
|
<TrademarkLink
|
||||||
space={context.space}
|
context={context}
|
||||||
customization={customization}
|
|
||||||
placement={SiteInsightsTrademarkPlacement.Pdf}
|
placement={SiteInsightsTrademarkPlacement.Pdf}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||||
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
|
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
|
||||||
import { defaultCustomization } from '@/lib/utils';
|
|
||||||
import { CustomizationThemeMode } from '@gitbook/api';
|
import { CustomizationThemeMode } from '@gitbook/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,12 +12,7 @@ export async function PDFRootLayout(props: {
|
|||||||
const { context, children } = props;
|
const { context, children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomizationRootLayout
|
<CustomizationRootLayout context={context} forcedTheme={CustomizationThemeMode.Light}>
|
||||||
customization={
|
|
||||||
'customization' in context ? context.customization : defaultCustomization()
|
|
||||||
}
|
|
||||||
forcedTheme={CustomizationThemeMode.Light}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</CustomizationRootLayout>
|
</CustomizationRootLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
+71
-19
@@ -2,31 +2,32 @@
|
|||||||
|
|
||||||
import { useAIChatState } from '@/components/AI';
|
import { useAIChatState } from '@/components/AI';
|
||||||
import type { Assistant } from '@/components/AI';
|
import type { Assistant } from '@/components/AI';
|
||||||
import { ChatGPTIcon } from '@/components/AIActions/assets/ChatGPTIcon';
|
import { ChatGPTIcon } from '@/components/PageActions/assets/ChatGPTIcon';
|
||||||
import { ClaudeIcon } from '@/components/AIActions/assets/ClaudeIcon';
|
import { ClaudeIcon } from '@/components/PageActions/assets/ClaudeIcon';
|
||||||
import { MarkdownIcon } from '@/components/AIActions/assets/MarkdownIcon';
|
import { MarkdownIcon } from '@/components/PageActions/assets/MarkdownIcon';
|
||||||
import { Button } from '@/components/primitives/Button';
|
import { Button } from '@/components/primitives/Button';
|
||||||
import { DropdownMenuItem, useDropdownMenuClose } from '@/components/primitives/DropdownMenu';
|
import { DropdownMenuItem, useDropdownMenuClose } from '@/components/primitives/DropdownMenu';
|
||||||
import { tString, useLanguage } from '@/intl/client';
|
import { tString, useLanguage } from '@/intl/client';
|
||||||
import type { TranslationLanguage } from '@/intl/translations';
|
import type { TranslationLanguage } from '@/intl/translations';
|
||||||
|
import type { GitSyncState } from '@gitbook/api';
|
||||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||||
import assertNever from 'assert-never';
|
import assertNever from 'assert-never';
|
||||||
import QuickLRU from 'quick-lru';
|
import QuickLRU from 'quick-lru';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
type AIActionType = 'button' | 'dropdown-menu-item';
|
type PageActionType = 'button' | 'dropdown-menu-item';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens our AI Docs Assistant.
|
* Opens our AI Docs Assistant.
|
||||||
*/
|
*/
|
||||||
export function OpenAIAssistant(props: { assistant: Assistant; type: AIActionType }) {
|
export function OpenAIAssistant(props: { assistant: Assistant; type: PageActionType }) {
|
||||||
const { assistant, type } = props;
|
const { assistant, type } = props;
|
||||||
const chat = useAIChatState();
|
const chat = useAIChatState();
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIActionWrapper
|
<PageActionWrapper
|
||||||
type={type}
|
type={type}
|
||||||
icon={assistant.icon}
|
icon={assistant.icon}
|
||||||
label={assistant.label}
|
label={assistant.label}
|
||||||
@@ -90,7 +91,7 @@ const markdownCache = new QuickLRU<string, string>({ maxSize: 10 });
|
|||||||
*/
|
*/
|
||||||
export function CopyMarkdown(props: {
|
export function CopyMarkdown(props: {
|
||||||
markdownPageUrl: string;
|
markdownPageUrl: string;
|
||||||
type: AIActionType;
|
type: PageActionType;
|
||||||
isDefaultAction?: boolean;
|
isDefaultAction?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { markdownPageUrl, type, isDefaultAction } = props;
|
const { markdownPageUrl, type, isDefaultAction } = props;
|
||||||
@@ -130,7 +131,7 @@ export function CopyMarkdown(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIActionWrapper
|
<PageActionWrapper
|
||||||
type={type}
|
type={type}
|
||||||
icon={copied ? 'check' : 'copy'}
|
icon={copied ? 'check' : 'copy'}
|
||||||
label={copied ? tString(language, 'code_copied') : tString(language, 'copy_page')}
|
label={copied ? tString(language, 'code_copied') : tString(language, 'copy_page')}
|
||||||
@@ -145,12 +146,12 @@ export function CopyMarkdown(props: {
|
|||||||
/**
|
/**
|
||||||
* Redirects to the markdown version of the page.
|
* Redirects to the markdown version of the page.
|
||||||
*/
|
*/
|
||||||
export function ViewAsMarkdown(props: { markdownPageUrl: string; type: AIActionType }) {
|
export function ViewAsMarkdown(props: { markdownPageUrl: string; type: PageActionType }) {
|
||||||
const { markdownPageUrl, type } = props;
|
const { markdownPageUrl, type } = props;
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIActionWrapper
|
<PageActionWrapper
|
||||||
type={type}
|
type={type}
|
||||||
icon={<MarkdownIcon className="size-4 fill-current" />}
|
icon={<MarkdownIcon className="size-4 fill-current" />}
|
||||||
label={tString(language, 'view_page_markdown')}
|
label={tString(language, 'view_page_markdown')}
|
||||||
@@ -166,7 +167,7 @@ export function ViewAsMarkdown(props: { markdownPageUrl: string; type: AIActionT
|
|||||||
export function OpenInLLM(props: {
|
export function OpenInLLM(props: {
|
||||||
provider: 'chatgpt' | 'claude';
|
provider: 'chatgpt' | 'claude';
|
||||||
url: string;
|
url: string;
|
||||||
type: AIActionType;
|
type: PageActionType;
|
||||||
}) {
|
}) {
|
||||||
const { provider, url, type } = props;
|
const { provider, url, type } = props;
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
@@ -174,7 +175,7 @@ export function OpenInLLM(props: {
|
|||||||
const providerLabel = provider === 'chatgpt' ? 'ChatGPT' : 'Claude';
|
const providerLabel = provider === 'chatgpt' ? 'ChatGPT' : 'Claude';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AIActionWrapper
|
<PageActionWrapper
|
||||||
type={type}
|
type={type}
|
||||||
icon={
|
icon={
|
||||||
provider === 'chatgpt' ? (
|
provider === 'chatgpt' ? (
|
||||||
@@ -191,11 +192,48 @@ export function OpenInLLM(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GitEditLink(props: {
|
||||||
|
type: PageActionType;
|
||||||
|
provider: GitSyncState['installationProvider'];
|
||||||
|
url: string;
|
||||||
|
}) {
|
||||||
|
const { type, provider, url } = props;
|
||||||
|
const language = useLanguage();
|
||||||
|
|
||||||
|
const providerName =
|
||||||
|
provider === 'github' ? 'GitHub' : provider === 'gitlab' ? 'GitLab' : 'Git';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageActionWrapper
|
||||||
|
type={type}
|
||||||
|
icon={provider === 'gitlab' ? 'gitlab' : 'github'}
|
||||||
|
label={tString(language, 'edit_on_git', providerName)}
|
||||||
|
shortLabel={tString(language, 'edit')}
|
||||||
|
href={url}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewAsPDF(props: { url: string; type: PageActionType }) {
|
||||||
|
const { url, type } = props;
|
||||||
|
const language = useLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageActionWrapper
|
||||||
|
type={type}
|
||||||
|
icon="file-pdf"
|
||||||
|
label={tString(language, 'pdf_download')}
|
||||||
|
href={url}
|
||||||
|
target="_self"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps an action in a button (for the default action) or dropdown menu item.
|
* Wraps an action in a button (for the default action) or dropdown menu item.
|
||||||
*/
|
*/
|
||||||
function AIActionWrapper(props: {
|
function PageActionWrapper(props: {
|
||||||
type: AIActionType;
|
type: PageActionType;
|
||||||
icon: IconName | React.ReactNode;
|
icon: IconName | React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
@@ -205,10 +243,22 @@ function AIActionWrapper(props: {
|
|||||||
onClick?: (e: React.MouseEvent) => void;
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
description?: string;
|
description?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
target?: React.HTMLAttributeAnchorTarget;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { type, icon, label, shortLabel, onClick, href, description, disabled, loading } = props;
|
const {
|
||||||
|
type,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
shortLabel,
|
||||||
|
onClick,
|
||||||
|
href,
|
||||||
|
target = '_blank',
|
||||||
|
description,
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
} = props;
|
||||||
|
|
||||||
if (type === 'button') {
|
if (type === 'button') {
|
||||||
return (
|
return (
|
||||||
@@ -222,7 +272,7 @@ function AIActionWrapper(props: {
|
|||||||
className="bg-tint-base text-sm"
|
className="bg-tint-base text-sm"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
href={href}
|
href={href}
|
||||||
target={href ? '_blank' : undefined}
|
target={href ? target : undefined}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
>
|
>
|
||||||
{shortLabel}
|
{shortLabel}
|
||||||
@@ -234,7 +284,7 @@ function AIActionWrapper(props: {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex items-stretch gap-2.5 p-2"
|
className="flex items-stretch gap-2.5 p-2"
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target={target}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
>
|
>
|
||||||
@@ -255,9 +305,11 @@ function AIActionWrapper(props: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col gap-0.5">
|
<div className="flex flex-1 flex-col gap-0.5">
|
||||||
<span className="flex items-center gap-2 text-tint-strong">
|
<span className="flex items-center gap-1 text-tint-strong">
|
||||||
<span className="truncate font-medium text-sm">{label}</span>
|
<span className="truncate font-medium text-sm">{label}</span>
|
||||||
{href ? <Icon icon="arrow-up-right" className="size-3 shrink-0" /> : null}
|
{href && target === '_blank' ? (
|
||||||
|
<Icon icon="arrow-up-right" className="size-3 shrink-0 text-tint-subtle" />
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
{description && <span className="text-tint text-xs">{description}</span>}
|
{description && <span className="text-tint text-xs">{description}</span>}
|
||||||
</div>
|
</div>
|
||||||
+57
-23
@@ -2,38 +2,50 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CopyMarkdown,
|
CopyMarkdown,
|
||||||
|
GitEditLink,
|
||||||
OpenAIAssistant,
|
OpenAIAssistant,
|
||||||
OpenInLLM,
|
OpenInLLM,
|
||||||
ViewAsMarkdown,
|
ViewAsMarkdown,
|
||||||
} from '@/components/AIActions/AIActions';
|
ViewAsPDF,
|
||||||
|
} from '@/components/PageActions/PageActions';
|
||||||
import { Button, ButtonGroup } from '@/components/primitives/Button';
|
import { Button, ButtonGroup } from '@/components/primitives/Button';
|
||||||
import { DropdownMenu, DropdownMenuSeparator } from '@/components/primitives/DropdownMenu';
|
import { DropdownMenu, DropdownMenuSeparator } from '@/components/primitives/DropdownMenu';
|
||||||
import { tString, useLanguage } from '@/intl/client';
|
import { tString, useLanguage } from '@/intl/client';
|
||||||
import type { SiteCustomizationSettings } from '@gitbook/api';
|
import type { GitSyncState, SiteCustomizationSettings } from '@gitbook/api';
|
||||||
import { Icon } from '@gitbook/icons';
|
import { Icon } from '@gitbook/icons';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useAI } from '../AI';
|
import { useAI } from '../AI';
|
||||||
|
|
||||||
interface AIActionsDropdownProps {
|
interface PageActionsDropdownProps {
|
||||||
markdownPageUrl: string;
|
markdownPageUrl: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
actions: SiteCustomizationSettings['pageActions'];
|
actions: SiteCustomizationSettings['pageActions'];
|
||||||
|
editOnGit?: {
|
||||||
|
provider: GitSyncState['installationProvider'];
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
pdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dropdown menu for the AI Actions (Ask Docs Assistant, Copy page, View as Markdown, Open in LLM).
|
* Dropdown menu for the AI Actions (Ask Docs Assistant, Copy page, View as Markdown, Open in LLM).
|
||||||
*/
|
*/
|
||||||
export function AIActionsDropdown(props: AIActionsDropdownProps) {
|
export function PageActionsDropdown(props: PageActionsDropdownProps) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const assistants = useAI().assistants.filter(
|
const assistants = useAI().assistants.filter(
|
||||||
(assistant) => assistant.ui === true && assistant.pageAction
|
(assistant) => assistant.ui === true && assistant.pageAction
|
||||||
);
|
);
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
|
||||||
return assistants.length > 0 || props.actions.markdown || props.actions.externalAI ? (
|
const defaultActions = [assistants.length > 0, props.actions.markdown, props.editOnGit].filter(
|
||||||
|
Boolean
|
||||||
|
);
|
||||||
|
const dropdownActions = [props.actions.externalAI, props.pdfUrl].filter(Boolean);
|
||||||
|
|
||||||
|
return [...defaultActions, ...dropdownActions].length > 0 ? (
|
||||||
<ButtonGroup ref={ref} className={props.className}>
|
<ButtonGroup ref={ref} className={props.className}>
|
||||||
<DefaultAction {...props} />
|
{defaultActions.length > 0 ? <DefaultAction {...props} /> : null}
|
||||||
{props.actions.markdown || props.actions.externalAI ? (
|
{dropdownActions.length > 0 ? (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
align="end"
|
align="end"
|
||||||
className="!min-w-60 max-w-max"
|
className="!min-w-60 max-w-max"
|
||||||
@@ -45,15 +57,18 @@ export function AIActionsDropdown(props: AIActionsDropdownProps) {
|
|||||||
className="size-3 transition-transform group-data-[state=open]/button:rotate-180"
|
className="size-3 transition-transform group-data-[state=open]/button:rotate-180"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={tString(language, 'more')}
|
label={tString(
|
||||||
iconOnly
|
language,
|
||||||
|
defaultActions.some(Boolean) ? 'more' : 'actions'
|
||||||
|
)}
|
||||||
|
iconOnly={defaultActions.some(Boolean)}
|
||||||
size="xsmall"
|
size="xsmall"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="bg-tint-base text-sm"
|
className="bg-tint-base text-sm"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AIActionsDropdownMenuContent {...props} />
|
<PageActionsDropdownMenuContent {...props} />
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : null}
|
) : null}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
@@ -63,7 +78,7 @@ export function AIActionsDropdown(props: AIActionsDropdownProps) {
|
|||||||
/**
|
/**
|
||||||
* The content of the dropdown menu.
|
* The content of the dropdown menu.
|
||||||
*/
|
*/
|
||||||
function AIActionsDropdownMenuContent(props: AIActionsDropdownProps) {
|
function PageActionsDropdownMenuContent(props: PageActionsDropdownProps) {
|
||||||
const { markdownPageUrl, 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
|
||||||
@@ -98,6 +113,22 @@ function AIActionsDropdownMenuContent(props: AIActionsDropdownProps) {
|
|||||||
<OpenInLLM provider="claude" url={markdownPageUrl} type="dropdown-menu-item" />
|
<OpenInLLM provider="claude" url={markdownPageUrl} type="dropdown-menu-item" />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{props.editOnGit || props.pdfUrl ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator className="first:hidden" />
|
||||||
|
{props.editOnGit ? (
|
||||||
|
<GitEditLink
|
||||||
|
type="dropdown-menu-item"
|
||||||
|
provider={props.editOnGit.provider}
|
||||||
|
url={props.editOnGit.url}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{props.pdfUrl ? (
|
||||||
|
<ViewAsPDF url={props.pdfUrl} type="dropdown-menu-item" />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,31 +136,34 @@ function AIActionsDropdownMenuContent(props: AIActionsDropdownProps) {
|
|||||||
/**
|
/**
|
||||||
* 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 DefaultAction(props: AIActionsDropdownProps) {
|
function DefaultAction(props: PageActionsDropdownProps) {
|
||||||
const { markdownPageUrl, 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
|
||||||
);
|
);
|
||||||
|
|
||||||
if (assistants.length) {
|
const assistant = assistants[0];
|
||||||
return <OpenAIAssistant assistant={assistants[0]} type="button" />;
|
if (assistant) {
|
||||||
|
return <OpenAIAssistant assistant={assistant} type="button" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.editOnGit) {
|
||||||
|
return (
|
||||||
|
<GitEditLink
|
||||||
|
type="button"
|
||||||
|
provider={props.editOnGit.provider}
|
||||||
|
url={props.editOnGit.url}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actions.markdown) {
|
if (actions.markdown) {
|
||||||
return (
|
return (
|
||||||
<CopyMarkdown
|
<CopyMarkdown
|
||||||
isDefaultAction={!assistants.length}
|
isDefaultAction={!assistant}
|
||||||
markdownPageUrl={markdownPageUrl}
|
markdownPageUrl={markdownPageUrl}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actions.externalAI) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<OpenInLLM provider="chatgpt" url={markdownPageUrl} type="button" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export const AsideSectionHighlight = React.memo(
|
|||||||
'rounded-md',
|
'rounded-md',
|
||||||
'straight-corners:rounded-none',
|
'straight-corners:rounded-none',
|
||||||
'circular-corners:rounded-2xl',
|
'circular-corners:rounded-2xl',
|
||||||
'sidebar-list-line:rounded-l-none',
|
'sidebar-list-line:rounded-l-none!',
|
||||||
|
|
||||||
'sidebar-list-pill:bg-primary',
|
'sidebar-list-pill:bg-primary',
|
||||||
'[html.theme-muted.sidebar-list-pill_&]:bg-primary-hover',
|
'[html.theme-muted.sidebar-list-pill_&]:bg-primary-hover',
|
||||||
|
|||||||
@@ -4,18 +4,15 @@ import {
|
|||||||
type RevisionPageDocument,
|
type RevisionPageDocument,
|
||||||
SiteAdsStatus,
|
SiteAdsStatus,
|
||||||
SiteInsightsAdPlacement,
|
SiteInsightsAdPlacement,
|
||||||
type Space,
|
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import { Icon } from '@gitbook/icons';
|
import { Icon } from '@gitbook/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import urlJoin from 'url-join';
|
|
||||||
|
|
||||||
import { getSpaceLanguage, t } from '@/intl/server';
|
import { getSpaceLanguage, t } from '@/intl/server';
|
||||||
import { getDocumentSections } from '@/lib/document-sections';
|
import { getDocumentSections } from '@/lib/document-sections';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { Ad } from '../Ads';
|
import { Ad } from '../Ads';
|
||||||
import { getPDFURLSearchParams } from '../PDF';
|
|
||||||
import { PageFeedbackForm } from '../PageFeedback';
|
import { PageFeedbackForm } from '../PageFeedback';
|
||||||
import { ThemeToggler } from '../ThemeToggler';
|
import { ThemeToggler } from '../ThemeToggler';
|
||||||
import { ScrollSectionsList } from './ScrollSectionsList';
|
import { ScrollSectionsList } from './ScrollSectionsList';
|
||||||
@@ -118,11 +115,7 @@ export function PageAside(props: {
|
|||||||
<PageAsideSections document={document} context={context} />
|
<PageAsideSections document={document} context={context} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
<PageAsideActions
|
<PageAsideActions page={page} withPageFeedback={withPageFeedback} />
|
||||||
page={page}
|
|
||||||
withPageFeedback={withPageFeedback}
|
|
||||||
context={context}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{customization.themes.toggeable || site.ads ? (
|
{customization.themes.toggeable || site.ads ? (
|
||||||
@@ -135,7 +128,7 @@ export function PageAside(props: {
|
|||||||
|
|
||||||
function PageAsideHeader(props: { context: GitBookSiteContext }) {
|
function PageAsideHeader(props: { context: GitBookSiteContext }) {
|
||||||
const { context } = props;
|
const { context } = props;
|
||||||
const language = getSpaceLanguage(context.customization);
|
const language = getSpaceLanguage(context);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -171,34 +164,16 @@ async function PageAsideSections(props: { document: JSONDocument; context: GitBo
|
|||||||
|
|
||||||
function PageAsideActions(props: {
|
function PageAsideActions(props: {
|
||||||
withPageFeedback: boolean;
|
withPageFeedback: boolean;
|
||||||
context: GitBookSiteContext;
|
|
||||||
page: RevisionPageDocument;
|
page: RevisionPageDocument;
|
||||||
}) {
|
}) {
|
||||||
const { page, withPageFeedback, context } = props;
|
const { page, withPageFeedback } = props;
|
||||||
const { customization, space } = context;
|
|
||||||
const language = getSpaceLanguage(customization);
|
|
||||||
|
|
||||||
const pdfHref = context.linker.toPathInSpace(
|
|
||||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
|
||||||
page: page.id,
|
|
||||||
only: true,
|
|
||||||
limit: 100,
|
|
||||||
}).toString()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'flex',
|
'flex flex-col gap-3',
|
||||||
'flex-col',
|
'border-tint-subtle border-t first:border-none',
|
||||||
'gap-3',
|
'sidebar-list-default:px-3 pt-5 first:pt-0 xl:max-2xl:page-api-block:p-5',
|
||||||
'sidebar-list-default:px-3',
|
|
||||||
'border-t',
|
|
||||||
'first:border-none',
|
|
||||||
'border-tint-subtle',
|
|
||||||
'pt-5',
|
|
||||||
'first:pt-0',
|
|
||||||
'xl:max-2xl:page-api-block:p-5',
|
|
||||||
'empty:hidden'
|
'empty:hidden'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -207,57 +182,6 @@ function PageAsideActions(props: {
|
|||||||
<PageFeedbackForm pageId={page.id} />
|
<PageFeedbackForm pageId={page.id} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href={urlJoin(space.gitSync.url, page.git.path)}
|
|
||||||
className={tcls(
|
|
||||||
'flex',
|
|
||||||
'flex-row',
|
|
||||||
'items-center',
|
|
||||||
'text-sm',
|
|
||||||
'hover:text-tint-strong',
|
|
||||||
'links-accent:hover:underline',
|
|
||||||
'links-accent:hover:underline-offset-4',
|
|
||||||
'links-accent:hover:decoration-[3px]',
|
|
||||||
'links-accent:hover:decoration-primary-subtle',
|
|
||||||
'py-2'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={
|
|
||||||
space.gitSync.installationProvider === 'gitlab'
|
|
||||||
? 'gitlab'
|
|
||||||
: 'github'
|
|
||||||
}
|
|
||||||
className={tcls('size-4', 'mr-1.5')}
|
|
||||||
/>
|
|
||||||
{t(language, 'edit_on_git', getGitSyncName(space))}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{customization.pdf.enabled ? (
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href={pdfHref}
|
|
||||||
className={tcls(
|
|
||||||
'flex',
|
|
||||||
'flex-row',
|
|
||||||
'items-center',
|
|
||||||
'text-sm',
|
|
||||||
'hover:text-tint-strong',
|
|
||||||
'links-accent:hover:underline',
|
|
||||||
'links-accent:hover:underline-offset-4',
|
|
||||||
'links-accent:hover:decoration-[3px]',
|
|
||||||
'links-accent:hover:decoration-primary-subtle',
|
|
||||||
'py-2'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon="file-pdf" className={tcls('size-4', 'mr-1.5')} />
|
|
||||||
{t(language, 'pdf_download')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -296,14 +220,3 @@ async function PageAsideFooter(props: { context: GitBookSiteContext }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGitSyncName(space: Space): string {
|
|
||||||
if (space.gitSync?.installationProvider === 'github') {
|
|
||||||
return 'GitHub';
|
|
||||||
}
|
|
||||||
if (space.gitSync?.installationProvider === 'gitlab') {
|
|
||||||
return 'GitLab';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Git';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,14 +19,8 @@ const SECTION_INTERSECTING_THRESHOLD = 0.9;
|
|||||||
*/
|
*/
|
||||||
const ACTIVE_ITEM_OFFSET = 100;
|
const ACTIVE_ITEM_OFFSET = 100;
|
||||||
|
|
||||||
export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }) {
|
||||||
const { sections } = props;
|
const ids = React.useMemo(() => sections.map(({ id }) => id), [sections]);
|
||||||
|
|
||||||
const ids = React.useMemo(() => {
|
|
||||||
return sections.map((section) => {
|
|
||||||
return section.id;
|
|
||||||
});
|
|
||||||
}, [sections]);
|
|
||||||
|
|
||||||
const enabled = useBodyLoaded();
|
const enabled = useBodyLoaded();
|
||||||
|
|
||||||
@@ -101,7 +95,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
|||||||
'rounded-md',
|
'rounded-md',
|
||||||
'straight-corners:rounded-none',
|
'straight-corners:rounded-none',
|
||||||
'circular-corners:rounded-2xl',
|
'circular-corners:rounded-2xl',
|
||||||
'sidebar-list-line:rounded-l-none',
|
'sidebar-list-line:rounded-l-none!',
|
||||||
|
|
||||||
'hover:bg-tint-hover',
|
'hover:bg-tint-hover',
|
||||||
'theme-gradient:hover:bg-tint-12/1',
|
'theme-gradient:hover:bg-tint-12/1',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import type { JSONDocument, RevisionPageDocument } from '@gitbook/api';
|
import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getSpaceLanguage } from '@/intl/server';
|
import { getSpaceLanguage } from '@/intl/server';
|
||||||
@@ -26,8 +26,9 @@ export function PageBody(props: {
|
|||||||
ancestors: AncestorRevisionPage[];
|
ancestors: AncestorRevisionPage[];
|
||||||
document: JSONDocument | null;
|
document: JSONDocument | null;
|
||||||
withPageFeedback: boolean;
|
withPageFeedback: boolean;
|
||||||
|
insightsDisplayContext: SiteInsightsDisplayContext;
|
||||||
}) {
|
}) {
|
||||||
const { page, context, ancestors, document, withPageFeedback } = props;
|
const { page, context, ancestors, document, withPageFeedback, insightsDisplayContext } = props;
|
||||||
const { customization } = context;
|
const { customization } = context;
|
||||||
|
|
||||||
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
|
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
|
||||||
@@ -42,7 +43,7 @@ export function PageBody(props: {
|
|||||||
: false;
|
: false;
|
||||||
const pageWidthWide = page.layout.width === 'wide';
|
const pageWidthWide = page.layout.width === 'wide';
|
||||||
const siteWidthWide = pageWidthWide || contentFullWidth;
|
const siteWidthWide = pageWidthWide || contentFullWidth;
|
||||||
const language = getSpaceLanguage(customization);
|
const language = getSpaceLanguage(context);
|
||||||
const updatedAt = page.updatedAt ?? page.createdAt;
|
const updatedAt = page.updatedAt ?? page.createdAt;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -117,7 +118,7 @@ export function PageBody(props: {
|
|||||||
}
|
}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<TrackPageViewEvent />
|
<TrackPageViewEvent displayContext={insightsDisplayContext} />
|
||||||
</CurrentPageProvider>
|
</CurrentPageProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export async function PageFooterNavigation(props: {
|
|||||||
page: RevisionPageDocument;
|
page: RevisionPageDocument;
|
||||||
}) {
|
}) {
|
||||||
const { context, page } = props;
|
const { context, page } = props;
|
||||||
const { customization, revision, linker } = context;
|
const { revision, linker } = context;
|
||||||
const { previous, next } = resolvePrevNextPages(revision.pages, page);
|
const { previous, next } = resolvePrevNextPages(revision.pages, page);
|
||||||
const language = getSpaceLanguage(customization);
|
const language = getSpaceLanguage(context);
|
||||||
const previousHref = previous
|
const previousHref = previous
|
||||||
? linker.toPathForPage({ pages: revision.pages, page: previous })
|
? linker.toPathForPage({ pages: revision.pages, page: previous })
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { AIActionsDropdown } from '@/components/AIActions/AIActionsDropdown';
|
|
||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import type { AncestorRevisionPage } from '@/lib/pages';
|
import type { AncestorRevisionPage } from '@/lib/pages';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import type { RevisionPageDocument } from '@gitbook/api';
|
import type { RevisionPageDocument } from '@gitbook/api';
|
||||||
import { Icon } from '@gitbook/icons';
|
import { Icon } from '@gitbook/icons';
|
||||||
|
import urlJoin from 'url-join';
|
||||||
|
import { getPDFURLSearchParams } from '../PDF';
|
||||||
|
import { PageActionsDropdown } from '../PageActions/PageActionsDropdown';
|
||||||
import { PageIcon } from '../PageIcon';
|
import { PageIcon } from '../PageIcon';
|
||||||
import { StyledLink } from '../primitives';
|
import { StyledLink } from '../primitives';
|
||||||
|
|
||||||
@@ -19,8 +21,6 @@ export async function PageHeader(props: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageActions = context.customization.pageActions;
|
|
||||||
|
|
||||||
const hasAncestors = ancestors.length > 0;
|
const hasAncestors = ancestors.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -38,9 +38,30 @@ export async function PageHeader(props: {
|
|||||||
>
|
>
|
||||||
{page.layout.tableOfContents ? (
|
{page.layout.tableOfContents ? (
|
||||||
// Show page actions if *any* of the actions are enabled
|
// Show page actions if *any* of the actions are enabled
|
||||||
<AIActionsDropdown
|
<PageActionsDropdown
|
||||||
markdownPageUrl={`${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`}
|
markdownPageUrl={`${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`}
|
||||||
actions={pageActions}
|
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
|
||||||
|
}
|
||||||
|
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',
|
||||||
hasAncestors ? '-my-1.5' : '-mt-3 xs:mt-2'
|
hasAncestors ? '-my-1.5' : '-mt-3 xs:mt-2'
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ import { RootLayoutClientContexts } from './RootLayoutClientContexts';
|
|||||||
|
|
||||||
import '@gitbook/icons/style.css';
|
import '@gitbook/icons/style.css';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
import type { GitBookAnyContext } from '@/lib/context';
|
||||||
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@/lib/env';
|
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@/lib/env';
|
||||||
|
import { defaultCustomization } from '@/lib/utils';
|
||||||
import { AnnouncementDismissedScript } from '../Announcement';
|
import { AnnouncementDismissedScript } from '../Announcement';
|
||||||
|
|
||||||
function preloadFont(fontData: FontData) {
|
function preloadFont(fontData: FontData) {
|
||||||
@@ -56,12 +58,14 @@ function preloadFont(fontData: FontData) {
|
|||||||
*/
|
*/
|
||||||
export async function CustomizationRootLayout(props: {
|
export async function CustomizationRootLayout(props: {
|
||||||
forcedTheme?: CustomizationThemeMode | null;
|
forcedTheme?: CustomizationThemeMode | null;
|
||||||
customization: SiteCustomizationSettings;
|
context: GitBookAnyContext;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { customization, forcedTheme, children } = props;
|
const { context, forcedTheme, children } = props;
|
||||||
|
const customization =
|
||||||
|
'customization' in context ? context.customization : defaultCustomization();
|
||||||
|
|
||||||
const language = getSpaceLanguage(customization);
|
const language = getSpaceLanguage(context);
|
||||||
const tintColor = getTintColor(customization);
|
const tintColor = getTintColor(customization);
|
||||||
const mixColor = getTintMixColor(customization.styling.primaryColor, tintColor);
|
const mixColor = getTintMixColor(customization.styling.primaryColor, tintColor);
|
||||||
const sidebarStyles = getSidebarStyles(customization);
|
const sidebarStyles = getSidebarStyles(customization);
|
||||||
@@ -94,7 +98,9 @@ export async function CustomizationRootLayout(props: {
|
|||||||
'depth' in customization.styling && `depth-${customization.styling.depth}`,
|
'depth' in customization.styling && `depth-${customization.styling.depth}`,
|
||||||
fontNotoColorEmoji.variable,
|
fontNotoColorEmoji.variable,
|
||||||
monospaceFontData.type === 'default' ? monospaceFontData.variable : null,
|
monospaceFontData.type === 'default' ? monospaceFontData.variable : null,
|
||||||
fontData.type === 'default' ? fontData.variable : null,
|
fontData.type === 'default'
|
||||||
|
? [fontData.variable, `font-${customization.styling.font}`]
|
||||||
|
: null,
|
||||||
|
|
||||||
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
|
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
|
||||||
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
|
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
|
||||||
@@ -138,7 +144,12 @@ export async function CustomizationRootLayout(props: {
|
|||||||
customization.styling.primaryColor.light
|
customization.styling.primaryColor.light
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
--header-link: ${hexToRgb(customization.header.linkColor?.light ?? colorContrast(tintColor?.light ?? customization.styling.primaryColor.light))};
|
--header-link: ${hexToRgb(
|
||||||
|
customization.header.linkColor?.light ??
|
||||||
|
colorContrast(
|
||||||
|
tintColor?.light ?? customization.styling.primaryColor.light
|
||||||
|
)
|
||||||
|
)};
|
||||||
|
|
||||||
${generateColorVariable('info', infoColor.light)}
|
${generateColorVariable('info', infoColor.light)}
|
||||||
${generateColorVariable('warning', warningColor.light)}
|
${generateColorVariable('warning', warningColor.light)}
|
||||||
@@ -152,7 +163,12 @@ export async function CustomizationRootLayout(props: {
|
|||||||
${generateColorVariable('neutral', DEFAULT_TINT_COLOR, { darkMode: true })}
|
${generateColorVariable('neutral', DEFAULT_TINT_COLOR, { darkMode: true })}
|
||||||
|
|
||||||
--header-background: ${hexToRgb(customization.header.backgroundColor?.dark ?? tintColor?.dark ?? customization.styling.primaryColor.dark)};
|
--header-background: ${hexToRgb(customization.header.backgroundColor?.dark ?? tintColor?.dark ?? customization.styling.primaryColor.dark)};
|
||||||
--header-link: ${hexToRgb(customization.header.linkColor?.dark ?? colorContrast(tintColor?.dark ?? customization.styling.primaryColor.dark))};
|
--header-link: ${hexToRgb(
|
||||||
|
customization.header.linkColor?.dark ??
|
||||||
|
colorContrast(
|
||||||
|
tintColor?.dark ?? customization.styling.primaryColor.dark
|
||||||
|
)
|
||||||
|
)};
|
||||||
|
|
||||||
${generateColorVariable('info', infoColor.dark, { darkMode: true })}
|
${generateColorVariable('info', infoColor.dark, { darkMode: true })}
|
||||||
${generateColorVariable('warning', warningColor.dark, { darkMode: true })}
|
${generateColorVariable('warning', warningColor.dark, { darkMode: true })}
|
||||||
@@ -161,16 +177,7 @@ export async function CustomizationRootLayout(props: {
|
|||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body className="site-background">
|
||||||
className={tcls(
|
|
||||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
|
||||||
'bg-tint-base',
|
|
||||||
'theme-muted:bg-tint-subtle',
|
|
||||||
|
|
||||||
'theme-gradient:bg-gradient-primary',
|
|
||||||
'theme-gradient-tint:bg-gradient-tint'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<IconsProvider
|
<IconsProvider
|
||||||
assetsURL={GITBOOK_ICONS_URL}
|
assetsURL={GITBOOK_ICONS_URL}
|
||||||
assetsURLToken={GITBOOK_ICONS_TOKEN}
|
assetsURLToken={GITBOOK_ICONS_TOKEN}
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility hide-scroll {
|
@utility no-scrollbar {
|
||||||
/* hide scroll bar but still scroll */
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -29,6 +28,22 @@
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility hide-scrollbar {
|
||||||
|
/* Hide scrollbar by default */
|
||||||
|
scrollbar-color: transparent transparent;
|
||||||
|
transition: scrollbar-color 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
/* Show scrollbar on hover */
|
||||||
|
scrollbar-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
/* Always show scrollbar in high contrast mode */
|
||||||
|
scrollbar-color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@utility linear-mask-gradient {
|
@utility linear-mask-gradient {
|
||||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 96px, rgba(0, 0, 0, 0));
|
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 96px, rgba(0, 0, 0, 0));
|
||||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 96px, rgba(0, 0, 0, 0));
|
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 96px, rgba(0, 0, 0, 0));
|
||||||
@@ -46,7 +61,6 @@
|
|||||||
|
|
||||||
@utility gutter-stable {
|
@utility gutter-stable {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
scrollbar-gutter: stable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility triangle {
|
@utility triangle {
|
||||||
@@ -143,45 +157,13 @@
|
|||||||
@apply bg-linear-to-bl from-tint-4 to-tint-base to-60% bg-fixed;
|
@apply bg-linear-to-bl from-tint-4 to-tint-base to-60% bg-fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility scroll-nojump {
|
@utility site-background {
|
||||||
/**
|
@apply [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle;
|
||||||
* Utility to prevent horizontal jumps when the vertical scrollbar appears
|
@apply bg-tint-base;
|
||||||
* It should be used on main containers of the layout, while preserving horizontal delimiters.
|
@apply theme-muted:bg-tint-subtle;
|
||||||
* It basically adds a margin-left with the width of the scrollbar, and a width that compensates for it; when a scrollbar is displayed.
|
|
||||||
* Inspired by https://aykevl.nl/2014/09/fix-jumping-scrollbar, but adapted to work on child elements of "html"
|
|
||||||
*/
|
|
||||||
width: 100%;
|
|
||||||
@media screen and (min-width: 1536px) {
|
|
||||||
margin-left: var(--scrollbar-width);
|
|
||||||
margin-right: 0;
|
|
||||||
width: calc(100% - var(--scrollbar-width));
|
|
||||||
body:has(.page-full-width) & {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
@apply theme-gradient:bg-gradient-primary;
|
||||||
* Utility to prevent horizontal jumps when the vertical scrollbar appears
|
@apply theme-gradient-tint:bg-gradient-tint;
|
||||||
* It should be used on main containers of the layout, while preserving horizontal delimiters.
|
|
||||||
* It basically adds a margin-left with the width of the scrollbar, and a width that compensates for it; when a scrollbar is displayed.
|
|
||||||
* Inspired by https://aykevl.nl/2014/09/fix-jumping-scrollbar, but adapted to work on child elements of "html"
|
|
||||||
*/
|
|
||||||
width: 100%;
|
|
||||||
@media screen and (min-width: 1536px) {
|
|
||||||
margin-left: var(--scrollbar-width);
|
|
||||||
margin-right: 0;
|
|
||||||
width: calc(100% - var(--scrollbar-width));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility page-full-width {
|
|
||||||
@media screen and (min-width: 1536px) {
|
|
||||||
body:has(&) .scroll-nojump {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility elevate-link {
|
@utility elevate-link {
|
||||||
@@ -211,14 +193,59 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--scrollbar-width: calc(100vw - 100%);
|
|
||||||
@apply leading-relaxed;
|
@apply leading-relaxed;
|
||||||
|
interpolate-size: allow-keywords; /* Opt-in for modern browsers to interpolate "auto" values in transitions/animations. */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modern browsers with `scrollbar-*` support */
|
||||||
|
@supports (scrollbar-color: auto) {
|
||||||
|
* {
|
||||||
|
scrollbar-color: theme("colors.tint.7") transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
* {
|
||||||
|
scrollbar-color: theme("colors.tint.11") transparent;
|
||||||
|
scrollbar-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Otherwise, use `::-webkit-scrollbar-*` pseudo-elements */
|
||||||
|
@supports selector(::-webkit-scrollbar) {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
background: transparent;
|
||||||
|
max-width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: theme("colors.tint.7");
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: theme("colors.tint.8");
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: theme("colors.tint.11");
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: theme("colors.tint.12");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply text-tint-strong antialiased;
|
@apply text-tint-strong antialiased;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
@apply gutter-stable;
|
scrollbar-width: auto;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
@apply tracking-[-0.025em] text-tint-strong text-balance;
|
@apply tracking-[-0.025em] text-tint-strong text-balance;
|
||||||
@@ -249,22 +276,6 @@
|
|||||||
@apply subpixel-antialiased;
|
@apply subpixel-antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light mode */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
@apply bg-tint-subtle;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
@apply bg-tint-7;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
@apply bg-tint-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-heading-hash {
|
.flip-heading-hash {
|
||||||
@apply [h1,h2,h3,h4]:content-start;
|
@apply [h1,h2,h3,h4]:content-start;
|
||||||
@apply [h1,h2,h3,h4]:auto-cols-[auto_1fr];
|
@apply [h1,h2,h3,h4]:auto-cols-[auto_1fr];
|
||||||
@@ -357,6 +368,11 @@ html.dark.code-monochrome {
|
|||||||
--shiki-token-function: rgb(var(--primary-10));
|
--shiki-token-function: rgb(var(--primary-10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.font-Lato {
|
||||||
|
/* Lato's default ligatures impact readability, so we turn them off */
|
||||||
|
font-variant-ligatures: no-common-ligatures;
|
||||||
|
}
|
||||||
|
|
||||||
/* Code blocks */
|
/* Code blocks */
|
||||||
.highlight-line {
|
.highlight-line {
|
||||||
@apply grid grid-cols-subgrid col-span-2 relative ring-1 ring-transparent hover:ring-tint-hover hover:z-1 rounded-sm;
|
@apply grid grid-cols-subgrid col-span-2 relative ring-1 ring-transparent hover:ring-tint-hover hover:z-1 rounded-sm;
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ interface SearchContainerProps {
|
|||||||
isMultiVariants: boolean;
|
isMultiVariants: boolean;
|
||||||
style: CustomizationSearchStyle;
|
style: CustomizationSearchStyle;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
viewport?: 'desktop' | 'mobile';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client component to render the search input and results.
|
* Client component to render the search input and results.
|
||||||
*/
|
*/
|
||||||
export function SearchContainer(props: SearchContainerProps) {
|
export function SearchContainer(props: SearchContainerProps) {
|
||||||
const { siteSpaceId, spaceTitle, isMultiVariants, style, className } = props;
|
const { siteSpaceId, spaceTitle, isMultiVariants, style, className, viewport } = props;
|
||||||
|
|
||||||
const { assistants } = useAI();
|
const { assistants } = useAI();
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
|||||||
initialRef.current = true;
|
initialRef.current = true;
|
||||||
|
|
||||||
// For simplicity we're only triggering the first assistant
|
// For simplicity we're only triggering the first assistant
|
||||||
assistants[0].open(state?.ask ?? undefined);
|
assistants[0]?.open(state?.ask ?? undefined);
|
||||||
}, [state?.ask, assistants.length, assistants[0]?.open]);
|
}, [state?.ask, assistants.length, assistants[0]?.open]);
|
||||||
|
|
||||||
const onClose = React.useCallback(
|
const onClose = React.useCallback(
|
||||||
@@ -88,6 +89,19 @@ export function SearchContainer(props: SearchContainerProps) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'mod+i',
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (assistants) {
|
||||||
|
assistants[0]?.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableOnFormTags: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const onOpen = React.useCallback(() => {
|
const onOpen = React.useCallback(() => {
|
||||||
if (state?.open) {
|
if (state?.open) {
|
||||||
return;
|
return;
|
||||||
@@ -145,6 +159,8 @@ export function SearchContainer(props: SearchContainerProps) {
|
|||||||
|
|
||||||
const showAsk = withSearchAI && normalizedAsk; // withSearchAI && normalizedAsk;
|
const showAsk = withSearchAI && normalizedAsk; // withSearchAI && normalizedAsk;
|
||||||
|
|
||||||
|
const visible = viewport === 'desktop' ? !isMobile : viewport === 'mobile' ? isMobile : true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchAskProvider value={searchAsk}>
|
<SearchAskProvider value={searchAsk}>
|
||||||
<Popover
|
<Popover
|
||||||
@@ -168,7 +184,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
rootProps={{
|
rootProps={{
|
||||||
open: state?.open ?? false,
|
open: visible && (state?.open ?? false),
|
||||||
onOpenChange: (open) => {
|
onOpenChange: (open) => {
|
||||||
open ? onOpen() : onClose();
|
open ? onOpen() : onClose();
|
||||||
},
|
},
|
||||||
@@ -178,7 +194,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
|||||||
onOpenAutoFocus: (event) => event.preventDefault(),
|
onOpenAutoFocus: (event) => event.preventDefault(),
|
||||||
align: 'start',
|
align: 'start',
|
||||||
className:
|
className:
|
||||||
'bg-tint-base has-[.empty]:hidden scroll-py-2 w-128 p-2 max-h-[min(32rem,var(--radix-popover-content-available-height))] max-w-[min(var(--radix-popover-content-available-width),32rem)]',
|
'bg-tint-base has-[.empty]:hidden gutter-stable scroll-py-2 w-128 p-2 pr-1 max-h-[min(32rem,var(--radix-popover-content-available-height))] max-w-[min(var(--radix-popover-content-available-width),32rem)]',
|
||||||
onInteractOutside: (event) => {
|
onInteractOutside: (event) => {
|
||||||
// Don't close if clicking on the search input itself
|
// Don't close if clicking on the search input itself
|
||||||
if (searchInputRef.current?.contains(event.target as Node)) {
|
if (searchInputRef.current?.contains(event.target as Node)) {
|
||||||
@@ -211,10 +227,11 @@ export function SearchContainer(props: SearchContainerProps) {
|
|||||||
</Popover>
|
</Popover>
|
||||||
{assistants
|
{assistants
|
||||||
.filter((assistant) => assistant.ui === true)
|
.filter((assistant) => assistant.ui === true)
|
||||||
.map((assistant) => (
|
.map((assistant, index) => (
|
||||||
<AIChatButton
|
<AIChatButton
|
||||||
key={assistant.id}
|
key={assistant.id}
|
||||||
assistant={assistant}
|
assistant={assistant}
|
||||||
|
withShortcut={index === 0}
|
||||||
showLabel={
|
showLabel={
|
||||||
assistants.filter((assistant) => assistant.ui === true).length === 1 &&
|
assistants.filter((assistant) => assistant.ui === true).length === 1 &&
|
||||||
style === CustomizationSearchStyle.Prominent
|
style === CustomizationSearchStyle.Prominent
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
|||||||
}, [isOpen, value]);
|
}, [isOpen, value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex size-9 grow">
|
<div className={tcls('relative flex size-9 grow', className)}>
|
||||||
{/* biome-ignore lint/a11y/useKeyWithClickEvents: this div needs an onClick to show the input on mobile, where it's normally hidden.
|
{/* biome-ignore lint/a11y/useKeyWithClickEvents: this div needs an onClick to show the input on mobile, where it's normally hidden.
|
||||||
Normally you'd also need to add a keyboard trigger to do the same without a pointer, but in this case the input already be focused on its own. */}
|
Normally you'd also need to add a keyboard trigger to do the same without a pointer, but in this case the input already be focused on its own. */}
|
||||||
<div
|
<div
|
||||||
@@ -62,8 +62,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
|||||||
'theme-bold:border-header-link/3 has-[input:focus-visible]:theme-bold:border-header-link/5 has-[input:focus-visible]:theme-bold:bg-header-link/3 has-[input:focus-visible]:theme-bold:ring-header-link/5',
|
'theme-bold:border-header-link/3 has-[input:focus-visible]:theme-bold:border-header-link/5 has-[input:focus-visible]:theme-bold:bg-header-link/3 has-[input:focus-visible]:theme-bold:ring-header-link/5',
|
||||||
'theme-bold:before:absolute theme-bold:before:inset-0 theme-bold:before:bg-header-background/7 theme-bold:before:backdrop-blur-xl ', // Special overlay to make the transparent colors of theme-bold visible.
|
'theme-bold:before:absolute theme-bold:before:inset-0 theme-bold:before:bg-header-background/7 theme-bold:before:backdrop-blur-xl ', // Special overlay to make the transparent colors of theme-bold visible.
|
||||||
'relative z-30 shrink grow justify-start max-md:absolute max-md:right-0',
|
'relative z-30 shrink grow justify-start max-md:absolute max-md:right-0',
|
||||||
isOpen ? 'max-md:w-56' : 'max-md:w-[38px]',
|
isOpen ? 'max-md:w-56' : 'max-md:w-[38px]'
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{value && isOpen ? (
|
{value && isOpen ? (
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
question={item.question}
|
question={item.question}
|
||||||
active={index === cursor}
|
active={index === cursor}
|
||||||
assistant={assistants[0]}
|
assistant={assistants[0]!}
|
||||||
recommended
|
recommended
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,8 +71,7 @@ export interface AskAnswerResult {
|
|||||||
export async function searchAllSiteContent(query: string): Promise<OrderedComputedResult[]> {
|
export async function searchAllSiteContent(query: string): Promise<OrderedComputedResult[]> {
|
||||||
return traceErrorOnly('Search.searchAllSiteContent', async () => {
|
return traceErrorOnly('Search.searchAllSiteContent', async () => {
|
||||||
const context = await getServerActionBaseContext();
|
const context = await getServerActionBaseContext();
|
||||||
|
return searchSiteContent(context, {
|
||||||
return await searchSiteContent(context, {
|
|
||||||
query,
|
query,
|
||||||
scope: { mode: 'all' },
|
scope: { mode: 'all' },
|
||||||
});
|
});
|
||||||
@@ -124,8 +123,7 @@ export async function streamAskQuestion({
|
|||||||
},
|
},
|
||||||
scope: {
|
scope: {
|
||||||
mode: 'default',
|
mode: 'default',
|
||||||
// Include the current site space regardless.
|
currentSiteSpace: context.siteSpace.id,
|
||||||
includedSiteSpaces: [context.siteSpace.id],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ format: 'document' }
|
{ format: 'document' }
|
||||||
@@ -270,7 +268,7 @@ async function searchSiteContent(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
searchResults.map(async (spaceItem) => {
|
searchResults.map((spaceItem) => {
|
||||||
const found = findSiteSpaceBy(
|
const found = findSiteSpaceBy(
|
||||||
structure,
|
structure,
|
||||||
(siteSpace) => siteSpace.space.id === spaceItem.id
|
(siteSpace) => siteSpace.space.id === spaceItem.id
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { buildVersion } from '@/lib/build';
|
|||||||
import { GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL, GITBOOK_ICONS_URL } from '@/lib/env';
|
import { GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL, GITBOOK_ICONS_URL } from '@/lib/env';
|
||||||
import { getResizedImageURL } from '@/lib/images';
|
import { getResizedImageURL } from '@/lib/images';
|
||||||
import { isSiteIndexable } from '@/lib/seo';
|
import { isSiteIndexable } from '@/lib/seo';
|
||||||
import { AIChatProvider, AIContextProvider } from '../AI';
|
import { AIContextProvider } from '../AI';
|
||||||
import { RocketLoaderDetector } from './RocketLoaderDetector';
|
import { RocketLoaderDetector } from './RocketLoaderDetector';
|
||||||
import { SiteLayoutClientContexts } from './SiteLayoutClientContexts';
|
import { SiteLayoutClientContexts } from './SiteLayoutClientContexts';
|
||||||
|
|
||||||
@@ -58,15 +58,13 @@ export async function SiteLayout(props: {
|
|||||||
aiMode={customization.ai?.mode}
|
aiMode={customization.ai?.mode}
|
||||||
trademark={customization.trademark.enabled}
|
trademark={customization.trademark.enabled}
|
||||||
>
|
>
|
||||||
<AIChatProvider>
|
<SpaceLayout
|
||||||
<SpaceLayout
|
context={context}
|
||||||
context={context}
|
withTracking={withTracking}
|
||||||
withTracking={withTracking}
|
visitorAuthClaims={visitorAuthClaims}
|
||||||
visitorAuthClaims={visitorAuthClaims}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</SpaceLayout>
|
||||||
</SpaceLayout>
|
|
||||||
</AIChatProvider>
|
|
||||||
</AIContextProvider>
|
</AIContextProvider>
|
||||||
|
|
||||||
{scripts.length > 0 ? (
|
{scripts.length > 0 ? (
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
import { getPageDocument } from '@/lib/data';
|
import { getPageDocument } from '@/lib/data';
|
||||||
import { CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api';
|
import {
|
||||||
|
CustomizationHeaderPreset,
|
||||||
|
CustomizationThemeMode,
|
||||||
|
SiteInsightsDisplayContext,
|
||||||
|
} from '@gitbook/api';
|
||||||
import type { Metadata, Viewport } from 'next';
|
import type { Metadata, Viewport } from 'next';
|
||||||
import { notFound, redirect } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -65,6 +69,7 @@ export async function SitePage(props: SitePageProps) {
|
|||||||
ancestors={ancestors}
|
ancestors={ancestors}
|
||||||
document={document}
|
document={document}
|
||||||
withPageFeedback={withPageFeedback}
|
withPageFeedback={withPageFeedback}
|
||||||
|
insightsDisplayContext={SiteInsightsDisplayContext.Site}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { TrackPageViewEvent } from '@/components/Insights';
|
import { TrackPageViewEvent } from '@/components/Insights';
|
||||||
import { t, useLanguage } from '@/intl/client';
|
import { t, useLanguage } from '@/intl/client';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSpaceBasePath } from '../SpaceLayout/SpaceLayoutContext';
|
import { useSpaceBasePath } from '../SpaceLayout/SpaceLayoutContext';
|
||||||
@@ -46,7 +47,7 @@ export function SitePageNotFound() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Track the page not found as a page view */}
|
{/* Track the page not found as a page view */}
|
||||||
<TrackPageViewEvent />
|
<TrackPageViewEvent displayContext={SiteInsightsDisplayContext.Site} />
|
||||||
</div>
|
</div>
|
||||||
</CurrentPageProvider>
|
</CurrentPageProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
|
|||||||
>
|
>
|
||||||
<TOCScrollContainer
|
<TOCScrollContainer
|
||||||
style={{ maxHeight: `${MAX_ITEMS * 3 + 2}rem` }}
|
style={{ maxHeight: `${MAX_ITEMS * 3 + 2}rem` }}
|
||||||
className="gutter-stable overflow-y-auto px-2 pb-6"
|
className="overflow-y-auto px-2 pb-4"
|
||||||
>
|
>
|
||||||
{sectionsAndGroups.map((item) => {
|
{sectionsAndGroups.map((item) => {
|
||||||
if (item.object === 'site-section-group') {
|
if (item.object === 'site-section-group') {
|
||||||
@@ -110,7 +110,7 @@ export function SiteSectionListItem(props: {
|
|||||||
className={tcls(
|
className={tcls(
|
||||||
'flex size-8 shrink-0 items-center justify-center rounded-md straight-corners:rounded-none bg-tint-subtle text-lg text-tint leading-none shadow-tint shadow-xs ring-1 ring-tint-subtle transition-transform group-hover/section-link:scale-110 group-hover/section-link:ring-tint-hover group-active/section-link:scale-90 group-active/section-link:shadow-none contrast-more:text-tint-strong dark:shadow-none',
|
'flex size-8 shrink-0 items-center justify-center rounded-md straight-corners:rounded-none bg-tint-subtle text-lg text-tint leading-none shadow-tint shadow-xs ring-1 ring-tint-subtle transition-transform group-hover/section-link:scale-110 group-hover/section-link:ring-tint-hover group-active/section-link:scale-90 group-active/section-link:shadow-none contrast-more:text-tint-strong dark:shadow-none',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-primary tint:bg-primary-solid text-primary-subtle tint:text-contrast-primary-solid shadow-md shadow-primary ring-primary group-hover/section-link:ring-primary-hover, contrast-more:text-primary contrast-more:ring-2 contrast-more:ring-primary'
|
? 'bg-primary tint:bg-primary-original text-primary-subtle tint:text-contrast-primary-original shadow-md shadow-primary ring-primary group-hover/section-link:ring-primary-hover, contrast-more:text-primary contrast-more:ring-2 contrast-more:ring-primary'
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
|
|||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
switch (item.object) {
|
switch (item.object) {
|
||||||
case 'site-section-group': {
|
case 'site-section-group': {
|
||||||
const sections = item.sections
|
const sections = item.sections.map((section) => encodeSection(context, section));
|
||||||
.filter((section) => shouldIncludeSection(context, section))
|
|
||||||
.map((section) => encodeSection(context, section));
|
|
||||||
|
|
||||||
// Skip empty groups
|
// Skip empty groups
|
||||||
if (sections.length === 0) {
|
if (sections.length === 0) {
|
||||||
@@ -74,38 +72,13 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if a section should be included in the list of sections.
|
|
||||||
*/
|
|
||||||
function shouldIncludeSection(context: GitBookSiteContext, section: SiteSection) {
|
|
||||||
if (context.site.id !== 'site_JOVzv') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Testing for a new mode of navigation where the multi-variants section are hidden
|
|
||||||
// if they do not include an equivalent of the current site space.
|
|
||||||
|
|
||||||
// TODO: replace with a proper flag on the section
|
|
||||||
const withNavigateOnlyIfEquivalent = section.id === 'sitesc_4jvEm';
|
|
||||||
|
|
||||||
if (!withNavigateOnlyIfEquivalent) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { siteSpace: currentSiteSpace } = context;
|
|
||||||
if (section.siteSpaces.length === 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return section.siteSpaces.some((siteSpace) =>
|
|
||||||
areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the best default site space to navigate to for a givent section:
|
* Find the best default site space to navigate to for a givent section:
|
||||||
* 1. If we are on the default, continue on the default.
|
* 1. If we are on the default, continue on the default.
|
||||||
* 2. If a site space has the same path as the current one, return it.
|
* 2. If there are site spaces with the same language as the current, filter by language.
|
||||||
* 3. Otherwise, return the default one.
|
* 3. If a site space has the same path as the current one, return it.
|
||||||
|
* 4. Otherwise, return the default first language match.
|
||||||
|
* 5. Otherwise, return the default one.
|
||||||
*/
|
*/
|
||||||
function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
||||||
const { siteSpace: currentSiteSpace } = context;
|
const { siteSpace: currentSiteSpace } = context;
|
||||||
@@ -114,9 +87,15 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
|||||||
return getSectionURL(context, section);
|
return getSectionURL(context, section);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bestMatch = section.siteSpaces.find((siteSpace) =>
|
const possibleMatches =
|
||||||
areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
|
section.siteSpaces.filter((siteSpace) =>
|
||||||
);
|
areSiteSpacesSameLanguage(siteSpace, currentSiteSpace)
|
||||||
|
) ?? section.siteSpaces;
|
||||||
|
|
||||||
|
const bestMatch =
|
||||||
|
possibleMatches.find((siteSpace) => areSiteSpacesEquivalent(siteSpace, currentSiteSpace)) ??
|
||||||
|
possibleMatches[0];
|
||||||
|
|
||||||
if (bestMatch) {
|
if (bestMatch) {
|
||||||
return getSiteSpaceURL(context, bestMatch);
|
return getSiteSpaceURL(context, bestMatch);
|
||||||
}
|
}
|
||||||
@@ -130,3 +109,7 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
|||||||
function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
|
function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
|
||||||
return siteSpace1.path === siteSpace2.path;
|
return siteSpace1.path === siteSpace2.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function areSiteSpacesSameLanguage(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
|
||||||
|
return siteSpace1.space.language === siteSpace2.space.language;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
CustomizationHeaderPreset,
|
CustomizationHeaderPreset,
|
||||||
CustomizationSearchStyle,
|
CustomizationSearchStyle,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { Footer } from '@/components/Footer';
|
import { Footer } from '@/components/Footer';
|
||||||
import { Header, HeaderLogo } from '@/components/Header';
|
import { Header, HeaderLogo } from '@/components/Header';
|
||||||
@@ -12,21 +12,22 @@ import { TableOfContents } from '@/components/TableOfContents';
|
|||||||
import { CONTAINER_STYLE } from '@/components/layout';
|
import { CONTAINER_STYLE } from '@/components/layout';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { getSpaceLanguage } from '@/intl/server';
|
||||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||||
import { GITBOOK_APP_URL } from '@/lib/env';
|
import { GITBOOK_APP_URL } from '@/lib/env';
|
||||||
|
import { AIChatProvider } from '../AI';
|
||||||
|
import type { RenderAIMessageOptions } from '../AI';
|
||||||
import { AIChat } from '../AIChat';
|
import { AIChat } from '../AIChat';
|
||||||
|
import { AdaptiveVisitorContextProvider } from '../Adaptive';
|
||||||
import { Announcement } from '../Announcement';
|
import { Announcement } from '../Announcement';
|
||||||
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
|
||||||
import { InsightsProvider } from '../Insights';
|
import { InsightsProvider } from '../Insights';
|
||||||
import { SearchContainer } from '../Search';
|
import { SearchContainer } from '../Search';
|
||||||
import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
|
import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
|
||||||
import { CurrentContentProvider } from '../hooks';
|
import { CurrentContentProvider } from '../hooks';
|
||||||
import { SpaceLayoutContextProvider } from './SpaceLayoutContext';
|
import { SpaceLayoutContextProvider } from './SpaceLayoutContext';
|
||||||
|
|
||||||
/**
|
type SpaceLayoutProps = {
|
||||||
* Render the entire layout of the space (header, table of contents, footer).
|
|
||||||
*/
|
|
||||||
export function SpaceLayout(props: {
|
|
||||||
context: GitBookSiteContext;
|
context: GitBookSiteContext;
|
||||||
|
|
||||||
/** Whether to enable tracking of events into site insights. */
|
/** Whether to enable tracking of events into site insights. */
|
||||||
@@ -35,39 +36,21 @@ export function SpaceLayout(props: {
|
|||||||
/** The visitor auth claims. */
|
/** The visitor auth claims. */
|
||||||
visitorAuthClaims: VisitorAuthClaims;
|
visitorAuthClaims: VisitorAuthClaims;
|
||||||
|
|
||||||
|
/** The options for rendering AI messages. */
|
||||||
|
aiChatRenderMessageOptions?: RenderAIMessageOptions;
|
||||||
|
|
||||||
/** The children of the layout. */
|
/** The children of the layout. */
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
};
|
||||||
const { context, withTracking, visitorAuthClaims, children } = props;
|
|
||||||
const { siteSpace, customization, sections, siteSpaces } = context;
|
|
||||||
|
|
||||||
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
|
/**
|
||||||
|
* Provide all contexts for a space.
|
||||||
|
*/
|
||||||
|
export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
|
||||||
|
const { context, withTracking, visitorAuthClaims, aiChatRenderMessageOptions, children } =
|
||||||
|
props;
|
||||||
|
|
||||||
const withSections = Boolean(sections && sections.list.length > 1);
|
const { customization } = context;
|
||||||
const isMultiVariants = Boolean(siteSpaces.length > 1);
|
|
||||||
|
|
||||||
const withFooter =
|
|
||||||
customization.themes.toggeable ||
|
|
||||||
customization.footer.copyright ||
|
|
||||||
customization.footer.logo ||
|
|
||||||
customization.footer.groups?.length;
|
|
||||||
|
|
||||||
const search = (
|
|
||||||
<div className="flex grow items-center gap-2">
|
|
||||||
<React.Suspense fallback={null}>
|
|
||||||
<SearchContainer
|
|
||||||
style={
|
|
||||||
customization.header.preset === CustomizationHeaderPreset.None
|
|
||||||
? CustomizationSearchStyle.Subtle
|
|
||||||
: customization.styling.search
|
|
||||||
}
|
|
||||||
isMultiVariants={siteSpaces.length > 1}
|
|
||||||
spaceTitle={siteSpace.title}
|
|
||||||
siteSpaceId={siteSpace.id}
|
|
||||||
/>
|
|
||||||
</React.Suspense>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const eventUrl = new URL(
|
const eventUrl = new URL(
|
||||||
context.linker.toAbsoluteURL(context.linker.toPathInSite('/~gitbook/__evt'))
|
context.linker.toAbsoluteURL(context.linker.toPathInSite('/~gitbook/__evt'))
|
||||||
@@ -75,99 +58,158 @@ export function SpaceLayout(props: {
|
|||||||
eventUrl.searchParams.set('o', context.organizationId);
|
eventUrl.searchParams.set('o', context.organizationId);
|
||||||
eventUrl.searchParams.set('s', context.site.id);
|
eventUrl.searchParams.set('s', context.site.id);
|
||||||
|
|
||||||
|
const getVisitorClaimsUrl = context.linker.toAbsoluteURL(
|
||||||
|
context.linker.toPathInSite('/~gitbook/visitor')
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SpaceLayoutContextProvider basePath={context.linker.toPathInSpace('')}>
|
<SpaceLayoutContextProvider basePath={context.linker.toPathInSpace('')}>
|
||||||
<CurrentContentProvider
|
<AdaptiveVisitorContextProvider
|
||||||
organizationId={context.organizationId}
|
contextId={context.contextId}
|
||||||
siteId={context.site.id}
|
visitorClaimsURL={getVisitorClaimsUrl}
|
||||||
siteSectionId={context.sections?.current?.id ?? null}
|
|
||||||
siteSpaceId={context.siteSpace.id}
|
|
||||||
siteShareKey={context.shareKey ?? null}
|
|
||||||
spaceId={context.space.id}
|
|
||||||
revisionId={context.revisionId}
|
|
||||||
visitorAuthClaims={visitorAuthClaims}
|
|
||||||
>
|
>
|
||||||
<InsightsProvider
|
<CurrentContentProvider
|
||||||
enabled={withTracking}
|
organizationId={context.organizationId}
|
||||||
appURL={GITBOOK_APP_URL}
|
siteId={context.site.id}
|
||||||
eventUrl={eventUrl.toString()}
|
siteSectionId={context.sections?.current?.id ?? null}
|
||||||
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
|
siteSpaceId={context.siteSpace.id}
|
||||||
|
siteShareKey={context.shareKey ?? null}
|
||||||
|
spaceId={context.space.id}
|
||||||
|
revisionId={context.revisionId}
|
||||||
|
visitorAuthClaims={visitorAuthClaims}
|
||||||
>
|
>
|
||||||
<Announcement context={context} />
|
<InsightsProvider
|
||||||
<Header withTopHeader={withTopHeader} context={context} search={search} />
|
enabled={withTracking}
|
||||||
{customization.ai?.mode === CustomizationAIMode.Assistant ? (
|
appURL={GITBOOK_APP_URL}
|
||||||
<AIChat trademark={customization.trademark.enabled} />
|
eventUrl={eventUrl.toString()}
|
||||||
) : null}
|
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
|
||||||
<div className="scroll-nojump">
|
>
|
||||||
<div className="motion-safe:transition-all motion-safe:duration-300 lg:chat-open:mr-80 xl:chat-open:mr-96">
|
<AIChatProvider renderMessageOptions={aiChatRenderMessageOptions}>
|
||||||
<div
|
{children}
|
||||||
className={tcls(
|
</AIChatProvider>
|
||||||
'flex',
|
</InsightsProvider>
|
||||||
'flex-col',
|
</CurrentContentProvider>
|
||||||
'lg:flex-row',
|
</AdaptiveVisitorContextProvider>
|
||||||
CONTAINER_STYLE,
|
|
||||||
'site-width-wide:max-w-full',
|
|
||||||
|
|
||||||
// Ensure the footer is display below the viewport even if the content is not enough
|
|
||||||
withFooter && 'min-h-[calc(100vh-64px)]',
|
|
||||||
withTopHeader ? null : 'lg:min-h-screen'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TableOfContents
|
|
||||||
context={context}
|
|
||||||
header={
|
|
||||||
withTopHeader ? null : (
|
|
||||||
<div
|
|
||||||
className={tcls(
|
|
||||||
'hidden',
|
|
||||||
'pr-4',
|
|
||||||
'md:flex',
|
|
||||||
'grow-0',
|
|
||||||
'flex-wrap',
|
|
||||||
'dark:shadow-light/1',
|
|
||||||
'text-base/tight'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<HeaderLogo context={context} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
innerHeader={
|
|
||||||
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
|
|
||||||
<>
|
|
||||||
{!withTopHeader && search}
|
|
||||||
{!withTopHeader && withSections && sections && (
|
|
||||||
<SiteSectionList
|
|
||||||
className={tcls('hidden', 'lg:block')}
|
|
||||||
sections={encodeClientSiteSections(
|
|
||||||
context,
|
|
||||||
sections
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isMultiVariants && !sections && (
|
|
||||||
<SpacesDropdown
|
|
||||||
context={context}
|
|
||||||
siteSpace={siteSpace}
|
|
||||||
siteSpaces={siteSpaces}
|
|
||||||
className={tcls(
|
|
||||||
'w-full',
|
|
||||||
'page-no-toc:hidden',
|
|
||||||
'page-no-toc:site-header-none:flex'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{withFooter ? <Footer context={context} /> : null}
|
|
||||||
</InsightsProvider>
|
|
||||||
</CurrentContentProvider>
|
|
||||||
</SpaceLayoutContextProvider>
|
</SpaceLayoutContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the entire layout of the space (header, table of contents, footer).
|
||||||
|
*/
|
||||||
|
export function SpaceLayout(props: SpaceLayoutProps) {
|
||||||
|
const { context, children } = props;
|
||||||
|
const { siteSpace, customization, sections, siteSpaces } = context;
|
||||||
|
|
||||||
|
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
|
||||||
|
|
||||||
|
const withSections = Boolean(sections && sections.list.length > 1);
|
||||||
|
|
||||||
|
const currentLanguage = getSpaceLanguage(context);
|
||||||
|
const withVariants: 'generic' | 'translations' | undefined =
|
||||||
|
siteSpaces.length > 1
|
||||||
|
? siteSpaces.some(
|
||||||
|
(space) => space.space.language && space.space.language !== currentLanguage.locale
|
||||||
|
)
|
||||||
|
? 'translations'
|
||||||
|
: 'generic'
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const withFooter =
|
||||||
|
customization.themes.toggeable ||
|
||||||
|
customization.footer.copyright ||
|
||||||
|
customization.footer.logo ||
|
||||||
|
customization.footer.groups?.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpaceLayoutServerContext {...props}>
|
||||||
|
<Announcement context={context} />
|
||||||
|
<Header withTopHeader={withTopHeader} withVariants={withVariants} context={context} />
|
||||||
|
{customization.ai?.mode === CustomizationAIMode.Assistant ? (
|
||||||
|
<AIChat trademark={customization.trademark.enabled} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="motion-safe:transition-all motion-safe:duration-300 lg:chat-open:mr-80 xl:chat-open:mr-96">
|
||||||
|
<div
|
||||||
|
className={tcls(
|
||||||
|
'flex',
|
||||||
|
'flex-col',
|
||||||
|
'lg:flex-row',
|
||||||
|
CONTAINER_STYLE,
|
||||||
|
'site-width-wide:max-w-full',
|
||||||
|
|
||||||
|
// Ensure the footer is display below the viewport even if the content is not enough
|
||||||
|
withFooter && [
|
||||||
|
'site-header:min-h-[calc(100vh-64px)]',
|
||||||
|
'site-header-sections:min-h-[calc(100vh-108px)]',
|
||||||
|
],
|
||||||
|
withTopHeader ? null : 'lg:min-h-screen'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TableOfContents
|
||||||
|
context={context}
|
||||||
|
header={
|
||||||
|
withTopHeader ? null : (
|
||||||
|
<div
|
||||||
|
className={tcls(
|
||||||
|
'hidden',
|
||||||
|
'pr-4',
|
||||||
|
'lg:flex',
|
||||||
|
'grow-0',
|
||||||
|
'dark:shadow-light/1',
|
||||||
|
'text-base/tight',
|
||||||
|
'items-center'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<HeaderLogo context={context} />
|
||||||
|
{withVariants === 'translations' ? (
|
||||||
|
<TranslationsDropdown
|
||||||
|
context={context}
|
||||||
|
siteSpace={siteSpace}
|
||||||
|
siteSpaces={siteSpaces}
|
||||||
|
className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerHeader={
|
||||||
|
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
|
||||||
|
<>
|
||||||
|
{!withTopHeader && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<SearchContainer
|
||||||
|
style={CustomizationSearchStyle.Subtle}
|
||||||
|
isMultiVariants={siteSpaces.length > 1}
|
||||||
|
spaceTitle={siteSpace.title}
|
||||||
|
siteSpaceId={siteSpace.id}
|
||||||
|
className="max-lg:hidden"
|
||||||
|
viewport="desktop"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!withTopHeader && withSections && sections && (
|
||||||
|
<SiteSectionList
|
||||||
|
className={tcls('hidden', 'lg:block')}
|
||||||
|
sections={encodeClientSiteSections(context, sections)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{withVariants === 'generic' && (
|
||||||
|
<SpacesDropdown
|
||||||
|
context={context}
|
||||||
|
siteSpace={siteSpace}
|
||||||
|
siteSpaces={siteSpaces}
|
||||||
|
className="w-full px-3 py-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{withFooter ? <Footer context={context} /> : null}
|
||||||
|
</SpaceLayoutServerContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { PagesList } from './PagesList';
|
import { PagesList } from './PagesList';
|
||||||
import { TOCScrollContainer } from './TOCScroller';
|
import { TOCScrollContainer } from './TOCScroller';
|
||||||
import { TableOfContentsScript } from './TableOfContentsScript';
|
import { TableOfContentsScript } from './TableOfContentsScript';
|
||||||
@@ -16,7 +15,7 @@ export async function TableOfContents(props: {
|
|||||||
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
|
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
|
||||||
}) {
|
}) {
|
||||||
const { innerHeader, context, header } = props;
|
const { innerHeader, context, header } = props;
|
||||||
const { space, customization, revision } = context;
|
const { customization, revision } = context;
|
||||||
|
|
||||||
const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages);
|
const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages);
|
||||||
|
|
||||||
@@ -57,6 +56,8 @@ export async function TableOfContents(props: {
|
|||||||
// Client-side dynamic positioning (CSS vars applied by script)
|
// Client-side dynamic positioning (CSS vars applied by script)
|
||||||
'lg:[html[style*="--toc-top-offset"]_&]:top-(--toc-top-offset)!',
|
'lg:[html[style*="--toc-top-offset"]_&]:top-(--toc-top-offset)!',
|
||||||
'lg:[html[style*="--toc-height"]_&]:h-(--toc-height)!',
|
'lg:[html[style*="--toc-height"]_&]:h-(--toc-height)!',
|
||||||
|
'lg:page-no-toc:[html[style*="--outline-top-offset"]_&]:top-(--outline-top-offset)!',
|
||||||
|
'lg:page-no-toc:[html[style*="--outline-height"]_&]:top-(--outline-height)!',
|
||||||
|
|
||||||
'pt-6',
|
'pt-6',
|
||||||
'pb-4',
|
'pb-4',
|
||||||
@@ -96,17 +97,16 @@ export async function TableOfContents(props: {
|
|||||||
'page-has-toc:[html.sidebar-filled.circular-corners_&]:rounded-3xl'
|
'page-has-toc:[html.sidebar-filled.circular-corners_&]:rounded-3xl'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>}
|
{innerHeader ? (
|
||||||
|
<div className="my-4 flex flex-col space-y-4 px-5 empty:hidden">
|
||||||
|
{innerHeader}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<TOCScrollContainer // The scrollview inside the sidebar
|
<TOCScrollContainer // The scrollview inside the sidebar
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'flex grow flex-col p-2',
|
'flex grow flex-col p-2',
|
||||||
customization.trademark.enabled && 'lg:pb-20',
|
customization.trademark.enabled && 'lg:pb-20',
|
||||||
'lg:gutter-stable overflow-y-auto',
|
'hide-scrollbar overflow-y-auto'
|
||||||
'[&::-webkit-scrollbar]:bg-transparent',
|
|
||||||
'[&::-webkit-scrollbar-thumb]:bg-transparent',
|
|
||||||
'[&::-webkit-scrollbar]:group-hover:bg-tint-subtle',
|
|
||||||
'[&::-webkit-scrollbar-thumb]:group-hover:bg-tint-7',
|
|
||||||
'[&::-webkit-scrollbar-thumb:hover]:group-hover:bg-tint-8'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PagesList
|
<PagesList
|
||||||
@@ -115,8 +115,7 @@ export async function TableOfContents(props: {
|
|||||||
/>
|
/>
|
||||||
{customization.trademark.enabled ? (
|
{customization.trademark.enabled ? (
|
||||||
<Trademark
|
<Trademark
|
||||||
space={space}
|
context={context}
|
||||||
customization={customization}
|
|
||||||
placement={SiteInsightsTrademarkPlacement.Sidebar}
|
placement={SiteInsightsTrademarkPlacement.Sidebar}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user