Skip to content

Commit ed9e8fe

Browse files
authored
Support button actions (#3866)
1 parent f478ddc commit ed9e8fe

File tree

6 files changed

+139
-37
lines changed

6 files changed

+139
-37
lines changed

.changeset/olive-parts-ring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": patch
3+
---
4+
5+
Add support for button actions

bun.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@
346346
"react-dom": "catalog:",
347347
},
348348
"catalog": {
349-
"@gitbook/api": "0.154.0",
349+
"@gitbook/api": "0.155.0",
350350
"@scalar/api-client-react": "^1.3.46",
351351
"@tsconfig/node20": "^20.1.6",
352352
"@tsconfig/strictest": "^2.0.6",
@@ -727,7 +727,7 @@
727727

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

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

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"catalog": {
4242
"@tsconfig/strictest": "^2.0.6",
4343
"@tsconfig/node20": "^20.1.6",
44-
"@gitbook/api": "0.154.0",
44+
"@gitbook/api": "0.155.0",
4545
"@scalar/api-client-react": "^1.3.46",
4646
"@types/react": "^19.0.0",
4747
"@types/react-dom": "^19.0.0",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use client';
2+
import { tString, useLanguage } from '@/intl/client';
3+
import { useAI, useAIChatController, useAIChatState } from '../AI';
4+
import { useSearch } from '../Search';
5+
import { Button, type ButtonProps, Input } from '../primitives';
6+
7+
export function InlineActionButton(
8+
props: { action: 'ask' | 'search'; query?: string } & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
9+
) {
10+
const { action, query, buttonProps } = props;
11+
12+
const { assistants } = useAI();
13+
const chatController = useAIChatController();
14+
const chatState = useAIChatState();
15+
const [, setSearchState] = useSearch();
16+
const language = useLanguage();
17+
18+
const handleSubmit = (value: string) => {
19+
if (action === 'ask') {
20+
chatController.open();
21+
if (value ?? query) {
22+
chatController.postMessage({ message: value ?? query });
23+
}
24+
} else if (action === 'search') {
25+
setSearchState((prev) => ({
26+
...prev,
27+
ask: null,
28+
scope: 'default',
29+
query: value ?? query,
30+
open: true,
31+
}));
32+
}
33+
};
34+
35+
const icon =
36+
action === 'ask' && buttonProps.icon === 'gitbook-assistant' && assistants.length > 0
37+
? assistants[0]?.icon
38+
: buttonProps.icon;
39+
40+
if (!query) {
41+
return (
42+
<Input
43+
inline
44+
label={buttonProps.label as string}
45+
sizing="medium"
46+
className="inline-flex max-w-full leading-normal [transition-property:translate,opacity,box-shadow,background,border]"
47+
submitButton={{
48+
label: tString(language, action === 'ask' ? 'send' : 'search'),
49+
}}
50+
clearButton={{
51+
className: 'text-[1em]',
52+
}}
53+
maxLength={action === 'ask' ? 2048 : 512}
54+
disabled={action === 'ask' && chatState.loading}
55+
aria-busy={action === 'ask' && chatState.loading}
56+
leading={icon}
57+
keyboardShortcut={false}
58+
onSubmit={(value) => handleSubmit(value as string)}
59+
containerStyle={{
60+
width: `${buttonProps.label ? buttonProps.label.toString().length + 10 : 20}ch`,
61+
}}
62+
/>
63+
);
64+
}
65+
66+
const label = action === 'ask' ? `Ask "${query}"` : `Search for "${query}"`;
67+
68+
const button = (
69+
<Button {...buttonProps} onClick={() => handleSubmit(query)} label={label}>
70+
{label !== buttonProps.label ? buttonProps.label : null}
71+
</Button>
72+
);
73+
74+
return button;
75+
}
Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,65 @@
11
import { resolveContentRef, resolveContentRefFallback } from '@/lib/references';
2-
import * as api from '@gitbook/api';
2+
import type * as api from '@gitbook/api';
33
import type { IconName } from '@gitbook/icons';
4-
import { Button } from '../primitives';
4+
import { Button, type ButtonProps } from '../primitives';
55
import type { InlineProps } from './Inline';
6+
import { InlineActionButton } from './InlineActionButton';
67
import { NotFoundRefHoverCard } from './NotFoundRefHoverCard';
78

8-
export async function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
9-
const { inline, context } = props;
9+
export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
10+
const { inline } = props;
1011

11-
const resolved = context.contentContext
12-
? await resolveContentRef(inline.data.ref, context.contentContext)
13-
: null;
12+
const buttonProps: ButtonProps = {
13+
label: inline.data.label,
14+
variant: inline.data.kind,
15+
icon: inline.data.icon as IconName | undefined,
16+
};
1417

15-
const href = resolved?.href ?? resolveContentRefFallback(inline.data.ref)?.href;
18+
const ButtonImplementation = () => {
19+
if ('action' in inline.data && 'query' in inline.data.action) {
20+
return (
21+
<InlineActionButton
22+
action={inline.data.action.action}
23+
query={inline.data.action.query ?? ''}
24+
buttonProps={buttonProps}
25+
/>
26+
);
27+
}
28+
29+
return <InlineLinkButton {...props} buttonProps={buttonProps} />;
30+
};
1631

1732
const inlineElement = (
1833
// Set the leading to have some vertical space between adjacent buttons
1934
<span className="inline-button leading-12 [&:has(+.inline-button)]:mr-2">
20-
<Button
21-
href={href}
22-
label={inline.data.label}
23-
// TODO: use a variant specifically for user-defined buttons.
24-
variant={inline.data.kind}
25-
className="leading-normal"
26-
disabled={href === undefined}
27-
icon={inline.data.icon as IconName | undefined}
28-
insights={{
29-
type: 'link_click',
30-
link: {
31-
target: inline.data.ref,
32-
position: api.SiteInsightsLinkPosition.Content,
33-
},
34-
}}
35-
/>
35+
<ButtonImplementation />
3636
</span>
3737
);
3838

39-
if (!resolved) {
40-
return <NotFoundRefHoverCard context={context}>{inlineElement}</NotFoundRefHoverCard>;
39+
return inlineElement;
40+
}
41+
42+
export async function InlineLinkButton(
43+
props: InlineProps<api.DocumentInlineButton> & { buttonProps: ButtonProps }
44+
) {
45+
const { inline, context, buttonProps } = props;
46+
47+
if (!('ref' in inline.data)) return;
48+
49+
const resolved =
50+
context.contentContext && inline.data.ref
51+
? await resolveContentRef(inline.data.ref, context.contentContext)
52+
: null;
53+
54+
const href =
55+
resolved?.href ??
56+
(inline.data.ref ? resolveContentRefFallback(inline.data.ref)?.href : undefined);
57+
58+
const button = <Button {...buttonProps} href={href} disabled={href === undefined} />;
59+
60+
if (inline.data.ref && !resolved) {
61+
return <NotFoundRefHoverCard context={context}>{button}</NotFoundRefHoverCard>;
4162
}
4263

43-
return inlineElement;
64+
return button;
4465
}

packages/gitbook/src/components/primitives/Input.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type CustomInputProps = {
1515
trailing?: React.ReactNode;
1616
sizing?: 'medium' | 'large'; // The `size` prop is already taken by the HTML input element.
1717
containerRef?: React.RefObject<HTMLDivElement | null>;
18+
containerStyle?: React.CSSProperties;
1819
/**
1920
* A submit button, shown to the right of the input.
2021
*/
@@ -63,6 +64,7 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
6364
keyboardShortcut,
6465
onSubmit,
6566
containerRef,
67+
containerStyle,
6668
resize = false,
6769
// HTML attributes we need to read
6870
value: passedValue,
@@ -153,7 +155,7 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
153155
};
154156

155157
const inputClassName = tcls(
156-
'peer -m-2 max-h-64 grow resize-none text-left outline-none placeholder:text-tint/8 aria-busy:cursor-progress',
158+
'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',
157159
sizes[sizing].input
158160
);
159161

@@ -197,10 +199,11 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
197199
}
198200
}}
199201
ref={containerRef}
202+
style={containerStyle}
200203
>
201204
<Tag
202205
className={tcls(
203-
'flex grow',
206+
'flex shrink grow',
204207
sizes[sizing].gap,
205208
multiline ? 'items-start' : 'items-center'
206209
)}
@@ -209,14 +212,12 @@ export const Input = React.forwardRef<InputElement, InputProps>((props, passedRe
209212
<Tag
210213
className={tcls(
211214
clearButton && hasValue ? 'group-focus-within/input:hidden' : '',
212-
multiline ? 'my-1.25' : ''
215+
multiline ? 'my-1.25' : '',
216+
'text-tint'
213217
)}
214218
>
215219
{typeof leading === 'string' ? (
216-
<Icon
217-
icon={leading as IconName}
218-
className="size-4 shrink-0 text-tint"
219-
/>
220+
<Icon icon={leading as IconName} className="size-4 shrink-0" />
220221
) : (
221222
leading
222223
)}

0 commit comments

Comments
 (0)