mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-11-01 17:59:52 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ca9a8b303 | |||
| 7675c2c411 | |||
| 7c71363af8 | |||
| d48926eb2a | |||
| d843e5e06f | |||
| a2e564738e | |||
| 54a797a0b1 | |||
| a78c1ecb4d | |||
| 4771c78001 | |||
| b7a5106f36 | |||
| 4bcbdc50ff | |||
| 5d72b35c7f | |||
| ff50ac215c |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/cache-do': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Disable cloudflare observability in production
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Smoother tab transition for sections
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Don't adjust fallback font for mono font.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed scalar api client routing
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Optimize performances by using new API endpoint for fetching site data.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Bump @scalar/api-client-react version
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Bumped scalar api client pacakge
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix an issue where the space dropdown was shown under the site sections in Safari.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix an issue where the space dropdown could appear behind the header.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'gitbook': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix the styling of site section tabs on smaller screens.
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
checks: write
|
checks: write
|
||||||
statuses: write
|
statuses: write
|
||||||
outputs:
|
outputs:
|
||||||
deployment_url: ${{ steps.deploy.outputs.url }}
|
deployment_url: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
||||||
- id: deploy
|
- id: deploy
|
||||||
name: Deploy to Cloudflare
|
name: Deploy to Cloudflare
|
||||||
uses: cloudflare/wrangler-action@v3
|
uses: cloudflare/wrangler-action@v3.11.0
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
|
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
|
||||||
- name: Outputs
|
- name: Outputs
|
||||||
run: |
|
run: |
|
||||||
echo "URL: ${{ steps.deploy.outputs.url }}"
|
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||||
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
|
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
|
||||||
- name: Archive build output
|
- name: Archive build output
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -77,12 +77,12 @@ jobs:
|
|||||||
# Until https://github.com/cloudflare/wrangler-action/issues/301 is done
|
# Until https://github.com/cloudflare/wrangler-action/issues/301 is done
|
||||||
- name: Update Deployment Status to Success
|
- name: Update Deployment Status to Success
|
||||||
env:
|
env:
|
||||||
DEPLOYMENT_URL: ${{ steps.deploy.outputs.url }}
|
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
|
||||||
run: |
|
run: |
|
||||||
curl -X POST \
|
curl -X POST \
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
-d '{"state": "success", "target_url": "${{ steps.deploy.outputs.url }}", "description": "Deployed Preview URL for commit", "context": "cloudflare/preview"}' \
|
-d '{"state": "success", "target_url": "${{ steps.deploy.outputs.deployment-url }}", "description": "Deployed Preview URL for commit", "context": "cloudflare/preview"}' \
|
||||||
https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}
|
https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}
|
||||||
|
|
||||||
- name: Find GitHub Comment
|
- name: Find GitHub Comment
|
||||||
@@ -102,8 +102,8 @@ jobs:
|
|||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
body: |
|
body: |
|
||||||
**GitBook Preview**
|
**GitBook Preview**
|
||||||
Latest commit: [${{ steps.deploy.outputs.url }}](${{ steps.deploy.outputs.url }})
|
Latest commit: [${{ steps.deploy.outputs.deployment-url }}](${{ steps.deploy.outputs.deployment-url }})
|
||||||
PR: [${{ steps.deploy.outputs.alias }}](${{ steps.deploy.outputs.alias }})
|
PR: [${{ steps.deploy.outputs.deployment-alias-url }}](${{ steps.deploy.outputs.deployment-alias-url }})
|
||||||
edit-mode: replace
|
edit-mode: replace
|
||||||
visual-testing:
|
visual-testing:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,8 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "bun@1.1.18",
|
"packageManager": "bun@1.1.18",
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@vercel/next@4.3.15": "patches/@vercel%2Fnext@4.3.15.patch"
|
"@vercel/next@4.3.15": "patches/@vercel%2Fnext@4.3.15.patch",
|
||||||
|
"@cloudflare/next-on-pages@1.13.5": "patches/@cloudflare%2Fnext-on-pages@1.13.5.patch"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ migrations = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[observability]
|
[observability]
|
||||||
enabled = true
|
enabled = false
|
||||||
head_sampling_rate = 0.01
|
|
||||||
|
|
||||||
[env.preview]
|
[env.preview]
|
||||||
name = "gitbook-open-cache-preview"
|
name = "gitbook-open-cache-preview"
|
||||||
@@ -25,4 +24,4 @@ migrations = [
|
|||||||
|
|
||||||
[env.preview.observability]
|
[env.preview.observability]
|
||||||
enabled = true
|
enabled = true
|
||||||
head_sampling_rate = 1
|
head_sampling_rate = 0.1
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "env-cmd --silent -f ../../.env.local 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 --custom-entrypoint=./src/cloudflare-entrypoint.ts",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
|
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/api": "^0.73.0",
|
"@gitbook/api": "^0.76.0",
|
||||||
"@gitbook/cache-do": "workspace:*",
|
"@gitbook/cache-do": "workspace:*",
|
||||||
"@gitbook/emoji-codepoints": "workspace:*",
|
"@gitbook/emoji-codepoints": "workspace:*",
|
||||||
"@gitbook/icons": "workspace:*",
|
"@gitbook/icons": "workspace:*",
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { SkeletonHeading, SkeletonParagraph } from '@/components/primitives';
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder when loading a page.
|
|
||||||
*/
|
|
||||||
export default function PageSkeleton() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={tcls(
|
|
||||||
'flex',
|
|
||||||
'flex-row',
|
|
||||||
'flex-1',
|
|
||||||
'relative',
|
|
||||||
'py-8',
|
|
||||||
'lg:px-16',
|
|
||||||
'xl:mr-56',
|
|
||||||
'items-center',
|
|
||||||
'lg:items-start',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'page-full-width:mx-0')}>
|
|
||||||
<SkeletonHeading style={tcls('mb-8')} />
|
|
||||||
<SkeletonParagraph style={tcls('mb-4')} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { getSpaceLanguage, t } from '@/intl/server';
|
import { getSpaceLanguage, t } from '@/intl/server';
|
||||||
import { getSiteLayoutData } from '@/lib/api';
|
import { getSiteData } from '@/lib/api';
|
||||||
import { getSiteContentPointer } from '@/lib/pointer';
|
import { getSiteContentPointer } from '@/lib/pointer';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
const pointer = getSiteContentPointer();
|
const pointer = getSiteContentPointer();
|
||||||
const { customization } = await getSiteLayoutData(pointer);
|
const { customization } = await getSiteData(pointer);
|
||||||
|
|
||||||
const language = getSpaceLanguage(customization);
|
const language = getSpaceLanguage(customization);
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default async function Page(props: {
|
|||||||
page,
|
page,
|
||||||
};
|
};
|
||||||
|
|
||||||
const withSections = Boolean(sections && sections.length > 0);
|
const withSections = Boolean(sections && sections.list.length > 0);
|
||||||
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
|
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ContentVisibility } from '@gitbook/api';
|
|
||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
import { getCollection, getSite, getSpace } from '@/lib/api';
|
import { getSpace, getSite } from '@/lib/api';
|
||||||
import { absoluteHref } from '@/lib/links';
|
import { absoluteHref } from '@/lib/links';
|
||||||
import { getSiteContentPointer } from '@/lib/pointer';
|
import { getSiteContentPointer } from '@/lib/pointer';
|
||||||
import { isSpaceIndexable } from '@/lib/seo';
|
import { isSpaceIndexable } from '@/lib/seo';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ImageResponse } from 'next/og';
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getCurrentSiteCustomization, getSite, getSpace } from '@/lib/api';
|
import { getSite, getSiteData, getSpace } from '@/lib/api';
|
||||||
import { getEmojiForCode } from '@/lib/emojis';
|
import { getEmojiForCode } from '@/lib/emojis';
|
||||||
import { getSiteContentPointer } from '@/lib/pointer';
|
import { getSiteContentPointer } from '@/lib/pointer';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
@@ -38,9 +38,9 @@ export async function GET(req: NextRequest) {
|
|||||||
const pointer = getSiteContentPointer();
|
const pointer = getSiteContentPointer();
|
||||||
const spaceId = pointer.spaceId;
|
const spaceId = pointer.spaceId;
|
||||||
|
|
||||||
const [space, customization] = await Promise.all([
|
const [space, { customization }] = await Promise.all([
|
||||||
getSpace(spaceId, pointer.siteShareKey),
|
getSpace(spaceId, pointer.siteShareKey),
|
||||||
getCurrentSiteCustomization(pointer),
|
getSiteData(pointer),
|
||||||
]);
|
]);
|
||||||
const site = await getSite(pointer.organizationId, pointer.siteId);
|
const site = await getSite(pointer.organizationId, pointer.siteId);
|
||||||
const contentTitle = getContentTitle(space, customization, site);
|
const contentTitle = getContentTitle(space, customization, site);
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { RevisionPage, SiteSection, SiteSpace, Space } from '@gitbook/api';
|
import { RevisionPage } from '@gitbook/api';
|
||||||
import { assert } from 'ts-essentials';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getRevisionPageByPath,
|
getRevisionPageByPath,
|
||||||
getDocument,
|
getDocument,
|
||||||
ContentTarget,
|
ContentTarget,
|
||||||
|
getSpaceContentData,
|
||||||
getSiteData,
|
getSiteData,
|
||||||
getSite,
|
|
||||||
getCurrentSiteCustomization,
|
|
||||||
getSiteStructure,
|
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import { resolvePagePath, resolvePageId } from '@/lib/pages';
|
import { resolvePagePath, resolvePageId } from '@/lib/pages';
|
||||||
import { getSiteContentPointer } from '@/lib/pointer';
|
import { getSiteContentPointer } from '@/lib/pointer';
|
||||||
@@ -21,35 +18,18 @@ export interface PageIdParams {
|
|||||||
pageId: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SectionsList = { list: SiteSection[]; section: SiteSection };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all the data needed to render the content layout.
|
* Fetch all the data needed to render the content layout.
|
||||||
*/
|
*/
|
||||||
export async function fetchContentData() {
|
export async function fetchContentData() {
|
||||||
const content = getSiteContentPointer();
|
const content = getSiteContentPointer();
|
||||||
|
|
||||||
const [{ space, contentTarget, pages, customization, scripts }, siteStructure] =
|
const [{ space, contentTarget, pages }, { customization, site, sections, spaces, scripts }] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
getSpaceContentData(content, content.siteShareKey),
|
||||||
getSiteData(content),
|
getSiteData(content),
|
||||||
fetchSiteStructure({
|
|
||||||
organizationId: content.organizationId,
|
|
||||||
siteId: content.siteId,
|
|
||||||
siteShareKey: content.siteShareKey,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const site = siteStructure.site;
|
|
||||||
|
|
||||||
const siteSections =
|
|
||||||
content.siteSectionId && siteStructure.sections
|
|
||||||
? parseSiteSectionsList(content.siteSectionId, siteStructure.sections)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const spaces =
|
|
||||||
siteStructure.spaces ??
|
|
||||||
(siteSections ? parseSpacesFromSiteSpaces(siteSections.section.siteSpaces) : []);
|
|
||||||
|
|
||||||
// we grab the space attached to the parent as it contains overriden customizations
|
// we grab the space attached to the parent as it contains overriden customizations
|
||||||
const spaceRelativeToParent = spaces?.find((space) => space.id === content.spaceId);
|
const spaceRelativeToParent = spaces?.find((space) => space.id === content.spaceId);
|
||||||
|
|
||||||
@@ -58,8 +38,8 @@ export async function fetchContentData() {
|
|||||||
contentTarget,
|
contentTarget,
|
||||||
space: spaceRelativeToParent ?? space,
|
space: spaceRelativeToParent ?? space,
|
||||||
pages,
|
pages,
|
||||||
sections: siteSections,
|
|
||||||
site,
|
site,
|
||||||
|
sections,
|
||||||
spaces,
|
spaces,
|
||||||
customization,
|
customization,
|
||||||
scripts,
|
scripts,
|
||||||
@@ -67,44 +47,21 @@ export async function fetchContentData() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSiteSectionsList(siteSectionId: string, sections: SiteSection[]) {
|
|
||||||
const section = sections.find((section) => section.id === siteSectionId);
|
|
||||||
assert(sectionIsDefined(section), 'A section must be defined when there are multiple sections');
|
|
||||||
return { list: sections, section } satisfies SectionsList;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sectionIsDefined(section?: SiteSection): section is NonNullable<SiteSection> {
|
|
||||||
return typeof section !== 'undefined' && section !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all the data needed to render the content.
|
* Fetch all the data needed to render the content.
|
||||||
* Optimized to fetch in parallel as much as possible.
|
* Optimized to fetch in parallel as much as possible.
|
||||||
*/
|
*/
|
||||||
export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
||||||
const content = getSiteContentPointer();
|
const contentData = await fetchContentData();
|
||||||
|
|
||||||
const { space, contentTarget, pages, customization, scripts } = await getSiteData(content);
|
const page = await resolvePage(contentData.contentTarget, contentData.pages, params);
|
||||||
const page = await resolvePage(contentTarget, pages, params);
|
const document = page?.page.documentId
|
||||||
const [siteStructure, document] = await Promise.all([
|
? await getDocument(contentData.space.id, page.page.documentId)
|
||||||
fetchSiteStructure({
|
: null;
|
||||||
organizationId: content.organizationId,
|
|
||||||
siteId: content.siteId,
|
|
||||||
siteShareKey: content.siteShareKey,
|
|
||||||
}),
|
|
||||||
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content,
|
...contentData,
|
||||||
contentTarget,
|
|
||||||
space,
|
|
||||||
pages,
|
|
||||||
customization,
|
|
||||||
scripts,
|
|
||||||
ancestors: [],
|
|
||||||
...page,
|
...page,
|
||||||
...siteStructure,
|
|
||||||
document,
|
document,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -150,59 +107,6 @@ async function resolvePage(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the structure of an organization site.
|
|
||||||
* This includes the site and its sections or spaces.
|
|
||||||
*/
|
|
||||||
async function fetchSiteStructure(args: {
|
|
||||||
organizationId: string;
|
|
||||||
siteId: string;
|
|
||||||
siteShareKey: string | undefined;
|
|
||||||
}) {
|
|
||||||
const { organizationId, siteId, siteShareKey } = args;
|
|
||||||
const [orgSite, siteStructure, siteParentCustomizations] = await Promise.all([
|
|
||||||
getSite(organizationId, siteId),
|
|
||||||
getSiteStructure({ organizationId, siteId, siteShareKey }),
|
|
||||||
getCurrentSiteCustomization({ organizationId, siteId, siteSpaceId: undefined }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const siteSections =
|
|
||||||
siteStructure.type === 'sections' && siteStructure.structure
|
|
||||||
? siteStructure.structure
|
|
||||||
: null;
|
|
||||||
const siteSpaces =
|
|
||||||
siteStructure.type === 'siteSpaces' && siteStructure.structure
|
|
||||||
? parseSpacesFromSiteSpaces(siteStructure.structure)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// override the title with the customization title
|
|
||||||
const site = {
|
|
||||||
...orgSite,
|
|
||||||
...(siteParentCustomizations?.title ? { title: siteParentCustomizations.title } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
site,
|
|
||||||
spaces: siteSpaces,
|
|
||||||
sections: siteSections,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSpacesFromSiteSpaces(siteSpaces: SiteSpace[]) {
|
|
||||||
const spaces: Record<string, Space> = {};
|
|
||||||
siteSpaces.forEach((siteSpace) => {
|
|
||||||
spaces[siteSpace.space.id] = {
|
|
||||||
...siteSpace.space,
|
|
||||||
title: siteSpace.title ?? siteSpace.space.title,
|
|
||||||
urls: {
|
|
||||||
...siteSpace.space.urls,
|
|
||||||
published: siteSpace.urls.published,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return Object.values(spaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the page path from the params.
|
* Get the page path from the params.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||||
import { getSiteLayoutData } from '@/lib/api';
|
import { getSiteData } from '@/lib/api';
|
||||||
import { getSiteContentPointer } from '@/lib/pointer';
|
import { getSiteContentPointer } from '@/lib/pointer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,7 +10,7 @@ export default async function SiteRootLayout(props: { children: React.ReactNode
|
|||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
const pointer = getSiteContentPointer();
|
const pointer = getSiteContentPointer();
|
||||||
const { customization } = await getSiteLayoutData(pointer);
|
const { customization } = await getSiteData(pointer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomizationRootLayout customization={customization}>{children}</CustomizationRootLayout>
|
<CustomizationRootLayout customization={customization}>{children}</CustomizationRootLayout>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SpaceIntegrationScript } from '@gitbook/api';
|
import { SpaceIntegrationScript } from '@gitbook/api';
|
||||||
|
|
||||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||||
import { getSiteLayoutData, getSpaceCustomization } from '@/lib/api';
|
import { getSiteData, getSpaceCustomization } from '@/lib/api';
|
||||||
|
|
||||||
import { getSiteOrSpacePointerForPDF } from './pointer';
|
import { getSiteOrSpacePointerForPDF } from './pointer';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export default async function PDFRootLayout(props: { children: React.ReactNode }
|
|||||||
|
|
||||||
const pointer = getSiteOrSpacePointerForPDF();
|
const pointer = getSiteOrSpacePointerForPDF();
|
||||||
const { customization } = await ('siteId' in pointer
|
const { customization } = await ('siteId' in pointer
|
||||||
? getSiteLayoutData(pointer)
|
? getSiteData(pointer)
|
||||||
: getSpaceLayoutData(pointer.spaceId));
|
: getSpaceLayoutData(pointer.spaceId));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -26,7 +26,7 @@ export default async function PDFRootLayout(props: { children: React.ReactNode }
|
|||||||
* Fetch all the layout data about a space at once.
|
* Fetch all the layout data about a space at once.
|
||||||
*/
|
*/
|
||||||
async function getSpaceLayoutData(spaceId: string) {
|
async function getSpaceLayoutData(spaceId: string) {
|
||||||
const [customization, scripts] = await Promise.all([
|
const [{ customization }, scripts] = await Promise.all([
|
||||||
getSpaceCustomization(spaceId),
|
getSpaceCustomization(spaceId),
|
||||||
[] as SpaceIntegrationScript[],
|
[] as SpaceIntegrationScript[],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
getSpace,
|
getSpace,
|
||||||
getSpaceCustomization,
|
getSpaceCustomization,
|
||||||
getSpaceContentData,
|
getSpaceContentData,
|
||||||
getCurrentSiteCustomization,
|
getSiteData,
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import { pagePDFContainerId, PageHrefContext, absoluteHref } from '@/lib/links';
|
import { pagePDFContainerId, PageHrefContext, absoluteHref } from '@/lib/links';
|
||||||
import { resolvePageId } from '@/lib/pages';
|
import { resolvePageId } from '@/lib/pages';
|
||||||
@@ -40,11 +40,9 @@ export const runtime = 'edge';
|
|||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const pointer = getSiteOrSpacePointerForPDF();
|
const pointer = getSiteOrSpacePointerForPDF();
|
||||||
const [space, customization] = await Promise.all([
|
const [space, { customization }] = await Promise.all([
|
||||||
getSpace(pointer.spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined),
|
getSpace(pointer.spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined),
|
||||||
'siteId' in pointer
|
'siteId' in pointer ? getSiteData(pointer) : getSpaceCustomization(pointer.spaceId),
|
||||||
? getCurrentSiteCustomization(pointer)
|
|
||||||
: getSpaceCustomization(pointer.spaceId),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -67,10 +65,8 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin
|
|||||||
currentPDFUrl += '?' + searchParams.toString();
|
currentPDFUrl += '?' + searchParams.toString();
|
||||||
|
|
||||||
// Load the content,
|
// Load the content,
|
||||||
const [customization, { space, contentTarget, pages: rootPages }] = await Promise.all([
|
const [{ customization }, { space, contentTarget, pages: rootPages }] = await Promise.all([
|
||||||
'siteId' in pointer
|
'siteId' in pointer ? getSiteData(pointer) : getSpaceCustomization(pointer.spaceId),
|
||||||
? getCurrentSiteCustomization(pointer)
|
|
||||||
: getSpaceCustomization(pointer.spaceId),
|
|
||||||
getSpaceContentData(pointer, 'siteId' in pointer ? pointer.siteShareKey : undefined),
|
getSpaceContentData(pointer, 'siteId' in pointer ? pointer.siteShareKey : undefined),
|
||||||
]);
|
]);
|
||||||
const language = getSpaceLanguage(customization);
|
const language = getSpaceLanguage(customization);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import nextOnPagesHandler from '@cloudflare/next-on-pages/fetch-handler';
|
||||||
|
|
||||||
|
import { withMiddlewareHeadersStorage } from './lib/middleware';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We use a custom entrypoint until we can move to opennext (https://github.com/opennextjs/opennextjs-cloudflare/issues/92).
|
||||||
|
* There is a bug in next-on-pages where headers can't be set on the response in the middleware for RSC requests (https://github.com/cloudflare/next-on-pages/issues/897).
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
async fetch(request, env, ctx) {
|
||||||
|
const response = await withMiddlewareHeadersStorage(() =>
|
||||||
|
nextOnPagesHandler.fetch(request, env, ctx),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
} as ExportedHandler<{ ASSETS: Fetcher }>;
|
||||||
@@ -22,10 +22,8 @@ import { Heading } from './Heading';
|
|||||||
import { Hint } from './Hint';
|
import { Hint } from './Hint';
|
||||||
import { Images } from './Images';
|
import { Images } from './Images';
|
||||||
import { IntegrationBlock } from './Integration';
|
import { IntegrationBlock } from './Integration';
|
||||||
|
import { List } from './List';
|
||||||
import { ListItem } from './ListItem';
|
import { ListItem } from './ListItem';
|
||||||
import { ListOrdered } from './ListOrdered';
|
|
||||||
import { ListTasks } from './ListTasks';
|
|
||||||
import { ListUnordered } from './ListUnordered';
|
|
||||||
import { BlockMath } from './Math';
|
import { BlockMath } from './Math';
|
||||||
import { OpenAPI } from './OpenAPI';
|
import { OpenAPI } from './OpenAPI';
|
||||||
import { Paragraph } from './Paragraph';
|
import { Paragraph } from './Paragraph';
|
||||||
@@ -65,11 +63,9 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
|
|||||||
case 'heading-3':
|
case 'heading-3':
|
||||||
return <Heading {...props} block={block} />;
|
return <Heading {...props} block={block} />;
|
||||||
case 'list-ordered':
|
case 'list-ordered':
|
||||||
return <ListOrdered {...props} block={block} />;
|
|
||||||
case 'list-unordered':
|
case 'list-unordered':
|
||||||
return <ListUnordered {...props} block={block} />;
|
|
||||||
case 'list-tasks':
|
case 'list-tasks':
|
||||||
return <ListTasks {...props} block={block} />;
|
return <List {...props} block={block} />;
|
||||||
case 'list-item':
|
case 'list-item':
|
||||||
return <ListItem {...props} block={block} />;
|
return <ListItem {...props} block={block} />;
|
||||||
case 'code':
|
case 'code':
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ async function SpaceRefCard(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spaceCustomization = await ignoreAPIError(getSpaceCustomization(spaceId));
|
const { customization: spaceCustomization } = getSpaceCustomization(spaceId);
|
||||||
const customFavicon = spaceCustomization?.favicon;
|
const customFavicon = spaceCustomization?.favicon;
|
||||||
const customEmoji = customFavicon && 'emoji' in customFavicon ? customFavicon.emoji : undefined;
|
const customEmoji = customFavicon && 'emoji' in customFavicon ? customFavicon.emoji : undefined;
|
||||||
const customIcon = customFavicon && 'icon' in customFavicon ? customFavicon.icon : undefined;
|
const customIcon = customFavicon && 'icon' in customFavicon ? customFavicon.icon : undefined;
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
DocumentBlockListOrdered,
|
||||||
|
DocumentBlockListTasks,
|
||||||
|
DocumentBlockListUnordered,
|
||||||
|
} from '@gitbook/api';
|
||||||
|
import assertNever from 'assert-never';
|
||||||
|
|
||||||
|
import { BlockProps } from './Block';
|
||||||
|
import { Blocks } from './Blocks';
|
||||||
|
|
||||||
|
export function List(
|
||||||
|
props: BlockProps<
|
||||||
|
DocumentBlockListUnordered | DocumentBlockListOrdered | DocumentBlockListTasks
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Blocks
|
||||||
|
{...contextProps}
|
||||||
|
tag={getListTag(block.type)}
|
||||||
|
nodes={block.nodes}
|
||||||
|
ancestorBlocks={[...ancestorBlocks, block]}
|
||||||
|
style={['space-y-2', style]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getListTag(
|
||||||
|
type:
|
||||||
|
| DocumentBlockListUnordered['type']
|
||||||
|
| DocumentBlockListOrdered['type']
|
||||||
|
| DocumentBlockListTasks['type'],
|
||||||
|
) {
|
||||||
|
switch (type) {
|
||||||
|
case 'list-ordered':
|
||||||
|
return 'ol';
|
||||||
|
case 'list-unordered':
|
||||||
|
case 'list-tasks':
|
||||||
|
return 'ul';
|
||||||
|
default:
|
||||||
|
assertNever(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
.olListItemBullet {
|
|
||||||
/* Align the bullet container with the first line of text */
|
|
||||||
line-height: inherit;
|
|
||||||
height: 1lh;
|
|
||||||
/* Align the bullet content with the middle of the text */
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.olListItemBullet::before {
|
|
||||||
@apply text-base;
|
|
||||||
--tw-content: attr(data-value) '. ';
|
|
||||||
content: var(--tw-content);
|
|
||||||
}
|
|
||||||
@@ -1,114 +1,231 @@
|
|||||||
import {
|
import {
|
||||||
|
DocumentBlock,
|
||||||
DocumentBlockListItem,
|
DocumentBlockListItem,
|
||||||
DocumentBlockListOrdered,
|
DocumentBlockListOrdered,
|
||||||
DocumentBlockListTasks,
|
|
||||||
DocumentBlockListUnordered,
|
DocumentBlockListUnordered,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import classNames from 'classnames';
|
import assertNever from 'assert-never';
|
||||||
|
import { assert } from 'ts-essentials';
|
||||||
|
|
||||||
import { Checkbox } from '@/components/primitives';
|
import { Checkbox } from '@/components/primitives';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { BlockProps } from './Block';
|
import { BlockProps } from './Block';
|
||||||
import { Blocks } from './Blocks';
|
import { Blocks } from './Blocks';
|
||||||
import styles from './ListItem.module.css';
|
|
||||||
import { getBlockTextStyle } from './spacing';
|
import { getBlockTextStyle } from './spacing';
|
||||||
|
|
||||||
export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
||||||
const { block, ancestorBlocks, ...contextProps } = props;
|
const { block, ancestorBlocks, ...contextProps } = props;
|
||||||
|
|
||||||
const textStyle = getBlockTextStyle(block);
|
const parent = ancestorBlocks[ancestorBlocks.length - 1];
|
||||||
|
assert(
|
||||||
|
(parent && parent.type === 'list-ordered') ||
|
||||||
|
parent.type === 'list-unordered' ||
|
||||||
|
parent.type === 'list-tasks',
|
||||||
|
'Invalid parent list type',
|
||||||
|
);
|
||||||
|
|
||||||
const parent = ancestorBlocks[ancestorBlocks.length - 1] as
|
const blocksElement = (
|
||||||
| DocumentBlockListOrdered
|
<Blocks
|
||||||
| DocumentBlockListUnordered
|
{...contextProps}
|
||||||
| DocumentBlockListTasks
|
nodes={block.nodes}
|
||||||
| undefined;
|
ancestorBlocks={[...ancestorBlocks, block]}
|
||||||
|
blockStyle={tcls(
|
||||||
|
'min-h-[1lh]',
|
||||||
|
// flip heading hash icon if list item is a heading
|
||||||
|
'flip-heading-hash',
|
||||||
|
// remove margin-top for the first heading in a list
|
||||||
|
'[&:is(h2)>div]:mt-0',
|
||||||
|
'[&:is(h3)>div]:mt-0',
|
||||||
|
'[&:is(h4)>div]:mt-0',
|
||||||
|
)}
|
||||||
|
style="space-y-2 flex flex-col flex-1"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const ListItemType = () => {
|
switch (parent.type) {
|
||||||
switch (parent?.type) {
|
case 'list-tasks':
|
||||||
case 'list-tasks':
|
return (
|
||||||
return (
|
<ListItemLI block={block}>
|
||||||
<li>
|
<ListItemPrefix block={block}>
|
||||||
<div className={tcls('flex', 'flex-row')}>
|
<Checkbox
|
||||||
<div
|
id={block.key!}
|
||||||
className={tcls(
|
disabled
|
||||||
textStyle.textSize,
|
checked={block.data?.checked}
|
||||||
'flex',
|
className="relative"
|
||||||
'flex-col',
|
size="small"
|
||||||
'justify-center',
|
|
||||||
'h-[1lh]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
id={block.key!}
|
|
||||||
disabled
|
|
||||||
checked={block.data?.checked}
|
|
||||||
className={tcls('relative')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label htmlFor={block.key} className={tcls('flex-1')}>
|
|
||||||
<Blocks
|
|
||||||
{...contextProps}
|
|
||||||
nodes={block.nodes}
|
|
||||||
ancestorBlocks={[...ancestorBlocks, block]}
|
|
||||||
blockStyle={tcls('flip-heading-hash')}
|
|
||||||
style={tcls('ml-2', 'space-y-2')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
case 'list-ordered':
|
|
||||||
const start = parent.data.start ?? 1;
|
|
||||||
const indexInParent = parent.nodes.findIndex((node) => node.key === block.key) ?? 0;
|
|
||||||
const index = indexInParent + start;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li value={index} className={tcls(textStyle.lineHeight)}>
|
|
||||||
<div
|
|
||||||
data-value={index}
|
|
||||||
className={classNames(
|
|
||||||
'bullet',
|
|
||||||
styles.olListItemBullet,
|
|
||||||
tcls(textStyle.textSize),
|
|
||||||
)}
|
|
||||||
></div>
|
|
||||||
{/* zero width space to force layouts with empty lists */}
|
|
||||||
<Blocks
|
|
||||||
{...contextProps}
|
|
||||||
nodes={block.nodes}
|
|
||||||
ancestorBlocks={[...ancestorBlocks, block]}
|
|
||||||
style={tcls('space-y-2', 'flex-1', 'flex', 'flex-col')}
|
|
||||||
blockStyle={tcls(
|
|
||||||
textStyle.lineHeight,
|
|
||||||
'min-h-[1lh]',
|
|
||||||
//flip heading hash icon if list item is a heading
|
|
||||||
'flip-heading-hash',
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</li>
|
</ListItemPrefix>
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<li className={tcls(textStyle.lineHeight)}>
|
|
||||||
<div className={tcls('bullet', textStyle.textSize)}></div>
|
|
||||||
<Blocks
|
|
||||||
{...contextProps}
|
|
||||||
nodes={block.nodes}
|
|
||||||
ancestorBlocks={[...ancestorBlocks, block]}
|
|
||||||
style={tcls('space-y-2', 'flex', 'flex-col')}
|
|
||||||
blockStyle={tcls(
|
|
||||||
'min-h-[1lh]',
|
|
||||||
//flip heading hash icon if list item is a heading
|
|
||||||
'flip-heading-hash',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return <ListItemType />;
|
<label htmlFor={block.key} className={tcls('flex-1')}>
|
||||||
|
{blocksElement}
|
||||||
|
</label>
|
||||||
|
</ListItemLI>
|
||||||
|
);
|
||||||
|
case 'list-ordered':
|
||||||
|
return (
|
||||||
|
<ListItemLI block={block}>
|
||||||
|
<ListItemPrefix block={block}>
|
||||||
|
<PseudoBefore
|
||||||
|
content={getOrderedListItemPrefixContent({
|
||||||
|
depth: getListItemDepth({ ancestorBlocks, type: parent.type }),
|
||||||
|
block,
|
||||||
|
parent,
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
fontSize: 'min(1em, 24px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemPrefix>
|
||||||
|
{blocksElement}
|
||||||
|
</ListItemLI>
|
||||||
|
);
|
||||||
|
case 'list-unordered':
|
||||||
|
return (
|
||||||
|
<ListItemLI block={block}>
|
||||||
|
<ListItemPrefix block={block}>
|
||||||
|
<PseudoBefore
|
||||||
|
content={getUnorderedListItemsPrefixContent({
|
||||||
|
depth: getListItemDepth({ ancestorBlocks, type: parent.type }),
|
||||||
|
})}
|
||||||
|
fontFamily="Arial"
|
||||||
|
style={{ fontSize: 'min(1.5em, 24px)', lineHeight: 1 }}
|
||||||
|
/>
|
||||||
|
</ListItemPrefix>
|
||||||
|
{blocksElement}
|
||||||
|
</ListItemLI>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
assertNever(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getListItemDepth(input: {
|
||||||
|
ancestorBlocks: DocumentBlock[];
|
||||||
|
type: DocumentBlockListOrdered['type'] | DocumentBlockListUnordered['type'];
|
||||||
|
}): number {
|
||||||
|
const { ancestorBlocks, type } = input;
|
||||||
|
|
||||||
|
let depth = -1;
|
||||||
|
|
||||||
|
for (let i = ancestorBlocks.length - 1; i >= 0; i--) {
|
||||||
|
const block = ancestorBlocks[i];
|
||||||
|
if (block.type === type) {
|
||||||
|
depth = depth + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block.type === 'list-item') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListItemLI(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
|
||||||
|
const textStyle = getBlockTextStyle(props.block);
|
||||||
|
return <li className={tcls(textStyle.lineHeight, 'flex items-start')}>{props.children}</li>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListItemPrefix(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
|
||||||
|
const textStyle = getBlockTextStyle(props.block);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={tcls(
|
||||||
|
textStyle.textSize,
|
||||||
|
textStyle.lineHeight,
|
||||||
|
'flex items-center justify-center mr-1 min-h-[1lh] min-w-6 text-dark/6 dark:text-light/5',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUnorderedListItemsPrefixContent(input: { depth: number }): string {
|
||||||
|
switch (input.depth % 3) {
|
||||||
|
case 0:
|
||||||
|
return '•';
|
||||||
|
case 1:
|
||||||
|
return '◦';
|
||||||
|
case 2:
|
||||||
|
return '▪';
|
||||||
|
default:
|
||||||
|
return '•';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function PseudoBefore(props: {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
content: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="before:font-var before:content-[--pseudoBefore--content]"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--pseudoBefore--content': `'${props.content}'`,
|
||||||
|
'--font-family': props.fontFamily ?? 'inherit',
|
||||||
|
...props.style,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderedListItemPrefixContent(input: {
|
||||||
|
depth: number;
|
||||||
|
parent: DocumentBlockListOrdered;
|
||||||
|
block: DocumentBlockListItem;
|
||||||
|
}): string {
|
||||||
|
const { parent, block } = input;
|
||||||
|
const start = parent.data.start ?? 1;
|
||||||
|
const index = parent.nodes.findIndex((node) => node.key === block.key) ?? 0;
|
||||||
|
const value = index + start;
|
||||||
|
switch (input.depth % 3) {
|
||||||
|
// Use numbers
|
||||||
|
case 0: {
|
||||||
|
return `${value}.`;
|
||||||
|
}
|
||||||
|
// Use letters
|
||||||
|
case 1: {
|
||||||
|
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
return `${letters[(value - 1) % letters.length]}.`;
|
||||||
|
}
|
||||||
|
// Use roman numbers
|
||||||
|
case 2: {
|
||||||
|
return `${toRoman(value).toLowerCase()}.`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return '•';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRoman(input: number): string {
|
||||||
|
const lookup = {
|
||||||
|
M: 1000,
|
||||||
|
CM: 900,
|
||||||
|
D: 500,
|
||||||
|
CD: 400,
|
||||||
|
C: 100,
|
||||||
|
XC: 90,
|
||||||
|
L: 50,
|
||||||
|
XL: 40,
|
||||||
|
X: 10,
|
||||||
|
IX: 9,
|
||||||
|
V: 5,
|
||||||
|
IV: 4,
|
||||||
|
I: 1,
|
||||||
|
};
|
||||||
|
let roman = '';
|
||||||
|
let number = input;
|
||||||
|
for (const i in lookup) {
|
||||||
|
while (number >= lookup[i as keyof typeof lookup]) {
|
||||||
|
roman += i;
|
||||||
|
number -= lookup[i as keyof typeof lookup];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roman;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { DocumentBlockListOrdered } from '@gitbook/api';
|
|
||||||
|
|
||||||
import { BlockProps } from './Block';
|
|
||||||
import { Blocks } from './Blocks';
|
|
||||||
|
|
||||||
export function ListOrdered(props: BlockProps<DocumentBlockListOrdered>) {
|
|
||||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Blocks
|
|
||||||
{...contextProps}
|
|
||||||
tag="ol"
|
|
||||||
nodes={block.nodes}
|
|
||||||
ancestorBlocks={[...ancestorBlocks, block]}
|
|
||||||
style={[
|
|
||||||
'space-y-2',
|
|
||||||
'flex',
|
|
||||||
'flex-col',
|
|
||||||
'[&>li]:gap-[1ch]',
|
|
||||||
|
|
||||||
'[counter-reset:list-decimal]',
|
|
||||||
|
|
||||||
'[&>li]:flex',
|
|
||||||
'[&>li]:flex-row',
|
|
||||||
|
|
||||||
/* '[&>li>.bullet]:w-[1ch]', */
|
|
||||||
'[&>li>.bullet]:tabular-nums',
|
|
||||||
'[&>li>.bullet]:whitespace-nowrap',
|
|
||||||
'[&>li>.bullet]:list-decimal',
|
|
||||||
|
|
||||||
'[&>li>.bullet]:before:h-[1lh]',
|
|
||||||
'[&>li>.bullet]:before:leading-[inherit]',
|
|
||||||
'[&>li>.bullet]:before:flex',
|
|
||||||
/* '[&>li>.bullet]:before:pr-[1ch]', */
|
|
||||||
'[&>li>.bullet]:text-dark/6',
|
|
||||||
|
|
||||||
//remove any spacing when using heading as list item
|
|
||||||
'[&>li>div_div]:mt-0',
|
|
||||||
|
|
||||||
'dark:[&>li>.bullet]:text-light/6',
|
|
||||||
style,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { DocumentBlockListTasks, DocumentBlockListUnordered } from '@gitbook/api';
|
|
||||||
|
|
||||||
import { BlockProps } from './Block';
|
|
||||||
import { Blocks } from './Blocks';
|
|
||||||
|
|
||||||
export function ListTasks(props: BlockProps<DocumentBlockListTasks>) {
|
|
||||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Blocks
|
|
||||||
{...contextProps}
|
|
||||||
tag="ul"
|
|
||||||
nodes={block.nodes}
|
|
||||||
ancestorBlocks={[...ancestorBlocks, block]}
|
|
||||||
style={[
|
|
||||||
'list-none',
|
|
||||||
'[&>li]:pl-[.25ch]',
|
|
||||||
//remove any spacing when using heading as list item
|
|
||||||
'[&>li>div_div]:mt-0',
|
|
||||||
'space-y-2',
|
|
||||||
style,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { DocumentBlockListUnordered } from '@gitbook/api';
|
|
||||||
|
|
||||||
import { BlockProps } from './Block';
|
|
||||||
import { Blocks } from './Blocks';
|
|
||||||
|
|
||||||
export function ListUnordered(props: BlockProps<DocumentBlockListUnordered>) {
|
|
||||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
|
||||||
|
|
||||||
const nestedBulletStyle = [
|
|
||||||
// Level 1
|
|
||||||
'[&>li>.bullet:before]:bullet-circleFilled',
|
|
||||||
// Level 2
|
|
||||||
`[&_&>li>.bullet:before]:bullet-circle`,
|
|
||||||
// Level 3
|
|
||||||
`[&_&_&>li>.bullet:before]:bullet-dash`,
|
|
||||||
// Level 4
|
|
||||||
`[&_&_&_&>li>.bullet:before]:bullet-squareFilled`,
|
|
||||||
// Level 5
|
|
||||||
`[&_&_&_&_&>li>.bullet:before]:bullet-square`,
|
|
||||||
// Level 6 // reset back to reg bullets here
|
|
||||||
`[&_&_&_&_&_&>li>.bullet:before]:bullet-circleFilled`,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Blocks
|
|
||||||
{...contextProps}
|
|
||||||
tag="ul"
|
|
||||||
nodes={block.nodes}
|
|
||||||
ancestorBlocks={[...ancestorBlocks, block]}
|
|
||||||
style={[
|
|
||||||
'list-none',
|
|
||||||
'space-y-2',
|
|
||||||
'[&>li]:relative',
|
|
||||||
'[&>li]:ps-[2.25ch]',
|
|
||||||
//remove any spacing when using heading as list item
|
|
||||||
'[&>li>div_div]:mt-0',
|
|
||||||
//custom content setup for lists
|
|
||||||
'[&>li>.bullet]:before:bg-dark/6',
|
|
||||||
'[&>li>.bullet]:before:absolute',
|
|
||||||
'[&>li>.bullet]:before:left-0',
|
|
||||||
'[&>li>.bullet]:before:w-[1ch]',
|
|
||||||
'[&>li>.bullet]:before:h-[1lh]',
|
|
||||||
'[&>li>.bullet]:before:[mask-repeat:no-repeat]',
|
|
||||||
'[&>li>.bullet]:before:[mask-position:left]',
|
|
||||||
'dark:[&>li>.bullet]:before:bg-light/6',
|
|
||||||
nestedBulletStyle,
|
|
||||||
style,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -6,46 +6,44 @@ import {
|
|||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import { assertNever } from 'assert-never';
|
import { assertNever } from 'assert-never';
|
||||||
|
|
||||||
import { ClassValue } from '@/lib/tailwind';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the line height of a block
|
* Get the line height of a block
|
||||||
*/
|
*/
|
||||||
export function getBlockTextStyle(block: DocumentBlock): {
|
export function getBlockTextStyle(block: DocumentBlock): {
|
||||||
/** Tailwind class for the text size */
|
/** Tailwind class for the text size */
|
||||||
textSize: ClassValue;
|
textSize: string;
|
||||||
/** Tailwind class for the height (h-*) */
|
/** Tailwind class for the height (h-*) */
|
||||||
lineHeight: ClassValue;
|
lineHeight: string;
|
||||||
/** Tailwind class for the margin top (mt-*) */
|
/** Tailwind class for the margin top (mt-*) */
|
||||||
marginTop?: ClassValue;
|
marginTop?: string;
|
||||||
} {
|
} {
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case 'paragraph':
|
case 'paragraph':
|
||||||
return {
|
return {
|
||||||
textSize: ['text-base'],
|
textSize: 'text-base',
|
||||||
lineHeight: 'leading-normal',
|
lineHeight: 'leading-normal',
|
||||||
};
|
};
|
||||||
case 'heading-1':
|
case 'heading-1':
|
||||||
return {
|
return {
|
||||||
textSize: ['text-3xl', 'font-semibold'],
|
textSize: 'text-3xl font-semibold',
|
||||||
lineHeight: 'leading-tight',
|
lineHeight: 'leading-tight',
|
||||||
marginTop: '[margin-top:_1em]',
|
marginTop: 'mt-[1em]',
|
||||||
};
|
};
|
||||||
case 'heading-2':
|
case 'heading-2':
|
||||||
return {
|
return {
|
||||||
textSize: ['text-2xl', 'font-semibold'],
|
textSize: 'text-2xl font-semibold',
|
||||||
lineHeight: 'leading-snug',
|
lineHeight: 'leading-snug',
|
||||||
marginTop: '[margin-top:_0.75em]',
|
marginTop: 'mt-[0.75em]',
|
||||||
};
|
};
|
||||||
case 'heading-3':
|
case 'heading-3':
|
||||||
return {
|
return {
|
||||||
textSize: ['text-base', 'font-semibold'],
|
textSize: 'text-base font-semibold',
|
||||||
lineHeight: 'leading-snug',
|
lineHeight: 'leading-snug',
|
||||||
marginTop: '[margin-top:_0.5em]',
|
marginTop: 'mt-[0.5em]',
|
||||||
};
|
};
|
||||||
case 'divider':
|
case 'divider':
|
||||||
return {
|
return {
|
||||||
textSize: [],
|
textSize: '',
|
||||||
lineHeight: 'leading-none',
|
lineHeight: 'leading-none',
|
||||||
};
|
};
|
||||||
case 'list-ordered':
|
case 'list-ordered':
|
||||||
@@ -56,7 +54,7 @@ export function getBlockTextStyle(block: DocumentBlock): {
|
|||||||
return getBlockTextStyle(block.nodes[0]);
|
return getBlockTextStyle(block.nodes[0]);
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
textSize: ['text-base'],
|
textSize: 'text-base',
|
||||||
lineHeight: 'leading-normal',
|
lineHeight: 'leading-normal',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function Dropdown<E extends HTMLElement>(props: {
|
|||||||
'absolute',
|
'absolute',
|
||||||
'top-full',
|
'top-full',
|
||||||
'left-0',
|
'left-0',
|
||||||
'z-10',
|
'z-20',
|
||||||
'origin-top-left',
|
'origin-top-left',
|
||||||
'invisible',
|
'invisible',
|
||||||
'transition-opacity',
|
'transition-opacity',
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import {
|
import { CustomizationSettings, Site, SiteCustomizationSettings, Space } from '@gitbook/api';
|
||||||
CustomizationSettings,
|
|
||||||
Site,
|
|
||||||
SiteCustomizationSettings,
|
|
||||||
SiteSection,
|
|
||||||
Space,
|
|
||||||
} from '@gitbook/api';
|
|
||||||
import { CustomizationHeaderPreset } from '@gitbook/api';
|
import { CustomizationHeaderPreset } from '@gitbook/api';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '@/components/layout';
|
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '@/components/layout';
|
||||||
import { t, getSpaceLanguage } from '@/intl/server';
|
import { t, getSpaceLanguage } from '@/intl/server';
|
||||||
|
import type { SectionsList } from '@/lib/api';
|
||||||
import { ContentRefContext } from '@/lib/references';
|
import { ContentRefContext } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
@@ -27,14 +22,12 @@ export function Header(props: {
|
|||||||
space: Space;
|
space: Space;
|
||||||
site: Site | null;
|
site: Site | null;
|
||||||
spaces: Space[];
|
spaces: Space[];
|
||||||
sections: { list: SiteSection[]; section: SiteSection } | null;
|
sections: SectionsList | null;
|
||||||
context: ContentRefContext;
|
context: ContentRefContext;
|
||||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||||
withTopHeader?: boolean;
|
withTopHeader?: boolean;
|
||||||
children?: React.ReactNode;
|
|
||||||
}) {
|
}) {
|
||||||
const { children, context, space, site, spaces, sections, customization, withTopHeader } =
|
const { context, space, site, spaces, sections, customization, withTopHeader } = props;
|
||||||
props;
|
|
||||||
const isCustomizationDefault =
|
const isCustomizationDefault =
|
||||||
customization.header.preset === CustomizationHeaderPreset.Default;
|
customization.header.preset === CustomizationHeaderPreset.Default;
|
||||||
const hasSiteSections = sections && sections.list.length > 1;
|
const hasSiteSections = sections && sections.list.length > 1;
|
||||||
@@ -149,12 +142,12 @@ export function Header(props: {
|
|||||||
{sections ? (
|
{sections ? (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'w-full shadow-thintop dark:shadow-light/1 bg-light dark:bg-dark z-[9] mt-0.5',
|
'w-full shadow-thintop dark:shadow-light/1 bg-light dark:bg-dark mt-0.5',
|
||||||
|
// Handle long section tabs, particularly on smaller screens.
|
||||||
|
'overflow-x-auto hide-scroll',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={tcls(CONTAINER_STYLE)}>
|
<SiteSectionTabs {...sections} />
|
||||||
<SiteSectionTabs sections={sections.list} section={sections.section} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -126,23 +126,6 @@
|
|||||||
@apply [&:is(h1,h2,h3,h4)>div:first-child]:[grid-area:1/2];
|
@apply [&:is(h1,h2,h3,h4)>div:first-child]:[grid-area:1/2];
|
||||||
@apply [&:is(h1,h2,h3,h4)>div:first-child]:ml-1;
|
@apply [&:is(h1,h2,h3,h4)>div:first-child]:ml-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* bullet icons */
|
|
||||||
.bullet-dash {
|
|
||||||
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjEiIHk9IjIuNSIgd2lkdGg9IjgiIGhlaWdodD0iMS41IiByeD0iMC42NiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==")];
|
|
||||||
}
|
|
||||||
.bullet-circleFilled {
|
|
||||||
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHJ4PSIzIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K")];
|
|
||||||
}
|
|
||||||
.bullet-circle {
|
|
||||||
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIuNzUiIHk9IjAuNzUiIHdpZHRoPSI1LjUiIGhlaWdodD0iNS41IiByeD0iMi43NSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPC9zdmc+Cg==")];
|
|
||||||
}
|
|
||||||
.bullet-squareFilled {
|
|
||||||
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHJ4PSIxIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K")];
|
|
||||||
}
|
|
||||||
.bullet-square {
|
|
||||||
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxtYXNrIGlkPSJwYXRoLTEtaW5zaWRlLTFfOV80OCIgZmlsbD0id2hpdGUiPgo8cmVjdCB4PSIyIiB3aWR0aD0iNyIgaGVpZ2h0PSI3IiByeD0iMSIvPgo8L21hc2s+CjxyZWN0IHg9IjIiIHdpZHRoPSI3IiBoZWlnaHQ9IjciIHJ4PSIxIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjMiIG1hc2s9InVybCgjcGF0aC0xLWluc2lkZS0xXzlfNDgpIi8+Cjwvc3ZnPgo=")];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|||||||
@@ -68,16 +68,11 @@ async function searchSiteContent(args: {
|
|||||||
scope.mode === 'current' ||
|
scope.mode === 'current' ||
|
||||||
(scope.mode === 'specific' && scope.siteSpaceIds.length > 1);
|
(scope.mode === 'specific' && scope.siteSpaceIds.length > 1);
|
||||||
|
|
||||||
const [searchResults, siteStructure] = await Promise.all([
|
const [searchResults, siteData] = await Promise.all([
|
||||||
api.searchSiteContent(pointer.organizationId, pointer.siteId, query, scope, cacheBust),
|
api.searchSiteContent(pointer.organizationId, pointer.siteId, query, scope, cacheBust),
|
||||||
needsStructure
|
needsStructure ? api.getSiteData(pointer) : null,
|
||||||
? api.getSiteStructure({
|
|
||||||
organizationId: pointer.organizationId,
|
|
||||||
siteId: pointer.siteId,
|
|
||||||
siteShareKey: pointer.siteShareKey,
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
]);
|
]);
|
||||||
|
const siteStructure = siteData?.structure;
|
||||||
|
|
||||||
const siteSpaces = siteStructure
|
const siteSpaces = siteStructure
|
||||||
? siteStructure.type === 'siteSpaces'
|
? siteStructure.type === 'siteSpaces'
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { SiteSection } from '@gitbook/api';
|
import { SiteSection } from '@gitbook/api';
|
||||||
import { Icon } from '@gitbook/icons';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { Button, Link } from '../primitives';
|
import { Link } from '../primitives';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of tabs representing site sections for multi-section sites
|
* A set of navigational tabs representing site sections for multi-section sites
|
||||||
*/
|
*/
|
||||||
export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteSection }) {
|
export function SiteSectionTabs(props: {
|
||||||
const { sections, section: currentSection } = props;
|
list: SiteSection[];
|
||||||
|
section: SiteSection;
|
||||||
|
index: number;
|
||||||
|
}) {
|
||||||
|
const { list: sections, section: currentSection, index: currentIndex } = props;
|
||||||
|
|
||||||
const tabs = sections.map((section) => ({
|
const tabs = sections.map((section) => ({
|
||||||
id: section.id,
|
id: section.id,
|
||||||
@@ -22,86 +25,100 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
|
|||||||
const currentTabRef = React.useRef<HTMLAnchorElement>(null);
|
const currentTabRef = React.useRef<HTMLAnchorElement>(null);
|
||||||
const navRef = React.useRef<HTMLDivElement>(null);
|
const navRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [currentIndex, setCurrentIndex] = React.useState(
|
|
||||||
sections.findIndex((section) => section.id === currentSection?.id),
|
|
||||||
);
|
|
||||||
const [tabDimensions, setTabDimensions] = React.useState<{
|
const [tabDimensions, setTabDimensions] = React.useState<{
|
||||||
left: number;
|
left: number;
|
||||||
width: number;
|
width: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const updateTabDimensions = React.useCallback(() => {
|
||||||
if (currentTabRef.current && navRef.current) {
|
if (currentTabRef.current && navRef.current) {
|
||||||
const rect = currentTabRef.current.getBoundingClientRect();
|
const rect = currentTabRef.current.getBoundingClientRect();
|
||||||
const navRect = navRef.current.getBoundingClientRect();
|
const navRect = navRef.current.getBoundingClientRect();
|
||||||
setTabDimensions({ left: rect.left - navRect.left, width: rect.width });
|
|
||||||
}
|
|
||||||
}, [currentIndex]);
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
setTabDimensions({
|
||||||
function onResize() {
|
left: rect.left - navRect.left,
|
||||||
if (currentTabRef.current && navRef.current) {
|
width: rect.width,
|
||||||
const rect = currentTabRef.current.getBoundingClientRect();
|
});
|
||||||
const navRect = navRef.current.getBoundingClientRect();
|
|
||||||
setTabDimensions({ left: rect.left - navRect.left, width: rect.width });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
() => window.removeEventListener('resize', onResize);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
updateTabDimensions();
|
||||||
|
}, [currentIndex, updateTabDimensions]);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
window.addEventListener('load', updateTabDimensions);
|
||||||
|
window.addEventListener('resize', updateTabDimensions);
|
||||||
|
() => {
|
||||||
|
window.removeEventListener('resize', updateTabDimensions);
|
||||||
|
window.removeEventListener('load', updateTabDimensions);
|
||||||
|
};
|
||||||
|
}, [updateTabDimensions]);
|
||||||
|
|
||||||
|
const opacity = Boolean(tabDimensions) ? 1 : 0.0;
|
||||||
const scale = (tabDimensions?.width ?? 0) * 0.01;
|
const scale = (tabDimensions?.width ?? 0) * 0.01;
|
||||||
const startPos = `${tabDimensions?.left ?? 0}px`;
|
const startPos = `${tabDimensions?.left ?? 0}px`;
|
||||||
|
|
||||||
const hasMoreSections = false; /** TODO: determine whether we need to show the more button */
|
|
||||||
|
|
||||||
return tabs.length > 0 ? (
|
return tabs.length > 0 ? (
|
||||||
<nav
|
<nav
|
||||||
aria-label="Sections"
|
aria-label="Sections"
|
||||||
ref={navRef}
|
ref={navRef}
|
||||||
className="flex flex-nowrap items-center max-w-screen mb-px"
|
className="flex flex-nowrap items-center mb-px max-w-screen-2xl mx-auto page-full-width:max-w-full"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
'--tab-opacity': `${opacity}`,
|
||||||
'--tab-scale': `${scale}`,
|
'--tab-scale': `${scale}`,
|
||||||
'--tab-start': `${startPos}`,
|
'--tab-start': `${startPos}`,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex flex-col bg-transparent">
|
||||||
className={tcls(
|
{/* An element for the tabs which includes the page padding */}
|
||||||
'relative',
|
<div
|
||||||
'flex',
|
role="tablist"
|
||||||
'gap-2',
|
className={tcls(
|
||||||
'bg-transparent',
|
'flex flex-row gap-2',
|
||||||
/* add a pseudo element for active tab indicator */
|
// Horizontal padding, which is the layout padding minus the padding of the tabs themselves.
|
||||||
'after:block',
|
'px-1',
|
||||||
"after:content-['']",
|
'sm:px-3',
|
||||||
'after:origin-left',
|
'md:px-5',
|
||||||
'after:absolute',
|
)}
|
||||||
'after:-bottom-px',
|
>
|
||||||
'after:left-0',
|
{tabs.map((tab, index) => (
|
||||||
'after:scale-x-[--tab-scale]',
|
<Tab
|
||||||
'after:transition-transform',
|
active={currentIndex === index}
|
||||||
'after:translate-x-[var(--tab-start)]',
|
key={index + tab.path}
|
||||||
'after:h-0.5',
|
label={tab.label}
|
||||||
'after:w-[100px]',
|
href={tab.path}
|
||||||
'after:bg-primary',
|
ref={currentIndex === index ? currentTabRef : null}
|
||||||
'dark:after:bg-primary-400',
|
/>
|
||||||
)}
|
))}
|
||||||
role="tablist"
|
</div>
|
||||||
>
|
{/* A container for a pseudo element for active tab indicator. A container is needed so we can set
|
||||||
{tabs.map((tab, index) => (
|
a relative position without breaking the z-index of other parts of the header. */}
|
||||||
<Tab
|
<div
|
||||||
active={currentIndex === index}
|
className={tcls(
|
||||||
key={index + tab.path}
|
'flex',
|
||||||
label={tab.label}
|
'relative',
|
||||||
href={tab.path}
|
'after:block',
|
||||||
ref={currentIndex === index ? currentTabRef : null}
|
"after:content-['']",
|
||||||
onClick={() => setCurrentIndex(index)}
|
'after:origin-left',
|
||||||
/>
|
'after:absolute',
|
||||||
))}
|
'after:-bottom-px',
|
||||||
|
'after:left-0',
|
||||||
|
'after:opacity-[--tab-opacity]',
|
||||||
|
'after:scale-x-[--tab-scale]',
|
||||||
|
'after:[transition:_opacity_150ms_25ms,transform_150ms]',
|
||||||
|
'after:motion-reduce:transition-none',
|
||||||
|
'after:translate-x-[var(--tab-start)]',
|
||||||
|
'after:will-change-transform',
|
||||||
|
'after:h-0.5',
|
||||||
|
'after:w-[100px]',
|
||||||
|
'after:bg-primary',
|
||||||
|
'dark:after:bg-primary-400',
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
{hasMoreSections ? <MoreSectionsButton /> : null}
|
|
||||||
</nav>
|
</nav>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
@@ -109,39 +126,24 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
|
|||||||
/**
|
/**
|
||||||
* The tab item - a link to a site section
|
* The tab item - a link to a site section
|
||||||
*/
|
*/
|
||||||
const Tab = React.forwardRef<
|
const Tab = React.forwardRef<HTMLSpanElement, { active: boolean; href: string; label: string }>(
|
||||||
HTMLSpanElement,
|
function Tab(props, ref) {
|
||||||
{ active: boolean; href: string; label: string; onClick?: () => void }
|
const { active, href, label } = props;
|
||||||
>(function Tab(props, ref) {
|
return (
|
||||||
const { active, href, label, onClick } = props;
|
<Link
|
||||||
return (
|
className={tcls(
|
||||||
<Link
|
'px-3 py-1 my-2 rounded straight-corners:rounded-none transition-colors',
|
||||||
className={tcls(
|
active && 'text-primary dark:text-primary-400',
|
||||||
'px-3 py-1 my-2 rounded straight-corners:rounded-none transition-colors',
|
!active &&
|
||||||
active && 'text-primary dark:text-primary-400',
|
'text-dark/8 hover:bg-dark/1 hover:text-dark/9 dark:text-light/8 dark:hover:bg-light/2 dark:hover:text-light/9',
|
||||||
!active &&
|
)}
|
||||||
'text-dark/8 hover:bg-dark/1 hover:text-dark/9 dark:text-light/8 dark:hover:bg-light/2 dark:hover:text-light/9',
|
role="tab"
|
||||||
)}
|
href={href}
|
||||||
role="tab"
|
>
|
||||||
href={href}
|
<span ref={ref} className={tcls('inline-flex w-full truncate')}>
|
||||||
onClick={onClick}
|
{label}
|
||||||
>
|
</span>
|
||||||
<span ref={ref} className={tcls('inline-flex w-full truncate')}>
|
</Link>
|
||||||
{label}
|
);
|
||||||
</span>
|
},
|
||||||
</Link>
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dropdown trigger for when there are too many sections to show them all
|
|
||||||
*/
|
|
||||||
function MoreSectionsButton() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Button variant="secondary" size="small">
|
|
||||||
<Icon icon="ellipsis-h" size={12} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
RevisionPageGroup,
|
RevisionPageGroup,
|
||||||
Site,
|
Site,
|
||||||
SiteCustomizationSettings,
|
SiteCustomizationSettings,
|
||||||
SiteSection,
|
|
||||||
Space,
|
Space,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -17,7 +16,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 { ContentTarget, SiteContentPointer } from '@/lib/api';
|
import { ContentTarget, type SectionsList, SiteContentPointer } from '@/lib/api';
|
||||||
import { ContentRefContext } from '@/lib/references';
|
import { ContentRefContext } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ export function SpaceLayout(props: {
|
|||||||
contentTarget: ContentTarget;
|
contentTarget: ContentTarget;
|
||||||
space: Space;
|
space: Space;
|
||||||
site: Site | null;
|
site: Site | null;
|
||||||
sections: { list: SiteSection[]; section: SiteSection } | null;
|
sections: SectionsList | null;
|
||||||
spaces: Space[];
|
spaces: Space[];
|
||||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||||
pages: Revision['pages'];
|
pages: Revision['pages'];
|
||||||
|
|||||||
@@ -6,16 +6,22 @@ import React from 'react';
|
|||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
export type CheckboxProps = React.ComponentProps<typeof CheckboxPrimitive.Root> & {
|
||||||
|
/**
|
||||||
|
* The size of the checkbox.
|
||||||
|
* @default medium
|
||||||
|
*/
|
||||||
|
size?: 'small' | 'medium';
|
||||||
|
};
|
||||||
|
|
||||||
export const Checkbox = React.forwardRef<
|
export const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
CheckboxProps
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, size = 'medium', ...props }, ref) => (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'peer',
|
'peer',
|
||||||
'h-5',
|
|
||||||
'w-5',
|
|
||||||
'shrink-0',
|
'shrink-0',
|
||||||
'rounded-sm',
|
'rounded-sm',
|
||||||
'straight-corners:rounded-none',
|
'straight-corners:rounded-none',
|
||||||
@@ -32,11 +38,12 @@ export const Checkbox = React.forwardRef<
|
|||||||
'dark:ring-light/3',
|
'dark:ring-light/3',
|
||||||
'dark:contrast-more:ring-light/6',
|
'dark:contrast-more:ring-light/6',
|
||||||
'dark:data-[state=checked]:bg-primary-500',
|
'dark:data-[state=checked]:bg-primary-500',
|
||||||
|
{ small: 'size-4', medium: 'size-5' }[size],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator className={tcls('relative', 'text-current')}>
|
<CheckboxPrimitive.Indicator className="relative text-current">
|
||||||
{props.checked ? (
|
{props.checked ? (
|
||||||
<Icon icon="check" iconStyle={IconStyle.Solid} className={'size-3'} />
|
<Icon icon="check" iconStyle={IconStyle.Solid} className={'size-3'} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export const ibmPlexMono = IBM_Plex_Mono({
|
|||||||
display: 'swap',
|
display: 'swap',
|
||||||
preload: false,
|
preload: false,
|
||||||
fallback: ['monospace'],
|
fallback: ['monospace'],
|
||||||
|
adjustFontFallback: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const firaSans = Fira_Sans_Extra_Condensed({
|
const firaSans = Fira_Sans_Extra_Condensed({
|
||||||
|
|||||||
+86
-201
@@ -13,11 +13,15 @@ import {
|
|||||||
RevisionFile,
|
RevisionFile,
|
||||||
SiteCustomizationSettings,
|
SiteCustomizationSettings,
|
||||||
RevisionReusableContent,
|
RevisionReusableContent,
|
||||||
|
SiteSpace,
|
||||||
|
Space,
|
||||||
|
SiteSection,
|
||||||
|
PublishedSiteContent,
|
||||||
} from '@gitbook/api';
|
} from '@gitbook/api';
|
||||||
import assertNever from 'assert-never';
|
import assertNever from 'assert-never';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { DeepPartial } from 'ts-essentials';
|
import { assert, DeepPartial } from 'ts-essentials';
|
||||||
|
|
||||||
import { batch } from './async';
|
import { batch } from './async';
|
||||||
import { buildVersion } from './build';
|
import { buildVersion } from './build';
|
||||||
@@ -131,22 +135,10 @@ export function withAPI<T>(client: GitBookAPI, fn: () => Promise<T>): Promise<T>
|
|||||||
return apiSyncStorage.run(client, fn);
|
return apiSyncStorage.run(client, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Fix @gitbook/api types for PublishedSiteContentLookup
|
type SpaceContentLookup = Pick<
|
||||||
type SpaceContentLookup = {
|
PublishedSiteContent,
|
||||||
kind: 'space';
|
'space' | 'changeRequest' | 'revision' | 'pathname' | 'basePath' | 'apiToken'
|
||||||
/** ID of the space matching. */
|
> & { kind: 'space' };
|
||||||
space: string;
|
|
||||||
/** Identifier of the change request being previewed in this URL. */
|
|
||||||
changeRequest?: string;
|
|
||||||
/** Identifier of the revision being previewed in this URL. */
|
|
||||||
revision?: string;
|
|
||||||
/** Path of the content relative to the space */
|
|
||||||
pathname: string;
|
|
||||||
/** Prefix of the path in the URL dedicated to the space */
|
|
||||||
basePath: string;
|
|
||||||
/** Short-lived API token to fetch content related to the space in the context of the URL. */
|
|
||||||
apiToken: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PublishedContentWithCache =
|
export type PublishedContentWithCache =
|
||||||
| ((SpaceContentLookup | PublishedSiteContentLookup) & {
|
| ((SpaceContentLookup | PublishedSiteContentLookup) & {
|
||||||
@@ -628,84 +620,6 @@ export const getDocument = cache({
|
|||||||
timeout: 20 * 1000,
|
timeout: 20 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the customization settings for a site-space from the API.
|
|
||||||
*/
|
|
||||||
const getSiteSpaceCustomizationFromAPI = cache({
|
|
||||||
name: 'api.getSiteSpaceCustomizationById',
|
|
||||||
tag: (organizationId, siteId, siteSpaceId) => getAPICacheTag({ tag: 'site', site: siteId }),
|
|
||||||
get: async (
|
|
||||||
organizationId: string,
|
|
||||||
siteId: string,
|
|
||||||
siteSpaceId: string,
|
|
||||||
options: CacheFunctionOptions,
|
|
||||||
) => {
|
|
||||||
const response = await api().orgs.getSiteSpaceCustomizationById(
|
|
||||||
organizationId,
|
|
||||||
siteId,
|
|
||||||
siteSpaceId,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
signal: options.signal,
|
|
||||||
...noCacheFetchOptions,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return cacheResponse(response, {
|
|
||||||
revalidateBefore: 60 * 60,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the customization settings for a site from the API.
|
|
||||||
*/
|
|
||||||
const getSiteCustomizationFromAPI = cache({
|
|
||||||
name: 'api.getSiteCustomizationById',
|
|
||||||
tag: (organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }),
|
|
||||||
get: async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
|
|
||||||
const response = await api().orgs.getSiteCustomizationById(
|
|
||||||
organizationId,
|
|
||||||
siteId,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
signal: options.signal,
|
|
||||||
...noCacheFetchOptions,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return cacheResponse(response, {
|
|
||||||
revalidateBefore: 60 * 60,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the customization settings for a site space from the API.
|
|
||||||
*/
|
|
||||||
async function getSiteSpaceCustomization(args: {
|
|
||||||
organizationId: string;
|
|
||||||
siteId: string;
|
|
||||||
siteSpaceId: string;
|
|
||||||
}): Promise<SiteCustomizationSettings> {
|
|
||||||
const raw = await getSiteSpaceCustomizationFromAPI(
|
|
||||||
args.organizationId,
|
|
||||||
args.siteId,
|
|
||||||
args.siteSpaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return mergeCustomizationWithExtend(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the customization settings for a site space from the API.
|
|
||||||
*/
|
|
||||||
async function getSiteCustomization(args: {
|
|
||||||
organizationId: string;
|
|
||||||
siteId: string;
|
|
||||||
}): Promise<SiteCustomizationSettings> {
|
|
||||||
const raw = await getSiteCustomizationFromAPI(args.organizationId, args.siteId);
|
|
||||||
return mergeCustomizationWithExtend(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the infos about a site by its ID.
|
* Get the infos about a site by its ID.
|
||||||
*/
|
*/
|
||||||
@@ -724,59 +638,24 @@ export const getSite = cache({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all the site-spaces variants published in a site.
|
* Get the published content site data for a site's published experience
|
||||||
*/
|
*/
|
||||||
export const getSiteSpaces = cache({
|
export const getPublishedContentSite = cache({
|
||||||
name: 'api.getSiteSpaces',
|
name: 'api.getPublishedContentSite',
|
||||||
tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }),
|
tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }),
|
||||||
get: async (
|
get: async (
|
||||||
args: {
|
args: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
siteId: string;
|
siteId: string /** Site share key that can be used as context to resolve site space published urls */;
|
||||||
/** Site share key that can be used as context to resolve site space published urls */
|
|
||||||
siteShareKey: string | undefined;
|
siteShareKey: string | undefined;
|
||||||
},
|
},
|
||||||
options: CacheFunctionOptions,
|
options: CacheFunctionOptions,
|
||||||
) => {
|
) => {
|
||||||
const response = await getAll((params) =>
|
const response = await api().orgs.getPublishedContentSite(
|
||||||
api().orgs.listSiteSpaces(
|
|
||||||
args.organizationId,
|
|
||||||
args.siteId,
|
|
||||||
{
|
|
||||||
...params,
|
|
||||||
...(args.siteShareKey ? { shareKey: args.siteShareKey } : {}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...noCacheFetchOptions,
|
|
||||||
signal: options.signal,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return cacheResponse(response, {
|
|
||||||
revalidateBefore: 60 * 60,
|
|
||||||
data: response.data.items.map((siteSpace) => siteSpace),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSiteStructure = cache({
|
|
||||||
name: 'api.getSiteStructure',
|
|
||||||
tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }),
|
|
||||||
get: async (
|
|
||||||
args: {
|
|
||||||
organizationId: string;
|
|
||||||
siteId: string;
|
|
||||||
/** Site share key that can be used as context to resolve site space published urls */
|
|
||||||
siteShareKey: string | undefined;
|
|
||||||
},
|
|
||||||
options: CacheFunctionOptions,
|
|
||||||
) => {
|
|
||||||
const response = await api().orgs.getSiteStructure(
|
|
||||||
args.organizationId,
|
args.organizationId,
|
||||||
args.siteId,
|
args.siteId,
|
||||||
{
|
{
|
||||||
...(args.siteShareKey ? { shareKey: args.siteShareKey } : {}),
|
shareKey: args.siteShareKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...noCacheFetchOptions,
|
...noCacheFetchOptions,
|
||||||
@@ -785,89 +664,93 @@ export const getSiteStructure = cache({
|
|||||||
);
|
);
|
||||||
return cacheResponse(response, {
|
return cacheResponse(response, {
|
||||||
revalidateBefore: 60 * 60,
|
revalidateBefore: 60 * 60,
|
||||||
data: response.data,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
export type SectionsList = { list: SiteSection[]; section: SiteSection; index: number };
|
||||||
* List the scripts to load for the site.
|
|
||||||
*/
|
|
||||||
export const getSiteIntegrationScripts = cache({
|
|
||||||
name: 'api.getSiteIntegrationScripts',
|
|
||||||
tag: (organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }),
|
|
||||||
get: async (organizationId: string, siteId: string, options: CacheFunctionOptions) => {
|
|
||||||
const response = await api().orgs.listSiteIntegrationScripts(organizationId, siteId, {
|
|
||||||
...noCacheFetchOptions,
|
|
||||||
signal: options.signal,
|
|
||||||
});
|
|
||||||
return cacheResponse(response, {
|
|
||||||
revalidateBefore: 60 * 60,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
function parseSpacesFromSiteSpaces(siteSpaces: SiteSpace[]) {
|
||||||
* Fetch all the data to render the current site at once.
|
const spaces: Record<string, Space> = {};
|
||||||
*/
|
siteSpaces.forEach((siteSpace) => {
|
||||||
export async function getSiteData(pointer: SiteContentPointer) {
|
spaces[siteSpace.space.id] = {
|
||||||
const [{ space, pages, contentTarget }, { customization, scripts }] = await Promise.all([
|
...siteSpace.space,
|
||||||
getSpaceContentData(pointer, pointer.siteShareKey),
|
title: siteSpace.title ?? siteSpace.space.title,
|
||||||
getSiteLayoutData(pointer),
|
urls: {
|
||||||
]);
|
...siteSpace.space.urls,
|
||||||
|
published: siteSpace.urls.published,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return Object.values(spaces);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
function parseSiteSectionsList(siteSectionId: string, sections: SiteSection[]) {
|
||||||
space,
|
const section = sections.find((section) => section.id === siteSectionId);
|
||||||
pages,
|
assert(section, 'A section must be defined when there are multiple sections');
|
||||||
contentTarget,
|
return { list: sections, section, index: sections.indexOf(section) } satisfies SectionsList;
|
||||||
customization,
|
|
||||||
scripts,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all the layout data about the current site at once.
|
* This function fetches the published content site data to render the published
|
||||||
|
* experience for the site (structure, customizations, scripts etc)
|
||||||
*/
|
*/
|
||||||
export async function getSiteLayoutData(args: {
|
export async function getSiteData(
|
||||||
organizationId: string;
|
pointer: Pick<
|
||||||
siteId: string;
|
SiteContentPointer,
|
||||||
siteSpaceId: string | undefined;
|
'organizationId' | 'siteId' | 'siteSectionId' | 'siteSpaceId' | 'siteShareKey'
|
||||||
}) {
|
>,
|
||||||
const [customization, scripts] = await Promise.all([
|
) {
|
||||||
getCurrentSiteCustomization(args),
|
const {
|
||||||
getSiteIntegrationScripts(args.organizationId, args.siteId),
|
site: orgSite,
|
||||||
]);
|
structure: siteStructure,
|
||||||
|
customizations,
|
||||||
|
scripts,
|
||||||
|
} = await getPublishedContentSite({
|
||||||
|
organizationId: pointer.organizationId,
|
||||||
|
siteId: pointer.siteId,
|
||||||
|
siteShareKey: pointer.siteShareKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const siteSections =
|
||||||
|
siteStructure.type === 'sections' && siteStructure.structure
|
||||||
|
? siteStructure.structure
|
||||||
|
: null;
|
||||||
|
const siteSpaces =
|
||||||
|
siteStructure.type === 'siteSpaces' && siteStructure.structure
|
||||||
|
? parseSpacesFromSiteSpaces(siteStructure.structure)
|
||||||
|
: null;
|
||||||
|
// override the title with the customization title
|
||||||
|
const site = {
|
||||||
|
...orgSite,
|
||||||
|
...(customizations.site?.title ? { title: customizations.site.title } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sections =
|
||||||
|
pointer.siteSectionId && siteSections
|
||||||
|
? parseSiteSectionsList(pointer.siteSectionId, siteSections)
|
||||||
|
: null;
|
||||||
|
const spaces =
|
||||||
|
siteSpaces ?? (sections ? parseSpacesFromSiteSpaces(sections.section.siteSpaces) : []);
|
||||||
|
|
||||||
|
const customization = mergeCustomizationWithExtend(
|
||||||
|
pointer.siteSpaceId ? customizations.siteSpaces[pointer.siteSpaceId] : customizations.site,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customization,
|
customization,
|
||||||
|
site,
|
||||||
|
structure: siteStructure,
|
||||||
|
sections,
|
||||||
|
spaces,
|
||||||
scripts,
|
scripts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the customization settings for the current site from the API.
|
|
||||||
*/
|
|
||||||
export async function getCurrentSiteCustomization(args: {
|
|
||||||
organizationId: string;
|
|
||||||
siteId: string;
|
|
||||||
siteSpaceId: string | undefined;
|
|
||||||
}): Promise<SiteCustomizationSettings> {
|
|
||||||
return args.siteSpaceId
|
|
||||||
? getSiteSpaceCustomization({
|
|
||||||
organizationId: args.organizationId,
|
|
||||||
siteId: args.siteId,
|
|
||||||
siteSpaceId: args.siteSpaceId,
|
|
||||||
})
|
|
||||||
: getSiteCustomization({
|
|
||||||
organizationId: args.organizationId,
|
|
||||||
siteId: args.siteId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the customization settings for a space from the API.
|
* Get the customization settings for a space from the API.
|
||||||
*/
|
*/
|
||||||
export async function getSpaceCustomization(spaceId: string): Promise<CustomizationSettings> {
|
export function getSpaceCustomization(spaceId: string): { customization: CustomizationSettings } {
|
||||||
const headersList = headers();
|
const headersList = headers();
|
||||||
const raw = defaultCustomizationForSpace();
|
const raw = defaultCustomizationForSpace();
|
||||||
|
|
||||||
@@ -875,7 +758,7 @@ export async function getSpaceCustomization(spaceId: string): Promise<Customizat
|
|||||||
if (extend) {
|
if (extend) {
|
||||||
try {
|
try {
|
||||||
const parsed = rison.decode_object<Partial<CustomizationSettings>>(extend);
|
const parsed = rison.decode_object<Partial<CustomizationSettings>>(extend);
|
||||||
return { ...raw, ...parsed };
|
return { customization: { ...raw, ...parsed } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to parse x-gitbook-customization header (ignored): ${
|
`Failed to parse x-gitbook-customization header (ignored): ${
|
||||||
@@ -885,7 +768,9 @@ export async function getSpaceCustomization(spaceId: string): Promise<Customizat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw;
|
return {
|
||||||
|
customization: raw,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,45 @@
|
|||||||
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a header on the middleware response.
|
||||||
|
* We do this because of https://github.com/opennextjs/opennextjs-cloudflare/issues/92
|
||||||
|
* It can be removed as soon as we move to opennext where hopefully this is fixed.
|
||||||
|
*/
|
||||||
|
export function setMiddlewareHeader(response: Response, name: string, value: string) {
|
||||||
|
const responseHeadersLocalStorage =
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.responseHeadersLocalStorage as AsyncLocalStorage<Headers> | undefined;
|
||||||
|
const responseHeaders = responseHeadersLocalStorage?.getStore();
|
||||||
|
response.headers.set(name, value);
|
||||||
|
|
||||||
|
if (responseHeaders) {
|
||||||
|
responseHeaders.set(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap some middleware with a the storage to store headers.
|
||||||
|
*/
|
||||||
|
export async function withMiddlewareHeadersStorage(
|
||||||
|
handler: () => Promise<Response>,
|
||||||
|
): Promise<Response> {
|
||||||
|
const responseHeadersLocalStorage =
|
||||||
|
// @ts-ignore
|
||||||
|
(globalThis.responseHeadersLocalStorage as AsyncLocalStorage<Headers>) ??
|
||||||
|
new AsyncLocalStorage<Headers>();
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.responseHeadersLocalStorage = responseHeadersLocalStorage;
|
||||||
|
|
||||||
|
const responseHeaders = new Headers();
|
||||||
|
const response = await responseHeadersLocalStorage.run(responseHeaders, handler);
|
||||||
|
|
||||||
|
for (const [name, value] of responseHeaders.entries()) {
|
||||||
|
response.headers.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content.
|
* For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content.
|
||||||
* The approach is optimized to aim at reusing cached lookup results as much as possible.
|
* The approach is optimized to aim at reusing cached lookup results as much as possible.
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import {
|
|||||||
userAgent,
|
userAgent,
|
||||||
withAPI,
|
withAPI,
|
||||||
DEFAULT_API_ENDPOINT,
|
DEFAULT_API_ENDPOINT,
|
||||||
getSiteLayoutData,
|
getPublishedContentSite,
|
||||||
getSite,
|
getSiteData,
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import { race } from '@/lib/async';
|
import { race } from '@/lib/async';
|
||||||
import { buildVersion } from '@/lib/build';
|
import { buildVersion } from '@/lib/build';
|
||||||
import { createContentSecurityPolicyNonce, getContentSecurityPolicy } from '@/lib/csp';
|
import { createContentSecurityPolicyNonce, getContentSecurityPolicy } from '@/lib/csp';
|
||||||
import { getURLLookupAlternatives, normalizeURL } from '@/lib/middleware';
|
import { getURLLookupAlternatives, normalizeURL, setMiddlewareHeader } from '@/lib/middleware';
|
||||||
import {
|
import {
|
||||||
VisitorAuthCookieValue,
|
VisitorAuthCookieValue,
|
||||||
getVisitorAuthCookieName,
|
getVisitorAuthCookieName,
|
||||||
@@ -175,10 +175,12 @@ export async function middleware(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { scripts } = await ('site' in resolved
|
const { scripts } = await ('site' in resolved
|
||||||
? getSiteLayoutData({
|
? getSiteData({
|
||||||
organizationId: resolved.organization,
|
organizationId: resolved.organization,
|
||||||
siteId: resolved.site,
|
siteId: resolved.site,
|
||||||
|
siteSectionId: resolved.siteSection,
|
||||||
siteSpaceId: resolved.siteSpace,
|
siteSpaceId: resolved.siteSpace,
|
||||||
|
siteShareKey: resolved.shareKey,
|
||||||
})
|
})
|
||||||
: { scripts: [] });
|
: { scripts: [] });
|
||||||
return getContentSecurityPolicy(scripts, nonce);
|
return getContentSecurityPolicy(scripts, nonce);
|
||||||
@@ -253,43 +255,31 @@ export async function middleware(request: NextRequest) {
|
|||||||
resolved.cookies,
|
resolved.cookies,
|
||||||
);
|
);
|
||||||
|
|
||||||
response.headers.set('x-gitbook-version', buildVersion());
|
setMiddlewareHeader(response, 'x-gitbook-version', buildVersion());
|
||||||
|
|
||||||
// Add Content Security Policy header
|
// Add Content Security Policy header
|
||||||
response.headers.set('content-security-policy', csp);
|
setMiddlewareHeader(response, 'content-security-policy', csp);
|
||||||
// Basic security headers
|
// Basic security headers
|
||||||
response.headers.set('strict-transport-security', 'max-age=31536000');
|
setMiddlewareHeader(response, 'strict-transport-security', 'max-age=31536000');
|
||||||
response.headers.set('referrer-policy', 'no-referrer-when-downgrade');
|
setMiddlewareHeader(response, 'referrer-policy', 'no-referrer-when-downgrade');
|
||||||
response.headers.set('x-content-type-options', 'nosniff');
|
setMiddlewareHeader(response, 'x-content-type-options', 'nosniff');
|
||||||
|
|
||||||
const isPrefetch = request.headers.has('x-middleware-prefetch');
|
if (typeof resolved.cacheMaxAge === 'number') {
|
||||||
|
const cacheControl = `public, max-age=0, s-maxage=${resolved.cacheMaxAge}, stale-if-error=0`;
|
||||||
|
|
||||||
if (isPrefetch) {
|
if (process.env.GITBOOK_OUTPUT_CACHE === 'true' && process.env.NODE_ENV !== 'development') {
|
||||||
// To avoid cache poisoning, we don't cache prefetch requests
|
setMiddlewareHeader(response, 'cache-control', cacheControl);
|
||||||
response.headers.set(
|
setMiddlewareHeader(response, 'Cloudflare-CDN-Cache-Control', cacheControl);
|
||||||
'cache-control',
|
} else {
|
||||||
'private, no-cache, no-store, max-age=0, must-revalidate',
|
setMiddlewareHeader(response, 'x-gitbook-cache-control', cacheControl);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (typeof resolved.cacheMaxAge === 'number') {
|
|
||||||
const cacheControl = `public, max-age=0, s-maxage=${resolved.cacheMaxAge}, stale-if-error=0`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
process.env.GITBOOK_OUTPUT_CACHE === 'true' &&
|
|
||||||
process.env.NODE_ENV !== 'development'
|
|
||||||
) {
|
|
||||||
response.headers.set('cache-control', cacheControl);
|
|
||||||
response.headers.set('Cloudflare-CDN-Cache-Control', cacheControl);
|
|
||||||
} else {
|
|
||||||
response.headers.set('x-gitbook-cache-control', cacheControl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
if (resolved.cacheTags && resolved.cacheTags.length > 0) {
|
if (resolved.cacheTags && resolved.cacheTags.length > 0) {
|
||||||
const headerCacheTag = resolved.cacheTags.join(',');
|
const headerCacheTag = resolved.cacheTags.join(',');
|
||||||
response.headers.set('cache-tag', headerCacheTag);
|
setMiddlewareHeader(response, 'cache-tag', headerCacheTag);
|
||||||
response.headers.set('x-gitbook-cache-tag', headerCacheTag);
|
setMiddlewareHeader(response, 'x-gitbook-cache-tag', headerCacheTag);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -492,7 +482,13 @@ async function lookupSiteOrSpaceInMultiIdMode(
|
|||||||
// Verify access to the site to avoid leaking cached data in this mode
|
// Verify access to the site to avoid leaking cached data in this mode
|
||||||
// (the cache is not dependend on the auth token, so it could leak data)
|
// (the cache is not dependend on the auth token, so it could leak data)
|
||||||
if (source.kind === 'site') {
|
if (source.kind === 'site') {
|
||||||
await withAPI(gitbookAPI, () => getSite.revalidate(decoded.organization, source.id));
|
await withAPI(gitbookAPI, () =>
|
||||||
|
getPublishedContentSite.revalidate({
|
||||||
|
organizationId: decoded.organization,
|
||||||
|
siteId: source.id,
|
||||||
|
siteShareKey: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookies: LookupCookies = {
|
const cookies: LookupCookies = {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const config: Config = {
|
|||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['var(--font-content)'],
|
sans: ['var(--font-content)'],
|
||||||
mono: ['var(--font-mono)'],
|
mono: ['var(--font-mono)'],
|
||||||
|
var: ['var(--font-family)'],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
// Dynamic colors matching the customization settings
|
// Dynamic colors matching the customization settings
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"@gitbook/api": "^0.72.0",
|
"@gitbook/api": "^0.76.0",
|
||||||
"assert-never": "^1.2.1"
|
"assert-never": "^1.2.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scalar/api-client-react": "1.0.65",
|
"@scalar/api-client-react": "1.0.76",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"flatted": "^3.2.9",
|
"flatted": "^3.2.9",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function OpenAPICodeSample(props: {
|
|||||||
tabs={samples}
|
tabs={samples}
|
||||||
overlay={
|
overlay={
|
||||||
data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
|
data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
|
||||||
<ScalarApiButton />
|
<ScalarApiButton method={data.method} path={data.path} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import React from 'react';
|
|||||||
/**
|
/**
|
||||||
* Button which launches the Scalar API Client
|
* Button which launches the Scalar API Client
|
||||||
*/
|
*/
|
||||||
export function ScalarApiButton() {
|
export function ScalarApiButton({ method, path }: { method: string; path: string }) {
|
||||||
const client = useApiClientModal();
|
const client = useApiClientModal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scalar scalar-activate">
|
<div className="scalar scalar-activate">
|
||||||
<button className="scalar-activate-button" onClick={() => client?.open()}>
|
<button
|
||||||
|
className="scalar-activate-button"
|
||||||
|
onClick={() => client?.open({ method, path })}
|
||||||
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
diff --git a/dist/index.js b/dist/index.js
|
||||||
|
index 32fec63484ec332eb291a7253e5e168223627535..653dee64794140bafe57219356c712b197c17530 100644
|
||||||
|
--- a/dist/index.js
|
||||||
|
+++ b/dist/index.js
|
||||||
|
@@ -6983,6 +6983,7 @@ async function buildWorkerFile({ vercelConfig, vercelOutput }, {
|
||||||
|
outfile: outputFile,
|
||||||
|
allowOverwrite: true,
|
||||||
|
bundle: true,
|
||||||
|
+ external: ["node:*", "cloudflare:*"],
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "custom-entrypoint-import-plugin",
|
||||||
Reference in New Issue
Block a user