Files
supabase/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx
Danny White 476d4a5851 refactor(ui): drop redundant Button variant="default" props (#50161)
## What kind of change does this PR introduce?

Mechanical cleanup on top of the Button default-variant change (#50160).

## What is the current behavior?

Many callsites still pass `variant="default"` even though that is now
the component default.

## What is the new behavior?

Removes redundant static `variant="default"` from legacy `Button` and
`ButtonTooltip` callsites. Keeps explicit defaults where they document
the API:

- `button-default.tsx` and `button-sizes.tsx` demos
- `DocsButton`, which pins neutral styling at the wrapper boundary

## To test

Studio:

- [Auth → Rate
Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits):
dirty the form so Cancel appears; Cancel stays neutral, Save stays green
- [Project Settings → API
Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys):
`DocsButton` in the header actions stays neutral

Design system:

- [Design system →
Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button):
`button-default` / `button-sizes` still show explicit default styling;
Primary (green) is restricted to the Primary section (and `asChild`)

WWW:

- [www → Brand
assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets):
Download logo kit / Download button kit stay neutral
2026-09-11 17:05:26 +10:00

802 lines
31 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { format } from 'date-fns'
import { ExternalLink, Trash, Upload } from 'lucide-react'
import { useRef, useState } from 'react'
import { useForm } from 'react-hook-form'
import {
Button,
Calendar,
Checkbox,
Form,
FormControl,
FormField,
FormInputGroupInput,
Input,
InputGroup,
InputGroupAddon,
InputGroupText,
RadioGroupStacked,
RadioGroupStackedItem,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetSection,
SheetTitle,
Switch,
Textarea,
} from 'ui'
import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
import {
DatePicker,
DatePickerButton,
DatePickerContent,
DatePickerTrigger,
} from 'ui-patterns/DatePicker'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import { getKeyValueFieldArrayValidationIssues } from 'ui-patterns/form/KeyValueFieldArray/validation'
import { SingleValueFieldArray } from 'ui-patterns/form/SingleValueFieldArray/SingleValueFieldArray'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
import * as z from 'zod'
const formSchema = z
.object({
name: z.string().min(1, 'Name is required'),
description: z.string().optional(),
maxConnections: z
.union([
z.literal(''),
z.coerce
.number()
.gte(1000, 'Max connections should be at least 1000')
.lte(10000, 'Max connections should not exceed 10000'),
])
.refine((value) => value !== '', 'Max connections is required'),
enableFeature: z.boolean(),
enableRls: z.boolean(),
enableNotifications: z.boolean(),
enableAnalytics: z.boolean(),
region: z.string().min(1, 'Region is required'),
schemas: z.array(z.string()).min(1, 'At least one schema is required'),
queueType: z.enum(['basic', 'partitioned']),
expiryDate: z.date(),
password: z.string().min(8, 'Password must be at least 8 characters'),
duration: z
.union([
z.literal(''),
z.coerce
.number()
.gte(1000, 'Duration should be at least 5ms')
.lte(10000, 'Duration should not exceed 30ms'),
])
.refine((value) => value !== '', 'Duration is required'),
redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
httpHeaders: z.array(z.object({ key: z.string().trim(), value: z.string().trim() })),
apiKey: z.string().optional(),
})
.superRefine((data, ctx) => {
getKeyValueFieldArrayValidationIssues({
rows: data.httpHeaders,
keyFieldName: 'key',
valueFieldName: 'value',
keyRequiredMessage: 'Header name is required',
valueRequiredMessage: 'Header value is required',
}).forEach((issue) => {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: issue.message,
path: ['httpHeaders', ...issue.path],
})
})
})
const fakeApiKey = 'sk_live_51H3x4mpl3_4nd_53cur3_k3y_1234567890'
export default function FormPatternsSidePanel() {
const [open, setOpen] = useState(false)
const uploadButtonRef = useRef<HTMLInputElement>(null)
const fileUploadRef = useRef<HTMLInputElement>(null)
const [logoFile, setLogoFile] = useState<File>()
const [logoUrl, setLogoUrl] = useState<string>()
const [uploadedFiles, setUploadedFiles] = useState<File[]>([])
const [isDragging, setIsDragging] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
description: '',
maxConnections: 10,
enableFeature: false,
enableRls: true,
enableNotifications: false,
enableAnalytics: true,
region: '',
schemas: ['public'],
queueType: 'basic',
expiryDate: undefined,
password: '',
duration: 10,
redirectUris: [{ value: '' }],
httpHeaders: [{ key: '', value: '' }],
apiKey: fakeApiKey,
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
setOpen(false)
}
const formId = 'sidepanel-form'
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Open form panel
</Button>
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent size="lg" className="flex flex-col gap-0">
<SheetHeader>
<SheetTitle>Create Configuration</SheetTitle>
</SheetHeader>
<Form {...form}>
<form
id={formId}
onSubmit={form.handleSubmit(onSubmit)}
className="overflow-auto grow px-0"
>
{/* Text Input */}
<SheetSection>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Text Input"
description="Single-line text entry for short values"
>
<FormControl className="col-span-6">
<Input {...field} placeholder="Enter text" />
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Password Input */}
<SheetSection>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Password Input"
description="Masked input for secure text entry"
>
<FormControl className="col-span-6">
<Input {...field} type="password" placeholder="Enter password" />
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Copyable Input */}
<SheetSection>
<FormField
control={form.control}
name="apiKey"
render={() => (
<FormItemLayout
layout="horizontal"
label="Copyable Input"
description="Read-only input with copy-to-clipboard functionality"
>
<FormControl className="col-span-6">
<PasswordInput
copy
readOnly
className="input-mono"
value={form.getValues('apiKey') || ''}
onChange={() => {}}
onCopy={() => console.log('Copied to clipboard')}
/>
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Number Input */}
<SheetSection>
<FormField
control={form.control}
name="maxConnections"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Number Input"
description="Numeric input with min/max validation"
>
<FormControl className="col-span-6">
<Input {...field} type="number" min={1} max={1000} />
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Input with Units */}
<SheetSection>
<FormField
control={form.control}
name="duration"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Input with Units"
description="Input with additional unit label"
>
<FormControl className="col-span-6">
<InputGroup>
<FormInputGroupInput {...field} type="number" min={5} max={30} />
<InputGroupAddon align="inline-end">
<InputGroupText>ms</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Textarea */}
<SheetSection>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Textarea"
description="Multi-line text input for longer content"
>
<FormControl className="col-span-6">
<Textarea
{...field}
rows={3}
placeholder="Enter multi-line text"
className="resize-none"
/>
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Icon Upload */}
<SheetSection>
<FormField
control={form.control}
name="description"
render={() => (
<FormItemLayout
layout="horizontal"
label="Icon upload"
description="For icons, avatars, or small images with preview"
>
<FormControl className="col-span-6">
<div className="flex gap-4 items-center">
<button
tabIndex={0}
type="button"
onClick={() => uploadButtonRef.current?.click()}
className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong focus-ring"
style={{
backgroundImage: logoUrl ? `url("${logoUrl}")` : 'none',
}}
>
{!logoUrl && <Upload size={14} />}
</button>
<div className="flex gap-2 items-center">
<Button
type="button"
variant="outline"
size="tiny"
icon={<Upload size={14} />}
onClick={() => uploadButtonRef.current?.click()}
>
Upload
</Button>
{logoUrl && (
<Button
size="tiny"
icon={<Trash size={12} />}
onClick={() => {
setLogoFile(undefined)
setLogoUrl(undefined)
}}
/>
)}
</div>
<input
type="file"
ref={uploadButtonRef}
className="hidden"
accept="image/png, image/jpeg"
onChange={(e) => {
const files = e.target.files
if (files && files.length > 0) {
const file = files[0]
setLogoFile(file)
setLogoUrl(URL.createObjectURL(file))
e.target.value = ''
}
}}
/>
</div>
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* File Upload */}
<SheetSection>
<FormField
control={form.control}
name="description"
render={() => (
<FormItemLayout
layout="horizontal"
label="File Upload"
description="Drag-and-drop or select files for upload"
>
<FormControl className="col-span-6">
<div
className={`border-2 rounded-lg p-6 text-center bg-muted transition-colors duration-300 ${
isDragging
? 'border-strong border-dashed bg-muted'
: 'border-border border-dashed'
}`}
onDragOver={(e) => {
e.preventDefault()
setIsDragging(true)
}}
onDragLeave={() => setIsDragging(false)}
onDrop={(e) => {
e.preventDefault()
setIsDragging(false)
const files = Array.from(e.dataTransfer.files)
setUploadedFiles((prev) => [...prev, ...files])
}}
>
<input
type="file"
ref={fileUploadRef}
className="hidden"
multiple
onChange={(e) => {
const files = e.target.files
if (files) {
setUploadedFiles((prev) => [...prev, ...Array.from(files)])
}
e.target.value = ''
}}
/>
<div className="flex flex-col items-center gap-y-2">
<Upload size={20} className="text-foreground-lighter" />
<p className="text-sm text-foreground-light">
{uploadedFiles.length > 0
? `${uploadedFiles.length} file${uploadedFiles.length > 1 ? 's' : ''} selected`
: 'Upload files'}
</p>
<p className="text-xs text-foreground-lighter">
Drag and drop or{' '}
<button
tabIndex={0}
type="button"
onClick={() => fileUploadRef.current?.click()}
className="underline cursor-pointer hover:text-foreground-light"
>
select files
</button>{' '}
to upload
</p>
{uploadedFiles.length > 0 && (
<div className="mt-4 w-full space-y-2">
{uploadedFiles.map((file, idx) => (
<div
key={`${file.name}-${idx}`}
className="flex items-center justify-between gap-2 p-2 bg rounded-sm border"
>
<span className="text-sm text-foreground-light truncate flex-1">
{file.name}
</span>
<Button
size="tiny"
icon={<Trash size={12} />}
onClick={() => {
setUploadedFiles((prev) => prev.filter((_, i) => i !== idx))
}}
/>
</div>
))}
</div>
)}
</div>
</div>
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Switch */}
<SheetSection>
<FormField
control={form.control}
name="enableFeature"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Switch"
description="Toggle for boolean on/off states"
>
<FormControl className="col-span-6">
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItemLayout>
)}
/>
</SheetSection>
{/* Checkbox */}
<SheetSection>
<FormItemLayout
layout="horizontal"
label="Checkbox"
description="Boolean values or multiple selections"
>
<div className="col-span-6 w-full flex flex-col gap-4">
<FormField
control={form.control}
name="enableRls"
render={({ field }) => (
<div className="flex items-center w-full justify-start space-x-2">
<FormControl>
<Checkbox
id="enable-rls"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<label
htmlFor="enable-rls"
className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
Enable Row Level Security
</label>
</div>
)}
/>
<FormField
control={form.control}
name="enableNotifications"
render={({ field }) => (
<div className="flex items-center w-full justify-start space-x-2">
<FormControl>
<Checkbox
id="enable-notifications"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<label
htmlFor="enable-notifications"
className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
Enable email notifications
</label>
</div>
)}
/>
<FormField
control={form.control}
name="enableAnalytics"
render={({ field }) => (
<div className="flex items-center w-full justify-start space-x-2">
<FormControl>
<Checkbox
id="enable-analytics"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<label
htmlFor="enable-analytics"
className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
Enable analytics tracking
</label>
</div>
)}
/>
</div>
</FormItemLayout>
</SheetSection>
<Separator className="w-full" />
{/* Select */}
<SheetSection>
<FormField
control={form.control}
name="region"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Select (Dropdown)"
description="Single selection from a list of options"
>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl className="col-span-6">
<SelectTrigger>
<SelectValue placeholder="Select an option" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="us-east-1">US East (N. Virginia)</SelectItem>
<SelectItem value="us-west-2">US West (Oregon)</SelectItem>
<SelectItem value="eu-west-1">EU West (Ireland)</SelectItem>
</SelectContent>
</Select>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Multi-Select */}
<SheetSection>
<FormField
control={form.control}
name="schemas"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Multi-Select"
description="Multiple selection from a list"
>
<div className="col-span-6">
<MultiSelector
onValuesChange={field.onChange}
values={field.value}
size="small"
className="w-full"
>
<FormControl>
<MultiSelectorTrigger
mode="inline-combobox"
label="Select options..."
badgeLimit="wrap"
showIcon={false}
deletableBadge
className="w-full"
/>
</FormControl>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="public">public</MultiSelectorItem>
<MultiSelectorItem value="auth">auth</MultiSelectorItem>
<MultiSelectorItem value="storage">storage</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
</div>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Radio Group */}
<SheetSection>
<FormField
control={form.control}
name="queueType"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Radio Group"
description="Single selection from multiple options"
>
<div className="col-span-6">
<RadioGroupStacked value={field.value} onValueChange={field.onChange}>
<RadioGroupStackedItem
value="basic"
label="Option 1"
description="First option description"
/>
<RadioGroupStackedItem
value="partitioned"
label="Option 2"
description="Second option description"
/>
</RadioGroupStacked>
</div>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Date Picker */}
<SheetSection>
<FormField
control={form.control}
name="expiryDate"
render={({ field, fieldState }) => (
<FormItemLayout
layout="horizontal"
label="Date Picker"
description="Date selection with calendar popover"
>
<DatePicker>
<FormControl className="col-span-6">
<DatePickerTrigger asChild>
<DatePickerButton block isInvalid={fieldState.invalid}>
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
</DatePickerButton>
</DatePickerTrigger>
</FormControl>
<DatePickerContent>
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
initialFocus
/>
</DatePickerContent>
</DatePicker>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Field Array */}
<SheetSection>
<FormField
control={form.control}
name="redirectUris"
render={() => (
<FormItemLayout
layout="horizontal"
label="Field Array"
description="Dynamic list for adding/removing items"
>
<div className="col-span-6">
<SingleValueFieldArray
control={form.control}
name="redirectUris"
valueFieldName="value"
createEmptyRow={() => ({ value: '' })}
placeholder="https://example.com/callback"
addLabel="Add redirect URI"
removeLabel="Remove redirect URI"
/>
</div>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Key/Value Field Array */}
<SheetSection>
<FormField
control={form.control}
name="httpHeaders"
render={() => (
<FormItemLayout
layout="horizontal"
label="Key/Value Field Array"
description="Repeated text pairs for headers, parameters, and config entries"
>
<div className="col-span-6">
<KeyValueFieldArray
control={form.control}
name="httpHeaders"
keyFieldName="key"
valueFieldName="value"
createEmptyRow={() => ({ key: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
removeLabel="Remove header"
/>
</div>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="w-full" />
{/* Action Field */}
<SheetSection>
<FormItemLayout
layout="horizontal"
label="Action Field"
description="Button or link for navigation or performable actions"
>
<div className="col-span-6 flex gap-2 items-center">
<Button
icon={<ExternalLink size={14} />}
onClick={() => console.log('Action performed')}
>
View documentation
</Button>
<Button onClick={() => console.log('Reset action')}>Reset API key</Button>
</div>
</FormItemLayout>
</SheetSection>
</form>
</Form>
<SheetFooter>
<Button
onClick={() => {
form.reset()
setOpen(false)
}}
>
Cancel
</Button>
<Button variant="primary" form={formId} type="submit">
Create
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</>
)
}