mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 14:00:28 +00:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7375d3c597 | |||
| 3548fa6dff | |||
| cb73040e0f | |||
| 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": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.136.0",
|
||||
"@gitbook/api": "^0.140.0",
|
||||
"bidc": "^0.0.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"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.
|
||||
*/
|
||||
@@ -182,7 +171,7 @@ export function getComputedContentSourceCacheTags(
|
||||
break;
|
||||
default:
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
@@ -198,9 +187,8 @@ export function getComputedContentSourceCacheTags(
|
||||
}
|
||||
|
||||
// We invalidate the computed content when a new version of the integration is deployed.
|
||||
|
||||
if (source.type.startsWith('integration:')) {
|
||||
const integration = source.type.split(':')[1];
|
||||
const integration = source.type.split(':')[1]!;
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 17dd382: Add `original` background color step
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 193d591: Fix return type for `colorContrast`
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.3",
|
||||
"version": "0.4.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ type RGBColor = [number, number, number];
|
||||
type OKLABColor = { L: number; A: number; B: 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 {
|
||||
backgrounds = 'backgrounds',
|
||||
@@ -19,7 +19,7 @@ export enum ColorCategory {
|
||||
}
|
||||
|
||||
type ColorSubScale = {
|
||||
[key: string]: number;
|
||||
[key: string]: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -56,6 +56,8 @@ export const scale: Record<ColorCategory, ColorSubScale> = {
|
||||
solid: 9,
|
||||
/** Hovered solid backgrounds */
|
||||
'solid-hover': 10,
|
||||
/** Original color */
|
||||
original: 'original',
|
||||
},
|
||||
[ColorCategory.text]: {
|
||||
/** Very low-contrast text
|
||||
@@ -211,8 +213,8 @@ export function colorScale(
|
||||
const result = [];
|
||||
|
||||
for (let index = 0; index < mapping.length; index++) {
|
||||
const targetL =
|
||||
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
|
||||
const step = mapping[index]!;
|
||||
const targetL = foregroundColor.L * step + backgroundColor.L * (1 - step);
|
||||
|
||||
if (
|
||||
index === 8 &&
|
||||
@@ -295,7 +297,7 @@ export function rgbArrayToHex(rgb: RGBColor): string {
|
||||
|
||||
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
|
||||
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);
|
||||
@@ -392,14 +394,14 @@ export function xyzToLab65(xyz: [number, number, number]): {
|
||||
B: number;
|
||||
} {
|
||||
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 {
|
||||
L: 116 * y - 16,
|
||||
A: 500 * (x - y),
|
||||
B: 200 * (y - z),
|
||||
L: 116 * y! - 16,
|
||||
A: 500 * (x! - y!),
|
||||
B: 200 * (y! - z!),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -419,7 +421,10 @@ export function dpsContrast(a: RGBColor, b: RGBColor) {
|
||||
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 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,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/embed
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6f368b5: Fix embed assistant window width on small screens
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"react": "./dist/react/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/icons": "workspace:",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
--gitbook-widget-top: 1rem;
|
||||
--gitbook-widget-bottom: 1rem;
|
||||
--gitbook-widget-right: 1rem;
|
||||
--gitbook-widget-left: 1rem;
|
||||
|
||||
--gitbook-widget-button-height: 46px;
|
||||
|
||||
@@ -117,7 +118,7 @@
|
||||
bottom: var(--gitbook-widget-window-bottom);
|
||||
right: var(--gitbook-widget-right);
|
||||
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))));
|
||||
background-color: var(--gitbook-widget-background-solid);
|
||||
border: 1px solid var(--gitbook-widget-border-color);
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @gitbook/expr
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3548fa6: Fix eval-estree-expr named import
|
||||
|
||||
## 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
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/expr",
|
||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -12,23 +12,26 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"eval-estree-expression": "^2.0.3",
|
||||
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-loose": "8.4.0",
|
||||
"acorn-walk": "^8.3.4",
|
||||
"escodegen": "^2.1.0",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.1.20",
|
||||
"tsdown": "^0.15.0",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@babel/types": "^7.26.0",
|
||||
"@types/json-schema": "^7.0.15"
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/escodegen": "^0.0.10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build": "tsdown --project tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"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.at',
|
||||
'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.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,11 +42,29 @@ describe('ExpressionRuntime', () => {
|
||||
},
|
||||
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',
|
||||
({ condition, inputs, expectedResult }) => {
|
||||
expect(runtime.evaluate(condition, inputs)).toBe(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toBe(expectedResult);
|
||||
expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './errors';
|
||||
export * from './input-values';
|
||||
export * from './input-values';
|
||||
export * from './runtime';
|
||||
export * from './symbols';
|
||||
export * from './template';
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
tokenizer,
|
||||
} from 'acorn';
|
||||
import { parse as parseLoose } from 'acorn-loose';
|
||||
import { evaluate } from 'eval-estree-expression';
|
||||
import escodegen from 'escodegen';
|
||||
import evalESTreeExpr from 'eval-estree-expression';
|
||||
const { evaluate } = evalESTreeExpr;
|
||||
|
||||
import { AutoComplete } from './autocomplete';
|
||||
import { ExpressionError } from './errors';
|
||||
@@ -49,6 +51,7 @@ export class ExpressionRuntime {
|
||||
return evaluate.sync<Expression>(parsed.result, inputs, {
|
||||
functions: true,
|
||||
withMembers: true,
|
||||
generate: escodegen.generate,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error instanceof Error
|
||||
|
||||
@@ -338,6 +338,8 @@ describe('ExpressionRuntime', () => {
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'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).',
|
||||
}),
|
||||
}),
|
||||
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
|
||||
*/
|
||||
generate?: boolean;
|
||||
generate?: boolean | ((node: any) => string);
|
||||
/**
|
||||
* 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 [startHex, endHex] = body.split('-');
|
||||
const start = Number.parseInt(startHex, 16);
|
||||
const start = Number.parseInt(startHex!, 16);
|
||||
const end = endHex ? Number.parseInt(endHex, 16) : start;
|
||||
|
||||
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)) {
|
||||
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
|
||||
hits[label]++;
|
||||
hits[label]!++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
# gitbook
|
||||
|
||||
## 0.17.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @gitbook/react-openapi@1.4.2
|
||||
|
||||
## 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
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
"GITBOOK_URL": "http://localhost:8771"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ const args = process.argv.slice(2);
|
||||
const versionId = args[0];
|
||||
|
||||
// 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(
|
||||
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
@@ -14,7 +14,7 @@
|
||||
"@gitbook/react-contentkit": "workspace:*",
|
||||
"@gitbook/react-math": "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-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
@@ -27,6 +27,7 @@
|
||||
"@tusbar/cache-control": "^1.0.2",
|
||||
"ai": "^4.2.2",
|
||||
"assert-never": "^1.2.1",
|
||||
"bidc": "catalog:",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
"direction": "^2.0.1",
|
||||
@@ -45,7 +46,7 @@
|
||||
"memoizee": "^0.4.17",
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"next": "^15.5.0",
|
||||
"next": "15.4.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -70,8 +71,7 @@
|
||||
"url-join": "^5.0.0",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"warn-once": "^0.1.1",
|
||||
"zustand": "^5.0.3",
|
||||
"bidc": "catalog:"
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"dev": "env-cmd --silent -f ../../.env.local next",
|
||||
"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",
|
||||
"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",
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ export default async function SiteDynamicLayout({
|
||||
const withTracking = shouldTrackEvents(await headers());
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout forcedTheme={forcedTheme} customization={context.customization}>
|
||||
<CustomizationRootLayout forcedTheme={forcedTheme} context={context}>
|
||||
<SiteLayout
|
||||
context={context}
|
||||
forcedTheme={forcedTheme}
|
||||
|
||||
+13
-2
@@ -5,6 +5,8 @@ import {
|
||||
generateEmbeddableViewport,
|
||||
} from '@/components/Embeddable';
|
||||
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||
import { shouldTrackEvents } from '@/lib/tracking';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
interface SiteStaticLayoutProps {
|
||||
params: Promise<RouteLayoutParams>;
|
||||
@@ -14,9 +16,18 @@ export default async function RootLayout({
|
||||
params,
|
||||
children,
|
||||
}: 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) {
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export default async function SiteStaticLayout({
|
||||
const withTracking = shouldTrackEvents();
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout customization={context.customization}>
|
||||
<CustomizationRootLayout context={context}>
|
||||
<SiteLayout
|
||||
context={context}
|
||||
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,
|
||||
} from '@/components/Embeddable';
|
||||
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||
import { shouldTrackEvents } from '@/lib/tracking';
|
||||
|
||||
interface SiteStaticLayoutProps {
|
||||
params: Promise<RouteLayoutParams>;
|
||||
@@ -14,9 +15,18 @@ export default async function RootLayout({
|
||||
params,
|
||||
children,
|
||||
}: 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) {
|
||||
|
||||
+4
-4
@@ -1,5 +1,6 @@
|
||||
import type { RouteLayoutParams } from '@/app/utils';
|
||||
import { getAssetURL } from '@/lib/assets';
|
||||
import { buildVersion } from '@/lib/build';
|
||||
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||
import type { CreateGitBookOptions } from '@gitbook/embed';
|
||||
import type { NextRequest } from 'next/server';
|
||||
@@ -14,7 +15,6 @@ export async function GET(
|
||||
{ params }: { params: Promise<RouteLayoutParams> }
|
||||
) {
|
||||
const { context } = await getEmbeddableStaticContext(await params);
|
||||
const maxAge = 7 * 24 * 60 * 60;
|
||||
const initOptions: CreateGitBookOptions = {
|
||||
siteURL: context.linker.toAbsoluteURL(context.linker.toPathInSite('')),
|
||||
};
|
||||
@@ -45,13 +45,13 @@ export async function GET(
|
||||
const load = function () {
|
||||
const style = document.createElement('link');
|
||||
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);
|
||||
|
||||
const script = d.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
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];
|
||||
latestScript.parentNode.insertBefore(script, latestScript);
|
||||
@@ -70,7 +70,7 @@ export async function GET(
|
||||
{
|
||||
headers: {
|
||||
'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 './useAIChat';
|
||||
export type { RenderAIMessageOptions } from './server-actions';
|
||||
|
||||
@@ -72,7 +72,7 @@ function DescriptionForPageContentToolCall(props: {
|
||||
}) {
|
||||
const { toolCall, context } = props;
|
||||
|
||||
const language = getSpaceLanguage(context.customization);
|
||||
const language = getSpaceLanguage(context);
|
||||
|
||||
return (
|
||||
<p>
|
||||
@@ -102,7 +102,7 @@ function DescriptionForMCPToolCall(props: {
|
||||
}) {
|
||||
const { toolCall, context } = props;
|
||||
|
||||
const language = getSpaceLanguage(context.customization);
|
||||
const language = getSpaceLanguage(context);
|
||||
|
||||
return (
|
||||
<p>
|
||||
@@ -130,7 +130,7 @@ async function DescriptionForSearchToolCall(props: {
|
||||
}) {
|
||||
const { toolCall, context } = props;
|
||||
|
||||
const language = getSpaceLanguage(context.customization);
|
||||
const language = getSpaceLanguage(context);
|
||||
|
||||
// Resolve all hrefs for search results in parallel
|
||||
const searchResultsWithHrefs = await Promise.all(
|
||||
@@ -187,7 +187,7 @@ async function DescriptionForSearchToolCall(props: {
|
||||
) : null}
|
||||
</summary>
|
||||
{hasResults ? (
|
||||
<div className="mt-1 max-h-0 overflow-y-auto circular-corners:rounded-2xl rounded-corners:rounded-lg border border-tint-subtle p-2 opacity-0 transition-all 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">
|
||||
{searchResultsWithHrefs.map((result, index) => (
|
||||
<li
|
||||
@@ -241,7 +241,7 @@ function DescriptionForGetPagesToolCall(props: {
|
||||
}) {
|
||||
const { toolCall, context } = props;
|
||||
|
||||
const language = getSpaceLanguage(context.customization);
|
||||
const language = getSpaceLanguage(context);
|
||||
|
||||
return (
|
||||
<p>
|
||||
|
||||
@@ -39,7 +39,9 @@ export async function streamRenderAIMessage(
|
||||
|
||||
if (message.steps[stepIndex]) {
|
||||
message.steps = [...message.steps];
|
||||
// @ts-expect-error
|
||||
message.steps[stepIndex] = { ...message.steps[stepIndex] };
|
||||
// @ts-expect-error
|
||||
callback(message.steps[stepIndex]);
|
||||
} else {
|
||||
message.steps = [
|
||||
@@ -52,6 +54,7 @@ export async function streamRenderAIMessage(
|
||||
},
|
||||
},
|
||||
];
|
||||
// @ts-expect-error
|
||||
callback(message.steps[stepIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
EmbeddableFrameSubtitle,
|
||||
EmbeddableFrameTitle,
|
||||
} from '../Embeddable/EmbeddableFrame';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { useNow } from '../hooks';
|
||||
import { Button } from '../primitives';
|
||||
import { AIChatControlButton } from './AIChatControlButton';
|
||||
@@ -46,17 +47,6 @@ export function AIChat(props: { trademark: boolean }) {
|
||||
}
|
||||
}, [chat.opened]);
|
||||
|
||||
useHotkeys(
|
||||
'mod+i',
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
chatController.open();
|
||||
},
|
||||
{
|
||||
enableOnFormTags: true,
|
||||
}
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'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) {
|
||||
return null;
|
||||
}
|
||||
@@ -224,7 +224,7 @@ export function AIChatBody(props: {
|
||||
<>
|
||||
<div
|
||||
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={{
|
||||
paddingBottom: `${inputHeight}px`,
|
||||
}}
|
||||
|
||||
@@ -12,8 +12,9 @@ import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
|
||||
export function AIChatButton(props: {
|
||||
assistant: Assistant;
|
||||
showLabel?: boolean;
|
||||
withShortcut?: boolean;
|
||||
}) {
|
||||
const { assistant, showLabel = true } = props;
|
||||
const { assistant, showLabel = true, withShortcut = true } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
return (
|
||||
@@ -27,7 +28,12 @@ export function AIChatButton(props: {
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
{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>
|
||||
}
|
||||
onClick={() => assistant.open()}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useLanguage } from '@/intl/client';
|
||||
import { tString } from '@/intl/translate';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { AIMessageRole } from '@gitbook/api';
|
||||
import type React from 'react';
|
||||
@@ -46,17 +48,9 @@ export function AIChatMessages(props: {
|
||||
{message.content ? message.content : null}
|
||||
|
||||
{isLastMessage && chat.loading ? (
|
||||
<div className="flex w-full animate-fade-in-slow 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 className="flex w-full animate-fade-in-slow flex-col gap-2">
|
||||
{!message.content ? <HoldMessage /> : null}
|
||||
<LoadingSkeleton />
|
||||
</div>
|
||||
) : 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 { Icon } from '@gitbook/icons';
|
||||
import { headers } from 'next/headers';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { DateRelative } from '../primitives';
|
||||
import { IframeWrapper } from './IframeWrapper';
|
||||
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
||||
import { Toolbar, ToolbarBody, ToolbarButton, ToolbarButtonGroups } from './Toolbar';
|
||||
|
||||
@@ -47,19 +46,21 @@ function ToolbarLayout(props: { children: React.ReactNode }) {
|
||||
*/
|
||||
export async function AdminToolbar(props: AdminToolbarProps) {
|
||||
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) {
|
||||
return <ChangeRequestToolbar context={context} />;
|
||||
return (
|
||||
<IframeWrapper>
|
||||
<ChangeRequestToolbar context={context} />
|
||||
</IframeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (context.revisionId !== context.space.revision) {
|
||||
return <RevisionToolbar context={context} />;
|
||||
return (
|
||||
<IframeWrapper>
|
||||
<RevisionToolbar context={context} />
|
||||
</IframeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
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 './IframeWrapper';
|
||||
|
||||
@@ -34,69 +34,67 @@ export function AnnouncementBanner(props: {
|
||||
className="theme-bold:bg-header-background pt-4 pb-2"
|
||||
data-nosnippet=""
|
||||
>
|
||||
<div className="scroll-nojump">
|
||||
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
<div className={tcls('relative', CONTAINER_STYLE)}>
|
||||
<Tag
|
||||
href={contentRef?.href ?? ''}
|
||||
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',
|
||||
style.container,
|
||||
closeable && 'pr-12',
|
||||
hasLink && style.hover
|
||||
)}
|
||||
insights={
|
||||
announcement.link
|
||||
? {
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: announcement.link.to,
|
||||
position: SiteInsightsLinkPosition.Announcement,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon={style.icon as IconName}
|
||||
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
|
||||
/>
|
||||
<div>
|
||||
{announcement.message}
|
||||
{hasLink ? (
|
||||
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
|
||||
{contentRef?.icon ? (
|
||||
<span className="mr-1 ml-2 *:inline">
|
||||
{contentRef?.icon}
|
||||
</span>
|
||||
) : null}
|
||||
{announcement.link?.title && (
|
||||
<span className="mr-1">{announcement.link?.title}</span>
|
||||
)}
|
||||
<Icon
|
||||
icon={
|
||||
announcement.link?.to.kind === 'url'
|
||||
? 'arrow-up-right'
|
||||
: 'chevron-right'
|
||||
}
|
||||
className={tcls('mb-0.5 inline size-3')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Tag>
|
||||
{closeable ? (
|
||||
<Button
|
||||
iconOnly
|
||||
icon="close"
|
||||
label={tString(language, 'close')}
|
||||
variant="blank"
|
||||
size="default"
|
||||
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}`}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
<div className={tcls('relative', CONTAINER_STYLE)}>
|
||||
<Tag
|
||||
href={contentRef?.href ?? ''}
|
||||
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',
|
||||
style.container,
|
||||
closeable && 'pr-12',
|
||||
hasLink && style.hover
|
||||
)}
|
||||
insights={
|
||||
announcement.link
|
||||
? {
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: announcement.link.to,
|
||||
position: SiteInsightsLinkPosition.Announcement,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon={style.icon as IconName}
|
||||
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
|
||||
/>
|
||||
<div>
|
||||
{announcement.message}
|
||||
{hasLink ? (
|
||||
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
|
||||
{contentRef?.icon ? (
|
||||
<span className="mr-1 ml-2 *:inline">
|
||||
{contentRef?.icon}
|
||||
</span>
|
||||
) : null}
|
||||
{announcement.link?.title && (
|
||||
<span className="mr-1">{announcement.link?.title}</span>
|
||||
)}
|
||||
<Icon
|
||||
icon={
|
||||
announcement.link?.to.kind === 'url'
|
||||
? 'arrow-up-right'
|
||||
: 'chevron-right'
|
||||
}
|
||||
className={tcls('mb-0.5 inline size-3')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Tag>
|
||||
{closeable ? (
|
||||
<Button
|
||||
iconOnly
|
||||
icon="close"
|
||||
label={tString(language, 'close')}
|
||||
variant="blank"
|
||||
size="default"
|
||||
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}`}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ export async function highlight(
|
||||
currentIndex += 1; // for the \n
|
||||
|
||||
return {
|
||||
highlighted: Boolean(lineBlock.data.highlighted),
|
||||
highlighted: Boolean(lineBlock?.data.highlighted),
|
||||
tokens: result,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import type { DocumentBlockColumns, Length } from '@gitbook/api';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { type DocumentBlockColumns, type Length, VerticalAlignment } from '@gitbook/api';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
|
||||
@@ -8,10 +8,12 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
||||
return (
|
||||
<div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}>
|
||||
{block.nodes.map((columnBlock) => {
|
||||
const width = columnBlock.data.width;
|
||||
const { className, style } = transformLengthToCSS(width);
|
||||
return (
|
||||
<Column key={columnBlock.key} className={className} style={style}>
|
||||
<Column
|
||||
key={columnBlock.key}
|
||||
width={columnBlock.data.width}
|
||||
verticalAlignment={columnBlock.data.verticalAlignment}
|
||||
>
|
||||
<Blocks
|
||||
key={columnBlock.key}
|
||||
nodes={columnBlock.nodes}
|
||||
@@ -29,11 +31,23 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
|
||||
|
||||
export function Column(props: {
|
||||
children?: React.ReactNode;
|
||||
className?: ClassValue;
|
||||
style?: React.CSSProperties;
|
||||
width?: Length;
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
}) {
|
||||
const { width, verticalAlignment } = props;
|
||||
const { className, style } = transformLengthToCSS(width);
|
||||
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}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,13 +7,16 @@ import { Block, type BlockProps } from './Block';
|
||||
import { Blocks } from './Blocks';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
|
||||
export function Hint(props: BlockProps<DocumentBlockHint>) {
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
export function Hint({
|
||||
block,
|
||||
style,
|
||||
ancestorBlocks,
|
||||
...contextProps
|
||||
}: BlockProps<DocumentBlockHint>) {
|
||||
const hintStyle = HINT_STYLES[block.data.style] ?? HINT_STYLES.info;
|
||||
const firstLine = getBlockTextStyle(block.nodes[0]);
|
||||
|
||||
const firstNode = block.nodes[0];
|
||||
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(block.nodes[0].type);
|
||||
const firstNode = block.nodes[0]!;
|
||||
const firstLine = getBlockTextStyle(firstNode);
|
||||
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(firstNode.type);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -20,10 +20,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
: null;
|
||||
const { contentContext } = context;
|
||||
|
||||
const language =
|
||||
contentContext && 'customization' in contentContext
|
||||
? getSpaceLanguage(contentContext.customization)
|
||||
: languages.en;
|
||||
const language = contentContext ? getSpaceLanguage(contentContext) : languages.en;
|
||||
|
||||
if (!contentContext || !resolved) {
|
||||
return (
|
||||
@@ -49,6 +46,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isMailto = resolved.href.startsWith('mailto:');
|
||||
const content = (
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
@@ -66,7 +64,12 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
{isMailto ? (
|
||||
<Icon
|
||||
icon="envelope"
|
||||
className="ml-1 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
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;
|
||||
|
||||
let breadcrumbs = resolved.ancestors ?? [];
|
||||
const isMailto = resolved.href.startsWith('mailto:');
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
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 = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_external_link'),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
} else if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_page_anchor'),
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -19,9 +19,9 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
||||
|
||||
const parent = ancestorBlocks[ancestorBlocks.length - 1];
|
||||
assert(
|
||||
(parent && parent.type === 'list-ordered') ||
|
||||
parent.type === 'list-unordered' ||
|
||||
parent.type === 'list-tasks',
|
||||
parent?.type === 'list-ordered' ||
|
||||
parent?.type === 'list-unordered' ||
|
||||
parent?.type === 'list-tasks',
|
||||
'Invalid parent list type'
|
||||
);
|
||||
|
||||
@@ -112,11 +112,11 @@ function getListItemDepth(input: {
|
||||
|
||||
for (let i = ancestorBlocks.length - 1; i >= 0; i--) {
|
||||
const block = ancestorBlocks[i];
|
||||
if (block.type === type) {
|
||||
if (block?.type === type) {
|
||||
depth = depth + 1;
|
||||
continue;
|
||||
}
|
||||
if (block.type === 'list-item') {
|
||||
if (block?.type === 'list-item') {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Heading } from '../Heading';
|
||||
|
||||
import './scalar.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 {
|
||||
AnyOpenAPIOperationsBlock,
|
||||
@@ -30,10 +30,7 @@ export function getOpenAPIContext(args: {
|
||||
const { props, specUrl, context } = args;
|
||||
const { block } = props;
|
||||
|
||||
const customization = context && 'customization' in context ? context.customization : null;
|
||||
const customizationLocale = customization
|
||||
? getCustomizationLocale(customization)
|
||||
: DEFAULT_LOCALE;
|
||||
const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE;
|
||||
const locale = checkIsValidLocale(customizationLocale) ? customizationLocale : DEFAULT_LOCALE;
|
||||
|
||||
return {
|
||||
|
||||
@@ -272,9 +272,8 @@ body {
|
||||
}
|
||||
.scalar-activate-button {
|
||||
@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 ring-1 ring-tint hover:ring-tint-hover;
|
||||
@apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
|
||||
@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 shadow-sm shadow-primary dark:shadow-primary-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 hover:scale-105 active:scale-100 transition-all;
|
||||
@apply grow-0 shrink-0 truncate;
|
||||
|
||||
@@ -565,7 +565,7 @@
|
||||
}
|
||||
|
||||
.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 */
|
||||
@@ -746,6 +746,10 @@ body:has(.openapi-select-popover) {
|
||||
@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 {
|
||||
@apply rotate-90;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
|
||||
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);
|
||||
|
||||
@@ -36,8 +36,8 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
|
||||
<div className="relative select-none">
|
||||
<div
|
||||
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',
|
||||
'font-medium text-contrast-primary-solid theme-muted:text-primary'
|
||||
'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-original theme-muted:text-primary contrast-more:text-contrast-primary-solid'
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Image } from '@/components/utils';
|
||||
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import {
|
||||
CardsImageObjectFit,
|
||||
type ContentRef,
|
||||
type DocumentTableViewCards,
|
||||
SiteInsightsLinkPosition,
|
||||
@@ -25,8 +26,12 @@ export async function RecordCard(
|
||||
: null;
|
||||
|
||||
const [lightCover, darkCover, target] = await Promise.all([
|
||||
light && context.contentContext ? resolveContentRef(light, context.contentContext) : null,
|
||||
dark && context.contentContext ? resolveContentRef(dark, context.contentContext) : null,
|
||||
light.contentRef && context.contentContext
|
||||
? resolveContentRef(light.contentRef, context.contentContext)
|
||||
: null,
|
||||
dark.contentRef && context.contentContext
|
||||
? resolveContentRef(dark.contentRef, context.contentContext)
|
||||
: null,
|
||||
targetRef && context.contentContext
|
||||
? resolveContentRef(targetRef, context.contentContext)
|
||||
: null,
|
||||
@@ -35,6 +40,10 @@ export async function RecordCard(
|
||||
const darkCoverIsSquareOrPortrait = isSquareOrPortrait(darkCover);
|
||||
const lightCoverIsSquareOrPortrait = isSquareOrPortrait(lightCover);
|
||||
|
||||
const darkObjectFit = `dark:${getObjectFitClass(dark.objectFit)}`;
|
||||
const lightObjectFit = getObjectFitClass(light.objectFit);
|
||||
const objectFits = `${lightObjectFit} ${darkObjectFit}`;
|
||||
|
||||
const body = (
|
||||
<div
|
||||
className={tcls(
|
||||
@@ -94,7 +103,7 @@ export async function RecordCard(
|
||||
'min-w-0',
|
||||
'w-full',
|
||||
'h-full',
|
||||
'object-cover',
|
||||
'bg-tint-subtle',
|
||||
lightCoverIsSquareOrPortrait || darkCoverIsSquareOrPortrait
|
||||
? [
|
||||
lightCoverIsSquareOrPortrait
|
||||
@@ -104,7 +113,8 @@ export async function RecordCard(
|
||||
? 'dark:min-[432px]:aspect-video dark:min-[432px]:h-auto'
|
||||
: '',
|
||||
].filter(Boolean)
|
||||
: ['h-auto', 'aspect-video']
|
||||
: ['h-auto', 'aspect-video'],
|
||||
objectFits
|
||||
)}
|
||||
priority={isOffscreen ? 'lazy' : 'high'}
|
||||
preload
|
||||
@@ -190,3 +200,23 @@ function isSquareOrPortrait(contentRef: ResolvedContentRef | null) {
|
||||
|
||||
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 { tcls } from '@/lib/tailwind';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { FileIcon } from '../FileIcon';
|
||||
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> = {
|
||||
'text-left': '**:text-left text-left',
|
||||
@@ -58,18 +63,28 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
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) {
|
||||
case 'checkbox':
|
||||
case 'checkbox': {
|
||||
if (value === null || typeof value !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Checkbox
|
||||
className={tcls('w-5', 'h-5')}
|
||||
checked={value as boolean}
|
||||
checked={value}
|
||||
disabled={true}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'rating': {
|
||||
const rating = value as number;
|
||||
if (typeof value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
const rating = value;
|
||||
const max = definition.max;
|
||||
|
||||
return (
|
||||
@@ -108,15 +123,21 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
case 'number':
|
||||
case 'number': {
|
||||
if (typeof value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tag
|
||||
className={tcls('text-base', 'tabular-nums', 'tracking-tighter')}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
>{`${value}`}</Tag>
|
||||
);
|
||||
}
|
||||
case 'text': {
|
||||
// @ts-ignore
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const fragment = getNodeFragmentByName(block, value);
|
||||
if (!fragment) {
|
||||
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
|
||||
@@ -149,8 +170,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'files': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const files = await Promise.all(
|
||||
(value as string[]).map((fileId) =>
|
||||
value.map((fileId) =>
|
||||
context.contentContext
|
||||
? resolveContentRef(
|
||||
{
|
||||
@@ -221,10 +245,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'content-ref': {
|
||||
const contentRef = value ? (value as ContentRef) : null;
|
||||
if (value === null || !isContentRef(value)) {
|
||||
return null;
|
||||
}
|
||||
const resolved =
|
||||
contentRef && context.contentContext
|
||||
? await resolveContentRef(contentRef, context.contentContext, {
|
||||
value && context.contentContext
|
||||
? await resolveContentRef(value, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
iconStyle: ['mr-2', 'text-tint-subtle'],
|
||||
})
|
||||
@@ -239,11 +265,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={
|
||||
contentRef
|
||||
value
|
||||
? {
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: contentRef,
|
||||
target: value,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}
|
||||
@@ -257,8 +283,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'users': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const resolved = await Promise.all(
|
||||
(value as string[]).map(async (userId) => {
|
||||
value.map(async (userId) => {
|
||||
const contentRef: ContentRefUser = {
|
||||
kind: 'user',
|
||||
user: userId,
|
||||
@@ -295,10 +324,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'select': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tag aria-labelledby={ariaLabelledBy}>
|
||||
<span className={tcls('inline-flex', 'gap-2', 'flex-wrap')}>
|
||||
{(value as string[]).map((selectId) => {
|
||||
{value.map((selectId) => {
|
||||
const option = definition.options.find(
|
||||
(option) => option.value === selectId
|
||||
);
|
||||
@@ -329,8 +361,15 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'image': {
|
||||
if (!isDocumentTableImageRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const image = context.contentContext
|
||||
? await resolveContentRef(value as ContentRef, context.contentContext)
|
||||
? await resolveContentRef(
|
||||
'ref' in value ? value.ref : value,
|
||||
context.contentContext
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!image) {
|
||||
|
||||
@@ -26,6 +26,7 @@ export function RecordRow(
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
});
|
||||
// @ts-expect-error
|
||||
const verticalAlignment = getColumnVerticalAlignment(block.data.definition[column]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,9 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
||||
/* Only show the header when configured and not empty */
|
||||
const withHeader =
|
||||
!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 (
|
||||
<div className={tcls(style, styles.tableWrapper)}>
|
||||
@@ -43,28 +45,31 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
||||
)}
|
||||
>
|
||||
<div role="row" className={tcls('flex', 'w-full')}>
|
||||
{view.columns.map((column) => (
|
||||
<div
|
||||
key={column}
|
||||
role="columnheader"
|
||||
className={tcls(
|
||||
styles.columnHeader,
|
||||
getColumnAlignment(block.data.definition[column])
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}),
|
||||
minWidth: columnWidths?.[column] || '100px',
|
||||
}}
|
||||
title={block.data.definition[column].title}
|
||||
>
|
||||
{block.data.definition[column].title}
|
||||
</div>
|
||||
))}
|
||||
{view.columns.map((column) => {
|
||||
const definition = block.data.definition[column]!;
|
||||
return (
|
||||
<div
|
||||
key={column}
|
||||
role="columnheader"
|
||||
className={tcls(
|
||||
styles.columnHeader,
|
||||
getColumnAlignment(definition)
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}),
|
||||
minWidth: columnWidths?.[column] || '100px',
|
||||
}}
|
||||
title={definition.title}
|
||||
>
|
||||
{definition.title}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type {
|
||||
CardsImageObjectFit,
|
||||
ContentRef,
|
||||
ContentRefFile,
|
||||
ContentRefURL,
|
||||
DocumentTableDefinition,
|
||||
DocumentTableImageRecord,
|
||||
DocumentTableRecord,
|
||||
DocumentTableViewCards,
|
||||
} from '@gitbook/api';
|
||||
@@ -21,53 +23,64 @@ export function getRecordValue<T extends number | string | boolean | string[] |
|
||||
|
||||
/**
|
||||
* 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 dark cover is a content ref (image column type).
|
||||
*/
|
||||
export function getRecordCardCovers(
|
||||
record: DocumentTableRecord,
|
||||
view: DocumentTableViewCards
|
||||
): { [key in 'light' | 'dark']: ContentRefFile | ContentRefURL | null } {
|
||||
return {
|
||||
light: (() => {
|
||||
if (!view.coverDefinition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = getRecordValue(record, view.coverDefinition) as
|
||||
| ContentRefFile
|
||||
| ContentRefURL
|
||||
| string[];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value[0] === 'string') {
|
||||
return { kind: 'file', file: value[0] };
|
||||
}
|
||||
}
|
||||
|
||||
return value as ContentRefFile | ContentRefURL;
|
||||
})(),
|
||||
dark: (() => {
|
||||
if (!view.coverDefinitionDark) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = getRecordValue(record, view.coverDefinitionDark) as
|
||||
| ContentRefFile
|
||||
| ContentRefURL;
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
})(),
|
||||
): {
|
||||
[key in 'light' | 'dark']: {
|
||||
contentRef: ContentRefFile | ContentRefURL | null;
|
||||
objectFit?: CardsImageObjectFit;
|
||||
};
|
||||
} {
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
'truncate',
|
||||
|
||||
active.id === tab.id
|
||||
active?.id === tab.id
|
||||
? [
|
||||
'shrink-0',
|
||||
'active-tab',
|
||||
@@ -222,7 +222,7 @@ export function DynamicTabs(
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active.id === tab.id}
|
||||
aria-selected={active?.id === tab.id}
|
||||
aria-controls={getTabPanelId(tab.id)}
|
||||
id={getTabButtonId(tab.id)}
|
||||
onClick={() => {
|
||||
@@ -255,7 +255,7 @@ export function DynamicTabs(
|
||||
role="tabpanel"
|
||||
id={getTabPanelId(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]}
|
||||
</div>
|
||||
|
||||
@@ -59,15 +59,15 @@ interface MarkedLeafProps<Mark extends DocumentTextMark> {
|
||||
}
|
||||
|
||||
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>) {
|
||||
return <i className={tcls('font-italic')}>{props.children}</i>;
|
||||
return <i className="font-italic">{props.children}</i>;
|
||||
}
|
||||
|
||||
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>) {
|
||||
|
||||
@@ -43,9 +43,9 @@ export function getBlockTextStyle(block: DocumentBlock): {
|
||||
case 'list-ordered':
|
||||
case 'list-tasks':
|
||||
case 'list-unordered':
|
||||
return getBlockTextStyle(block.nodes[0]);
|
||||
return getBlockTextStyle(block.nodes[0]!);
|
||||
case 'list-item':
|
||||
return getBlockTextStyle(block.nodes[0]);
|
||||
return getBlockTextStyle(block.nodes[0]!);
|
||||
default:
|
||||
return {
|
||||
textSize: 'text-base',
|
||||
|
||||
@@ -21,7 +21,7 @@ export function isBlockOffscreen(
|
||||
|
||||
const allAncestors = [document, ...ancestorBlocks];
|
||||
for (let index = allAncestors.length - 1; index >= 0; index--) {
|
||||
const parent = allAncestors[index];
|
||||
const parent = allAncestors[index]!;
|
||||
const offset = getBlockOffset({ block: current, parent });
|
||||
|
||||
if (offset > screenHeight) {
|
||||
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
AIChatDynamicIcon,
|
||||
AIChatSubtitle,
|
||||
} from '@/components/AIChat';
|
||||
import * as api from '@gitbook/api';
|
||||
import React from 'react';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import {
|
||||
EmbeddableFrame,
|
||||
EmbeddableFrameBody,
|
||||
@@ -28,6 +31,20 @@ export function EmbeddableAIChat(props: {
|
||||
const chatController = useAIChatController();
|
||||
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 (
|
||||
<EmbeddableFrame>
|
||||
<EmbeddableFrameHeader>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type PagePathParams, getSitePageData } from '@/components/SitePage';
|
||||
|
||||
import { PageBody } from '@/components/PageBody';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
import type { Metadata } from 'next';
|
||||
import { Button } from '../primitives';
|
||||
import {
|
||||
@@ -54,6 +55,7 @@ export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
|
||||
ancestors={ancestors}
|
||||
document={document}
|
||||
withPageFeedback={withPageFeedback}
|
||||
insightsDisplayContext={SiteInsightsDisplayContext.Embed}
|
||||
/>
|
||||
</div>
|
||||
</EmbeddableFrameBody>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { AIChatProvider, AIContextProvider } from '@/components/AI';
|
||||
import { AIContextProvider } from '@/components/AI';
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import {
|
||||
SiteLayoutClientContexts,
|
||||
generateSiteLayoutMetadata,
|
||||
generateSiteLayoutViewport,
|
||||
} from '@/components/SiteLayout';
|
||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { CustomizationAIMode } from '@gitbook/api';
|
||||
import { SpaceLayoutServerContext } from '../SpaceLayout';
|
||||
import { EmbeddableIframeAPI } from './EmbeddableIframeAPI';
|
||||
|
||||
type EmbeddableRootLayoutProps = {
|
||||
context: GitBookSiteContext;
|
||||
withTracking: boolean;
|
||||
visitorAuthClaims: VisitorAuthClaims;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -18,10 +22,12 @@ type EmbeddableRootLayoutProps = {
|
||||
*/
|
||||
export async function EmbeddableRootLayout({
|
||||
context,
|
||||
withTracking,
|
||||
visitorAuthClaims,
|
||||
children,
|
||||
}: React.PropsWithChildren<EmbeddableRootLayoutProps>) {
|
||||
return (
|
||||
<CustomizationRootLayout customization={context.customization}>
|
||||
<CustomizationRootLayout context={context}>
|
||||
<SiteLayoutClientContexts
|
||||
forcedTheme={context.customization.themes.default}
|
||||
externalLinksTarget={context.customization.externalLinks.target}
|
||||
@@ -31,8 +37,11 @@ export async function EmbeddableRootLayout({
|
||||
aiMode={CustomizationAIMode.Assistant}
|
||||
trademark={context.customization.trademark.enabled}
|
||||
>
|
||||
<AIChatProvider
|
||||
renderMessageOptions={{
|
||||
<SpaceLayoutServerContext
|
||||
context={context}
|
||||
withTracking={withTracking}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
aiChatRenderMessageOptions={{
|
||||
withLinkPreviews: false,
|
||||
asEmbeddable: true,
|
||||
}}
|
||||
@@ -41,17 +50,17 @@ export async function EmbeddableRootLayout({
|
||||
<EmbeddableIframeAPI
|
||||
baseURL={context.linker.toPathInSpace('~gitbook/embed/')}
|
||||
/>
|
||||
</AIChatProvider>
|
||||
</SpaceLayoutServerContext>
|
||||
</AIContextProvider>
|
||||
</SiteLayoutClientContexts>
|
||||
</CustomizationRootLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateEmbeddableViewport({ context }: EmbeddableRootLayoutProps) {
|
||||
export async function generateEmbeddableViewport({ context }: { context: GitBookSiteContext }) {
|
||||
return generateSiteLayoutViewport(context);
|
||||
}
|
||||
|
||||
export async function generateEmbeddableMetadata({ context }: EmbeddableRootLayoutProps) {
|
||||
export async function generateEmbeddableMetadata({ context }: { context: GitBookSiteContext }) {
|
||||
return generateSiteLayoutMetadata(context);
|
||||
}
|
||||
|
||||
@@ -32,111 +32,109 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
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(
|
||||
'mx-auto grid max-w-3xl site-width-wide:max-w-screen-2xl justify-between gap-12 lg:max-w-none!',
|
||||
'grid-cols-[auto_auto]',
|
||||
'lg:grid-cols-[18rem_minmax(auto,48rem)_auto]',
|
||||
'xl:grid-cols-[18rem_minmax(auto,48rem)_14rem]',
|
||||
'lg:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_auto]',
|
||||
'xl:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_14rem]',
|
||||
'lg:page-no-toc:grid-cols-[minmax(auto,48rem)_auto]',
|
||||
'xl:page-no-toc:grid-cols-[14rem_minmax(auto,48rem)_14rem]',
|
||||
'lg:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[minmax(auto,90rem)_auto]',
|
||||
'xl:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[14rem_minmax(auto,90rem)_14rem]'
|
||||
)}
|
||||
>
|
||||
{
|
||||
// Footer Logo
|
||||
customization.footer.logo ? (
|
||||
<div className="col-start-1 row-start-1">
|
||||
<Image
|
||||
alt="Logo"
|
||||
resize={context.imageResizer}
|
||||
sources={{
|
||||
light: {
|
||||
src: customization.footer.logo.light,
|
||||
},
|
||||
dark: customization.footer.logo.dark
|
||||
? {
|
||||
src: customization.footer.logo.dark,
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
priority="lazy"
|
||||
style={[
|
||||
'w-auto',
|
||||
'max-w-40',
|
||||
'lg:max-w-64',
|
||||
'max-h-10',
|
||||
'lg:max-h-12',
|
||||
'object-contain',
|
||||
'object-left',
|
||||
'rounded-sm',
|
||||
'straight-corners:rounded-xs',
|
||||
]}
|
||||
sizes={[
|
||||
{
|
||||
width: 320,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
<div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}>
|
||||
<div
|
||||
className={tcls(
|
||||
'mx-auto grid max-w-3xl site-width-wide:max-w-screen-2xl justify-between gap-12 lg:max-w-none!',
|
||||
'grid-cols-[auto_auto]',
|
||||
'lg:grid-cols-[18rem_minmax(auto,48rem)_auto]',
|
||||
'xl:grid-cols-[18rem_minmax(auto,48rem)_14rem]',
|
||||
'lg:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_auto]',
|
||||
'xl:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_14rem]',
|
||||
'lg:page-no-toc:grid-cols-[minmax(auto,48rem)_auto]',
|
||||
'xl:page-no-toc:grid-cols-[14rem_minmax(auto,48rem)_14rem]',
|
||||
'lg:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[minmax(auto,90rem)_auto]',
|
||||
'xl:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[14rem_minmax(auto,90rem)_14rem]'
|
||||
)}
|
||||
>
|
||||
{
|
||||
// Footer Logo
|
||||
customization.footer.logo ? (
|
||||
<div className="col-start-1 row-start-1">
|
||||
<Image
|
||||
alt="Logo"
|
||||
resize={context.imageResizer}
|
||||
sources={{
|
||||
light: {
|
||||
src: customization.footer.logo.light,
|
||||
},
|
||||
dark: customization.footer.logo.dark
|
||||
? {
|
||||
src: customization.footer.logo.dark,
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
priority="lazy"
|
||||
style={[
|
||||
'w-auto',
|
||||
'max-w-40',
|
||||
'lg:max-w-64',
|
||||
'max-h-10',
|
||||
'lg:max-h-12',
|
||||
'object-contain',
|
||||
'object-left',
|
||||
'rounded-sm',
|
||||
'straight-corners:rounded-xs',
|
||||
]}
|
||||
sizes={[
|
||||
{
|
||||
width: 320,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Theme Toggle
|
||||
customization.themes.toggeable ? (
|
||||
<div className="-col-start-2 row-start-1 flex items-start justify-end xl:hidden">
|
||||
<React.Suspense fallback={null}>
|
||||
<ThemeToggler />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
// Theme Toggle
|
||||
customization.themes.toggeable ? (
|
||||
<div className="-col-start-2 row-start-1 flex items-start justify-end xl:hidden">
|
||||
<React.Suspense fallback={null}>
|
||||
<ThemeToggler />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Navigation groups (split into equal columns)
|
||||
customization.footer.groups?.length > 0 ? (
|
||||
<div
|
||||
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'
|
||||
{
|
||||
// Navigation groups (split into equal columns)
|
||||
customization.footer.groups?.length > 0 ? (
|
||||
<div
|
||||
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'
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Legal
|
||||
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">
|
||||
<p>{customization.footer.copyright}</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
// Legal
|
||||
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">
|
||||
<p>{customization.footer.copyright}</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -3,13 +3,14 @@ import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '@/components/layout';
|
||||
import { getSpaceLanguage, t } from '@/intl/server';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { SearchContainer } from '../Search';
|
||||
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
|
||||
import { HeaderLink } from './HeaderLink';
|
||||
import { HeaderLinkMore } from './HeaderLinkMore';
|
||||
import { HeaderLinks } from './HeaderLinks';
|
||||
import { HeaderLogo } from './HeaderLogo';
|
||||
import { HeaderMobileMenu } from './HeaderMobileMenu';
|
||||
import { SpacesDropdown } from './SpacesDropdown';
|
||||
import { TranslationsDropdown } from './SpacesDropdown';
|
||||
|
||||
/**
|
||||
* Render the header for the space.
|
||||
@@ -17,9 +18,9 @@ import { SpacesDropdown } from './SpacesDropdown';
|
||||
export function Header(props: {
|
||||
context: GitBookSiteContext;
|
||||
withTopHeader?: boolean;
|
||||
search?: React.ReactNode;
|
||||
withVariants?: 'generic' | 'translations';
|
||||
}) {
|
||||
const { context, withTopHeader, search } = props;
|
||||
const { context, withTopHeader, withVariants } = props;
|
||||
const { siteSpace, siteSpaces, sections, customization } = context;
|
||||
|
||||
return (
|
||||
@@ -56,135 +57,146 @@ export function Header(props: {
|
||||
'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
|
||||
className={tcls(
|
||||
'gap-4',
|
||||
'lg:gap-6',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-between',
|
||||
'w-full',
|
||||
'py-3',
|
||||
'min-h-16',
|
||||
'sm:h-16',
|
||||
CONTAINER_STYLE
|
||||
'flex max-w-full lg:basis-72',
|
||||
'min-w-0 shrink items-center justify-start gap-2 lg:gap-4'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
<HeaderMobileMenu
|
||||
className={tcls(
|
||||
'flex max-w-full lg:basis-72',
|
||||
'min-w-0 shrink items-center justify-start gap-2 lg:gap-4'
|
||||
'-ml-2',
|
||||
'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
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
<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']
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
|
||||
{sections || siteSpaces.length > 1 ? (
|
||||
<div className="scroll-nojump">
|
||||
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
{sections ? (
|
||||
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
|
||||
<div
|
||||
className={tcls(
|
||||
'w-full',
|
||||
'overflow-x-auto',
|
||||
'no-scrollbar',
|
||||
'-mb-4 pb-4', // Positive padding / negative margin allows the navigation menu indicator to show in a scroll viewƒ
|
||||
!sections ? ['hidden', 'page-no-toc:flex'] : 'flex'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'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'
|
||||
CONTAINER_STYLE,
|
||||
'grow',
|
||||
'flex',
|
||||
'items-end',
|
||||
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
CONTAINER_STYLE,
|
||||
'grow',
|
||||
'flex',
|
||||
'items-end',
|
||||
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]'
|
||||
)}
|
||||
>
|
||||
{siteSpaces.length > 1 && (
|
||||
<div
|
||||
id="variants"
|
||||
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"
|
||||
>
|
||||
<SpacesDropdown
|
||||
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>
|
||||
{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)}
|
||||
/>
|
||||
) : null}
|
||||
{withVariants === 'translations' ? (
|
||||
<div className="site-background before:contents[] -mr-4 sm:-mr-6 md:-mr-8 sticky inset-y-0 right-0 z-10 ml-6 flex h-full items-center py-2 pr-4 before:mr-4 before:h-full before:border-tint before:border-l sm:pr-6 md:pr-8">
|
||||
<TranslationsDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -138,7 +138,7 @@ function HeaderItemButton(
|
||||
|
||||
function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPreset }) {
|
||||
return tcls(
|
||||
'flex items-center',
|
||||
'flex items-center gap-1',
|
||||
'shrink',
|
||||
'contrast-more:underline',
|
||||
'truncate',
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import type { SiteSpace } from '@gitbook/api';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getSiteSpaceURL } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Button, type ButtonProps } from '../primitives';
|
||||
import { DropdownChevron, DropdownMenu } from '../primitives/DropdownMenu';
|
||||
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: {
|
||||
context: GitBookSiteContext;
|
||||
siteSpace: SiteSpace;
|
||||
siteSpaces: SiteSpace[];
|
||||
className?: string;
|
||||
variant?: ButtonProps['variant'];
|
||||
icon?: ButtonProps['icon'];
|
||||
}) {
|
||||
const { context, siteSpace, siteSpaces, className } = props;
|
||||
const { context, siteSpace, siteSpaces, className, variant = 'secondary', icon } = props;
|
||||
|
||||
return (
|
||||
<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
|
||||
)}
|
||||
button={
|
||||
<div
|
||||
<Button
|
||||
icon={icon}
|
||||
data-testid="space-dropdown-button"
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'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
|
||||
)}
|
||||
size="medium"
|
||||
variant={variant}
|
||||
trailing={<DropdownChevron />}
|
||||
className={tcls('bg-tint-base', className)}
|
||||
>
|
||||
<span className={tcls('truncate', 'grow')}>{siteSpace.title}</span>
|
||||
<DropdownChevron />
|
||||
</div>
|
||||
<span className="button-content">{siteSpace.title}</span>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<SpacesDropdownMenuItems
|
||||
@@ -77,3 +56,32 @@ export function SpacesDropdown(props: {
|
||||
</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.
|
||||
*/
|
||||
export interface InsightsEventPageContext {
|
||||
displayContext: api.SiteInsightsDisplayContext;
|
||||
pageId: string | null;
|
||||
}
|
||||
|
||||
@@ -46,7 +47,9 @@ type TrackEventCallback = <EventName extends InsightsEventName>(
|
||||
options?: InsightsEventOptions
|
||||
) => void;
|
||||
|
||||
const InsightsContext = React.createContext<TrackEventCallback>(() => {});
|
||||
const InsightsContext = React.createContext<TrackEventCallback>(() => {
|
||||
console.error('useTrackEvent must be used within an InsightsProvider');
|
||||
});
|
||||
|
||||
interface InsightsProviderProps {
|
||||
/** If true, the events will be sent to the server. */
|
||||
@@ -263,6 +266,7 @@ function transformEvents(input: {
|
||||
siteShareKey: input.context.siteShareKey ?? null,
|
||||
revision: input.context.revisionId,
|
||||
page: input.pageContext.pageId,
|
||||
displayContext: input.pageContext.displayContext,
|
||||
};
|
||||
|
||||
return input.events.map((partialEvent) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import type { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
import * as React from 'react';
|
||||
|
||||
import { useCurrentPage } from '../hooks';
|
||||
@@ -8,7 +9,10 @@ import { useTrackEvent } from './InsightsProvider';
|
||||
/**
|
||||
* Track a page view event.
|
||||
*/
|
||||
export function TrackPageViewEvent() {
|
||||
export function TrackPageViewEvent(props: {
|
||||
displayContext: SiteInsightsDisplayContext;
|
||||
}) {
|
||||
const { displayContext } = props;
|
||||
const page = useCurrentPage();
|
||||
const trackEvent = useTrackEvent();
|
||||
|
||||
@@ -19,9 +23,10 @@ export function TrackPageViewEvent() {
|
||||
},
|
||||
{
|
||||
pageId: page?.pageId ?? null,
|
||||
displayContext,
|
||||
}
|
||||
);
|
||||
}, [page, trackEvent]);
|
||||
}, [page, trackEvent, displayContext]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function PDFPage(props: {
|
||||
|
||||
const customization =
|
||||
'customization' in baseContext ? baseContext.customization : defaultCustomization();
|
||||
const language = getSpaceLanguage(customization);
|
||||
const language = getSpaceLanguage(baseContext);
|
||||
|
||||
// Compute the pages to render
|
||||
const { pages, total } = selectPages(baseContext.revision.pages, pdfParams);
|
||||
@@ -149,8 +149,7 @@ export async function PDFPage(props: {
|
||||
trademark={
|
||||
customization.trademark.enabled ? (
|
||||
<TrademarkLink
|
||||
space={context.space}
|
||||
customization={customization}
|
||||
context={context}
|
||||
placement={SiteInsightsTrademarkPlacement.Pdf}
|
||||
/>
|
||||
) : null
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
import { CustomizationThemeMode } from '@gitbook/api';
|
||||
|
||||
/**
|
||||
@@ -13,12 +12,7 @@ export async function PDFRootLayout(props: {
|
||||
const { context, children } = props;
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout
|
||||
customization={
|
||||
'customization' in context ? context.customization : defaultCustomization()
|
||||
}
|
||||
forcedTheme={CustomizationThemeMode.Light}
|
||||
>
|
||||
<CustomizationRootLayout context={context} forcedTheme={CustomizationThemeMode.Light}>
|
||||
{children}
|
||||
</CustomizationRootLayout>
|
||||
);
|
||||
|
||||
+71
-19
@@ -2,31 +2,32 @@
|
||||
|
||||
import { useAIChatState } from '@/components/AI';
|
||||
import type { Assistant } from '@/components/AI';
|
||||
import { ChatGPTIcon } from '@/components/AIActions/assets/ChatGPTIcon';
|
||||
import { ClaudeIcon } from '@/components/AIActions/assets/ClaudeIcon';
|
||||
import { MarkdownIcon } from '@/components/AIActions/assets/MarkdownIcon';
|
||||
import { ChatGPTIcon } from '@/components/PageActions/assets/ChatGPTIcon';
|
||||
import { ClaudeIcon } from '@/components/PageActions/assets/ClaudeIcon';
|
||||
import { MarkdownIcon } from '@/components/PageActions/assets/MarkdownIcon';
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
import { DropdownMenuItem, useDropdownMenuClose } from '@/components/primitives/DropdownMenu';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import type { TranslationLanguage } from '@/intl/translations';
|
||||
import type { GitSyncState } from '@gitbook/api';
|
||||
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
|
||||
import assertNever from 'assert-never';
|
||||
import QuickLRU from 'quick-lru';
|
||||
import type React from 'react';
|
||||
import { create } from 'zustand';
|
||||
|
||||
type AIActionType = 'button' | 'dropdown-menu-item';
|
||||
type PageActionType = 'button' | 'dropdown-menu-item';
|
||||
|
||||
/**
|
||||
* 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 chat = useAIChatState();
|
||||
const language = useLanguage();
|
||||
|
||||
return (
|
||||
<AIActionWrapper
|
||||
<PageActionWrapper
|
||||
type={type}
|
||||
icon={assistant.icon}
|
||||
label={assistant.label}
|
||||
@@ -90,7 +91,7 @@ const markdownCache = new QuickLRU<string, string>({ maxSize: 10 });
|
||||
*/
|
||||
export function CopyMarkdown(props: {
|
||||
markdownPageUrl: string;
|
||||
type: AIActionType;
|
||||
type: PageActionType;
|
||||
isDefaultAction?: boolean;
|
||||
}) {
|
||||
const { markdownPageUrl, type, isDefaultAction } = props;
|
||||
@@ -130,7 +131,7 @@ export function CopyMarkdown(props: {
|
||||
};
|
||||
|
||||
return (
|
||||
<AIActionWrapper
|
||||
<PageActionWrapper
|
||||
type={type}
|
||||
icon={copied ? 'check' : 'copy'}
|
||||
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.
|
||||
*/
|
||||
export function ViewAsMarkdown(props: { markdownPageUrl: string; type: AIActionType }) {
|
||||
export function ViewAsMarkdown(props: { markdownPageUrl: string; type: PageActionType }) {
|
||||
const { markdownPageUrl, type } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
return (
|
||||
<AIActionWrapper
|
||||
<PageActionWrapper
|
||||
type={type}
|
||||
icon={<MarkdownIcon className="size-4 fill-current" />}
|
||||
label={tString(language, 'view_page_markdown')}
|
||||
@@ -166,7 +167,7 @@ export function ViewAsMarkdown(props: { markdownPageUrl: string; type: AIActionT
|
||||
export function OpenInLLM(props: {
|
||||
provider: 'chatgpt' | 'claude';
|
||||
url: string;
|
||||
type: AIActionType;
|
||||
type: PageActionType;
|
||||
}) {
|
||||
const { provider, url, type } = props;
|
||||
const language = useLanguage();
|
||||
@@ -174,7 +175,7 @@ export function OpenInLLM(props: {
|
||||
const providerLabel = provider === 'chatgpt' ? 'ChatGPT' : 'Claude';
|
||||
|
||||
return (
|
||||
<AIActionWrapper
|
||||
<PageActionWrapper
|
||||
type={type}
|
||||
icon={
|
||||
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.
|
||||
*/
|
||||
function AIActionWrapper(props: {
|
||||
type: AIActionType;
|
||||
function PageActionWrapper(props: {
|
||||
type: PageActionType;
|
||||
icon: IconName | React.ReactNode;
|
||||
label: string;
|
||||
/**
|
||||
@@ -205,10 +243,22 @@ function AIActionWrapper(props: {
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
description?: string;
|
||||
href?: string;
|
||||
target?: React.HTMLAttributeAnchorTarget;
|
||||
disabled?: 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') {
|
||||
return (
|
||||
@@ -222,7 +272,7 @@ function AIActionWrapper(props: {
|
||||
className="bg-tint-base text-sm"
|
||||
onClick={onClick}
|
||||
href={href}
|
||||
target={href ? '_blank' : undefined}
|
||||
target={href ? target : undefined}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
{shortLabel}
|
||||
@@ -234,7 +284,7 @@ function AIActionWrapper(props: {
|
||||
<DropdownMenuItem
|
||||
className="flex items-stretch gap-2.5 p-2"
|
||||
href={href}
|
||||
target="_blank"
|
||||
target={target}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
@@ -255,9 +305,11 @@ function AIActionWrapper(props: {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{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>
|
||||
{description && <span className="text-tint text-xs">{description}</span>}
|
||||
</div>
|
||||
+57
-23
@@ -2,38 +2,50 @@
|
||||
|
||||
import {
|
||||
CopyMarkdown,
|
||||
GitEditLink,
|
||||
OpenAIAssistant,
|
||||
OpenInLLM,
|
||||
ViewAsMarkdown,
|
||||
} from '@/components/AIActions/AIActions';
|
||||
ViewAsPDF,
|
||||
} from '@/components/PageActions/PageActions';
|
||||
import { Button, ButtonGroup } from '@/components/primitives/Button';
|
||||
import { DropdownMenu, DropdownMenuSeparator } from '@/components/primitives/DropdownMenu';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import type { SiteCustomizationSettings } from '@gitbook/api';
|
||||
import type { GitSyncState, SiteCustomizationSettings } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { useRef } from 'react';
|
||||
import { useAI } from '../AI';
|
||||
|
||||
interface AIActionsDropdownProps {
|
||||
interface PageActionsDropdownProps {
|
||||
markdownPageUrl: string;
|
||||
className?: string;
|
||||
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).
|
||||
*/
|
||||
export function AIActionsDropdown(props: AIActionsDropdownProps) {
|
||||
export function PageActionsDropdown(props: PageActionsDropdownProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
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}>
|
||||
<DefaultAction {...props} />
|
||||
{props.actions.markdown || props.actions.externalAI ? (
|
||||
{defaultActions.length > 0 ? <DefaultAction {...props} /> : null}
|
||||
{dropdownActions.length > 0 ? (
|
||||
<DropdownMenu
|
||||
align="end"
|
||||
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"
|
||||
/>
|
||||
}
|
||||
label={tString(language, 'more')}
|
||||
iconOnly
|
||||
label={tString(
|
||||
language,
|
||||
defaultActions.some(Boolean) ? 'more' : 'actions'
|
||||
)}
|
||||
iconOnly={defaultActions.some(Boolean)}
|
||||
size="xsmall"
|
||||
variant="secondary"
|
||||
className="bg-tint-base text-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AIActionsDropdownMenuContent {...props} />
|
||||
<PageActionsDropdownMenuContent {...props} />
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</ButtonGroup>
|
||||
@@ -63,7 +78,7 @@ export function AIActionsDropdown(props: AIActionsDropdownProps) {
|
||||
/**
|
||||
* The content of the dropdown menu.
|
||||
*/
|
||||
function AIActionsDropdownMenuContent(props: AIActionsDropdownProps) {
|
||||
function PageActionsDropdownMenuContent(props: PageActionsDropdownProps) {
|
||||
const { markdownPageUrl, actions } = props;
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
@@ -98,6 +113,22 @@ function AIActionsDropdownMenuContent(props: AIActionsDropdownProps) {
|
||||
<OpenInLLM provider="claude" url={markdownPageUrl} type="dropdown-menu-item" />
|
||||
</>
|
||||
) : 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
|
||||
*/
|
||||
function DefaultAction(props: AIActionsDropdownProps) {
|
||||
function DefaultAction(props: PageActionsDropdownProps) {
|
||||
const { markdownPageUrl, actions } = props;
|
||||
const assistants = useAI().assistants.filter(
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
|
||||
if (assistants.length) {
|
||||
return <OpenAIAssistant assistant={assistants[0]} type="button" />;
|
||||
const assistant = assistants[0];
|
||||
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) {
|
||||
return (
|
||||
<CopyMarkdown
|
||||
isDefaultAction={!assistants.length}
|
||||
isDefaultAction={!assistant}
|
||||
markdownPageUrl={markdownPageUrl}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (actions.externalAI) {
|
||||
return (
|
||||
<>
|
||||
<OpenInLLM provider="chatgpt" url={markdownPageUrl} type="button" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const AsideSectionHighlight = React.memo(
|
||||
'rounded-md',
|
||||
'straight-corners:rounded-none',
|
||||
'circular-corners:rounded-2xl',
|
||||
'sidebar-list-line:rounded-l-none',
|
||||
'sidebar-list-line:rounded-l-none!',
|
||||
|
||||
'sidebar-list-pill:bg-primary',
|
||||
'[html.theme-muted.sidebar-list-pill_&]:bg-primary-hover',
|
||||
|
||||
@@ -4,18 +4,15 @@ import {
|
||||
type RevisionPageDocument,
|
||||
SiteAdsStatus,
|
||||
SiteInsightsAdPlacement,
|
||||
type Space,
|
||||
} from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import React from 'react';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { getSpaceLanguage, t } from '@/intl/server';
|
||||
import { getDocumentSections } from '@/lib/document-sections';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Ad } from '../Ads';
|
||||
import { getPDFURLSearchParams } from '../PDF';
|
||||
import { PageFeedbackForm } from '../PageFeedback';
|
||||
import { ThemeToggler } from '../ThemeToggler';
|
||||
import { ScrollSectionsList } from './ScrollSectionsList';
|
||||
@@ -118,11 +115,7 @@ export function PageAside(props: {
|
||||
<PageAsideSections document={document} context={context} />
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
<PageAsideActions
|
||||
page={page}
|
||||
withPageFeedback={withPageFeedback}
|
||||
context={context}
|
||||
/>
|
||||
<PageAsideActions page={page} withPageFeedback={withPageFeedback} />
|
||||
</div>
|
||||
) : null}
|
||||
{customization.themes.toggeable || site.ads ? (
|
||||
@@ -135,7 +128,7 @@ export function PageAside(props: {
|
||||
|
||||
function PageAsideHeader(props: { context: GitBookSiteContext }) {
|
||||
const { context } = props;
|
||||
const language = getSpaceLanguage(context.customization);
|
||||
const language = getSpaceLanguage(context);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -171,34 +164,16 @@ async function PageAsideSections(props: { document: JSONDocument; context: GitBo
|
||||
|
||||
function PageAsideActions(props: {
|
||||
withPageFeedback: boolean;
|
||||
context: GitBookSiteContext;
|
||||
page: RevisionPageDocument;
|
||||
}) {
|
||||
const { page, withPageFeedback, context } = props;
|
||||
const { customization, space } = context;
|
||||
const language = getSpaceLanguage(customization);
|
||||
|
||||
const pdfHref = context.linker.toPathInSpace(
|
||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
||||
page: page.id,
|
||||
only: true,
|
||||
limit: 100,
|
||||
}).toString()}`
|
||||
);
|
||||
const { page, withPageFeedback } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-3',
|
||||
'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',
|
||||
'flex flex-col gap-3',
|
||||
'border-tint-subtle border-t first:border-none',
|
||||
'sidebar-list-default:px-3 pt-5 first:pt-0 xl:max-2xl:page-api-block:p-5',
|
||||
'empty:hidden'
|
||||
)}
|
||||
>
|
||||
@@ -207,57 +182,6 @@ function PageAsideActions(props: {
|
||||
<PageFeedbackForm pageId={page.id} />
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
|
||||
<div>
|
||||
<a
|
||||
href={urlJoin(space.gitSync.url, page.git.path)}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'text-sm',
|
||||
'hover:text-tint-strong',
|
||||
'links-accent:hover:underline',
|
||||
'links-accent:hover:underline-offset-4',
|
||||
'links-accent:hover:decoration-[3px]',
|
||||
'links-accent:hover:decoration-primary-subtle',
|
||||
'py-2'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
space.gitSync.installationProvider === 'gitlab'
|
||||
? 'gitlab'
|
||||
: 'github'
|
||||
}
|
||||
className={tcls('size-4', 'mr-1.5')}
|
||||
/>
|
||||
{t(language, 'edit_on_git', getGitSyncName(space))}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
{customization.pdf.enabled ? (
|
||||
<div>
|
||||
<a
|
||||
href={pdfHref}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'text-sm',
|
||||
'hover:text-tint-strong',
|
||||
'links-accent:hover:underline',
|
||||
'links-accent:hover:underline-offset-4',
|
||||
'links-accent:hover:decoration-[3px]',
|
||||
'links-accent:hover:decoration-primary-subtle',
|
||||
'py-2'
|
||||
)}
|
||||
>
|
||||
<Icon icon="file-pdf" className={tcls('size-4', 'mr-1.5')} />
|
||||
{t(language, 'pdf_download')}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -296,14 +220,3 @@ async function PageAsideFooter(props: { context: GitBookSiteContext }) {
|
||||
</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;
|
||||
|
||||
export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
const { sections } = props;
|
||||
|
||||
const ids = React.useMemo(() => {
|
||||
return sections.map((section) => {
|
||||
return section.id;
|
||||
});
|
||||
}, [sections]);
|
||||
export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }) {
|
||||
const ids = React.useMemo(() => sections.map(({ id }) => id), [sections]);
|
||||
|
||||
const enabled = useBodyLoaded();
|
||||
|
||||
@@ -101,7 +95,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
'rounded-md',
|
||||
'straight-corners:rounded-none',
|
||||
'circular-corners:rounded-2xl',
|
||||
'sidebar-list-line:rounded-l-none',
|
||||
'sidebar-list-line:rounded-l-none!',
|
||||
|
||||
'hover:bg-tint-hover',
|
||||
'theme-gradient:hover:bg-tint-12/1',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { getSpaceLanguage } from '@/intl/server';
|
||||
@@ -26,8 +26,9 @@ export function PageBody(props: {
|
||||
ancestors: AncestorRevisionPage[];
|
||||
document: JSONDocument | null;
|
||||
withPageFeedback: boolean;
|
||||
insightsDisplayContext: SiteInsightsDisplayContext;
|
||||
}) {
|
||||
const { page, context, ancestors, document, withPageFeedback } = props;
|
||||
const { page, context, ancestors, document, withPageFeedback, insightsDisplayContext } = props;
|
||||
const { customization } = context;
|
||||
|
||||
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
|
||||
@@ -42,7 +43,7 @@ export function PageBody(props: {
|
||||
: false;
|
||||
const pageWidthWide = page.layout.width === 'wide';
|
||||
const siteWidthWide = pageWidthWide || contentFullWidth;
|
||||
const language = getSpaceLanguage(customization);
|
||||
const language = getSpaceLanguage(context);
|
||||
const updatedAt = page.updatedAt ?? page.createdAt;
|
||||
|
||||
return (
|
||||
@@ -117,7 +118,7 @@ export function PageBody(props: {
|
||||
}
|
||||
</main>
|
||||
|
||||
<TrackPageViewEvent />
|
||||
<TrackPageViewEvent displayContext={insightsDisplayContext} />
|
||||
</CurrentPageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ export async function PageFooterNavigation(props: {
|
||||
page: RevisionPageDocument;
|
||||
}) {
|
||||
const { context, page } = props;
|
||||
const { customization, revision, linker } = context;
|
||||
const { revision, linker } = context;
|
||||
const { previous, next } = resolvePrevNextPages(revision.pages, page);
|
||||
const language = getSpaceLanguage(customization);
|
||||
const language = getSpaceLanguage(context);
|
||||
const previousHref = 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 { AncestorRevisionPage } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type { RevisionPageDocument } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import urlJoin from 'url-join';
|
||||
import { getPDFURLSearchParams } from '../PDF';
|
||||
import { PageActionsDropdown } from '../PageActions/PageActionsDropdown';
|
||||
import { PageIcon } from '../PageIcon';
|
||||
import { StyledLink } from '../primitives';
|
||||
|
||||
@@ -19,8 +21,6 @@ export async function PageHeader(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pageActions = context.customization.pageActions;
|
||||
|
||||
const hasAncestors = ancestors.length > 0;
|
||||
|
||||
return (
|
||||
@@ -38,9 +38,30 @@ export async function PageHeader(props: {
|
||||
>
|
||||
{page.layout.tableOfContents ? (
|
||||
// Show page actions if *any* of the actions are enabled
|
||||
<AIActionsDropdown
|
||||
<PageActionsDropdown
|
||||
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(
|
||||
'float-right ml-4 xl:max-2xl:page-api-block:mr-62',
|
||||
hasAncestors ? '-my-1.5' : '-mt-3 xs:mt-2'
|
||||
|
||||
@@ -31,7 +31,9 @@ import { RootLayoutClientContexts } from './RootLayoutClientContexts';
|
||||
|
||||
import '@gitbook/icons/style.css';
|
||||
import './globals.css';
|
||||
import type { GitBookAnyContext } from '@/lib/context';
|
||||
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@/lib/env';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
import { AnnouncementDismissedScript } from '../Announcement';
|
||||
|
||||
function preloadFont(fontData: FontData) {
|
||||
@@ -56,12 +58,14 @@ function preloadFont(fontData: FontData) {
|
||||
*/
|
||||
export async function CustomizationRootLayout(props: {
|
||||
forcedTheme?: CustomizationThemeMode | null;
|
||||
customization: SiteCustomizationSettings;
|
||||
context: GitBookAnyContext;
|
||||
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 mixColor = getTintMixColor(customization.styling.primaryColor, tintColor);
|
||||
const sidebarStyles = getSidebarStyles(customization);
|
||||
@@ -94,7 +98,9 @@ export async function CustomizationRootLayout(props: {
|
||||
'depth' in customization.styling && `depth-${customization.styling.depth}`,
|
||||
fontNotoColorEmoji.variable,
|
||||
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
|
||||
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
|
||||
@@ -138,7 +144,12 @@ export async function CustomizationRootLayout(props: {
|
||||
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('warning', warningColor.light)}
|
||||
@@ -152,7 +163,12 @@ export async function CustomizationRootLayout(props: {
|
||||
${generateColorVariable('neutral', DEFAULT_TINT_COLOR, { darkMode: true })}
|
||||
|
||||
--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('warning', warningColor.dark, { darkMode: true })}
|
||||
@@ -161,16 +177,7 @@ export async function CustomizationRootLayout(props: {
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body
|
||||
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'
|
||||
)}
|
||||
>
|
||||
<body className="site-background">
|
||||
<IconsProvider
|
||||
assetsURL={GITBOOK_ICONS_URL}
|
||||
assetsURLToken={GITBOOK_ICONS_TOKEN}
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@utility hide-scroll {
|
||||
/* hide scroll bar but still scroll */
|
||||
@utility no-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -29,6 +28,22 @@
|
||||
-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 {
|
||||
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 {
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
@utility triangle {
|
||||
@@ -143,45 +157,13 @@
|
||||
@apply bg-linear-to-bl from-tint-4 to-tint-base to-60% bg-fixed;
|
||||
}
|
||||
|
||||
@utility scroll-nojump {
|
||||
/**
|
||||
* Utility to prevent horizontal jumps when the vertical scrollbar appears
|
||||
* 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));
|
||||
body:has(.page-full-width) & {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@utility site-background {
|
||||
@apply [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle;
|
||||
@apply bg-tint-base;
|
||||
@apply theme-muted:bg-tint-subtle;
|
||||
|
||||
/**
|
||||
* Utility to prevent horizontal jumps when the vertical scrollbar appears
|
||||
* 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%;
|
||||
}
|
||||
}
|
||||
@apply theme-gradient:bg-gradient-primary;
|
||||
@apply theme-gradient-tint:bg-gradient-tint;
|
||||
}
|
||||
|
||||
@utility elevate-link {
|
||||
@@ -211,14 +193,59 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--scrollbar-width: calc(100vw - 100%);
|
||||
@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 {
|
||||
@apply text-tint-strong antialiased;
|
||||
}
|
||||
html {
|
||||
@apply gutter-stable;
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
h1 {
|
||||
@apply tracking-[-0.025em] text-tint-strong text-balance;
|
||||
@@ -249,22 +276,6 @@
|
||||
@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 {
|
||||
@apply [h1,h2,h3,h4]:content-start;
|
||||
@apply [h1,h2,h3,h4]:auto-cols-[auto_1fr];
|
||||
@@ -357,6 +368,11 @@ html.dark.code-monochrome {
|
||||
--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 */
|
||||
.highlight-line {
|
||||
@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;
|
||||
style: CustomizationSearchStyle;
|
||||
className?: string;
|
||||
viewport?: 'desktop' | 'mobile';
|
||||
}
|
||||
|
||||
/**
|
||||
* Client component to render the search input and results.
|
||||
*/
|
||||
export function SearchContainer(props: SearchContainerProps) {
|
||||
const { siteSpaceId, spaceTitle, isMultiVariants, style, className } = props;
|
||||
const { siteSpaceId, spaceTitle, isMultiVariants, style, className, viewport } = props;
|
||||
|
||||
const { assistants } = useAI();
|
||||
|
||||
@@ -55,7 +56,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
initialRef.current = true;
|
||||
|
||||
// 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]);
|
||||
|
||||
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(() => {
|
||||
if (state?.open) {
|
||||
return;
|
||||
@@ -145,6 +159,8 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
|
||||
const showAsk = withSearchAI && normalizedAsk; // withSearchAI && normalizedAsk;
|
||||
|
||||
const visible = viewport === 'desktop' ? !isMobile : viewport === 'mobile' ? isMobile : true;
|
||||
|
||||
return (
|
||||
<SearchAskProvider value={searchAsk}>
|
||||
<Popover
|
||||
@@ -168,7 +184,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
) : null
|
||||
}
|
||||
rootProps={{
|
||||
open: state?.open ?? false,
|
||||
open: visible && (state?.open ?? false),
|
||||
onOpenChange: (open) => {
|
||||
open ? onOpen() : onClose();
|
||||
},
|
||||
@@ -178,7 +194,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
onOpenAutoFocus: (event) => event.preventDefault(),
|
||||
align: 'start',
|
||||
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) => {
|
||||
// Don't close if clicking on the search input itself
|
||||
if (searchInputRef.current?.contains(event.target as Node)) {
|
||||
@@ -211,10 +227,11 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
</Popover>
|
||||
{assistants
|
||||
.filter((assistant) => assistant.ui === true)
|
||||
.map((assistant) => (
|
||||
.map((assistant, index) => (
|
||||
<AIChatButton
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
withShortcut={index === 0}
|
||||
showLabel={
|
||||
assistants.filter((assistant) => assistant.ui === true).length === 1 &&
|
||||
style === CustomizationSearchStyle.Prominent
|
||||
|
||||
@@ -46,7 +46,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
||||
}, [isOpen, value]);
|
||||
|
||||
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.
|
||||
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
|
||||
@@ -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: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',
|
||||
isOpen ? 'max-md:w-56' : 'max-md:w-[38px]',
|
||||
className
|
||||
isOpen ? 'max-md:w-56' : 'max-md:w-[38px]'
|
||||
)}
|
||||
>
|
||||
{value && isOpen ? (
|
||||
|
||||
@@ -296,7 +296,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
key={item.id}
|
||||
question={item.question}
|
||||
active={index === cursor}
|
||||
assistant={assistants[0]}
|
||||
assistant={assistants[0]!}
|
||||
recommended
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -71,8 +71,7 @@ export interface AskAnswerResult {
|
||||
export async function searchAllSiteContent(query: string): Promise<OrderedComputedResult[]> {
|
||||
return traceErrorOnly('Search.searchAllSiteContent', async () => {
|
||||
const context = await getServerActionBaseContext();
|
||||
|
||||
return await searchSiteContent(context, {
|
||||
return searchSiteContent(context, {
|
||||
query,
|
||||
scope: { mode: 'all' },
|
||||
});
|
||||
@@ -124,8 +123,7 @@ export async function streamAskQuestion({
|
||||
},
|
||||
scope: {
|
||||
mode: 'default',
|
||||
// Include the current site space regardless.
|
||||
includedSiteSpaces: [context.siteSpace.id],
|
||||
currentSiteSpace: context.siteSpace.id,
|
||||
},
|
||||
},
|
||||
{ format: 'document' }
|
||||
@@ -270,7 +268,7 @@ async function searchSiteContent(
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
searchResults.map(async (spaceItem) => {
|
||||
searchResults.map((spaceItem) => {
|
||||
const found = findSiteSpaceBy(
|
||||
structure,
|
||||
(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 { getResizedImageURL } from '@/lib/images';
|
||||
import { isSiteIndexable } from '@/lib/seo';
|
||||
import { AIChatProvider, AIContextProvider } from '../AI';
|
||||
import { AIContextProvider } from '../AI';
|
||||
import { RocketLoaderDetector } from './RocketLoaderDetector';
|
||||
import { SiteLayoutClientContexts } from './SiteLayoutClientContexts';
|
||||
|
||||
@@ -58,15 +58,13 @@ export async function SiteLayout(props: {
|
||||
aiMode={customization.ai?.mode}
|
||||
trademark={customization.trademark.enabled}
|
||||
>
|
||||
<AIChatProvider>
|
||||
<SpaceLayout
|
||||
context={context}
|
||||
withTracking={withTracking}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
>
|
||||
{children}
|
||||
</SpaceLayout>
|
||||
</AIChatProvider>
|
||||
<SpaceLayout
|
||||
context={context}
|
||||
withTracking={withTracking}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
>
|
||||
{children}
|
||||
</SpaceLayout>
|
||||
</AIContextProvider>
|
||||
|
||||
{scripts.length > 0 ? (
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
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 { notFound, redirect } from 'next/navigation';
|
||||
import React from 'react';
|
||||
@@ -65,6 +69,7 @@ export async function SitePage(props: SitePageProps) {
|
||||
ancestors={ancestors}
|
||||
document={document}
|
||||
withPageFeedback={withPageFeedback}
|
||||
insightsDisplayContext={SiteInsightsDisplayContext.Site}
|
||||
/>
|
||||
</div>
|
||||
<React.Suspense fallback={null}>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { TrackPageViewEvent } from '@/components/Insights';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useSpaceBasePath } from '../SpaceLayout/SpaceLayoutContext';
|
||||
@@ -46,7 +47,7 @@ export function SitePageNotFound() {
|
||||
</div>
|
||||
|
||||
{/* Track the page not found as a page view */}
|
||||
<TrackPageViewEvent />
|
||||
<TrackPageViewEvent displayContext={SiteInsightsDisplayContext.Site} />
|
||||
</div>
|
||||
</CurrentPageProvider>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
|
||||
>
|
||||
<TOCScrollContainer
|
||||
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) => {
|
||||
if (item.object === 'site-section-group') {
|
||||
@@ -110,7 +110,7 @@ export function SiteSectionListItem(props: {
|
||||
className={tcls(
|
||||
'flex size-8 shrink-0 items-center justify-center rounded-md straight-corners:rounded-none bg-tint-subtle text-lg text-tint leading-none shadow-tint shadow-xs ring-1 ring-tint-subtle transition-transform group-hover/section-link:scale-110 group-hover/section-link:ring-tint-hover group-active/section-link:scale-90 group-active/section-link:shadow-none contrast-more:text-tint-strong dark:shadow-none',
|
||||
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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -30,9 +30,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
|
||||
for (const item of list) {
|
||||
switch (item.object) {
|
||||
case 'site-section-group': {
|
||||
const sections = item.sections
|
||||
.filter((section) => shouldIncludeSection(context, section))
|
||||
.map((section) => encodeSection(context, section));
|
||||
const sections = item.sections.map((section) => encodeSection(context, section));
|
||||
|
||||
// Skip empty groups
|
||||
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:
|
||||
* 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.
|
||||
* 3. Otherwise, return the default one.
|
||||
* 2. If there are site spaces with the same language as the current, filter by language.
|
||||
* 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) {
|
||||
const { siteSpace: currentSiteSpace } = context;
|
||||
@@ -114,9 +87,15 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
||||
return getSectionURL(context, section);
|
||||
}
|
||||
|
||||
const bestMatch = section.siteSpaces.find((siteSpace) =>
|
||||
areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
|
||||
);
|
||||
const possibleMatches =
|
||||
section.siteSpaces.filter((siteSpace) =>
|
||||
areSiteSpacesSameLanguage(siteSpace, currentSiteSpace)
|
||||
) ?? section.siteSpaces;
|
||||
|
||||
const bestMatch =
|
||||
possibleMatches.find((siteSpace) => areSiteSpacesEquivalent(siteSpace, currentSiteSpace)) ??
|
||||
possibleMatches[0];
|
||||
|
||||
if (bestMatch) {
|
||||
return getSiteSpaceURL(context, bestMatch);
|
||||
}
|
||||
@@ -130,3 +109,7 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
||||
function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
|
||||
return siteSpace1.path === siteSpace2.path;
|
||||
}
|
||||
|
||||
function areSiteSpacesSameLanguage(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
|
||||
return siteSpace1.space.language === siteSpace2.space.language;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
CustomizationHeaderPreset,
|
||||
CustomizationSearchStyle,
|
||||
} from '@gitbook/api';
|
||||
import React from 'react';
|
||||
import type React from 'react';
|
||||
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Header, HeaderLogo } from '@/components/Header';
|
||||
@@ -12,21 +12,22 @@ import { TableOfContents } from '@/components/TableOfContents';
|
||||
import { CONTAINER_STYLE } from '@/components/layout';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||
import { GITBOOK_APP_URL } from '@/lib/env';
|
||||
import { AIChatProvider } from '../AI';
|
||||
import type { RenderAIMessageOptions } from '../AI';
|
||||
import { AIChat } from '../AIChat';
|
||||
import { AdaptiveVisitorContextProvider } from '../Adaptive';
|
||||
import { Announcement } from '../Announcement';
|
||||
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
||||
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
|
||||
import { InsightsProvider } from '../Insights';
|
||||
import { SearchContainer } from '../Search';
|
||||
import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
|
||||
import { CurrentContentProvider } from '../hooks';
|
||||
import { SpaceLayoutContextProvider } from './SpaceLayoutContext';
|
||||
|
||||
/**
|
||||
* Render the entire layout of the space (header, table of contents, footer).
|
||||
*/
|
||||
export function SpaceLayout(props: {
|
||||
type SpaceLayoutProps = {
|
||||
context: GitBookSiteContext;
|
||||
|
||||
/** Whether to enable tracking of events into site insights. */
|
||||
@@ -35,39 +36,21 @@ export function SpaceLayout(props: {
|
||||
/** The visitor auth claims. */
|
||||
visitorAuthClaims: VisitorAuthClaims;
|
||||
|
||||
/** The options for rendering AI messages. */
|
||||
aiChatRenderMessageOptions?: RenderAIMessageOptions;
|
||||
|
||||
/** The children of the layout. */
|
||||
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 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 { customization } = context;
|
||||
|
||||
const eventUrl = new URL(
|
||||
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('s', context.site.id);
|
||||
|
||||
const getVisitorClaimsUrl = context.linker.toAbsoluteURL(
|
||||
context.linker.toPathInSite('/~gitbook/visitor')
|
||||
);
|
||||
|
||||
return (
|
||||
<SpaceLayoutContextProvider basePath={context.linker.toPathInSpace('')}>
|
||||
<CurrentContentProvider
|
||||
organizationId={context.organizationId}
|
||||
siteId={context.site.id}
|
||||
siteSectionId={context.sections?.current?.id ?? null}
|
||||
siteSpaceId={context.siteSpace.id}
|
||||
siteShareKey={context.shareKey ?? null}
|
||||
spaceId={context.space.id}
|
||||
revisionId={context.revisionId}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
<AdaptiveVisitorContextProvider
|
||||
contextId={context.contextId}
|
||||
visitorClaimsURL={getVisitorClaimsUrl}
|
||||
>
|
||||
<InsightsProvider
|
||||
enabled={withTracking}
|
||||
appURL={GITBOOK_APP_URL}
|
||||
eventUrl={eventUrl.toString()}
|
||||
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
|
||||
<CurrentContentProvider
|
||||
organizationId={context.organizationId}
|
||||
siteId={context.site.id}
|
||||
siteSectionId={context.sections?.current?.id ?? null}
|
||||
siteSpaceId={context.siteSpace.id}
|
||||
siteShareKey={context.shareKey ?? null}
|
||||
spaceId={context.space.id}
|
||||
revisionId={context.revisionId}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
>
|
||||
<Announcement context={context} />
|
||||
<Header withTopHeader={withTopHeader} context={context} search={search} />
|
||||
{customization.ai?.mode === CustomizationAIMode.Assistant ? (
|
||||
<AIChat trademark={customization.trademark.enabled} />
|
||||
) : null}
|
||||
<div className="scroll-nojump">
|
||||
<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 && '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>
|
||||
<InsightsProvider
|
||||
enabled={withTracking}
|
||||
appURL={GITBOOK_APP_URL}
|
||||
eventUrl={eventUrl.toString()}
|
||||
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
|
||||
>
|
||||
<AIChatProvider renderMessageOptions={aiChatRenderMessageOptions}>
|
||||
{children}
|
||||
</AIChatProvider>
|
||||
</InsightsProvider>
|
||||
</CurrentContentProvider>
|
||||
</AdaptiveVisitorContextProvider>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user