Skip to content
Merged
12 changes: 12 additions & 0 deletions src/pages/playground.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BannerCard } from '@shared/ui/BannerCard/BannerCard';
import { Button } from '@shared/ui/Button/Button';
import { Card } from '@shared/ui/Card/Card';
import { ChatInput } from '@shared/ui/ChatInput';
import { Checkbox } from '@shared/ui/Checkbox/Checkbox';
import { FavoriteButton } from '@shared/ui/FavoriteButton';
import { Header } from '@shared/ui/Header/Header';
Expand All @@ -15,6 +16,7 @@ import { useState } from 'react';
export default function Playground() {
const [radioValue, setRadioValue] = useState('option1');
const [isModalOpen, setIsModalOpen] = useState(false);
const [chatMessage, setChatMessage] = useState('');

return (
<div className="flex flex-col gap-6 p-8">
Expand Down Expand Up @@ -123,6 +125,16 @@ export default function Playground() {
<h2 className="typo-body-1">SearchBar</h2>
<SearchBar placeholder="어떤 제품을 찾으시나요?" onSearch={() => {}} />
</section>
{/* Chat Input */}
<section className="flex flex-col gap-4">
<h2 className="typo-body-1">ChatInput</h2>
<ChatInput
placeholder="무슨 견적을 원하시나요?"
value={chatMessage}
onChange={setChatMessage}
onSend={() => {}}
/>
</section>
{/* Favorite Button */}
<section className="flex flex-col gap-4">
<h2 className="typo-body-1">FavoriteButton</h2>
Expand Down
1 change: 1 addition & 0 deletions src/shared/assets/icons/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as CaretDownMdIcon } from './caret-down-md.svg?react';
export { default as CheckIcon } from './check.svg?react';
export { default as DoneIcon } from './done.svg?react';
export { default as SendIcon } from './send.svg?react';
export { default as SearchMagnifyingGlassIcon } from './search-magnifying-glass.svg?react';
export { default as HeartIcon } from './heart-outline.svg?react';
11 changes: 11 additions & 0 deletions src/shared/assets/icons/common/send.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/shared/assets/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { CaretDownMdIcon, CheckIcon, DoneIcon, HeartIcon, SearchMagnifyingGlassIcon } from './common';
export { CaretDownMdIcon, CheckIcon, DoneIcon, HeartIcon, SendIcon, SearchMagnifyingGlassIcon } from './common';
export { GearIcon, ShoppingIcon, SellIcon } from './banner';
94 changes: 94 additions & 0 deletions src/shared/ui/ChatInput/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { SendIcon } from '@shared/assets/icons';
import { useLayoutEffect, useRef, useState, type ChangeEventHandler, type KeyboardEventHandler } from 'react';
import { chatInputVariants } from './ChatInput.variants';

const TEXTAREA_LINE_HEIGHT = 24;
const TEXTAREA_MAX_LINES = 4;

export interface ChatInputProps {
placeholder?: string;
onSend: (message: string) => void;
value?: string;
onChange?: (value: string) => void;
}

export const ChatInput = ({ placeholder, onSend, value, onChange }: ChatInputProps) => {
const [internalMessage, setInternalMessage] = useState('');
const [isFocused, setFocused] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);

const isControlled = value !== undefined;
const message = isControlled ? value : internalMessage;
const trimmedMessage = message.trim();
const isFilled = trimmedMessage.length > 0;

const styles = chatInputVariants({
state: isFocused ? (isFilled ? 'filled' : 'focused') : isFilled ? 'filled' : 'default',
sendState: isFilled ? 'active' : 'inactive',
});

const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (event) => {
const nextValue = event.target.value;
if (isControlled) {
onChange?.(nextValue);
} else {
setInternalMessage(nextValue);
}
};

const handleSend = () => {
if (!trimmedMessage) {
return;
}

onSend(trimmedMessage);

if (isControlled) {
onChange?.('');
} else {
setInternalMessage('');
}
};

const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (event) => {
if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) {
event.preventDefault();
handleSend();
}
};

useLayoutEffect(() => {
const element = textareaRef.current;
if (!element) {
return;
}

element.style.height = 'auto';
const maxHeight = TEXTAREA_LINE_HEIGHT * TEXTAREA_MAX_LINES;
const nextHeight = Math.min(element.scrollHeight, maxHeight);
element.style.height = `${nextHeight}px`;
element.style.overflowY = element.scrollHeight > maxHeight ? 'auto' : 'hidden';
}, [message]);

return (
<div className={styles.root()}>
<div className={styles.wrapper()}>
<textarea
ref={textareaRef}
value={message}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onChange={handleChange}
className={styles.input()}
placeholder={placeholder}
aria-label={placeholder ?? 'Chat input'}
onKeyDown={handleKeyDown}
rows={1}
/>
<button type="button" className={styles.button()} onClick={handleSend} disabled={!isFilled} aria-label="Send">
<SendIcon className={styles.icon()} aria-hidden="true" />
</button>
</div>
</div>
);
};
59 changes: 59 additions & 0 deletions src/shared/ui/ChatInput/ChatInput.variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { tv } from 'tailwind-variants';

export const chatInputVariants = tv({
slots: {
root: ['flex w-full'],
wrapper: [
'flex items-center',
'w-[1200px] max-w-full min-h-[60px]',
'px-[22px] py-[11px]',
'gap-[16px]',
'rounded-[var(--radius-l)]',
'border-[1px] border-[var(--color-green-700)]',
'bg-white',
],
input: [
'w-full bg-transparent outline-none resize-none',
'typo-body-1',
'placeholder:text-[var(--color-gray-400)]',
],
button: ['flex items-center justify-center', 'w-[38px] h-[38px]', 'rounded-full', 'transition-colors', 'shrink-0'],
icon: ['w-[38px] h-[38px]', '[&_.send-circle]:transition-colors', '[&_.send-arrow]:transition-colors'],
},

variants: {
state: {
default: {
input: ['text-[var(--color-gray-500)]'],
},
focused: {
input: ['text-[var(--color-black)]'],
},
filled: {
input: ['text-[var(--color-black)]'],
},
},
sendState: {
inactive: {
button: ['cursor-not-allowed'],
icon: [
'[&_.send-circle]:fill-white',
'[&_.send-circle]:stroke-[var(--color-gray-400)]',
'[&_.send-arrow]:stroke-[var(--color-gray-400)]',
],
},
active: {
icon: [
'[&_.send-circle]:fill-[var(--color-green-500)]',
'[&_.send-circle]:stroke-[var(--color-black)]',
'[&_.send-arrow]:stroke-[var(--color-black)]',
],
},
},
},

defaultVariants: {
state: 'default',
sendState: 'inactive',
},
});
2 changes: 2 additions & 0 deletions src/shared/ui/ChatInput/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ChatInput } from './ChatInput';
export type { ChatInputProps } from './ChatInput';
Loading