mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
Update Cursor rules structure (#41338)
* update and expand cursor rules structure * rule copy * rule updates
This commit is contained in:
@@ -1,267 +0,0 @@
|
||||
---
|
||||
description: Docs GraphQL Architecture
|
||||
globs: apps/docs/resources/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Docs GraphQL Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The `/apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure.
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
Each GraphQL query follows this structure:
|
||||
|
||||
```
|
||||
resources/
|
||||
├── queryObject/
|
||||
│ ├── queryObjectModel.ts # Data models and business logic
|
||||
│ ├── queryObjectSchema.ts # GraphQL type definitions
|
||||
│ ├── queryObjectResolver.ts # Query resolver and arguments
|
||||
│ ├── queryObjectTypes.ts # TypeScript interfaces (optional)
|
||||
│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional)
|
||||
├── utils/
|
||||
│ ├── connections.ts # GraphQL connection/pagination utilities
|
||||
│ └── fields.ts # GraphQL field selection utilities
|
||||
├── rootSchema.ts # Main GraphQL schema with all queries
|
||||
└── rootSync.ts # Root sync script for syncing to database
|
||||
```
|
||||
|
||||
## Example queries
|
||||
|
||||
1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content
|
||||
2. **error** (`error/`) - Error code lookup for Supabase services
|
||||
3. **schema** - GraphQL schema introspection
|
||||
|
||||
## Key Files
|
||||
|
||||
### `rootSchema.ts`
|
||||
- Main GraphQL schema definition
|
||||
- Imports all resolvers and combines them into the root query
|
||||
- Defines the `RootQueryType` with all top-level fields
|
||||
|
||||
### `utils/connections.ts`
|
||||
- Provides `createCollectionType()` for paginated collections
|
||||
- `GraphQLCollectionBuilder` for building collection responses
|
||||
- Standard pagination arguments and edge/node patterns
|
||||
|
||||
### `utils/fields.ts`
|
||||
- `graphQLFields()` utility to analyze requested fields in resolvers
|
||||
- Used for optimizing data fetching based on what fields are actually requested
|
||||
|
||||
## Creating a New Top-Level Query
|
||||
|
||||
To add a new GraphQL query, follow these steps:
|
||||
|
||||
### 1. Create Query Folder Structure
|
||||
```bash
|
||||
mkdir resources/newQuery
|
||||
touch resources/newQuery/newQueryModel.ts
|
||||
touch resources/newQuery/newQuerySchema.ts
|
||||
touch resources/newQuery/newQueryResolver.ts
|
||||
```
|
||||
|
||||
### 2. Define GraphQL Schema (`newQuerySchema.ts`)
|
||||
```typescript
|
||||
import { GraphQLObjectType, GraphQLString } from 'graphql'
|
||||
|
||||
export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const
|
||||
|
||||
export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({
|
||||
name: 'NewQuery',
|
||||
description: 'Description of what this query returns',
|
||||
fields: {
|
||||
id: {
|
||||
type: GraphQLString,
|
||||
description: 'Unique identifier',
|
||||
},
|
||||
// Add other fields...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Create Data Model (`newQueryModel.ts`)
|
||||
|
||||
> [!NOTE]
|
||||
> The data model should be agnostic to GraphQL. It may import argument types
|
||||
> from `~/__generated__/graphql`, but otherwise all functions and classes
|
||||
> should be unaware of whether they are called for GraphQL resolution.
|
||||
|
||||
> [!TIP]
|
||||
> The types in `~/__generated__/graphql` for a new endpoint will not exist
|
||||
> until the code generation is run in the next step.
|
||||
|
||||
```typescript
|
||||
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
|
||||
import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils'
|
||||
import { Result } from '~/features/helpers.fn'
|
||||
import { supabase } from '~/lib/supabase'
|
||||
|
||||
export class NewQueryModel {
|
||||
constructor(public readonly data: {
|
||||
id: string
|
||||
// other properties...
|
||||
}) {}
|
||||
|
||||
static async loadData(
|
||||
args: RootQueryTypeNewQueryArgs,
|
||||
requestedFields: Array<string>
|
||||
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
// Implement data fetching logic
|
||||
const result = new Result(
|
||||
await supabase()
|
||||
.from('your_table')
|
||||
.select('*')
|
||||
// Add filters based on args
|
||||
)
|
||||
.map((data) => data.map((item) => new NewQueryModel(item)))
|
||||
.mapError(convertPostgrestToApiError)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Create Resolver (`newQueryResolver.ts`)
|
||||
```typescript
|
||||
import { GraphQLError, GraphQLNonNull, GraphQLString, type GraphQLResolveInfo } from 'graphql'
|
||||
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
|
||||
import { convertUnknownToApiError } from '~/app/api/utils'
|
||||
import { Result } from '~/features/helpers.fn'
|
||||
import { graphQLFields } from '../utils/fields'
|
||||
import { NewQueryModel } from './newQueryModel'
|
||||
import { GRAPHQL_FIELD_NEW_QUERY, GraphQLObjectTypeNewQuery } from './newQuerySchema'
|
||||
|
||||
async function resolveNewQuery(
|
||||
_parent: unknown,
|
||||
args: RootQueryTypeNewQueryArgs,
|
||||
_context: unknown,
|
||||
info: GraphQLResolveInfo
|
||||
): Promise<NewQueryModel[] | GraphQLError> {
|
||||
return (
|
||||
await Result.tryCatchFlat(
|
||||
resolveNewQueryImpl,
|
||||
convertUnknownToApiError,
|
||||
args,
|
||||
info
|
||||
)
|
||||
).match(
|
||||
(data) => data,
|
||||
(error) => {
|
||||
console.error(`Error resolving ${GRAPHQL_FIELD_NEW_QUERY}:`, error)
|
||||
return new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function resolveNewQueryImpl(
|
||||
args: RootQueryTypeNewQueryArgs,
|
||||
info: GraphQLResolveInfo
|
||||
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
const fieldsInfo = graphQLFields(info)
|
||||
const requestedFields = Object.keys(fieldsInfo)
|
||||
return await NewQueryModel.loadData(args, requestedFields)
|
||||
}
|
||||
|
||||
export const newQueryRoot = {
|
||||
[GRAPHQL_FIELD_NEW_QUERY]: {
|
||||
description: 'Description of what this query does',
|
||||
args: {
|
||||
id: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'Required argument description',
|
||||
},
|
||||
// Add other arguments...
|
||||
},
|
||||
type: GraphQLObjectTypeNewQuery, // or createCollectionType() for lists
|
||||
resolve: resolveNewQuery,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Register in Root Schema
|
||||
In `rootSchema.ts`, add your resolver:
|
||||
|
||||
```typescript
|
||||
// Import your resolver
|
||||
import { newQueryRoot } from './newQuery/newQueryResolver'
|
||||
|
||||
// Add to the query fields
|
||||
export const rootGraphQLSchema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'RootQueryType',
|
||||
fields: {
|
||||
...introspectRoot,
|
||||
...searchRoot,
|
||||
...errorRoot,
|
||||
...newQueryRoot, // Add this line
|
||||
},
|
||||
}),
|
||||
types: [
|
||||
GraphQLObjectTypeGuide,
|
||||
GraphQLObjectTypeReferenceCLICommand,
|
||||
GraphQLObjectTypeReferenceSDKFunction,
|
||||
GraphQLObjectTypeTroubleshooting,
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### 6. Update TypeScript Types
|
||||
Run the GraphQL codegen to update TypeScript types:
|
||||
```bash
|
||||
pnpm run -F docs codegen:graphql
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Error Handling**: Error handling always uses the Result class, defined in apps/docs/features/helpers.fn.ts
|
||||
2. **Field Optimization**: Use `graphQLFields()` to only fetch requested data
|
||||
3. **Collections**: Use `createCollectionType()` for paginated lists
|
||||
4. **Naming**: Use `GRAPHQL_FIELD_*` constants for field names
|
||||
5. **Documentation**: Add GraphQL descriptions to all fields and types
|
||||
6. **Database**: Use `supabase()` client for database operations with `convertPostgrestToApiError`
|
||||
|
||||
## Testing
|
||||
|
||||
Tests are located in apps/docs/app/api/graphql/tests. Each top-level query
|
||||
should have its own test file, located at <queryName>.test.ts.
|
||||
|
||||
### Test data
|
||||
|
||||
Test data uses a local database, seeded with the file at supabase/seed.sql. Add
|
||||
any data required for running your new query.
|
||||
|
||||
### Integration tests
|
||||
|
||||
Integration tests import the POST function defined in
|
||||
apps/docs/api/graphql/route.ts, then make a request to this function.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { POST } from '../route'
|
||||
|
||||
it('test name', async () => {
|
||||
const query = `
|
||||
query {
|
||||
...
|
||||
}
|
||||
`
|
||||
const request = new Request('http://localhost/api/graphql', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query }),
|
||||
})
|
||||
|
||||
const result = await POST(request)
|
||||
})
|
||||
```
|
||||
|
||||
Include at least the following tests:
|
||||
|
||||
1. A test that requests all fields (including nested fields) on the new query
|
||||
object, and asserts that there are no errors, and the requested fields are
|
||||
properly returned.
|
||||
2. A test that triggers and error, and asserts that a GraphQL error is properly
|
||||
returned.
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
description: Docs Testing Procedure
|
||||
globs: apps/docs/**/*.test.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Docs Test Requirements
|
||||
|
||||
Rules for running tests in the docs application, ensuring proper Supabase setup and test execution.
|
||||
|
||||
<rule>
|
||||
name: docs_test_requirements
|
||||
description: Standards for running tests in the docs application with proper Supabase setup
|
||||
filters:
|
||||
# Match test files in the docs app
|
||||
- type: file_extension
|
||||
pattern: "\\.(test|spec)\\.(ts|tsx)$"
|
||||
- type: path
|
||||
pattern: "^apps/docs/.*"
|
||||
# Match test execution events
|
||||
- type: event
|
||||
pattern: "test_execution"
|
||||
|
||||
actions:
|
||||
- type: suggest
|
||||
message: |
|
||||
Before running tests in the docs app:
|
||||
|
||||
1. Check Supabase status:
|
||||
```bash
|
||||
pnpm supabase status
|
||||
```
|
||||
|
||||
2. If Supabase is not running:
|
||||
```bash
|
||||
pnpm supabase start
|
||||
```
|
||||
|
||||
3. Reset the database to ensure clean state:
|
||||
```bash
|
||||
pnpm supabase db reset --local
|
||||
```
|
||||
|
||||
4. Run the tests:
|
||||
```bash
|
||||
pnpm run -F docs test:local:unwatch
|
||||
```
|
||||
|
||||
Important notes:
|
||||
- Always ensure Supabase is running before tests
|
||||
- Database must be reset to ensure clean state
|
||||
- Use test:local:unwatch to run tests without watch mode
|
||||
- Tests are located in apps/docs/**/*.{test,spec}.{ts,tsx}
|
||||
|
||||
examples:
|
||||
- input: |
|
||||
# Bad: Running tests without proper setup
|
||||
pnpm run -F docs test
|
||||
pnpm run -F docs test:local
|
||||
|
||||
# Good: Proper test execution sequence
|
||||
pnpm supabase status
|
||||
pnpm supabase start # if not running
|
||||
pnpm supabase db reset --local
|
||||
pnpm run -F docs test:local:unwatch
|
||||
output: "Correctly executed docs tests with proper Supabase setup"
|
||||
|
||||
metadata:
|
||||
priority: high
|
||||
version: 1.0
|
||||
</rule>
|
||||
+15
-6
@@ -1,3 +1,10 @@
|
||||
---
|
||||
description: "Docs: embeddings generation pipeline (apps/docs/scripts/search)"
|
||||
globs:
|
||||
- apps/docs/scripts/search/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Documentation Embeddings Generation System
|
||||
|
||||
## Overview
|
||||
@@ -12,31 +19,34 @@ The documentation embeddings generation system processes various documentation s
|
||||
## Architecture
|
||||
|
||||
### Main Entry Point
|
||||
- `generate-embeddings.ts` - Main script that orchestrates the entire process
|
||||
|
||||
- `apps/docs/scripts/search/generate-embeddings.ts` - Main script that orchestrates the entire process
|
||||
- Supports `--refresh` flag to force regeneration of all content
|
||||
|
||||
### Content Sources (`sources/` directory)
|
||||
|
||||
#### Base Classes
|
||||
|
||||
- `BaseLoader` - Abstract class for loading content from different sources
|
||||
- `BaseSource` - Abstract class for processing and formatting content
|
||||
|
||||
#### Source Types
|
||||
1. **Markdown Sources** (`markdown.ts`)
|
||||
|
||||
1. **Markdown Sources** (`apps/docs/scripts/search/sources/markdown.ts`)
|
||||
- Processes `.mdx` files from guides and documentation
|
||||
- Extracts frontmatter metadata and content sections
|
||||
|
||||
2. **Reference Documentation** (`reference-doc.ts`)
|
||||
2. **Reference Documentation** (`apps/docs/scripts/search/sources/reference-doc.ts`)
|
||||
- **OpenAPI References** - Management API documentation from OpenAPI specs
|
||||
- **Client Library References** - JavaScript, Dart, Python, C#, Swift, Kotlin SDKs
|
||||
- **CLI References** - Command-line interface documentation
|
||||
- Processes YAML/JSON specs and matches with common sections
|
||||
|
||||
3. **GitHub Discussions** (`github-discussion.ts`)
|
||||
3. **GitHub Discussions** (`apps/docs/scripts/search/sources/github-discussion.ts`)
|
||||
- Fetches troubleshooting discussions from GitHub using GraphQL API
|
||||
- Uses GitHub App authentication for access
|
||||
|
||||
4. **Partner Integrations** (`partner-integrations.ts`)
|
||||
4. **Partner Integrations** (`apps/docs/scripts/search/sources/partner-integrations.ts`)
|
||||
- Fetches approved partner integration documentation from Supabase database
|
||||
- Technology integrations only (excludes agencies)
|
||||
|
||||
@@ -56,4 +66,3 @@ The documentation embeddings generation system processes various documentation s
|
||||
|
||||
- **`page`** table: Stores page metadata, content, checksum, version
|
||||
- **`page_section`** table: Stores individual sections with embeddings, token counts
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
description: "Docs: GraphQL architecture for apps/docs/resources"
|
||||
globs:
|
||||
- apps/docs/resources/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Docs GraphQL Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The `apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure.
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
Each GraphQL query follows this structure:
|
||||
|
||||
```
|
||||
resources/
|
||||
├── queryObject/
|
||||
│ ├── queryObjectModel.ts # Data models and business logic
|
||||
│ ├── queryObjectSchema.ts # GraphQL type definitions
|
||||
│ ├── queryObjectResolver.ts # Query resolver and arguments
|
||||
│ ├── queryObjectTypes.ts # TypeScript interfaces (optional)
|
||||
│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional)
|
||||
├── utils/
|
||||
│ ├── connections.ts # GraphQL connection/pagination utilities
|
||||
│ └── fields.ts # GraphQL field selection utilities
|
||||
├── rootSchema.ts # Main GraphQL schema with all queries
|
||||
└── rootSync.ts # Root sync script for syncing to database
|
||||
```
|
||||
|
||||
## Example queries
|
||||
|
||||
1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content
|
||||
2. **error** (`error/`) - Error code lookup for Supabase services
|
||||
3. **schema** - GraphQL schema introspection
|
||||
|
||||
## Key Files
|
||||
|
||||
### `rootSchema.ts`
|
||||
|
||||
- Main GraphQL schema definition
|
||||
- Imports all resolvers and combines them into the root query
|
||||
- Defines the `RootQueryType` with all top-level fields
|
||||
|
||||
### `utils/connections.ts`
|
||||
|
||||
- Provides `createCollectionType()` for paginated collections
|
||||
- `GraphQLCollectionBuilder` for building collection responses
|
||||
- Standard pagination arguments and edge/node patterns
|
||||
|
||||
### `utils/fields.ts`
|
||||
|
||||
- `graphQLFields()` utility to analyze requested fields in resolvers
|
||||
- Used for optimizing data fetching based on what fields are actually requested
|
||||
|
||||
## Creating a New Top-Level Query
|
||||
|
||||
To add a new GraphQL query, follow these steps:
|
||||
|
||||
### 1. Create Query Folder Structure
|
||||
|
||||
```bash
|
||||
mkdir resources/newQuery
|
||||
touch resources/newQuery/newQueryModel.ts
|
||||
touch resources/newQuery/newQuerySchema.ts
|
||||
touch resources/newQuery/newQueryResolver.ts
|
||||
```
|
||||
|
||||
### 2. Define GraphQL Schema (`newQuerySchema.ts`)
|
||||
|
||||
```typescript
|
||||
import { GraphQLObjectType, GraphQLString } from 'graphql'
|
||||
|
||||
export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const
|
||||
|
||||
export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({
|
||||
name: 'NewQuery',
|
||||
description: 'Description of what this query returns',
|
||||
fields: {
|
||||
id: {
|
||||
type: GraphQLString,
|
||||
description: 'Unique identifier',
|
||||
},
|
||||
// Add other fields...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Create Data Model (`newQueryModel.ts`)
|
||||
|
||||
> [!NOTE]
|
||||
> The data model should be agnostic to GraphQL. It may import argument types
|
||||
> from `~/__generated__/graphql`, but otherwise all functions and classes
|
||||
> should be unaware of whether they are called for GraphQL resolution.
|
||||
|
||||
> [!TIP]
|
||||
> The types in `~/__generated__/graphql` for a new endpoint will not exist
|
||||
> until the code generation is run in the next step.
|
||||
|
||||
```typescript
|
||||
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
|
||||
import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils'
|
||||
import { Result } from '~/features/helpers.fn'
|
||||
import { supabase } from '~/lib/supabase'
|
||||
|
||||
export class NewQueryModel {
|
||||
constructor(
|
||||
public readonly data: {
|
||||
id: string
|
||||
// other properties...
|
||||
}
|
||||
) {}
|
||||
|
||||
static async loadData(
|
||||
args: RootQueryTypeNewQueryArgs,
|
||||
requestedFields: Array<string>
|
||||
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
// Implement data fetching logic
|
||||
const result = new Result(
|
||||
await supabase()
|
||||
.from('your_table')
|
||||
.select('*')
|
||||
// Add filters based on args
|
||||
)
|
||||
.map((data) => data.map((item) => new NewQueryModel(item)))
|
||||
.mapError(convertPostgrestToApiError)
|
||||
return result
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
description: "Docs: how to run tests locally (Supabase setup + correct commands)"
|
||||
globs:
|
||||
- apps/docs/**/*.{test,spec}.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Docs test requirements
|
||||
|
||||
Before running tests for `apps/docs`, ensure local Supabase is available and the DB is in a known state.
|
||||
|
||||
## Recommended sequence
|
||||
|
||||
```bash
|
||||
pnpm supabase status
|
||||
pnpm supabase start # if not running
|
||||
pnpm supabase db reset --local
|
||||
pnpm run -F docs test:local:unwatch
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Always reset the local DB before running docs tests to avoid state leakage.
|
||||
- Prefer `test:local:unwatch` for non-watch CI-like runs.
|
||||
|
||||
@@ -1,409 +0,0 @@
|
||||
---
|
||||
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 (
|
||||
<ScaffoldContainer>
|
||||
<MyPageComponent />
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
MyPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<AuthLayout>{page}</AuthLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default MyPage
|
||||
|
||||
export const MyPageComponent = () => (
|
||||
<ScaffoldSection isFullWidth>
|
||||
<div>
|
||||
<ScaffoldSectionTitle>My page section</ScaffoldSectionTitle>
|
||||
<ScaffoldSectionDescription>A brief description of the purpose of the page</ScaffoldSectionDescription>
|
||||
</div>
|
||||
// Content goes here
|
||||
</ScaffoldSection>
|
||||
)
|
||||
```
|
||||
|
||||
## 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<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { name: '', enableFeature: false },
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onBlur',
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// handle mutation with onSuccess/onError toast
|
||||
}
|
||||
|
||||
return (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Name"
|
||||
description="A descriptive name for this resource"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ {...field} placeholder="Enter name" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="enableFeature"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Enable Feature"
|
||||
description="Toggle this feature on or off"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{form.formState.isDirty && (
|
||||
<Button type="default" onClick={() => form.reset()}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button type="primary" htmlType="submit" disabled={!form.formState.isDirty}>
|
||||
Submit
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { name: '' },
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// handle mutation
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetContent size="lg" className="flex flex-col gap-0">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Create Resource</SheetTitle>
|
||||
</SheetHeader>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={formId}
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="overflow-auto flex-grow px-0"
|
||||
>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="horizontal" label="Name" description="A descriptive name">
|
||||
<FormControl_Shadcn_ className="col-span-6 min-w-100">
|
||||
<Input_Shadcn_ {...field} placeholder="Enter name" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
<SheetFooter>
|
||||
<Button type="default" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" form={formId} htmlType="submit">
|
||||
Create
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 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: `<PrePostTab postTab="MB"><Input_Shadcn_ /></PrePostTab>`
|
||||
- **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'
|
||||
;<Table>
|
||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Invoice</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">INV001</TableCell>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>Credit Card</TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
```
|
||||
|
||||
## 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
|
||||
<Admonition
|
||||
type="note"
|
||||
title="No authentication logs available for this user"
|
||||
description="Auth events such as logging in will be shown here"
|
||||
/>
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
description: 'Studio: index rule for architecture, style, and UI composition patterns'
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio
|
||||
|
||||
Use the nested rules in this folder for focused guidance while working in `apps/studio/`.
|
||||
|
||||
## Architecture and style
|
||||
|
||||
- `studio/project-structure`
|
||||
- `studio/component-system`
|
||||
- `studio/styling`
|
||||
- `studio/best-practices`
|
||||
|
||||
## UI composition (Design System patterns)
|
||||
|
||||
- `studio/layout`
|
||||
- `studio/forms`
|
||||
- `studio/tables`
|
||||
- `studio/charts`
|
||||
- `studio/empty-states`
|
||||
- `studio/navigation`
|
||||
|
||||
## Common UI building blocks
|
||||
|
||||
- `studio/sheets`
|
||||
- `studio/cards`
|
||||
- `studio/alerts`
|
||||
- `studio/react-query`
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
description: "Studio: alert/admonition usage and placement"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio alerts
|
||||
|
||||
- Use `Admonition` to call out important actions, restrictions, or critical context.
|
||||
- Place at the top of a page’s content (below the page title) or at the top of the relevant section (below the section title).
|
||||
- Use sparingly.
|
||||
|
||||
+10
-11
@@ -1,9 +1,8 @@
|
||||
---
|
||||
description: React best practices and coding standards for Studio
|
||||
description: "Studio: React and TypeScript best practices for maintainable Studio code"
|
||||
globs:
|
||||
- apps/studio/**/*.tsx
|
||||
- apps/studio/**/*.ts
|
||||
alwaysApply: true
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio Best Practices
|
||||
@@ -307,15 +306,15 @@ const Component = ({ onClose, onSave }: Props) => {
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - creates new function every render
|
||||
<ExpensiveList
|
||||
items={items}
|
||||
onItemClick={(item) => handleItemClick(item)}
|
||||
/>
|
||||
<ExpensiveList items={items} onItemClick={(item) => handleItemClick(item)} />
|
||||
|
||||
// ✅ Good - stable reference with useCallback
|
||||
const handleItemClick = useCallback((item: Item) => {
|
||||
// handle click
|
||||
}, [dependencies])
|
||||
const handleItemClick = useCallback(
|
||||
(item: Item) => {
|
||||
// handle click
|
||||
},
|
||||
[dependencies]
|
||||
)
|
||||
|
||||
<ExpensiveList items={items} onItemClick={handleItemClick} />
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
description: "Studio: Card usage for grouping related content and actions"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio cards
|
||||
|
||||
- Use cards to group related pieces of information.
|
||||
- Use `CardContent` for sections and `CardFooter` for actions.
|
||||
- Only use `CardHeader`/`CardTitle` when the card content is not already described by surrounding content (page title, section title, etc).
|
||||
- Prefer headers/titles when multiple cards represent distinct groups (e.g. multiple settings groups).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: "Studio: composable chart patterns built on Recharts and our chart presentational components"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio charts
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/charts.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-demo.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-basic.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-states.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-metrics.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-actions.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-table.tsx`
|
||||
|
||||
## Best practices
|
||||
|
||||
- Prefer provided chart building blocks over passing raw Recharts components to `ChartContent`.
|
||||
- Use `useChart` context flags for consistent loading/disabled handling.
|
||||
- Keep chart composition straightforward; avoid over-abstraction.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
description: 'Studio: UI component system (packages/ui + shadcn primitives)'
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
- packages/ui/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio component system
|
||||
|
||||
Our primitive component system lives in `packages/ui` and is based on shadcn/ui patterns.
|
||||
|
||||
- Prefer using components exported from `ui` (e.g. `import { Button } from 'ui'`).
|
||||
- Prefer `_Shadcn_`-suffixed components for form components e.g. `Input_Shadcn_`.
|
||||
- Avoid introducing new primitives unless explicitly requested.
|
||||
- Browse available exports in `packages/ui/index.tsx` before composing new UI.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
description: 'Studio: empty state patterns (presentational vs informational vs zero-results vs missing route)'
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio empty states
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/empty-states.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/registry/default/example/empty-state-presentational-icon.tsx`
|
||||
- `apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx`
|
||||
- `apps/design-system/registry/default/example/empty-state-zero-items-table.tsx`
|
||||
- `apps/design-system/registry/default/example/data-grid-empty-state.tsx`
|
||||
- `apps/design-system/registry/default/example/empty-state-missing-route.tsx`
|
||||
|
||||
## Quick guidance
|
||||
|
||||
- Initial states: use presentational empty states when onboarding/value prop + a clear next action helps.
|
||||
- Data-heavy lists: prefer informational empty states that match the list/table layout.
|
||||
- Zero results: keep the UI consistent with the data state to avoid jarring transitions.
|
||||
- Missing routes: prefer a centered `Admonition` pattern.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: "Studio: form patterns (page layouts + side panels) and react-hook-form conventions"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio forms
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/forms.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/registry/default/example/form-patterns-pagelayout.tsx`
|
||||
- `apps/design-system/registry/default/example/form-patterns-sidepanel.tsx`
|
||||
|
||||
## Requirements
|
||||
|
||||
- Build forms with `react-hook-form` + `zod`.
|
||||
- Use `FormItemLayout` instead of manually composing `FormItem`/`FormLabel`/`FormMessage`/`FormDescription`.
|
||||
- Wrap inputs with `FormControl_Shadcn_`.
|
||||
- Use `_Shadcn_` imports from `ui` for form primitives where available.
|
||||
|
||||
## Layout selection
|
||||
|
||||
- Page layouts: `FormItemLayout layout="flex-row-reverse"` inside `Card` (`CardContent` per field; `CardFooter` for actions).
|
||||
- Side panels (wide): `FormItemLayout layout="horizontal"` inside `SheetSection`.
|
||||
- Side panels (narrow, `size="sm"` or below): `FormItemLayout layout="vertical"`.
|
||||
|
||||
## Actions and state
|
||||
|
||||
- Handle dirty state (`form.formState.isDirty`) to show Cancel and to disable Save.
|
||||
- Show loading on submit buttons via `loading`.
|
||||
- When submit button is outside the `<form>`, set a stable `formId` and use the button’s `form` prop.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
description: 'Studio: page layout patterns (PageContainer/PageHeader/PageSection) and sizing guidance. Use to learn how to create or update existing pages in Studio.'
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio layout
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/layout.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/registry/default/example/page-layout-settings.tsx`
|
||||
- `apps/design-system/registry/default/example/page-layout-list.tsx`
|
||||
- `apps/design-system/registry/default/example/page-layout-list-simple.tsx`
|
||||
- `apps/design-system/registry/default/example/page-layout-detail.tsx`
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Build pages using `PageContainer`, `PageHeader`, and `PageSection` for consistent spacing and max-widths.
|
||||
- Choose `size` based on content:
|
||||
- Settings/config: `size="default"`
|
||||
- List/table-heavy: `size="large"`
|
||||
- Full-screen experiences: `size="full"`
|
||||
- For list pages:
|
||||
- If filters/search exist, align table actions with filters (avoid `PageHeaderAside`/`PageSectionAside` for those actions).
|
||||
- If no filters/search, actions can go in `PageHeaderAside` or `PageSectionAside` depending on context.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
description: "Studio: navigation patterns (page-level NavMenu + URL-driven navigation)"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio navigation
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/navigation.mdx`
|
||||
|
||||
## NavMenu
|
||||
|
||||
- Use `NavMenu` for a horizontal list of related views within a consistent page layout.
|
||||
- Activating an item should trigger a URL change (no local-only tab state).
|
||||
- See: `apps/design-system/content/docs/components/nav-menu.mdx`
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
description: "Studio: project structure and where code lives"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio project structure
|
||||
|
||||
- Studio is a Next.js app using the pages router.
|
||||
- Pages live in `apps/studio/pages`.
|
||||
- Project pages: `apps/studio/pages/projects/[ref]`
|
||||
- Org pages: `apps/studio/pages/org/[slug]`
|
||||
- Studio components live in `apps/studio/components`.
|
||||
- Studio UI helpers: `apps/studio/components/ui`
|
||||
- Interface/page components: `apps/studio/components/interfaces` (e.g. `apps/studio/components/interfaces/Auth`)
|
||||
- Shared hooks: `apps/studio/hooks`
|
||||
- Shared helpers: `apps/studio/lib`
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
description: 'Studio: data fetching conventions for queries/mutations (React Query hooks)'
|
||||
globs:
|
||||
- apps/studio/data/**/*.{ts,tsx}
|
||||
- apps/studio/pages/**/*.{ts,tsx}
|
||||
- apps/studio/components/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio queries & mutations (React Query)
|
||||
|
||||
Follow the `apps/studio/data/` patterns used by edge functions:
|
||||
|
||||
- Query hook: `apps/studio/data/edge-functions/edge-functions-query.ts`
|
||||
- Mutation hook: `apps/studio/data/edge-functions/edge-functions-update-mutation.ts`
|
||||
- Keys: `apps/studio/data/edge-functions/keys.ts`
|
||||
- Page usage: `apps/studio/pages/project/[ref]/functions/index.tsx`
|
||||
|
||||
## Organize query keys
|
||||
|
||||
- Define a `keys.ts` per domain and export `*Keys` helpers (use array keys with `as const`).
|
||||
- Do not inline query keys in components.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export const edgeFunctionsKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const,
|
||||
detail: (projectRef: string | undefined, slug: string | undefined) =>
|
||||
['projects', projectRef, 'edge-function', slug, 'detail'] as const,
|
||||
}
|
||||
```
|
||||
|
||||
## Write a query hook
|
||||
|
||||
- Export `Variables`, `Data`, and `Error` types from the file.
|
||||
- Implement a `getX(variables, signal?)` function that:
|
||||
- throws if required variables are missing
|
||||
- passes the `signal` through to the fetcher for cancellation
|
||||
- calls `handleError(error)` and returns `data`
|
||||
- Wrap it in `useXQuery()` using `useQuery`, `UseCustomQueryOptions`, and a domain key helper.
|
||||
- Gate with `enabled` so the query doesn’t run until required variables exist (and platform-only queries should include `IS_PLATFORM`).
|
||||
|
||||
Template:
|
||||
|
||||
```ts
|
||||
export type XVariables = { projectRef?: string }
|
||||
export type XError = ResponseError
|
||||
|
||||
export async function getX({ projectRef }: XVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
const { data, error } = await get('/v1/projects/{ref}/x', {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
export type XData = Awaited<ReturnType<typeof getX>>
|
||||
|
||||
export const useXQuery = <TData = XData>(
|
||||
{ projectRef }: XVariables,
|
||||
{ enabled = true, ...options }: UseCustomQueryOptions<XData, XError, TData> = {}
|
||||
) =>
|
||||
useQuery<XData, XError, TData>({
|
||||
queryKey: xKeys.list(projectRef),
|
||||
queryFn: ({ signal }) => getX({ projectRef }, signal),
|
||||
enabled: IS_PLATFORM && enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
})
|
||||
```
|
||||
|
||||
## Write a mutation hook
|
||||
|
||||
- Export a `Variables` type that includes `projectRef`, identifiers (e.g. `slug`), and `payload`.
|
||||
- Implement an `updateX(vars)` function that validates required variables and uses `handleError`.
|
||||
- Prefer a `useXMutation()` wrapper that:
|
||||
- accepts `UseCustomMutationOptions` (omit `mutationFn`)
|
||||
- invalidates the relevant `list()` + `detail()` keys in `onSuccess` and `await`s them via `Promise.all`
|
||||
- defaults to a `toast.error(...)` when `onError` isn’t provided
|
||||
|
||||
Template:
|
||||
|
||||
```ts
|
||||
export const useXUpdateMutation = ({ onSuccess, onError, ...options } = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: updateX,
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: xKeys.detail(variables.projectRef, variables.slug),
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: xKeys.list(variables.projectRef) }),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(error, variables, context) {
|
||||
if (onError === undefined) toast.error(`Failed to update: ${error.message}`)
|
||||
else onError(error, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Component usage
|
||||
|
||||
- Prefer React Query’s v5 flags:
|
||||
- `isPending` for initial load (often aliased to `isLoading`)
|
||||
- `isFetching` for background refetches
|
||||
- Render states explicitly (pending → error → success), like `apps/studio/pages/project/[ref]/functions/index.tsx`.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
description: "Studio: side panels (Sheet) for context-preserving workflows"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio sheets
|
||||
|
||||
Use a `Sheet` when switching to a new page would be disruptive and the user should keep context (e.g. selecting an item from a list to edit details).
|
||||
|
||||
## Structure
|
||||
|
||||
- Prefer `SheetContent` with `size="lg"` for forms that need horizontal layout.
|
||||
- Use `SheetHeader`, `SheetTitle`, `SheetSection`, and `SheetFooter` for consistent structure.
|
||||
- Place submit/cancel actions in `SheetFooter`.
|
||||
|
||||
## Forms in sheets
|
||||
|
||||
- Prefer `FormItemLayout`:
|
||||
- `layout="horizontal"` for wider sheets
|
||||
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
|
||||
- See `@studio/forms` for the canonical patterns and demos.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
description: "Studio: styling rules (Tailwind + semantic tokens + typography/focus utilities)"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx,scss}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio styling
|
||||
|
||||
- Use Tailwind.
|
||||
- Do not hardcode Tailwind color tokens; use our semantic classes:
|
||||
- backgrounds: `bg`, `bg-muted`, `bg-warning`, `bg-destructive`
|
||||
- text: `text-foreground`, `text-foreground-light`, `text-foreground-lighter`, `text-warning`, `text-destructive`
|
||||
- Use existing typography utilities from `apps/studio/styles/typography.scss` instead of recreating styles.
|
||||
- Use existing focus utilities from `apps/studio/styles/focus.scss` for consistent keyboard focus styling.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
description: "Studio: table patterns (Table vs Data Table vs Data Grid) and placement of actions/filters"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio tables
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/tables.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/registry/default/example/table-demo.tsx`
|
||||
- `apps/design-system/registry/default/example/data-table-demo.tsx`
|
||||
- `apps/design-system/registry/default/example/data-grid-demo.tsx`
|
||||
|
||||
## Choose the right pattern
|
||||
|
||||
- `Table`: simple, static, semantic table display.
|
||||
- Data Table: TanStack-powered pattern for sorting/filtering/pagination; composed per use case.
|
||||
- Data Grid: only when you need virtualization, column resizing, or complex cell editing.
|
||||
|
||||
## Actions and filters placement
|
||||
|
||||
- Actions: above the table, aligned right.
|
||||
- Search/filters: above the table, aligned left.
|
||||
- If the table is the primary page content and has no filters/search, actions can live in the page’s primary/secondary actions area.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
description: E2E testing best practices for Playwright tests in Studio
|
||||
description: "Testing: Playwright E2E best practices for Studio tests (avoid flake + race conditions)"
|
||||
globs:
|
||||
- e2e/studio/**/*.ts
|
||||
- e2e/studio/**/*.spec.ts
|
||||
alwaysApply: true
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# E2E Testing Best Practices
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description: "Testing: unit/integration conventions for Studio test files"
|
||||
globs:
|
||||
- apps/studio/**/*.test.ts
|
||||
- apps/studio/**/*.test.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
Follow the guidelines in `apps/studio/tests/README.md` when writing tests for Studio.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs: apps/studio/**/*.test.ts,apps/studio/**/*.test.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
Make sure to follow the guidelines in this file to write tests: [README.md](mdc:apps/studio/tests/README.md)
|
||||
Reference in New Issue
Block a user