input search

This commit is contained in:
Nate Kelley 2025-02-24 19:11:26 -07:00
parent 4ae7b41191
commit f8bf374772
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 184 additions and 49 deletions

View File

@ -39,7 +39,7 @@ export const buttonVariants = cva(
'inline-flex items-center overflow-hidden text-base justify-center gap-[5px] shadow rounded transition-all duration-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed data-[loading=true]:cursor-progress', 'inline-flex items-center overflow-hidden text-base justify-center gap-[5px] shadow rounded transition-all duration-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed data-[loading=true]:cursor-progress',
{ {
variants: { variants: {
buttonType: buttonTypeClasses, variant: buttonTypeClasses,
size: sizeVariants, size: sizeVariants,
iconButton: { iconButton: {
true: '', true: '',
@ -69,7 +69,7 @@ export const buttonVariants = cva(
} }
], ],
defaultVariants: { defaultVariants: {
buttonType: 'default', variant: 'default',
size: 'default', size: 'default',
iconButton: false, iconButton: false,
block: false block: false
@ -79,7 +79,7 @@ export const buttonVariants = cva(
export const buttonIconVariants = cva('', { export const buttonIconVariants = cva('', {
variants: { variants: {
buttonType: { variant: {
default: 'text-icon-color!', default: 'text-icon-color!',
black: 'text-white!', black: 'text-white!',
primary: 'text-white!', primary: 'text-white!',
@ -120,7 +120,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
( (
{ {
className = '', className = '',
buttonType = 'default', variant = 'default',
size = 'default', size = 'default',
asChild = false, asChild = false,
prefix, prefix,
@ -145,7 +145,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
return ( return (
<Comp <Comp
className={cn( className={cn(
buttonVariants({ buttonType, size, iconButton, rounding, block, className }), buttonVariants({ variant, size, iconButton, rounding, block, className }),
onClick && 'cursor-pointer' onClick && 'cursor-pointer'
)} )}
onClick={onClick} onClick={onClick}
@ -155,17 +155,17 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
data-selected={selected} data-selected={selected}
{...props}> {...props}>
{loading ? ( {loading ? (
<LoadingIcon buttonType={buttonType} size={size} /> <LoadingIcon variant={variant} size={size} />
) : ( ) : (
prefix && ( prefix && (
<span className={cn(buttonIconVariants({ buttonType, size }), prefixClassName)}> <span className={cn(buttonIconVariants({ variant, size }), prefixClassName)}>
{prefix} {prefix}
</span> </span>
) )
)} )}
{hasChildren && <span className="">{children}</span>} {hasChildren && <span className="">{children}</span>}
{suffix && ( {suffix && (
<span className={cn(buttonIconVariants({ buttonType, size }), suffixClassName)}> <span className={cn(buttonIconVariants({ variant, size }), suffixClassName)}>
{suffix} {suffix}
</span> </span>
)} )}
@ -177,22 +177,22 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
Button.displayName = 'Button'; Button.displayName = 'Button';
export const LoadingIcon: React.FC<{ export const LoadingIcon: React.FC<{
buttonType: ButtonProps['buttonType']; variant: ButtonProps['variant'];
size: ButtonProps['size']; size: ButtonProps['size'];
}> = ({ buttonType = 'default', size = 'default' }) => { }> = ({ variant = 'default', size = 'default' }) => {
return ( return (
<div <div
className={cn( className={cn(
'flex items-center justify-center text-black dark:text-white', 'flex items-center justify-center text-black dark:text-white',
buttonType === 'black' && 'dark', variant === 'black' && 'dark',
loadingSizeVariants[size || 'default'] loadingSizeVariants[size || 'default']
)}> )}>
<CircleSpinnerLoader <CircleSpinnerLoader
size={size === 'tall' ? 12.5 : 9.5} size={size === 'tall' ? 12.5 : 9.5}
fill={ fill={
buttonType === 'black' variant === 'black'
? 'var(--color-white)' ? 'var(--color-white)'
: buttonType === 'primary' : variant === 'primary'
? 'var(--color-primary)' ? 'var(--color-primary)'
: 'var(--color-primary)' : 'var(--color-primary)'
} }

View File

@ -9,7 +9,7 @@ export const inputVariants = cva(
variants: { variants: {
variant: { variant: {
default: default:
'shadow disabled:bg-item-select bg-background border placeholder:text-gray-light hover:border-primary focus:border-primary focus-visible:border-primary outline-none disabled:border-border', 'shadow disabled:bg-item-select bg-background border placeholder:text-gray-light hover:border-foreground focus:border-foreground focus-visible:border-foreground outline-none disabled:border-border',
ghost: 'border-none bg-transparent shadow-none disabled:bg-transparent' ghost: 'border-none bg-transparent shadow-none disabled:bg-transparent'
}, },
size: { size: {

View File

@ -22,6 +22,7 @@ export interface InputTextAreaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>, extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
VariantProps<typeof inputTextAreaVariants> { VariantProps<typeof inputTextAreaVariants> {
autoResize?: AutoResizeOptions; autoResize?: AutoResizeOptions;
onPressMetaEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
} }
export const InputTextArea = React.forwardRef<HTMLTextAreaElement, InputTextAreaProps>( export const InputTextArea = React.forwardRef<HTMLTextAreaElement, InputTextAreaProps>(
@ -92,6 +93,14 @@ export const InputTextArea = React.forwardRef<HTMLTextAreaElement, InputTextArea
requestAnimationFrame(adjustHeight); requestAnimationFrame(adjustHeight);
}); });
const onPressMetaEnter = useMemoizedFn((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.metaKey && e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
onPressMetaEnter?.(e);
}
});
useEffect(() => { useEffect(() => {
const textarea = textareaRef.current; const textarea = textareaRef.current;
if (!textarea || !autoResize) return; if (!textarea || !autoResize) return;

View File

@ -0,0 +1,66 @@
import type { Meta, StoryObj } from '@storybook/react';
import { InputTextAreaButton } from './InputTextAreaButton';
const meta: Meta<typeof InputTextAreaButton> = {
title: 'Base/InputTextAreaButton',
component: InputTextAreaButton,
tags: ['autodocs'],
args: {},
argTypes: {
disabled: {
control: 'boolean'
},
placeholder: {
control: 'text'
},
rows: {
control: 'number'
},
className: {
control: 'text'
}
}
};
export default meta;
type Story = StoryObj<typeof InputTextAreaButton>;
export const Default: Story = {
args: {
placeholder: 'Enter text here...',
rows: 4
}
};
export const WithAutoResize: Story = {
args: {
placeholder: 'Type to see auto-resize in action...',
autoResize: {
minRows: 1,
maxRows: 6
}
}
};
export const Disabled: Story = {
args: {
disabled: true,
placeholder: 'Disabled textarea',
value: 'Cannot edit this text',
rows: 4
}
};
export const CustomPlaceholder: Story = {
args: {
placeholder: 'Type your message here...',
rows: 3
}
};
export const WithInitialValue: Story = {
args: {
value: 'This is some initial text in the textarea...',
rows: 4
}
};

View File

@ -1,55 +1,115 @@
import React from 'react'; import React, { useMemo, useRef } from 'react';
import { InputTextArea, InputTextAreaProps } from './InputTextArea'; import { InputTextArea, InputTextAreaProps } from './InputTextArea';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
import { cva } from 'class-variance-authority'; import { cva } from 'class-variance-authority';
import { Button } from '../buttons/Button';
import { ArrowUp } from '../icons/NucleoIconOutlined';
import { ShapeSquare } from '../icons/NucleoIconFilled';
import { useMemoizedFn } from 'ahooks';
export interface InputTextAreaButtonProps extends Omit<InputTextAreaProps, 'variant'> {} const inputTextAreaButtonVariants = cva(
'relative flex w-full items-center overflow-hidden rounded border border-border transition-all duration-200',
{
variants: {
variant: {
default:
'has-[textarea:hover]:border-foreground has-[textarea:focus]:border-foreground has-[textarea:disabled]:border-border'
}
}
}
);
const inputTextAreaButtonVariants = cva('relative flex w-full items-center overflow-hidden', { export interface InputTextAreaButtonProps extends Omit<InputTextAreaProps, 'variant' | 'onSubmit'> {
variants: {} sendIcon?: React.ReactNode;
}); loadingIcon?: React.ReactNode;
loading?: boolean;
onSubmit: (text: string) => void;
variant?: 'default';
}
export const InputTextAreaButton: React.FC<InputTextAreaButtonProps> = ({ export const InputTextAreaButton: React.FC<InputTextAreaButtonProps> = ({
className, className,
disabled, disabled,
autoResize,
sendIcon = <ArrowUp />,
loadingIcon = <ShapeSquare />,
loading = false,
onSubmit,
variant = 'default',
...props ...props
}) => { }) => {
return ( const textRef = useRef<HTMLTextAreaElement>(null);
<div
className={cn(
// styles.inputContainer,
// isFocused && 'focused',
// loading && 'loading',
inputTextAreaButtonVariants()
)}>
<InputTextArea
disabled={disabled}
variant="ghost"
className="inline-block w-full pt-2! pr-9! pb-2! pl-3.5! align-middle"
{...props}
// ref={inputRef} const onSubmitPreflight = useMemoizedFn(() => {
// variant="borderless" if (disabled) return;
// onBlur={onBlurInput} const text = textRef.current?.value || '';
// onFocus={onFocusInput} if (text.trim() === '') return;
// className="inline-block w-full pt-2! pr-9! pb-2! pl-3.5! align-middle" onSubmit(text);
// placeholder="Ask a follow up..." });
// value={inputValue}
// autoFocus={true} const onPressMetaEnter = useMemoizedFn(() => {
// onChange={onChangeInput} onSubmitPreflight();
// onPressEnter={onPressEnter} });
// disabled={loading}
// autoSize={autoSize} return (
<div className={cn(inputTextAreaButtonVariants(), loading && 'border-border!', className)}>
<InputTextArea
ref={textRef}
disabled={disabled || loading}
variant="ghost"
className={cn('w-full pr-10 align-middle leading-[1.2]', loading && '!cursor-default')}
autoResize={autoResize}
onPressMetaEnter={onPressMetaEnter}
{...props}
/> />
<div className="absolute right-2 bottom-2"> <div className="absolute right-2 bottom-2">
HERE <SubmitButton
{/* <SubmitButton disabled={disabled}
disableSendButton={disableSendButton}
loading={loading} loading={loading}
sendIcon={sendIcon}
loadingIcon={loadingIcon}
onSubmitPreflight={onSubmitPreflight} onSubmitPreflight={onSubmitPreflight}
/> */} />
</div> </div>
</div> </div>
); );
}; };
const SubmitButton: React.FC<{
loading: boolean;
disabled?: boolean;
sendIcon: React.ReactNode;
loadingIcon: React.ReactNode;
onSubmitPreflight: () => void;
}> = ({ disabled, sendIcon, loading, loadingIcon, onSubmitPreflight }) => {
const memoizedPrefix = useMemo(() => {
return (
<div
className={cn(
'relative h-4 w-4 transition-all duration-300 active:scale-80',
loading && '!cursor-default'
)}>
<div
className={`absolute inset-0 transition-all duration-300 ${loading ? 'scale-95 opacity-0' : 'scale-100 opacity-100'}`}>
{sendIcon}
</div>
<div
className={`absolute inset-0 flex items-center justify-center text-sm transition-all duration-300 ${loading ? 'scale-100 opacity-100' : 'scale-95 opacity-0'}`}>
{loadingIcon}
</div>
</div>
);
}, [loading, sendIcon, loadingIcon]);
return (
<Button
rounding={'large'}
variant="black"
prefix={memoizedPrefix}
className="active:scale-95"
onClick={onSubmitPreflight}
disabled={disabled}
/>
);
};

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React from 'react';
import { AppMaterialIcons } from '@/components/ui'; import { AppMaterialIcons } from '@/components/ui';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';