diff --git a/.cursor/rules/e2e-testing.mdc b/.cursor/rules/e2e-testing.mdc new file mode 100644 index 0000000000000..346ecf4b75d81 --- /dev/null +++ b/.cursor/rules/e2e-testing.mdc @@ -0,0 +1,324 @@ +--- +description: E2E testing best practices for Playwright tests in Studio +globs: + - e2e/studio/**/*.ts + - e2e/studio/**/*.spec.ts +alwaysApply: true +--- + +# E2E Testing Best Practices + +## Getting Context + +Before writing or modifying tests, use the Playwright MCP to understand: + +- Available page elements and their roles/locators +- Current page state and network activity +- Existing test patterns in the codebase + +Avoid extensive code reading - let Playwright's inspection tools guide your understanding of the UI. + +## Avoiding Race Conditions + +### Set up API waiters BEFORE triggering actions + +The most common source of flaky tests is race conditions between UI actions and API calls. Always create response waiters before clicking buttons or navigating. + +```ts +// ❌ Bad - race condition: response might complete before waiter is set up +await page.getByRole('button', { name: 'Save' }).click() +await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create') + +// ✅ Good - waiter is ready before action +const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create') +await page.getByRole('button', { name: 'Save' }).click() +await apiPromise +``` + +### Use `createApiResponseWaiter` for pre-navigation waits + +When you need to wait for a response that happens during navigation: + +```ts +// ✅ Good - waiter created before navigation +const loadPromise = waitForTableToLoad(page, ref) +await page.goto(toUrl(`/project/${ref}/editor?schema=public`)) +await loadPromise +``` + +### Wait for multiple related API calls with Promise.all + +When an action triggers multiple API calls, wait for all of them: + +```ts +// ✅ Good - wait for all related API calls +const createTablePromise = waitForApiResponseWithTimeout(page, (response) => + response.url().includes('query?key=table-create') +) +const tablesPromise = waitForApiResponseWithTimeout(page, (response) => + response.url().includes('tables?include_columns=true') +) +const entitiesPromise = waitForApiResponseWithTimeout(page, (response) => + response.url().includes('query?key=entity-types-') +) + +await page.getByRole('button', { name: 'Save' }).click() +await Promise.all([createTablePromise, tablesPromise, entitiesPromise]) +``` + +## Waiting Strategies + +### Prefer Playwright's built-in auto-waiting + +Playwright automatically waits for elements to be actionable. Use this instead of manual timeouts: + +```ts +// ❌ Bad - arbitrary timeout +await page.waitForTimeout(2000) +await page.getByRole('button', { name: 'Submit' }).click() + +// ✅ Good - auto-waits for element to be visible and enabled +await page.getByRole('button', { name: 'Submit' }).click() +``` + +### Use `expect.poll` for dynamic assertions + +When waiting for state to change: + +```ts +// ✅ Good - polls until condition is met +await expect + .poll(async () => { + return await page.getByLabel(`View ${tableName}`).count() + }) + .toBe(0) +``` + +### Use `waitForSelector` with state for element lifecycle + +```ts +// ✅ Good - wait for panel to close +await page.waitForSelector('[data-testid="side-panel"]', { state: 'detached' }) +``` + +### Avoid `networkidle` - use specific API waits instead + +```ts +// ❌ Bad - unreliable and slow +await page.waitForLoadState('networkidle') + +// ✅ Good - wait for specific API response +await waitForApiResponse(page, 'pg-meta', ref, 'tables') +``` + +### Use timeouts sparingly and only for non-API waits + +```ts +// ✅ Acceptable - waiting for client-side debounce +await page.getByRole('textbox').fill('search term') +await page.waitForTimeout(300) // Allow debounce to complete + +// ✅ Acceptable - waiting for clipboard API +await page.evaluate(() => navigator.clipboard.readText()) +await page.waitForTimeout(500) +``` + +## Test Structure + +### Use the custom test utility + +Always import from the custom test utility for consistent fixtures: + +```ts +import { test } from '../utils/test.js' +``` + +### Use `withFileOnceSetup` for expensive setup + +When setup is expensive (cleanup, seeding), run it once per file: + +```ts +test.beforeAll(async ({ browser, ref }) => { + await withFileOnceSetup(import.meta.url, async () => { + const ctx = await browser.newContext() + const page = await ctx.newPage() + + // Expensive setup logic (e.g., cleanup old test data) + await deleteTestTables(page, ref) + }) +}) + +test.afterAll(async () => { + await releaseFileOnceCleanup(import.meta.url) +}) +``` + +### Dismiss toasts before interacting with UI + +Toasts can overlay buttons and block interactions: + +```ts +const dismissToastsIfAny = async (page: Page) => { + const closeButtons = page.getByRole('button', { name: 'Close toast' }) + const count = await closeButtons.count() + for (let i = 0; i < count; i++) { + await closeButtons.nth(i).click() + } +} + +// ✅ Good - dismiss toasts before clicking +await dismissToastsIfAny(page) +await page.getByRole('button', { name: 'New table' }).click() +``` + +## Assertions + +### Always include descriptive messages + +```ts +// ❌ Bad - no context on failure +await expect(page.getByRole('button', { name: 'Save' })).toBeVisible() + +// ✅ Good - clear message on failure +await expect( + page.getByRole('button', { name: 'Save' }), + 'Save button should be visible after form is filled' +).toBeVisible() +``` + +### Use appropriate timeouts for slow operations + +```ts +// ✅ Good - explicit timeout for slow operations +await expect( + page.getByText(`Table ${tableName} is good to go!`), + 'Success toast should be visible after table creation' +).toBeVisible({ timeout: 50000 }) +``` + +## Locators + +### Prefer role-based locators + +```ts +// ✅ Good - semantic and resilient +page.getByRole('button', { name: 'Save' }) +page.getByRole('textbox', { name: 'Username' }) +page.getByRole('menuitem', { name: 'Delete' }) + +// ❌ Avoid - brittle CSS selectors +page.locator('.btn-primary') +page.locator('#submit-button') +``` + +### Use test IDs for complex elements + +```ts +// ✅ Good - stable identifier for complex elements +page.getByTestId('table-editor-side-panel') +page.getByTestId('action-bar-save-row') +``` + +### Use `filter` for finding elements in context + +```ts +// ✅ Good - find button within specific row +const bucketRow = page.getByRole('row').filter({ hasText: bucketName }) +await bucketRow.getByRole('button').click() +``` + +## Helper Functions + +### Extract reusable operations into helpers + +Create helper functions for common operations: + +```ts +// e2e/studio/utils/storage-helpers.ts +export const createBucket = async ( + page: Page, + ref: string, + bucketName: string, + isPublic: boolean = false +) => { + await navigateToStorageFiles(page, ref) + + // Check if already exists + const bucketRow = page.getByRole('row').filter({ hasText: bucketName }) + if ((await bucketRow.count()) > 0) return + + await dismissToastsIfAny(page) + + // Create bucket with proper waits + const apiPromise = waitForApiResponse(page, 'storage', ref, 'bucket', { method: 'POST' }) + await page.getByRole('button', { name: 'New bucket' }).click() + await page.getByRole('textbox', { name: 'Bucket name' }).fill(bucketName) + await page.getByRole('button', { name: 'Create' }).click() + await apiPromise + + await expect( + page.getByRole('row').filter({ hasText: bucketName }), + `Bucket ${bucketName} should be visible` + ).toBeVisible() +} +``` + +### Use the existing wait utilities + +```ts +import { + createApiResponseWaiter, + waitForApiResponse, + waitForGridDataToLoad, + waitForTableToLoad, +} from '../utils/wait-for-response.js' +``` + +## API Mocking + +### Mock APIs for isolated testing + +```ts +// ✅ Good - mock API response +await page.route('*/**/logs.all*', async (route) => { + await route.fulfill({ body: JSON.stringify(mockAPILogs) }) +}) +``` + +### Use soft waits for optional API calls + +```ts +// ✅ Good - don't fail if API doesn't respond +await waitForApiResponse(page, 'pg-meta', ref, 'optional-endpoint', { + soft: true, + fallbackWaitMs: 1000, +}) +``` + +## Cleanup + +### Clean up test data in beforeAll/beforeEach + +```ts +test.beforeEach(async ({ page, ref }) => { + await deleteAllBuckets(page, ref) +}) +``` + +### Handle existing state gracefully + +```ts +// ✅ Good - check before trying to delete +const bucketRow = page.getByRole('row').filter({ hasText: bucketName }) +if ((await bucketRow.count()) === 0) return +// proceed with deletion +``` + +### Reset local storage when needed + +```ts +import { resetLocalStorage } from '../utils/reset-local-storage.js' + +// Clean up after tests that modify local storage +await resetLocalStorage(page, ref) +``` diff --git a/.cursor/rules/studio-best-practices.mdc b/.cursor/rules/studio-best-practices.mdc new file mode 100644 index 0000000000000..ebb06fc7e1f88 --- /dev/null +++ b/.cursor/rules/studio-best-practices.mdc @@ -0,0 +1,434 @@ +--- +description: React best practices and coding standards for Studio +globs: + - apps/studio/**/*.tsx + - apps/studio/**/*.ts +alwaysApply: true +--- + +# Studio Best Practices + +## Boolean Handling + +### Assign complex conditions to descriptive variables + +When you have multiple conditions in a single expression, extract them into well-named boolean variables. This improves readability and makes the code self-documenting. + +```tsx +// ❌ Bad - complex inline condition +{ + !isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && ( + + ) +} + +// ✅ Good - extract to descriptive variables +const isTableEntity = isTableLike(selectedTable) +const canShowAddButton = !isSchemaLocked && isTableEntity && canUpdateColumns && !isLoading + +{ + canShowAddButton && +} +``` + +### Use consistent naming conventions for booleans + +- Use `is` prefix for state/identity: `isLoading`, `isPaused`, `isNewRecord`, `isError` +- Use `has` prefix for possession: `hasPermission`, `hasShownModal`, `hasData` +- Use `can` prefix for capability/permission: `canUpdateColumns`, `canDelete`, `canEdit` +- Use `should` prefix for conditional behavior: `shouldFetch`, `shouldRender`, `shouldValidate` + +```tsx +// ✅ Good examples from codebase +const isNewRecord = column === undefined +const isPaused = project?.status === PROJECT_STATUS.INACTIVE +const isMatureProject = dayjs(project?.inserted_at).isBefore(dayjs().subtract(10, 'day')) +const { can: canUpdateColumns } = useAsyncCheckPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'columns' +) +``` + +### Derive boolean state instead of storing it + +When a boolean can be computed from existing state, derive it rather than storing it separately. + +```tsx +// ❌ Bad - storing derived state +const [isFormValid, setIsFormValid] = useState(false) + +useEffect(() => { + setIsFormValid(name.length > 0 && email.includes('@')) +}, [name, email]) + +// ✅ Good - derive from existing state +const isFormValid = name.length > 0 && email.includes('@') +``` + +## Component Structure + +### Break down large components + +Components should ideally be under 200-300 lines. If a component grows larger, consider splitting it. + +**Signs a component should be split:** + +- Multiple distinct UI sections +- Complex conditional rendering logic +- Multiple useState hooks for unrelated state +- Difficult to understand at a glance + +```tsx +// ❌ Bad - monolithic component with everything inline +const UserDashboard = () => { + // 50 lines of hooks and state + // 100 lines of handlers + // 300 lines of JSX with nested conditions +} + +// ✅ Good - split into focused sub-components +const UserDashboard = () => { + return ( +
+ + + + +
+ ) +} +``` + +### Co-locate related components + +Place sub-components in the same directory as the parent component. Use an index file for cleaner imports. + +``` +components/interfaces/Auth/Users/ +├── UserPanel.tsx +├── UserOverview.tsx +├── UserLogs.tsx +├── Users.constants.ts +└── index.ts +``` + +### Extract repeated JSX patterns + +If you find yourself copying similar JSX blocks, extract them into a component. + +```tsx +// ❌ Bad - repeated pattern + + Overview + + + Logs + + +// ✅ Good - extract to component +const PanelTab = ({ value, children }: { value: string; children: ReactNode }) => ( + + {children} + +) +``` + +## Loading and Error States + +### Use consistent loading/error/success pattern + +Follow a consistent pattern for handling async states: + +```tsx +const { data, error, isLoading, isError, isSuccess } = useQuery() + +// Handle loading state first +if (isLoading) { + return +} + +// Handle error state +if (isError) { + return +} + +// Handle empty state if needed +if (isSuccess && data.length === 0) { + return +} + +// Render success state +return +``` + +### Use early returns for guard clauses + +Prefer early returns over deeply nested conditionals: + +```tsx +// ❌ Bad - deeply nested +const Component = () => { + if (data) { + if (!isError) { + if (hasPermission) { + return + } + } + } + return null +} + +// ✅ Good - early returns +const Component = () => { + if (!data) return null + if (isError) return + if (!hasPermission) return + + return +} +``` + +## State Management + +### Keep state as local as possible + +Start with local state and lift up only when needed. + +```tsx +// ✅ Good - state lives where it's used +const SearchableList = () => { + const [filterString, setFilterString] = useState('') + + const filteredItems = items.filter((item) => item.name.includes(filterString)) + + return ( +
+ setFilterString(e.target.value)} /> + +
+ ) +} +``` + +### Group related state with objects or reducers + +When you have multiple related pieces of state, consider grouping them: + +```tsx +// ❌ Bad - multiple related useState calls +const [name, setName] = useState('') +const [email, setEmail] = useState('') +const [phone, setPhone] = useState('') + +// ✅ Good - grouped state for forms (use react-hook-form) +const form = useForm({ + defaultValues: { name: '', email: '', phone: '' }, +}) +``` + +## Custom Hooks + +### Extract complex logic into custom hooks + +When logic becomes reusable or complex, extract it: + +```tsx +// ✅ Good - extracted to custom hook +export function useAsyncCheckPermissions(action: string, resource: string) { + const { permissions, isLoading, isSuccess } = useGetProjectPermissions() + + const can = useMemo(() => { + if (!IS_PLATFORM) return true + if (!isSuccess || !permissions) return false + return doPermissionsCheck(permissions, action, resource) + }, [isSuccess, permissions, action, resource]) + + return { isLoading, isSuccess, can } +} + +// Usage +const { can: canUpdateColumns } = useAsyncCheckPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'columns' +) +``` + +### Return objects from hooks for better extensibility + +```tsx +// ❌ Bad - returning array (hard to extend) +const useToggle = () => { + const [value, setValue] = useState(false) + return [value, () => setValue((v) => !v)] +} + +// ✅ Good - returning object (easy to extend) +const useToggle = (initial = false) => { + const [value, setValue] = useState(initial) + return { + value, + toggle: () => setValue((v) => !v), + setTrue: () => setValue(true), + setFalse: () => setValue(false), + } +} +``` + +## Event Handlers + +### Name handlers consistently + +Use `on` prefix for prop callbacks and `handle` prefix for internal handlers: + +```tsx +interface Props { + onClose: () => void // Callback prop + onSave: (data: Data) => void +} + +const Component = ({ onClose, onSave }: Props) => { + const handleSubmit = () => { + // Internal handler + // process data + onSave(data) + } + + const handleCancel = () => { + // cleanup + onClose() + } +} +``` + +### Avoid inline arrow functions for expensive operations + +```tsx +// ❌ Bad - creates new function every render + handleItemClick(item)} +/> + +// ✅ Good - stable reference with useCallback +const handleItemClick = useCallback((item: Item) => { + // handle click +}, [dependencies]) + + +``` + +## Conditional Rendering + +### Use appropriate patterns for different scenarios + +```tsx +// Simple show/hide - use && +{ + isVisible && +} + +// Binary choice - use ternary +{ + isLoading ? : +} + +// Multiple conditions - use early returns or extracted component +const StatusDisplay = ({ status }: { status: Status }) => { + if (status === 'loading') return + if (status === 'error') return + if (status === 'empty') return + return +} +``` + +### Avoid nested ternaries + +```tsx +// ❌ Bad - nested ternary +{ + isLoading ? : isError ? : +} + +// ✅ Good - separate conditions or early returns +if (isLoading) return +if (isError) return +return +``` + +## Performance + +### Use useMemo for expensive computations + +```tsx +// ✅ Good - memoize expensive filtering +const filteredItems = useMemo( + () => items.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase())), + [items, searchQuery] +) +``` + +### Avoid premature optimization + +Don't wrap everything in useMemo/useCallback. Only optimize when: + +- You have measured a performance problem +- The computation is genuinely expensive +- The value is passed to memoized children + +## TypeScript + +### Define prop interfaces explicitly + +```tsx +interface UserCardProps { + user: User + onEdit: (user: User) => void + onDelete: (userId: string) => void + isEditable?: boolean +} + +export const UserCard = ({ user, onEdit, onDelete, isEditable = true }: UserCardProps) => { + // ... +} +``` + +### Use discriminated unions for complex state + +```tsx +type AsyncState = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: T } + | { status: 'error'; error: Error } +``` + +### Avoid type casting, prefer validation with zod + +Never use type casting (e.g., `as any`, `as Type`). Instead, validate values at runtime using zod schemas. This ensures type safety and catches runtime errors. + +```tsx +// ❌ Bad - type casting bypasses type checking +const user = apiResponse as User +const data = unknownValue as string + +// ✅ Good - validate with zod schema +const userSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email(), +}) + +const user = userSchema.parse(apiResponse) +const data = z.string().parse(unknownValue) + +// ✅ Good - safe parsing with error handling +const result = userSchema.safeParse(apiResponse) +if (result.success) { + const user = result.data +} else { + // handle validation errors +} +``` diff --git a/.cursor/rules/studio-ui.mdc b/.cursor/rules/studio-ui.mdc index 391978294b4bb..754cca1520b8f 100644 --- a/.cursor/rules/studio-ui.mdc +++ b/.cursor/rules/studio-ui.mdc @@ -125,63 +125,106 @@ export const MyPageComponent = () => ( ## Forms -- Build forms with `react-hook-form` + `zod`. -- Use our `_Shadcn_` form primitives from `ui` and prefer `FormItemLayout` with layout="flex-row-reverse" for most controls (see `apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx`). -- Keep imports from `ui` with `_Shadcn_` suffixes. -- Forms should generally be wrapped in a Card unless specified -- If the submit button is outside the form, add a new variable named formId outside the component, and set it as property id on the form element and formId on the button. +Forms in Supabase Studio should follow consistent patterns to ensure a cohesive user experience across settings pages and side panels. -### Example (single field) +### Core Principles + +- Build forms with `react-hook-form` + `zod` +- Always use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription` +- Always wrap form inputs with `FormControl_Shadcn_` to ensure proper form integration +- Keep imports from `ui` with `_Shadcn_` suffixes +- Handle dirty state: Show cancel buttons and disable save buttons based on `form.formState.isDirty` +- Show loading states on submit buttons using the `loading` prop +- If the submit button is outside the form, add a `formId` variable outside the component, set it as `id` on the form element and `form` prop on the button + +### Layout Selection + +- **Page layouts**: Use `FormItemLayout` with `layout="flex-row-reverse"` for horizontal alignment. Forms should be wrapped in a `Card` with each form field in its own `CardContent`, and `CardFooter` for actions. The layout automatically handles consistent input widths (50% on md, 40% on xl, min-w-100). +- **Side panels (wide)**: Use `FormItemLayout` with `layout="horizontal"`. Use `SheetSection` to wrap each field group. +- **Side panels (narrow, size="sm" or below)**: Use `FormItemLayout` with `layout="vertical"` + +### Page Layout Form Example ```tsx import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' import * as z from 'zod' -import { Button, Form_Shadcn_, FormField_Shadcn_, FormControl_Shadcn_, Input_Shadcn_ } from 'ui' +import { + Button, + Card, + CardContent, + CardFooter, + Form_Shadcn_, + FormField_Shadcn_, + FormControl_Shadcn_, + Input_Shadcn_, + Switch, +} from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -const profileSchema = z.object({ - username: z.string().min(2, 'Username must be at least 2 characters'), +const formSchema = z.object({ + name: z.string().min(1, 'Name is required'), + enableFeature: z.boolean(), }) -const formId = `profile-form` - -export function ProfileForm() { - const form = useForm>({ - resolver: zodResolver(profileSchema), - defaultValues: { username: '' }, +export function SettingsForm() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { name: '', enableFeature: false }, mode: 'onSubmit', reValidateMode: 'onBlur', }) - function onSubmit(values: z.infer) { - // handle values + function onSubmit(values: z.infer) { + // handle mutation with onSuccess/onError toast } return ( -
+ - + + ( + + + + + + )} + /> + + ( - + )} /> - - + )} + @@ -192,6 +235,106 @@ export function ProfileForm() { } ``` +### Side Panel Form Example + +```tsx +import { zodResolver } from '@hookform/resolvers/zod' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +import { + Button, + Form_Shadcn_, + FormField_Shadcn_, + FormControl_Shadcn_, + Input_Shadcn_, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +const formSchema = z.object({ + name: z.string().min(1, 'Name is required'), +}) + +const formId = 'sidepanel-form' + +export function CreateResourcePanel() { + const [open, setOpen] = useState(false) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { name: '' }, + }) + + function onSubmit(values: z.infer) { + // handle mutation + setOpen(false) + } + + return ( + + + + Create Resource + + + + + ( + + + + + + )} + /> + + + + + + + + + + ) +} +``` + +### Common Form Field Types + +- **Text Input**: `Input_Shadcn_` with `placeholder` +- **Password Input**: `Input_Shadcn_` with `type="password"` +- **Number Input**: `Input_Shadcn_` with `type="number"` and `onChange={(e) => field.onChange(Number(e.target.value))}` +- **Input with Units**: Wrap `Input_Shadcn_` with `PrePostTab` component: `` +- **Textarea**: `Textarea` component with `rows` and `className="resize-none"` +- **Switch**: `Switch` with `checked={field.value} onCheckedChange={field.onChange}` +- **Checkbox**: `Checkbox_Shadcn_` with label, use multiple for checkbox groups +- **Select**: `Select_Shadcn_` with `SelectTrigger_Shadcn_`, `SelectContent_Shadcn_`, `SelectItem_Shadcn_` +- **Multi-Select**: Use `MultiSelector` from `ui-patterns/multi-select` +- **Radio Group**: `RadioGroupStacked` with `RadioGroupStackedItem` for stacked options with descriptions +- **Date Picker**: `Calendar` inside `Popover_Shadcn_` with a trigger button +- **Copyable Input**: Use `Input` from `ui-patterns/DataInputs/Input` with `copy` and `readOnly` props +- **Field Array**: Use `useFieldArray` from `react-hook-form` for dynamic add/remove fields +- **Action Field**: Use `FormItemLayout` without form control, just buttons for navigation or performable actions. Wrap buttons in a div with `justify-end` to align them to the right + ## Cards - Use cards when needing to group related pieces of information @@ -203,6 +346,11 @@ export function ProfileForm() { ## Sheets - Use a sheet when needing to reveal more complicated forms or information relating to an object and context switching away to a new page would be disruptive e.g. we list auth providers, clicking an auth provider opens a sheet with information about that provider and a form to enable, user can close sheet to go back to providers list +- Use `SheetContent` with `size="lg"` for forms that need horizontal layout +- Use `SheetHeader`, `SheetTitle`, `SheetSection`, and `SheetFooter` for consistent structure +- Place submit/cancel buttons in `SheetFooter` +- For forms in sheets, use `FormItemLayout` with `layout="horizontal"` for wider panels or `layout="vertical"` for narrow panels (size="sm" or below) +- See the Forms section for a complete side panel form example ## React Query @@ -223,7 +371,6 @@ export function ProfileForm() { ```jsx import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from 'ui' - ;A list of your recent invoices. diff --git a/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx index 71cf285027401..1dead4a7f7383 100644 --- a/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx +++ b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx @@ -27,7 +27,6 @@ import { SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, - Separator, Switch, Textarea, } from 'ui' @@ -116,10 +115,10 @@ export default function FormPatternsPageLayout() { -
+ - - {/* Text Input */} + {/* Text Input */} + @@ -136,10 +134,10 @@ export default function FormPatternsPageLayout() { )} /> + - - - {/* Password Input */} + {/* Password Input */} + @@ -156,10 +153,10 @@ export default function FormPatternsPageLayout() { )} /> + - - - {/* Copyable Input */} + {/* Copyable Input */} + )} /> + - - - {/* Number Input */} + {/* Number Input */} + )} /> + - - - {/* Input with Units */} + {/* Input with Units */} + @@ -230,10 +224,10 @@ export default function FormPatternsPageLayout() { )} /> + - - - {/* Textarea */} + {/* Textarea */} +