mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-25 11:52:10 +00:00
Support button actions (#3866)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add support for button actions
|
||||
@@ -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",
|
||||
@@ -727,7 +727,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "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"],
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 button;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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,
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user