mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-14 06:35:17 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36877d92a6 | |||
| 23433d1287 | |||
| 5d9183e1b4 | |||
| a1d5326ffa | |||
| cc26fb34aa | |||
| b1abbf603b | |||
| a70e8cd7dc | |||
| b38eed850f | |||
| f4c50c7e3c | |||
| 5d308c764e | |||
| 08e780496b | |||
| e4cd67ebe2 | |||
| 939b160bfa | |||
| 689e78d8e7 | |||
| 37c1d4b8ea | |||
| 75855585f5 | |||
| dcf0f9aac5 | |||
| 1d7253307a |
+4
-1
@@ -12,7 +12,7 @@
|
||||
"@codemirror/state": "6.4.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"@gitbook/api": "0.106.0"
|
||||
"@gitbook/api": "0.107.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -38,5 +38,8 @@
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tooltip": "^1.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@upstash/redis": "^1.27.1",
|
||||
"ai": "^4.1.46",
|
||||
"ajv": "^8.12.0",
|
||||
"ai": "^4.2.2",
|
||||
"assert-never": "^1.2.1",
|
||||
"bun-types": "^1.1.20",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -68,7 +66,12 @@
|
||||
"tailwind-shades": "^1.1.2",
|
||||
"unified": "^11.0.5",
|
||||
"url-join": "^5.0.0",
|
||||
"usehooks-ts": "^3.1.0"
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"zod": "^3.24.2",
|
||||
"zod-to-json-schema": "^3.24.5",
|
||||
"event-iterator": "^2.0.0",
|
||||
"partial-json": "^0.1.7",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^4.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/primitives/Button';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export default function ErrorPage(props: {
|
||||
@@ -35,9 +35,8 @@ export default function ErrorPage(props: {
|
||||
}}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
>
|
||||
{t(language, 'unexpected_error_retry')}
|
||||
</Button>
|
||||
label={tString(language, 'unexpected_error_retry')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Loading } from '../primitives';
|
||||
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
|
||||
|
||||
/**
|
||||
* Summarise a page's content for use in a link preview
|
||||
*/
|
||||
export function AIPageLinkSummary(props: {
|
||||
currentSpaceId: string;
|
||||
currentPageId: string;
|
||||
currentPageTitle: string;
|
||||
targetSpaceId: string;
|
||||
targetPageId: string;
|
||||
linkPreview?: string;
|
||||
linkTitle?: string;
|
||||
showTrademark: boolean;
|
||||
}) {
|
||||
const {
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
showTrademark = true,
|
||||
} = props;
|
||||
|
||||
const [highlight, setHighlight] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
setHighlight('');
|
||||
|
||||
(async () => {
|
||||
const stream = await streamLinkPageSummary({
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
previousPageIds: [],
|
||||
});
|
||||
|
||||
for await (const highlight of stream) {
|
||||
if (canceled) return;
|
||||
setHighlight(highlight ?? '');
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [currentSpaceId, currentPageId, targetSpaceId, targetPageId, linkPreview, linkTitle]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex w-screen items-center gap-1 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
|
||||
{showTrademark ? (
|
||||
<Loading className="size-4" busy={!highlight || highlight.length === 0} />
|
||||
) : (
|
||||
<Icon icon="sparkle" className="size-3" />
|
||||
)}
|
||||
<h6 className="text-tint">Page highlight</h6>
|
||||
</div>
|
||||
{highlight.length > 0 ? <p>{highlight}</p> : null}
|
||||
{highlight.length > 0 ? (
|
||||
<div className="text-tint-subtle text-xs">
|
||||
Based on your context. May contain mistakes.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AIPageLinkSummary';
|
||||
@@ -0,0 +1,124 @@
|
||||
'use server';
|
||||
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
|
||||
import type { GitBookBaseContext } from '@v2/lib/context';
|
||||
import { EventIterator } from 'event-iterator';
|
||||
import type { MaybePromise } from 'p-map';
|
||||
import * as partialJson from 'partial-json';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
import type { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
/**
|
||||
* Get the latest value from a stream and the response id.
|
||||
*/
|
||||
export async function generate<T>(
|
||||
promise: MaybePromise<{
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
}>
|
||||
) {
|
||||
const input = await promise;
|
||||
let value: T | undefined;
|
||||
|
||||
for await (const event of input.stream) {
|
||||
value = event;
|
||||
}
|
||||
|
||||
const { responseId } = await input.response;
|
||||
return {
|
||||
responseId,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the generation of an object using the AI.
|
||||
*/
|
||||
export async function streamGenerateObject<T>(
|
||||
context: GitBookBaseContext,
|
||||
{
|
||||
organizationId,
|
||||
siteId,
|
||||
}: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
},
|
||||
{
|
||||
schema,
|
||||
messages,
|
||||
model = AIModel.Fast,
|
||||
}: {
|
||||
schema: z.ZodSchema<T>;
|
||||
messages: AIMessageInput[];
|
||||
model?: AIModel;
|
||||
previousResponseId?: string;
|
||||
}
|
||||
) {
|
||||
const apiClient = await context.dataFetcher.api();
|
||||
|
||||
const rawStream = apiClient.orgs.streamAiResponseInSite(organizationId, siteId, {
|
||||
input: messages,
|
||||
output: {
|
||||
type: 'object',
|
||||
schema: zodToJsonSchema(schema),
|
||||
},
|
||||
model,
|
||||
});
|
||||
|
||||
let json = '';
|
||||
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
|
||||
if (event.type === 'response_object') {
|
||||
json += event.jsonChunk;
|
||||
|
||||
const parsed = partialJson.parse(json, partialJson.ALL);
|
||||
return parsed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a stream from the API to extract the responseId.
|
||||
*/
|
||||
function parseResponse<T>(
|
||||
responseStream: EventIterator<AIStreamResponse>,
|
||||
parse: (response: AIStreamResponse) => T | undefined
|
||||
): {
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
} {
|
||||
let resolveResponse: (value: { responseId: string }) => void;
|
||||
const response = new Promise<{ responseId: string }>((resolve) => {
|
||||
resolveResponse = resolve;
|
||||
});
|
||||
|
||||
const stream = new EventIterator<T>((queue) => {
|
||||
(async () => {
|
||||
let foundResponse = false;
|
||||
|
||||
for await (const event of responseStream) {
|
||||
if (event.type === 'response_finish') {
|
||||
foundResponse = true;
|
||||
resolveResponse({ responseId: event.responseId });
|
||||
} else {
|
||||
const parsed = parse(event);
|
||||
if (parsed !== undefined) {
|
||||
queue.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundResponse) {
|
||||
throw new Error('No response found');
|
||||
}
|
||||
})().then(
|
||||
() => {
|
||||
queue.stop();
|
||||
},
|
||||
(error) => {
|
||||
queue.fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return { stream, response };
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './streamLinkPageSummary';
|
||||
@@ -0,0 +1,127 @@
|
||||
'use server';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { AIMessageRole } from '@gitbook/api';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { z } from 'zod';
|
||||
import { streamGenerateObject } from './api';
|
||||
|
||||
/**
|
||||
* Get a summary of a page, in the context of another page
|
||||
*/
|
||||
export async function* streamLinkPageSummary({
|
||||
currentSpaceId,
|
||||
currentPageId,
|
||||
targetSpaceId,
|
||||
targetPageId,
|
||||
linkPreview,
|
||||
linkTitle,
|
||||
}: {
|
||||
currentSpaceId: string;
|
||||
currentPageId: string;
|
||||
targetSpaceId: string;
|
||||
targetPageId: string;
|
||||
linkPreview?: string;
|
||||
linkTitle?: string;
|
||||
previousPageIds?: string[];
|
||||
}) {
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const [{ stream }] = await Promise.all([
|
||||
streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
highlight: z.string().describe('The most important content of the target page'),
|
||||
// questions: z.array(z.string().describe('The questions to sea')).max(3),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# Role
|
||||
You are a documentation navigator. Your job is to help the user read documentation more efficiently. Your aim is to prevent the user from having to read the target page by giving them all the information they need to know.
|
||||
|
||||
# Task
|
||||
Using both the current page context and the target page content, produce a page highlight that:
|
||||
- Highlights the key facts from the target page.
|
||||
- Relates strongly to the topic the user is currently reading about.
|
||||
- Is very succinct and direct, using only one or two short sentences (each sentence using no more than one comma).
|
||||
- Remains strictly factual, without referring to “the page”.
|
||||
|
||||
# Instructions
|
||||
1. Identify the key paragraph surrounding the link's text (e.g., “change request”) from the current page.
|
||||
2. Extract and combine relevant information from the target page to address the link's context.
|
||||
3. Combine in one or two short sentences that are direct and brief.
|
||||
|
||||
# Examples
|
||||
## Example 1
|
||||
- Link context: “This feature is only available on the Ultimate plan.”
|
||||
- Link preview: “Pricing: Learn about our different pricing tiers.”
|
||||
- Response: “The Ultimate plan costs $25 per month. A Pro plan is available too.”
|
||||
|
||||
## Example 2
|
||||
- Link context: “You can use keyboard shortcuts to get to the Search menu faster.”
|
||||
- Link preview: “Keyboard shortcuts: A quick reference guide to all the keyboard shortcuts available.”
|
||||
- Response: “To open the Search menu, use the keyboard shortcut ⌘K or Ctrl+K.”
|
||||
|
||||
## Example 3
|
||||
- Link context: “This feature can only be enabled by an admin.”
|
||||
- Link preview: “Roles: An overview of the different roles on the platform.”
|
||||
- Response: “The admin role is reserved for the creator of the organisation.”`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# Context
|
||||
## Current page
|
||||
The content of the current page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Target page
|
||||
The content of the target page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page',
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Link preview
|
||||
The content of the link preview is:
|
||||
> ${linkPreview}`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `I'm considering reading the link titled "${linkTitle}" to page ID ${targetPageId}. Give the most relevant information from this page. Relate it to my current page and in particular the paragraph I'm currently reading. Be very concise.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
fetchServerActionSiteContext(baseContext),
|
||||
]);
|
||||
|
||||
for await (const value of stream) {
|
||||
const highlight = value.highlight;
|
||||
if (!highlight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield highlight;
|
||||
}
|
||||
}
|
||||
@@ -97,9 +97,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
onClick={() => {
|
||||
onUpdateState(true);
|
||||
}}
|
||||
>
|
||||
{t(language, 'cookies_accept')}
|
||||
</Button>
|
||||
label={tString(language, 'cookies_accept')}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
@@ -107,9 +106,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
onClick={() => {
|
||||
onUpdateState(false);
|
||||
}}
|
||||
>
|
||||
{t(language, 'cookies_reject')}
|
||||
</Button>
|
||||
label={tString(language, 'cookies_reject')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { StyledLink } from '../primitives';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { InlineLinkTooltip } from './InlineLinkTooltip';
|
||||
import { Inlines } from './Inlines';
|
||||
|
||||
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const { inline, document, context, ancestorInlines } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext)
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!resolved) {
|
||||
if (!context.contentContext || !resolved) {
|
||||
return (
|
||||
<span title="Broken link" className="underline">
|
||||
<Inlines
|
||||
@@ -25,24 +28,38 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
|
||||
return (
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
<InlineLinkTooltip
|
||||
inline={inline}
|
||||
document={document}
|
||||
context={context}
|
||||
ancestorInlines={ancestorInlines}
|
||||
>
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
</StyledLink>
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={{
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: inline.data.ref,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
</StyledLink>
|
||||
</InlineLinkTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import type { DocumentInlineLink } from '@gitbook/api';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { Fragment } from 'react';
|
||||
import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
|
||||
import { Button, StyledLink } from '../primitives';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { Inlines } from './Inlines';
|
||||
|
||||
export async function InlineLinkTooltip(
|
||||
props: InlineProps<DocumentInlineLink> & { children: React.ReactNode }
|
||||
) {
|
||||
const { inline, document, context, ancestorInlines, children } = props;
|
||||
|
||||
const resolved = context.contentContext
|
||||
? await resolveContentRef(inline.data.ref, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!context.contentContext || !resolved) {
|
||||
return (
|
||||
<span title="Broken link" className="underline">
|
||||
<Inlines
|
||||
context={context}
|
||||
document={document}
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let breadcrumbs = resolved.ancestors;
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
if (isExternal) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'External link to',
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: 'Jump to section',
|
||||
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
|
||||
},
|
||||
];
|
||||
resolved.subText = undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="z-40 w-screen max-w-md animate-present px-4 sm:w-auto">
|
||||
<div className="overflow-hidden rounded-md straight-corners:rounded-none shadow-lg shadow-tint-12/4 ring-1 ring-tint-subtle dark:shadow-tint-1 ">
|
||||
<div className="bg-tint-base p-4">
|
||||
<div className={tcls('flex gap-4')}>
|
||||
<div className="flex flex-col">
|
||||
{breadcrumbs && breadcrumbs.length > 0 ? (
|
||||
<div className="mb-1 flex grow flex-wrap items-center gap-x-2 gap-y-0.5 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
|
||||
{breadcrumbs.map((crumb, index) => {
|
||||
const Tag = crumb.href ? StyledLink : 'div';
|
||||
|
||||
return (
|
||||
<Fragment key={crumb.label}>
|
||||
{index !== 0 ? (
|
||||
<Icon
|
||||
icon="chevron-right"
|
||||
className="size-3 text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
<Tag
|
||||
className={tcls(
|
||||
'flex gap-1',
|
||||
crumb.href &&
|
||||
'links-default:text-tint no-underline hover:underline contrast-more:underline contrast-more:decoration-current'
|
||||
)}
|
||||
href={crumb.href ?? '#'}
|
||||
>
|
||||
{crumb.icon ? (
|
||||
<span className="mt-0.5 text-tint-subtle empty:hidden">
|
||||
{crumb.icon}
|
||||
</span>
|
||||
) : null}
|
||||
{crumb.label}
|
||||
</Tag>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex gap-2 leading-snug',
|
||||
isExternal && 'text-sm [overflow-wrap:anywhere]'
|
||||
)}
|
||||
>
|
||||
{resolved.icon ? (
|
||||
<div className="mt-1 text-tint-subtle empty:hidden">
|
||||
{resolved.icon}
|
||||
</div>
|
||||
) : null}
|
||||
<h5 className="font-semibold">{resolved.text}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{!isSamePage && resolved.href ? (
|
||||
<Button
|
||||
className={tcls(
|
||||
'-mx-2 -my-2 ml-auto',
|
||||
breadcrumbs?.length === 0
|
||||
? null
|
||||
: 'place-self-start'
|
||||
)}
|
||||
variant="blank"
|
||||
href={resolved.href}
|
||||
target="_blank"
|
||||
label="Open in new tab"
|
||||
size="small"
|
||||
icon="arrow-up-right-from-square"
|
||||
iconOnly={true}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{resolved.subText ? (
|
||||
<p className="mt-1 text-sm text-tint">{resolved.subText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{'customization' in context.contentContext &&
|
||||
context.contentContext.customization.ai?.pageLinkSummaries.enabled &&
|
||||
!isExternal &&
|
||||
'page' in context.contentContext &&
|
||||
inline.data.ref.kind === 'page' ? (
|
||||
<div className="border-tint-subtle border-t bg-tint p-4">
|
||||
<AIPageLinkSummary
|
||||
currentPageId={context.contentContext.page.id}
|
||||
currentSpaceId={context.contentContext.space.id}
|
||||
currentPageTitle={context.contentContext.page.title}
|
||||
targetPageId={
|
||||
inline.data.ref.page ?? context.contentContext.page.id
|
||||
}
|
||||
targetSpaceId={
|
||||
inline.data.ref.space ?? context.contentContext.space.id
|
||||
}
|
||||
linkTitle={inline.nodes
|
||||
.map((node) => {
|
||||
if (node.object === 'text') {
|
||||
return node.leaves
|
||||
.map((leaf) => leaf.text)
|
||||
.join('');
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('')}
|
||||
linkPreview={`**${resolved.text}**: ${resolved.subText}`}
|
||||
showTrademark={
|
||||
context.contentContext.customization.trademark.enabled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Tooltip.Arrow className="fill-tint-1" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
@@ -168,7 +168,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
key={index}
|
||||
href={ref.href}
|
||||
target="_blank"
|
||||
style={['flex', 'flex-row', 'items-center', 'gap-2']}
|
||||
className="flex flex-row items-center gap-2"
|
||||
insights={
|
||||
ref.file
|
||||
? {
|
||||
|
||||
@@ -142,10 +142,9 @@ function HeaderItemButton(
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
}}
|
||||
label={title}
|
||||
{...rest}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
|
||||
import { useScrollActiveId } from '@/components/hooks';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { type PDFSearchParams, getPDFURLSearchParams } from './urls';
|
||||
|
||||
@@ -58,9 +58,8 @@ export function PageControlButtons(props: {
|
||||
only: true,
|
||||
}).toString()}`}
|
||||
variant="secondary"
|
||||
>
|
||||
{t(language, 'pdf_mode_only_page')}
|
||||
</Button>
|
||||
label={tString(language, 'pdf_mode_only_page')}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
href={`?${getPDFURLSearchParams({
|
||||
@@ -69,9 +68,8 @@ export function PageControlButtons(props: {
|
||||
only: false,
|
||||
}).toString()}`}
|
||||
variant="secondary"
|
||||
>
|
||||
{t(language, 'pdf_mode_all')}
|
||||
</Button>
|
||||
label={tString(language, 'pdf_mode_all')}
|
||||
/>
|
||||
|
||||
{trademark ? <div className={tcls('mt-5')}>{trademark}</div> : null}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ import urlJoin from 'url-join';
|
||||
import { getSpaceLanguage, t } from '@/intl/server';
|
||||
import { getDocumentSections } from '@/lib/document-sections';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Ad } from '../Ads';
|
||||
import { getPDFURLSearchParams } from '../PDF';
|
||||
import { PageFeedbackForm } from '../PageFeedback';
|
||||
@@ -42,12 +41,14 @@ export function PageAside(props: {
|
||||
limit: 100,
|
||||
}).toString()}`
|
||||
);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={tcls(
|
||||
'group/aside',
|
||||
'hidden',
|
||||
'xl:flex',
|
||||
'text-sm',
|
||||
// 'page-no-toc:lg:flex',
|
||||
'flex-col',
|
||||
'basis-56',
|
||||
@@ -92,139 +93,109 @@ export function PageAside(props: {
|
||||
)}
|
||||
>
|
||||
{page.layout.outline ? (
|
||||
<>
|
||||
<div
|
||||
className={tcls(
|
||||
'overflow-y-auto',
|
||||
'overflow-x-visible',
|
||||
|
||||
'flex',
|
||||
'flex-col',
|
||||
'shrink',
|
||||
'pb-12',
|
||||
|
||||
'sticky',
|
||||
'lg:top:0',
|
||||
'site-header:lg:top-16',
|
||||
'site-header-sections:lg:top-[6.75rem]',
|
||||
|
||||
'gap-2',
|
||||
'pt-8',
|
||||
|
||||
'page-api-block:xl:max-2xl:py-0',
|
||||
// Hide it for api page, until hovered
|
||||
'page-api-block:xl:max-2xl:hidden',
|
||||
'page-api-block:xl:max-2xl:group-hover/aside:flex'
|
||||
)}
|
||||
>
|
||||
{document ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<PageAsideSections document={document} context={context} />
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={tcls(
|
||||
'hidden',
|
||||
'page-api-block:xl:max-2xl:flex',
|
||||
'text-xs',
|
||||
'tracking-wide',
|
||||
'font-semibold',
|
||||
'uppercase',
|
||||
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'gap-2'
|
||||
)}
|
||||
>
|
||||
<Icon icon="block-quote" className={tcls('size-3')} />
|
||||
{t(language, 'on_this_page')}
|
||||
<Icon
|
||||
icon="chevron-down"
|
||||
className={tcls(
|
||||
'size-3',
|
||||
'opacity-6',
|
||||
'ml-auto',
|
||||
'page-api-block:xl:max-2xl:group-hover/aside:hidden'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={tcls(
|
||||
'overflow-y-auto',
|
||||
'overflow-x-visible',
|
||||
|
||||
'flex',
|
||||
'flex-col',
|
||||
'shrink',
|
||||
'pb-12',
|
||||
|
||||
'sticky',
|
||||
'lg:top:0',
|
||||
'site-header:lg:top-16',
|
||||
'site-header-sections:lg:top-[6.75rem]',
|
||||
|
||||
'gap-6',
|
||||
'pt-8',
|
||||
|
||||
'page-api-block:xl:max-2xl:py-0',
|
||||
// Hide it for api page, until hovered
|
||||
'page-api-block:xl:max-2xl:hidden',
|
||||
'page-api-block:xl:max-2xl:group-hover/aside:flex'
|
||||
'gap-3',
|
||||
'sidebar-list-default:px-3',
|
||||
'border-t',
|
||||
'first:border-none',
|
||||
'border-tint-subtle',
|
||||
'py-4',
|
||||
'first:pt-0',
|
||||
'page-api-block:xl:max-2xl:px-3',
|
||||
'empty:hidden'
|
||||
)}
|
||||
>
|
||||
{document ? (
|
||||
{withPageFeedback ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<PageAsideSections document={document} context={context} />
|
||||
<PageFeedbackForm pageId={page.id} className={tcls('mt-2')} />
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-3',
|
||||
'sidebar-list-default:px-3',
|
||||
'border-t',
|
||||
'first:border-none',
|
||||
'border-tint-subtle',
|
||||
'py-4',
|
||||
'first:pt-0',
|
||||
'page-api-block:xl:max-2xl:px-3',
|
||||
'empty:hidden'
|
||||
)}
|
||||
>
|
||||
{withPageFeedback ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<PageFeedbackForm pageId={page.id} className={tcls('mt-2')} />
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
|
||||
<div>
|
||||
<a
|
||||
href={urlJoin(space.gitSync.url, page.git.path)}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'text-sm',
|
||||
'hover:text-tint-strong',
|
||||
'links-accent:hover:underline',
|
||||
'links-accent:hover:underline-offset-4',
|
||||
'links-accent:hover:decoration-[3px]',
|
||||
'links-accent:hover:decoration-primary-subtle',
|
||||
'py-2'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
space.gitSync.installationProvider === 'gitlab'
|
||||
? 'gitlab'
|
||||
: 'github'
|
||||
}
|
||||
className={tcls('size-4', 'mr-1.5')}
|
||||
/>
|
||||
{t(language, 'edit_on_git', getGitSyncName(space))}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
{customization.pdf.enabled ? (
|
||||
<div>
|
||||
<a
|
||||
href={pdfHref}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'text-sm',
|
||||
'hover:text-tint-strong',
|
||||
'links-accent:hover:underline',
|
||||
'links-accent:hover:underline-offset-4',
|
||||
'links-accent:hover:decoration-[3px]',
|
||||
'links-accent:hover:decoration-primary-subtle',
|
||||
'py-2'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon="file-pdf"
|
||||
className={tcls('size-4', 'mr-1.5')}
|
||||
/>
|
||||
{t(language, 'pdf_download')}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{customization.git.showEditLink && space.gitSync?.url && page.git ? (
|
||||
<div>
|
||||
<a
|
||||
href={urlJoin(space.gitSync.url, page.git.path)}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'text-sm',
|
||||
'hover:text-tint-strong',
|
||||
'links-accent:hover:underline',
|
||||
'links-accent:hover:underline-offset-4',
|
||||
'links-accent:hover:decoration-[3px]',
|
||||
'links-accent:hover:decoration-primary-subtle',
|
||||
'py-2'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
space.gitSync.installationProvider === 'gitlab'
|
||||
? 'gitlab'
|
||||
: 'github'
|
||||
}
|
||||
className={tcls('size-4', 'mr-1.5')}
|
||||
/>
|
||||
{t(language, 'edit_on_git', getGitSyncName(space))}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
{customization.pdf.enabled ? (
|
||||
<div>
|
||||
<a
|
||||
href={pdfHref}
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'items-center',
|
||||
'text-sm',
|
||||
'hover:text-tint-strong',
|
||||
'links-accent:hover:underline',
|
||||
'links-accent:hover:underline-offset-4',
|
||||
'links-accent:hover:decoration-[3px]',
|
||||
'links-accent:hover:decoration-primary-subtle',
|
||||
'py-2'
|
||||
)}
|
||||
>
|
||||
<Icon icon="file-pdf" className={tcls('size-4', 'mr-1.5')} />
|
||||
{t(language, 'pdf_download')}
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -7,7 +7,6 @@ import { t } from '@/intl/translate';
|
||||
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
|
||||
import type { AncestorRevisionPage } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
import { TrackPageViewEvent } from '../Insights';
|
||||
import { PageFeedbackForm } from '../PageFeedback';
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function PageHeader(props: {
|
||||
<li key={breadcrumb.id}>
|
||||
<StyledLink
|
||||
href={href}
|
||||
style={tcls(
|
||||
className={tcls(
|
||||
'no-underline',
|
||||
'hover:underline',
|
||||
'text-xs',
|
||||
|
||||
@@ -106,9 +106,8 @@ export function PageFeedbackForm(props: {
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => onSubmitComment(rating, comment)}
|
||||
>
|
||||
{t(languages, 'submit')}
|
||||
</Button>
|
||||
label={tString(languages, 'submit')}
|
||||
/>
|
||||
{comment.length > MAX_COMMENT_LENGTH * 0.8 ? (
|
||||
<span
|
||||
className={
|
||||
|
||||
@@ -1,62 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributeAnchorTarget, HTMLAttributes } from 'react';
|
||||
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { Link, type LinkInsightsProps } from './Link';
|
||||
|
||||
type ButtonProps = {
|
||||
href?: string;
|
||||
variant?: 'primary' | 'secondary';
|
||||
variant?: 'primary' | 'secondary' | 'blank';
|
||||
icon?: IconName;
|
||||
iconOnly?: boolean;
|
||||
size?: 'default' | 'medium' | 'small';
|
||||
className?: ClassValue;
|
||||
label?: string;
|
||||
} & LinkInsightsProps &
|
||||
HTMLAttributes<HTMLElement>;
|
||||
|
||||
const variantClasses = {
|
||||
primary: [
|
||||
'bg-primary-solid',
|
||||
'text-contrast-primary-solid',
|
||||
'hover:bg-primary-solid-hover',
|
||||
'hover:text-contrast-primary-solid-hover',
|
||||
'ring-0',
|
||||
'contrast-more:ring-1',
|
||||
],
|
||||
blank: [
|
||||
'bg-transparent',
|
||||
'text-tint',
|
||||
'ring-0',
|
||||
'shadow-none',
|
||||
'hover:bg-primary-hover',
|
||||
'hover:text-primary',
|
||||
'hover:scale-1',
|
||||
'hover:shadow-none',
|
||||
'contrast-more:bg-tint-subtle',
|
||||
],
|
||||
secondary: [
|
||||
'bg-tint',
|
||||
'text-tint',
|
||||
'hover:bg-tint-hover',
|
||||
'hover:text-primary',
|
||||
'contrast-more:bg-tint-subtle',
|
||||
],
|
||||
};
|
||||
|
||||
export function Button({
|
||||
href,
|
||||
children,
|
||||
variant = 'primary',
|
||||
size = 'default',
|
||||
className,
|
||||
insights,
|
||||
target,
|
||||
label,
|
||||
icon,
|
||||
iconOnly = false,
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
const variantClasses =
|
||||
variant === 'primary'
|
||||
? //PRIMARY
|
||||
[
|
||||
'bg-primary-solid',
|
||||
'text-contrast-primary-solid',
|
||||
'hover:bg-primary-solid-hover',
|
||||
'hover:text-contrast-primary-solid-hover',
|
||||
'ring-0',
|
||||
'contrast-more:ring-1',
|
||||
]
|
||||
: // SECONDARY
|
||||
[
|
||||
'bg-tint',
|
||||
'text-tint',
|
||||
'hover:bg-tint-hover',
|
||||
'hover:text-primary',
|
||||
'contrast-more:bg-tint-subtle',
|
||||
];
|
||||
|
||||
}: ButtonProps & { target?: HTMLAttributeAnchorTarget }) {
|
||||
const sizes = {
|
||||
default: ['text-base', 'px-4', 'py-2'],
|
||||
medium: ['text-sm', 'px-3', 'py-1.5'],
|
||||
small: ['text-xs', 'px-3 py-2'],
|
||||
small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'],
|
||||
};
|
||||
|
||||
const sizeClasses = sizes[size] || sizes.default;
|
||||
|
||||
const domClassName = tcls(
|
||||
'button',
|
||||
'inline-block',
|
||||
'inline-flex',
|
||||
'items-center',
|
||||
'gap-2',
|
||||
'rounded-md',
|
||||
'straight-corners:rounded-none',
|
||||
'place-self-start',
|
||||
// 'place-self-start',
|
||||
|
||||
'ring-1',
|
||||
'ring-tint',
|
||||
@@ -79,22 +97,31 @@ export function Button({
|
||||
'grow-0',
|
||||
'shrink-0',
|
||||
'truncate',
|
||||
variantClasses,
|
||||
variantClasses[variant],
|
||||
sizeClasses,
|
||||
className
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} className={domClassName} insights={insights} {...rest}>
|
||||
{children}
|
||||
<Link
|
||||
href={href}
|
||||
className={domClassName}
|
||||
insights={insights}
|
||||
aria-label={label}
|
||||
target={target}
|
||||
{...rest}
|
||||
>
|
||||
{icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
|
||||
{iconOnly ? null : label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className={domClassName} {...rest}>
|
||||
{children}
|
||||
<button type="button" className={domClassName} aria-label={label} {...rest}>
|
||||
{icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
|
||||
{iconOnly ? null : label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { SVGProps } from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
|
||||
export const Loading = ({
|
||||
busy = true,
|
||||
...props
|
||||
}: { busy?: boolean } & SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
width="100%"
|
||||
@@ -14,7 +17,11 @@ export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
className={tcls('animate-[pathLoading_2s_ease_infinite_forwards]')}
|
||||
className={tcls(
|
||||
busy
|
||||
? 'animate-[pathLoading_2s_ease_infinite_forwards]'
|
||||
: 'animate-[pathLoading_2s_ease_forwards]'
|
||||
)}
|
||||
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
|
||||
stroke="currentColor"
|
||||
pathLength="100"
|
||||
@@ -27,7 +34,8 @@ export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
|
||||
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
|
||||
stroke="currentColor"
|
||||
pathLength="100"
|
||||
strokeOpacity="0.24"
|
||||
strokeOpacity={busy ? 0.24 : 1}
|
||||
className="transition-opacity duration-1000"
|
||||
fill="none"
|
||||
strokeWidth="11"
|
||||
strokeLinecap="round"
|
||||
|
||||
@@ -25,11 +25,11 @@ export const linkStyles = [
|
||||
/**
|
||||
* Styled version of Link component.
|
||||
*/
|
||||
export function StyledLink(props: Omit<LinkProps, 'style'> & { style?: ClassValue }) {
|
||||
const { style, ...rest } = props;
|
||||
export function StyledLink(props: Omit<LinkProps, 'style'> & { className?: ClassValue }) {
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Link {...rest} className={tcls(linkStyles, style)}>
|
||||
<Link {...rest} className={tcls(linkStyles, className)}>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { getGitbookAppHref } from './links';
|
||||
import { resolvePageId } from './pages';
|
||||
import { findSiteSpaceById } from './sites';
|
||||
import type { ClassValue } from './tailwind';
|
||||
import { filterOutNullable } from './typescript';
|
||||
|
||||
export interface ResolvedContentRef {
|
||||
/** Text to render in the content ref */
|
||||
@@ -27,8 +28,12 @@ export interface ResolvedContentRef {
|
||||
subText?: string;
|
||||
/** Icon associated with it */
|
||||
icon?: React.ReactNode;
|
||||
/** ID of the content ref */
|
||||
id?: string;
|
||||
/** Emoji associated with the reference */
|
||||
emoji?: string;
|
||||
/** The content ref's ancestors */
|
||||
ancestors?: { icon?: React.ReactNode; label: string; href?: string }[];
|
||||
/** URL to open for the content ref */
|
||||
href: string;
|
||||
/** True if the content ref is active */
|
||||
@@ -115,6 +120,14 @@ export async function resolveContentRef(
|
||||
: resolvePageId(pages, contentRef.page);
|
||||
|
||||
const page = resolvePageResult?.page;
|
||||
const ancestors =
|
||||
resolvePageResult?.ancestors.map((ancestor) => ({
|
||||
label: ancestor.title,
|
||||
icon: <PageIcon page={ancestor} style={iconStyle} />,
|
||||
href: resolveAsAbsoluteURL
|
||||
? linker.toAbsoluteURL(linker.toPathForPage({ page: ancestor, pages }))
|
||||
: linker.toPathForPage({ page: ancestor, pages }),
|
||||
})) ?? [];
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
@@ -125,10 +138,16 @@ export async function resolveContentRef(
|
||||
let text = '';
|
||||
let icon: React.ReactNode | undefined = undefined;
|
||||
let emoji: string | undefined = undefined;
|
||||
const href = linker.toPathForPage({ page, pages, anchor });
|
||||
|
||||
// Compute the text to display for the link
|
||||
if (anchor) {
|
||||
text = `#${anchor}`;
|
||||
ancestors.push({
|
||||
label: page.title,
|
||||
icon: <PageIcon page={page} style={iconStyle} />,
|
||||
href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href,
|
||||
});
|
||||
|
||||
if (resolveAnchorText) {
|
||||
const document = await getPageDocument(dataFetcher, space, page);
|
||||
@@ -151,13 +170,14 @@ export async function resolveContentRef(
|
||||
icon = <PageIcon page={page} style={iconStyle} />;
|
||||
}
|
||||
|
||||
const href = linker.toPathForPage({ page, pages, anchor });
|
||||
|
||||
return {
|
||||
href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href,
|
||||
text,
|
||||
subText: page.description,
|
||||
ancestors: ancestors,
|
||||
emoji,
|
||||
icon,
|
||||
id: page.id,
|
||||
active: !anchor && page.id === activePage?.id,
|
||||
};
|
||||
}
|
||||
@@ -346,6 +366,12 @@ async function resolveContentRefInSpace(
|
||||
|
||||
return {
|
||||
...resolved,
|
||||
subText: space.title,
|
||||
ancestors: [
|
||||
{
|
||||
label: space.title,
|
||||
href: baseURL.toString(),
|
||||
},
|
||||
...(resolved.ancestors ?? []),
|
||||
].filter(filterOutNullable),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ const config: Config = {
|
||||
),
|
||||
},
|
||||
animation: {
|
||||
present: 'present .5s ease-out both',
|
||||
present: 'present 200ms cubic-bezier(0.25, 1, 0.5, 1) both',
|
||||
scaleIn: 'scaleIn 200ms ease',
|
||||
scaleOut: 'scaleOut 200ms ease',
|
||||
fadeIn: 'fadeIn 200ms ease forwards',
|
||||
@@ -330,7 +330,7 @@ const config: Config = {
|
||||
present: {
|
||||
from: {
|
||||
opacity: '0',
|
||||
transform: 'translateY(2rem) scale(0.9)',
|
||||
transform: 'translateY(1rem) scale(90%)',
|
||||
},
|
||||
to: {
|
||||
opacity: '1',
|
||||
|
||||
Reference in New Issue
Block a user