mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-15 23:25:16 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc45cccee2 | |||
| 4cbcc5bb20 | |||
| 51955dae3b | |||
| 51fa3ab2c8 | |||
| af9e66e87e | |||
| 8878dc7f80 | |||
| 4336bc20e0 | |||
| 4a7613a521 | |||
| 689f55340f | |||
| 160ae3d49d | |||
| c079c3c6d9 | |||
| cb782a7557 | |||
| 502d8fc087 | |||
| 6295881138 | |||
| 17f71ba680 | |||
| 5fe7adb87b | |||
| 41684d091b | |||
| 9f6c3347cb | |||
| aa321989c4 | |||
| 0f1565cb4b | |||
| 3996110ebd | |||
| 3422ad4c84 | |||
| 3c07e65b9a | |||
| 287df91661 | |||
| 776bc3135a | |||
| 4638958c9f | |||
| 1f24fe46c4 | |||
| ccbcab8822 | |||
| 094e9cdac2 | |||
| 5c35f36b3a | |||
| d7ec2b57a4 | |||
| 373a1e0312 | |||
| e9fe3975bb | |||
| 32a73b55d4 | |||
| 0426312d6c | |||
| ede2335fb3 | |||
| 709f1a1ccb | |||
| 24b785cbfb | |||
| acc3f2f418 | |||
| d4a653d6a9 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Avoid multiple <h1> in the page by using a <div> for the title in the header
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds content-visibility css property to OpenAPI Operation for better render performance
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
RND-3532: drop down menu for hidden links at small screen size
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Change dark mode shadow for multi-space search toolbar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
bump: scalar from 1.0.5 to 1.0.7
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Replace all icons, previously imported from Geist, by new package `@gitbook/icons`
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for page icons
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Only display spaces dropdown in compact header when site is multi-variants
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/icons': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Initial release for `@gitbook/icons`, a package of content icons to be used for pages, documents, etc.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update lockfile
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Use url hash to open Expandable and scroll to anchor
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix margin for paragraphs in quote blocks
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix inconsistent click area in table because of scroll indicator
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': minor
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Rollback of scalar modal while fixing perf issue
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Optimize images rendered in community ads
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-contentkit': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix rendering of webframe with SSR causing wrong communication between frame and renderer
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for icons style customization for sites
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds tabs to Response Example section e.g. for status code examples
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update Scalar client to latest version
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Update rendering of community ads to match new API response, and make it possible to preview ads.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-math': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Export binary `gitbook-math` to copy assets to a directory
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix "ip" being passed to BSA for community ads
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/icons': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix bin gitbook-icons when installed without the Pro token
|
||||||
@@ -7,7 +7,7 @@ runs:
|
|||||||
- name: 🏗 Prepare Playwright env
|
- name: 🏗 Prepare Playwright env
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version')
|
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["gitbook"].dependencies["@playwright/test"].version')
|
||||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
# Cache browser binaries, cache key is based on Playwright version and OS
|
# Cache browser binaries, cache key is based on Playwright version and OS
|
||||||
@@ -24,6 +24,7 @@ runs:
|
|||||||
- name: 🏗 Install Playwright browser binaries & OS dependencies
|
- name: 🏗 Install Playwright browser binaries & OS dependencies
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
working-directory: packages/gitbook
|
||||||
run: |
|
run: |
|
||||||
bun x playwright install --with-deps chromium
|
bun x playwright install --with-deps chromium
|
||||||
|
|
||||||
@@ -31,5 +32,6 @@ runs:
|
|||||||
- name: 🏗 Install Playwright OS dependencies
|
- name: 🏗 Install Playwright OS dependencies
|
||||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
working-directory: packages/gitbook
|
||||||
run: |
|
run: |
|
||||||
bun x playwright install-deps
|
bun x playwright install-deps
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
env:
|
||||||
|
NPMRC_FONT_AWESOME_TOKEN: ${{ secrets.NPMRC_FONT_AWESOME_TOKEN }}
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy to Cloudflare Pages
|
name: Deploy to Cloudflare Pages
|
||||||
@@ -157,6 +158,24 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PUPPETEER_SKIP_DOWNLOAD: 1
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
- run: bun unit
|
- run: bun unit
|
||||||
|
build-oss:
|
||||||
|
# CI to check that the repository builds correctly on a machine without the credentials
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Build (Open Source)
|
||||||
|
env:
|
||||||
|
NPMRC_FONT_AWESOME_TOKEN: ''
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: 1.1.18
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: 1
|
||||||
|
- run: bun run build
|
||||||
typecheck:
|
typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Typecheck
|
name: Typecheck
|
||||||
|
|||||||
@@ -20,3 +20,5 @@ yarn-error.log*
|
|||||||
# Vercel
|
# Vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
# Env files
|
||||||
|
.env.local
|
||||||
|
|||||||
@@ -4,3 +4,7 @@
|
|||||||
# Generated
|
# Generated
|
||||||
packages/emoji-codepoints/index.ts
|
packages/emoji-codepoints/index.ts
|
||||||
packages/gitbook/public/~gitbook/static/
|
packages/gitbook/public/~gitbook/static/
|
||||||
|
packages/icons/src/data/*.json
|
||||||
|
|
||||||
|
# Build files
|
||||||
|
dist/
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ On the con side, you become responsible for the reliability of your published si
|
|||||||
|
|
||||||
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
|
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
|
||||||
|
|
||||||
If you plan to distribute the code, you must the source code public to comply with GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
If you plan to distribute the code, you must make the source code public to comply with the GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||||
|
|
||||||
See `LICENSE` for more information.
|
See `LICENSE` for more information.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[install.scopes]
|
||||||
|
"awesome.me" = { token = "$NPMRC_FONT_AWESOME_TOKEN", url = "https://npm.fontawesome.com/" }
|
||||||
+9
-1
@@ -8,13 +8,15 @@
|
|||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:cloudflare": "turbo run build:cloudflare",
|
"build:cloudflare": "turbo run build:cloudflare",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
|
"lint:fix": "turbo run lint -- --fix",
|
||||||
"typecheck": "turbo run typecheck",
|
"typecheck": "turbo run typecheck",
|
||||||
"format": "prettier ./ --ignore-unknown --write",
|
"format": "prettier ./ --ignore-unknown --write",
|
||||||
"format:check": "prettier ./ --ignore-unknown --list-different",
|
"format:check": "prettier ./ --ignore-unknown --list-different",
|
||||||
"unit": "turbo run unit",
|
"unit": "turbo run unit",
|
||||||
"e2e": "turbo run e2e",
|
"e2e": "turbo run e2e",
|
||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"release": "turbo run build && changeset publish"
|
"release": "turbo run release && changeset publish",
|
||||||
|
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -23,5 +25,11 @@
|
|||||||
"@changesets/cli": "^2.27.7",
|
"@changesets/cli": "^2.27.7",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"turbo": "^2.0.9"
|
"turbo": "^2.0.9"
|
||||||
|
},
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@scalar/api-client-react": "0.3.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
index.ts
|
dist/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import fs from 'fs';
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
import emojisRaws from 'emoji-assets/emoji.json';
|
import emojisRaws from 'emoji-assets/emoji.json';
|
||||||
|
|
||||||
interface EmojiData {
|
interface EmojiData {
|
||||||
@@ -20,7 +21,8 @@ Object.entries(emojis).forEach(([key, value]) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fs.mkdirSync(path.resolve(__dirname, 'dist'), { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
'index.ts',
|
path.resolve(__dirname, 'dist/index.ts'),
|
||||||
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`,
|
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"exports": "./index.ts",
|
"exports": "./dist/index.ts",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"emoji-assets": "^8.0.0"
|
"emoji-assets": "^8.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "bun ./build.ts"
|
"generate": "bun ./build.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# gitbook
|
# gitbook
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 24b785c: Update shiki for code block syntax highlighting, with support for more languages and fixes for diffs. It also patches the deployment on Cloudflare to support edge functions larger than 4MB.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- acc3f2f: Fix error with the "Try it" of OpenAPI block because of the Scalar proxy failing on Cloudflare with the `cache` option
|
||||||
|
- Updated dependencies [709f1a1]
|
||||||
|
- Updated dependencies [ede2335]
|
||||||
|
- Updated dependencies [0426312]
|
||||||
|
- @gitbook/react-openapi@0.6.0
|
||||||
|
|
||||||
## 0.2.2
|
## 0.2.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { DeepPartial } from 'ts-essentials';
|
||||||
import { argosScreenshot } from '@argos-ci/playwright';
|
import { argosScreenshot } from '@argos-ci/playwright';
|
||||||
import {
|
import {
|
||||||
CustomizationHeaderPreset,
|
CustomizationHeaderPreset,
|
||||||
|
CustomizationIconsStyle,
|
||||||
CustomizationLocale,
|
CustomizationLocale,
|
||||||
CustomizationSettings,
|
SiteCustomizationSettings,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import { test, expect, Page } from '@playwright/test';
|
import { test, expect, Page } from '@playwright/test';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
@@ -230,7 +232,7 @@ const testCases: TestsCase[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Versioning',
|
name: 'Versioning',
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
name: 'Revision',
|
name: 'Revision',
|
||||||
@@ -241,7 +243,7 @@ const testCases: TestsCase[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'PDF',
|
name: 'PDF',
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
name: 'PDF',
|
name: 'PDF',
|
||||||
@@ -251,7 +253,7 @@ const testCases: TestsCase[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Content tests',
|
name: 'Content tests',
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
name: 'Text',
|
name: 'Text',
|
||||||
@@ -349,6 +351,11 @@ const testCases: TestsCase[] = [
|
|||||||
url: 'blocks/embeds',
|
url: 'blocks/embeds',
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Page links',
|
||||||
|
url: 'blocks/page-links',
|
||||||
|
fullPage: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Annotations',
|
name: 'Annotations',
|
||||||
url: 'blocks/annotations',
|
url: 'blocks/annotations',
|
||||||
@@ -361,7 +368,7 @@ const testCases: TestsCase[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Page options',
|
name: 'Page options',
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
name: 'Hidden',
|
name: 'Hidden',
|
||||||
@@ -383,11 +390,16 @@ const testCases: TestsCase[] = [
|
|||||||
url: 'page-options/page-with-cover-and-no-toc',
|
url: 'page-options/page-with-cover-and-no-toc',
|
||||||
run: waitForCookiesDialog,
|
run: waitForCookiesDialog,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'With icon',
|
||||||
|
url: 'page-options/page-with-icon',
|
||||||
|
run: waitForCookiesDialog,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Customization',
|
name: 'Customization',
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
name: 'Without header',
|
name: 'Without header',
|
||||||
@@ -399,16 +411,37 @@ const testCases: TestsCase[] = [
|
|||||||
}),
|
}),
|
||||||
run: waitForCookiesDialog,
|
run: waitForCookiesDialog,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'With duotone icons',
|
||||||
|
url:
|
||||||
|
'page-options/page-with-icon' +
|
||||||
|
getCustomizationURL({
|
||||||
|
styling: {
|
||||||
|
icons: CustomizationIconsStyle.Duotone,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
run: waitForCookiesDialog,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ads',
|
||||||
|
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||||
|
tests: [
|
||||||
|
{
|
||||||
|
name: 'Without previewed ads',
|
||||||
|
url: 'text-page?ads_preview=1',
|
||||||
|
run: waitForCookiesDialog,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Share links',
|
name: 'Share links',
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-share-links/',
|
baseUrl: 'https://gitbook.gitbook.io/gbo-tests-share-links/',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
name: 'Valid link',
|
name: 'Valid link',
|
||||||
url: 'Fc6mMII9FKgnwm7qqynx/',
|
url: 'TGs8PkF4GWVtbmPnWhYL/',
|
||||||
run: waitForCookiesDialog,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Invalid link',
|
name: 'Invalid link',
|
||||||
@@ -611,13 +644,12 @@ const testCases: TestsCase[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Languages',
|
name: 'Languages',
|
||||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
baseUrl: 'https://gitbook.gitbook.io/test-gitbook-open/',
|
||||||
tests: allLocales.map((locale) => ({
|
tests: allLocales.map((locale) => ({
|
||||||
name: locale,
|
name: locale,
|
||||||
url: getCustomizationURL({
|
url: getCustomizationURL({
|
||||||
internationalization: {
|
internationalization: {
|
||||||
locale,
|
locale,
|
||||||
inherit: false,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
run: async (page) => {
|
run: async (page) => {
|
||||||
@@ -658,7 +690,7 @@ for (const testCase of testCases) {
|
|||||||
/**
|
/**
|
||||||
* Create a URL with customization settings.
|
* Create a URL with customization settings.
|
||||||
*/
|
*/
|
||||||
function getCustomizationURL(partial: Partial<CustomizationSettings>): string {
|
function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettings>): string {
|
||||||
const encoded = rison.encode_object(partial);
|
const encoded = rison.encode_object(partial);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ module.exports = withSentryConfig(
|
|||||||
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
||||||
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
|
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
|
||||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
||||||
|
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
||||||
|
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
||||||
},
|
},
|
||||||
|
|
||||||
webpack(config, { dev, webpack }) {
|
webpack(config, { dev, webpack }) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "gitbook",
|
"name": "gitbook",
|
||||||
"version": "0.2.2",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"build:cloudflare": "next-on-pages",
|
"build:cloudflare": "next-on-pages",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
@@ -11,14 +11,16 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"e2e": "playwright test",
|
"e2e": "playwright test",
|
||||||
"unit": "bun test {src,packages}",
|
"unit": "bun test {src,packages}",
|
||||||
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R ../../node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
|
"generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math",
|
||||||
|
"copy:icons": "gitbook-icons ./public/~gitbook/static/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@geist-ui/icons": "^1.0.2",
|
"@gitbook/api": "^0.60.0",
|
||||||
"@gitbook/api": "^0.56.0",
|
"@gitbook/icons": "workspace:*",
|
||||||
"@gitbook/react-math": "workspace:*",
|
"@gitbook/react-math": "workspace:*",
|
||||||
"@gitbook/react-openapi": "workspace:*",
|
"@gitbook/react-openapi": "workspace:*",
|
||||||
"@gitbook/react-contentkit": "workspace:*",
|
"@gitbook/react-contentkit": "workspace:*",
|
||||||
|
"@gitbook/emoji-codepoints": "workspace:*",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@sentry/nextjs": "^7.94.1",
|
"@sentry/nextjs": "^7.94.1",
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
"katex": "^0.16.9",
|
"katex": "^0.16.9",
|
||||||
"mathjax": "^3.2.2",
|
"mathjax": "^3.2.2",
|
||||||
"memoizee": "^0.4.15",
|
"memoizee": "^0.4.15",
|
||||||
"next": "^14.1.3",
|
"next": "^14.2.5",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nuqs": "^1.17.4",
|
"nuqs": "^1.17.4",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
"remark-rehype": "^11.1.0",
|
"remark-rehype": "^11.1.0",
|
||||||
"rison": "^0.1.1",
|
"rison": "^0.1.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"shiki": "^1.2.0",
|
"shiki": "^1.11.1",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
"tailwind-shades": "^1.1.2",
|
"tailwind-shades": "^1.1.2",
|
||||||
"unified": "^11.0.4",
|
"unified": "^11.0.4",
|
||||||
@@ -64,8 +66,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@argos-ci/playwright": "^2.0.0",
|
"@argos-ci/playwright": "^2.0.0",
|
||||||
"@playwright/test": "^1.42.1",
|
"@playwright/test": "^1.42.1",
|
||||||
"@cloudflare/next-on-pages": "^1.11.3",
|
"@cloudflare/next-on-pages": "^1.12.1",
|
||||||
"@cloudflare/workers-types": "^4.20231218.0",
|
"@cloudflare/workers-types": "^4.20240725.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/jsontoxml": "^1.0.5",
|
"@types/jsontoxml": "^1.0.5",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
@@ -84,6 +86,8 @@
|
|||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"psi": "^4.1.0",
|
"psi": "^4.1.0",
|
||||||
"tailwindcss": "^3.4.0",
|
"tailwindcss": "^3.4.0",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3",
|
||||||
|
"env-cmd": "^10.1.0",
|
||||||
|
"ts-essentials": "^10.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export default async function Page(props: {
|
|||||||
const withPageFeedback = customization.feedback.enabled;
|
const withPageFeedback = customization.feedback.enabled;
|
||||||
|
|
||||||
const contentRefContext: ContentRefContext = {
|
const contentRefContext: ContentRefContext = {
|
||||||
|
siteContext: 'siteId' in contentPointer ? contentPointer : null,
|
||||||
space,
|
space,
|
||||||
revisionId: contentTarget.revisionId,
|
revisionId: contentTarget.revisionId,
|
||||||
pages,
|
pages,
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export const runtime = 'edge';
|
|||||||
*/
|
*/
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const pointer = getContentPointer();
|
const pointer = getContentPointer();
|
||||||
const space = await getSpace(pointer.spaceId);
|
const space = await getSpace(
|
||||||
|
pointer.spaceId,
|
||||||
|
'siteId' in pointer ? pointer.siteShareKey : undefined,
|
||||||
|
);
|
||||||
const parent =
|
const parent =
|
||||||
'siteId' in pointer
|
'siteId' in pointer
|
||||||
? await getSite(pointer.organizationId, pointer.siteId)
|
? await getSite(pointer.organizationId, pointer.siteId)
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ export const runtime = 'edge';
|
|||||||
* Generate a sitemap.xml for the current space.
|
* Generate a sitemap.xml for the current space.
|
||||||
*/
|
*/
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const { pages: rootPages } = await getSpaceContentData(getContentPointer());
|
const pointer = getContentPointer();
|
||||||
|
const { pages: rootPages } = await getSpaceContentData(
|
||||||
|
pointer,
|
||||||
|
'siteId' in pointer ? pointer.siteShareKey : undefined,
|
||||||
|
);
|
||||||
const pages = flattenPages(rootPages, (page) => !page.hidden);
|
const pages = flattenPages(rootPages, (page) => !page.hidden);
|
||||||
const urls = pages.map(({ page, depth }) => {
|
const urls = pages.map(({ page, depth }) => {
|
||||||
// Decay priority with depth
|
// Decay priority with depth
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function GET(req: NextRequest) {
|
|||||||
const spaceId = pointer.spaceId;
|
const spaceId = pointer.spaceId;
|
||||||
|
|
||||||
const [space, customization] = await Promise.all([
|
const [space, customization] = await Promise.all([
|
||||||
getSpace(spaceId),
|
getSpace(spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined),
|
||||||
'siteId' in pointer ? getCurrentSiteCustomization(pointer) : getSpaceCustomization(spaceId),
|
'siteId' in pointer ? getCurrentSiteCustomization(pointer) : getSpaceCustomization(spaceId),
|
||||||
]);
|
]);
|
||||||
const parent =
|
const parent =
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { AlertTriangle } from '@geist-ui/icons';
|
import { Icon } from '@gitbook/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useScrollActiveId } from '@/components/hooks';
|
import { useScrollActiveId } from '@/components/hooks';
|
||||||
@@ -105,7 +105,10 @@ export function PageControlButtons(props: {
|
|||||||
'max-w-sm',
|
'max-w-sm',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AlertTriangle className={tcls('size-6', 'mr-3', 'mt-1')} />{' '}
|
<Icon
|
||||||
|
icon="triangle-exclamation"
|
||||||
|
className={tcls('size-6', 'mr-3', 'mt-1')}
|
||||||
|
/>{' '}
|
||||||
<div>
|
<div>
|
||||||
<div>{t(language, 'pdf_limit_reached', total, pageIds.length)}</div>
|
<div>{t(language, 'pdf_limit_reached', total, pageIds.length)}</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { ArrowLeft, Printer } from '@geist-ui/icons';
|
|
||||||
import {
|
import {
|
||||||
CustomizationSettings,
|
CustomizationSettings,
|
||||||
Revision,
|
Revision,
|
||||||
RevisionPageDocument,
|
RevisionPageDocument,
|
||||||
RevisionPageGroup,
|
RevisionPageGroup,
|
||||||
RevisionPageLink,
|
|
||||||
SiteCustomizationSettings,
|
SiteCustomizationSettings,
|
||||||
Space,
|
Space,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
@@ -42,7 +41,10 @@ export const runtime = 'edge';
|
|||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const contentPointer = getContentPointer();
|
const contentPointer = getContentPointer();
|
||||||
const [space, customization] = await Promise.all([
|
const [space, customization] = await Promise.all([
|
||||||
getSpace(contentPointer.spaceId),
|
getSpace(
|
||||||
|
contentPointer.spaceId,
|
||||||
|
'siteId' in contentPointer ? contentPointer.siteShareKey : undefined,
|
||||||
|
),
|
||||||
'siteId' in contentPointer
|
'siteId' in contentPointer
|
||||||
? getCurrentSiteCustomization(contentPointer)
|
? getCurrentSiteCustomization(contentPointer)
|
||||||
: getSpaceCustomization(contentPointer.spaceId),
|
: getSpaceCustomization(contentPointer.spaceId),
|
||||||
@@ -72,7 +74,10 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
|||||||
'siteId' in contentPointer
|
'siteId' in contentPointer
|
||||||
? getCurrentSiteCustomization(contentPointer)
|
? getCurrentSiteCustomization(contentPointer)
|
||||||
: getSpaceCustomization(contentPointer.spaceId),
|
: getSpaceCustomization(contentPointer.spaceId),
|
||||||
getSpaceContentData(contentPointer),
|
getSpaceContentData(
|
||||||
|
contentPointer,
|
||||||
|
'siteId' in contentPointer ? contentPointer.siteShareKey : undefined,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
const language = getSpaceLanguage(customization);
|
const language = getSpaceLanguage(customization);
|
||||||
|
|
||||||
@@ -110,7 +115,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
|||||||
'border',
|
'border',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ArrowLeft className={tcls('size-6')} />
|
<Icon icon="arrow-left" className={tcls('size-6')} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -136,7 +141,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
|||||||
'border',
|
'border',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Printer className={tcls('size-6')} />
|
<Icon icon="print" className={tcls('size-6')} />
|
||||||
</PrintButton>
|
</PrintButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -170,6 +175,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
|||||||
space={space}
|
space={space}
|
||||||
page={page}
|
page={page}
|
||||||
refContext={{
|
refContext={{
|
||||||
|
siteContext: 'siteId' in contentPointer ? contentPointer : null,
|
||||||
space,
|
space,
|
||||||
revisionId: contentTarget.revisionId,
|
revisionId: contentTarget.revisionId,
|
||||||
pages: rootPages,
|
pages: rootPages,
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ export function getContentPointer(): ContentPointer | SiteContentPointer {
|
|||||||
export async function fetchSpaceData() {
|
export async function fetchSpaceData() {
|
||||||
const content = getContentPointer();
|
const content = getContentPointer();
|
||||||
|
|
||||||
|
const siteShareKey = 'siteId' in content ? content.siteShareKey : undefined;
|
||||||
|
|
||||||
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
|
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
|
||||||
'siteId' in content
|
'siteId' in content
|
||||||
? [
|
? [
|
||||||
@@ -78,10 +80,10 @@ export async function fetchSpaceData() {
|
|||||||
fetchParentSite({
|
fetchParentSite({
|
||||||
organizationId: content.organizationId,
|
organizationId: content.organizationId,
|
||||||
siteId: content.siteId,
|
siteId: content.siteId,
|
||||||
siteShareKey: content.siteShareKey,
|
siteShareKey,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: [getSpaceData(content)],
|
: [getSpaceData(content, siteShareKey)],
|
||||||
);
|
);
|
||||||
|
|
||||||
const parent = await (parentSite ?? fetchParentCollection(space));
|
const parent = await (parentSite ?? fetchParentCollection(space));
|
||||||
@@ -104,9 +106,10 @@ export async function fetchSpaceData() {
|
|||||||
*/
|
*/
|
||||||
export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
||||||
const content = getContentPointer();
|
const content = getContentPointer();
|
||||||
|
const siteShareKey = 'siteId' in content ? content.siteShareKey : undefined;
|
||||||
const { space, contentTarget, pages, customization, scripts } = await ('siteId' in content
|
const { space, contentTarget, pages, customization, scripts } = await ('siteId' in content
|
||||||
? getCurrentSiteData(content)
|
? getCurrentSiteData(content)
|
||||||
: getSpaceData(content));
|
: getSpaceData(content, siteShareKey));
|
||||||
|
|
||||||
const page = await resolvePage(contentTarget, pages, params);
|
const page = await resolvePage(contentTarget, pages, params);
|
||||||
const [parent, document] = await Promise.all([
|
const [parent, document] = await Promise.all([
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
CustomizationBackground,
|
CustomizationBackground,
|
||||||
CustomizationCorners,
|
CustomizationCorners,
|
||||||
|
CustomizationIconsStyle,
|
||||||
CustomizationHeaderPreset,
|
CustomizationHeaderPreset,
|
||||||
CustomizationSettings,
|
CustomizationSettings,
|
||||||
SiteCustomizationSettings,
|
SiteCustomizationSettings,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
|
import { IconsProvider, IconStyle } from '@gitbook/icons';
|
||||||
import assertNever from 'assert-never';
|
import assertNever from 'assert-never';
|
||||||
import colors from 'tailwindcss/colors';
|
import colors from 'tailwindcss/colors';
|
||||||
|
|
||||||
@@ -12,11 +14,13 @@ import { emojiFontClassName } from '@/components/primitives';
|
|||||||
import { fonts, ibmPlexMono } from '@/fonts';
|
import { fonts, ibmPlexMono } from '@/fonts';
|
||||||
import { getSpaceLanguage } from '@/intl/server';
|
import { getSpaceLanguage } from '@/intl/server';
|
||||||
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
|
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
|
||||||
|
import { getStaticFileURL } from '@/lib/assets';
|
||||||
import { hexToRgb, shadesOfColor } from '@/lib/colors';
|
import { hexToRgb, shadesOfColor } from '@/lib/colors';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { ClientContexts } from './ClientContexts';
|
import { ClientContexts } from './ClientContexts';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
import '@gitbook/icons/style.css';
|
||||||
import { getContentPointer } from './fetch';
|
import { getContentPointer } from './fetch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,7 +109,22 @@ export default async function SpaceRootLayout(props: { children: React.ReactNode
|
|||||||
'dark:bg-dark',
|
'dark:bg-dark',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ClientContexts language={language}>{children}</ClientContexts>
|
<IconsProvider
|
||||||
|
assetsURL={process.env.GITBOOK_ICONS_URL ?? getStaticFileURL('icons')}
|
||||||
|
assetsURLToken={process.env.GITBOOK_ICONS_TOKEN}
|
||||||
|
assetsByStyles={{
|
||||||
|
'custom-icons': {
|
||||||
|
assetsURL: getStaticFileURL('icons'),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
iconStyle={
|
||||||
|
('icons' in customization.styling
|
||||||
|
? apiToIconsStyles[customization.styling.icons]
|
||||||
|
: null) || IconStyle.Regular
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ClientContexts language={language}>{children}</ClientContexts>
|
||||||
|
</IconsProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
@@ -188,3 +207,13 @@ function generateHeaderTheme(customization: CustomizationSettings | SiteCustomiz
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const apiToIconsStyles: {
|
||||||
|
[key in CustomizationIconsStyle]: IconStyle;
|
||||||
|
} = {
|
||||||
|
[CustomizationIconsStyle.Regular]: IconStyle.Regular,
|
||||||
|
[CustomizationIconsStyle.Solid]: IconStyle.Solid,
|
||||||
|
[CustomizationIconsStyle.Duotone]: IconStyle.Duotone,
|
||||||
|
[CustomizationIconsStyle.Thin]: IconStyle.Thin,
|
||||||
|
[CustomizationIconsStyle.Light]: IconStyle.Light,
|
||||||
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export async function POST(req: NextRequest) {
|
|||||||
// no-cors, *cors, same-origin
|
// no-cors, *cors, same-origin
|
||||||
// mode: 'cors', // Not supported on Cloudflare Workers
|
// mode: 'cors', // Not supported on Cloudflare Workers
|
||||||
// *default, no-cache, reload, force-cache, only-if-cached
|
// *default, no-cache, reload, force-cache, only-if-cached
|
||||||
cache: 'no-cache',
|
// cache: 'no-cache', // Not supported on Cloudflare Workers
|
||||||
// include, *same-origin, omit
|
// include, *same-origin, omit
|
||||||
// credentials: 'include', // Not supported on Cloudflare Workers
|
// credentials: 'include', // Not supported on Cloudflare Workers
|
||||||
headers: requestBody.headers,
|
headers: requestBody.headers,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import IconHeart from '@geist-ui/icons/heart';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { AdClassicRendering } from './AdClassicRendering';
|
import { renderAd } from './renderAd';
|
||||||
import { AdCoverRendering } from './AdCoverRendering';
|
|
||||||
import { AdItem, AdsResponse } from './types';
|
/**
|
||||||
|
* Zone ID provided by BuySellAds for the preview.
|
||||||
|
*/
|
||||||
|
const PREVIEW_ZONE_ID = 'CVAIKKQM';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch and render the Ad placement.
|
* Fetch and render the Ad placement.
|
||||||
@@ -21,7 +23,7 @@ export function Ad({
|
|||||||
style,
|
style,
|
||||||
mode = 'auto',
|
mode = 'auto',
|
||||||
}: {
|
}: {
|
||||||
zoneId: string;
|
zoneId: string | null;
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
placement: string;
|
placement: string;
|
||||||
ignore: boolean;
|
ignore: boolean;
|
||||||
@@ -30,8 +32,7 @@ export function Ad({
|
|||||||
}) {
|
}) {
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [visible, setVisible] = React.useState(false);
|
const [visible, setVisible] = React.useState(false);
|
||||||
const [failed, setFailed] = React.useState(false);
|
const [ad, setAd] = React.useState<React.ReactNode | undefined>(undefined);
|
||||||
const [ad, setAd] = React.useState<AdItem | undefined>(undefined);
|
|
||||||
|
|
||||||
// Observe the container visibility
|
// Observe the container visibility
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -68,39 +69,35 @@ export function Ad({
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
|
const preview = new URL(window.location.href).searchParams.has('ads_preview');
|
||||||
|
const realZoneId = preview ? PREVIEW_ZONE_ID : zoneId;
|
||||||
|
|
||||||
|
if (!realZoneId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
|
const result = await renderAd({
|
||||||
url.searchParams.set('segment', `placement:${placement}`);
|
spaceId,
|
||||||
url.searchParams.set('v', 'true');
|
placement,
|
||||||
if (ignore) {
|
ignore: ignore || preview,
|
||||||
url.searchParams.set('ignore', 'true');
|
zoneId: realZoneId,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (result) {
|
||||||
const res = await fetch(url);
|
setAd(result);
|
||||||
const json: AdsResponse = await res.json();
|
|
||||||
|
|
||||||
if (cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const first = json.ads[0];
|
|
||||||
if (first && 'active' in first) {
|
|
||||||
setAd(first);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
'Failed to fetch ad, it might have been blocked by a ad-blocker',
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
setFailed(true);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [visible, zoneId, ignore, placement]);
|
}, [visible, spaceId, zoneId, ignore, placement, mode]);
|
||||||
|
|
||||||
const viaUrl = new URL('https://www.gitbook.com');
|
const viaUrl = new URL('https://www.gitbook.com');
|
||||||
viaUrl.searchParams.set('utm_source', 'content');
|
viaUrl.searchParams.set('utm_source', 'content');
|
||||||
@@ -109,92 +106,7 @@ export function Ad({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={tcls(style)}>
|
<div ref={containerRef} className={tcls(style)}>
|
||||||
{ad && ad.description && ad.statlink ? (
|
{ad ? ad : null}
|
||||||
<>
|
|
||||||
{mode === 'classic' || !('callToAction' in ad) ? (
|
|
||||||
<AdClassicRendering ad={ad} />
|
|
||||||
) : (
|
|
||||||
<AdCoverRendering ad={ad} />
|
|
||||||
)}
|
|
||||||
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
|
|
||||||
<p
|
|
||||||
className={tcls(
|
|
||||||
'mt-2',
|
|
||||||
'mr-2',
|
|
||||||
'text-xs',
|
|
||||||
'text-right',
|
|
||||||
'text-dark/5',
|
|
||||||
'dark:text-light/5',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href={viaUrl.toString()}
|
|
||||||
className={tcls('hover:underline')}
|
|
||||||
>
|
|
||||||
Ads via GitBook
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : failed ? (
|
|
||||||
<AdBlockerPlaceholder />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render attribution or verification pixels.
|
|
||||||
* https://docs.buysellads.com/ad-serving-api#pixels
|
|
||||||
*/
|
|
||||||
function AdPixels({ rawPixel }: { rawPixel: string }) {
|
|
||||||
const pixels = rawPixel.split('||');
|
|
||||||
const time = String(Math.round(Date.now() / 1e4) | 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={tcls('hidden')}>
|
|
||||||
{pixels.map((pixel, index) => {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
key={index}
|
|
||||||
src={pixel.replace('[timestamp]', time)}
|
|
||||||
width="1"
|
|
||||||
height="1"
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
alt="Ads tracking pixel"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder when visitor has an ad-blocker.
|
|
||||||
*/
|
|
||||||
function AdBlockerPlaceholder() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={tcls(
|
|
||||||
'flex',
|
|
||||||
'flex-col',
|
|
||||||
'gap-3',
|
|
||||||
'bg-light-2',
|
|
||||||
'text-dark/7',
|
|
||||||
'dark:bg-dark-2',
|
|
||||||
'dark:text-light/7',
|
|
||||||
'rounded-lg',
|
|
||||||
'p-4',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={tcls('flex', 'flex-row', 'gap-2', 'items-center')}>
|
|
||||||
<IconHeart className={tcls('w-4', 'h-4', 'text-primary-500')} />
|
|
||||||
<p className={tcls('text-xs', 'font-semibold')}>Ad disabled</p>
|
|
||||||
</div>
|
|
||||||
<p className={tcls('text-xs')}>
|
|
||||||
{`It looks like you're using an adblocker. Whitelist this site to help support this
|
|
||||||
project.`}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { getResizedImageURL } from '@/lib/images';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { AdItem } from './types';
|
import { AdItem } from './types';
|
||||||
@@ -7,7 +8,7 @@ import { AdItem } from './types';
|
|||||||
/**
|
/**
|
||||||
* Classic rendering for an ad.
|
* Classic rendering for an ad.
|
||||||
*/
|
*/
|
||||||
export function AdClassicRendering({ ad }: { ad: AdItem }) {
|
export async function AdClassicRendering({ ad }: { ad: AdItem }) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className={tcls(
|
className={tcls(
|
||||||
@@ -29,14 +30,21 @@ export function AdClassicRendering({ ad }: { ad: AdItem }) {
|
|||||||
>
|
>
|
||||||
{'smallImage' in ad ? (
|
{'smallImage' in ad ? (
|
||||||
<div>
|
<div>
|
||||||
<img alt="Ads logo" className={tcls('rounded-md')} src={ad.smallImage} />
|
<img
|
||||||
|
alt="Ads logo"
|
||||||
|
className={tcls('rounded-md')}
|
||||||
|
src={await getResizedImageURL(ad.smallImage, { width: 192, dpr: 2 })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={tcls('px-6', 'py-4', 'rounded-md')}
|
className={tcls('px-6', 'py-4', 'rounded-md')}
|
||||||
style={{ backgroundColor: ad.backgroundColor }}
|
style={{ backgroundColor: ad.backgroundColor }}
|
||||||
>
|
>
|
||||||
<img alt="Ads logo" src={ad.logo} />
|
<img
|
||||||
|
alt="Ads logo"
|
||||||
|
src={await getResizedImageURL(ad.logo, { width: 192 - 48, dpr: 2 })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={tcls('flex', 'flex-col')}>
|
<div className={tcls('flex', 'flex-col')}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { hexToRgba } from '@/lib/colors';
|
import { hexToRgba } from '@/lib/colors';
|
||||||
|
import { getResizedImageURL } from '@/lib/images';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { AdCover } from './types';
|
import { AdCover } from './types';
|
||||||
@@ -8,7 +9,9 @@ import { AdCover } from './types';
|
|||||||
/**
|
/**
|
||||||
* Cover rendering for an ad.
|
* Cover rendering for an ad.
|
||||||
*/
|
*/
|
||||||
export function AdCoverRendering({ ad }: { ad: AdCover }) {
|
export async function AdCoverRendering({ ad }: { ad: AdCover }) {
|
||||||
|
const largeImage = await getResizedImageURL(ad.largeImage, { width: 128, dpr: 2 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className={tcls(
|
className={tcls(
|
||||||
@@ -43,14 +46,14 @@ export function AdCoverRendering({ ad }: { ad: AdCover }) {
|
|||||||
'z-0',
|
'z-0',
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${ad.largeImage})`,
|
backgroundImage: `url(${largeImage})`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={tcls('z-[2]')}>
|
<div className={tcls('z-[2]')}>
|
||||||
<img
|
<img
|
||||||
alt="Large image"
|
alt="Large image"
|
||||||
src={ad.largeImage}
|
src={largeImage}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'rounded-md',
|
'rounded-md',
|
||||||
'shadow-md',
|
'shadow-md',
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render attribution or verification pixels.
|
||||||
|
* https://docs.buysellads.com/ad-serving-api#pixels
|
||||||
|
*/
|
||||||
|
export function AdPixels({ rawPixel }: { rawPixel: string }) {
|
||||||
|
const pixels = rawPixel.split('||');
|
||||||
|
const time = String(Math.round(Date.now() / 1e4) | 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={tcls('hidden')}>
|
||||||
|
{pixels.map((pixel, index) => {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
key={index}
|
||||||
|
src={pixel.replace('[timestamp]', time)}
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
alt="Ads tracking pixel"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { headers } from 'next/headers';
|
||||||
|
|
||||||
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { AdClassicRendering } from './AdClassicRendering';
|
||||||
|
import { AdCoverRendering } from './AdCoverRendering';
|
||||||
|
import { AdPixels } from './AdPixels';
|
||||||
|
import { AdItem, AdsResponse } from './types';
|
||||||
|
|
||||||
|
interface FetchAdOptions {
|
||||||
|
/** ID of the zone to fetch Ads for */
|
||||||
|
zoneId: string;
|
||||||
|
/** ID of the space */
|
||||||
|
spaceId: string;
|
||||||
|
/** Mode to render the Ad */
|
||||||
|
mode: 'classic' | 'auto' | 'cover';
|
||||||
|
/** Name of the placement for the ad */
|
||||||
|
placement: string;
|
||||||
|
/** If true, we'll not track it as an impression */
|
||||||
|
ignore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server action to render the Ad placement.
|
||||||
|
* We use a server-action to avoid caching issues with server-side components,
|
||||||
|
* and properly access user-agent and IP.
|
||||||
|
*/
|
||||||
|
export async function renderAd(options: FetchAdOptions) {
|
||||||
|
const { spaceId, mode } = options;
|
||||||
|
const result = await fetchAd(options);
|
||||||
|
if (!result || !result.ad.description || !result.ad.statlink) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ad, ip } = result;
|
||||||
|
|
||||||
|
const viaUrl = new URL('https://www.gitbook.com');
|
||||||
|
viaUrl.searchParams.set('utm_source', 'content');
|
||||||
|
viaUrl.searchParams.set('utm_medium', 'ads');
|
||||||
|
viaUrl.searchParams.set('utm_campaign', spaceId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mode === 'classic' || !('callToAction' in ad) ? (
|
||||||
|
<AdClassicRendering ad={ad} />
|
||||||
|
) : (
|
||||||
|
<AdCoverRendering ad={ad} />
|
||||||
|
)}
|
||||||
|
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
|
||||||
|
<p
|
||||||
|
className={tcls(
|
||||||
|
'mt-2',
|
||||||
|
'mr-2',
|
||||||
|
'text-xs',
|
||||||
|
'text-right',
|
||||||
|
'text-dark/5',
|
||||||
|
'dark:text-light/5',
|
||||||
|
)}
|
||||||
|
data-debug-ip={ip}
|
||||||
|
>
|
||||||
|
<a target="_blank" href={viaUrl.toString()} className={tcls('hover:underline')}>
|
||||||
|
Sponsored via GitBook
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAd({
|
||||||
|
zoneId,
|
||||||
|
placement,
|
||||||
|
ignore,
|
||||||
|
}: FetchAdOptions): Promise<{ ad: AdItem; ip: string } | null> {
|
||||||
|
const headersSet = headers();
|
||||||
|
const ip =
|
||||||
|
headersSet.get('x-gitbook-ipv4') ??
|
||||||
|
headersSet.get('x-gitbook-ip') ??
|
||||||
|
headersSet.get('cf-pseudo-ipv4') ??
|
||||||
|
headersSet.get('cf-connecting-ip') ??
|
||||||
|
headersSet.get('x-forwarded-for') ??
|
||||||
|
'';
|
||||||
|
const userAgent = headersSet.get('user-agent') ?? '';
|
||||||
|
|
||||||
|
const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
|
||||||
|
url.searchParams.set('segment', `placement:${placement}`);
|
||||||
|
url.searchParams.set('v', 'true');
|
||||||
|
url.searchParams.set('forwardedip', ip);
|
||||||
|
url.searchParams.set('useragent', userAgent);
|
||||||
|
if (ignore) {
|
||||||
|
url.searchParams.set('ignore', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json: AdsResponse = await res.json();
|
||||||
|
|
||||||
|
const first = json.ads[0];
|
||||||
|
if (first && 'active' in first) {
|
||||||
|
return { ad: first, ip };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import IconX from '@geist-ui/icons/x';
|
import { Icon } from '@gitbook/icons';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { Button } from '@/components/primitives';
|
import { Button } from '@/components/primitives';
|
||||||
@@ -92,7 +92,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
|||||||
)}
|
)}
|
||||||
title={tString(language, 'cookies_close')}
|
title={tString(language, 'cookies_close')}
|
||||||
>
|
>
|
||||||
<IconX className={tcls('w-4')} />
|
<Icon icon="xmark" className={tcls('size-4')} />
|
||||||
</button>
|
</button>
|
||||||
<div className={tcls('mt-3', 'flex', 'flex-row', 'gap-2')}>
|
<div className={tcls('mt-3', 'flex', 'flex-row', 'gap-2')}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export async function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
|
|||||||
ancestorBlocks={[]}
|
ancestorBlocks={[]}
|
||||||
context={context}
|
context={context}
|
||||||
nodes={fragment.nodes}
|
nodes={fragment.nodes}
|
||||||
|
style={['space-y-4']}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { DocumentBlockContentRef } from '@gitbook/api';
|
import { DocumentBlockContentRef } from '@gitbook/api';
|
||||||
|
|
||||||
import { LogoIcon } from '@/components/icons';
|
|
||||||
import { Card, Emoji } from '@/components/primitives';
|
import { Card, Emoji } from '@/components/primitives';
|
||||||
import { getSpaceCustomization, ignoreAPIError } from '@/lib/api';
|
import { getSpaceCustomization, ignoreAPIError } from '@/lib/api';
|
||||||
import { ResolvedContentRef } from '@/lib/references';
|
import { ResolvedContentRef } from '@/lib/references';
|
||||||
|
|
||||||
import { BlockProps } from './Block';
|
import { BlockProps } from './Block';
|
||||||
|
import { SpaceIcon } from '../Space/SpaceIcon';
|
||||||
|
|
||||||
export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>) {
|
export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>) {
|
||||||
const { block, context, style } = props;
|
const { block, context, style } = props;
|
||||||
|
|
||||||
const resolved = await context.resolveContentRef(block.data.ref, {
|
const resolved = await context.resolveContentRef(block.data.ref, {
|
||||||
resolveAnchorText: true,
|
resolveAnchorText: true,
|
||||||
|
iconStyle: ['text-xl', 'text-dark/6', 'dark:text-light/6'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
@@ -29,7 +30,7 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
leadingIcon={resolved.emoji ? <Emoji code={resolved.emoji} style="text-xl" /> : null}
|
leadingIcon={resolved.icon ? resolved.icon : null}
|
||||||
href={resolved.href}
|
href={resolved.href}
|
||||||
title={resolved.text}
|
title={resolved.text}
|
||||||
style={style}
|
style={style}
|
||||||
@@ -55,7 +56,7 @@ async function SpaceRefCard(
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
leadingIcon={
|
leadingIcon={
|
||||||
<LogoIcon
|
<SpaceIcon
|
||||||
icon={customIcon}
|
icon={customIcon}
|
||||||
emoji={customEmoji}
|
emoji={customEmoji}
|
||||||
alt=""
|
alt=""
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
--shiki-token-string-expression: #6a4906;
|
--shiki-token-string-expression: #6a4906;
|
||||||
--shiki-token-punctuation: theme('colors.pomegranate.700/.92');
|
--shiki-token-punctuation: theme('colors.pomegranate.700/.92');
|
||||||
--shiki-token-link: theme('colors.dark.DEFAULT');
|
--shiki-token-link: theme('colors.dark.DEFAULT');
|
||||||
|
--shiki-token-inserted: #22863a;
|
||||||
|
--shiki-token-deleted: #b31d28;
|
||||||
|
--shiki-token-changed: #8250df;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark {
|
html.dark {
|
||||||
@@ -22,4 +25,7 @@ html.dark {
|
|||||||
--shiki-token-string-expression: theme('colors.light.4');
|
--shiki-token-string-expression: theme('colors.light.4');
|
||||||
--shiki-token-punctuation: #acc6ee;
|
--shiki-token-punctuation: #acc6ee;
|
||||||
--shiki-token-link: theme('colors.pomegranate.400');
|
--shiki-token-link: theme('colors.pomegranate.400');
|
||||||
|
--shiki-token-inserted: #85e89d;
|
||||||
|
--shiki-token-deleted: #fdaeb7;
|
||||||
|
--shiki-token-changed: #56b6c2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useHash } from '@/components/hooks';
|
||||||
|
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details component rendered on client so it can expand dependent on url hash changes.
|
||||||
|
*/
|
||||||
|
export function Details(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
id: string;
|
||||||
|
contentIds?: string[];
|
||||||
|
open?: boolean;
|
||||||
|
className?: ClassValue;
|
||||||
|
}) {
|
||||||
|
const { children, id, className } = props;
|
||||||
|
|
||||||
|
const detailsRef = React.useRef<HTMLDetailsElement>(null);
|
||||||
|
|
||||||
|
const [openFromHash, setOpenFromHash] = React.useState(false);
|
||||||
|
|
||||||
|
const hash = useHash();
|
||||||
|
/**
|
||||||
|
* Open the details element if the url hash refers to the id of the details element
|
||||||
|
* or the id of some element contained within the details element.
|
||||||
|
*/
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!hash || !detailsRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hash === id) {
|
||||||
|
setOpenFromHash(true);
|
||||||
|
}
|
||||||
|
const activeElement = document.getElementById(hash);
|
||||||
|
setOpenFromHash(Boolean(activeElement && detailsRef.current?.contains(activeElement)));
|
||||||
|
}, [hash, id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details
|
||||||
|
ref={detailsRef}
|
||||||
|
id={id}
|
||||||
|
open={props.open || openFromHash}
|
||||||
|
className={tcls(
|
||||||
|
className,
|
||||||
|
'group/expandable',
|
||||||
|
'shadow-dark/1',
|
||||||
|
'bg-gradient-to-t',
|
||||||
|
'from-light-1',
|
||||||
|
'to-light-1',
|
||||||
|
'border',
|
||||||
|
'border-b-0',
|
||||||
|
'border-dark-3/3',
|
||||||
|
//all
|
||||||
|
'[&]:mt-[0px]',
|
||||||
|
//select first child
|
||||||
|
'[&:first-child]:mt-5',
|
||||||
|
'[&:first-child]:rounded-t-lg',
|
||||||
|
//select first in group
|
||||||
|
'[:not(&)_+&]:mt-5',
|
||||||
|
'[:not(&)_+&]:rounded-t-lg',
|
||||||
|
//select last in group
|
||||||
|
'[&:not(:has(+_&))]:mb-5',
|
||||||
|
'[&:not(:has(+_&))]:rounded-b-lg',
|
||||||
|
'[&:not(:has(+_&))]:border-b',
|
||||||
|
/* '[&:not(:has(+_&))]:shadow-1xs', */
|
||||||
|
|
||||||
|
'dark:border-light-2/[0.06]',
|
||||||
|
'dark:from-dark-2',
|
||||||
|
'dark:to-dark-2',
|
||||||
|
'dark:shadow-none',
|
||||||
|
|
||||||
|
'group open:dark:to-dark-2/8',
|
||||||
|
'group open:to-light-1/6',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import ChevronRight from '@geist-ui/icons/chevronRight';
|
|
||||||
import IconHash from '@geist-ui/icons/hash';
|
|
||||||
import { DocumentBlockExpandable } from '@gitbook/api';
|
import { DocumentBlockExpandable } from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
|
|
||||||
import { getNodeFragmentByType } from '@/lib/document';
|
import { getNodeFragmentByType } from '@/lib/document';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { Details } from './Details';
|
||||||
import { BlockProps } from '../Block';
|
import { BlockProps } from '../Block';
|
||||||
import { Blocks } from '../Blocks';
|
import { Blocks } from '../Blocks';
|
||||||
import { Inlines } from '../Inlines';
|
import { Inlines } from '../Inlines';
|
||||||
@@ -25,42 +25,7 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
|||||||
id = context.getId ? context.getId(id) : id;
|
id = context.getId ? context.getId(id) : id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details
|
<Details id={id} open={context.mode === 'print'} className={style}>
|
||||||
id={id}
|
|
||||||
open={context.mode === 'print'}
|
|
||||||
className={tcls(
|
|
||||||
style,
|
|
||||||
'group/expandable',
|
|
||||||
'shadow-dark/1',
|
|
||||||
'bg-gradient-to-t',
|
|
||||||
'from-light-1',
|
|
||||||
'to-light-1',
|
|
||||||
'border',
|
|
||||||
'border-b-0',
|
|
||||||
'border-dark-3/3',
|
|
||||||
//all
|
|
||||||
'[&]:mt-[0px]',
|
|
||||||
//select first child
|
|
||||||
'[&:first-child]:mt-5',
|
|
||||||
'[&:first-child]:rounded-t-lg',
|
|
||||||
//select first in group
|
|
||||||
'[:not(&)_+&]:mt-5',
|
|
||||||
'[:not(&)_+&]:rounded-t-lg',
|
|
||||||
//select last in group
|
|
||||||
'[&:not(:has(+_&))]:mb-5',
|
|
||||||
'[&:not(:has(+_&))]:rounded-b-lg',
|
|
||||||
'[&:not(:has(+_&))]:border-b',
|
|
||||||
/* '[&:not(:has(+_&))]:shadow-1xs', */
|
|
||||||
|
|
||||||
'dark:border-light-2/[0.06]',
|
|
||||||
'dark:from-dark-2',
|
|
||||||
'dark:to-dark-2',
|
|
||||||
'dark:shadow-none',
|
|
||||||
|
|
||||||
'group open:dark:to-dark-2/8',
|
|
||||||
'group open:to-light-1/6',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<summary
|
<summary
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'cursor-pointer',
|
'cursor-pointer',
|
||||||
@@ -78,11 +43,11 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
|||||||
'[&::-webkit-details-marker]:hidden',
|
'[&::-webkit-details-marker]:hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChevronRight
|
<Icon
|
||||||
|
icon="chevron-right"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'inline-block',
|
'inline-block',
|
||||||
'w-4',
|
'size-3',
|
||||||
'h-4',
|
|
||||||
'mr-2',
|
'mr-2',
|
||||||
'mb-1',
|
'mb-1',
|
||||||
'transition-transform',
|
'transition-transform',
|
||||||
@@ -111,15 +76,15 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
|||||||
'dark:ring-0',
|
'dark:ring-0',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconHash
|
<Icon
|
||||||
|
icon="hashtag"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'inline-block',
|
'inline-block',
|
||||||
'w-4',
|
'size-3',
|
||||||
'h-4',
|
|
||||||
'transition-colors',
|
'transition-colors',
|
||||||
'stroke-transparent',
|
'text-transparent',
|
||||||
'group-hover:stroke-dark/6',
|
'group-hover:text-dark/6',
|
||||||
'dark:group-hover:stroke-light/5',
|
'dark:group-hover:text-light/5',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -129,8 +94,8 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
|
|||||||
document={document}
|
document={document}
|
||||||
ancestorBlocks={[...ancestorBlocks, block]}
|
ancestorBlocks={[...ancestorBlocks, block]}
|
||||||
context={context}
|
context={context}
|
||||||
style={['px-10', 'pb-5', 'space-y-3']}
|
style={['px-10', 'pb-5', 'space-y-4']}
|
||||||
/>
|
/>
|
||||||
</details>
|
</Details>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,11 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
|
|||||||
'dark:border-light/2',
|
'dark:border-light/2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={tcls('*:w-5', '*:h-5', '*:stroke-primary')}>
|
<div>
|
||||||
<FileIcon contentType={contentType} />
|
<FileIcon
|
||||||
|
contentType={contentType}
|
||||||
|
className={tcls('size-5', 'text-primary')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import IconDownload from '@geist-ui/icons/download';
|
import { Icon } from '@gitbook/icons';
|
||||||
import IconFileText from '@geist-ui/icons/fileText';
|
|
||||||
import IconImage from '@geist-ui/icons/image';
|
|
||||||
import IconPaperClip from '@geist-ui/icons/paperclip';
|
|
||||||
|
|
||||||
import { SimplifiedFileType } from '@/lib/files';
|
import { SimplifiedFileType } from '@/lib/files';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render an appropriate icon for a file.
|
* Render an appropriate icon for a file.
|
||||||
*/
|
*/
|
||||||
export function FileIcon(props: { contentType: SimplifiedFileType | null }) {
|
export function FileIcon(props: { contentType: SimplifiedFileType | null; className: string }) {
|
||||||
const { contentType } = props;
|
const { contentType, className } = props;
|
||||||
|
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case 'pdf':
|
case 'pdf':
|
||||||
return <IconFileText />;
|
return <Icon icon="file-pdf" className={className} />;
|
||||||
case 'image':
|
case 'image':
|
||||||
return <IconImage />;
|
return <Icon icon="file-image" className={className} />;
|
||||||
case 'archive':
|
case 'archive':
|
||||||
return <IconPaperClip />;
|
return <Icon icon="file-archive" className={className} />;
|
||||||
default:
|
default:
|
||||||
return <IconDownload />;
|
return <Icon icon="file-download" className={className} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import IconHash from '@geist-ui/icons/hash';
|
|
||||||
import { DocumentBlockHeading } from '@gitbook/api';
|
import { DocumentBlockHeading } from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
@@ -48,14 +48,15 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
|
|||||||
textStyle.lineHeight,
|
textStyle.lineHeight,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconHash
|
<Icon
|
||||||
|
icon="hashtag"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'w-3.5',
|
'w-3.5',
|
||||||
'h-[1lh]',
|
'h-[1lh]',
|
||||||
'transition-colors',
|
'transition-colors',
|
||||||
'stroke-transparent',
|
'text-transparent',
|
||||||
'group-hover:stroke-dark/6',
|
'group-hover:text-dark/6',
|
||||||
'dark:group-hover:stroke-light/5',
|
'dark:group-hover:text-light/5',
|
||||||
'lg:w-4',
|
'lg:w-4',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DocumentBlockHint } from '@gitbook/api';
|
import { DocumentBlockHint } from '@gitbook/api';
|
||||||
|
import { Icon, IconName } from '@gitbook/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ClassValue, tcls } from '@/lib/tailwind';
|
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||||
@@ -9,9 +10,8 @@ import { getBlockTextStyle } from './spacing';
|
|||||||
|
|
||||||
export function Hint(props: BlockProps<DocumentBlockHint>) {
|
export function Hint(props: BlockProps<DocumentBlockHint>) {
|
||||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||||
const hintStyle = HINT_STYLES[block.data.style];
|
const hintStyle = HINT_STYLES[block.data.style] ?? HINT_STYLES.info;
|
||||||
const firstLine = getBlockTextStyle(block.nodes[0]);
|
const firstLine = getBlockTextStyle(block.nodes[0]);
|
||||||
const RenderIcon = HINT_STYLES[block.data.style].icon;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -34,17 +34,17 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
|
|||||||
'pr-3',
|
'pr-3',
|
||||||
'mt-0.5',
|
'mt-0.5',
|
||||||
firstLine.lineHeight,
|
firstLine.lineHeight,
|
||||||
hintStyle.anchorColor,
|
hintStyle.iconColor,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RenderIcon />
|
<Icon icon={hintStyle.icon} className={tcls('size-4')} />
|
||||||
</div>
|
</div>
|
||||||
<Blocks
|
<Blocks
|
||||||
{...contextProps}
|
{...contextProps}
|
||||||
ancestorBlocks={[...ancestorBlocks, block]}
|
ancestorBlocks={[...ancestorBlocks, block]}
|
||||||
nodes={block.nodes}
|
nodes={block.nodes}
|
||||||
blockStyle={tcls(
|
blockStyle={tcls(
|
||||||
hintStyle.anchorColor,
|
hintStyle.bodyColor,
|
||||||
// render hash icon on the other side of the heading
|
// render hash icon on the other side of the heading
|
||||||
'flip-heading-hash',
|
'flip-heading-hash',
|
||||||
)}
|
)}
|
||||||
@@ -57,14 +57,16 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
|
|||||||
|
|
||||||
const HINT_STYLES: {
|
const HINT_STYLES: {
|
||||||
[style in DocumentBlockHint['data']['style']]: {
|
[style in DocumentBlockHint['data']['style']]: {
|
||||||
icon: () => React.ReactNode;
|
icon: IconName;
|
||||||
anchorColor: ClassValue;
|
iconColor: ClassValue;
|
||||||
|
bodyColor: ClassValue;
|
||||||
style: ClassValue;
|
style: ClassValue;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
info: {
|
info: {
|
||||||
icon: InfoIcon,
|
icon: 'circle-info',
|
||||||
anchorColor: [
|
iconColor: ['text-periwinkle-700', 'dark:text-periwinkle-400'],
|
||||||
|
bodyColor: [
|
||||||
'[&>a]:text-periwinkle-700',
|
'[&>a]:text-periwinkle-700',
|
||||||
'[&>a:hover]:text-periwinkle-800',
|
'[&>a:hover]:text-periwinkle-800',
|
||||||
'[&>code]:bg-periwinkle-700/4',
|
'[&>code]:bg-periwinkle-700/4',
|
||||||
@@ -72,8 +74,6 @@ const HINT_STYLES: {
|
|||||||
'[&>code]:shadow-none',
|
'[&>code]:shadow-none',
|
||||||
|
|
||||||
'text-periwinkle-900',
|
'text-periwinkle-900',
|
||||||
'fill-periwinkle-700',
|
|
||||||
'stroke-periwinkle-700',
|
|
||||||
|
|
||||||
'dark:text-periwinkle-200',
|
'dark:text-periwinkle-200',
|
||||||
'dark:[&>a]:text-periwinkle',
|
'dark:[&>a]:text-periwinkle',
|
||||||
@@ -82,8 +82,6 @@ const HINT_STYLES: {
|
|||||||
'dark:[&>code]:text-inherit',
|
'dark:[&>code]:text-inherit',
|
||||||
'decoration-periwinkle-700/6',
|
'decoration-periwinkle-700/6',
|
||||||
'dark:decoration-periwinkle/6',
|
'dark:decoration-periwinkle/6',
|
||||||
'dark:fill-periwinkle-400',
|
|
||||||
'dark:stroke-periwinkle-400',
|
|
||||||
],
|
],
|
||||||
style: [
|
style: [
|
||||||
'bg-gradient-to-b',
|
'bg-gradient-to-b',
|
||||||
@@ -98,8 +96,9 @@ const HINT_STYLES: {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
icon: WarningIcon,
|
icon: 'circle-exclamation',
|
||||||
anchorColor: [
|
iconColor: ['text-yellow-800', 'dark:text-yellow'],
|
||||||
|
bodyColor: [
|
||||||
'[&>a]:text-yellow-700',
|
'[&>a]:text-yellow-700',
|
||||||
'[&>a:hover]:text-yellow-800',
|
'[&>a:hover]:text-yellow-800',
|
||||||
'[&>code]:bg-yellow-600/5',
|
'[&>code]:bg-yellow-600/5',
|
||||||
@@ -108,7 +107,6 @@ const HINT_STYLES: {
|
|||||||
|
|
||||||
'text-yellow-900',
|
'text-yellow-900',
|
||||||
|
|
||||||
'fill-yellow-800',
|
|
||||||
'dark:text-yellow-200',
|
'dark:text-yellow-200',
|
||||||
'dark:[&>a]:text-yellow',
|
'dark:[&>a]:text-yellow',
|
||||||
'dark:[&>a:hover]:text-yellow-600',
|
'dark:[&>a:hover]:text-yellow-600',
|
||||||
@@ -116,7 +114,6 @@ const HINT_STYLES: {
|
|||||||
'dark:[&>code]:text-inherit',
|
'dark:[&>code]:text-inherit',
|
||||||
'decoration-yellow-700/6',
|
'decoration-yellow-700/6',
|
||||||
'dark:decoration-yellow/6',
|
'dark:decoration-yellow/6',
|
||||||
'dark:fill-yellow',
|
|
||||||
],
|
],
|
||||||
style: [
|
style: [
|
||||||
'bg-gradient-to-b',
|
'bg-gradient-to-b',
|
||||||
@@ -131,8 +128,9 @@ const HINT_STYLES: {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
danger: {
|
danger: {
|
||||||
icon: DangerIcon,
|
icon: 'triangle-exclamation',
|
||||||
anchorColor: [
|
iconColor: ['text-pomegranate-700', 'dark:text-pomegranate-400'],
|
||||||
|
bodyColor: [
|
||||||
'[&>a]:text-pomegranate-600/9',
|
'[&>a]:text-pomegranate-600/9',
|
||||||
'[&>a:hover]:text-pomegranate-800',
|
'[&>a:hover]:text-pomegranate-800',
|
||||||
'[&>code]:bg-pomegranate-600/4',
|
'[&>code]:bg-pomegranate-600/4',
|
||||||
@@ -141,7 +139,6 @@ const HINT_STYLES: {
|
|||||||
|
|
||||||
'text-pomegranate-900',
|
'text-pomegranate-900',
|
||||||
|
|
||||||
'fill-pomegranate-700',
|
|
||||||
'dark:text-pomegranate-100',
|
'dark:text-pomegranate-100',
|
||||||
'dark:[&>a]:text-pomegranate',
|
'dark:[&>a]:text-pomegranate',
|
||||||
'dark:[&>a:hover]:text-pomegranate-600',
|
'dark:[&>a:hover]:text-pomegranate-600',
|
||||||
@@ -149,7 +146,6 @@ const HINT_STYLES: {
|
|||||||
'dark:[&>code]:text-inherit',
|
'dark:[&>code]:text-inherit',
|
||||||
'decoration-pomegranate-600/6',
|
'decoration-pomegranate-600/6',
|
||||||
'dark:decoration-pomegranate/6',
|
'dark:decoration-pomegranate/6',
|
||||||
'dark:fill-pomegranate-400',
|
|
||||||
],
|
],
|
||||||
style: [
|
style: [
|
||||||
'bg-gradient-to-b',
|
'bg-gradient-to-b',
|
||||||
@@ -164,8 +160,9 @@ const HINT_STYLES: {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
icon: SuccessIcon,
|
icon: 'circle-check',
|
||||||
anchorColor: [
|
iconColor: ['text-teal-700', 'dark:text-teal-400'],
|
||||||
|
bodyColor: [
|
||||||
'[&>a]:text-teal-600',
|
'[&>a]:text-teal-600',
|
||||||
'[&>a:hover]:text-teal-800',
|
'[&>a:hover]:text-teal-800',
|
||||||
'[&>code]:bg-teal-600/4',
|
'[&>code]:bg-teal-600/4',
|
||||||
@@ -173,8 +170,6 @@ const HINT_STYLES: {
|
|||||||
'[&>code]:shadow-none',
|
'[&>code]:shadow-none',
|
||||||
|
|
||||||
'text-teal-800',
|
'text-teal-800',
|
||||||
'fill-teal-700',
|
|
||||||
'stroke-teal-700',
|
|
||||||
|
|
||||||
'dark:text-teal-100',
|
'dark:text-teal-100',
|
||||||
'dark:[&>a]:text-teal-400',
|
'dark:[&>a]:text-teal-400',
|
||||||
@@ -183,8 +178,6 @@ const HINT_STYLES: {
|
|||||||
'dark:[&>code]:text-inherit',
|
'dark:[&>code]:text-inherit',
|
||||||
'decoration-teal/6',
|
'decoration-teal/6',
|
||||||
'dark:decoration-teal/6',
|
'dark:decoration-teal/6',
|
||||||
'dark:fill-teal-400',
|
|
||||||
'dark:stroke-teal-300',
|
|
||||||
],
|
],
|
||||||
style: [
|
style: [
|
||||||
'bg-gradient-to-b',
|
'bg-gradient-to-b',
|
||||||
@@ -199,131 +192,3 @@ const HINT_STYLES: {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function InfoIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={tcls('overflow-visible')}
|
|
||||||
>
|
|
||||||
<circle cx="8" cy="4.5" r="1.5" fill="inherit" fillOpacity="1" stroke="none" />
|
|
||||||
<circle
|
|
||||||
cx="8"
|
|
||||||
cy="4.5"
|
|
||||||
r="1.5"
|
|
||||||
fill="inherit"
|
|
||||||
fillOpacity="0.2"
|
|
||||||
stroke="none"
|
|
||||||
className={tcls(
|
|
||||||
'animate-[pingAlt_8s_ease_infinite_forwards]',
|
|
||||||
'origin-[8px_4.5px]',
|
|
||||||
'w-[4px]',
|
|
||||||
'h-[4px]',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="7"
|
|
||||||
y="7"
|
|
||||||
width="2"
|
|
||||||
rx="1"
|
|
||||||
height="6"
|
|
||||||
fill="inherit"
|
|
||||||
fillOpacity="1"
|
|
||||||
stroke="none"
|
|
||||||
/>
|
|
||||||
<circle cx="8" cy="8" r="8" fill="inherit" fillOpacity="0.16" stroke="none" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DangerIcon() {
|
|
||||||
return (
|
|
||||||
<svg width="18" height="16" viewBox="0 0 18 16" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M7.2546 1C8.03034 -0.333333 9.96967 -0.333333 10.7454 1L17.727 13C18.5027 14.3333 17.533 16 15.9816 16H2.01842C0.466953 16 -0.502711 14.3333 0.27302 13L7.2546 1Z"
|
|
||||||
fill="inherit"
|
|
||||||
fillOpacity="0.16"
|
|
||||||
stroke="none"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="8"
|
|
||||||
y="4"
|
|
||||||
width="2"
|
|
||||||
height="6"
|
|
||||||
rx="1"
|
|
||||||
fill="inherit"
|
|
||||||
fillOpacity="0.92"
|
|
||||||
stroke="none"
|
|
||||||
className={tcls(
|
|
||||||
'animate-[wag_4s_ease_infinite_forwards]',
|
|
||||||
'[animation-composition:accumulate]',
|
|
||||||
'origin-[9px_12.5px]',
|
|
||||||
'will-change-transform',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<circle cx="9" cy="12.5" r="1.5" fill="inherit" fillOpacity="0.92" stroke="none" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WarningIcon() {
|
|
||||||
return (
|
|
||||||
<svg width="16" height="18" viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M6.9736 0.277489C7.60874 -0.0924963 8.39126 -0.0924962 9.0264 0.277489L14.9736 3.74189C15.6087 4.11187 16 4.79563 16 5.5356V12.4644C16 13.2044 15.6087 13.8881 14.9736 14.2581L9.0264 17.7225C8.39126 18.0925 7.60874 18.0925 6.9736 17.7225L1.0264 14.2581C0.391261 13.8881 0 13.2044 0 12.4644V5.5356C0 4.79563 0.391262 4.11187 1.0264 3.74189L6.9736 0.277489Z"
|
|
||||||
fill="inherit"
|
|
||||||
fillOpacity="0.16"
|
|
||||||
stroke="none"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="7"
|
|
||||||
y="4"
|
|
||||||
width="2"
|
|
||||||
height="6"
|
|
||||||
rx="1"
|
|
||||||
fill="inherit"
|
|
||||||
fillOpacity="0.8"
|
|
||||||
stroke="none"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="8"
|
|
||||||
cy="12.5"
|
|
||||||
r="1.5"
|
|
||||||
fill="inherit"
|
|
||||||
fillOpacity="1"
|
|
||||||
stroke="none"
|
|
||||||
className={tcls('animate-[pulse_1s_ease_infinite_forwards]')}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SuccessIcon() {
|
|
||||||
return (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="16" height="16" rx="3" fill="inherit" fillOpacity="0.16" stroke="none" />
|
|
||||||
<path
|
|
||||||
className={tcls('animate-[stroke_8s_ease_infinite_forwards]')}
|
|
||||||
d="M4 8.5L6.5 11L12 5.5"
|
|
||||||
stroke="inherit"
|
|
||||||
fill="none"
|
|
||||||
strokeOpacity="0.8"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
pathLength="100"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M4 8.5L6.5 11L12 5.5"
|
|
||||||
stroke="inherit"
|
|
||||||
fill="none"
|
|
||||||
strokeOpacity="0.16"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
pathLength="100"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
import {
|
|
||||||
Edit,
|
|
||||||
Github,
|
|
||||||
Gitlab,
|
|
||||||
Maximize,
|
|
||||||
X,
|
|
||||||
Mail,
|
|
||||||
Settings,
|
|
||||||
Search,
|
|
||||||
Delete,
|
|
||||||
Star,
|
|
||||||
AlertTriangle,
|
|
||||||
Link,
|
|
||||||
Eye,
|
|
||||||
Lock,
|
|
||||||
} from '@geist-ui/icons';
|
|
||||||
import { ContentKitContext, DocumentBlockIntegration } from '@gitbook/api';
|
import { ContentKitContext, DocumentBlockIntegration } from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
import { ContentKit, ContentKitOutput, ContentKitServerContext } from '@gitbook/react-contentkit';
|
import { ContentKit, ContentKitOutput, ContentKitServerContext } from '@gitbook/react-contentkit';
|
||||||
|
|
||||||
import { ignoreAPIError, renderIntegrationUi } from '@/lib/api';
|
import { ignoreAPIError, renderIntegrationUi } from '@/lib/api';
|
||||||
|
import { INTEGRATIONS_HOST } from '@/lib/csp';
|
||||||
import { parseMarkdown } from '@/lib/markdown';
|
import { parseMarkdown } from '@/lib/markdown';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
@@ -27,21 +13,21 @@ import { PlainCodeBlock } from '../CodeBlock';
|
|||||||
|
|
||||||
const outputContext: ContentKitServerContext = {
|
const outputContext: ContentKitServerContext = {
|
||||||
icons: {
|
icons: {
|
||||||
maximize: Maximize,
|
maximize: (props) => <Icon icon="maximize" {...props} />,
|
||||||
edit: Edit,
|
edit: (props) => <Icon icon="edit" {...props} />,
|
||||||
github: Github,
|
github: (props) => <Icon icon="github" {...props} />,
|
||||||
gitlab: Gitlab,
|
gitlab: (props) => <Icon icon="gitlab" {...props} />,
|
||||||
close: X,
|
close: (props) => <Icon icon="x" {...props} />,
|
||||||
email: Mail,
|
email: (props) => <Icon icon="envelope" {...props} />,
|
||||||
settings: Settings,
|
settings: (props) => <Icon icon="gear" {...props} />,
|
||||||
search: Search,
|
search: (props) => <Icon icon="magnifying-glass" {...props} />,
|
||||||
delete: Delete,
|
delete: (props) => <Icon icon="trash" {...props} />,
|
||||||
star: Star,
|
star: (props) => <Icon icon="star" {...props} />,
|
||||||
warning: AlertTriangle,
|
warning: (props) => <Icon icon="triangle-exclamation" {...props} />,
|
||||||
link: Link,
|
link: (props) => <Icon icon="link" {...props} />,
|
||||||
'link-external': Link,
|
'link-external': (props) => <Icon icon="arrow-up-right-from-square" {...props} />,
|
||||||
eye: Eye,
|
eye: (props) => <Icon icon="eye" {...props} />,
|
||||||
lock: Lock,
|
lock: (props) => <Icon icon="lock" {...props} />,
|
||||||
},
|
},
|
||||||
codeBlock: (props) => {
|
codeBlock: (props) => {
|
||||||
return <PlainCodeBlock code={props.code} syntax={props.syntax} />;
|
return <PlainCodeBlock code={props.code} syntax={props.syntax} />;
|
||||||
@@ -84,7 +70,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
|||||||
return (
|
return (
|
||||||
<div className={tcls(style)}>
|
<div className={tcls(style)}>
|
||||||
<ContentKit
|
<ContentKit
|
||||||
security={{ firstPartyDomains: ['integrations.gitbook.com'] }}
|
security={{ firstPartyDomains: [INTEGRATIONS_HOST] }}
|
||||||
initialInput={initialInput}
|
initialInput={initialInput}
|
||||||
initialOutput={initialOutput}
|
initialOutput={initialOutput}
|
||||||
render={async (request) => {
|
render={async (request) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { tcls } from '@/lib/tailwind';
|
|||||||
import { BlockProps } from './Block';
|
import { BlockProps } from './Block';
|
||||||
import { InlineProps } from './Inline';
|
import { InlineProps } from './Inline';
|
||||||
|
|
||||||
const mathJaxUrl = getStaticFileURL('mathjax@3.2.2/tex-chtml.js');
|
const assetsUrl = getStaticFileURL('math');
|
||||||
|
|
||||||
export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
|
export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
|
||||||
const { block, style } = props;
|
const { block, style } = props;
|
||||||
@@ -17,7 +17,7 @@ export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
|
|||||||
formula={block.data.formula}
|
formula={block.data.formula}
|
||||||
inline={false}
|
inline={false}
|
||||||
className={tcls(style, 'overflow-x-auto')}
|
className={tcls(style, 'overflow-x-auto')}
|
||||||
mathJaxUrl={mathJaxUrl}
|
assetsUrl={assetsUrl}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,5 @@ export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
|
|||||||
export async function InlineMath(props: InlineProps<DocumentInlineMath>) {
|
export async function InlineMath(props: InlineProps<DocumentInlineMath>) {
|
||||||
const { inline } = props;
|
const { inline } = props;
|
||||||
|
|
||||||
return <MathFormula formula={inline.data.formula} inline={true} mathJaxUrl={mathJaxUrl} />;
|
return <MathFormula formula={inline.data.formula} inline={true} assetsUrl={assetsUrl} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChevronDown, ChevronRight } from '@geist-ui/icons';
|
|
||||||
import { DocumentBlockSwagger } from '@gitbook/api';
|
import { DocumentBlockSwagger } from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
import { OpenAPIOperation } from '@gitbook/react-openapi';
|
import { OpenAPIOperation } from '@gitbook/react-openapi';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@@ -20,11 +20,6 @@ export async function OpenAPI(props: BlockProps<DocumentBlockSwagger>) {
|
|||||||
const { block, style } = props;
|
const { block, style } = props;
|
||||||
return (
|
return (
|
||||||
<div className={tcls('w-full', 'flex', 'flex-row', style, 'max-w-full')}>
|
<div className={tcls('w-full', 'flex', 'flex-row', style, 'max-w-full')}>
|
||||||
{/*
|
|
||||||
Invisible span with the ID to correctly identify the active section in the aside navigation.
|
|
||||||
We don't use the full <div> because it can be longer than the viewport and will not work well with IntersectionObserver.
|
|
||||||
*/}
|
|
||||||
<span id={block.meta?.id} />
|
|
||||||
<React.Suspense fallback={<OpenAPIFallback />}>
|
<React.Suspense fallback={<OpenAPIFallback />}>
|
||||||
<OpenAPIBody {...props} />
|
<OpenAPIBody {...props} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
@@ -55,11 +50,12 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
|
|||||||
data={data}
|
data={data}
|
||||||
context={{
|
context={{
|
||||||
icons: {
|
icons: {
|
||||||
chevronDown: <ChevronDown />,
|
chevronDown: <Icon icon="chevron-down" />,
|
||||||
chevronRight: <ChevronRight />,
|
chevronRight: <Icon icon="chevron-right" />,
|
||||||
},
|
},
|
||||||
CodeBlock: PlainCodeBlock,
|
CodeBlock: PlainCodeBlock,
|
||||||
defaultInteractiveOpened: context.mode === 'print',
|
defaultInteractiveOpened: context.mode === 'print',
|
||||||
|
id: block.meta?.id,
|
||||||
}}
|
}}
|
||||||
className="openapi-block"
|
className="openapi-block"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
.openapi-operation {
|
.openapi-operation {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-height: 600px;
|
||||||
@apply flex-1 flex flex-col gap-6;
|
@apply flex-1 flex flex-col gap-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +348,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-section-toggle svg {
|
.openapi-section-toggle svg {
|
||||||
@apply size-4;
|
@apply size-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Markdown */
|
/** Markdown */
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function Quote(props: BlockProps<DocumentBlockQuote>) {
|
|||||||
'border-dark/2',
|
'border-dark/2',
|
||||||
'dark:text-light/7',
|
'dark:text-light/7',
|
||||||
'dark:border-light/2',
|
'dark:border-light/2',
|
||||||
|
'space-y-4',
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import IconStar from '@geist-ui/icons/star';
|
|
||||||
import { ContentRef, DocumentBlockTable } from '@gitbook/api';
|
import { ContentRef, DocumentBlockTable } from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
import assertNever from 'assert-never';
|
import assertNever from 'assert-never';
|
||||||
|
|
||||||
import { Checkbox, Emoji } from '@/components/primitives';
|
import { Checkbox, Emoji } from '@/components/primitives';
|
||||||
@@ -54,12 +54,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
<>
|
<>
|
||||||
<span className={tcls('inline-flex', 'grid-area-1-1', 'gap-0.5')}>
|
<span className={tcls('inline-flex', 'grid-area-1-1', 'gap-0.5')}>
|
||||||
{Array.from({ length: max }).map((_, i) => (
|
{Array.from({ length: max }).map((_, i) => (
|
||||||
<IconStar
|
<Icon
|
||||||
key={i}
|
key={i}
|
||||||
|
icon="star"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'size-[15px]',
|
'size-[15px]',
|
||||||
'stroke-primary-700/5',
|
'text-primary-700/5',
|
||||||
'dark:stroke-primary-300/5',
|
'dark:text-primary-300/5',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -73,9 +74,10 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
className={tcls('inline-flex', 'grid-area-1-1', 'gap-0.5')}
|
className={tcls('inline-flex', 'grid-area-1-1', 'gap-0.5')}
|
||||||
>
|
>
|
||||||
{Array.from({ length: rating }).map((_, i) => (
|
{Array.from({ length: rating }).map((_, i) => (
|
||||||
<IconStar
|
<Icon
|
||||||
key={i}
|
key={i}
|
||||||
className={tcls('size-[15px]', 'stroke-primary')}
|
icon="star"
|
||||||
|
className={tcls('size-[15px]', 'text-primary')}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
@@ -157,7 +159,10 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
priority="lazy"
|
priority="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FileIcon contentType={contentType} />
|
<FileIcon
|
||||||
|
contentType={contentType}
|
||||||
|
className={tcls('size-4')}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{ref.text}
|
{ref.text}
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
@@ -169,13 +174,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
|||||||
const resolved = value
|
const resolved = value
|
||||||
? await context.resolveContentRef(value as ContentRef, {
|
? await context.resolveContentRef(value as ContentRef, {
|
||||||
resolveAnchorText: true,
|
resolveAnchorText: true,
|
||||||
|
iconStyle: ['mr-2', 'text-dark/6', 'dark:text-light/6'],
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
return (
|
return (
|
||||||
<Tag className={tcls('text-base', 'text-balance')}>
|
<Tag className={tcls('text-base', 'text-balance', 'flex', 'items-center')}>
|
||||||
{resolved && resolved.emoji ? (
|
{resolved?.icon ?? null}
|
||||||
<Emoji code={resolved.emoji} style={['mr-2']} />
|
|
||||||
) : null}
|
|
||||||
{resolved ? (
|
{resolved ? (
|
||||||
<StyledLink href={resolved.href}>{resolved.text}</StyledLink>
|
<StyledLink href={resolved.href}>{resolved.text}</StyledLink>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
|||||||
'z-[1]',
|
'z-[1]',
|
||||||
'sticky',
|
'sticky',
|
||||||
'left-[calc(100%-5rem)]',
|
'left-[calc(100%-5rem)]',
|
||||||
|
'pointer-events-none',
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
|
|||||||
ancestorBlocks={[...ancestorBlocks, block, tab]}
|
ancestorBlocks={[...ancestorBlocks, block, tab]}
|
||||||
context={context}
|
context={context}
|
||||||
blockStyle={tcls('flip-heading-hash')}
|
blockStyle={tcls('flip-heading-hash')}
|
||||||
style={tcls('w-full', 'space-y-6')}
|
style={tcls('w-full', 'space-y-4')}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getSpaceLanguage } from '@/intl/server';
|
|||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { HeaderLogo } from './HeaderLogo';
|
import { HeaderLogo } from './HeaderLogo';
|
||||||
|
import { SpacesDropdown } from './SpacesDropdown';
|
||||||
import { SearchButton } from '../Search';
|
import { SearchButton } from '../Search';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,7 +24,11 @@ export function CompactHeader(props: {
|
|||||||
spaces: Space[];
|
spaces: Space[];
|
||||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||||
}) {
|
}) {
|
||||||
const { space, parent, customization } = props;
|
const { space, spaces, parent, customization } = props;
|
||||||
|
|
||||||
|
const isMultiVariants =
|
||||||
|
parent?.object === 'collection' ||
|
||||||
|
(parent && parent.object === 'site' && spaces.length > 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -53,6 +58,11 @@ export function CompactHeader(props: {
|
|||||||
'justify-self-end',
|
'justify-self-end',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{isMultiVariants ? (
|
||||||
|
<div className={tcls('mb-2')}>
|
||||||
|
<SpacesDropdown space={space} spaces={spaces} buttonKind="bordered" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
<SearchButton>
|
<SearchButton>
|
||||||
<span className={tcls('flex-1')}>
|
<span className={tcls('flex-1')}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import IconChevronDown from '@geist-ui/icons/chevronDown';
|
import { Icon } from '@gitbook/icons';
|
||||||
import { DetailedHTMLProps, HTMLAttributes, useId } from 'react';
|
import { DetailedHTMLProps, HTMLAttributes, useId } from 'react';
|
||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { Link } from '../primitives';
|
import { Link } from '../primitives';
|
||||||
|
|
||||||
@@ -18,8 +18,10 @@ export function Dropdown<E extends HTMLElement>(props: {
|
|||||||
button: (buttonProps: DropdownButtonProps<E>) => React.ReactNode;
|
button: (buttonProps: DropdownButtonProps<E>) => React.ReactNode;
|
||||||
/** Content of the dropdown */
|
/** Content of the dropdown */
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
/** Custom styles */
|
||||||
|
className?: ClassValue;
|
||||||
}) {
|
}) {
|
||||||
const { button, children } = props;
|
const { button, children, className } = props;
|
||||||
const dropdownId = useId();
|
const dropdownId = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,6 +38,8 @@ export function Dropdown<E extends HTMLElement>(props: {
|
|||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
aria-labelledby={dropdownId}
|
aria-labelledby={dropdownId}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
|
'w-52',
|
||||||
|
'max-h-56',
|
||||||
'flex',
|
'flex',
|
||||||
'absolute',
|
'absolute',
|
||||||
'top-full',
|
'top-full',
|
||||||
@@ -47,13 +51,13 @@ export function Dropdown<E extends HTMLElement>(props: {
|
|||||||
'duration-1000',
|
'duration-1000',
|
||||||
'group-hover/dropdown:visible',
|
'group-hover/dropdown:visible',
|
||||||
'group-focus-within/dropdown:visible',
|
'group-focus-within/dropdown:visible',
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'mt-2',
|
'mt-2',
|
||||||
'w-52',
|
'w-full',
|
||||||
'max-h-56',
|
|
||||||
'bg-light',
|
'bg-light',
|
||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
'straight-corners:rounded-sm',
|
'straight-corners:rounded-sm',
|
||||||
@@ -80,11 +84,11 @@ export function Dropdown<E extends HTMLElement>(props: {
|
|||||||
*/
|
*/
|
||||||
export function DropdownChevron(props: {}) {
|
export function DropdownChevron(props: {}) {
|
||||||
return (
|
return (
|
||||||
<IconChevronDown
|
<Icon
|
||||||
|
icon="chevron-down"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'opacity-6',
|
'opacity-6',
|
||||||
'w-4',
|
'size-3',
|
||||||
'h-4',
|
|
||||||
'ms-1',
|
'ms-1',
|
||||||
'transition-transform',
|
'transition-transform',
|
||||||
'group-hover/dropdown:rotate-180',
|
'group-hover/dropdown:rotate-180',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { ContentRefContext } from '@/lib/references';
|
|||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { HeaderLink } from './HeaderLink';
|
import { HeaderLink } from './HeaderLink';
|
||||||
|
import { HeaderLinkMore } from './HeaderLinkMore';
|
||||||
import { HeaderLinks } from './HeaderLinks';
|
import { HeaderLinks } from './HeaderLinks';
|
||||||
import { HeaderLogo } from './HeaderLogo';
|
import { HeaderLogo } from './HeaderLogo';
|
||||||
import { SpacesDropdown } from './SpacesDropdown';
|
import { SpacesDropdown } from './SpacesDropdown';
|
||||||
@@ -90,6 +91,11 @@ export function Header(props: {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<HeaderLinkMore
|
||||||
|
label={t(getSpaceLanguage(customization), 'more')}
|
||||||
|
links={customization.header.links}
|
||||||
|
context={context}
|
||||||
|
/>
|
||||||
</HeaderLinks>
|
</HeaderLinks>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
@@ -110,11 +116,11 @@ export function Header(props: {
|
|||||||
'text-header-link/8',
|
'text-header-link/8',
|
||||||
'ring-header-background-200/5',
|
'ring-header-background-200/5',
|
||||||
'[&>span]:text-header-link/7',
|
'[&>span]:text-header-link/7',
|
||||||
'[&_svg]:stroke-header-link',
|
'[&_svg]:text-header-link',
|
||||||
'dark:bg-header-link-600/3',
|
'dark:bg-header-link-600/3',
|
||||||
'dark:ring-header-link-600/2',
|
'dark:ring-header-link-600/2',
|
||||||
'dark:text-header-link/7',
|
'dark:text-header-link/7',
|
||||||
'dark:[&_svg]:stroke-header-link/7',
|
'dark:[&_svg]:text-header-link/7',
|
||||||
]
|
]
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { CustomizationHeaderLink } from '@gitbook/api';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
||||||
|
|
||||||
|
import { Dropdown, DropdownChevron, DropdownMenu, DropdownMenuItem } from './Dropdown';
|
||||||
|
import styles from './headerLinks.module.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dropdown menu for header links hidden at small screen size.
|
||||||
|
*/
|
||||||
|
export function HeaderLinkMore(props: {
|
||||||
|
label: React.ReactNode;
|
||||||
|
links: CustomizationHeaderLink[];
|
||||||
|
context: ContentRefContext;
|
||||||
|
}) {
|
||||||
|
const { label, links, context } = props;
|
||||||
|
|
||||||
|
const renderButton = () => (
|
||||||
|
<button className="px-1">
|
||||||
|
<span className="sr-only">{label}</span>
|
||||||
|
<DropdownChevron />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.linkEllipsis} items-center`}>
|
||||||
|
<Dropdown button={renderButton} className="-translate-x-48 md:translate-x-0">
|
||||||
|
<DropdownMenu>
|
||||||
|
{links.map((link, index) => (
|
||||||
|
<MoreMenuLink key={index} link={link} context={context} />
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function MoreMenuLink(props: { context: ContentRefContext; link: CustomizationHeaderLink }) {
|
||||||
|
const { context, link } = props;
|
||||||
|
|
||||||
|
const target = await resolveContentRef(link.to, context);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DropdownMenuItem href={target.href}>{link.title}</DropdownMenuItem>;
|
||||||
|
}
|
||||||
@@ -8,13 +8,13 @@ import {
|
|||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
|
|
||||||
import { HeaderMobileMenu } from '@/components/Header/HeaderMobileMenu';
|
import { HeaderMobileMenu } from '@/components/Header/HeaderMobileMenu';
|
||||||
import { LogoIcon } from '@/components/icons';
|
|
||||||
import { Image } from '@/components/utils';
|
import { Image } from '@/components/utils';
|
||||||
import { absoluteHref } from '@/lib/links';
|
import { absoluteHref } from '@/lib/links';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import { getContentTitle } from '@/lib/utils';
|
import { getContentTitle } from '@/lib/utils';
|
||||||
|
|
||||||
import { Link } from '../primitives';
|
import { Link } from '../primitives';
|
||||||
|
import { SpaceIcon } from '../Space/SpaceIcon';
|
||||||
|
|
||||||
interface HeaderLogoProps {
|
interface HeaderLogoProps {
|
||||||
parent: Site | Collection | null;
|
parent: Site | Collection | null;
|
||||||
@@ -30,7 +30,7 @@ export function HeaderLogo(props: HeaderLogoProps) {
|
|||||||
const { customization } = props;
|
const { customization } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={tcls('flex', 'flex-row', 'gap-3')}>
|
<div className={tcls('flex', 'flex-row', 'gap-2')}>
|
||||||
<HeaderMobileMenu
|
<HeaderMobileMenu
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'lg:hidden',
|
'lg:hidden',
|
||||||
@@ -100,7 +100,7 @@ function LogoFallback(props: HeaderLogoProps) {
|
|||||||
const customEmoji = 'emoji' in customization.favicon ? customization.favicon.emoji : undefined;
|
const customEmoji = 'emoji' in customization.favicon ? customization.favicon.emoji : undefined;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LogoIcon
|
<SpaceIcon
|
||||||
icon={customIcon}
|
icon={customIcon}
|
||||||
emoji={customEmoji}
|
emoji={customEmoji}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -108,7 +108,7 @@ function LogoFallback(props: HeaderLogoProps) {
|
|||||||
style={['object-contain', 'size-8']}
|
style={['object-contain', 'size-8']}
|
||||||
fetchPriority="high"
|
fetchPriority="high"
|
||||||
/>
|
/>
|
||||||
<h1
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'text-pretty',
|
'text-pretty',
|
||||||
'line-clamp-3',
|
'line-clamp-3',
|
||||||
@@ -126,7 +126,7 @@ function LogoFallback(props: HeaderLogoProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getContentTitle(space, customization, parent)}
|
{getContentTitle(space, customization, parent)}
|
||||||
</h1>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { IconMenu } from '@/components/icons/IconMenu';
|
|
||||||
import { useLanguage, tString } from '@/intl/client';
|
import { useLanguage, tString } from '@/intl/client';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
@@ -53,19 +53,20 @@ export function HeaderMobileMenu(props: Partial<React.ButtonHTMLAttributes<HTMLB
|
|||||||
{...props}
|
{...props}
|
||||||
aria-label={tString(language, 'table_of_contents_button_label')}
|
aria-label={tString(language, 'table_of_contents_button_label')}
|
||||||
onClick={toggleNavigation}
|
onClick={toggleNavigation}
|
||||||
|
className={tcls(
|
||||||
|
'flex',
|
||||||
|
'flex-row',
|
||||||
|
'items-center',
|
||||||
|
'hover:bg-dark/3',
|
||||||
|
'py-1',
|
||||||
|
'px-2',
|
||||||
|
'dark:hover:bg-light/2',
|
||||||
|
'rounded',
|
||||||
|
'straight-corners:rounded-sm',
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<IconMenu
|
<Icon icon="bars" className={tcls('size-4', 'text-inherit')} />
|
||||||
className={tcls(
|
|
||||||
'w-7',
|
|
||||||
'h-7',
|
|
||||||
'rounded',
|
|
||||||
'straight-corners:rounded-sm',
|
|
||||||
'text-inherit',
|
|
||||||
'hover:bg-dark/3',
|
|
||||||
'p-[0.25rem]',
|
|
||||||
'dark:hover:bg-light/2',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ import { tcls } from '@/lib/tailwind';
|
|||||||
import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown';
|
import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown';
|
||||||
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
|
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
|
||||||
|
|
||||||
export function SpacesDropdown(props: { space: Space; spaces: Space[] }) {
|
export function SpacesDropdown(props: {
|
||||||
const { space, spaces } = props;
|
space: Space;
|
||||||
|
spaces: Space[];
|
||||||
|
buttonKind?: 'default' | 'bordered';
|
||||||
|
}) {
|
||||||
|
const { space, spaces, buttonKind = 'default' } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
className={buttonKind === 'bordered' ? tcls('w-full') : undefined}
|
||||||
button={(buttonProps) => (
|
button={(buttonProps) => (
|
||||||
<div
|
<div
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
@@ -22,6 +27,23 @@ export function SpacesDropdown(props: { space: Space; spaces: Space[] }) {
|
|||||||
'px-3',
|
'px-3',
|
||||||
'py-1.5',
|
'py-1.5',
|
||||||
'text-header-link-500',
|
'text-header-link-500',
|
||||||
|
buttonKind === 'bordered'
|
||||||
|
? [
|
||||||
|
'ring-1',
|
||||||
|
'ring-inset',
|
||||||
|
'ring-dark/2',
|
||||||
|
'pointer-events-auto',
|
||||||
|
'justify-between',
|
||||||
|
'bg-light',
|
||||||
|
'dark:bg-dark',
|
||||||
|
'rounded-lg',
|
||||||
|
'straight-corners:rounded-none',
|
||||||
|
'lg:ring-0',
|
||||||
|
'border',
|
||||||
|
'border-dark/2',
|
||||||
|
'dark:border-light/2',
|
||||||
|
]
|
||||||
|
: [],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{space.title}
|
{space.title}
|
||||||
|
|||||||
@@ -4,36 +4,83 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.containerHeaderlinks > * {
|
.containerHeaderlinks > * {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.linkEllipsis {
|
||||||
display: none;
|
display: none;
|
||||||
|
& div > a {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@container headerlinks ( width > 150px ) {
|
@container headerlinks ( width < 150px ) {
|
||||||
.containerHeaderlinks > *:nth-child(1) {
|
.containerHeaderlinks > :nth-child(n + 1) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.containerHeaderlinks > :nth-child(n + 1) ~ .linkEllipsis {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
& div > a:nth-of-type(n + 1) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@container headerlinks ( width > 300px ) {
|
@container headerlinks ( width < 300px ) {
|
||||||
.containerHeaderlinks > *:nth-child(2) {
|
.containerHeaderlinks > :nth-child(n + 2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.containerHeaderlinks > :nth-child(n + 2) ~ .linkEllipsis {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
& div > a:nth-of-type(n + 2) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@container headerlinks ( width > 450px ) {
|
@container headerlinks ( width < 450px ) {
|
||||||
.containerHeaderlinks > *:nth-child(3) {
|
.containerHeaderlinks > :nth-child(n + 3) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.containerHeaderlinks > :nth-child(n + 3) ~ .linkEllipsis {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
& div > a:nth-of-type(n + 3) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@container headerlinks ( width > 600px ) {
|
@container headerlinks ( width < 600px ) {
|
||||||
.containerHeaderlinks > *:nth-child(4) {
|
.containerHeaderlinks > :nth-child(n + 4) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.containerHeaderlinks > :nth-child(n + 4) ~ .linkEllipsis {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
& div > a:nth-of-type(n + 4) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@container headerlinks ( width > 750px ) {
|
@container headerlinks ( width < 750px ) {
|
||||||
.containerHeaderlinks > *:nth-child(5) {
|
.containerHeaderlinks > :nth-child(n + 5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.containerHeaderlinks > :nth-child(n + 5) ~ .linkEllipsis {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
& div > :nth-child(n + 5) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@container headerlinks ( width < 900px ) {
|
||||||
|
.containerHeaderlinks > :nth-child(n + 6) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.containerHeaderlinks > :nth-child(n + 6) ~ .linkEllipsis {
|
||||||
|
display: flex;
|
||||||
|
& div > a:nth-of-type(n + 6) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@container headerlinks ( width > 900px ) {
|
@container headerlinks ( width > 900px ) {
|
||||||
.containerHeaderlinks > *:nth-child(n + 5) {
|
.containerHeaderlinks > *:not(.linkEllipsis) {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { Menu } from '@geist-ui/icons';
|
|
||||||
import DownloadCloud from '@geist-ui/icons/downloadCloud';
|
|
||||||
import Github from '@geist-ui/icons/github';
|
|
||||||
import Gitlab from '@geist-ui/icons/gitlab';
|
|
||||||
import {
|
import {
|
||||||
CustomizationSettings,
|
CustomizationSettings,
|
||||||
JSONDocument,
|
JSONDocument,
|
||||||
RevisionPageDocument,
|
RevisionPageDocument,
|
||||||
Site,
|
Site,
|
||||||
|
SiteAdsStatus,
|
||||||
SiteCustomizationSettings,
|
SiteCustomizationSettings,
|
||||||
Space,
|
Space,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import urlJoin from 'url-join';
|
import urlJoin from 'url-join';
|
||||||
|
|
||||||
@@ -97,7 +95,7 @@ export async function PageAside(props: {
|
|||||||
'py-2',
|
'py-2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Menu className={tcls('size-4')} />
|
<Icon icon="bars" className={tcls('size-3')} />
|
||||||
{t(language, 'on_this_page')}
|
{t(language, 'on_this_page')}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -147,11 +145,14 @@ export async function PageAside(props: {
|
|||||||
'dark:text-light/5',
|
'dark:text-light/5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{space.gitSync.installationProvider === 'gitlab' ? (
|
<Icon
|
||||||
<Gitlab className={tcls('size-4', 'mr-1.5')} />
|
icon={
|
||||||
) : (
|
space.gitSync.installationProvider === 'gitlab'
|
||||||
<Github className={tcls('size-4', 'mr-1.5')} />
|
? 'gitlab'
|
||||||
)}
|
: 'github'
|
||||||
|
}
|
||||||
|
className={tcls('size-4', 'mr-1.5')}
|
||||||
|
/>
|
||||||
{t(language, 'edit_on_git', getGitSyncName(space))}
|
{t(language, 'edit_on_git', getGitSyncName(space))}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,22 +177,22 @@ export async function PageAside(props: {
|
|||||||
'dark:text-light/5',
|
'dark:text-light/5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DownloadCloud className={tcls('size-4', 'mr-1.5')} />
|
<Icon icon="file-pdf" className={tcls('size-4', 'mr-1.5')} />
|
||||||
{t(language, 'pdf_download')}
|
{t(language, 'pdf_download')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{site?.ads ? (
|
<Ad
|
||||||
<Ad
|
zoneId={
|
||||||
zoneId={site.ads.zone}
|
site?.ads && site.ads.status === SiteAdsStatus.Live ? site.ads.zoneId : null
|
||||||
placement="page.aside"
|
}
|
||||||
spaceId={space.id}
|
placement="page.aside"
|
||||||
ignore={process.env.NODE_ENV !== 'production'}
|
spaceId={space.id}
|
||||||
style={tcls('mt-4')}
|
ignore={process.env.NODE_ENV !== 'production'}
|
||||||
/>
|
style={tcls('mt-4')}
|
||||||
) : null}
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ContentPointer, ContentTarget, SiteContentPointer, api } from '@/lib/ap
|
|||||||
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
|
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
|
||||||
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
import { shouldTrackPageViews } from '@/lib/tracking';
|
||||||
|
|
||||||
import { PageBodyBlankslate } from './PageBodyBlankslate';
|
import { PageBodyBlankslate } from './PageBodyBlankslate';
|
||||||
import { PageCover } from './PageCover';
|
import { PageCover } from './PageCover';
|
||||||
@@ -141,14 +142,14 @@ export function PageBody(props: {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<React.Suspense fallback={null}>
|
{shouldTrackPageViews() ? (
|
||||||
<TrackPageView
|
<TrackPageView
|
||||||
sitePointer={sitePointer}
|
sitePointer={sitePointer}
|
||||||
spaceId={space.id}
|
spaceId={space.id}
|
||||||
pageId={page.id}
|
pageId={page.id}
|
||||||
apiHost={api().endpoint}
|
apiHost={api().endpoint}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { pageHref } from '@/lib/links';
|
|||||||
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { PageIcon } from '../PageIcon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blankslate when the page has no document or the document is empty.
|
* Blankslate when the page has no document or the document is empty.
|
||||||
*/
|
*/
|
||||||
@@ -33,18 +35,33 @@ export async function PageBodyBlankslate(props: {
|
|||||||
>
|
>
|
||||||
{await Promise.all(
|
{await Promise.all(
|
||||||
page.pages.map(async (child) => {
|
page.pages.map(async (child) => {
|
||||||
|
const icon = (
|
||||||
|
<PageIcon
|
||||||
|
page={child}
|
||||||
|
style={['text-base', 'text-dark/6', 'dark:text-light/6']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
if (child.type === RevisionPageType.Link) {
|
if (child.type === RevisionPageType.Link) {
|
||||||
const resolved = await resolveContentRef(child.target, context);
|
const resolved = await resolveContentRef(child.target, context);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Card key={child.id} title={child.title} href={resolved.href} />;
|
return (
|
||||||
|
<Card
|
||||||
|
key={child.id}
|
||||||
|
leadingIcon={icon}
|
||||||
|
title={child.title}
|
||||||
|
href={resolved.href}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={child.id}
|
key={child.id}
|
||||||
title={child.title}
|
title={child.title}
|
||||||
|
leadingIcon={icon}
|
||||||
href={pageHref(rootPages, child)}
|
href={pageHref(rootPages, child)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import ChevronLeft from '@geist-ui/icons/chevronLeft';
|
|
||||||
import ChevronRight from '@geist-ui/icons/chevronRight';
|
|
||||||
import {
|
import {
|
||||||
CustomizationSettings,
|
CustomizationSettings,
|
||||||
Revision,
|
Revision,
|
||||||
@@ -7,6 +5,7 @@ import {
|
|||||||
SiteCustomizationSettings,
|
SiteCustomizationSettings,
|
||||||
Space,
|
Space,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
|
import { Icon, IconName } from '@gitbook/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { t, getSpaceLanguage } from '@/intl/server';
|
import { t, getSpaceLanguage } from '@/intl/server';
|
||||||
@@ -44,7 +43,7 @@ export function PageFooterNavigation(props: {
|
|||||||
>
|
>
|
||||||
{previous ? (
|
{previous ? (
|
||||||
<NavigationCard
|
<NavigationCard
|
||||||
icon={ChevronLeft}
|
icon="chevron-left"
|
||||||
label={t(language, 'previous_page')}
|
label={t(language, 'previous_page')}
|
||||||
title={previous.title}
|
title={previous.title}
|
||||||
href={pageHref(pages, previous)}
|
href={pageHref(pages, previous)}
|
||||||
@@ -53,7 +52,7 @@ export function PageFooterNavigation(props: {
|
|||||||
) : null}
|
) : null}
|
||||||
{next ? (
|
{next ? (
|
||||||
<NavigationCard
|
<NavigationCard
|
||||||
icon={ChevronRight}
|
icon="chevron-right"
|
||||||
label={t(language, 'next_page')}
|
label={t(language, 'next_page')}
|
||||||
title={next.title}
|
title={next.title}
|
||||||
href={pageHref(pages, next)}
|
href={pageHref(pages, next)}
|
||||||
@@ -64,13 +63,13 @@ export function PageFooterNavigation(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function NavigationCard(props: {
|
function NavigationCard(props: {
|
||||||
icon: React.FunctionComponent<{ className?: string }>;
|
icon: IconName;
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
href: string;
|
href: string;
|
||||||
reversed?: boolean;
|
reversed?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { icon: IconCo, label, title, href, reversed } = props;
|
const { icon, label, title, href, reversed } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -111,14 +110,14 @@ function NavigationCard(props: {
|
|||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<IconCo
|
<Icon
|
||||||
|
icon={icon}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'hidden',
|
'hidden',
|
||||||
'w-5',
|
'size-4',
|
||||||
'h-5',
|
'text-dark/5',
|
||||||
'stroke-dark/5',
|
'group-hover:text-primary',
|
||||||
'group-hover:stroke-primary',
|
'dark:text-light/4',
|
||||||
'dark:stroke-light/4',
|
|
||||||
'md:block',
|
'md:block',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { RevisionPageDocument } from '@gitbook/api';
|
import { RevisionPageDocument } from '@gitbook/api';
|
||||||
|
|
||||||
import { Emoji } from '@/components/primitives';
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { PageIcon } from '../PageIcon';
|
||||||
|
|
||||||
export function PageHeader(props: { page: RevisionPageDocument }) {
|
export function PageHeader(props: { page: RevisionPageDocument }) {
|
||||||
const { page } = props;
|
const { page } = props;
|
||||||
|
|
||||||
@@ -15,8 +16,8 @@ export function PageHeader(props: { page: RevisionPageDocument }) {
|
|||||||
className={tcls('max-w-3xl', 'mx-auto', 'mb-6', 'space-y-3', 'page-api-block:ml-0')}
|
className={tcls('max-w-3xl', 'mx-auto', 'mb-6', 'space-y-3', 'page-api-block:ml-0')}
|
||||||
>
|
>
|
||||||
{page.layout.title ? (
|
{page.layout.title ? (
|
||||||
<h1 className={tcls('text-4xl', 'font-bold')}>
|
<h1 className={tcls('text-4xl', 'font-bold', 'flex', 'items-center', 'gap-4')}>
|
||||||
{page.emoji ? <Emoji code={page.emoji} style={['mr-3']} /> : null}
|
<PageIcon page={page} style={['text-dark/6', 'dark:text-light/6']} />
|
||||||
{page.title}
|
{page.title}
|
||||||
</h1>
|
</h1>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { RevisionPage } from '@gitbook/api';
|
||||||
|
import { Icon, IconName } from '@gitbook/icons';
|
||||||
|
|
||||||
|
import { Emoji } from '@/components/primitives';
|
||||||
|
import { ClassValue, tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
export function PageIcon(props: { page: RevisionPage; style?: ClassValue }) {
|
||||||
|
const { page, style } = props;
|
||||||
|
|
||||||
|
if (page.emoji) {
|
||||||
|
return (
|
||||||
|
<Emoji
|
||||||
|
code={page.emoji}
|
||||||
|
style={[
|
||||||
|
style,
|
||||||
|
// We reset the color that could be passed as "style"
|
||||||
|
// as emojis should always be rendered with normal text opacity
|
||||||
|
'text-inherit',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.icon) {
|
||||||
|
return <Icon icon={page.icon as IconName} className={tcls('size-[1em]', style)} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './PageIcon';
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import IconBox from '@geist-ui/icons/box';
|
import { Icon } from '@gitbook/icons';
|
||||||
import IconSearch from '@geist-ui/icons/search';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { atom, useRecoilState } from 'recoil';
|
import { atom, useRecoilState } from 'recoil';
|
||||||
|
|
||||||
@@ -187,14 +186,9 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) {
|
|||||||
ask: true,
|
ask: true,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<IconSearch
|
<Icon
|
||||||
className={tcls(
|
icon="magnifying-glass"
|
||||||
'w-[15px]',
|
className={tcls('size-[15px]', 'shrink-0', 'mt-0.5', '[opacity:0.64]')}
|
||||||
'h-[15px]',
|
|
||||||
'shrink-0',
|
|
||||||
'mt-0.5',
|
|
||||||
'[&>path]:[stroke-opacity:0.64]',
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<span>{question}</span>
|
<span>{question}</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -242,15 +236,16 @@ function AnswerSources(props: {
|
|||||||
href={source.href}
|
href={source.href}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
>
|
>
|
||||||
<IconBox
|
<Icon
|
||||||
|
icon="arrow-up-right"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'stroke-dark/6',
|
'text-dark/6',
|
||||||
'w-[15px]',
|
'w-[15px]',
|
||||||
'h-[15px]',
|
'h-[15px]',
|
||||||
'shrink-0',
|
'shrink-0',
|
||||||
'mt-0.5',
|
'mt-0.5',
|
||||||
'mr-0.5',
|
'mr-0.5',
|
||||||
'dark:stroke-light/6',
|
'dark:text-light/6',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{source.title}
|
{source.title}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import IconSearch from '@geist-ui/icons/search';
|
import { Icon } from '@gitbook/icons';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useLanguage, tString } from '@/intl/client';
|
import { useLanguage, tString } from '@/intl/client';
|
||||||
@@ -65,7 +65,7 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={tcls('text-dark/7', 'pt-1.5', 'pb-2', 'dark:text-light/7')}>
|
<div className={tcls('text-dark/7', 'pt-1.5', 'pb-2', 'dark:text-light/7')}>
|
||||||
<IconSearch className={tcls('shrink-0', 'w-5', 'h-5')} />
|
<Icon icon="magnifying-glass" className={tcls('shrink-0', 'size-4')} />
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
<Shortcut />
|
<Shortcut />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import IconSearch from '@geist-ui/icons/search';
|
|
||||||
import { Collection, Site } from '@gitbook/api';
|
import { Collection, Site } from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -198,8 +198,11 @@ function SearchModalBody(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={tcls('flex', 'flex-row', 'items-center')}>
|
<div className={tcls('flex', 'flex-row', 'items-center')}>
|
||||||
<div className={tcls('text-dark/4', 'p-3', 'dark:text-light/5')}>
|
<div className={tcls('p-2', 'pl-4')}>
|
||||||
<IconSearch className={tcls('w-6', 'h-6')} />
|
<Icon
|
||||||
|
icon="magnifying-glass"
|
||||||
|
className={tcls('size-4', 'text-dark/4', 'dark:text-light/5')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={tcls('flex-1')}>
|
<div className={tcls('flex-1')}>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import IconSearch from '@geist-ui/icons/search';
|
import { Icon } from '@gitbook/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { t, useLanguage } from '@/intl/client';
|
import { t, useLanguage } from '@/intl/client';
|
||||||
@@ -44,15 +44,16 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion
|
|||||||
query: question,
|
query: question,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<IconSearch
|
<Icon
|
||||||
|
icon="magnifying-glass"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'w-[15px]',
|
'w-[15px]',
|
||||||
'h-[15px]',
|
'h-[15px]',
|
||||||
'shrink-0',
|
'shrink-0',
|
||||||
'mt-0.5',
|
'mt-0.5',
|
||||||
'mr-2',
|
'mr-4',
|
||||||
'stroke-dark/5',
|
'text-dark/5',
|
||||||
'dark:stroke-light/5',
|
'dark:text-light/5',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{recommended ? question : t(language, 'search_ask', [question])}
|
{recommended ? question : t(language, 'search_ask', [question])}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
|||||||
: searchSpaceContent(spaceId, revisionId, query));
|
: searchSpaceContent(spaceId, revisionId, query));
|
||||||
|
|
||||||
setResults(withAsk ? withQuestionResult(fetchedResults, query) : fetchedResults);
|
setResults(withAsk ? withQuestionResult(fetchedResults, query) : fetchedResults);
|
||||||
}, 250);
|
}, 350);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (debounceTimeout.current) {
|
if (debounceTimeout.current) {
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ export function SearchScopeToggle(props: { spaceTitle: string }) {
|
|||||||
'from-white',
|
'from-white',
|
||||||
'to-transparent',
|
'to-transparent',
|
||||||
'from-60%',
|
'from-60%',
|
||||||
'dark:from-dark-4',
|
'dark:from-dark-3',
|
||||||
'dark:to-transparent',
|
|
||||||
'shadow-[0_1px_0_0_inset]',
|
'shadow-[0_1px_0_0_inset]',
|
||||||
'shadow-dark/2',
|
'shadow-dark/2',
|
||||||
'dark:shadow-light/2',
|
'dark:shadow-light/2',
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ export async function searchSiteContent(args: {
|
|||||||
const { siteSpaceIds, query, cacheBust } = args;
|
const { siteSpaceIds, query, cacheBust } = args;
|
||||||
const pointer = getContentPointer();
|
const pointer = getContentPointer();
|
||||||
|
|
||||||
|
if (query.length <= 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (siteSpaceIds?.length === 0) {
|
if (siteSpaceIds?.length === 0) {
|
||||||
// if we have no siteSpaces to search in then we won't find anything. skip the call.
|
// if we have no siteSpaces to search in then we won't find anything. skip the call.
|
||||||
return [];
|
return [];
|
||||||
@@ -165,7 +169,7 @@ export const streamAskQuestion = streamResponse(async function* (spaceId: string
|
|||||||
const stream = api
|
const stream = api
|
||||||
.api()
|
.api()
|
||||||
.spaces.streamAskInSpace(spaceId, { query, format: 'document', details: true });
|
.spaces.streamAskInSpace(spaceId, { query, format: 'document', details: true });
|
||||||
const pagesPromise = api.getSpaceContentData({ spaceId });
|
const pagesPromise = api.getSpaceContentData({ spaceId }, undefined);
|
||||||
|
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
// We run the AI search and fetch the pages in parallel
|
// We run the AI search and fetch the pages in parallel
|
||||||
@@ -266,7 +270,7 @@ function transformSectionsAndPage(args: {
|
|||||||
type: 'page',
|
type: 'page',
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
href: getURL(item.path),
|
href: getURL(item.path, spaceURL),
|
||||||
spaceTitle: space?.title,
|
spaceTitle: space?.title,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -5,7 +5,10 @@ import { absoluteHref } from '@/lib/links';
|
|||||||
|
|
||||||
import { Emoji } from '../primitives';
|
import { Emoji } from '../primitives';
|
||||||
|
|
||||||
export function LogoIcon(
|
/**
|
||||||
|
* Icon for a space.
|
||||||
|
*/
|
||||||
|
export function SpaceIcon(
|
||||||
props: { icon?: CustomizationThemedURL; emoji?: string } & Omit<
|
props: { icon?: CustomizationThemedURL; emoji?: string } & Omit<
|
||||||
React.ComponentProps<typeof Image>,
|
React.ComponentProps<typeof Image>,
|
||||||
'sources'
|
'sources'
|
||||||
@@ -17,7 +17,7 @@ import { CONTAINER_STYLE } from '@/components/layout';
|
|||||||
import { ColorDebugger } from '@/components/primitives/ColorDebugger';
|
import { ColorDebugger } from '@/components/primitives/ColorDebugger';
|
||||||
import { SearchModal } from '@/components/Search';
|
import { SearchModal } from '@/components/Search';
|
||||||
import { TableOfContents } from '@/components/TableOfContents';
|
import { TableOfContents } from '@/components/TableOfContents';
|
||||||
import { ContentPointer, ContentTarget } from '@/lib/api';
|
import { ContentPointer, ContentTarget, SiteContentPointer } from '@/lib/api';
|
||||||
import { ContentRefContext } from '@/lib/references';
|
import { ContentRefContext } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ import { tcls } from '@/lib/tailwind';
|
|||||||
* Render the entire content of the space (header, table of contents, footer, and page content).
|
* Render the entire content of the space (header, table of contents, footer, and page content).
|
||||||
*/
|
*/
|
||||||
export function SpaceLayout(props: {
|
export function SpaceLayout(props: {
|
||||||
content: ContentPointer;
|
content: ContentPointer | SiteContentPointer;
|
||||||
contentTarget: ContentTarget;
|
contentTarget: ContentTarget;
|
||||||
space: Space;
|
space: Space;
|
||||||
parent: Site | Collection | null;
|
parent: Site | Collection | null;
|
||||||
@@ -50,6 +50,7 @@ export function SpaceLayout(props: {
|
|||||||
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
|
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
|
||||||
|
|
||||||
const contentRefContext: ContentRefContext = {
|
const contentRefContext: ContentRefContext = {
|
||||||
|
siteContext: 'siteId' in content ? content : null,
|
||||||
space,
|
space,
|
||||||
revisionId: contentTarget.revisionId,
|
revisionId: contentTarget.revisionId,
|
||||||
pages,
|
pages,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
|
import { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
|
||||||
|
|
||||||
import { Emoji } from '@/components/primitives';
|
|
||||||
import { pageHref } from '@/lib/links';
|
import { pageHref } from '@/lib/links';
|
||||||
import { getPagePath } from '@/lib/pages';
|
import { getPagePath } from '@/lib/pages';
|
||||||
import { ContentRefContext } from '@/lib/references';
|
import { ContentRefContext } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { PagesList } from './PagesList';
|
import { PagesList } from './PagesList';
|
||||||
|
import { TOCPageIcon } from './TOCPageIcon';
|
||||||
import { ToggleableLinkItem } from './ToggleableLinkItem';
|
import { ToggleableLinkItem } from './ToggleableLinkItem';
|
||||||
|
|
||||||
export function PageDocumentItem(props: {
|
export function PageDocumentItem(props: {
|
||||||
@@ -40,9 +40,9 @@ export function PageDocumentItem(props: {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{page.emoji ? (
|
{page.emoji || page.icon ? (
|
||||||
<span className={tcls('flex', 'gap-3', 'flex-row')}>
|
<span className={tcls('flex', 'gap-3', 'items-center')}>
|
||||||
<Emoji code={page.emoji} />
|
<TOCPageIcon page={page} />
|
||||||
{page.title}
|
{page.title}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
|
import { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
|
||||||
|
|
||||||
import { Emoji } from '@/components/primitives';
|
|
||||||
import { ContentRefContext } from '@/lib/references';
|
import { ContentRefContext } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { PagesList } from './PagesList';
|
import { PagesList } from './PagesList';
|
||||||
|
import { TOCPageIcon } from './TOCPageIcon';
|
||||||
|
|
||||||
export function PageGroupItem(props: {
|
export function PageGroupItem(props: {
|
||||||
rootPages: RevisionPage[];
|
rootPages: RevisionPage[];
|
||||||
@@ -18,6 +18,9 @@ export function PageGroupItem(props: {
|
|||||||
<li className={tcls('flex', 'flex-col')}>
|
<li className={tcls('flex', 'flex-col')}>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'gap-3',
|
||||||
'px-5',
|
'px-5',
|
||||||
'pt-6',
|
'pt-6',
|
||||||
'pb-1.5',
|
'pb-1.5',
|
||||||
@@ -37,7 +40,7 @@ export function PageGroupItem(props: {
|
|||||||
'dark:to-transparent',
|
'dark:to-transparent',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{page.emoji ? <Emoji code={page.emoji} style={['mr-3']} /> : null}
|
<TOCPageIcon page={page} />
|
||||||
{page.title}
|
{page.title}
|
||||||
</div>
|
</div>
|
||||||
{page.pages && page.pages.length ? (
|
{page.pages && page.pages.length ? (
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import IconExternal from '@geist-ui/icons/externalLink';
|
|
||||||
import { RevisionPageLink } from '@gitbook/api';
|
import { RevisionPageLink } from '@gitbook/api';
|
||||||
|
import { Icon } from '@gitbook/icons';
|
||||||
|
|
||||||
import { Emoji, Link } from '@/components/primitives';
|
import { Link } from '@/components/primitives';
|
||||||
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
import { ContentRefContext, resolveContentRef } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { TOCPageIcon } from './TOCPageIcon';
|
||||||
|
|
||||||
export async function PageLinkItem(props: { page: RevisionPageLink; context: ContentRefContext }) {
|
export async function PageLinkItem(props: { page: RevisionPageLink; context: ContentRefContext }) {
|
||||||
const { page, context } = props;
|
const { page, context } = props;
|
||||||
|
|
||||||
@@ -16,7 +18,6 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Con
|
|||||||
href={resolved?.href ?? '#'}
|
href={resolved?.href ?? '#'}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'flex',
|
'flex',
|
||||||
'flex-row',
|
|
||||||
'justify-start',
|
'justify-start',
|
||||||
'items-center',
|
'items-center',
|
||||||
'gap-3',
|
'gap-3',
|
||||||
@@ -35,21 +36,21 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Con
|
|||||||
'dark:hover:bg-light/2',
|
'dark:hover:bg-light/2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{page.emoji ? <Emoji code={page.emoji} /> : null}
|
<TOCPageIcon page={page} />
|
||||||
{page.title}
|
{page.title}
|
||||||
<IconExternal
|
<Icon
|
||||||
|
icon="arrow-up-right-from-square"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'w-3',
|
'size-3',
|
||||||
'h-3',
|
|
||||||
'mr-1',
|
'mr-1',
|
||||||
'mt-1',
|
'mt-1',
|
||||||
'place-self-start',
|
'place-self-start',
|
||||||
'shrink-0',
|
'shrink-0',
|
||||||
'stroke-current',
|
'text-current',
|
||||||
'transition-colors',
|
'transition-colors',
|
||||||
'[&>path]:transition-[stroke-opacity]',
|
'[&>path]:transition-[opacity]',
|
||||||
'[&>path]:[stroke-opacity:0.40]',
|
'[&>path]:[opacity:0.40]',
|
||||||
'group-hover:[&>path]:[stroke-opacity:1]',
|
'group-hover:[&>path]:[opacity:1]',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { RevisionPage } from '@gitbook/api';
|
||||||
|
|
||||||
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { PageIcon } from '../PageIcon';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Styled page icon for the table of contents.
|
||||||
|
*/
|
||||||
|
export function TOCPageIcon({ page }: { page: RevisionPage }) {
|
||||||
|
return (
|
||||||
|
<PageIcon
|
||||||
|
page={page}
|
||||||
|
style={tcls(
|
||||||
|
'text-base',
|
||||||
|
'text-dark/6',
|
||||||
|
'dark:text-light/6',
|
||||||
|
'group-aria-selected/toclink:text-primary',
|
||||||
|
'group-aria-selected/toclink:dark:text-primary-400',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user