mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat: Use expanding text area in the Ai Assistant (SQL Editor) (#27930)
* Add a new textarea component which autoexpands depending on number of content rows. * Use the new ExpandingTextArea in the AIAssistantPanel. * Remove old unused text area. * Move around some of the props in the ExpandingTextArea. * Make the value prop mandatory. * Fix minor stuff in the Design System app. * Add examples for Expanding Text Area. * add max height to the expanding text area. * Add todo comment in database-new app.
This commit is contained in:
1 parent
fe4f3fc30c
commit
22c5324f0c
17 files changed
+182
-95
No files matched your search
@@ -1864,5 +1864,16 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"expanding-textarea-demo": {
|
||||
name: "expanding-textarea-demo",
|
||||
type: "docs:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/expanding-textarea-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/expanding-textarea-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { ThemeProvider } from './Providers'
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
title: 'Supabase Design System',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
|
||||
@@ -374,6 +374,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/components/tree-view',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Expanding Textarea',
|
||||
href: '/docs/components/expanding-textarea',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Expanding Textarea
|
||||
description: Displays a textarea which autoexpands (or shrinks) based on its content.
|
||||
component: true
|
||||
source:
|
||||
shadcn: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="expanding-textarea-demo" peekCode wide />
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ExpandingTextArea } from 'ui'
|
||||
```
|
||||
|
||||
```tsx
|
||||
<ExpandingTextArea />
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Default
|
||||
|
||||
<ComponentPreview name="expanding-textarea-demo" peekCode wide />
|
||||
@@ -1,5 +1,7 @@
|
||||
import { withContentlayer } from 'next-contentlayer2'
|
||||
|
||||
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ['ui', 'common', 'shared-data', 'icons', 'tsconfig'],
|
||||
@@ -22,11 +24,11 @@ const nextConfig = {
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
...(process.env.NEXT_PUBLIC_BASE_PATH?.length
|
||||
...(BASE_PATH.length
|
||||
? [
|
||||
{
|
||||
source: '/',
|
||||
destination: process.env.NEXT_PUBLIC_BASE_PATH,
|
||||
destination: BASE_PATH,
|
||||
basePath: false,
|
||||
permanent: false,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useState } from 'react'
|
||||
import { ExpandingTextArea } from 'ui'
|
||||
|
||||
export default function ExpandingTextareaDemo() {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<ExpandingTextArea
|
||||
placeholder="Type your message in multiple lines here."
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1065,4 +1065,9 @@ export const examples: Registry = [
|
||||
type: 'docs:example',
|
||||
files: ['example/color-usage-surface-studio-frame.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'expanding-textarea-demo',
|
||||
type: 'docs:example',
|
||||
files: ['example/expanding-textarea-demo.tsx'],
|
||||
},
|
||||
]
|
||||
@@ -15,12 +15,12 @@ import { IS_PLATFORM, LOCAL_STORAGE_KEYS, OPT_IN_TAGS } from 'lib/constants'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import {
|
||||
ExpandingTextArea,
|
||||
Button,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormItem_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { AiIcon } from 'ui-patterns/Cmdk'
|
||||
@@ -86,6 +86,8 @@ export const AiAssistantPanel = ({
|
||||
}
|
||||
}, [loading])
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-w-[400px] w-[400px] border-l border-control">
|
||||
<div
|
||||
@@ -150,6 +152,7 @@ export const AiAssistantPanel = ({
|
||||
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="sticky p-5 flex-0 border-t"
|
||||
onSubmit={form.handleSubmit((data: z.infer<typeof FormSchema>) => {
|
||||
onSubmit(data.chat)
|
||||
@@ -168,18 +171,23 @@ export const AiAssistantPanel = ({
|
||||
control={form.control}
|
||||
name="chat"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_>
|
||||
<FormItem_Shadcn_ asChild>
|
||||
<FormControl_Shadcn_>
|
||||
<div className="relative">
|
||||
<AiIcon className="absolute top-2 left-3 [&>div>div]:border-black dark:[&>div>div]:border-white" />
|
||||
<Input_Shadcn_
|
||||
<ExpandingTextArea
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
className={`bg-surface-300 dark:bg-black rounded-full pl-10 ${
|
||||
loading ? 'pr-10' : ''
|
||||
}`}
|
||||
spellCheck={false}
|
||||
className="pl-12 text-sm rounded-[18px] max-h-96"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
formRef?.current?.requestSubmit()
|
||||
}
|
||||
}}
|
||||
placeholder="Ask a question about your SQL query"
|
||||
/>
|
||||
{loading && <Loader2 className="absolute top-2 right-3 animate-spin" />}
|
||||
|
||||
@@ -95,6 +95,7 @@ const AssistantChatForm = React.forwardRef<HTMLFormElement, FormProps>(
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
{/* TODO: Replace this with ExpandingTextArea */}
|
||||
<TextArea
|
||||
ref={textAreaRef}
|
||||
autoFocus
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import React, { createRef, useEffect } from 'react'
|
||||
import { TextArea } from 'ui/src/components/shadcn/ui/text-area'
|
||||
import { cn } from 'ui/src/lib/utils'
|
||||
|
||||
export interface ExpandingTextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const ExpandingTextArea = ({
|
||||
className,
|
||||
disabled = false,
|
||||
value,
|
||||
onSubmit,
|
||||
placeholder,
|
||||
...props
|
||||
}: ExpandingTextAreaProps) => {
|
||||
const textAreaRef = createRef<HTMLTextAreaElement>()
|
||||
|
||||
/**
|
||||
* This effect is used to resize the textarea based on the content
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
if (!value && textAreaRef && textAreaRef.current) {
|
||||
textAreaRef.current.style.height = '40px'
|
||||
} else if (textAreaRef && textAreaRef.current) {
|
||||
const newHeight = textAreaRef.current.scrollHeight + 'px'
|
||||
textAreaRef.current.style.height = newHeight
|
||||
}
|
||||
}
|
||||
}, [value, textAreaRef])
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
ref={textAreaRef}
|
||||
rows={1}
|
||||
contentEditable
|
||||
aria-expanded={false}
|
||||
className={cn(
|
||||
'transition-all text-sm pr-10 rounded-[18px] resize-none box-border leading-6',
|
||||
className
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
spellCheck={false}
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
ExpandingTextArea.displayName = 'AssistantChatTextArea'
|
||||
|
||||
export { ExpandingTextArea }
|
||||
@@ -56,6 +56,7 @@ export * from './src/components/Radio'
|
||||
export * from './src/components/Toggle'
|
||||
export * from './src/components/Upload'
|
||||
export * from './src/components/Form'
|
||||
export * from './src/components/ExpandingTextArea'
|
||||
|
||||
// layout
|
||||
export * from './src/components/LoadingLine'
|
||||
@@ -257,10 +258,6 @@ export * from './src/layout/banners'
|
||||
|
||||
// export { default as Config } from './../ui.config'
|
||||
|
||||
// ARCHIVE
|
||||
|
||||
// export * from './src/components/Textarea'
|
||||
|
||||
// AUTH
|
||||
|
||||
// export * from './src/components/Auth'
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import React, { createRef, useEffect } from 'react'
|
||||
import { cn } from '../../lib/utils'
|
||||
import { TextArea } from '../shadcn/ui/text-area'
|
||||
|
||||
export interface ExpandingTextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
/* The value of the textarea. Required to calculate the height of the textarea. */
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a custom TextArea component that expands based on the content.
|
||||
*/
|
||||
const ExpandingTextArea = ({ className, value, ...props }: ExpandingTextAreaProps) => {
|
||||
const textAreaRef = createRef<HTMLTextAreaElement>()
|
||||
|
||||
/**
|
||||
* This effect is used to resize the textarea based on the content
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
if (!value && textAreaRef && textAreaRef.current) {
|
||||
textAreaRef.current.style.height = '40px'
|
||||
} else if (textAreaRef && textAreaRef.current) {
|
||||
const newHeight = textAreaRef.current.scrollHeight + 'px'
|
||||
textAreaRef.current.style.height = newHeight
|
||||
}
|
||||
}
|
||||
}, [value, textAreaRef])
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
ref={textAreaRef}
|
||||
rows={1}
|
||||
aria-expanded={false}
|
||||
className={cn('transition-all resize-none leading-6 box-border', className)}
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
ExpandingTextArea.displayName = 'ExpandingTextArea'
|
||||
|
||||
export { ExpandingTextArea }
|
||||
@@ -1,11 +0,0 @@
|
||||
.form-textarea:disabled {
|
||||
@apply bg-gray-100 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.form-textarea--error {
|
||||
@apply border-red-500;
|
||||
}
|
||||
|
||||
.form-textarea--borderless {
|
||||
@apply border-none;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import './Textarea.css'
|
||||
|
||||
/**
|
||||
* @deprecated Use ./TextArea_Shadcn_ instead
|
||||
*/
|
||||
const Textarea = ({
|
||||
className = '',
|
||||
isError = false,
|
||||
rows = 3,
|
||||
children,
|
||||
borderless = false,
|
||||
...props
|
||||
}) => {
|
||||
const classes = []
|
||||
|
||||
if (isError) {
|
||||
classes.push('form-textarea--error')
|
||||
}
|
||||
|
||||
if (borderless) {
|
||||
classes.push('form-textarea--borderless')
|
||||
}
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className={`form-textarea border-solid ${classes.join(' ')} ${className}`}
|
||||
rows={rows}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Textarea.propTypes = {
|
||||
isError: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
borderless: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default Textarea
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Textarea from './Textarea'
|
||||
|
||||
describe('#Textarea', () => {
|
||||
it('should render textarea correctly', async () => {
|
||||
render(<Textarea data-testid="form-textarea" />)
|
||||
expect(screen.queryByTestId('form-textarea')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have "form-textarea--error" class', () => {
|
||||
render(<Textarea isError data-testid="form-textarea" />)
|
||||
expect(screen.queryByTestId('form-textarea')).toHaveClass('form-textarea border-solid form-textarea--error')
|
||||
})
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Textarea } from '.'
|
||||
|
||||
export default {
|
||||
title: 'Archive/Textarea',
|
||||
component: Textarea,
|
||||
}
|
||||
|
||||
export const Default = (args) => <Textarea {...args} />
|
||||
Default.args = {
|
||||
placeholder: 'Type text here ...',
|
||||
disabled: false
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Textarea } from './Textarea'
|
||||
Reference in new issue
Block a user