mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-30 07:39:06 +08:00
feat(textarea): add Textarea component with variants and Storybook examples (#11260)
* feat(textarea): add Textarea component with variants and Storybook examples * feat(textarea): enhance Textarea component with context, improved variants, and Storybook examples * Fine-tuning the style * fix ci * feat(textarea): refactor Textarea stories to use custom label and caption components * feat(textarea): add TextareaContext for managing textarea state * fix: format * feat(textarea): refactor TextareaInput to simplify props and remove autoSize handling * feat(textarea): remove TextareaContext and update stories to reflect new error handling * refactor(textarea): remove TextareaRoot component After removing TextareaContext, TextareaRoot became a simple wrapper div with no functionality beyond applying layout styles. This change: - Removes TextareaRoot component and its exports - Updates all Storybook stories to use plain divs with the same styling - Simplifies the component API while maintaining the same functionality Addresses reviewer feedback: https://github.com/CherryHQ/cherry-studio/pull/11260#discussion_r2580009134 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: format --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: MyPrototypeWhat <daoquqiexing@gmail.com>
This commit is contained in:
parent
1a6263cf7f
commit
ebddfd3e56
@ -74,4 +74,4 @@ export * from './primitives/radioGroup'
|
||||
export * from './primitives/select'
|
||||
export * from './primitives/shadcn-io/dropzone'
|
||||
export * from './primitives/tabs'
|
||||
export * from './primitives/textarea'
|
||||
export * as Textarea from './primitives/textarea'
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Button } from '@cherrystudio/ui/components/primitives/button'
|
||||
import type { InputProps } from '@cherrystudio/ui/components/primitives/input'
|
||||
import { Input } from '@cherrystudio/ui/components/primitives/input'
|
||||
import { Textarea } from '@cherrystudio/ui/components/primitives/textarea'
|
||||
import type { TextareaInputProps } from '@cherrystudio/ui/components/primitives/textarea'
|
||||
import * as Textarea from '@cherrystudio/ui/components/primitives/textarea'
|
||||
import { cn } from '@cherrystudio/ui/utils/index'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
@ -131,9 +132,9 @@ function InputGroupInput({ className, ...props }: InputProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
function InputGroupTextarea({ className, ...props }: TextareaInputProps) {
|
||||
return (
|
||||
<Textarea
|
||||
<Textarea.Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||
|
||||
@ -1,17 +1,116 @@
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
import { cn } from '@cherrystudio/ui/utils/index'
|
||||
import { composeEventHandlers } from '@radix-ui/primitive'
|
||||
import { useCallbackRef } from '@radix-ui/react-use-callback-ref'
|
||||
import { useControllableState } from '@radix-ui/react-use-controllable-state'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Variants
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const textareaVariants = cva(
|
||||
cn(
|
||||
'flex field-sizing-content min-h-16 w-full border bg-transparent px-4 py-3 text-lg shadow-xs transition-[color,box-shadow] outline-none resize-y',
|
||||
'rounded-xs',
|
||||
'border-input text-foreground placeholder:text-foreground-secondary',
|
||||
'focus-visible:border-primary focus-visible:ring-ring focus-visible:ring-[3px]',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'md:text-sm'
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
hasError: {
|
||||
true: 'aria-invalid:border-destructive aria-invalid:ring-destructive/20',
|
||||
false: ''
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
hasError: false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* TextareaInput
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const INPUT_NAME = 'TextareaInput'
|
||||
|
||||
interface TextareaInputProps extends Omit<React.ComponentPropsWithoutRef<'textarea'>, 'value' | 'defaultValue'> {
|
||||
value?: string
|
||||
defaultValue?: string
|
||||
onValueChange?: (value: string) => void
|
||||
hasError?: boolean
|
||||
ref?: React.Ref<HTMLTextAreaElement>
|
||||
}
|
||||
|
||||
function TextareaInput({
|
||||
value: valueProp,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
hasError = false,
|
||||
className,
|
||||
ref,
|
||||
...props
|
||||
}: TextareaInputProps) {
|
||||
const [value = '', setValue] = useControllableState({
|
||||
prop: valueProp,
|
||||
defaultProp: defaultValue ?? '',
|
||||
onChange: onValueChange
|
||||
})
|
||||
|
||||
const handleChange = useCallbackRef((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = event.target.value
|
||||
if (props.maxLength && newValue.length > props.maxLength) {
|
||||
return
|
||||
}
|
||||
setValue(newValue)
|
||||
})
|
||||
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
data-slot="textarea-input"
|
||||
{...props}
|
||||
ref={ref}
|
||||
value={value}
|
||||
onChange={composeEventHandlers(props.onChange, handleChange)}
|
||||
aria-invalid={hasError}
|
||||
className={cn(textareaVariants({ hasError }), className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
TextareaInput.displayName = INPUT_NAME
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* TextareaCharCount
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const CHAR_COUNT_NAME = 'TextareaCharCount'
|
||||
|
||||
interface TextareaCharCountProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
value?: string
|
||||
maxLength?: number
|
||||
}
|
||||
|
||||
function TextareaCharCount({ value = '', maxLength, className, ...props }: TextareaCharCountProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="textarea-char-count"
|
||||
{...props}
|
||||
className={cn('absolute bottom-2 right-2 text-xs text-muted-foreground', className)}>
|
||||
{value.length}/{maxLength}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TextareaCharCount.displayName = CHAR_COUNT_NAME
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------- */
|
||||
|
||||
const Input = TextareaInput
|
||||
const CharCount = TextareaCharCount
|
||||
|
||||
export { CharCount, Input }
|
||||
export type { TextareaCharCountProps, TextareaInputProps }
|
||||
|
||||
418
packages/ui/stories/components/primitives/Textarea.stories.tsx
Normal file
418
packages/ui/stories/components/primitives/Textarea.stories.tsx
Normal file
@ -0,0 +1,418 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import * as Textarea from '../../../src/components/primitives/textarea'
|
||||
|
||||
const meta: Meta<typeof Textarea.Input> = {
|
||||
title: 'Components/Primitives/Textarea',
|
||||
component: Textarea.Input,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'A composable multi-line text input built with Radix primitives. Supports controlled/uncontrolled modes, auto-resize (via field-sizing-content), character counting, and error states.'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Basic Usage
|
||||
export const Basic: Story = {
|
||||
render: () => (
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<Textarea.Input placeholder="Type your message here..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Label
|
||||
export const WithLabel: Story = {
|
||||
render: () => (
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Description</div>
|
||||
<Textarea.Input placeholder="Tell us about yourself..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Required Field
|
||||
export const RequiredField: Story = {
|
||||
render: () => (
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">
|
||||
<span className="text-destructive mr-1">*</span>Bio
|
||||
</div>
|
||||
<Textarea.Input placeholder="This field is required..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Caption
|
||||
export const WithCaption: Story = {
|
||||
render: () => (
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Comments</div>
|
||||
<Textarea.Input placeholder="Enter your comments..." />
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-foreground-muted">
|
||||
Please provide detailed feedback
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Error State
|
||||
export const ErrorState: Story = {
|
||||
render: () => (
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Message</div>
|
||||
<Textarea.Input placeholder="Enter your message..." hasError />
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
<span>This field cannot be empty</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Character Count
|
||||
export const WithCharacterCount: Story = {
|
||||
render: function WithCharacterCountExample() {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Tweet</div>
|
||||
<div className="relative">
|
||||
<Textarea.Input value={value} onValueChange={setValue} maxLength={280} placeholder="What's happening?" />
|
||||
<Textarea.CharCount value={value} maxLength={280} />
|
||||
</div>
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-foreground-muted">Maximum 280 characters</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto Resize (built-in via field-sizing-content)
|
||||
export const AutoResize: Story = {
|
||||
render: function AutoResizeExample() {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Auto-resizing Textarea</div>
|
||||
<Textarea.Input value={value} onValueChange={setValue} placeholder="This textarea grows with your content..." />
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-foreground-muted">
|
||||
Try typing multiple lines
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled State
|
||||
export const Disabled: Story = {
|
||||
render: () => (
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px] cursor-not-allowed opacity-70">Disabled Field</div>
|
||||
<Textarea.Input defaultValue="This textarea is disabled" disabled />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Controlled
|
||||
export const Controlled: Story = {
|
||||
render: function ControlledExample() {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Controlled Textarea</div>
|
||||
<Textarea.Input value={value} onValueChange={setValue} placeholder="Type something..." />
|
||||
</div>
|
||||
|
||||
<div className="w-[400px] text-sm text-muted-foreground">
|
||||
<div className="rounded-md border border-border bg-muted p-3">
|
||||
<div className="mb-1 font-medium">Current value:</div>
|
||||
<pre className="text-xs">{value || '(empty)'}</pre>
|
||||
<div className="mt-2 text-xs">Characters: {value.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// All States
|
||||
export const AllStates: Story = {
|
||||
render: function AllStatesExample() {
|
||||
const [value1, setValue1] = useState('')
|
||||
const [value2, setValue2] = useState('This textarea has some content')
|
||||
const [value4, setValue4] = useState('')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-semibold text-muted-foreground">Default State</p>
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Default</div>
|
||||
<Textarea.Input value={value1} onValueChange={setValue1} placeholder="Enter text..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-semibold text-muted-foreground">Filled State</p>
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Filled</div>
|
||||
<Textarea.Input value={value2} onValueChange={setValue2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-semibold text-muted-foreground">Disabled State</p>
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px] cursor-not-allowed opacity-70">Disabled</div>
|
||||
<Textarea.Input defaultValue="Disabled textarea with content" disabled />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-semibold text-muted-foreground">Error State</p>
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Error</div>
|
||||
<Textarea.Input value={value4} onValueChange={setValue4} hasError />
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
<span>This field is required</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-semibold text-muted-foreground">Focus State (click to focus)</p>
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Focus</div>
|
||||
<Textarea.Input placeholder="Click to see focus state" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Real World Examples
|
||||
export const RealWorldExamples: Story = {
|
||||
render: function RealWorldExample() {
|
||||
const [tweet, setTweet] = useState('')
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
const tweetError = tweet.length > 280 ? 'Tweet is too long' : undefined
|
||||
const messageError =
|
||||
message.length > 0 && message.length < 10 ? 'Message must be at least 10 characters' : undefined
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* Tweet Composer */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Tweet Composer</h3>
|
||||
<div className="flex w-full flex-col gap-2 w-[500px]">
|
||||
<div className="text-lg font-bold leading-[22px]">What's happening?</div>
|
||||
<div className="relative">
|
||||
<Textarea.Input
|
||||
value={tweet}
|
||||
onValueChange={setTweet}
|
||||
maxLength={280}
|
||||
placeholder="Share your thoughts..."
|
||||
hasError={!!tweetError}
|
||||
/>
|
||||
<Textarea.CharCount value={tweet} maxLength={280} />
|
||||
</div>
|
||||
{tweetError && (
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
<span>{tweetError}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feedback Form */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">User Feedback</h3>
|
||||
<div className="flex w-full flex-col gap-2 w-[500px]">
|
||||
<div className="text-lg font-bold leading-[22px]">
|
||||
<span className="text-destructive mr-1">*</span>Feedback
|
||||
</div>
|
||||
<Textarea.Input
|
||||
value={feedback}
|
||||
onValueChange={setFeedback}
|
||||
placeholder="Please share your thoughts..."
|
||||
rows={4}
|
||||
/>
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-foreground-muted">
|
||||
Your feedback helps us improve
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Contact Us</h3>
|
||||
<div className="flex w-full flex-col gap-2 w-[500px]">
|
||||
<div className="text-lg font-bold leading-[22px]">
|
||||
<span className="text-destructive mr-1">*</span>Message
|
||||
</div>
|
||||
<Textarea.Input
|
||||
value={message}
|
||||
onValueChange={setMessage}
|
||||
placeholder="How can we help you?"
|
||||
rows={6}
|
||||
hasError={!!messageError}
|
||||
/>
|
||||
{messageError ? (
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
<span>{messageError}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-foreground-muted">
|
||||
Minimum 10 characters required
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Dark Mode
|
||||
export const DarkMode: Story = {
|
||||
render: () => (
|
||||
<div className="dark rounded-lg bg-background p-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Default (Dark)</div>
|
||||
<Textarea.Input placeholder="Dark mode textarea..." />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">With Content (Dark)</div>
|
||||
<Textarea.Input defaultValue="This is some content in dark mode" />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px]">Error (Dark)</div>
|
||||
<Textarea.Input hasError />
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
<span>Error in dark mode</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-2 w-[400px]">
|
||||
<div className="text-lg font-bold leading-[22px] cursor-not-allowed opacity-70">Disabled (Dark)</div>
|
||||
<Textarea.Input defaultValue="Disabled in dark mode" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Composition Example
|
||||
export const CompositionExample: Story = {
|
||||
render: function CompositionExampleRender() {
|
||||
const [bio, setBio] = useState('')
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2 w-[500px]">
|
||||
<div className="text-lg font-bold leading-[22px]">
|
||||
<span className="text-destructive mr-1">*</span>Profile Bio
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Textarea.Input value={bio} onValueChange={setBio} placeholder="Tell us about yourself..." maxLength={500} />
|
||||
<Textarea.CharCount value={bio} maxLength={500} />
|
||||
</div>
|
||||
<div className="text-sm flex items-center gap-1.5 leading-4 text-foreground-muted">
|
||||
This will be displayed on your profile (max 500 characters)
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user