Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-parts-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gitbook": patch
---

Add support for button actions
4 changes: 2 additions & 2 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@
"react-dom": "catalog:",
},
"catalog": {
"@gitbook/api": "0.154.0",
"@gitbook/api": "0.155.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
Expand Down Expand Up @@ -727,7 +727,7 @@

"@fortawesome/fontawesome-svg-core": ["@fortawesome/[email protected]", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],

"@gitbook/api": ["@gitbook/api@0.154.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-huEgWWa2H+H4ofYEAQl9r49u0gVjDHHWVNJKhd6SgGCfefjNPHkQj9zaky/5DBQzlu/+SDmutWiOtYdrykQO8w=="],
"@gitbook/api": ["@gitbook/api@0.155.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-g4UfJRnej/GZ9pORHcS9mV0kKiJLRvhMlGOeOoEVU/LSIz6s35VDqauSBs7rH00AOaMABDg7uEko8tUSA4aaww=="],

"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.154.0",
"@gitbook/api": "0.155.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client';
import { tString, useLanguage } from '@/intl/client';
import { useAI, useAIChatController, useAIChatState } from '../AI';
import { useSearch } from '../Search';
import { Button, type ButtonProps, Input } from '../primitives';

export function InlineActionButton(
props: { action: 'ask' | 'search'; query?: string } & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
) {
const { action, query, buttonProps } = props;

const { assistants } = useAI();
const chatController = useAIChatController();
const chatState = useAIChatState();
const [, setSearchState] = useSearch();
const language = useLanguage();

const handleSubmit = (value: string) => {
if (action === 'ask') {
chatController.open();
if (value ?? query) {
chatController.postMessage({ message: value ?? query });
}
} else if (action === 'search') {
setSearchState((prev) => ({
...prev,
ask: null,
scope: 'default',
query: value ?? query,
open: true,
}));
}
};

const icon =
action === 'ask' && buttonProps.icon === 'gitbook-assistant' && assistants.length > 0
? assistants[0]?.icon
: buttonProps.icon;

if (!query) {
return (
<Input
inline
label={buttonProps.label as string}
sizing="medium"
className="inline-flex max-w-full leading-normal [transition-property:translate,opacity,box-shadow,background,border]"
submitButton={{
label: tString(language, action === 'ask' ? 'send' : 'search'),
}}
clearButton={{
className: 'text-[1em]',
}}
maxLength={action === 'ask' ? 2048 : 512}
disabled={action === 'ask' && chatState.loading}
aria-busy={action === 'ask' && chatState.loading}
leading={icon}
keyboardShortcut={false}
onSubmit={(value) => handleSubmit(value as string)}
containerStyle={{
width: `${buttonProps.label ? buttonProps.label.toString().length + 10 : 20}ch`,
}}
/>
);
}

const label = action === 'ask' ? `Ask "${query}"` : `Search for "${query}"`;

const button = (
<Button {...buttonProps} onClick={() => handleSubmit(query)} label={label}>
{label !== buttonProps.label ? buttonProps.label : null}
</Button>
);

return button;
}
75 changes: 48 additions & 27 deletions packages/gitbook/src/components/DocumentView/InlineButton.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,65 @@
import { resolveContentRef, resolveContentRefFallback } from '@/lib/references';
import * as api from '@gitbook/api';
import type * as api from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
import { Button } from '../primitives';
import { Button, type ButtonProps } from '../primitives';
import type { InlineProps } from './Inline';
import { InlineActionButton } from './InlineActionButton';
import { NotFoundRefHoverCard } from './NotFoundRefHoverCard';

export async function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
const { inline, context } = props;
export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
const { inline } = props;

const resolved = context.contentContext
? await resolveContentRef(inline.data.ref, context.contentContext)
: null;
const buttonProps: ButtonProps = {
label: inline.data.label,
variant: inline.data.kind,
icon: inline.data.icon as IconName | undefined,
};

const href = resolved?.href ?? resolveContentRefFallback(inline.data.ref)?.href;
const ButtonImplementation = () => {
if ('action' in inline.data && 'query' in inline.data.action) {
return (
<InlineActionButton
action={inline.data.action.action}
query={inline.data.action.query ?? ''}
buttonProps={buttonProps}
/>
);
}

return <InlineLinkButton {...props} buttonProps={buttonProps} />;
};

const inlineElement = (
// Set the leading to have some vertical space between adjacent buttons
<span className="inline-button leading-12 [&:has(+.inline-button)]:mr-2">
<Button
href={href}
label={inline.data.label}
// TODO: use a variant specifically for user-defined buttons.
variant={inline.data.kind}
className="leading-normal"
disabled={href === undefined}
icon={inline.data.icon as IconName | undefined}
insights={{
type: 'link_click',
link: {
target: inline.data.ref,
position: api.SiteInsightsLinkPosition.Content,
},
}}
/>
<ButtonImplementation />
</span>
);

if (!resolved) {
return <NotFoundRefHoverCard context={context}>{inlineElement}</NotFoundRefHoverCard>;
return inlineElement;
}

export async function InlineLinkButton(
props: InlineProps<api.DocumentInlineButton> & { buttonProps: ButtonProps }
) {
const { inline, context, buttonProps } = props;

if (!('ref' in inline.data)) return;

const resolved =
context.contentContext && inline.data.ref
? await resolveContentRef(inline.data.ref, context.contentContext)
: null;

const href =
resolved?.href ??
(inline.data.ref ? resolveContentRefFallback(inline.data.ref)?.href : undefined);

const button = <Button {...buttonProps} href={href} disabled={href === undefined} />;

if (inline.data.ref && !resolved) {
return <NotFoundRefHoverCard context={context}>{button}</NotFoundRefHoverCard>;
}

return inlineElement;
return button;
}
15 changes: 8 additions & 7 deletions packages/gitbook/src/components/primitives/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type CustomInputProps = {
trailing?: React.ReactNode;
sizing?: 'medium' | 'large'; // The `size` prop is already taken by the HTML input element.
containerRef?: React.RefObject<HTMLDivElement | null>;
containerStyle?: React.CSSProperties;
/**
* A submit button, shown to the right of the input.
*/
Expand Down Expand Up @@ -63,6 +64,7 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
keyboardShortcut,
onSubmit,
containerRef,
containerStyle,
resize = false,
// HTML attributes we need to read
value: passedValue,
Expand Down Expand Up @@ -153,7 +155,7 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
};

const inputClassName = tcls(
'peer -m-2 max-h-64 grow resize-none text-left outline-none placeholder:text-tint/8 aria-busy:cursor-progress',
'peer -m-2 max-h-64 grow shrink resize-none text-left outline-none placeholder:text-tint/8 placeholder-shown:text-ellipsis aria-busy:cursor-progress',
sizes[sizing].input
);

Expand Down Expand Up @@ -197,10 +199,11 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
}
}}
ref={containerRef}
style={containerStyle}
>
<Tag
className={tcls(
'flex grow',
'flex shrink grow',
sizes[sizing].gap,
multiline ? 'items-start' : 'items-center'
)}
Expand All @@ -209,14 +212,12 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
<Tag
className={tcls(
clearButton && hasValue ? 'group-focus-within/input:hidden' : '',
multiline ? 'my-1.25' : ''
multiline ? 'my-1.25' : '',
'text-tint'
)}
>
{typeof leading === 'string' ? (
<Icon
icon={leading as IconName}
className="size-4 shrink-0 text-tint"
/>
<Icon icon={leading as IconName} className="size-4 shrink-0" />
) : (
leading
)}
Expand Down