mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-21 04:35:24 +00:00
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>
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user