--- description: How to generate pages and interfaces in Studio, a web interface for managing Supabase projects globs: alwaysApply: true --- ## Project Structure - Next.js app using pages router - Pages go in @apps/studio/pages - Project related pages go in @apps/studio/pages/projects/[ref] - Organization related pages go in @apps/studio/pages/org/[slug] - Studio specific components go in @apps/studio/components - Studio specific generic UI components go in @apps/studio/components/ui - Studio specific components related to individual pages go in @apps/studio/components/interfaces e.g. @apps/studio/components/interfaces/Auth - Generic helper functions go in @apps/studio/lib - Generic hooks go in @apps/studio/hooks ## Component system Our primitive component system is in @packages/ui and is based off shadcn/ui components. These components can be shared across all @apps e.g. studio and docs. Do not introduce new ui components unless asked to. - UI components are imported from this package across apps e.g. import { Button, Badge } from 'ui' - Some components have a _Shadcn_ namespace appended to component name e.g. import { Input*Shadcn* } from 'ui' - We should be using _Shadcn_ components where possible - Before composing interfaces, read @packages/ui/index.tsx file for a full list of available components ## Styling We use Tailwind for styling. - You should never use tailwind classes for colours and instead use classes we've defined ourselves - Backgrounds // most of the time you will not need to define a background - 'bg' used for main app surface background - 'bg-muted' for elevating content // you can use Card instead - 'bg-warning' for highlighting information that needs to be acted on - 'bg-destructive' for highlighting issues - Text - 'text-foreground' for primary text like headings - 'text-foreground-light' for body text - 'text-foreground-lighter' for subtle text - 'text-warning' for calling out information that needs action - 'text-destructive' for calling out when something went wrong - When needing to apply typography styles, read @apps/studio/styles/typography.scss and use one of the available classes instead of hard coding classes e.g. use "heading-default" instead of "text-sm font-medium" - When applying focus styles for keyboard navigation, read @apps/studio/styles/focus.scss for any appropriate classes for consistency with other focus styles ## Page structure When creating a new page follow these steps: - Create the page in @apps/studio/pages - Use the PageLayout component that has the following props ```jsx export interface NavigationItem { id?: string label: string href?: string icon?: ReactNode onClick?: () => void badge?: string active?: boolean } interface PageLayoutProps { children?: ReactNode title?: string | ReactNode subtitle?: string | ReactNode icon?: ReactNode breadcrumbs?: Array<{ label?: string href?: string element?: ReactNode }> primaryActions?: ReactNode secondaryActions?: ReactNode navigationItems?: NavigationItem[] className?: string size?: 'default' | 'full' | 'large' | 'small' isCompact?: boolean } ``` - If a page has page related actions, add them to primary and secondary action props e.g. Users page has "Create new user" action - If a page is within an existing section (e.g. Auth), you should use the related layout component e.g. AuthLayout - Create a new component in @apps/studio/components/interfaces for the contents of the page - Use ScaffoldContainer if the page should be center aligned in a container - Use ScaffoldSection, ScaffoldSectionTitle, ScaffoldSectionDescription if the page has multiple sections ### Page example ```jsx import { MyPageComponent } from 'components/interfaces/MyPage/MyPageComponent' import AuthLayout from './AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer } from 'components/layouts/Scaffold' import type { NextPageWithLayout } from 'types' const MyPage: NextPageWithLayout = () => { return ( ) } MyPage.getLayout = (page) => ( {page} ) export default MyPage export const MyPageComponent = () => (
My page section A brief description of the purpose of the page
// Content goes here
) ``` ## Forms Forms in Supabase Studio should follow consistent patterns to ensure a cohesive user experience across settings pages and side panels. ### 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, Card, CardContent, CardFooter, Form_Shadcn_, FormField_Shadcn_, FormControl_Shadcn_, Input_Shadcn_, Switch, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' const formSchema = z.object({ name: z.string().min(1, 'Name is required'), enableFeature: z.boolean(), }) export function SettingsForm() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { name: '', enableFeature: false }, mode: 'onSubmit', reValidateMode: 'onBlur', }) function onSubmit(values: z.infer) { // handle mutation with onSuccess/onError toast } return (
( )} /> ( )} /> {form.formState.isDirty && ( )}
) } ``` ### 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 - Cards can have sections with CardContent - Use CardFooter for actions - Only use CardHeader and CardTitle if the card content has not been described by the surrounding content e.g. Page title or ScaffoldSectionTitle - Use CardHeader and CardTitle when you are using multiple Cards to group related pieces of content e.g. Primary branch, Persistent branches, Preview branches ## 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 - When doing a mutation, always use the mutate function. Always use onSuccess and onError with a toast.success and toast.error. - Use mutateAsync only if the mutation is part of multiple async actions. Wrap the mutateAsync call with try/catch block and add toast.success and toast.error. ## Tables - Use the generic ui table components for most tables - Tables are generally contained witin a card - If a table has associated actions, they should go above on right hand side - If a table has associated search or filters, they should go above on left hand side - If a table is the main content of a page, and it does not have search or filters, you can add table actions to primary and secondary actions of PageLayout - If a table is the main content of a page section, and it does not have search or filters, you can add table actions to the right of ScaffoldSectionTitle - For simple lists of objects you can use ResourceList with ResourceListItem instead ### Table example ```jsx import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from 'ui' ;A list of your recent invoices. Invoice Status Method Amount INV001 Paid Credit Card $250.00
``` ## Alerts - Use Admonition component to alert users of important actions or restrictions in place - Place the Admonition either at the top of the contents of the page (below page title) or at the top of the related ScaffoldSection , below ScaffoldTitle - Use sparingly ### Alert example ```jsx ```