Skip to content

Commit f478ddc

Browse files
authored
Create <Input> component (#3857)
1 parent bd11c0d commit f478ddc

File tree

10 files changed

+510
-204
lines changed

10 files changed

+510
-204
lines changed

.changeset/cool-pigs-wink.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 Input component

packages/gitbook/src/components/AIChat/AIChat.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ export function AIChatBody(props: {
201201
const { chatController, chat, suggestions, greeting } = props;
202202
const { trademark } = useAI().config;
203203

204-
const [input, setInput] = React.useState('');
205204
const language = useLanguage();
206205
const now = useNow(60 * 60 * 1000); // Refresh every hour for greeting
207206

@@ -271,13 +270,10 @@ export function AIChatBody(props: {
271270
{chat.error ? <AIChatError chatController={chatController} /> : null}
272271

273272
<AIChatInput
274-
value={input}
275-
onChange={setInput}
276273
loading={chat.loading}
277274
disabled={chat.loading || chat.error}
278-
onSubmit={() => {
279-
chatController.postMessage({ message: input });
280-
setInput('');
275+
onSubmit={(value) => {
276+
chatController.postMessage({ message: value });
281277
}}
282278
/>
283279
</div>
Lines changed: 34 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,26 @@
11
import { t, tString, useLanguage } from '@/intl/client';
2-
import { tcls } from '@/lib/tailwind';
32
import { Icon } from '@gitbook/icons';
43
import { useEffect, useRef } from 'react';
54
import { useHotkeys } from 'react-hotkeys-hook';
65
import { useAIChatState } from '../AI/useAIChat';
7-
import { Button, HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
8-
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
6+
import { HoverCard, HoverCardRoot, HoverCardTrigger } from '../primitives';
7+
import { Input } from '../primitives/Input';
98

109
export function AIChatInput(props: {
11-
value: string;
1210
disabled?: boolean;
1311
/**
1412
* When true, the input is disabled
1513
*/
1614
loading: boolean;
17-
onChange: (value: string) => void;
1815
onSubmit: (value: string) => void;
1916
}) {
20-
const { value, onChange, onSubmit, disabled, loading } = props;
17+
const { onSubmit, disabled, loading } = props;
2118

2219
const language = useLanguage();
2320
const chat = useAIChatState();
2421

2522
const inputRef = useRef<HTMLTextAreaElement>(null);
2623

27-
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
28-
const textarea = event.currentTarget;
29-
onChange(textarea.value);
30-
31-
// Auto-resize
32-
textarea.style.height = 'auto';
33-
textarea.style.height = `${textarea.scrollHeight}px`;
34-
};
35-
3624
useEffect(() => {
3725
if (chat.opened && !disabled && !loading) {
3826
// Add a small delay to ensure the input is rendered before focusing
@@ -57,57 +45,33 @@ export function AIChatInput(props: {
5745
);
5846

5947
return (
60-
<div className="depth-subtle:has-[textarea:focus]:-translate-y-px relative flex animate-blur-in-slow flex-col overflow-hidden circular-corners:rounded-3xl rounded-corners:rounded-xl bg-tint-base/9 depth-subtle:shadow-sm shadow-tint/6 ring-1 ring-tint-subtle backdrop-blur-lg transition-all depth-subtle:has-[textarea:focus]:shadow-lg has-[textarea:focus]:shadow-primary-subtle has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-hover contrast-more:bg-tint-base dark:shadow-tint-1">
61-
<textarea
62-
ref={inputRef}
63-
disabled={disabled || loading}
64-
data-loading={loading}
65-
data-testid="ai-chat-input"
66-
className={tcls(
67-
'resize-none',
68-
'focus:outline-hidden',
69-
'focus:ring-0',
70-
'w-full',
71-
'px-3',
72-
'py-3',
73-
'pb-12',
74-
'h-auto',
75-
'bg-transparent',
76-
'peer',
77-
'max-h-64',
78-
'placeholder:text-tint/8',
79-
'transition-colors',
80-
'disabled:bg-tint-subtle',
81-
'delay-300',
82-
'disabled:delay-0',
83-
'disabled:cursor-not-allowed',
84-
'data-[loading=true]:cursor-progress',
85-
'data-[loading=true]:opacity-50'
86-
)}
87-
value={value}
88-
rows={1}
89-
placeholder={tString(language, 'ai_chat_input_placeholder')}
90-
onChange={handleInput}
91-
onKeyDown={(event) => {
92-
if (event.key === 'Escape') {
93-
event.preventDefault();
94-
event.currentTarget.blur();
95-
return;
96-
}
97-
98-
if (event.key === 'Enter' && !event.shiftKey && value.trim()) {
99-
event.preventDefault();
100-
event.currentTarget.style.height = 'auto';
101-
onSubmit(value);
102-
}
103-
}}
104-
/>
105-
{!disabled ? (
106-
<div className="absolute top-2.5 right-3 animate-[fadeIn_0.2s_0.5s_ease-in-out_both] peer-focus:hidden">
107-
<KeyboardShortcut keys={['mod', 'i']} className="bg-tint-base" />
108-
</div>
109-
) : null}
110-
<div className="absolute inset-x-0 bottom-0 flex items-center gap-2 px-2 py-2">
48+
<Input
49+
data-testid="ai-chat-input"
50+
name="ai-chat-input"
51+
multiline
52+
resize
53+
sizing="large"
54+
label="Assistant chat input"
55+
placeholder={tString(language, 'ai_chat_input_placeholder')}
56+
onSubmit={(val) => onSubmit(val as string)}
57+
submitButton={{
58+
label: tString(language, 'send'),
59+
}}
60+
className="animate-blur-in-slow bg-tint-base/9 backdrop-blur-lg contrast-more:bg-tint-base"
61+
rows={1}
62+
maxLength={2048}
63+
keyboardShortcut={
64+
!disabled && !loading
65+
? {
66+
keys: ['mod', 'i'],
67+
className: 'bg-tint-base group-focus-within/input:hidden',
68+
}
69+
: undefined
70+
}
71+
disabled={disabled || loading}
72+
aria-busy={loading}
73+
ref={inputRef}
74+
trailing={
11175
<HoverCardRoot openDelay={500}>
11276
<HoverCard
11377
className="max-w-xs bg-tint p-2 text-sm text-tint"
@@ -135,7 +99,8 @@ export function AIChatInput(props: {
13599
</div>
136100
</HoverCard>
137101
<HoverCardTrigger>
138-
<div className="flex cursor-help items-center gap-1 circular-corners:rounded-2xl rounded-corners:rounded-md px-2.5 py-1.5 text-tint/7 text-xs transition-all hover:bg-tint">
102+
{/* Negative margin to compensate for Input's padding, so the badge appears flush with the cursor */}
103+
<div className="-ml-1 flex cursor-help items-center gap-1 circular-corners:rounded-2xl rounded-corners:rounded-md px-2.5 py-1.5 text-tint/7 text-xs transition-all hover:bg-tint">
139104
<span className="-ml-1 circular-corners:rounded-2xl rounded-corners:rounded-sm bg-tint-11/7 px-1 py-0.5 font-mono font-semibold text-[0.65rem] text-contrast-tint-11 leading-none">
140105
{t(language, 'ai_chat_context_badge')}
141106
</span>{' '}
@@ -146,14 +111,7 @@ export function AIChatInput(props: {
146111
</div>
147112
</HoverCardTrigger>
148113
</HoverCardRoot>
149-
<Button
150-
label={tString(language, 'send')}
151-
size="medium"
152-
className="ml-auto"
153-
disabled={disabled || !value.trim()}
154-
onClick={() => onSubmit(value)}
155-
/>
156-
</div>
157-
</div>
114+
}
115+
/>
158116
);
159117
}

packages/gitbook/src/components/DocumentView/Integration/contentkit.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,10 @@
156156

157157
/** Text input */
158158
.contentkit-textinput {
159-
@apply w-full rounded border border-tint text-tint-strong placeholder:text-tint flex resize-none flex-1 px-2 py-1.5 text-sm bg-transparent whitespace-pre-line;
160-
@apply focus:outline-primary focus:border-primary;
159+
@apply w-full circular-corners:rounded-3xl ring-primary-hover rounded-corners:rounded-lg border border-tint text-tint-strong transition-all placeholder:text-tint/8 flex resize-none flex-1 px-2 py-1.5 text-sm bg-tint-base whitespace-pre-line;
160+
@apply shadow-tint/6 depth-subtle:focus-within:-translate-y-px depth-subtle:shadow-sm depth-subtle:focus-within:shadow-lg dark:shadow-tint-1;
161+
@apply focus:border-primary-hover focus:shadow-primary-subtle focus:ring-2 hover:border-tint-hover focus:hover:border-primary-hover;
162+
@apply disabled:cursor-not-allowed disabled:border-tint-subtle disabled:bg-tint-subtle;
161163
}
162164

163165
/** Form */

packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import React, { type ButtonHTMLAttributes } from 'react';
66
import { useLanguage } from '@/intl/client';
77
import { t, tString } from '@/intl/translate';
88
import { tcls } from '@/lib/tailwind';
9-
109
import { useTrackEvent } from '../Insights';
11-
import { Button, ButtonGroup } from '../primitives';
10+
import { Button, ButtonGroup, Input } from '../primitives';
1211

12+
const MIN_COMMENT_LENGTH = 3;
1313
const MAX_COMMENT_LENGTH = 512;
1414

1515
/**
@@ -24,7 +24,6 @@ export function PageFeedbackForm(props: {
2424
const trackEvent = useTrackEvent();
2525
const inputRef = React.useRef<HTMLTextAreaElement>(null);
2626
const [rating, setRating] = React.useState<PageFeedbackRating>();
27-
const [comment, setComment] = React.useState('');
2827
const [submitted, setSubmitted] = React.useState(false);
2928

3029
const onSubmitRating = (rating: PageFeedbackRating) => {
@@ -86,43 +85,21 @@ export function PageFeedbackForm(props: {
8685
</ButtonGroup>
8786
</div>
8887
{rating ? (
89-
<div className="flex flex-col gap-2">
90-
{!submitted ? (
91-
<>
92-
<textarea
93-
ref={inputRef}
94-
name="comment"
95-
className="mx-0.5 max-h-40 min-h-16 grow rounded-sm straight-corners:rounded-none bg-tint-base p-2 ring-1 ring-tint ring-inset placeholder:text-sm placeholder:text-tint contrast-more:ring-tint-12 contrast-more:placeholder:text-tint-strong"
96-
placeholder={tString(languages, 'was_this_helpful_comment')}
97-
aria-label={tString(languages, 'was_this_helpful_comment')}
98-
onChange={(e) => setComment(e.target.value)}
99-
value={comment}
100-
rows={3}
101-
maxLength={MAX_COMMENT_LENGTH}
102-
/>
103-
<div className="flex items-center justify-between gap-4">
104-
<Button
105-
size="small"
106-
onClick={() => onSubmitComment(rating, comment)}
107-
label={tString(languages, 'submit')}
108-
/>
109-
{comment.length > MAX_COMMENT_LENGTH * 0.8 ? (
110-
<span
111-
className={
112-
comment.length === MAX_COMMENT_LENGTH
113-
? 'text-red-500'
114-
: ''
115-
}
116-
>
117-
{comment.length} / {MAX_COMMENT_LENGTH}
118-
</span>
119-
) : null}
120-
</div>
121-
</>
122-
) : (
123-
<p>{t(languages, 'was_this_helpful_thank_you')}</p>
124-
)}
125-
</div>
88+
<Input
89+
ref={inputRef}
90+
label={tString(languages, 'was_this_helpful_comment')}
91+
multiline
92+
submitButton
93+
rows={3}
94+
name="page-feedback-comment"
95+
onSubmit={(comment) => onSubmitComment(rating, comment as string)}
96+
maxLength={MAX_COMMENT_LENGTH}
97+
minLength={MIN_COMMENT_LENGTH}
98+
disabled={submitted}
99+
submitMessage={tString(languages, 'was_this_helpful_thank_you')}
100+
className="animate-blur-in"
101+
resize
102+
/>
126103
) : null}
127104
</div>
128105
);

0 commit comments

Comments
 (0)