diff --git a/.cursor/rules/e2e-testing.mdc b/.cursor/rules/e2e-testing.mdc new file mode 100644 index 00000000000..346ecf4b75d --- /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 00000000000..ebb06fc7e1f --- /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 391978294b4..754cca1520b 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/README.md b/README.md index 1e438a1917a..08120545543 100644 --- a/README.md +++ b/README.md @@ -96,11 +96,11 @@ Our approach for client libraries is modular. Each sub-library is a standalone i - - - - - + + + + + diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index a1c90d5fc24..1a6766deeab 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -38,17 +38,6 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, - "ConfirmDialog": { - name: "ConfirmDialog", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/Dialogs/ConfirmDialog")), - source: "", - files: ["registry/default//Dialogs/ConfirmDialog.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, "PageContainer": { name: "PageContainer", type: "components:fragment", @@ -2986,5 +2975,93 @@ export const Index: Record = { subcategory: "Composed", chunks: [] }, + "copy-button-verbs": { + name: "copy-button-verbs", + type: "components:example", + registryDependencies: ["button"], + component: React.lazy(() => import("@/registry/default/example/copy-button-verbs")), + source: "", + files: ["registry/default/example/copy-button-verbs.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-form-labels": { + name: "copy-form-labels", + type: "components:example", + registryDependencies: ["form"], + component: React.lazy(() => import("@/registry/default/example/copy-form-labels")), + source: "", + files: ["registry/default/example/copy-form-labels.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-error-messages": { + name: "copy-error-messages", + type: "components:example", + registryDependencies: ["form"], + component: React.lazy(() => import("@/registry/default/example/copy-error-messages")), + source: "", + files: ["registry/default/example/copy-error-messages.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-success-messages": { + name: "copy-success-messages", + type: "components:example", + registryDependencies: ["form"], + component: React.lazy(() => import("@/registry/default/example/copy-success-messages")), + source: "", + files: ["registry/default/example/copy-success-messages.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-tooltips": { + name: "copy-tooltips", + type: "components:example", + registryDependencies: ["tooltip"], + component: React.lazy(() => import("@/registry/default/example/copy-tooltips")), + source: "", + files: ["registry/default/example/copy-tooltips.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-loading-states": { + name: "copy-loading-states", + type: "components:example", + registryDependencies: ["loading-state"], + component: React.lazy(() => import("@/registry/default/example/copy-loading-states")), + source: "", + files: ["registry/default/example/copy-loading-states.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-empty-states": { + name: "copy-empty-states", + type: "components:example", + registryDependencies: ["empty-state"], + component: React.lazy(() => import("@/registry/default/example/copy-empty-states")), + source: "", + files: ["registry/default/example/copy-empty-states.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, + "copy-confirmations": { + name: "copy-confirmations", + type: "components:example", + registryDependencies: ["confirmation"], + component: React.lazy(() => import("@/registry/default/example/copy-confirmations")), + source: "", + files: ["registry/default/example/copy-confirmations.tsx"], + category: "Getting Started", + subcategory: "Copywriting", + chunks: [] + }, }, } diff --git a/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx b/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx index b6b883b9323..9454a245b8a 100644 --- a/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx +++ b/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx @@ -4,7 +4,7 @@ import { SourcePanel } from '@/components/source-panel' import { DashboardTableOfContents } from '@/components/toc' import { siteConfig } from '@/config/site' import { getTableOfContents } from '@/lib/toc' -import { absoluteUrl, cn } from '@/lib/utils' +import { absoluteUrl } from '@/lib/utils' import '@/styles/code-block-variables.css' import '@/styles/mdx.css' import { allDocs } from 'contentlayer/generated' @@ -83,15 +83,15 @@ export default async function DocPage(props: DocPageProps) { const toc = await getTableOfContents(doc.body.raw) return ( -
-
+
+
Docs
{doc.title}
-

{doc.title}

+

{doc.title}

{doc.description && (

{doc.description} @@ -107,15 +107,15 @@ export default async function DocPage(props: DocPageProps) {

{doc.toc && (
-
+
-
+
)} -
+ ) } diff --git a/apps/design-system/app/(app)/layout.tsx b/apps/design-system/app/(app)/layout.tsx index 49b81a77a8f..775f444ff4f 100644 --- a/apps/design-system/app/(app)/layout.tsx +++ b/apps/design-system/app/(app)/layout.tsx @@ -1,34 +1,28 @@ -import SideNavigation from '@/components/side-navigation' +import { SideNavigation } from '@/components/side-navigation' import { SiteFooter } from '@/components/site-footer' -// import ThemeSettings from '@/components/theme-settings' -import TopNavigation from '@/components/top-navigation' +import { TopNavigation } from '@/components/top-navigation' +import { MobileSidebarSheet } from '@/components/mobile-sidebar-sheet' import { ScrollArea } from 'ui' interface AppLayoutProps { children: React.ReactNode } -export default function AppLayout({ children }: AppLayoutProps) { +export default async function AppLayout({ children }: AppLayoutProps) { return ( <> - {/* main container */} -
- {/* main content */} -
- {/* {children} */} -
-
- - {children} -
-
-
-
+ +
+
+ + {children} +
+
) diff --git a/apps/design-system/app/(app)/page.tsx b/apps/design-system/app/(app)/page.tsx index 38bfce96e77..9540c812a46 100644 --- a/apps/design-system/app/(app)/page.tsx +++ b/apps/design-system/app/(app)/page.tsx @@ -1,85 +1,59 @@ import { HomepageSvgHandler } from '@/components/homepage-svg-handler' import Link from 'next/link' +import { Paintbrush } from 'lucide-react' +import { Realtime, Database, Auth } from 'icons/src/icons' export default function Home() { return ( -
-
-
- {/*
- -
*/} -
-

Supabase Design System

-

- Design resources for building consistent user experiences -

-
+
+
+
+

Supabase Design System

+

+ Design resources for building consistent user experiences +

- {/* Homepage items */} - -
- -
- +
+ +
+
+ +

Atom components

Building blocks of User interfaces

- -
- + +
+
+ +

Fragment components

Components assembled from Atoms

- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
+ +
+
+ +
+
+

UI Patterns components

+

+ Components assembled from Atoms & Fragments +

+
+
+ + +
+
+

Colors

@@ -88,27 +62,33 @@ export default function Home() {
- -
- + +
+
+ +

Theming

-

Components assembled from Atoms

+

Simple extensible theming system

- -
- + +
+
+ + + +

Icons

-

Components assembled from Atoms

+

Custom icons for Supabase

-
+ ) } diff --git a/apps/design-system/app/Providers.tsx b/apps/design-system/app/Providers.tsx index 87bac1f1e2f..d2ace2cd703 100644 --- a/apps/design-system/app/Providers.tsx +++ b/apps/design-system/app/Providers.tsx @@ -5,12 +5,15 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes' import { ThemeProviderProps } from 'next-themes/dist/types' import { TooltipProvider } from 'ui' +import { MobileSidebarProvider } from '@/context/mobile-sidebar-context' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return ( - {children} + + {children} + ) diff --git a/apps/design-system/components/command-menu.tsx b/apps/design-system/components/command-menu.tsx index 1673e8f2ed0..5a2f558b385 100644 --- a/apps/design-system/components/command-menu.tsx +++ b/apps/design-system/components/command-menu.tsx @@ -1,6 +1,6 @@ 'use client' -import { CircleIcon, LaptopIcon, MoonIcon, SunIcon } from 'lucide-react' +import { CircleIcon, LaptopIcon, MoonIcon, SunIcon, Search } from 'lucide-react' import { useTheme } from 'next-themes' import { useRouter } from 'next/navigation' import * as React from 'react' @@ -17,6 +17,7 @@ import { CommandList_Shadcn_, CommandSeparator_Shadcn_, DialogProps, + DialogTitle, } from 'ui' export function CommandMenu({ ...props }: DialogProps) { @@ -55,8 +56,7 @@ export function CommandMenu({ ...props }: DialogProps) { + + /> Theme diff --git a/apps/design-system/components/top-navigation.tsx b/apps/design-system/components/top-navigation.tsx index 4aec103a965..5e9c85dff48 100644 --- a/apps/design-system/components/top-navigation.tsx +++ b/apps/design-system/components/top-navigation.tsx @@ -1,30 +1,37 @@ -// import { docsConfig } from '@/config/docs' +'use client' + import Link from 'next/link' +import { Menu } from 'lucide-react' +import { Button } from 'ui' import { CommandMenu } from './command-menu' import { HomepageSvgHandler } from './homepage-svg-handler' import { ThemeSwitcherDropdown } from './theme-switcher-dropdown' +import { useMobileSidebar } from '@/hooks/use-mobile-sidebar' + +export const TopNavigation = () => { + const { toggle } = useMobileSidebar() -function TopNavigation() { return ( -
-
-
JavaScript (TypeScript) supabase-jspostgrest-jsgotrue-jsrealtime-jsstorage-jsfunctions-jspostgrest-jsauth-jsrealtime-jsstorage-jsfunctions-js
Flutter