From 37c1d4b8ea74b0c8ac6a938dca88100c37816b23 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Tue, 1 Apr 2025 01:47:10 +0200 Subject: [PATCH] Initial UI for tooltip experiment --- bun.lock | 5 + package.json | 3 + .../components/DocumentView/InlineLink.tsx | 207 ++++++++++++++++-- .../DocumentView/Table/RecordColumnValue.tsx | 2 +- .../src/components/PageBody/PageHeader.tsx | 2 +- .../src/components/primitives/Button.tsx | 20 +- .../src/components/primitives/StyledLink.tsx | 6 +- packages/gitbook/src/lib/references.tsx | 29 ++- packages/gitbook/tailwind.config.ts | 4 +- 9 files changed, 243 insertions(+), 35 deletions(-) diff --git a/bun.lock b/bun.lock index 3ac17c64c..51d88669a 100644 --- a/bun.lock +++ b/bun.lock @@ -3,6 +3,9 @@ "workspaces": { "": { "name": "gitbook", + "dependencies": { + "@radix-ui/react-tooltip": "^1.1.8", + }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@changesets/cli": "^2.27.12", @@ -836,6 +839,8 @@ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], diff --git a/package.json b/package.json index b4d134bad..570f5626e 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/packages/gitbook/src/components/DocumentView/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink.tsx index a12c59e08..700261e49 100644 --- a/packages/gitbook/src/components/DocumentView/InlineLink.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineLink.tsx @@ -2,7 +2,11 @@ import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api' import { resolveContentRef } from '@/lib/references'; -import { StyledLink } from '../primitives'; +import { tcls } from '@/lib/tailwind'; +import { Icon } from '@gitbook/icons'; +import * as Tooltip from '@radix-ui/react-tooltip'; +import { Fragment } from 'react'; +import { Button, StyledLink } from '../primitives'; import type { InlineProps } from './Inline'; import { Inlines } from './Inlines'; @@ -10,7 +14,9 @@ export async function InlineLink(props: InlineProps) { 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) { @@ -26,23 +32,186 @@ export async function InlineLink(props: InlineProps) { ); } + let breadcrumbs = resolved.ancestors; + const isExternal = inline.data.ref.kind === 'url'; + if (isExternal) { + breadcrumbs = [ + { + label: 'External link to', + }, + ]; + } + return ( - - - + + + + + + {isExternal ? ( + + ) : null} + + + + +
+
+ {breadcrumbs ? ( +
+
+ {breadcrumbs.map((crumb, index) => { + const Tag = crumb.href ? StyledLink : 'div'; + + return ( + + {index !== 0 ? ( + + ) : null} + + {crumb.icon ? ( + + {crumb.icon} + + ) : null} + {crumb.label} + + + ); + })} +
+ {resolved.href ? ( +
+ ) : null} +
+ {resolved.icon ? ( +
+ {resolved.icon} +
+ ) : null} +
{resolved.text}
+
+ {resolved.subText ? ( +

{resolved.subText}

+ ) : null} +
+ + {!isExternal ? ( +
+
+ +
AI Summary
+
+

+ This integration synchronizes your docs with the code + repository so that your API spec updates in GitBook mirror + any changes made in GitHub or GitLab, keeping your content + aligned with your code. +

+ +
+
+ + + Who can configure it? + +
+ Administrators and creators set it up. +
+
+
+ + + Does it sync both ways? + +
+ Edits in GitBook and commits on GitHub or GitLab + update each other. +
+
+
+ + + Which platforms connect? + +
+ It works with GitHub and GitLab. +
+
+
+ +
+
+
+ ) : null} +
+ +
+
+
+
); } diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx index 7c1ecb995..d5ddcdc49 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx @@ -168,7 +168,7 @@ export async function RecordColumnValue( 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 ? { diff --git a/packages/gitbook/src/components/PageBody/PageHeader.tsx b/packages/gitbook/src/components/PageBody/PageHeader.tsx index 42564c84e..f12a85a39 100644 --- a/packages/gitbook/src/components/PageBody/PageHeader.tsx +++ b/packages/gitbook/src/components/PageBody/PageHeader.tsx @@ -35,7 +35,7 @@ export async function PageHeader(props: {
  • - {icon ? : null} + {icon ? : null} {iconOnly ? null : label} ); @@ -112,7 +120,7 @@ export function Button({ return ( ); diff --git a/packages/gitbook/src/components/primitives/StyledLink.tsx b/packages/gitbook/src/components/primitives/StyledLink.tsx index 13b68f0e0..3fb346f5f 100644 --- a/packages/gitbook/src/components/primitives/StyledLink.tsx +++ b/packages/gitbook/src/components/primitives/StyledLink.tsx @@ -25,11 +25,11 @@ export const linkStyles = [ /** * Styled version of Link component. */ -export function StyledLink(props: Omit & { style?: ClassValue }) { - const { style, ...rest } = props; +export function StyledLink(props: Omit & { className?: ClassValue }) { + const { className, ...rest } = props; return ( - + {props.children} ); diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 83b5a6f09..3709bf5b5 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -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 */ @@ -29,6 +30,8 @@ export interface ResolvedContentRef { icon?: React.ReactNode; /** 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 +118,14 @@ export async function resolveContentRef( : resolvePageId(pages, contentRef.page); const page = resolvePageResult?.page; + const ancestors = + resolvePageResult?.ancestors.map((ancestor) => ({ + label: ancestor.title, + icon: , + href: resolveAsAbsoluteURL + ? linker.toAbsoluteURL(linker.toPathForPage({ page: ancestor, pages })) + : linker.toPathForPage({ page: ancestor, pages }), + })) ?? []; if (!page) { return null; } @@ -125,10 +136,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: , + href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href, + }); if (resolveAnchorText) { const document = await getPageDocument(dataFetcher, space, page); @@ -151,11 +168,11 @@ export async function resolveContentRef( icon = ; } - const href = linker.toPathForPage({ page, pages, anchor }); - return { href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href, text, + subText: page.description, + ancestors: ancestors, emoji, icon, active: !anchor && page.id === activePage?.id, @@ -346,6 +363,12 @@ async function resolveContentRefInSpace( return { ...resolved, - subText: space.title, + ancestors: [ + { + label: space.title, + href: baseURL, + }, + ...(resolved.ancestors ?? []), + ].filter(filterOutNullable), }; } diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 8af0fc22a..148f5cf30 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -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(20%) scale(90%)', }, to: { opacity: '1',