Compare commits

...

2 Commits

Author SHA1 Message Date
Viktor Renkema a7015bbf72 changeset
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:03:57 +02:00
Viktor Renkema 3ed4fb070a Mark blocks, pages and titles as comment targets
Adds data attributes (block keys, table cells, page and page title) and
ignore markers for columns, annotations and reusable content, so a
commenting layer can resolve what was clicked. No visible change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:03:57 +02:00
28 changed files with 105 additions and 31 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Mark blocks, pages and titles so comments can target them.
@@ -21,13 +21,16 @@ export function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
return (
<AnnotationPopover
body={
<Blocks
document={document}
ancestorBlocks={[]}
context={context}
nodes={fragment.nodes}
style={['contents']}
/>
// The app doesn't allow comments inside annotation bodies.
<div data-comment-ignore="" className="contents">
<Blocks
document={document}
ancestorBlocks={[]}
context={context}
nodes={fragment.nodes}
style={['contents']}
/>
</div>
}
>
{content}
@@ -31,6 +31,7 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
return (
<NotFoundRefHoverCard context={context}>
<BlockContentRefCard
data-block-key={block.key}
contentRef={block.data.ref}
resolved={fallback}
style={style}
@@ -39,7 +40,14 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
);
}
return <BlockContentRefCard contentRef={block.data.ref} resolved={resolved} style={style} />;
return (
<BlockContentRefCard
data-block-key={block.key}
contentRef={block.data.ref}
resolved={resolved}
style={style}
/>
);
}
function BlockContentRefCard(
@@ -34,6 +34,8 @@ export function Caption(
block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile;
withBorder?: boolean;
withFrame?: boolean;
/** Expose the block key for commenting; off for images nested in an images block. */
withBlockKey?: boolean;
} & DocumentContextProps
) {
const {
@@ -59,8 +61,11 @@ export function Caption(
],
style,
align = 'center',
withBlockKey = false,
} = props;
const blockKey = withBlockKey ? block.key : undefined;
const caption = getNodeFragmentByName(block, 'caption');
const captionParagraph = caption?.nodes[0];
@@ -69,11 +74,15 @@ export function Caption(
captionParagraph.type !== 'paragraph' ||
isNodeEmpty(captionParagraph)
) {
return <div className={tcls(style, wrapperStyle)}>{children}</div>;
return (
<div data-block-key={blockKey} className={tcls(style, wrapperStyle)}>
{children}
</div>
);
}
return (
<picture className={tcls('relative', style)}>
<picture data-block-key={blockKey} className={tcls('relative', style)}>
<div className={tcls(wrapperStyle, captionAlignStyles[align])}>{children}</div>
<figcaption
className={tcls(
@@ -50,6 +50,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
<div
ref={ref}
id={codeId}
data-block-key={block.key}
aria-busy={ariaBusy}
className={tcls(
'group/codeblock shiki relative flex shrink flex-col overflow-hidden print:overflow-visible',
@@ -218,7 +218,12 @@ export function MermaidCodeBlock(
return (
<>
{/* Inline slot: hosts the diagram in the document flow until it goes fullscreen. */}
<div ref={rootRef} className={tcls('relative', style)} contentEditable={false} />
<div
ref={rootRef}
data-block-key={block.key}
className={tcls('relative', style)}
contentEditable={false}
/>
{diagramHostRef.current ? createPortal(diagram, diagramHostRef.current) : null}
<Dialog.Root
open={isFullscreen}
@@ -37,6 +37,8 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) {
return (
<div
// The app doesn't allow comments inside columns.
data-comment-ignore=""
className={tcls(
'grid w-full @2xl:grid-flow-col @2xl:grid-cols-[repeat(var(--grid-slices),minmax(0,1fr))] grid-cols-1 @2xl:grid-rows-1 gap-x-8 gap-y-4',
style
@@ -18,7 +18,7 @@ export async function Drawing(props: BlockProps<DocumentBlockDrawing>) {
}
return (
<Caption {...props}>
<Caption {...props} withBlockKey>
<Image
sources={{
light: {
@@ -32,7 +32,7 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
}
return (
<Caption {...props} withBorder>
<Caption {...props} withBorder withBlockKey>
{embed.type === 'rich' ? (
<>
<div
@@ -14,6 +14,8 @@ export function Details(props: {
contentIds?: string[];
open?: boolean;
className?: ClassValue;
/** Node key of the expandable, so comments can anchor to it. */
blockKey?: string;
}) {
const { children, id, className } = props;
@@ -46,6 +48,7 @@ export function Details(props: {
<details
ref={ref}
id={id}
data-block-key={props.blockKey}
open={props.open || openFromHash}
className={tcls(
className,
@@ -37,10 +37,12 @@ export async function Expandable(props: BlockProps<DocumentBlockExpandable>) {
return (
<Details
id={id}
blockKey={block.key}
open={context.mode === 'print' || isDefaultExpanded}
className={tcls('scroll-mt-(--content-scroll-margin)', style)}
>
<summary
data-block-key={titleParagraph.key}
className={tcls(
'cursor-pointer',
'px-4',
@@ -40,7 +40,7 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
};
return (
<Caption {...props} withBorder>
<Caption {...props} withBorder withBlockKey>
<div className="flex flex-wrap items-center gap-5 px-5 py-3">
<div className="flex min-h-8 min-w-14 flex-col items-center justify-center gap-1 border-r border-tint-subtle pr-4">
{contentType === 'image' ? (
@@ -30,6 +30,7 @@ export async function Heading(props: BlockProps<DocumentBlockHeading>) {
<HeadingRevealWrapper
as={Tag}
id={id}
data-block-key={block.key}
className={tcls(
textStyle.textSize,
'heading',
@@ -15,6 +15,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
return (
<div
data-block-key={block.key}
className={tcls(
style,
'flex',
@@ -61,7 +61,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
}
return (
<div className={tcls(style)}>
<div data-block-key={block.key} className={tcls(style)}>
<pre>
Unexpected error with integration {block.data.integration}:{' '}
{initialResponse.error.message}
@@ -99,7 +99,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
};
return (
<div className={tcls(style)}>
<div data-block-key={block.key} className={tcls(style)}>
<ContentKitWithClientContext
{...contentKitProps}
canAccessVisitorClaims={canAccessVisitorClaims}
@@ -125,7 +125,14 @@ function getListItemDepth(input: {
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>;
return (
<li
data-block-key={props.block.key}
className={tcls(textStyle.lineHeight, 'flex items-start')}
>
{props.children}
</li>
);
}
function ListItemPrefix(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
@@ -11,13 +11,16 @@ const assetsUrl = getAssetURL('math');
export async function BlockMath(props: BlockProps<DocumentBlockMath>) {
const { block, style } = props;
// `MathFormula` only forwards `className`, so the comment anchor needs its own wrapper.
return (
<MathFormula
formula={block.data.formula}
inline={false}
className={tcls(style, 'overflow-x-auto')}
assetsUrl={assetsUrl}
/>
<div data-block-key={block.key} className={tcls(style)}>
<MathFormula
formula={block.data.formula}
inline={false}
className="overflow-x-auto"
assetsUrl={assetsUrl}
/>
</div>
);
}
@@ -9,9 +9,12 @@ import { tcls } from '@/lib/tailwind';
* Render an openapi block or an openapi-operation block.
*/
export async function OpenAPIOperation(props: BlockProps<AnyOpenAPIOperationsBlock>) {
const { style } = props;
const { style, block } = props;
return (
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<div
data-block-key={block.key}
className={tcls('flex w-full min-w-0', style, 'max-w-full')}
>
<OpenAPIOperationBody {...props} />
</div>
);
@@ -9,9 +9,9 @@ import { tcls } from '@/lib/tailwind';
* Render an openapi-schemas block.
*/
export async function OpenAPISchemas(props: BlockProps<OpenAPISchemasBlock>) {
const { style } = props;
const { style, block } = props;
return (
<div className={tcls('flex w-full', style, 'max-w-full')}>
<div data-block-key={block.key} className={tcls('flex w-full', style, 'max-w-full')}>
<OpenAPISchemasBody {...props} />
</div>
);
@@ -9,9 +9,12 @@ import { tcls } from '@/lib/tailwind';
* Render an openapi block or an openapi-webhook block.
*/
export async function OpenAPIWebhook(props: BlockProps<OpenAPIWebhookBlock>) {
const { style } = props;
const { style, block } = props;
return (
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<div
data-block-key={block.key}
className={tcls('flex w-full min-w-0', style, 'max-w-full')}
>
<OpenAPIWebhookBody {...props} />
</div>
);
@@ -10,9 +10,12 @@ import { tcls } from '@/lib/tailwind';
export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
const { block, style, ancestorBlocks, ...contextProps } = props;
const { context } = contextProps;
// Like the app, a list item owns the comments on its direct paragraph.
const isCommentTarget = ancestorBlocks.at(-1)?.type !== 'list-item';
const paragraph = (
<p
data-block-key={isCommentTarget ? block.key : undefined}
// Cover-aware contrast text applies only to the page body, not to documents
// rendered in overlays (search answers, AI chat) on a background-cover page.
data-cover-aware-text={context.isPageBody ? '' : undefined}
@@ -50,6 +50,9 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
nodes={reusableContentDocument.nodes}
document={reusableContentDocument}
ancestorBlocks={[...ancestorBlocks, block]}
// Its blocks carry keys of the reusable content's own document, which comments on the
// page can't anchor to. A class, not a wrapper, which would break the page's spacing.
blockStyle="comment-ignore"
context={{
...context,
contentContext: reusableContent.context,
@@ -32,7 +32,7 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
})();
return (
<div className={tcls('flex flex-row gap-4 md:gap-8', style)}>
<div data-block-key={block.key} className={tcls('flex flex-row gap-4 md:gap-8', style)}>
<div className="relative select-none">
<div
className={tcls(
@@ -219,6 +219,8 @@ function NativeViewGridBody(
colSpan={colSpan}
aria-rowspan={rowSpan}
aria-colspan={colSpan}
data-table-record={recordId}
data-table-column={column}
data-table-row-start={recordIndex}
data-table-row-end={
recordIndex + (cellMerge?.merge.rowSpan ?? 1) - 1
@@ -54,6 +54,8 @@ export function RecordRow(
<div
key={column}
role="cell"
data-table-record={record[0]}
data-table-column={column}
aria-colspan={cellMerge?.merge.colSpan}
className={tcls(
'relative flex flex-1 border-r px-3 py-2 align-middle text-sm last:border-r-0',
@@ -74,7 +74,11 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
block.data.view.type === 'grid' ? cellMergeLayout.recordGroups : undefined
}
>
<div className={tcls(style, 'flex flex-col gap-3')}>
<div
data-block-key={block.key}
data-comment-table=""
className={tcls(style, 'flex flex-col gap-3')}
>
{showSearch ? (
<TableSearchInput
selectColumns={selectColumns}
@@ -100,6 +100,8 @@ export async function PageBody(props: {
/>
) : null}
<main
// Comments on the whole page target anywhere in it that isn't a block.
data-comment-page=""
className={tcls(
'relative min-w-0 flex-1',
'break-anywhere', // Allow words to break if they are too long.
@@ -255,6 +255,8 @@ export async function PageHeader(props: {
<PageTags page={page} revision={revision} />
{page.layout.title ? (
<h1
// Comments on the whole page get their pin next to the title.
data-comment-page-title=""
data-cover-aware-text
data-over-cover
className={tcls(