mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 02:45:07 +03:00
Merge branch 'master' into jordi/debug-73-migrate-reports-queries-to-otel-endpoint
This commit is contained in:
1894 files changed
+51500
-19514
No files matched your search
@@ -1,15 +1,21 @@
|
||||
---
|
||||
name: vitest
|
||||
description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.
|
||||
description: >-
|
||||
Vitest API and config reference (Jest-compatible) — mocking with vi.*, spies,
|
||||
fake timers, coverage configuration, fixtures, snapshots, and test filtering.
|
||||
Use for Vitest API and configuration questions anywhere in the monorepo; for
|
||||
Studio-specific test strategy and component-test setup, start with
|
||||
studio-testing and studio-mock-api-tests.
|
||||
metadata:
|
||||
author: Anthony Fu
|
||||
version: "2026.1.28"
|
||||
version: '2026.1.28'
|
||||
source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills
|
||||
---
|
||||
|
||||
Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates
|
||||
- Jest-compatible: Drop-in replacement for most Jest test suites
|
||||
- Smart watch mode: Only reruns affected tests based on module graph
|
||||
@@ -22,31 +28,31 @@ Vitest is a next-generation testing framework powered by Vite. It provides a Jes
|
||||
|
||||
## Core
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) |
|
||||
| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) |
|
||||
| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) |
|
||||
| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) |
|
||||
| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) |
|
||||
| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) |
|
||||
| Topic | Description | Reference |
|
||||
| ------------- | --------------------------------------------------------------- | -------------------------------------------- |
|
||||
| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) |
|
||||
| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) |
|
||||
| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) |
|
||||
| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) |
|
||||
| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) |
|
||||
| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) |
|
||||
|
||||
## Features
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) |
|
||||
| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) |
|
||||
| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) |
|
||||
| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) |
|
||||
| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) |
|
||||
| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) |
|
||||
| Topic | Description | Reference |
|
||||
| ------------ | -------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) |
|
||||
| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) |
|
||||
| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) |
|
||||
| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) |
|
||||
| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) |
|
||||
| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) |
|
||||
|
||||
## Advanced
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) |
|
||||
| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) |
|
||||
| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) |
|
||||
| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) |
|
||||
| Topic | Description | Reference |
|
||||
| ------------ | ------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) |
|
||||
| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) |
|
||||
| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) |
|
||||
| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) |
|
||||
+49
-21
@@ -1,42 +1,70 @@
|
||||
# Supabase Monorepo
|
||||
|
||||
pnpm 10 + Turborepo monorepo. Requires Node >= 22.
|
||||
pnpm 11 + Turborepo monorepo. Requires Node >= 22.13.
|
||||
|
||||
## Structure
|
||||
|
||||
| Directory | Purpose |
|
||||
| ----------------- | ------------------------------------------------------------ |
|
||||
| `apps/studio` | Supabase Studio/Dashboard — Next.js (pages router), React 19 |
|
||||
| `apps/docs` | Documentation site |
|
||||
| `apps/www` | Marketing website |
|
||||
| `packages/ui` | Shared UI components (shadcn/ui based) |
|
||||
| `packages/common` | Shared utilities and telemetry constants |
|
||||
| `e2e/studio` | Playwright E2E tests for Studio |
|
||||
| Directory | Purpose |
|
||||
| ------------------------ | --------------------------------------------------------------------------- |
|
||||
| `apps/studio` | Supabase Studio/Dashboard — has its own `apps/studio/CLAUDE.md` (see below) |
|
||||
| `apps/docs` | Documentation site — Next.js app router, MDX (port 3001) |
|
||||
| `apps/www` | Marketing website — Next.js, app + pages (port 3000) |
|
||||
| `apps/design-system` | Component demos — source of truth for Studio UI patterns (port 3003) |
|
||||
| `apps/ui-library` | shadcn-style registry site for Supabase UI blocks (port 3004) |
|
||||
| `apps/lite-studio` | Lightweight Studio — different stack: React Router 7 + Vite + Tailwind v4 |
|
||||
| `packages/ui` | Shared UI components (shadcn/ui based) — `import { Button } from 'ui'` |
|
||||
| `packages/ui-patterns` | Composite components — subpath imports, e.g. `ui-patterns/AssistantChat` |
|
||||
| `packages/common` | Shared utils, telemetry constants, feature flags |
|
||||
| `packages/api-types` | Generated platform Management API types |
|
||||
| `packages/pg-meta` | SQL builders for Postgres introspection (`SafeSqlFragment`) |
|
||||
| `packages/shared-data` | Static data: pricing, plans, regions, error codes |
|
||||
| `e2e/studio`, `e2e/docs` | Playwright E2E tests |
|
||||
| `supabase/` | Local Supabase project: edge functions, migrations, config.toml |
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
pnpm install # install dependencies
|
||||
pnpm dev:studio # run Studio dev server
|
||||
pnpm test:studio # run Studio unit tests (vitest)
|
||||
pnpm --prefix e2e/studio run e2e # run Studio E2E tests (playwright)
|
||||
pnpm build --filter=studio # build Studio
|
||||
pnpm lint --filter=studio # lint Studio
|
||||
pnpm typecheck # typecheck all packages
|
||||
pnpm dev:studio # run Studio dev server → http://localhost:8082
|
||||
pnpm dev:docs # run docs dev server
|
||||
pnpm dev:www # run www dev server
|
||||
pnpm test:studio # Studio unit tests (vitest)
|
||||
pnpm e2e # Studio E2E tests (playwright)
|
||||
pnpm build --filter=studio # build Studio
|
||||
pnpm lint --filter=studio # lint Studio
|
||||
pnpm typecheck # typecheck all packages
|
||||
pnpm format # Prettier write (check: pnpm test:prettier)
|
||||
pnpm generate:types # local DB types → supabase/functions/common/database-types.ts
|
||||
pnpm api:codegen # platform Management API types → packages/api-types
|
||||
```
|
||||
|
||||
## CI
|
||||
|
||||
Every PR must pass typecheck + lint (one workflow), Prettier, and a typos check. Other checks are path-filtered: Studio unit tests/build and the lint ratchet (ESLint warning count must not increase) run on `apps/studio/**` changes; app-specific test suites run on their own paths.
|
||||
|
||||
Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.gen.ts`, `**/__generated__/**`, `apps/docs/features/docs/generated/**`, `apps/www/.generated/**`, `supabase/functions/common/database-types.ts`.
|
||||
|
||||
## Conventions
|
||||
|
||||
**UI** — import from `'ui'`, use `_Shadcn_` suffixed variants for form primitives. Check `packages/ui/index.tsx` before creating new primitives.
|
||||
**UI** — import from `'ui'`; primitives are shadcn/ui-based and exported unsuffixed (`Input`, `Select`, `Form`, …). Use `Button` — the in-house component and the standard everywhere (a raw shadcn `Button_Shadcn_` also exists but is rarely the right choice). Check `packages/ui/index.tsx` before creating new primitives. Higher-level patterns live in `packages/ui-patterns`.
|
||||
|
||||
**Styling** — Tailwind only, semantic tokens (`bg-muted`, `text-foreground-light`), no hardcoded colors.
|
||||
|
||||
**Exports** — named exports only; default exports are allowed only where a framework requires them (`pages/**`, `app/**`, config files — the eslint preset has the exact carve-out list). Lint-enforced across all apps via `eslint-config-supabase` (severity `warn` everywhere; hard-enforced in Studio by the lint ratchet).
|
||||
|
||||
**Language** — Use U.S. English everywhere.
|
||||
|
||||
**Studio shortcuts** — when adding or changing repeated Studio UI actions, use the shared shortcut registry and primitives in `apps/studio/state/shortcuts/` and `apps/studio/components/ui/Shortcut*.tsx`. Prefer registered, discoverable shortcuts over one-off keyboard listeners; keep `G then ...` chords for navigation.
|
||||
## Skills
|
||||
|
||||
The skills in `.claude/skills/` are the source of truth for conventions — load the relevant ones before working, don't guess:
|
||||
|
||||
- `copywriting` — any user-facing text, anywhere in the monorepo
|
||||
- `docs-content` — anything under `apps/docs`
|
||||
- `telemetry-standards` — PostHog events, `packages/common/telemetry-constants.ts`
|
||||
- `dev-toolbar-review` — `packages/dev-tools`, `packages/common/posthog-client.ts`, `packages/common/feature-flags.tsx`
|
||||
- `safe-sql-execution` — any code that builds or executes SQL against user databases
|
||||
- `react-hook-form` — writing or modifying any form code, anywhere in the monorepo
|
||||
- `vitest` / `vercel-composition-patterns` — generic unit-testing and React composition references
|
||||
|
||||
## Studio
|
||||
|
||||
Pages router. Co-locate sub-components with parent. Avoid barrel re-export files.
|
||||
|
||||
See studio-\* skills for detailed studio conventions.
|
||||
Before working on anything in `apps/studio`, read `apps/studio/CLAUDE.md` if it isn't already in context — it maps Studio tasks to required skills and covers the TanStack Start migration rules.
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Edit(packages/api-types/types/**)",
|
||||
"Edit(**/routeTree.gen.ts)",
|
||||
"Edit(**/__generated__/**)",
|
||||
"Edit(apps/docs/features/docs/generated/**)",
|
||||
"Edit(apps/www/.generated/**)",
|
||||
"Edit(supabase/functions/common/database-types.ts)"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: copywriting
|
||||
description: Write or audit UI copy (buttons, labels, empty states, error messages, tooltips, form text) anywhere in the monorepo. Always check this before shipping or reviewing user-facing text.
|
||||
description: Write or audit UI copy (buttons, labels, empty states, error messages, tooltips, form text) anywhere in the monorepo. Load it before shipping or reviewing any user-facing text — including when copy is incidental to the task, like a new feature that adds buttons, toasts, dialogs, or validation messages.
|
||||
---
|
||||
|
||||
# Copywriting
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: dev-toolbar-review
|
||||
description: Use when reviewing PRs that touch packages/dev-tools/, packages/common/posthog-client.ts,
|
||||
description: Safety rules for the dev toolbar, PostHog client, and feature flags. Use
|
||||
when writing or reviewing any change to packages/dev-tools/, packages/common/posthog-client.ts,
|
||||
or packages/common/feature-flags.tsx. Covers environment guards, flag override cookies,
|
||||
telemetry event subscription, and SSE stream safety.
|
||||
---
|
||||
@@ -30,10 +31,12 @@ so PRs touching only those files won't auto-request review. Watch for these in t
|
||||
**Files:** `packages/dev-tools/index.ts`, `DevToolbar.tsx`, `DevToolbarTrigger.tsx`, `DevToolbarContext.tsx`
|
||||
|
||||
The toolbar uses two layers of protection:
|
||||
|
||||
- **Build-time tree-shaking** in `index.ts`: `process.env.NODE_ENV !== 'development'` ternaries that replace components with noops/stubs so the implementation is eliminated from production bundles.
|
||||
- **Runtime guards** in components: `IS_LOCAL_DEV` checks — `DevToolbar` and `DevToolbarTrigger` return `null` to hide themselves, while `DevToolbarProvider` passes children through (`<>{children}</>`) to preserve the component tree.
|
||||
|
||||
**Check for:**
|
||||
|
||||
- Guards being removed or broadened. The toolbar is expanding to staging and preview deploys but must remain invisible in production.
|
||||
- Tree-shaking ternaries in `index.ts` staying intact — these are the primary production safety mechanism.
|
||||
- New components or exports that bypass the existing guard pattern.
|
||||
@@ -43,14 +46,17 @@ The toolbar uses two layers of protection:
|
||||
**Files:** `packages/dev-tools/DevToolbar.tsx`, `packages/common/posthog-client.ts`, `packages/common/feature-flags.tsx`
|
||||
|
||||
The toolbar writes two cookies that override feature flags locally:
|
||||
|
||||
- `x-ph-flag-overrides` — PostHog flag overrides
|
||||
- `x-cc-flag-overrides` — ConfigCat flag overrides
|
||||
|
||||
These are read by:
|
||||
|
||||
- `posthog-client.ts:getFeatureFlag()` — checks the PostHog override cookie before querying the SDK
|
||||
- `feature-flags.tsx` — merges both override cookies into the flag store during initialization
|
||||
|
||||
**Check for:**
|
||||
|
||||
- Cookie name changes (must stay in sync across writer and all readers)
|
||||
- Changes to the merge/precedence logic in `feature-flags.tsx` (currently: `vercel-flag-overrides` first, then `x-cc-flag-overrides` takes precedence in local dev)
|
||||
- Override cookies being read outside the `IS_LOCAL_DEV` / `isLocalDev` guard — overrides must never affect production flag evaluation
|
||||
@@ -66,6 +72,7 @@ and `identify`. Note: `captureExperimentExposure` calls `posthog.capture()` dire
|
||||
without emitting to dev listeners — experiment exposure events are invisible in the toolbar.
|
||||
|
||||
**Check for:**
|
||||
|
||||
- Changes to `emitToDevListeners` or `subscribeToEvents` that could introduce side effects on the actual capture path (e.g., throwing errors, blocking, mutating event data)
|
||||
- The listener set (`devListeners`) being iterated synchronously in a way that could delay event dispatch
|
||||
- New PostHog client methods that capture events but don't call `emitToDevListeners` (gap in toolbar visibility)
|
||||
@@ -78,6 +85,7 @@ The toolbar connects to `${apiUrl}/telemetry/stream` via Server-Sent Events to d
|
||||
server-side telemetry. Uses exponential backoff on connection errors.
|
||||
|
||||
**Check for:**
|
||||
|
||||
- Changes to the SSE endpoint URL or `session_id` cookie handling
|
||||
- Reconnection logic changes that could cause excessive retries or connection leaks
|
||||
- Note: the stream endpoint lives in the platform repo — cross-repo changes need coordinated review
|
||||
@@ -85,16 +93,19 @@ server-side telemetry. Uses exponential backoff on connection errors.
|
||||
### 5. App-Level Mounting
|
||||
|
||||
**Provider + toolbar panel** (`DevToolbarProvider`, `DevToolbar`):
|
||||
|
||||
- `apps/studio/pages/_app.tsx`
|
||||
- `apps/www/pages/_app.tsx`, `apps/www/app/providers.tsx`
|
||||
- `apps/docs/features/app.providers.tsx`
|
||||
|
||||
**Trigger button** (`DevToolbarTrigger`) — rendered separately in nav/header components:
|
||||
|
||||
- `apps/studio/components/layouts/Navigation/LayoutHeader/LayoutHeader.tsx`
|
||||
- `apps/www/components/Nav/index.tsx`
|
||||
- `apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx`
|
||||
|
||||
**Check for:**
|
||||
|
||||
- Provider being added or removed from an app
|
||||
- `apiUrl` prop changes (must point to the correct platform API)
|
||||
- Rendering order changes that could affect the toolbar's access to PostHog context
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: docs-content
|
||||
description: Write, edit, organize, and review Supabase content anywhere in apps/docs — guides, explainers, tutorials, troubleshooting entries, reference docs, and partials. Use for MDX/TOML authoring, frontmatter, navigation, terminology, links, code samples, content listings, and docs validation.
|
||||
---
|
||||
|
||||
# Supabase docs authoring
|
||||
|
||||
## Sources of truth
|
||||
|
||||
Before changing docs content:
|
||||
|
||||
1. Read `apps/docs/CONTRIBUTING.md` for content types, structure, components, and
|
||||
style.
|
||||
2. Read `apps/docs/WORD_LIST.md` for preferred terminology, spelling, and
|
||||
capitalization.
|
||||
3. Inspect nearby content of the same type and the relevant navigation section
|
||||
before deciding on file placement or structure. Guides, explainers, and
|
||||
tutorials live under `apps/docs/content/guides`. Troubleshooting entries live
|
||||
under `apps/docs/content/troubleshooting` and use TOML frontmatter — follow
|
||||
`_template.mdx` in that directory rather than a guide's YAML frontmatter.
|
||||
Reference docs are generated from `apps/docs/spec` and library source, so
|
||||
look for the spec file or repo definition instead of editing rendered output
|
||||
directly.
|
||||
|
||||
When guidance conflicts, follow `apps/docs/CONTRIBUTING.md`. Match literal code,
|
||||
API names, UI labels, and third-party product names even when they differ from the
|
||||
word list.
|
||||
|
||||
## Writing workflow
|
||||
|
||||
1. Identify the document type: explainer, tutorial, guide, or reference, per
|
||||
`apps/docs/CONTRIBUTING.md`. A guide is a concise procedure for a targeted
|
||||
task; a tutorial covers a larger goal and includes more explanatory context;
|
||||
an explainer is conceptual and prose-based; reference content is factual,
|
||||
like a dictionary entry. Troubleshooting entries follow their own TOML
|
||||
structure rather than these four types.
|
||||
2. Define the reader's goal and prerequisites before drafting.
|
||||
3. Classify substantial sections as contextual, procedural, or reference content.
|
||||
In a mixed page, group sections by information type so that context doesn't
|
||||
interrupt the procedural path.
|
||||
4. For a long or mixed page, add a short introduction that links to its major
|
||||
section groups and tells readers when to use each one. Skip this navigation
|
||||
when a short page is already easy to scan.
|
||||
5. Connect contextual sections to their corresponding procedures when useful.
|
||||
Add introductions to section groups, transitions between information types,
|
||||
and outcomes after procedures. Don't link every adjacent section.
|
||||
6. Use second person, present tense, short paragraphs, and ordered steps for
|
||||
sequential actions.
|
||||
7. Search `apps/docs/WORD_LIST.md` when introducing or reviewing technical terms,
|
||||
UI actions, abbreviations, and potentially ambiguous language.
|
||||
8. Keep code samples executable in their stated context and consistent with
|
||||
repository formatting. Clearly mark intentionally omitted code. Use lowercase
|
||||
SQL keywords.
|
||||
9. Reuse repeated content through `apps/docs/content/_partials` instead of copying
|
||||
it.
|
||||
10. Add new guide, explainer, and tutorial pages to
|
||||
`apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts`.
|
||||
File placement alone doesn't add a page to navigation. Troubleshooting
|
||||
entries are indexed automatically and don't need a navigation entry.
|
||||
11. Use `/docs/...` paths for pages in Supabase docs and site-root paths such as
|
||||
`/dashboard` for pages outside docs. Use descriptive link text and sparse
|
||||
admonitions with the appropriate severity.
|
||||
|
||||
## Validation
|
||||
|
||||
From `apps/docs`, run:
|
||||
|
||||
```bash
|
||||
pnpm lint:mdx
|
||||
pnpm build:guides-markdown
|
||||
```
|
||||
|
||||
`pnpm lint:mdx` covers all content under `apps/docs/content`, including
|
||||
troubleshooting entries. `pnpm build:guides-markdown` only applies to guides,
|
||||
explainers, and tutorials.
|
||||
|
||||
From the repository root, run `pnpm format` to apply Prettier to any changed
|
||||
MDX (and other) files. This enforces repo-wide formatting rules, including
|
||||
lowercase SQL keyword casing in code samples.
|
||||
|
||||
Run broader type checking or tests when the change affects MDX components,
|
||||
content listings, navigation code, or generated output.
|
||||
|
||||
For a mixed page, verify that context and procedures are grouped, introductory
|
||||
navigation links resolve to the intended sections, related context and procedures
|
||||
are cross-referenced where useful, and transitions make the reading path clear.
|
||||
|
||||
Treat lint replacements as suggestions when context matters. Rewrite the sentence
|
||||
instead of applying a replacement that changes its technical meaning.
|
||||
|
||||
Anchor IDs are generated from heading text at render time, and nothing in CI
|
||||
checks that `#anchor` links still resolve. Before renaming, removing, or
|
||||
substantially rewording a heading, run
|
||||
`grep -rn "#<old-anchor-slug>" apps/docs/content` to find in-page and
|
||||
cross-file links that target it, and update every match. If a heading needs a
|
||||
stable anchor independent of its wording, pin it with a custom anchor, for
|
||||
example `## Some heading [#some-heading]`.
|
||||
@@ -0,0 +1,284 @@
|
||||
---
|
||||
name: react-hook-form
|
||||
description: Correct React Hook Form usage anywhere in the monorepo — data flow, subscriptions,
|
||||
reset, dirty state, number inputs, and controlled-input rules. Load this BEFORE
|
||||
writing or modifying ANY form code, adding a field to an existing form, touching
|
||||
watch/useWatch/formState/getValues/setValue/reset, wiring a form into a dialog or
|
||||
sheet, or building a submit/cancel footer — even when the change looks trivial.
|
||||
The codebase contains widespread RHF anti-patterns; without this skill you will
|
||||
copy them. For form layout and which components to use, also load
|
||||
studio-ui-patterns.
|
||||
---
|
||||
|
||||
# React Hook Form
|
||||
|
||||
How to write forms that stay correct as they grow. The existing codebase is **not**
|
||||
a safe reference: `form.watch()` off prop-drilled form objects, subscription-only
|
||||
watches, unguarded `valueAsNumber`, and `?? undefined` controlled values are all
|
||||
common in older code and all wrong. Follow this skill, not the neighboring file.
|
||||
|
||||
**Policy — fix what you touch.** New code must follow these rules. When you modify
|
||||
existing form code, upgrade the specific fields/hooks/components you're editing to
|
||||
match (e.g. a component you touch that calls `form.watch` gets converted to
|
||||
`useWatch`). Leave untouched code alone, but tell the user about anti-patterns you
|
||||
noticed and didn't fix. Never add new violations: `react-hook-form/no-use-watch`
|
||||
is ratcheted in Studio CI — any increase in the warning count fails the build.
|
||||
|
||||
## Mental model: subscriptions decide who re-renders
|
||||
|
||||
RHF is uncontrolled at heart. Values live in refs; nothing re-renders unless a
|
||||
subscription says so. Every read API is a subscription decision:
|
||||
|
||||
| API | Subscribes | Re-renders | Use for |
|
||||
| ----------------------------- | ---------- | -------------------------- | ---------------------------------------------- |
|
||||
| `useWatch({ control, name })` | yes | only the calling component | reactive value reads, anywhere |
|
||||
| `useFormState({ control })` | yes | only the calling component | `isDirty`/`errors`/etc. outside the form owner |
|
||||
| `formState` (destructured) | yes | the `useForm` owner | form state **in the owner component only** |
|
||||
| `form.watch(name)` | yes | the **entire form tree** | avoid — lint-flagged, see below |
|
||||
| `getValues()` | no | never | event handlers and `onSubmit` only |
|
||||
| `subscribe()` | callback | none | side effects outside render |
|
||||
|
||||
Two facts explain most of the bugs we've shipped:
|
||||
|
||||
1. **`form.watch()` and `form.formState` hoist their subscription to the `useForm`
|
||||
owner**, no matter which component calls them. A child that reads
|
||||
`form.watch('x')` off a prop works today only because the whole tree re-renders
|
||||
on every change — it silently goes stale the moment anyone adds `React.memo`
|
||||
between owner and child, and until then it re-renders every sibling on every
|
||||
keystroke. A no-arg `form.watch()` sets `watchAll` and re-renders the tree on
|
||||
every field change for the life of the form.
|
||||
2. **`formState` is a Proxy** — reading a property is what arms the subscription.
|
||||
Destructure it (`const { isDirty } = form.formState`), never pass the object
|
||||
around or read it conditionally (`a && formState.isValid` may never subscribe).
|
||||
Enforced by `react-hook-form/destructuring-formstate` (error).
|
||||
|
||||
### Reading values, by location
|
||||
|
||||
- **In the component that owns `useForm`:** destructure `formState`; prefer
|
||||
`useWatch` over `form.watch` even here (the `no-use-watch` rule flags every
|
||||
`watch`, and `useWatch` scopes the re-render if the JSX is later extracted).
|
||||
- **In any child component or custom hook:** accept `control` (not the whole
|
||||
`form`) and use `useWatch({ control, name })` / `useFormState({ control })`.
|
||||
Inside `<Form {...form}>` (which _is_ `FormProvider`), `useFormContext()` +
|
||||
`useWatch({ name })` also works and avoids prop-drilling entirely.
|
||||
- **Consume the return value.** Never call a watch for its subscription side
|
||||
effect and then read via `getValues()` — the watch list and the read list will
|
||||
drift apart (it has already happened; fields silently lost reactivity). The
|
||||
value you render must _be_ the value you subscribed to.
|
||||
- **One read path per value per render.** Mixing `useWatch('x')` on one line and
|
||||
`getValues('x')` a few lines later lets the two disagree within a single render.
|
||||
- **Name what you watch.** `useWatch({ control })` with no `name` re-renders on
|
||||
every keystroke in every field. Subscribe to the specific names you use.
|
||||
- `watch(callback)` is deprecated — use `subscribe()` for render-free listeners,
|
||||
and always return its cleanup from `useEffect`.
|
||||
|
||||
```tsx
|
||||
// ❌ common in the codebase — all three subscriptions hoist to the form owner
|
||||
function Fields({ form }: { form: UseFormReturn<FormValues> }) {
|
||||
form.watch(['storageType', 'totalSize']) // return value discarded
|
||||
const { errors } = form.formState // prop-form formState
|
||||
const size = form.getValues('totalSize') // non-reactive read in render
|
||||
...
|
||||
}
|
||||
|
||||
// ✅ child subscribes for itself and consumes what it watches
|
||||
function Fields({ control }: { control: Control<FormValues> }) {
|
||||
const [storageType, totalSize] = useWatch({ control, name: ['storageType', 'totalSize'] })
|
||||
const { errors } = useFormState({ control })
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## The canonical form
|
||||
|
||||
zod schema → `z.infer` type → `useForm` with `zodResolver` and **complete**
|
||||
`defaultValues` → `<Form {...form}>` → `FormField` render-prop per field →
|
||||
`FormItemLayout` → `FormControl` → primitive from `ui`. Layout/container choices
|
||||
(Card vs Sheet, `layout=` variants) are covered by the `studio-ui-patterns` skill
|
||||
and the demos in `apps/design-system/registry/default/example/`
|
||||
(`form-patterns-pagelayout.tsx`, `form-patterns-sidepanel.tsx`) — check them
|
||||
before inventing structure.
|
||||
|
||||
```tsx
|
||||
// Module level — static references, not recreated on every render
|
||||
const FORM_ID = 'pool-config-form'
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
maxConnections: z
|
||||
.union([z.literal(''), z.coerce.number().gte(1, 'Must be at least 1')])
|
||||
.refine((v) => v !== '', 'Max connections is required'),
|
||||
})
|
||||
type FormValues = z.infer<typeof FormSchema>
|
||||
|
||||
const defaultValues: FormValues = { name: '', maxConnections: '' }
|
||||
|
||||
// Inside the component
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
<Form {...form}>
|
||||
<form id={FORM_ID} onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="horizontal" label="Name">
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
```
|
||||
|
||||
Define the schema, `type`, static `defaultValues`, and the form's id at module
|
||||
level, outside the component. Rebuilding them per render is wasted work and
|
||||
unstable references — RHF reads `defaultValues` only on the first render, but
|
||||
anything else comparing against these objects sees a fresh identity each time.
|
||||
When they genuinely depend on runtime data, build the schema with `useMemo` and
|
||||
feed server-driven defaults through the `values` option (next section) instead
|
||||
of hoisting.
|
||||
|
||||
Submit buttons living outside the `<form>` (sheet/dialog footers) use the same
|
||||
module-level `FORM_ID` via `form={FORM_ID}` on the button. A module-level id is
|
||||
only safe for singleton forms — if the component can mount more than once at a
|
||||
time, duplicate ids make external buttons submit the first matching form, so
|
||||
mint a per-instance id with `useId()` and share it between the `<form>` and its
|
||||
buttons.
|
||||
|
||||
## defaultValues, server data, and reset
|
||||
|
||||
- **Provide a complete `defaultValues` object — every field, no `undefined`.**
|
||||
`isDirty`, `dirtyFields`, and Cancel-reset all compare against it; a missing or
|
||||
`undefined` default breaks all three, and `undefined` also makes React treat the
|
||||
input as uncontrolled (see below).
|
||||
- **Form populated from an API? Use the `values` option, not a hand-rolled
|
||||
effect.** `values` reacts to the query resolving and resets the form for you;
|
||||
computing `defaultValues` from a query that may not have loaded freezes whatever
|
||||
happened to be in cache at mount. Add
|
||||
`resetOptions: { keepDirtyValues: true }` when a background refetch must not
|
||||
clobber the user's in-progress edits. (Good examples:
|
||||
`components/interfaces/Settings/Database/ConnectionLogging.tsx`,
|
||||
`components/interfaces/Storage/EditBucketModal.tsx`.)
|
||||
- **After a successful mutation, re-baseline the form** in `onSuccess` so the
|
||||
saved state becomes the new baseline (`isDirty` returns to false, Cancel now
|
||||
reverts to the saved values). Prefer what the server actually persisted: if the
|
||||
form uses `values` and the mutation invalidates the query, the refetch handles
|
||||
this for you; if the mutation returns the updated resource, `reset(response)`.
|
||||
`reset(submittedValues)` is the fallback for APIs that store exactly what was
|
||||
sent — if the server normalizes or fills values, it baselines the form to data
|
||||
that was never saved. A bare `reset()` reverts to the _previous_ defaults —
|
||||
wrong after a save.
|
||||
- Cancel buttons call `form.reset()`. This only visually restores fields whose
|
||||
values round-trip through defined, controlled values — which is why the null
|
||||
rules below matter.
|
||||
|
||||
## Controlled inputs: never let `value` flip to `undefined`
|
||||
|
||||
React decides controlled vs uncontrolled per render from whether `value` is
|
||||
defined. A field whose value can be `undefined` (or becomes `undefined` on reset)
|
||||
flips modes: console warnings, and — worse — `reset()` stops clearing the visible
|
||||
text because React abandoned the DOM value. `value={field.value ?? undefined}` is
|
||||
a bug, not a fix.
|
||||
|
||||
- Text fields: default to `''`, never `null`/`undefined`.
|
||||
- **Normalize `null` from the API at the form boundary** (`growthPercent ?? ''`
|
||||
when building defaults) and convert back on submit (`'' → null`). Do not paper
|
||||
over a `null` default with a `placeholder` that looks like a value: the user
|
||||
sees "50", the form holds `null`, and every downstream comparison
|
||||
(`defaultValues.growthPercent !== watched` → `null !== 50`) reports a permanent
|
||||
phantom change while Cancel silently fails to reset the field.
|
||||
- Selects/radios: default to `''` or a real option value; checkboxes/switches to
|
||||
`false`.
|
||||
|
||||
## Number inputs
|
||||
|
||||
The blessed pattern keeps `''` as the "empty" sentinel so the input stays
|
||||
controlled, and lets zod coerce on validation (see `maxConnections` above):
|
||||
`z.union([z.literal(''), z.coerce.number()...]).refine((v) => v !== '', '…')`
|
||||
with a plain `<Input {...field} type="number" />`.
|
||||
|
||||
If you instead wire `onChange` through `e.target.valueAsNumber` (or
|
||||
`valueAsNumber: true`), an empty or partially-typed input produces `NaN`, which
|
||||
lands in form state and propagates into every calculation, price preview, and
|
||||
`value` attribute downstream. Guard it with the **same empty sentinel the
|
||||
field's schema declares** — with the `''`-union schema above:
|
||||
`field.onChange(Number.isNaN(e.target.valueAsNumber) ? '' : e.target.valueAsNumber)`.
|
||||
Never let `NaN` into form state.
|
||||
|
||||
A nullable API field (`null` = "unset", e.g. a platform default applies)
|
||||
doesn't change the in-form sentinel — keep `''` inside the form and convert at
|
||||
the boundaries:
|
||||
|
||||
```tsx
|
||||
// inbound: null → '' when building defaults/values
|
||||
values: { growthPercent: data.growth_percent ?? '' },
|
||||
// schema: '' stays the in-form sentinel, zod coerces real input
|
||||
growthPercent: z.union([z.literal(''), z.coerce.number().gte(10).lte(100)]),
|
||||
// outbound: '' → null in onSubmit
|
||||
mutate({ growth_percent: values.growthPercent === '' ? null : values.growthPercent })
|
||||
```
|
||||
|
||||
If `null` does end up in form state (some existing forms hold it), keep it out
|
||||
of both the input and the coercion: render via `value={field.value ?? ''}`, and
|
||||
don't pass the value through `z.coerce.number()` — `Number(null)` is `0`, so a
|
||||
nullable field fed into the coercing union silently validates empty as `0`.
|
||||
Either way it's one sentinel per field, used consistently across defaults,
|
||||
schema, `onChange`, rendering, and the submit mapping.
|
||||
|
||||
## Dirty state and change detection
|
||||
|
||||
- Gate Save on `isDirty`; show Cancel only when dirty. In the owner, destructure
|
||||
from `form.formState`; anywhere else, `useFormState({ control })`.
|
||||
- When the form lives in a Sheet or Dialog, also wire dirty dismissal:
|
||||
`useConfirmOnClose` + `DiscardChangesConfirmationDialog`. Route Cancel,
|
||||
Escape, and backdrop through the guard; call the raw `onClose` on successful
|
||||
submit so you do not prompt after save. Details:
|
||||
`apps/design-system/content/docs/ui-patterns/modality.mdx` (Dirty form
|
||||
dismissal) and the studio-ui-patterns skill Sheets section.
|
||||
- To show _which_ fields changed (review/summary dialogs), read `dirtyFields`
|
||||
from the same subscription instead of hand-comparing
|
||||
`defaultValues.x !== watchedX`. RHF already does that comparison correctly;
|
||||
hand-rolled versions break on the null-vs-placeholder mismatch and must be
|
||||
kept in sync with the watch list by hand.
|
||||
- `setValue` outside user input needs explicit flags:
|
||||
`setValue('x', v, { shouldDirty: true, shouldValidate: true })` — otherwise the
|
||||
change is invisible to `isDirty` and validation.
|
||||
|
||||
## Disabling and gating
|
||||
|
||||
If a field must not be edited (plan tier, permissions, cooldown), disable the
|
||||
field itself — a notice next to an editable input gates nothing. Wire the same
|
||||
condition into both the notice and the control. Permission checks come from
|
||||
`useAsyncCheckPermissions`; disabled buttons that need an explanation use
|
||||
`ButtonTooltip`.
|
||||
|
||||
Caution: `register`/`useController` `disabled: true` removes the field's value
|
||||
from submission data. For "visible but locked" fields whose value must survive
|
||||
submit, use the input's own `disabled`/`readOnly` prop (as `FormField` +
|
||||
primitive props do) rather than RHF-level disabling, or the form-level
|
||||
`disabled` option to freeze everything during async work.
|
||||
|
||||
## Submit and mutations
|
||||
|
||||
`onSubmit` receives validated, typed data — trust it; don't re-read via
|
||||
`getValues()`. Mutations follow Studio conventions: `onSuccess` → `toast.success`
|
||||
|
||||
- `reset(values)` (or query invalidation when using `values:`), `onError` →
|
||||
`toast.error`; pass the mutation's `isPending` to the button's `loading` prop.
|
||||
Default validation `mode: 'onSubmit'` is right for most forms — pick another mode
|
||||
deliberately, not by copying.
|
||||
|
||||
## Lint rules in force (Studio)
|
||||
|
||||
| Rule | Level | Meaning |
|
||||
| ------------------------------------------- | ---------------- | ------------------------------------------------ |
|
||||
| `react-hook-form/destructuring-formstate` | error | destructure `formState`, never hold the object |
|
||||
| `react-hook-form/no-access-control` | error | don't reach into `control` internals |
|
||||
| `react-hook-form/no-nested-object-setvalue` | error | `setValue('a.b', v)`, not `setValue('a', {b:v})` |
|
||||
| `react-hook-form/no-use-watch` | warn (ratcheted) | use `useWatch`, not `watch` |
|
||||
@@ -1,6 +1,20 @@
|
||||
---
|
||||
name: safe-sql-execution
|
||||
description: Safely execute SQL queries against a user database without risking SQL injection or other security vulnerabilities.
|
||||
description: >-
|
||||
Use whenever code will build, return, fetch, or execute SQL that runs against
|
||||
a user's real Postgres database — even when the request reads like an ordinary
|
||||
feature or bug fix and never says "security," "injection," or
|
||||
"SafeSqlFragment." This covers: writing or editing any pg-meta function, query
|
||||
builder, or endpoint that builds/returns SQL for database objects (tables,
|
||||
views, functions, DB triggers, indexes, RLS policies); interpolating a
|
||||
schema/table/column/search/route-param value into SQL text; storing, fetching,
|
||||
or re-running SQL that round-trips from the database (a policy's definition, a
|
||||
function/view definition, a snippet's saved content); and any
|
||||
"Run"/"Apply"/"Execute" action that sends SQL to a project's database (SQL
|
||||
editor run-selection, policy editor apply, snippet runner). Load this BEFORE
|
||||
writing such code, not only when reviewing a finished diff. Skip only for
|
||||
changes that never touch SQL text or execution — styling, unrelated data
|
||||
hooks, non-SQL form validation, or UI layout work.
|
||||
---
|
||||
|
||||
# Safe SQL execution
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
---
|
||||
name: studio-best-practices
|
||||
description: React and TypeScript best practices for Supabase Studio. Use when writing
|
||||
or reviewing Studio components — covers boolean naming, component structure, loading/error
|
||||
states, state management, custom hooks, event handlers, conditional rendering,
|
||||
performance, and TypeScript conventions.
|
||||
---
|
||||
|
||||
# Studio Best Practices
|
||||
|
||||
Applies to `apps/studio/**/*.{ts,tsx}`.
|
||||
|
||||
## Boolean Naming
|
||||
|
||||
Use descriptive prefixes — derive from existing state rather than storing separately:
|
||||
|
||||
- `is` — state/identity: `isLoading`, `isPaused`, `isNewRecord`
|
||||
- `has` — possession: `hasPermission`, `hasData`
|
||||
- `can` — capability: `canUpdateColumns`, `canDelete`
|
||||
- `should` — conditional behavior: `shouldFetch`, `shouldRender`
|
||||
|
||||
Extract complex conditions into named variables:
|
||||
|
||||
```tsx
|
||||
// ❌ inline multi-condition
|
||||
{
|
||||
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && <Button />
|
||||
}
|
||||
|
||||
// ✅ named variable
|
||||
const canShowAddButton =
|
||||
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading
|
||||
{
|
||||
canShowAddButton && <Button />
|
||||
}
|
||||
```
|
||||
|
||||
Derive booleans — don't store them:
|
||||
|
||||
```tsx
|
||||
// ❌ stored derived state
|
||||
const [isFormValid, setIsFormValid] = useState(false)
|
||||
useEffect(() => {
|
||||
setIsFormValid(name.length > 0 && email.includes('@'))
|
||||
}, [name, email])
|
||||
|
||||
// ✅ derived
|
||||
const isFormValid = name.length > 0 && email.includes('@')
|
||||
```
|
||||
|
||||
## Component Structure
|
||||
|
||||
See `vercel-composition-patterns` skill for compound component and composition patterns.
|
||||
|
||||
Keep components under 200–300 lines. Split when you see:
|
||||
|
||||
- Multiple distinct UI sections
|
||||
- Complex conditional rendering
|
||||
- Multiple unrelated `useState` calls
|
||||
- Hard to understand at a glance
|
||||
|
||||
Co-locate sub-components in the same directory as the parent. Avoid barrel re-export files.
|
||||
|
||||
Extract repeated JSX patterns into small components.
|
||||
|
||||
## Data Fetching
|
||||
|
||||
All data fetching uses TanStack Query (React Query). See `studio-queries` skill for query/mutation patterns and `studio-error-handling` skill for error display conventions.
|
||||
|
||||
### Loading / Error / Success Pattern
|
||||
|
||||
Top level:
|
||||
|
||||
```tsx
|
||||
const { data, error, isLoading, isError, isSuccess } = useQuery(...)
|
||||
|
||||
if (isLoading) return <GenericSkeletonLoader />
|
||||
if (isError) return <AlertError error={error} subject="Failed to load data" />
|
||||
if (isSuccess && data.length === 0) return <EmptyState />
|
||||
return <DataDisplay data={data} />
|
||||
```
|
||||
|
||||
Use early returns — avoid deeply nested conditionals.
|
||||
|
||||
Inline:
|
||||
|
||||
```tsx
|
||||
<div>
|
||||
{isLoading && <InlineLoader />}
|
||||
{isError && <InlineError error={error} />}
|
||||
{isSuccess && data.length === 0 && <EmptyState />}
|
||||
{isSuccess && data.length > 0 && <DataDisplay data={data} />}
|
||||
</div>
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
Keep state as local as possible; lift only when needed.
|
||||
|
||||
Group related form state with `react-hook-form` rather than multiple `useState` calls. See `studio-ui-patterns` skill for form layout and component conventions.
|
||||
|
||||
```tsx
|
||||
// ❌ multiple related useState
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
// ✅ grouped with react-hook-form
|
||||
const form = useForm<FormValues>({ defaultValues: { name: '', email: '' } })
|
||||
```
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
Extract complex or reusable logic into hooks. Return objects, not arrays:
|
||||
|
||||
```tsx
|
||||
// ❌ array return (hard to extend)
|
||||
return [value, toggle]
|
||||
|
||||
// ✅ object return
|
||||
return { value, toggle, setTrue, setFalse }
|
||||
```
|
||||
|
||||
## Event Handlers
|
||||
|
||||
- Prop callbacks: `on` prefix (`onClose`, `onSave`)
|
||||
- Internal handlers: `handle` prefix (`handleSubmit`, `handleCancel`)
|
||||
|
||||
Use `useCallback` for handlers passed to memoized children; avoid unnecessary inline arrow functions.
|
||||
|
||||
## Conditional Rendering
|
||||
|
||||
```tsx
|
||||
// Simple show/hide
|
||||
<>{isVisible && <Component />}</>
|
||||
|
||||
// Binary choice
|
||||
<>{isLoading ? <Spinner /> : <Content />}</>
|
||||
|
||||
// Multiple conditions — use early returns, not nested ternaries
|
||||
if (isLoading) return <Spinner />
|
||||
if (isError) return <Error />
|
||||
return <Content />
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
`useMemo` for genuinely expensive computations (measured, not assumed). Don't wrap everything — only optimize when you have a measured problem or are passing values to memoized children.
|
||||
|
||||
## TypeScript
|
||||
|
||||
Define prop interfaces explicitly. Use discriminated unions for complex state:
|
||||
|
||||
```tsx
|
||||
type AsyncState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: Error }
|
||||
```
|
||||
|
||||
Avoid `as any` / `as Type` casts. Validate at boundaries with zod:
|
||||
|
||||
```tsx
|
||||
// ❌ type cast
|
||||
const user = apiResponse as User
|
||||
|
||||
// ✅ zod parse
|
||||
const user = userSchema.parse(apiResponse)
|
||||
// or safe:
|
||||
const result = userSchema.safeParse(apiResponse)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Extract logic into `.utils.ts` pure functions and test exhaustively. See the `studio-testing` skill for the full testing strategy and decision tree.
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: studio-e2e-tests
|
||||
description: Write and run Playwright E2E tests for Supabase Studio. Use when asked
|
||||
to run e2e tests, write new E2E tests, or debug flaky tests. Covers running commands,
|
||||
avoiding race conditions, waiting strategies, selectors, helper functions, and CI
|
||||
vs local differences.
|
||||
description: Write and run Playwright E2E tests for Supabase Studio (e2e/studio).
|
||||
Use when asked to run e2e tests, write new E2E tests, or debug flaky or failing
|
||||
Playwright tests. Covers running commands, avoiding race conditions, waiting
|
||||
strategies, selectors, helper functions, and CI vs local differences.
|
||||
---
|
||||
|
||||
# E2E Studio Tests
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
name: studio-error-handling
|
||||
description: Error display and troubleshooting pattern for Supabase Studio. Use when
|
||||
rendering API errors in the UI, adding inline troubleshooting steps for a new
|
||||
error type, or wiring up the AI assistant debug button from an error state.
|
||||
showing a failed API request or query error in the UI (AlertError, toast, inline
|
||||
message), adding troubleshooting steps for a new error type, or wiring up the AI
|
||||
assistant debug button from an error state.
|
||||
---
|
||||
|
||||
# Studio Error Handling Pattern
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
name: studio-queries
|
||||
description: React Query conventions for data fetching in Supabase Studio. Use when
|
||||
writing or reviewing query hooks, mutation hooks, or query keys in apps/studio/data/.
|
||||
writing or reviewing query hooks, mutation hooks, or query keys in apps/studio/data/
|
||||
— including adding the first fetch or mutation for a new API endpoint or resource.
|
||||
Covers queryOptions pattern, keys.ts structure, mutation hook template, and imperative
|
||||
fetching.
|
||||
---
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: studio-testing
|
||||
description: Testing strategy for Supabase Studio. Use when writing tests, deciding what
|
||||
type of test to write, extracting logic from components into testable utility
|
||||
functions, or reviewing test coverage. Covers unit tests, component tests,
|
||||
and E2E test selection criteria.
|
||||
description: Testing strategy for Supabase Studio. Use when writing tests, deciding
|
||||
whether a change needs tests and which type, extracting logic from components into
|
||||
testable utility functions, or reviewing test coverage. Covers unit tests, component
|
||||
tests, and E2E test selection criteria.
|
||||
---
|
||||
|
||||
# Studio Testing Strategy
|
||||
@@ -162,14 +162,14 @@ try/finally for resource cleanup. For E2E execution details, see the
|
||||
|
||||
## Codebase References
|
||||
|
||||
| What | Where |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| What | Where |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Util test examples | `apps/studio/tests/components/Grid/Grid.utils.test.ts`, `apps/studio/tests/components/Billing/TaxID.utils.test.ts`, `apps/studio/tests/components/Editor/SpreadsheetImport.utils.test.ts` |
|
||||
| Component test examples | `apps/studio/tests/features/logs/LogsFilterPopover.test.tsx`, `apps/studio/tests/components/CopyButton.test.tsx` |
|
||||
| E2E test example | `e2e/studio/features/filter-bar.spec.ts` |
|
||||
| E2E helpers pattern | `e2e/studio/utils/filter-bar-helpers.ts` |
|
||||
| Custom render | `apps/studio/tests/lib/custom-render.tsx` |
|
||||
| MSW mock setup | `apps/studio/tests/lib/msw.ts` (`addAPIMock`) |
|
||||
| Test README | `apps/studio/tests/README.md` |
|
||||
| Vitest config | `apps/studio/vitest.config.ts` |
|
||||
| Related skills | `studio-e2e-tests` (running E2E), `vitest` (API reference), `vercel-composition-patterns` (component architecture) |
|
||||
| Component test examples | `apps/studio/tests/features/logs/LogsFilterPopover.test.tsx`, `apps/studio/tests/components/CopyButton.test.tsx` |
|
||||
| E2E test example | `e2e/studio/features/filter-bar.spec.ts` |
|
||||
| E2E helpers pattern | `e2e/studio/utils/filter-bar-helpers.ts` |
|
||||
| Custom render | `apps/studio/tests/lib/custom-render.tsx` |
|
||||
| MSW mock setup | `apps/studio/tests/lib/msw.ts` (`addAPIMock`) |
|
||||
| Test README | `apps/studio/tests/README.md` |
|
||||
| Vitest config | `apps/studio/vitest.config.ts` |
|
||||
| Related skills | `studio-e2e-tests` (running E2E), `vitest` (API reference), `vercel-composition-patterns` (component architecture) |
|
||||
@@ -34,7 +34,7 @@ Docs: `apps/design-system/content/docs/ui-patterns/forms.mdx`
|
||||
|
||||
- Use `react-hook-form` + `zod`
|
||||
- Use `FormItemLayout` instead of manually composing `FormItem`/`FormLabel`/`FormMessage`/`FormDescription`
|
||||
- Wrap inputs with `FormControl`; use `_Shadcn_` imports from `ui` for primitives
|
||||
- Wrap inputs with `FormControl`; import primitives from `ui`
|
||||
|
||||
Layout selection:
|
||||
|
||||
@@ -125,6 +125,10 @@ Forms in sheets:
|
||||
|
||||
- `layout="horizontal"` for wider sheets
|
||||
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
|
||||
- When the sheet contains a form, wire dirty dismissal with `useConfirmOnClose` +
|
||||
`DiscardChangesConfirmationDialog` (Cancel, Escape, and backdrop). Source of
|
||||
truth: `apps/design-system/content/docs/ui-patterns/modality.mdx` (Dirty form
|
||||
dismissal). Also see the react-hook-form skill for `isDirty` destructuring.
|
||||
|
||||
## Copy
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
name: telemetry-standards
|
||||
description: PostHog event tracking standards for Supabase Studio. Use when reviewing
|
||||
PRs for telemetry compliance or implementing new event tracking. Covers event naming,
|
||||
property conventions, approved patterns, and implementation guide.
|
||||
description: PostHog event tracking standards for Supabase Studio. Use when adding
|
||||
useTrack() calls, defining events in packages/common/telemetry-constants.ts,
|
||||
implementing tracking for a new feature, or reviewing PRs for telemetry compliance.
|
||||
Covers event naming, property conventions, approved patterns, and implementation
|
||||
guide.
|
||||
---
|
||||
|
||||
# Telemetry Standards for Supabase Studio
|
||||
@@ -19,16 +21,19 @@ opened, clicked, submitted, created, removed, updated, intended, evaluated, adde
|
||||
enabled, disabled, copied, exposed, failed, converted, closed, completed, applied, sent, moved
|
||||
|
||||
**Flag these:**
|
||||
|
||||
- Unapproved verbs (saved, viewed, seen, pressed, etc.)
|
||||
- Wrong order: `click_product_card` → should be `product_card_clicked`
|
||||
- Wrong casing: `productCardClicked` → should be `product_card_clicked`
|
||||
|
||||
**Good examples:**
|
||||
|
||||
- `product_card_clicked`
|
||||
- `backup_button_clicked`
|
||||
- `sql_query_submitted`
|
||||
|
||||
**Common mistakes with corrections:**
|
||||
|
||||
- `database_saved` → `save_button_clicked` or `database_updated` (unapproved verb)
|
||||
- `click_backup_button` → `backup_button_clicked` (wrong order)
|
||||
- `dashboardViewed` → don't track passive views on page load
|
||||
@@ -39,10 +44,12 @@ enabled, disabled, copied, exposed, failed, converted, closed, completed, applie
|
||||
**Casing:** camelCase preferred for new events. The codebase has existing snake_case properties (e.g., `schema_name`, `table_name`) — when adding properties to an existing event, match its established convention.
|
||||
|
||||
**Names must be self-explanatory:**
|
||||
|
||||
- `{ productType: 'database', planTier: 'pro' }`
|
||||
- `{ assistantType: 'sql', suggestionType: 'optimization' }`
|
||||
|
||||
**Flag these:**
|
||||
|
||||
- Generic names: `label`, `value`, `name`, `data`
|
||||
- PascalCase properties
|
||||
- Inconsistent names across similar events (e.g., `assistantType` in one event, `aiType` in a related event)
|
||||
@@ -117,6 +124,7 @@ When reviewing a PR, flag these as **required changes:**
|
||||
5. **Inaccurate docs** — `@page`/`@source` descriptions that don't match the actual implementation
|
||||
|
||||
When a PR adds user-facing interactions (buttons, forms, toggles, modals) **without** tracking, suggest:
|
||||
|
||||
- "This adds a user interaction that may benefit from tracking."
|
||||
- Propose the event name following `[object]_[verb]` convention
|
||||
- Propose the `useTrack()` call with suggested properties
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ knowledge_base:
|
||||
code_guidelines:
|
||||
filePatterns:
|
||||
# Studio code conventions — React/TS, UI patterns, composition, data fetching, errors
|
||||
- files: '.claude/skills/{studio-best-practices,studio-ui-patterns,vercel-composition-patterns,studio-queries,studio-error-handling}/SKILL.md'
|
||||
- files: '.claude/skills/{studio-best-practices,studio-ui-patterns,vercel-composition-patterns,studio-queries,studio-error-handling,react-hook-form}/SKILL.md'
|
||||
applyTo: 'apps/studio/**/*.{ts,tsx}'
|
||||
# Studio unit / component test conventions
|
||||
- files: '.claude/skills/{studio-testing,studio-mock-api-tests}/SKILL.md'
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
name: Dashboard PR Reminder
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 10am Singapore Time (2am UTC)
|
||||
- cron: '0 2 * * *'
|
||||
# Run at 10am US Eastern Time (2pm UTC = 10am EDT / 9am EST)
|
||||
- cron: '0 14 * * *'
|
||||
workflow_dispatch: # Allow manual trigger for testing
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-dashboard-prs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
scripts
|
||||
patches
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Find stale Dashboard PRs and notify Slack
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DASHBOARD_WEBHOOK_URL }}
|
||||
run: pnpm tsx scripts/actions/find-stale-dashboard-prs.ts | pnpm tsx scripts/actions/send-slack-pr-notification.ts
|
||||
@@ -0,0 +1,186 @@
|
||||
name: Docs E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
|
||||
branches: ['master']
|
||||
paths:
|
||||
- 'apps/docs/content/guides/**/*.mdx'
|
||||
- 'apps/docs/content/troubleshooting/**/*.mdx'
|
||||
- 'apps/docs/content/_partials/**'
|
||||
- 'e2e/docs/features/**'
|
||||
- 'e2e/docs/utils/**'
|
||||
- 'e2e/docs/scripts/**'
|
||||
- 'e2e/docs/playwright.config.ts'
|
||||
- 'e2e/docs/package.json'
|
||||
- 'e2e/docs/tsconfig.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.github/workflows/docs-e2e.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_url:
|
||||
description: 'Base URL to test against'
|
||||
required: false
|
||||
default: 'https://supabase.com'
|
||||
type: string
|
||||
page_paths:
|
||||
description: 'Comma-separated /docs/... paths to test (required for manual runs)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
statuses: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: Docs E2E
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false
|
||||
timeout-minutes: 30
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Need full history on PRs so we can diff against the base branch.
|
||||
# Use string '0' — numeric 0 is falsy in GitHub Actions expressions.
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
|
||||
sparse-checkout: |
|
||||
e2e/docs
|
||||
scripts
|
||||
patches
|
||||
apps/docs/content/guides
|
||||
apps/docs/content/troubleshooting
|
||||
apps/docs/content/_partials
|
||||
apps/docs/scripts/federated-content/sources
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
# Map changed owned content (guides, troubleshooting, partials) to page
|
||||
# URLs. Harness-only PRs resolve to skip=true and exit before Playwright.
|
||||
- name: Resolve docs E2E scope
|
||||
id: scope
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
PAGE_PATHS_INPUT: ${{ inputs.page_paths }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
if [ -z "$PAGE_PATHS_INPUT" ]; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "paths=" >> "$GITHUB_OUTPUT"
|
||||
echo "Manual run requires the page_paths input."
|
||||
exit 0
|
||||
fi
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
printf 'paths=%s\n' "$PAGE_PATHS_INPUT" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git diff --name-only --diff-filter=ACMR "origin/$BASE_REF"...HEAD \
|
||||
| node --experimental-strip-types e2e/docs/scripts/resolve-docs-scope.ts
|
||||
|
||||
- name: Skip Playwright (no in-scope pages)
|
||||
if: steps.scope.outputs.skip == 'true'
|
||||
run: echo "No in-scope docs pages changed; skipping Playwright suite."
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
if: steps.scope.outputs.skip != 'true'
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Enable pnpm store cache
|
||||
if: steps.scope.outputs.skip != 'true'
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
# Vercel skips the docs preview when a PR only changes the harness
|
||||
# (e2e/docs, workflow). Wait for a preview only when apps/docs changed.
|
||||
- name: Detect docs app changes
|
||||
if: steps.scope.outputs.skip != 'true' && github.event_name == 'pull_request'
|
||||
id: filter
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
docs_app:
|
||||
- 'apps/docs/**'
|
||||
|
||||
# Vercel's GitHub App stopped writing GitHub Deployment objects on
|
||||
# 2026-02-17 (broken app auth), so vercel/wait-for-deployment-action
|
||||
# times out polling that API even though the preview builds fine.
|
||||
# Poll the "Vercel – docs" commit status instead — Vercel keeps posting
|
||||
# those — then resolve the deployment it points to via Vercel's own API
|
||||
# to get the actual preview URL. See scripts/waitForVercelDocsPreview.js.
|
||||
- name: Wait for Vercel docs preview
|
||||
if: steps.scope.outputs.skip != 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.filter.outputs.docs_app == 'true'
|
||||
id: deployment
|
||||
run: node scripts/waitForVercelDocsPreview.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
|
||||
|
||||
- name: Resolve base URL
|
||||
if: steps.scope.outputs.skip != 'true'
|
||||
id: base-url
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_URL_INPUT: ${{ inputs.base_url }}
|
||||
DEPLOYMENT_URL: ${{ steps.deployment.outputs.deployment-url }}
|
||||
DOCS_APP_CHANGED: ${{ steps.filter.outputs.docs_app }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
printf 'url=%s\n' "$BASE_URL_INPUT" >> "$GITHUB_OUTPUT"
|
||||
echo "use_bypass=false" >> "$GITHUB_OUTPUT"
|
||||
elif [ "$DOCS_APP_CHANGED" = "true" ] && [ -n "$DEPLOYMENT_URL" ]; then
|
||||
printf 'url=%s\n' "$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "use_bypass=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Harness-only PRs have no docs preview; test against production.
|
||||
echo "url=https://supabase.com" >> "$GITHUB_OUTPUT"
|
||||
echo "use_bypass=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.scope.outputs.skip != 'true'
|
||||
run: pnpm install --frozen-lockfile --filter=e2e-docs...
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
if: steps.scope.outputs.skip != 'true'
|
||||
run: pnpm -C e2e/docs exec playwright install chromium --with-deps --only-shell
|
||||
|
||||
- name: Run docs E2E
|
||||
if: steps.scope.outputs.skip != 'true'
|
||||
working-directory: e2e/docs
|
||||
run: pnpm run e2e:docs
|
||||
env:
|
||||
PLAYWRIGHT_BASE_URL: ${{ steps.base-url.outputs.url }}
|
||||
DOCS_E2E_PAGE_PATHS: ${{ steps.scope.outputs.paths }}
|
||||
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ steps.base-url.outputs.use_bypass == 'true' && secrets.VERCEL_AUTOMATION_BYPASS_DOCS || '' }}
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: failure() && steps.scope.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: docs-playwright-report
|
||||
path: |
|
||||
e2e/docs/playwright-report/
|
||||
e2e/docs/test-results/
|
||||
retention-days: 7
|
||||
@@ -3,7 +3,7 @@ name: Update Mgmt Api Docs
|
||||
on:
|
||||
schedule:
|
||||
# Run at 00:00 UTC every Monday
|
||||
- cron: '0 0 * * 1'
|
||||
- cron: "0 0 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: master
|
||||
ref: ${{ github.ref }}
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
patches
|
||||
@@ -32,8 +32,8 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -55,8 +55,8 @@ jobs:
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
commit-message: 'feat: update mgmt api docs'
|
||||
title: 'feat: update mgmt api docs'
|
||||
body: 'This PR updates mgmt api docs automatically.'
|
||||
branch: 'gha/auto-update-mgmt-api-docs'
|
||||
base: 'master'
|
||||
commit-message: "feat: update mgmt api docs"
|
||||
title: "feat: update mgmt api docs"
|
||||
body: "This PR updates mgmt api docs automatically."
|
||||
branch: "gha/auto-update-mgmt-api-docs"
|
||||
base: "master"
|
||||
@@ -6,6 +6,11 @@ on:
|
||||
paths:
|
||||
- 'apps/docs/**/*.ts*'
|
||||
- 'apps/docs/spec/**/*.json'
|
||||
- 'apps/docs/.env.development'
|
||||
- 'apps/docs/package.json'
|
||||
- 'e2e/docs/local-smoke/**'
|
||||
- 'e2e/docs/playwright.local-smoke.config.ts'
|
||||
- 'e2e/docs/package.json'
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
concurrency:
|
||||
@@ -70,3 +75,52 @@ jobs:
|
||||
echo "GITHUB_CLIENT_ID=dummy-id" >> .env
|
||||
echo "GITHUB_SECRET=dummy-secret" >> .env
|
||||
pnpm run test:docs
|
||||
|
||||
local-dev-smoke:
|
||||
name: Local dev smoke (no credentials)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
examples
|
||||
packages
|
||||
supabase
|
||||
patches
|
||||
e2e/docs
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm -C e2e/docs exec playwright install chromium --with-deps --only-shell
|
||||
|
||||
# Deliberately does not set DOCS_GITHUB_APP_*, SUPABASE_SECRET_KEY,
|
||||
# OPENAI_API_KEY, or DOCS_REVALIDATION_KEYS — their absence here is what
|
||||
# verifies `pnpm run dev:docs` still works without private credentials.
|
||||
- name: Run local dev smoke tests
|
||||
run: pnpm run e2e:docs:local-smoke
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: docs-local-smoke-playwright-report
|
||||
path: |
|
||||
e2e/docs/playwright-report-local-smoke/
|
||||
e2e/docs/test-results/
|
||||
retention-days: 7
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: label
|
||||
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||
uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
|
||||
|
||||
- name: Comment when api-deploy-required is auto-applied
|
||||
if: contains(steps.label.outputs.new-labels, 'api-deploy-required')
|
||||
|
||||
@@ -29,10 +29,10 @@ jobs:
|
||||
with:
|
||||
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
|
||||
aws-region: us-east-1
|
||||
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
tags: |
|
||||
type=raw,value=${{ needs.settings.outputs.image_version }}_${{ env.arch }}
|
||||
|
||||
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
|
||||
|
||||
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Label Stale Issues
|
||||
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
stale-issue-message: 'After 30 days of inactivity, this issue has been marked as stale. Commenting (or other activity) will remove the stale label.'
|
||||
|
||||
@@ -36,3 +36,8 @@ jobs:
|
||||
- name: Build
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
run: docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --no-cache
|
||||
# Reuses the shared stages (deps/dev) from the first build's layer
|
||||
# cache; only the tanstack build/runtime stages run fresh.
|
||||
- name: Build (tanstack)
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
run: docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local-tanstack --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --build-arg STUDIO_FRAMEWORK=tanstack
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
with:
|
||||
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
|
||||
aws-region: us-east-1
|
||||
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
if: steps.filter.outputs.studio == 'true' && !github.event.pull_request.head.repo.fork
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
|
||||
@@ -37,6 +37,12 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
# Needs no dependencies, so it runs before install and fails fast. Catches a
|
||||
# class of bug that only breaks on case-insensitive filesystems (macOS and
|
||||
# Windows dev machines) and is therefore invisible to typecheck/lint on CI.
|
||||
- name: Check for case-sensitivity hazards
|
||||
run: node scripts/check-case-hazards.mjs
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
||||
+1
-1
@@ -123,8 +123,8 @@ next-env.d.ts
|
||||
!.claude/scripts/
|
||||
!.claude/skills/
|
||||
.claude/skills/me-*
|
||||
CLAUDE.md
|
||||
!.claude/CLAUDE.md
|
||||
CLAUDE.local.md
|
||||
|
||||
#include template .env file for docker-compose
|
||||
!docker/.env
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
engine-strict=true
|
||||
update-notifier=false
|
||||
@jsr:registry=https://npm.jsr.io
|
||||
+1
-1
@@ -97,7 +97,7 @@ You can run any of the sites individually by using the scope name. For example:
|
||||
pnpm dev:www
|
||||
```
|
||||
|
||||
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`
|
||||
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`. For `docs`, see [`apps/docs/DEVELOPERS.md`](./apps/docs/DEVELOPERS.md).
|
||||
|
||||
#### Shared components
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and
|
||||
- [Storage](https://github.com/supabase/storage-api) a RESTful API for managing files in S3, with Postgres handling permissions.
|
||||
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API.
|
||||
- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc.
|
||||
- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway.
|
||||
- [Envoy](https://github.com/envoyproxy/envoy) is a cloud-native, high-performance edge and service proxy.
|
||||
|
||||
#### Client libraries
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ export default function Component() {
|
||||
const chart = key as keyof typeof chartConfig
|
||||
return (
|
||||
<button
|
||||
tabIndex={0}
|
||||
key={chart}
|
||||
data-active={activeChart === chart}
|
||||
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-surface-100 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
|
||||
|
||||
@@ -2502,6 +2502,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"skip-to-content-demo": {
|
||||
name: "skip-to-content-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/skip-to-content-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/skip-to-content-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"page-container-demo": {
|
||||
name: "page-container-demo",
|
||||
type: "components:example",
|
||||
@@ -2601,6 +2612,72 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"connect-interstitial-demo": {
|
||||
name: "connect-interstitial-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/connect-interstitial-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"connect-interstitial-action-error": {
|
||||
name: "connect-interstitial-action-error",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-action-error")),
|
||||
source: "",
|
||||
files: ["registry/default/example/connect-interstitial-action-error.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"connect-interstitial-logo-pair": {
|
||||
name: "connect-interstitial-logo-pair",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-pair")),
|
||||
source: "",
|
||||
files: ["registry/default/example/connect-interstitial-logo-pair.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"connect-interstitial-logo-single": {
|
||||
name: "connect-interstitial-logo-single",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-single")),
|
||||
source: "",
|
||||
files: ["registry/default/example/connect-interstitial-logo-single.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"connect-interstitial-logo-unknown": {
|
||||
name: "connect-interstitial-logo-unknown",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-unknown")),
|
||||
source: "",
|
||||
files: ["registry/default/example/connect-interstitial-logo-unknown.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"connect-interstitial-logo-uploaded": {
|
||||
name: "connect-interstitial-logo-uploaded",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-uploaded")),
|
||||
source: "",
|
||||
files: ["registry/default/example/connect-interstitial-logo-uploaded.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"page-layout-auth-emails": {
|
||||
name: "page-layout-auth-emails",
|
||||
type: "components:example",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ScrollArea } from 'ui'
|
||||
import { SkipToContent } from 'ui-patterns/SkipToContent'
|
||||
|
||||
import { MobileSidebarSheet } from '@/components/mobile-sidebar-sheet'
|
||||
import { SideNavigation } from '@/components/side-navigation'
|
||||
@@ -12,18 +13,22 @@ interface AppLayoutProps {
|
||||
export default async function AppLayout({ children }: AppLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SkipToContent href="#main" />
|
||||
<TopNavigation />
|
||||
<MobileSidebarSheet />
|
||||
<main className="flex-1 max-w-site mx-auto w-full border-l border-r border-b">
|
||||
<div className="flex-1 max-w-site mx-auto w-full border-l border-r border-b">
|
||||
<div className="flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] lg:grid-cols-[240px_minmax(0,1fr)]">
|
||||
<aside className="fixed top-10 z-30 hidden h-[calc(100vh-3rem)] w-full shrink-0 md:sticky md:block border-r">
|
||||
<ScrollArea className="h-full">
|
||||
<SideNavigation />
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
{children}
|
||||
{/* Content-only landmark: sidebar must stay outside so skip/Tab don't land in the nav */}
|
||||
<main id="main" tabIndex={-1} className="outline-hidden scroll-mt-12 min-w-0">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<SiteFooter />
|
||||
</>
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,53 +0,0 @@
|
||||
import { Source_Code_Pro } from 'next/font/google'
|
||||
import localFont from 'next/font/local'
|
||||
|
||||
export const customFont = localFont({
|
||||
variable: '--font-custom',
|
||||
display: 'swap',
|
||||
fallback: ['Circular', 'custom-font', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
|
||||
src: [
|
||||
{
|
||||
path: './CustomFont-Book.woff2',
|
||||
weight: '400',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './CustomFont-BookItalic.woff2',
|
||||
weight: '400',
|
||||
style: 'italic',
|
||||
},
|
||||
{
|
||||
path: './CustomFont-Medium.woff2',
|
||||
weight: '500',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './CustomFont-Bold.woff2',
|
||||
weight: '700',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './CustomFont-BoldItalic.woff2',
|
||||
weight: '700',
|
||||
style: 'italic',
|
||||
},
|
||||
{
|
||||
path: './CustomFont-Black.woff2',
|
||||
weight: '800',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './CustomFont-BlackItalic.woff2',
|
||||
weight: '800',
|
||||
style: 'italic',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export const sourceCodePro = Source_Code_Pro({
|
||||
subsets: ['latin'],
|
||||
fallback: ['Source Code Pro', 'Office Code Pro', 'Menlo', 'monospace'],
|
||||
variable: '--font-source-code-pro',
|
||||
display: 'swap',
|
||||
weight: ['400', '500', '600', '700'],
|
||||
})
|
||||
@@ -3,11 +3,11 @@ import '@/styles/globals.css'
|
||||
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
|
||||
import { customFont, sourceCodePro } from './fonts'
|
||||
import { Providers } from './Providers'
|
||||
import { Toaster } from './toaster'
|
||||
import { inter, manrope, sourceCodePro } from '@/lib/fonts'
|
||||
|
||||
const className = `${customFont.variable} ${sourceCodePro.variable}`
|
||||
const className = `${inter.variable} ${manrope.variable} ${sourceCodePro.variable}`
|
||||
|
||||
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'
|
||||
|
||||
@@ -126,7 +126,7 @@ export default async function Layout({ children }: RootLayoutProps) {
|
||||
{/* [Danny]: This has to be an inline style tag here and not a separate component due to next/font */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `:root{--font-custom:${customFont.style.fontFamily};--font-source-code-pro:${sourceCodePro.style.fontFamily};}`,
|
||||
__html: `:root{--font-sans:${inter.style.fontFamily};--font-heading:${manrope.style.fontFamily};--font-source-code-pro:${sourceCodePro.style.fontFamily};}`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
|
||||
@@ -7,6 +7,7 @@ export function ClickCounter() {
|
||||
|
||||
return (
|
||||
<button
|
||||
tabIndex={0}
|
||||
onClick={() => setCount(count + 1)}
|
||||
className="whitespace-nowrap rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100 hover:bg-gray-500 hover:text-white"
|
||||
>
|
||||
|
||||
@@ -60,10 +60,11 @@ const ColorPalette = () => {
|
||||
const isCopied = copied === reference
|
||||
return (
|
||||
<button
|
||||
tabIndex={0}
|
||||
key={step * 100}
|
||||
type="button"
|
||||
onClick={() => handleCopy(reference)}
|
||||
className="group relative flex aspect-square w-full items-center justify-center rounded-sm border border-overlay/40 transition hover:scale-[1.05] focus:outline-none focus-visible:ring-2 focus-visible:ring-foreground"
|
||||
className="group relative flex aspect-square w-full items-center justify-center rounded-sm border border-overlay/40 transition-transform hover:scale-[1.05] focus-ring"
|
||||
style={{ backgroundColor: reference }}
|
||||
title={reference}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
import { ComponentProps } from './component-props'
|
||||
import { SonnerExpandConfig } from './sonner-expand-config'
|
||||
|
||||
@@ -70,6 +70,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/ui-patterns/charts',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Connect Interstitials',
|
||||
href: '/docs/ui-patterns/connect-interstitials',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Empty States',
|
||||
href: '/docs/ui-patterns/empty-states',
|
||||
@@ -242,6 +247,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/fragments/single-value-field-array',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Skip to Content',
|
||||
href: '/docs/fragments/skip-to-content',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,14 +25,68 @@ All interactive page elements should be reachable by keyboard. Given the below i
|
||||
|
||||
Chromium-based browsers and Firefox handle this automatically via the Tab key. Safari, by default, requires the Option key to also be held down. Enabling _Keyboard navigation_ on macOS Settings [removes this requirement](https://mayank.co/blog/safari-focus/#keyboard-navigation) but makes links non-tabbable as a result.
|
||||
|
||||
Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use consistent focus styles such as `inset-focus` so users recognize this state instantly.
|
||||
Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use one shared focus ring so users recognize this state instantly.
|
||||
|
||||
[Button](components/button) has all of the above built-in. Bespoke interactive elements however, such as the below interactive [Table Row](components/table#examples), require these props to be added manually:
|
||||
### Focus ring recipe
|
||||
|
||||
Prefer the shared utilities over inventing local styles:
|
||||
|
||||
| Utility | Use when |
|
||||
| ------------- | -------------------------------------------------------------------------- |
|
||||
| `focus-ring` | Buttons, inputs, and most controls (offset **ring**) |
|
||||
| `focus-inset` | Dense or flush surfaces such as interactive table rows (inset **outline**) |
|
||||
|
||||
```tsx
|
||||
className = 'focus-ring'
|
||||
// or
|
||||
className = 'relative cursor-pointer focus-inset'
|
||||
```
|
||||
|
||||
These expand to:
|
||||
|
||||
**`focus-ring`**
|
||||
|
||||
```txt
|
||||
outline-hidden
|
||||
focus-visible:ring-2
|
||||
focus-visible:ring-ring
|
||||
focus-visible:ring-offset-2
|
||||
focus-visible:ring-offset-background
|
||||
```
|
||||
|
||||
**`focus-inset`**
|
||||
|
||||
Uses `outline` (not `ring`) so it paints reliably on interactive `<tr>`s. Tailwind `ring` is `box-shadow`, which browsers often skip on `display: table-row` (notably Safari). Do not put `focus-ring` or raw `ring-*` on a `<tr>`, and do not add `outline-hidden` alongside `focus-inset`. `outline-hidden` sets `outline-style: none` and will hide the indicator.
|
||||
|
||||
```txt
|
||||
&:focus-visible {
|
||||
outline-style: solid
|
||||
outline-width: 2px
|
||||
outline-offset: -2px
|
||||
outline-color: var(--ring)
|
||||
border-radius: var(--radius-md)
|
||||
}
|
||||
```
|
||||
|
||||
`outline-hidden` is always on (not `focus-visible:`-prefixed) so mouse click does not show the browser’s default outline; the focus indicator replaces it for keyboard focus only.
|
||||
|
||||
Rules:
|
||||
|
||||
- Prefer `:focus-visible` over `:focus` so click/tap does not show a focus indicator
|
||||
- Never use `outline-none` / `outline-hidden` without a ring or outline replacement
|
||||
- Always use the shared color (`ring-ring` / `outline-ring`). Variants (primary, danger, warning) do not change focus colour
|
||||
- Do not animate the focus indicator; avoid `transition-all` / `transition` on controls that show one (prefer `transition-colors`)
|
||||
- Prefer `focus-ring` / `focus-inset` over copy-pasting the class stack
|
||||
- On interactive `<tr>`s, use `focus-inset` only. `focus-ring` will look fine in some browsers and invisible in others
|
||||
|
||||
When the focused element is not the thing that should show the ring (e.g. a wrapping `Link` with `group`, or an `InputGroup` parent using `:has()`), keep the explicit `group-focus-visible:ring-*` / `has-[…]:focus-visible:ring-*` stack. The utilities bake in `:focus-visible` on the same element and do not compose as `group-focus-visible:focus-ring`.
|
||||
|
||||
[Button](components/button) has focus, `tabIndex`, and the shared ring built-in. The same explicit `tabIndex` default is also baked into Checkbox, Switch, Select Trigger, Toggle, Accordion Trigger, Collapsible Trigger, Dropdown Menu Trigger, Popover Trigger, Dialog Trigger, Sheet Trigger, Alert Dialog Trigger, and the Sidebar Menu and action buttons. Bespoke interactive elements however, such as the below interactive [Table Row](components/table#examples), require these props to be added manually:
|
||||
|
||||
```tsx showLineNumbers {4-14}
|
||||
<TableRow
|
||||
key={id}
|
||||
className="relative cursor-pointer h-16 inset-focus"
|
||||
className="relative cursor-pointer h-16 focus-inset"
|
||||
onClick={(event) => {
|
||||
if (event.currentTarget !== event.target) return
|
||||
handleBucketNavigation(bucket.id, event)
|
||||
@@ -62,11 +116,13 @@ Individual options inside of a group can be reached by arrow keys (↑ ↓ ←
|
||||
|
||||
### Jumping ahead
|
||||
|
||||
Some keyboard-navigable content may be contain hundreds or thousands of items. Help users jump to specific content with the following mitigation strategies:
|
||||
Some keyboard-navigable content may contain hundreds or thousands of items. Help users jump to specific content with:
|
||||
|
||||
- Search and filtering
|
||||
- Pagination or virtualization
|
||||
- “Jump to” shortcuts to skip ahead
|
||||
- Skip links and “jump to” shortcuts
|
||||
|
||||
Apps with persistent header and sidebar chrome should expose a skip link as the first focusable element. Use the shared [Skip to Content](fragments/skip-to-content) fragment which owns the component API, usage sample, and target landmark contract.
|
||||
|
||||
## Screen readers
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Button
|
||||
description: Displays a button or a component that looks like a button.
|
||||
description: Displays a button or a link that looks like a button.
|
||||
featured: true
|
||||
component: true
|
||||
---
|
||||
@@ -144,5 +144,6 @@ Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions,
|
||||
- Enabled buttons default to `tabIndex={0}` (keyboard accessible)
|
||||
- Disabled buttons default to `tabIndex={-1}` (removed from tab order)
|
||||
- You can still override with an explicit `tabIndex` prop when needed
|
||||
- Keyboard focus uses the shared `focus-ring` utility; variants do not change ring colour
|
||||
|
||||
You therefore don't need to manually set `tabIndex`, as Button handles it automatically based on its `disabled` state.
|
||||
@@ -13,7 +13,7 @@ source:
|
||||
<ComponentPreview name="label-demo" peekCode wide />
|
||||
|
||||
<Admonition
|
||||
variant="warning"
|
||||
type="warning"
|
||||
title="Do not use this Label component in a Form"
|
||||
>
|
||||
|
||||
|
||||
@@ -58,6 +58,21 @@ import { toast } from 'sonner'
|
||||
toast('Event has been created.')
|
||||
```
|
||||
|
||||
## When to use
|
||||
|
||||
Use a toast for short-lived, non-blocking feedback or to confirm an operation
|
||||
after its originating surface has closed or navigated away.
|
||||
|
||||
Do not use a toast as the only feedback for:
|
||||
|
||||
- Field validation. Use `FormMessage` or `FieldError` beside the field.
|
||||
- A failed submission that leaves the form or interstitial visible. Show a
|
||||
`role="alert"` message in destructive text near the actions.
|
||||
- A blocking or materially changed page state. Use an inline state component
|
||||
such as `Admonition` or `ErrorDisplay`.
|
||||
|
||||
Keep error copy specific and include the next step when it is not obvious.
|
||||
|
||||
## Expand
|
||||
|
||||
You can change the amount of toasts visible through the visibleToasts prop.
|
||||
|
||||
@@ -221,7 +221,7 @@ Avoid adding other actions when using row-level navigation, as multiple interact
|
||||
When implementing row-level navigation, pay close attention to [Accessibility](/accessibility#focus-management) requirements. The row must be keyboard accessible with proper focus management. Also consider these affordances:
|
||||
|
||||
- Handle `Enter` and `Space` key presses for activation
|
||||
- Provide visual focus indicators using classes like `inset-focus`
|
||||
- Provide visual focus indicators using classes like `focus-inset`
|
||||
- Support modifier keys (`Ctrl`/`Cmd`, middle-click) for opening links in new tabs
|
||||
- Consider using the shared `createNavigationHandler` function to handle modifier keys
|
||||
- Avoid bubbling up action events from _within_ the row
|
||||
|
||||
@@ -24,7 +24,7 @@ Avoid title-only Admonitions in new code. A callout with `title` should also inc
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
```
|
||||
|
||||
```tsx
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Skip to Content
|
||||
description: Keyboard-accessible skip link that jumps past chrome to the main landmark.
|
||||
component: true
|
||||
fragment: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="skip-to-content-demo" peekCode wide />
|
||||
|
||||
Keyboard-accessible skip link composed from [Button](../components/button) for apps with persistent header and sidebar chrome. Hidden until focused via Tab, then slides into view so users can jump past navigation to the main landmark.
|
||||
|
||||
The preview above shows the focused appearance. In product apps, the link stays off-screen until keyboard focus.
|
||||
|
||||
See [Accessibility](../accessibility#jumping-ahead) for more information on when to use skip links, and how they coexist alongside other navigation aids.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { SkipToContent } from 'ui-patterns/SkipToContent'
|
||||
```
|
||||
|
||||
```tsx
|
||||
<SkipToContent href="#main" />
|
||||
|
||||
<main id="main" tabIndex={-1} className="scroll-mt-(--header-height) outline-hidden">
|
||||
{children}
|
||||
</main>
|
||||
```
|
||||
|
||||
Place the skip link at the root of the app chrome so Tab reaches it before navigation. The target landmark must be content only. Do not wrap a sidebar inside the same `<main>`, otherwise a Tab after skip will land in the sidebar navigation.
|
||||
|
||||
## Props
|
||||
|
||||
### `href`
|
||||
|
||||
Hash href to the main content landmark, e.g. `#main`.
|
||||
|
||||
### `children`
|
||||
|
||||
Link label. Defaults to `Skip to content`.
|
||||
|
||||
### `className`
|
||||
|
||||
Optional classes merged onto the positioning wrapper (useful for demos or layout overrides).
|
||||
|
||||
## Target landmark
|
||||
|
||||
Callers own the landmark the skip link points at:
|
||||
|
||||
- Matching `id`
|
||||
- `tabIndex={-1}` so Enter moves focus onto the landmark
|
||||
- `scroll-mt` when a sticky header is present
|
||||
- `outline-hidden` so the landmark has no visible focus ring. The subsequent Tab will land on the first interactive child
|
||||
@@ -45,7 +45,7 @@ function app() {
|
||||
}
|
||||
```
|
||||
|
||||
**Default props**: All icons default to `size={24}`. Stroke and fill defaults come from the source SVG's root attributes (e.g. `stroke-width="1"` on the root `<svg>` becomes the component's default `strokeWidth`). You can override those defaults at the call site, but any `stroke`, `fill`, or `stroke-width` set on individual child paths will still win.
|
||||
**Default props**: All icons default to `size={24}`. Stroke icons use `strokeWidth={1.5}` and fill-only icons use `stroke="none"`. These defaults come from the source SVG's root attributes and can be intentionally overridden at the call site.
|
||||
|
||||
### Adding new custom icons
|
||||
|
||||
@@ -54,7 +54,7 @@ Follow these steps to add a new custom icon to the Supabase icon library.
|
||||
1. **Create SVG file**: Add your SVG file to `packages/icons/src/raw-icons/` with a kebab-case name (e.g., `my-new-icon.svg`). Make sure it follows these requirements:
|
||||
- Exported at 24×24px with `viewBox="0 0 24 24"`
|
||||
- Uses `stroke="currentColor"` for strokes (no hardcoded colors)
|
||||
- Uses `stroke-width="1.5"` (deviate based on optical weight if necessary)
|
||||
- Uses `stroke-width="1.5"`
|
||||
- Uses `fill="none"` for fills (no hardcoded colors)
|
||||
- Icon content is optically centered and around 18×18px within the 24×24 frame
|
||||
- Any unnecessary elements like `<clipPath>`, `<defs>`, and `<g>` wrappers have been removed
|
||||
@@ -62,7 +62,7 @@ Follow these steps to add a new custom icon to the Supabase icon library.
|
||||
|
||||
For **fill-only icons** (e.g. logos that use shapes instead of strokes), add `stroke="none"` to the root `<svg>` element. The build will propagate this so the component never renders an unwanted stroke.
|
||||
|
||||
Prefer putting shared styling like `fill`, `stroke`, `stroke-width`, `stroke-linecap`, and `stroke-linejoin` on the root `<svg>`. The build propagates those root attributes as the component's defaults, and also converts them to camel-case for React compatibility (e.g. `strokeWidth`). If you put those attributes on individual child paths instead, they become fixed path-level styling and will override props passed to the component.
|
||||
Put shared styling like `fill`, `stroke`, `stroke-width`, `stroke-linecap`, and `stroke-linejoin` on the root `<svg>`. The build validates this contract, propagates the root attributes as the component's defaults, and converts them to camel-case for React compatibility (e.g. `strokeWidth`). Child-level stroke styling is rejected because it would override props passed to the component.
|
||||
|
||||
2. **Build the component**: Run `npm run build:icons` from inside the `packages/icons` directory
|
||||
|
||||
@@ -132,4 +132,4 @@ Note `stroke="none"` on the root to prevent unwanted strokes, and `fill="current
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If your SVG specifies `stroke-width` attributes on individual child paths, they will override the component's `strokeWidth` prop. Keep shared stroke attributes on the root `<svg>` and remove them from individual paths if you want consumers to control stroke weight.
|
||||
The icon build reports the source file and invalid attribute when an SVG does not follow the stroke or fill-only contract. Run `pnpm validate:icons` from `packages/icons` to check sources without regenerating components.
|
||||
@@ -0,0 +1,268 @@
|
||||
---
|
||||
title: Connect Interstitials
|
||||
description: Shared layout guidance for focused authorisation, invite, marketplace, CLI, and credit redemption flows.
|
||||
---
|
||||
|
||||
Connect interstitials are focused, single-card flows that sit outside the main
|
||||
Studio shell. Use the shared `InterstitialLayout` family instead of building
|
||||
bespoke centered cards, logos, account rows, or organisation selectors.
|
||||
|
||||
<ComponentPreview
|
||||
name="connect-interstitial-demo"
|
||||
description="Centered 400px card with partner branding, account row, and a single primary action"
|
||||
align="start"
|
||||
className="p-0"
|
||||
padded={false}
|
||||
peekCode
|
||||
wide
|
||||
/>
|
||||
|
||||
## Use this pattern for
|
||||
|
||||
This pattern fits short-lived connect flows: partner authorisation and consent
|
||||
(OAuth, MCP, Stripe Projects), organisation invites, marketplace and billing
|
||||
connections (AWS Marketplace, Vercel install, credit redemption), and CLI or
|
||||
device-code sign-in. Use the same shell for their loading, error, success, and
|
||||
wrong-account states.
|
||||
|
||||
Do not use it for normal authenticated Studio pages. Those should use the
|
||||
standard [page layout](./layout) patterns.
|
||||
|
||||
## Source of truth
|
||||
|
||||
```tsx
|
||||
import { OrganizationSelector } from '@/components/interfaces/Connect/OrganizationSelector'
|
||||
import {
|
||||
InterstitialAccountRow,
|
||||
InterstitialLayout,
|
||||
LogoBox,
|
||||
LogoPair,
|
||||
PartnerLogo,
|
||||
SupabaseLogo,
|
||||
} from '@/components/layouts/InterstitialLayout'
|
||||
```
|
||||
|
||||
## Basic shape
|
||||
|
||||
Use `InterstitialLayout` for the outer card, then put route-specific content in
|
||||
`px-6 pb-6`. Widen the card only when the flow embeds a real tool, such as
|
||||
project linking.
|
||||
|
||||
```tsx
|
||||
<InterstitialLayout
|
||||
logo={
|
||||
<LogoPair
|
||||
left={<PartnerLogo src={`${BASE_PATH}/img/icons/stripe-icon.svg`} alt="Stripe" />}
|
||||
right={<SupabaseLogo />}
|
||||
/>
|
||||
}
|
||||
title="Authorize Stripe Projects"
|
||||
description="This will create an organization on your behalf in Supabase"
|
||||
>
|
||||
<div className="px-6 pb-6">
|
||||
<InterstitialAccountRow displayName={displayName} />
|
||||
<Button variant="primary" block>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</InterstitialLayout>
|
||||
```
|
||||
|
||||
```tsx
|
||||
<InterstitialLayout
|
||||
logo={<LogoPair left={<VercelLogo />} right={<SupabaseLogo />} />}
|
||||
title="Connect Vercel project"
|
||||
containerClassName="items-start"
|
||||
cardClassName="max-w-[900px]"
|
||||
>
|
||||
<div className="px-6 pb-6">{projectLinker}</div>
|
||||
</InterstitialLayout>
|
||||
```
|
||||
|
||||
## Logos
|
||||
|
||||
Use `LogoPair` when the user is connecting two known services, and
|
||||
`SupabaseLogo` alone for first-party flows or when the requester has no trusted
|
||||
mark. `PartnerLogo` fills the 48px box edge-to-edge; `LogoBox` is for custom
|
||||
inset marks or logos that need their own background. Store new partner icons in
|
||||
`apps/studio/public/img/icons`.
|
||||
|
||||
### Pairing
|
||||
|
||||
| Requester logo | Header treatment |
|
||||
| -------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| Curated partner / MCP client, or a trusted uploaded icon | `LogoPair` with requester left, `SupabaseLogo` right |
|
||||
| Unknown, missing, blocked, or failed-to-load icon | `SupabaseLogo` alone. Do not invent an initial tile. |
|
||||
|
||||
The user is usually arriving from the third-party app. The interstitial should
|
||||
confirm they are connecting to Supabase. Put the requester name in the title and
|
||||
description; do not manufacture a letter avatar to fill the left side of a pair.
|
||||
|
||||
**Known services.** When both sides are curated (or otherwise known), pair them.
|
||||
Theme-reactive tiles are fine when both marks have matching light/dark
|
||||
treatment.
|
||||
|
||||
<ComponentPreview
|
||||
name="connect-interstitial-logo-pair"
|
||||
description="LogoPair when the user is connecting two services"
|
||||
align="start"
|
||||
className="p-0"
|
||||
padded={false}
|
||||
peekCode
|
||||
wide
|
||||
/>
|
||||
|
||||
**No trusted requester mark.** If the icon is missing, blocked, or fails to
|
||||
load, show `SupabaseLogo` alone. Do not invent an initial tile to fill the
|
||||
pair.
|
||||
|
||||
<ComponentPreview
|
||||
name="connect-interstitial-logo-unknown"
|
||||
description="No trusted requester mark: Supabase alone"
|
||||
align="start"
|
||||
className="p-0"
|
||||
padded={false}
|
||||
peekCode
|
||||
wide
|
||||
/>
|
||||
|
||||
**Uploaded organisation OAuth icons.** Icons published via Studio’s OAuth app
|
||||
builder are unclassified bitmaps — we do not know if they were authored for
|
||||
light or dark. Treat the pair as light on both Studio themes: fixed light tile
|
||||
chrome (`border-black/10 bg-white`, `SupabaseLogo forceLight`) on both sides.
|
||||
Do not invent a dark variant for the upload. Toggle the docs theme to dark to
|
||||
see the light tiles hold against the Studio chrome.
|
||||
|
||||
<ComponentPreview
|
||||
name="connect-interstitial-logo-uploaded"
|
||||
description="Uploaded OAuth app icon: forced-light tiles on both sides"
|
||||
align="start"
|
||||
className="p-0"
|
||||
padded={false}
|
||||
peekCode
|
||||
wide
|
||||
/>
|
||||
|
||||
### Assets
|
||||
|
||||
Treat Connect logos as assets, not theme tokens.
|
||||
|
||||
**Default to light.** Prefer a single static light mark inside `LogoBox`.
|
||||
Light assets read fine on both light and dark Studio themes. That is the
|
||||
default for Connect tiles.
|
||||
|
||||
**Keep pairs matched.** In a `LogoPair`, both marks must use the same
|
||||
treatment: both light, or both dark. Do not mix a light partner tile with a
|
||||
dark-theme-only Supabase treatment, or the reverse. Theme-aware dark variants
|
||||
are fine for curated partners that already have them, but then both sides of
|
||||
the pair should use the dark set together.
|
||||
|
||||
**What not to do**
|
||||
|
||||
- Do not invent light/dark pairs for arbitrary remote OAuth icons.
|
||||
- Do not recolour vendor SVGs with theme CSS. Monochrome identity-provider
|
||||
masks (for example GitHub on sign-in) stay a separate pattern.
|
||||
|
||||
**Where logos come from on `/authorize`**
|
||||
|
||||
- Curated partner logos resolve from allowlisted `redirect_uri` hosts, or from
|
||||
a trusted partner name when `redirect_uri` is localhost / loopback (local MCP
|
||||
clients). Do not resolve curated logos from self-asserted `name` or `website`
|
||||
on a remote host. Those pairs may use theme tiles and dark assets when the
|
||||
partner has them.
|
||||
- Published organisation OAuth app icons uploaded in Studio remain trusted
|
||||
remote images, paired with forced-light tiles on both sides.
|
||||
- Everything else falls back to `SupabaseLogo` alone.
|
||||
- If the requester name looks like a known partner but `redirect_uri` is a
|
||||
remote host outside that partner's allowlist, show a caution admonition.
|
||||
Localhost MCP redirects are excluded.
|
||||
|
||||
## Account row
|
||||
|
||||
Use `InterstitialAccountRow` for signed-in context. Do not recreate it locally.
|
||||
|
||||
```tsx
|
||||
<InterstitialAccountRow avatarUrl={avatarUrl} displayName={displayName} action={signOutButton} />
|
||||
```
|
||||
|
||||
## Organisation selection
|
||||
|
||||
Use `OrganizationSelector` when the flow needs an organisation pick. Extend it
|
||||
for new states instead of inventing a parallel card style.
|
||||
|
||||
```tsx
|
||||
<OrganizationSelector
|
||||
organizations={linkableOrganizations}
|
||||
selectedSlug={selectedOrgSlug}
|
||||
onSelect={setSelectedOrgSlug}
|
||||
createLabel="Create new organization"
|
||||
onCreate={() => setShowOrgCreationDialog(true)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
Prefer one full-width primary action. A full-width text button is fine for a
|
||||
secondary action that still belongs in the flow.
|
||||
|
||||
### Action feedback
|
||||
|
||||
Match feedback to its scope:
|
||||
|
||||
- Use `FormMessage` or `FieldError` beside a field when that field needs to
|
||||
change.
|
||||
- Show a submission or action failure as simple destructive text below the
|
||||
actions. Separate it with a subtle divider when needed for composition. Keep
|
||||
the current account, selections, and actions visible so the user can retry.
|
||||
- Use `Admonition` when the whole interstitial is blocked or has materially
|
||||
changed state, such as an invalid link, wrong account, or partially completed
|
||||
setup.
|
||||
- Use a toast only for non-blocking feedback or a completed action whose
|
||||
originating surface is no longer visible. A toast must not be the only
|
||||
feedback for a failure the user needs to resolve on the current card.
|
||||
|
||||
```tsx
|
||||
<InterstitialActionError error={actionError} />
|
||||
```
|
||||
|
||||
Clear stale action feedback when the user retries or changes a relevant
|
||||
selection. Error copy should say what failed and, when it is not obvious, what
|
||||
the user can do next. When passive supporting copy occupies the same footer
|
||||
region, replace it with the action error until the error is cleared instead of
|
||||
stacking both messages.
|
||||
|
||||
<ComponentPreview
|
||||
name="connect-interstitial-action-error"
|
||||
description="Retryable action error shown beside the actions"
|
||||
align="start"
|
||||
className="p-0"
|
||||
padded={false}
|
||||
peekCode
|
||||
wide
|
||||
/>
|
||||
|
||||
## States
|
||||
|
||||
Keep loading, invalid, error, and success states inside the same card when the
|
||||
route can explain them. Use `ShimmeringLoader` for loading, and `Admonition`
|
||||
for warning, error, note, and success copy.
|
||||
|
||||
<ComponentPreview
|
||||
name="connect-interstitial-logo-single"
|
||||
description="Wrong-account warning inside the same interstitial card"
|
||||
align="start"
|
||||
className="p-0"
|
||||
padded={false}
|
||||
peekCode
|
||||
wide
|
||||
/>
|
||||
|
||||
## Copy
|
||||
|
||||
Use sentence case. Prefer `sign in` over `login`. Titles and primary actions
|
||||
should follow `Verb -> Thing`, for example `Authorize Stripe Projects` or
|
||||
`Install Vercel`.
|
||||
|
||||
Keep the layout title static across states and put state-specific copy in the
|
||||
body. Header descriptions should stay short and should not end with a full
|
||||
stop.
|
||||
@@ -42,7 +42,7 @@ Keep repeated-row validation in the form schema or shared validation helper, not
|
||||
|
||||
Build a custom row when the cells are mixed controls, such as an input paired with a `Select`.
|
||||
|
||||
## Best Practices
|
||||
## Best practices
|
||||
|
||||
1. **Always use FormItemLayout**: Use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription`.
|
||||
|
||||
@@ -55,9 +55,9 @@ Build a custom row when the cells are mixed controls, such as an input paired wi
|
||||
|
||||
4. **Use Cards for grouping**: Wrap form sections in `Card` components with `CardContent` and `CardFooter` for actions.
|
||||
|
||||
5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`. Make sure you destructure `isDirty` from `form.formState` (see https://react-hook-form.com/docs/useform/formstate)
|
||||
5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`. Make sure you destructure `isDirty` from `form.formState` (see https://react-hook-form.com/docs/useform/formstate). When the form is in a dialog or sheet, also follow [Dirty form dismissal](./modality#dirty-form-dismissal) so Cancel, Escape, and backdrop ask before discarding unsaved changes.
|
||||
|
||||
6. **Error handling**: Always use mutations with `onSuccess` and `onError` callbacks that show toast notifications.
|
||||
6. **Error handling**: Match feedback to its scope. Use `FormMessage` or `FieldError` for field validation. Show submission failures inline near the form actions when the user needs to retry or change something. Reserve toasts for non-blocking feedback or completed operations whose originating surface is no longer visible.
|
||||
|
||||
7. **Loading states**: Show loading states on submit buttons using the `loading` prop.
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ Supabase has a necessarily complex navigation system to handle multiple products
|
||||
|
||||
## Components
|
||||
|
||||
### NavMenu
|
||||
### [Nav Menu](../components/nav-menu)
|
||||
|
||||
A horizontal list of related views within a consistent PageLayout context, allowing for clearer page-level organisation. Activating a NavMenu item should trigger a URL change.
|
||||
|
||||
[NavMenu component guidelines](../components/nav-menu)
|
||||
### [Skip To Content](../fragments/skip-to-content)
|
||||
|
||||
Keyboard-accessible skip link for apps with persistent header and sidebar chrome. Hidden until focused via Tab, then slides into view so users can jump past navigation to the main landmark. See [Accessibility](../accessibility#jumping-ahead) for broader context.
|
||||
|
||||
## Page titles
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Inter, Manrope, Source_Code_Pro } from 'next/font/google'
|
||||
|
||||
export const manrope = Manrope({
|
||||
variable: '--font-manrope',
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
export const inter = Inter({
|
||||
variable: '--font-inter',
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
export const sourceCodePro = Source_Code_Pro({
|
||||
subsets: ['latin'],
|
||||
fallback: ['Source Code Pro', 'Office Code Pro', 'Menlo', 'monospace'],
|
||||
variable: '--font-source-code-pro',
|
||||
display: 'swap',
|
||||
weight: ['400', '500', '600', '700'],
|
||||
})
|
||||
@@ -13,14 +13,14 @@
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"content:build": "contentlayer2 build",
|
||||
"clean": "rimraf node_modules .next .turbo",
|
||||
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo",
|
||||
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.1.1",
|
||||
"@tanstack/react-table": "catalog:",
|
||||
"contentlayer2": "0.4.6",
|
||||
"common": "workspace:*",
|
||||
"contentlayer2": "0.4.6",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "1.11.13",
|
||||
"eslint-config-supabase": "workspace:*",
|
||||
@@ -57,6 +57,7 @@
|
||||
"@types/lodash.template": "4.5.0",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@typescript/native": "catalog:",
|
||||
"config": "workspace:*",
|
||||
"mdast-util-toc": "^6.1.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -66,7 +67,6 @@
|
||||
"tailwindcss": "catalog:",
|
||||
"tsconfig": "workspace:*",
|
||||
"tsx": "catalog:",
|
||||
"@typescript/native": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unist-builder": "3.0.0"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 137 B |
@@ -147,6 +147,7 @@ export default function Component() {
|
||||
const chart = key as keyof typeof chartConfig
|
||||
return (
|
||||
<button
|
||||
tabIndex={0}
|
||||
key={chart}
|
||||
data-active={activeChart === chart}
|
||||
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-surface-100 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function ChartComposedActions() {
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
width: 36,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
@@ -92,7 +92,7 @@ export default function ChartComposedActions() {
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
width: 36,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function ComposedChartBasic() {
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
width: 36,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
@@ -129,7 +129,7 @@ export default function ComposedChartBasic() {
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
width: 36,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function ChartComposedTable() {
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
width: 36,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionButtonSplitDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionDescriptionOnly() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionDestructive() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Card, CardContent, CardHeader, CardTitle } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionSuccess() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
export default function AdmonitionWarning() {
|
||||
return (
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
AlertDialogTrigger,
|
||||
Button,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
const resetTemplate = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200))
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Button } from 'ui'
|
||||
|
||||
import {
|
||||
AccountRow,
|
||||
InterstitialActionError,
|
||||
InterstitialShell,
|
||||
LogoPair,
|
||||
StripeLogo,
|
||||
SupabaseLogo,
|
||||
} from './connect-interstitial-shared'
|
||||
|
||||
export default function ConnectInterstitialActionError() {
|
||||
return (
|
||||
<InterstitialShell
|
||||
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
|
||||
title="Authorize Stripe Projects"
|
||||
description="This will create an organization on your behalf in Supabase"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<AccountRow displayName="alex@example.com" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="primary" block>
|
||||
Authorize Stripe Projects
|
||||
</Button>
|
||||
<Button variant="text" block>
|
||||
Cancel
|
||||
</Button>
|
||||
<InterstitialActionError error="Failed to authorize Stripe Projects. Please try again." />
|
||||
</div>
|
||||
</div>
|
||||
</InterstitialShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Button } from 'ui'
|
||||
|
||||
import {
|
||||
AccountRow,
|
||||
InterstitialShell,
|
||||
LogoPair,
|
||||
SignOutButton,
|
||||
StripeLogo,
|
||||
SupabaseLogo,
|
||||
} from './connect-interstitial-shared'
|
||||
|
||||
export default function ConnectInterstitialDemo() {
|
||||
return (
|
||||
<InterstitialShell
|
||||
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
|
||||
title="Authorize Stripe Projects"
|
||||
description="This will create an organization on your behalf in Supabase"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
|
||||
<Button variant="primary" block>
|
||||
Authorize Stripe Projects
|
||||
</Button>
|
||||
<Button variant="text" block>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</InterstitialShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Button } from 'ui'
|
||||
|
||||
import {
|
||||
AccountRow,
|
||||
InterstitialShell,
|
||||
LogoPair,
|
||||
SignOutButton,
|
||||
StripeLogo,
|
||||
SupabaseLogo,
|
||||
} from './connect-interstitial-shared'
|
||||
|
||||
export default function ConnectInterstitialLogoPair() {
|
||||
return (
|
||||
<InterstitialShell
|
||||
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
|
||||
title="Authorize Stripe Projects"
|
||||
description="This will create an organization on your behalf in Supabase"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
|
||||
<Button variant="primary" block>
|
||||
Authorize Stripe Projects
|
||||
</Button>
|
||||
</div>
|
||||
</InterstitialShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
import { AccountRow, InterstitialShell, SupabaseLogo } from './connect-interstitial-shared'
|
||||
|
||||
export default function ConnectInterstitialLogoSingle() {
|
||||
return (
|
||||
<InterstitialShell
|
||||
logo={<SupabaseLogo />}
|
||||
title="Join organization"
|
||||
description="You have been invited to Acme Labs"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Admonition
|
||||
type="warning"
|
||||
title="Wrong account"
|
||||
description="Sign in with the Supabase account that received this invite, then open the link again."
|
||||
/>
|
||||
<AccountRow displayName="alex@example.com" />
|
||||
<Button variant="primary" block>
|
||||
Sign out and continue
|
||||
</Button>
|
||||
</div>
|
||||
</InterstitialShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Button } from 'ui'
|
||||
|
||||
import {
|
||||
AccountRow,
|
||||
InterstitialShell,
|
||||
SignOutButton,
|
||||
SupabaseLogo,
|
||||
} from './connect-interstitial-shared'
|
||||
|
||||
export default function ConnectInterstitialLogoUnknown() {
|
||||
return (
|
||||
<InterstitialShell
|
||||
logo={<SupabaseLogo />}
|
||||
title="Authorize Acme"
|
||||
description="Acme is requesting access to your organization"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
|
||||
<Button variant="primary" block>
|
||||
Authorize Acme
|
||||
</Button>
|
||||
</div>
|
||||
</InterstitialShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Button } from 'ui'
|
||||
|
||||
import {
|
||||
AccountRow,
|
||||
InterstitialShell,
|
||||
LogoBox,
|
||||
LogoPair,
|
||||
SignOutButton,
|
||||
SupabaseLogo,
|
||||
} from './connect-interstitial-shared'
|
||||
|
||||
/** Stand-in uploaded OAuth icon: checked-in solid-colour bitmap (not a real brand). */
|
||||
function UploadedAppLogo() {
|
||||
return (
|
||||
<LogoBox className="border-black/10 bg-white">
|
||||
<img
|
||||
alt="Acme"
|
||||
src={`${process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'}/img/icons/acme-oauth-icon.png`}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</LogoBox>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ConnectInterstitialLogoUploaded() {
|
||||
return (
|
||||
<InterstitialShell
|
||||
logo={<LogoPair left={<UploadedAppLogo />} right={<SupabaseLogo forceLight />} />}
|
||||
title="Authorize Acme"
|
||||
description="Acme is requesting access to your organization"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
|
||||
<Button variant="primary" block>
|
||||
Authorize Acme
|
||||
</Button>
|
||||
</div>
|
||||
</InterstitialShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { ArrowRightLeft, LogOut } from 'lucide-react'
|
||||
import { Avatar, AvatarFallback, Button, Card, CardContent, CardHeader, cn } from 'ui'
|
||||
|
||||
export function LogoBox({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-12 items-center justify-center overflow-hidden rounded-xl border bg-muted',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LogoPair({ left, right }: { left: React.ReactNode; right: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2.5">
|
||||
{left}
|
||||
<ArrowRightLeft className="size-4 text-foreground-muted" />
|
||||
{right}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function StripeLogo() {
|
||||
return (
|
||||
<LogoBox className="border-[#533afd] bg-[#533afd]">
|
||||
<svg viewBox="0 0 512 512" className="size-full" aria-hidden>
|
||||
<path
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
d="m132 380 248-52.593V132l-248 53.208z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</LogoBox>
|
||||
)
|
||||
}
|
||||
|
||||
export function SupabaseLogo({ forceLight = false }: { forceLight?: boolean } = {}) {
|
||||
return (
|
||||
<LogoBox className={forceLight ? 'border-black/10 bg-white' : 'bg-surface-75'}>
|
||||
<svg viewBox="0 0 109 113" className="size-7" aria-hidden>
|
||||
<path
|
||||
d="M63.708 110.284c-2.86 3.601-8.658 1.628-8.727-2.97L53.974 40.063h45.22c8.19 0 12.758 9.46 7.665 15.874L63.708 110.284Z"
|
||||
fill="#3ECF8E"
|
||||
/>
|
||||
<path
|
||||
d="M45.317 2.071c2.86-3.601 8.658-1.628 8.726 2.97l.442 67.251H9.831C1.64 72.292-2.928 62.832 2.166 56.418L45.317 2.071Z"
|
||||
fill="#3ECF8E"
|
||||
/>
|
||||
</svg>
|
||||
</LogoBox>
|
||||
)
|
||||
}
|
||||
|
||||
export function AccountRow({
|
||||
displayName,
|
||||
action,
|
||||
}: {
|
||||
displayName: string
|
||||
action?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Card className={cn('shadow-none', !action && 'border-muted bg-surface-200/50')}>
|
||||
<CardContent
|
||||
className={cn('flex gap-3 border-none', action ? 'items-center px-4 py-3' : 'p-3')}
|
||||
>
|
||||
<Avatar className="size-8 border border-muted">
|
||||
<AvatarFallback className="text-xs">A</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-foreground-light">Signed in as</p>
|
||||
<p className="truncate text-sm text-foreground">{displayName}</p>
|
||||
</div>
|
||||
{action}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function InterstitialShell({
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: {
|
||||
logo: React.ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-[520px] w-full items-center justify-center bg-studio px-2 py-6">
|
||||
<Card className="w-full max-w-[400px] overflow-hidden">
|
||||
<CardHeader className="items-center gap-0 space-y-0 border-0 px-6 py-6 text-center">
|
||||
<div className="mb-4 flex justify-center">{logo}</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<h1 className="text-balance text-lg font-medium tracking-tight text-foreground">
|
||||
{title}
|
||||
</h1>
|
||||
{description ? (
|
||||
<p className="m-0 px-3 text-balance text-sm leading-tight text-foreground-lighter">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className="px-6 pb-6">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function InterstitialActionError({ error }: { error?: React.ReactNode }) {
|
||||
if (!error) return null
|
||||
|
||||
return (
|
||||
<div className="mt-3 border-t border-muted pt-5">
|
||||
<p role="alert" className="text-center text-xs text-destructive text-balance">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SignOutButton() {
|
||||
return <Button variant="default" icon={<LogOut />} className="px-2" aria-label="Sign out" />
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
const bucketId = 'user_avatars'
|
||||
|
||||
|
||||
@@ -434,6 +434,7 @@ export default function FormPatternsPageLayout() {
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
Drag and drop or{' '}
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
onClick={() => fileUploadRef.current?.click()}
|
||||
className="underline cursor-pointer hover:text-foreground-light"
|
||||
|
||||
@@ -316,9 +316,10 @@ export default function FormPatternsSidePanel() {
|
||||
<FormControl className="col-span-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
onClick={() => uploadButtonRef.current?.click()}
|
||||
className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong focus-visible:outline-brand-600"
|
||||
className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong focus-ring"
|
||||
style={{
|
||||
backgroundImage: logoUrl ? `url("${logoUrl}")` : 'none',
|
||||
}}
|
||||
@@ -424,6 +425,7 @@ export default function FormPatternsSidePanel() {
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
Drag and drop or{' '}
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
onClick={() => fileUploadRef.current?.click()}
|
||||
className="underline cursor-pointer hover:text-foreground-light"
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
NavMenuItem,
|
||||
Switch,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
|
||||
import { PageContainer } from 'ui-patterns/PageContainer'
|
||||
@@ -139,6 +139,7 @@ export default function PageLayoutAuthEmails() {
|
||||
{subPages.map((page) => (
|
||||
<NavMenuItem key={page.id} active={activePage === page.id}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
aria-pressed={activePage === page.id}
|
||||
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
|
||||
@@ -212,6 +213,7 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
|
||||
{AUTHENTICATION_TEMPLATES.map((template) => (
|
||||
<CardContent key={template.title} className="p-0">
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between px-6 py-4 text-left transition-colors hover:bg-surface-200"
|
||||
>
|
||||
@@ -248,7 +250,11 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
|
||||
key={template.id}
|
||||
className="flex h-full w-full items-center justify-between p-0 transition-colors hover:bg-surface-200"
|
||||
>
|
||||
<button type="button" className="flex flex-1 flex-col px-6 py-4 text-left">
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
className="flex flex-1 flex-col px-6 py-4 text-left"
|
||||
>
|
||||
<h3 className="text-sm text-foreground">{template.title}</h3>
|
||||
<p className="text-sm text-foreground-lighter">{template.purpose}</p>
|
||||
</button>
|
||||
@@ -263,7 +269,12 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<button type="button" className="py-6 pr-6" aria-label="Edit template">
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
className="py-6 pr-6"
|
||||
aria-label="Edit template"
|
||||
>
|
||||
<ChevronRight size={16} className="text-foreground-muted" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -261,6 +261,7 @@ export default function PageLayoutEdgeFunction() {
|
||||
{pages.map((page) => (
|
||||
<NavMenuItem key={page.id} active={activePage === page.id}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
aria-pressed={activePage === page.id}
|
||||
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function PageNavDemo() {
|
||||
{pages.map((page) => (
|
||||
<NavMenuItem key={page.id} active={activePage === page.id}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
aria-pressed={activePage === page.id}
|
||||
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SkipToContent } from 'ui-patterns/SkipToContent'
|
||||
|
||||
export default function SkipToContentDemo() {
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden rounded-md border bg-studio">
|
||||
<div className="flex items-center border-b px-4 py-3 text-sm text-foreground-muted">
|
||||
Demo header / navigation
|
||||
</div>
|
||||
{/* Preview shows the focused appearance; production hides until Tab */}
|
||||
<SkipToContent href="#skip-demo-main" className="relative left-3 top-2 translate-y-0" />
|
||||
<main id="skip-demo-main" tabIndex={-1} className="outline-hidden p-6 pt-2">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Main content landmark. In a real app, Tab once to reveal the skip link, then Enter to jump
|
||||
here.
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default function SuccessCheckSelected() {
|
||||
|
||||
return (
|
||||
<button
|
||||
tabIndex={0}
|
||||
key={option}
|
||||
type="button"
|
||||
aria-pressed={isSelected}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function TableRowLinkActions() {
|
||||
{policies.map((policy) => (
|
||||
<TableRow
|
||||
key={policy.id}
|
||||
className="relative cursor-pointer inset-focus"
|
||||
className="relative cursor-pointer focus-inset"
|
||||
onClick={(event) => {
|
||||
if (event.currentTarget !== event.target) return
|
||||
handlePolicyNavigation(policy.id, event)
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function TableRowLink() {
|
||||
{buckets.map((bucket) => (
|
||||
<TableRow
|
||||
key={bucket.id}
|
||||
className="relative cursor-pointer inset-focus"
|
||||
className="relative cursor-pointer focus-inset"
|
||||
onClick={(event) => {
|
||||
if (event.currentTarget !== event.target) return
|
||||
handleBucketNavigation(bucket.id, event)
|
||||
|
||||
@@ -1351,6 +1351,11 @@ export const examples: Registry = [
|
||||
type: 'components:example',
|
||||
files: ['example/info-tooltip-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'skip-to-content-demo',
|
||||
type: 'components:example',
|
||||
files: ['example/skip-to-content-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'page-container-demo',
|
||||
type: 'components:example',
|
||||
@@ -1398,6 +1403,36 @@ export const examples: Registry = [
|
||||
type: 'components:example',
|
||||
files: ['example/page-layout-settings.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'connect-interstitial-demo',
|
||||
type: 'components:example',
|
||||
files: ['example/connect-interstitial-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'connect-interstitial-action-error',
|
||||
type: 'components:example',
|
||||
files: ['example/connect-interstitial-action-error.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'connect-interstitial-logo-pair',
|
||||
type: 'components:example',
|
||||
files: ['example/connect-interstitial-logo-pair.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'connect-interstitial-logo-single',
|
||||
type: 'components:example',
|
||||
files: ['example/connect-interstitial-logo-single.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'connect-interstitial-logo-unknown',
|
||||
type: 'components:example',
|
||||
files: ['example/connect-interstitial-logo-unknown.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'connect-interstitial-logo-uploaded',
|
||||
type: 'components:example',
|
||||
files: ['example/connect-interstitial-logo-uploaded.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'page-layout-auth-emails',
|
||||
type: 'components:example',
|
||||
|
||||
@@ -9,9 +9,30 @@
|
||||
@source './../../../packages/ui/src/**/*.{tsx,ts,js}';
|
||||
@source './../../../packages/ui-patterns/src/**/*.{tsx,ts,js}';
|
||||
|
||||
@theme inline {
|
||||
--font-sans:
|
||||
var(--font-inter), Inter, Helvetica Neue, Helvetica, ui-sans-serif, system-ui, sans-serif;
|
||||
--font-heading: var(--font-manrope, var(--font-sans));
|
||||
--font-mono: var(--font-source-code-pro), 'Source Code Pro', ui-monospace, Menlo, monospace;
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* added to get max-w-site */
|
||||
--container-site: 128rem;
|
||||
/* font sizing and weights optimized for Inter */
|
||||
--text-sm: 0.8125rem;
|
||||
--text-base: 0.9375rem;
|
||||
--text-lg: 1rem;
|
||||
--text-xl: 1.125rem;
|
||||
--text-2xl: 1.375rem;
|
||||
--text-3xl: 1.75rem;
|
||||
--text-4xl: 2.125rem;
|
||||
--text-5xl: 2.875rem;
|
||||
--text-6xl: 3.625rem;
|
||||
--text-7xl: 4.375rem;
|
||||
--text-8xl: 5.875rem;
|
||||
--text-9xl: 7.875rem;
|
||||
--font-weight-normal: 450;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -30,9 +51,7 @@
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
@@ -40,13 +59,35 @@
|
||||
@apply scroll-smooth;
|
||||
}
|
||||
body {
|
||||
@apply bg-default text-foreground;
|
||||
@apply bg-default text-foreground font-normal;
|
||||
/* font-feature-settings: "rlig" 1, "calt" 1; */
|
||||
font-synthesis-weight: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code,
|
||||
.code-content,
|
||||
pre,
|
||||
kbd,
|
||||
samp,
|
||||
.font-mono {
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 1.875rem;
|
||||
--text-4xl: 2.25rem;
|
||||
--text-5xl: 3rem;
|
||||
--text-6xl: 3.75rem;
|
||||
--text-7xl: 4.5rem;
|
||||
--text-8xl: 6rem;
|
||||
--text-9xl: 8rem;
|
||||
--font-weight-normal: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@@ -76,6 +117,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
h1:not(.font-mono),
|
||||
h2:not(.font-mono),
|
||||
h3:not(.font-mono),
|
||||
h4:not(.font-mono),
|
||||
h5:not(.font-mono),
|
||||
h6:not(.font-mono),
|
||||
.h1:not(.font-mono),
|
||||
.h2:not(.font-mono),
|
||||
.h3:not(.font-mono),
|
||||
.h4:not(.font-mono),
|
||||
.h5:not(.font-mono),
|
||||
.h6:not(.font-mono) {
|
||||
@apply font-heading font-semibold;
|
||||
}
|
||||
|
||||
.rdg-cell {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
@@ -22,5 +22,5 @@ NEXT_PUBLIC_MARKETPLACE_API_URL="https://fgxbxpvumhvzrhqngsyu.supabase.co"
|
||||
NEXT_PUBLIC_MARKETPLACE_PUBLISHABLE_KEY="sb_publishable_VuF5ZvGqj6ODhZgN1J_vMw_YbiEs1R6"
|
||||
|
||||
# Supabase project containing docs content information
|
||||
NEXT_PUBLIC_MARKETPLACE_API_URL="https://otqhrpbxhxkrhrnjqbba.supabase.co/"
|
||||
NEXT_PUBLIC_MARKETPLACE_PUBLISHABLE_KEY="sb_publishable_ZVVKKu1s88KsSBWVYlou-g_phb2OJVQ"
|
||||
NEXT_PUBLIC_SUPABASE_URL="https://otqhrpbxhxkrhrnjqbba.supabase.co/"
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY="sb_publishable_ZVVKKu1s88KsSBWVYlou-g_phb2OJVQ"
|
||||
+11
-2
@@ -29,8 +29,11 @@ yarn-error.log*
|
||||
public/sitemap.xml
|
||||
# Per-source llms files (generated by build:llms, served by www)
|
||||
public/llms/
|
||||
# Generated guide and reference markdown files
|
||||
public/markdown/
|
||||
# Generated guide and reference markdown files. manifest.json is committed
|
||||
# with a placeholder empty array so middleware.ts's import always resolves,
|
||||
# even when build:markdown hasn't run (e.g. in local dev, which skips it).
|
||||
public/markdown/*
|
||||
!public/markdown/manifest.json
|
||||
public/docs.tar.gz
|
||||
public/docs/
|
||||
|
||||
@@ -44,6 +47,12 @@ public/docs/
|
||||
# scripts/federated-content/fetch-federated-content.ts
|
||||
/content/guides/graphql/
|
||||
/content/guides/graphql.mdx
|
||||
/content/guides/deployment/terraform.mdx
|
||||
/content/guides/deployment/terraform/tutorial.mdx
|
||||
/content/guides/deployment/ci/
|
||||
/content/guides/ai/python/
|
||||
/content/guides/database/extensions/wrappers/*
|
||||
!/content/guides/database/extensions/wrappers/overview.mdx
|
||||
|
||||
# Downloaded TypeDoc dumps under spec/reference/<lib>/<ver>/. Regenerated by
|
||||
# `cd apps/docs/spec && make download.tsdoc.v2`. Hand-authored files in the
|
||||
|
||||
+107
-79
@@ -8,13 +8,14 @@ Here are some general guidelines on writing docs for Supabase.
|
||||
|
||||
## General principles
|
||||
|
||||
Docs should be helpful, quick to read, and easy to understand. We have an audience of global readers who speak different native languages.
|
||||
Write helpful, concise, and understandable documentation. We have a global audience whose members speak different native languages.
|
||||
|
||||
To make docs as clear as possible:
|
||||
|
||||
- Write for the user. Think about what task they want to complete by reading your doc. Tell them what, and only what, they need to know.
|
||||
- Write like you talk. Use words and sentences that sound natural when speaking. Cut unnecessary words. Read your writing out loud to help you choose the clearest and simplest phrases.
|
||||
- Each paragraph should have one topic only. Start a new paragraph whenever you change the topic. Don't worry about paragraphs being too short.
|
||||
- Write like you talk. Conversational English is easier for a global audience to understand and localize. Many readers who use English as an additional language learn conversational rather than academic English. Use words and sentences that sound natural when speaking. Cut unnecessary words. Read your writing out loud to help you choose the clearest and simplest phrases.
|
||||
- Prefer short, direct sentences. Express one relationship at a time, and avoid unnecessary compound structures. This makes each sentence easier to understand, localize, and interpret consistently.
|
||||
- Cover one topic in each paragraph. Start a new paragraph whenever you change the topic. Don't worry about paragraphs being too short.
|
||||
- Avoid using idioms and colloquialisms, such as `piece of cake`. These phrases are often specific to a region or culture.
|
||||
- Refer to the reader as `you`. Don't use `we` to refer to the reader. Use `we` only to refer to the Supabase team.
|
||||
|
||||
@@ -31,7 +32,7 @@ Explainers help the reader to learn a topic. They are conceptual and mostly pros
|
||||
- Some examples of _when_ to use it
|
||||
- A high-level explanation of _how_ it works
|
||||
|
||||
They shouldn't include:
|
||||
Explainers don't include:
|
||||
|
||||
- Instructions on how to use it
|
||||
|
||||
@@ -39,7 +40,7 @@ They shouldn't include:
|
||||
|
||||
Tutorials are goal-oriented. They help a reader to finish a large, complex goal, such as setting up a web app that uses multiple Supabase features.
|
||||
|
||||
Tutorials mix prose explanations with procedures (lists of steps for the reader to follow). They provide context for why certain instructions are given.
|
||||
Tutorials mix prose explanations with procedures. Procedures are lists of steps for the reader to follow. Tutorials provide context for why certain instructions are given.
|
||||
|
||||
For inspiration, see [an example of a tutorial](https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs).
|
||||
|
||||
@@ -47,22 +48,35 @@ For inspiration, see [an example of a tutorial](https://supabase.com/docs/guides
|
||||
|
||||
Guides are also goal-oriented, but they focus on shorter, more targeted tasks. For example, a guide might explain how to set up user login for an app.
|
||||
|
||||
Guides contain mostly procedures. Think of an instruction manual for building a desk: it's a list of concise steps that the user can go through quickly.
|
||||
Guides contain mostly procedures: concise steps that readers can follow in sequence.
|
||||
|
||||
For inspiration, see [an example of a guide](https://supabase.com/docs/guides/auth/auth-email).
|
||||
Begin each guide with a sentence that declares its intent, such as `This guide explains how to set up email login.` This helps readers and agents confirm that the guide matches their goal and expected outcome.
|
||||
|
||||
Keep procedures focused on what the reader must do. Move substantial background or conceptual explanations into a separate section or an explainer. Cross-reference the authoritative explanation instead of repeating it in the procedure. This keeps the action path scannable, gives readers optional depth, and maintains one source of truth.
|
||||
|
||||
- Recommended: `This guide explains how to enable Row Level Security. To learn how Row Level Security controls access, see [Row Level Security](...).`
|
||||
- Not recommended: Begin with several paragraphs about how Row Level Security works before stating what the guide helps the reader do.
|
||||
|
||||
**Mixed information types:** When a guide contains substantial context or reference material, group sections by information type. Keep contextual and reference sections separate from the procedure group so that background information doesn't interrupt the action path.
|
||||
|
||||
**Navigation:** Begin a long guide with a short outline of its major section groups. Link to each group and state when a reader should use it. Don't add section navigation to a short guide when the headings are already easy to scan.
|
||||
|
||||
**Cross-references and glue:** Connect contextual sections to their corresponding procedures when the relationship helps readers navigate. Add a brief introduction to each section group, a transition when the information type changes, and an outcome after a procedure. Add links selectively rather than linking every adjacent section.
|
||||
|
||||
For inspiration, see [an example of a guide](/docs/guides/auth/auth-email-passwordless).
|
||||
|
||||
### Reference
|
||||
|
||||
References are factual and to the point. Think of dictionary entries.
|
||||
|
||||
They should include:
|
||||
References include:
|
||||
|
||||
- Function parameters
|
||||
- Return types
|
||||
- Code samples
|
||||
- Warnings for critical errors (for example, missteps that can cause data loss)
|
||||
- Warnings about critical errors, such as missteps that can cause data loss
|
||||
|
||||
They shouldn't include:
|
||||
References don't include:
|
||||
|
||||
- Explanations of the context for a feature
|
||||
- Examples of use cases
|
||||
@@ -72,7 +86,7 @@ They shouldn't include:
|
||||
|
||||
Most docs pages are contained in the `apps/docs/content` directory. Some docs sections are federated from other repositories, for example [`pg_graphql`](https://github.com/supabase/pg_graphql/tree/master/docs). Reference docs are generated from spec files in the `spec` directory.
|
||||
|
||||
You can usually identify a federated or reference doc because it uses a Next.js dynamic route (for example, `[[...slug]].tsx`). Look for the spec file import or the repo definition to find the content location.
|
||||
You can usually identify a federated or reference doc because it uses a Next.js dynamic route. For example, it might use `[[...slug]].tsx`. Look for the spec file import or the repo definition to find the content location.
|
||||
|
||||
Example spec file import:
|
||||
|
||||
@@ -94,12 +108,12 @@ Check the sections for [guide structure](#guide-structure) and [reference struct
|
||||
|
||||
## Guide structure
|
||||
|
||||
The Supabase docs use [MDX](https://mdxjs.com/). Guides are written in unstructured prose as MDX documents.
|
||||
The Supabase docs use [MDX](https://mdxjs.com/). Guides are MDX documents that combine concise prose with structured procedures.
|
||||
|
||||
Adding a new guide requires:
|
||||
|
||||
- YAML frontmatter
|
||||
- A navigation entry (in a separate file)
|
||||
- A navigation entry in a separate file
|
||||
|
||||
Frontmatter looks like this. `title` is mandatory. There are also optional properties that you can use to control the page display, including `subtitle`, `tocVideo`, and `hideToc`.
|
||||
|
||||
@@ -112,7 +126,7 @@ hideToc: true
|
||||
|
||||
The navigation is defined in [`NavigationMenu.constants.ts`](https://github.com/supabase/supabase/blob/master/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts).
|
||||
|
||||
Add an entry with the `name`, `url`, and (optional) `icon` for your page.
|
||||
Add an entry with the `name`, `url`, and optional `icon` for your page.
|
||||
|
||||
## Reference structure
|
||||
|
||||
@@ -120,13 +134,13 @@ Reference docs are produced from the reference specs and library source code. A
|
||||
|
||||
### Common spec file
|
||||
|
||||
Each type of library (for example, language SDK or CLI) has a common spec file. For example, see the [spec file for the language SDKs](https://github.com/supabase/supabase/blob/master/apps/docs/spec/common-client-libs-sections.json). This file contains definitions for the common SDK functions:
|
||||
Each type of library, such as a language SDK or CLI, has a common spec file. For example, see the [spec file for the language SDKs](https://github.com/supabase/supabase/blob/master/apps/docs/spec/common-client-libs-sections.json). This file contains definitions for the common SDK functions:
|
||||
|
||||
- **id** - Identifies the function
|
||||
- **title** - Human-readable title
|
||||
- **slug** - URL slug
|
||||
- **product** - Supabase product that owns the function. For example, database operations are owned by `database`, and auth functions are owned by`auth`
|
||||
- **type** - `function` for a structured function definition or `markdown` for a prose explainer section.
|
||||
- `id`: Identifies the function
|
||||
- `title`: Provides the human-readable title
|
||||
- `slug`: Provides the URL slug
|
||||
- `product`: Identifies the Supabase product that owns the function. For example, database operations are owned by `database`, and Auth operations are owned by `auth`.
|
||||
- `type`: Uses `function` for a structured function definition or `markdown` for a prose explainer section
|
||||
|
||||
To add a new function, manually add an entry to this common file.
|
||||
|
||||
@@ -140,10 +154,10 @@ Each function contains a description, code examples, and optional notes. The par
|
||||
|
||||
If you're a library maintainer, follow these steps when updating function parameters or return values:
|
||||
|
||||
1. Get your changes merged to `master` in your library
|
||||
2. This will kick off an action that automatically updates the spec file in the library's `gh-pages` branch
|
||||
3. Run `make` in `/spec` of the `supabase/supabase` repo. This will regenerate all of the `tsdoc` files that the docs site uses
|
||||
4. You should now see the changes you've made in the docs site locally
|
||||
1. Merge your changes into the library's `master` branch.
|
||||
2. Wait for the action to update the specification in the `gh-pages` branch.
|
||||
3. Run `make` from `apps/docs/spec` in the `supabase/supabase` repository.
|
||||
4. Verify the changes on your local documentation site.
|
||||
|
||||
## Content reuse
|
||||
|
||||
@@ -153,23 +167,31 @@ To use a partial, import it into your MDX file. You can also set up a partial to
|
||||
|
||||
## Components and elements
|
||||
|
||||
Docs include normal Markdown elements such as lists, and custom components such as admonitions (callouts).
|
||||
Docs include normal Markdown elements such as lists and custom components such as admonitions, also known as callouts.
|
||||
|
||||
Here are some guidelines for using elements:
|
||||
|
||||
### Admonitions
|
||||
|
||||
Admonitions (or callouts) draw reader attention to an important point or an aside. They highlight important information, but get less effective if they're overused.
|
||||
Admonitions draw reader attention to an important point or an aside. They highlight important information, but get less effective if they're overused.
|
||||
|
||||
Use admonitions sparingly. Don't stack them on top of each other.
|
||||
Use an admonition when a reader might otherwise miss information that affects the outcome of their task, or when you want to separate helpful but optional guidance from the main flow. Don't use an admonition for information that belongs in the main explanation or procedure.
|
||||
|
||||
Use admonitions sparingly. Don't stack them on top of each other or use them as decoration.
|
||||
|
||||
Begin every admonition with its impact and purpose: the "so what." Use the first sentence to tell the reader why the information matters, such as what could happen, what changes, or what benefit they gain. Add background or instructions after the impact is clear.
|
||||
|
||||
For example:
|
||||
|
||||
- Recommended: `Deleting this project permanently removes its database and backups. Export any data that you want to keep before you continue.`
|
||||
- Not recommended: `Before you continue, there are a few things that you should know about project deletion.`
|
||||
|
||||
Choose the appropriate `type` for your admonition:
|
||||
|
||||
- `danger` to warn the user about any missteps that could cause data loss or data leaks
|
||||
- `deprecation` to notify the user about features that are (or will soon be) deprecated
|
||||
- `caution` to warn about anything that could cause a bug or serious user inconvenience
|
||||
- `tip` to point out helpful but optional actions
|
||||
- `note` for anything else
|
||||
- `danger`: Warn about actions or conditions that could cause data loss, expose sensitive data, or create another severe and difficult-to-reverse outcome. State the consequence first, and then explain how to avoid it.
|
||||
- `deprecation`: Identify a deprecated feature or behavior. State how the change affects the reader, and then provide the supported alternative or migration path.
|
||||
- `caution`: Warn about behavior that could cause bugs, failed operations, unexpected results, or serious inconvenience but doesn't rise to the severity of `danger`.
|
||||
- `note`: Highlight an important prerequisite, constraint, clarification, or optional shortcut that doesn't represent a risk. If the information is essential to completing a step, include it in the procedure instead.
|
||||
|
||||
```
|
||||
<Admonition type="note" title="Optional title">
|
||||
@@ -189,13 +211,13 @@ Keep code lines short to avoid scrolling. For example, you can split long shell
|
||||
|
||||
- **JavaScript/TypeScript**
|
||||
|
||||
The `supabase` repo uses Prettier, which also formats JS/TS in code blocks. Your PR is blocked from merging if the Prettier check fails. Ensure that your code blocks are formatted by running `npm run format`, or by setting up auto-formatting in your IDE.
|
||||
The `supabase` repository uses Prettier, which also formats JS/TS in code blocks. Your PR is blocked from merging if the Prettier check fails. From the repository root, run `pnpm format`, or set up automatic formatting in your IDE.
|
||||
|
||||
- **SQL**
|
||||
|
||||
Prefer lowercase for SQL. For example, `select * from table` rather than `SELECT * FROM table`.
|
||||
|
||||
Optionally specify a filename for the codeblock by including it after the opening backticks and language specifier:
|
||||
Optionally specify a filename for the code block by including it after the opening backticks and language specifier:
|
||||
|
||||
````md
|
||||
```ts environment.ts
|
||||
@@ -211,6 +233,16 @@ Optionally highlight lines by using `mark=${lineNumber}`.
|
||||
```
|
||||
````
|
||||
|
||||
### Emphasis
|
||||
|
||||
Use **bold**, _italics_, and `code` formatting for distinct purposes. Don't use them interchangeably or to add visual emphasis alone.
|
||||
|
||||
- **Bold**: Mark UI labels the reader interacts with, such as buttons, menu items, and field names. For example, `Click **Save**.` Also use bold for a term the reader must not miss, such as `**Never** commit your service role key.`
|
||||
- _Italics_: Introduce a new term the first time you define it, or reference a title, such as a book or a third-party product name written in italics by convention. Use italics sparingly. Don't use italics for UI labels or for general emphasis.
|
||||
- `Code`: Mark anything the reader types or copies verbatim, or anything the system reads literally. This includes filenames, paths, commands, flags, environment variables, function and parameter names, configuration keys, and literal values. For example, `` Set `SUPABASE_URL` in your `.env` file. ``
|
||||
|
||||
If a phrase fits more than one category, pick the most specific one. A command name is `code`, not **bold**, even though the reader also interacts with it.
|
||||
|
||||
### Content listings
|
||||
|
||||
Overview and index pages use a single `<ContentListings id="..." />` component for curated link sections such as "Get started", "Next steps", "Examples", or "Resources". Refer to [`storage.data.ts`](data/content-listings/storage.data.ts) and [`storage.mdx`](content/guides/storage.mdx) for a full example.
|
||||
@@ -218,7 +250,7 @@ Overview and index pages use a single `<ContentListings id="..." />` component f
|
||||
**Prompt to add content listings:**
|
||||
|
||||
```text
|
||||
Add a content listing block for [TOPIC] / [SECTION] (for example, Storage / Examples).
|
||||
Add a content listing block for [TOPIC] / [SECTION]. For example, use Storage / Examples.
|
||||
Follow CONTRIBUTING § Content listings in apps/docs.
|
||||
Copy structure from `storageGetStarted` in apps/docs/data/content-listings/storage.data.ts.
|
||||
Pick a globally-unique kebab-case id like `[topic]-[section]`.
|
||||
@@ -227,11 +259,11 @@ Run `pnpm test:local lib/content-listings.test.ts` from apps/docs.
|
||||
|
||||
**Manually add content listings:**
|
||||
|
||||
1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups (e.g. `storage-get-started`, not just `get-started`) — it is used both as the lookup key and as the telemetry `listingId`.
|
||||
2. Place the component inline in guide MDX, for example `<ContentListings id="storage-get-started" />`. Use a partial only when the block is reused or gated with `$Show` at the partial level.
|
||||
1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups. For example, use `storage-get-started` rather than `get-started`. The ID is both the lookup key and the telemetry `listingId`.
|
||||
2. Place the component inline in guide MDX, for example `<ContentListings id="storage-get-started" />`. Use a partial only when the block is reused or gated with `$Show` at the partial level. For individual items that depend on a feature flag (for example `sdk:dart`), set `feature` on the item instead of wrapping the whole listing.
|
||||
3. Run `pnpm test:local lib/content-listings.test.ts` from `apps/docs`.
|
||||
|
||||
Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets): `cl-data` (data export with namespaced id) and `cl-inline` (MDX component).
|
||||
Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets). Use `cl-data` for a data export with a namespaced ID. Use `cl-inline` for an MDX component.
|
||||
|
||||
|
||||
### Footnotes
|
||||
@@ -240,7 +272,7 @@ Don't use footnotes.
|
||||
|
||||
### Graphs
|
||||
|
||||
Render diagrams (flowcharts, sequence diagrams, entity-relationship diagrams, etc.) by writing a fenced code block with `mermaid` as the language. The MDX renderer routes these blocks through the shared `Mermaid` component, so theming follows light/dark mode automatically.
|
||||
Render diagrams, including flowcharts, sequence diagrams, and entity-relationship diagrams, by writing a fenced code block with `mermaid` as the language. The MDX renderer routes these blocks through the shared `Mermaid` component, so theming follows light and dark mode automatically.
|
||||
|
||||
For the full list of supported diagram types and their syntax, see the [official Mermaid diagram reference](https://mermaid.js.org/intro/syntax-reference.html).
|
||||
|
||||
@@ -259,7 +291,7 @@ sequenceDiagram
|
||||
```
|
||||
````
|
||||
|
||||
Flowchart (`flowchart` accepts a direction like `LR`, `TD`, etc.):
|
||||
The `flowchart` keyword accepts a direction such as `LR` or `TD`:
|
||||
|
||||
````mdx
|
||||
```mermaid
|
||||
@@ -273,9 +305,9 @@ flowchart LR
|
||||
|
||||
A few tips:
|
||||
|
||||
- Use the standard Mermaid diagram keywords (`sequenceDiagram`, `flowchart`, `erDiagram`, etc.) on the first line of the block.
|
||||
- Use a standard Mermaid diagram keyword, such as `sequenceDiagram`, `flowchart`, or `erDiagram`, on the first line of the block.
|
||||
- Keep diagrams focused on a single flow or concept. If a diagram gets too dense, split it into multiple smaller diagrams.
|
||||
- Wrap node labels that contain special characters (`*`, `/`, spaces, punctuation) in double quotes, for example `A["content/**/*.md"]`.
|
||||
- Wrap node labels that contain special characters in double quotes. Special characters include `*`, `/`, spaces, and punctuation. For example, use `A["content/**/*.md"]`.
|
||||
- Don't hardcode colors. The component themes the diagram automatically so it matches both light and dark mode.
|
||||
- Use diagrams to support the prose, not replace it. Explain the key takeaway in text near the diagram.
|
||||
|
||||
@@ -283,17 +315,34 @@ A few tips:
|
||||
|
||||
Images are uploaded in the `apps/docs/public/img` folder.
|
||||
|
||||
For vector illustrations, use `svg`. For screenshots and non-vector graphics, use `png`. (These are automatically converted to `webp` for supported browsers.)
|
||||
For vector illustrations, use `.svg` files. For screenshots and non-vector graphics, use `.png` files. Supported browsers receive `.webp` versions automatically.
|
||||
|
||||
Redact any sensitive information, such as API keys.
|
||||
|
||||
### Links
|
||||
|
||||
Link text should be descriptive. The reader should understand where the link goes from reading the link text alone. This is important for accessibility. For example, don't use `here` as link text.
|
||||
Use descriptive link text that tells the reader where the link goes. This is important for accessibility. For example, don't use `here` as link text.
|
||||
|
||||
But link text shouldn't be too long. Use the shortest part of the link that is descriptive enough. For example, `see the [reference section](/link)` rather than `[see the reference section](/link)`.
|
||||
Keep link text concise. Use the shortest part of the link that is descriptive enough. For example, `see the [reference section](/link)` rather than `[see the reference section](/link)`.
|
||||
|
||||
Use relative links when linking within the `supabase.com` domain. For example, `[link to another page in Supabase docs](/docs/guides/getting-started)`.
|
||||
Don't include the `https://supabase.com` origin when linking to pages on `supabase.com`. Use a `/docs/...` path for a page in Supabase docs, such as `[getting started](/docs/guides/getting-started)`. Use a site-root path for a page outside docs, such as `[open the Supabase Dashboard](/dashboard)`.
|
||||
|
||||
### Procedures
|
||||
|
||||
Use a procedure when a human or agent must perform actions to reach an outcome. The procedural format makes that expectation explicit.
|
||||
|
||||
Write sequential actions as an ordered list. Begin each step with an imperative verb, and include one action or a closely related set of actions per step. Give the reader enough context to know where to act.
|
||||
|
||||
Apply the [Information Mapping chunking principle](https://informationmapping.com/blogs/news/writing-for-the-web-the-magical-number-seven-plus-or-minus-two) to procedures. Present 7 ± 2 related steps at a time. This gives readers a manageable chunk of five to nine actions. Aim for the lower end of the range when the task is complex or unfamiliar.
|
||||
|
||||
If a procedure has more than nine steps, group related steps into named phases or smaller procedures. If one step contains multiple distinct actions, split it into separate steps. Don't add steps to reach a minimum. The range is a guideline for organizing information, not a required procedure length.
|
||||
|
||||
An apparent one-step procedure can become two steps when there is a real orientation action. For example:
|
||||
|
||||
1. Open a terminal in your project directory.
|
||||
2. Run `supabase start`.
|
||||
|
||||
The first step establishes the operating context for both readers and agents. Don't add a redundant orientation step to a genuinely atomic instruction. For example, write `Click **Save**.` instead of adding `Locate the **Save** button` as a separate step.
|
||||
|
||||
### Lists
|
||||
|
||||
@@ -319,7 +368,7 @@ Don't nest lists more than two deep.
|
||||
|
||||
Use tabs to provide alternative instructions for different platforms or languages.
|
||||
|
||||
The `queryGroup` param is optional. It lets you link directly to a tab by using the query group as a query param in the URL, for example: `https://supabase.com/docs/my-page?packagemanager=ts`
|
||||
The optional `queryGroup` prop lets you link directly to a tab. For this example, use `/docs/my-page?packagemanager=npm`.
|
||||
|
||||
```
|
||||
<Tabs
|
||||
@@ -344,59 +393,38 @@ The `queryGroup` param is optional. It lets you link directly to a tab by using
|
||||
|
||||
### Videos
|
||||
|
||||
Include videos as TOC (Table of Contents) videos rather than putting them in the main text.
|
||||
Include videos as table of contents (TOC) videos instead of placing them in the main text.
|
||||
|
||||
You can define a TOC video in the page frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
tocVideo: 'rzglqRdZUQE',
|
||||
tocVideo: 'rzglqRdZUQE'
|
||||
---
|
||||
```
|
||||
|
||||
## Styling, formatting, and grammar
|
||||
|
||||
Don't worry too much about grammar rules. Grammar is useful if, and only if, it makes your writing clearer. For example, you can use sentence fragments if they're self-explanatory.
|
||||
Grammar is useful when it makes your writing clearer. Use complete sentences by default because they identify the actor and action. This reduces ambiguity for readers, translators, and agents. Use sentence fragments only where they improve scanning, such as headings, labels, or short list items.
|
||||
|
||||
Headings guide the reader's eye and organize the page, but they don't carry information by themselves. Make the content beneath a heading understandable without relying on the heading. The first sentence can restate the heading, even if it sounds redundant. Readers often skim headings and then return to the section that interests them, so use the opening sentence to confirm the context.
|
||||
|
||||
Don't use parentheses for asides or supplementary information. Rewrite that information as part of the sentence or as a separate sentence. Use parentheses to introduce an acronym after spelling out its meaning, such as full-text search (FTS), or to mark an item as `(Optional)`. Parentheses that are required by Markdown links or code syntax aren't prose parentheticals.
|
||||
|
||||
That said, a few rules help keep the docs concise, consistent, and clear:
|
||||
|
||||
- Format headings in sentence case. Capitalize the first word and any proper nouns. All other words are lowercase. For example, `Set up authentication` rather than `Set Up Authentication`.
|
||||
- Use the Oxford comma (a comma before the `and` that marks the last item in a list). For example, `realtime, database, and authentication` rather than `realtime, database and authentication`.
|
||||
- Use the Oxford comma. Place a comma before the `and` that marks the last item in a list. For example, use `functions, tables, and indexes` rather than `functions, tables and indexes`.
|
||||
- Use the present tense as much as possible. For example, `the AI assistant answers your question` rather than `the AI assistant will answer your question`.
|
||||
|
||||
## Word usage and spelling
|
||||
|
||||
Use American English. If in doubt, consult the [Merriam-Webster dictionary](https://www.merriam-webster.com/).
|
||||
|
||||
Here are some exceptions and Supabase-specific guidelines.
|
||||
|
||||
### General word usage
|
||||
|
||||
- **Filler words**: You can often make your writing more concise by removing these words. (Some of these words can also sound patronizing.) Most filler, marketing, and vague-verb phrases are flagged by `supa-mdx-lint` as warnings, with suggested alternatives where a direct replacement exists. Run `pnpm lint:mdx` in `apps/docs` to check your changes.
|
||||
- Actually
|
||||
- Easy, easily
|
||||
- Just
|
||||
- Let's
|
||||
- Please
|
||||
- Simple, simply
|
||||
- **UI elements**
|
||||
- Buttons are `click`ed.
|
||||
- Checkboxes are `select`ed.
|
||||
- Toggles are `enable`d and `disable`d.
|
||||
- Labels of UI elements are bolded. For example, `Click **Confirm**.`
|
||||
|
||||
### Word list
|
||||
|
||||
- `Frontend` isn't hyphenated (not `front-end`).
|
||||
- `Backend` isn't hyphenated (not `back-end`).
|
||||
- `Login` is a noun. `Log in` is a verb.
|
||||
- `Postgres` is capitalized, except in code, and used instead of `PostgreSQL`.
|
||||
- `Setup` is a noun. `Set up` is a verb.
|
||||
- `Supabase` is capitalized (not `supabase`), except in code.
|
||||
- `Supabase Platform` is in title case (not `Supabase platform`).
|
||||
Follow the [Supabase documentation word list](./WORD_LIST.md) for preferred spelling, capitalization, and usage. The word list includes the terminology rules checked by `supa-mdx-lint`. Run `pnpm lint:mdx` in `apps/docs` to check your changes.
|
||||
|
||||
## Search
|
||||
|
||||
Search is handled using a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/generate-embeddings.ts) aggregates all content sources (for example, guides, reference docs, etc), indexes them using OpenAI embeddings, and stores them in a Supabase database.
|
||||
Search uses a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/generate-embeddings.ts) collects guides, reference documentation, and other content. The script creates OpenAI embeddings and stores the search index in a Supabase database.
|
||||
|
||||
Search uses a hybrid of native Postgres FTS and embedding similarity search based on [`pgvector`](https://github.com/pgvector/pgvector). At runtime, a PostgREST call triggers the RPC that runs the weighted FTS search, and an [Edge Function](https://github.com/supabase/supabase/tree/master/supabase/functions) is executed to perform the embedding search.
|
||||
Search combines native Postgres full-text search (FTS) with embedding similarity search based on [`pgvector`](https://github.com/pgvector/pgvector). At runtime, a PostgREST call invokes the weighted FTS RPC. An [Edge Function](https://github.com/supabase/supabase/tree/master/supabase/functions) runs the embedding search.
|
||||
@@ -17,7 +17,7 @@ For a complete run-down on how all of our tools work together, see the main DEVE
|
||||
[supabase.com/docs](https://supabase.com/docs) is a Next.js site. You can get setup by following the same steps for all of our other Next.js projects:
|
||||
|
||||
1. Follow the steps outlined in the Local Development section of the main [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md)
|
||||
2. If you work at Supabase, run `dev:secrets:pull` to pull down the internal environment variables. If you're a community member, create a `.env` file and add this line to it: `NEXT_PUBLIC_IS_PLATFORM=false`
|
||||
2. If you work at Supabase, from `apps/docs` run `pnpm run dev:secrets:pull` to write internal env vars to `.env.local`. If you're a community member, create `apps/docs/.env.local` and add this line: `NEXT_PUBLIC_IS_PLATFORM=false`
|
||||
3. Start the local docs site by navigating to `/apps/docs` and running `pnpm run dev`
|
||||
4. Visit http://localhost:3001/docs in your browser - don't forget to append the `/docs` to the end
|
||||
5. Your local site should look exactly like [https://supabase.com/docs](https://supabase.com/docs)
|
||||
|
||||
@@ -0,0 +1,920 @@
|
||||
# Supabase documentation word list
|
||||
|
||||
Use this list when you write or review Supabase documentation. It records preferred
|
||||
spelling, capitalization, and usage for terms that commonly appear in developer
|
||||
documentation.
|
||||
|
||||
This list supplements [CONTRIBUTING.md](./CONTRIBUTING.md). If the two documents
|
||||
conflict, follow `CONTRIBUTING.md`. Match literal code, API names, UI labels, and
|
||||
third-party product names even when they differ from this guidance, and format them
|
||||
as code or UI text as appropriate.
|
||||
|
||||
Many unambiguous rules in this list are checked by `supa-mdx-lint`. Run
|
||||
`pnpm lint:mdx` from `apps/docs` after editing MDX. A lint warning still requires
|
||||
judgment: rewrite the sentence instead of applying a replacement that changes its
|
||||
meaning.
|
||||
|
||||
## Numbers and symbols
|
||||
|
||||
### `+`
|
||||
|
||||
Don't use `+` to mean _or later_.
|
||||
|
||||
- Recommended: Postgres 15 or later
|
||||
- Not recommended: Postgres 15+
|
||||
|
||||
### `&`
|
||||
|
||||
Use _and_ instead of `&` in prose, headings, navigation, and tables of contents.
|
||||
Keep `&` when it is part of a UI label, code, or a space-constrained table or
|
||||
diagram label.
|
||||
|
||||
## A
|
||||
|
||||
### abbreviations
|
||||
|
||||
Spell out an unfamiliar abbreviation on first use. Don't expand familiar technical
|
||||
abbreviations such as API, CPU, HTML, HTTP, or SQL unless the audience needs it.
|
||||
|
||||
Use `for example` instead of `e.g.` when practical. If space is constrained, write
|
||||
`e.g.` with both periods. Use `that is` instead of `i.e.`.
|
||||
|
||||
The linter warns about malformed forms of `e.g.` and about `i.e.`.
|
||||
|
||||
### abort
|
||||
|
||||
Use _stop_, _exit_, _cancel_, or _end_ in general prose. Use `abort` when it is the
|
||||
name of a command, signal, API, or operation.
|
||||
|
||||
### above
|
||||
|
||||
Don't use _above_ to refer to a location in a document or UI. Link to or name the
|
||||
section or control. For versions, use _later_.
|
||||
|
||||
### access
|
||||
|
||||
When possible, use a more specific verb such as _view_, _find_, _edit_, _open_, or
|
||||
_use_. Keep _access_ when it accurately describes authorization or connectivity.
|
||||
|
||||
### admin
|
||||
|
||||
Use _administrator_ in prose. Use _admin_ when it is part of a product name, API,
|
||||
role, command, or UI label.
|
||||
|
||||
### AI
|
||||
|
||||
You can use _AI_ without spelling out _artificial intelligence_ when the audience
|
||||
is familiar with the term.
|
||||
|
||||
### allowlist and denylist
|
||||
|
||||
Use _allowlist_ and _denylist_ as nouns. Prefer a precise verb that describes the
|
||||
action instead of using either term as a verb.
|
||||
|
||||
- Recommended: Allow requests from the IP address.
|
||||
- Recommended: Add the IP address to the allowlist.
|
||||
- Not recommended: Allowlist the IP address.
|
||||
|
||||
Don't use _blacklist_ or _whitelist_. The linter reports these terms as errors.
|
||||
When a literal code item contains one of them, format the item as code and explain
|
||||
what it does.
|
||||
|
||||
### allows you to
|
||||
|
||||
Use _lets you_, or make the reader the subject of the sentence.
|
||||
|
||||
- Recommended: You can query the table.
|
||||
- Recommended: The API lets you query the table.
|
||||
- Not recommended: The API allows you to query the table.
|
||||
|
||||
### alpha and beta
|
||||
|
||||
Use lowercase when describing a release stage. Preserve capitalization when it is
|
||||
part of an official product name.
|
||||
|
||||
### among and between
|
||||
|
||||
Use _between_ for distinct items, even when there are more than two. Use _among_
|
||||
for members of a group or items that aren't distinct.
|
||||
|
||||
### and/or
|
||||
|
||||
Rewrite to use _and_, _or_, or explicitly state that either or both apply.
|
||||
|
||||
### API
|
||||
|
||||
Use _API_ for a web API or a language-specific API. Don't use _API_ to mean an
|
||||
individual method, function, class, or endpoint.
|
||||
|
||||
### app and application
|
||||
|
||||
Use _app_ for web and mobile software intended for end users. Use _application_
|
||||
when it is part of an established term, such as _application programming
|
||||
interface_, or when the distinction is technically useful.
|
||||
|
||||
### as and since
|
||||
|
||||
Use _because_ when you mean causation. _As_ and _since_ can be mistaken for
|
||||
references to time.
|
||||
|
||||
### authentication and authorization
|
||||
|
||||
Authentication verifies an identity. Authorization determines what an
|
||||
authenticated identity can access or do. Don't use the terms interchangeably.
|
||||
|
||||
Avoid _authN_ and _authZ_ in prose. Use _authentication_ and _authorization_.
|
||||
|
||||
### auto-
|
||||
|
||||
Follow the spelling established by the relevant technology. Common closed forms
|
||||
include _autoscaling_, _autofill_, and _autogenerate_. Don't invent a hyphenated
|
||||
variation when an established form exists.
|
||||
|
||||
## B
|
||||
|
||||
### backend
|
||||
|
||||
Write _backend_, not _back-end_ or _back end_.
|
||||
|
||||
### base64
|
||||
|
||||
Use _base64_ in general prose. Use the capitalization required by a formal name or
|
||||
literal code item.
|
||||
|
||||
### below
|
||||
|
||||
Don't use _below_ to refer to a location in a document or UI. Link to or name the
|
||||
section or control. For versions, use _earlier_.
|
||||
|
||||
### black-box, gray-box, and white-box
|
||||
|
||||
Prefer a description of what the monitoring or testing method can observe. If the
|
||||
established term is necessary, define it on first use.
|
||||
|
||||
### boolean
|
||||
|
||||
Use the spelling and capitalization of the programming-language type when
|
||||
referring to code. Use lowercase _boolean_ for the abstract data type and uppercase
|
||||
_Boolean_ for Boolean logic.
|
||||
|
||||
### button
|
||||
|
||||
Use _button_ only for an element that is actually a button. In desktop
|
||||
instructions, users _click_ a button. Preserve the exact button label and format
|
||||
it in bold.
|
||||
|
||||
## C
|
||||
|
||||
### can, may, might, must, and should
|
||||
|
||||
- Use _can_ for ability, permission, or an optional action.
|
||||
- Use _might_ for possibility or an uncertain outcome.
|
||||
- Reserve _may_ for policy or legal guidance when possible.
|
||||
- Use _must_ or _need to_ for a requirement.
|
||||
- Avoid ambiguous _should_. State whether an action is required, recommended, or
|
||||
optional.
|
||||
|
||||
### checkboxes
|
||||
|
||||
Users _select_ and _clear_ checkboxes. Don't use _check_, _uncheck_, or _deselect_
|
||||
for these actions.
|
||||
|
||||
### click
|
||||
|
||||
Use _click_ for buttons, links, and other controls in a desktop interface. Don't
|
||||
write _click on_. Use _tap_ when the environment is specifically a touch
|
||||
interface.
|
||||
|
||||
### click here
|
||||
|
||||
Don't use _click here_ or _here_ as link text. Describe the destination or action.
|
||||
|
||||
### client
|
||||
|
||||
In API documentation, a _client_ is usually an app that sends requests. Don't use
|
||||
_client_ as an abbreviation for _client library_ when that could be ambiguous.
|
||||
|
||||
Use _concurrent connections_, not _concurrent clients_, when discussing database
|
||||
connections. The linter checks this usage.
|
||||
|
||||
### codebase
|
||||
|
||||
Write _codebase_, not _code base_.
|
||||
|
||||
### command-line interface
|
||||
|
||||
Name the specific interface, such as _Supabase CLI_. Use _CLI_ after the name is
|
||||
clear.
|
||||
|
||||
### config
|
||||
|
||||
Use _configuration_ in general prose. Keep _config_ when referring to a literal
|
||||
file, command, property, or established technical name.
|
||||
|
||||
### console and dashboard
|
||||
|
||||
Use the product's official name. Don't use _console_ and _dashboard_
|
||||
interchangeably, and don't call a UI a dashboard unless it presents a dashboard.
|
||||
Use _Supabase Dashboard_ for the Supabase product.
|
||||
|
||||
### currently
|
||||
|
||||
Avoid _currently_ when the sentence describes the product's present behavior.
|
||||
State the behavior directly.
|
||||
|
||||
## D
|
||||
|
||||
### data
|
||||
|
||||
Treat _data_ as a singular mass noun: _the data is_ and _less data_.
|
||||
|
||||
### data center
|
||||
|
||||
Write _data center_, not _datacenter_.
|
||||
|
||||
### data source
|
||||
|
||||
Use _data source_ in prose. Preserve `datasource` when it is a code item or
|
||||
official product term.
|
||||
|
||||
### data type
|
||||
|
||||
Write _data type_, not _datatype_.
|
||||
|
||||
### deprecate
|
||||
|
||||
Use _deprecated_ when use is discouraged, usually because support will end. Don't
|
||||
use it to mean _removed_, _deleted_, or _unavailable_.
|
||||
|
||||
### dialog
|
||||
|
||||
Use _dialog_ for a UI element that presents information or asks for input. Don't
|
||||
use _dialogue_ or _popup_.
|
||||
|
||||
### directory and folder
|
||||
|
||||
Use _directory_ in command-line contexts and _folder_ in graphical interfaces.
|
||||
Match the product UI when it uses a specific term.
|
||||
|
||||
### disable
|
||||
|
||||
Use _disable_ or _turn off_ for an available feature or option. Don't use
|
||||
_disabled_ to mean that something is broken or unavailable.
|
||||
|
||||
### display
|
||||
|
||||
_Display_ is a transitive verb and requires an object.
|
||||
|
||||
- Recommended: The Dashboard displays the query results.
|
||||
- Recommended: The query results appear.
|
||||
- Not recommended: The query results display.
|
||||
|
||||
### docs
|
||||
|
||||
Use _documentation_ in prose. Use _docs_ in informal contributor instructions,
|
||||
repository paths, URLs, or established product names.
|
||||
|
||||
### dropdown
|
||||
|
||||
Prefer the specific control name, such as _list_ or _menu_. Use _dropdown_ only
|
||||
when the distinction matters, and don't use _drop-down_.
|
||||
|
||||
### dummy
|
||||
|
||||
Don't use _dummy_ for placeholders or sample values. Use _placeholder_, _sample_,
|
||||
or a name that describes the value's role. For the statistical concept commonly
|
||||
called a dummy variable, use _indicator variable_ or another established,
|
||||
context-appropriate term.
|
||||
|
||||
## E
|
||||
|
||||
### easy, quick, and simple
|
||||
|
||||
Avoid claiming that a task is _easy_, _quick_, or _simple_. These words can be
|
||||
subjective and usually add no information. The linter warns about _easy_,
|
||||
_easily_, _quickly_, _simple_, and _simply_.
|
||||
|
||||
### email
|
||||
|
||||
Write _email_, not _e-mail_. Don't use _email_ as a verb; use _send email_.
|
||||
|
||||
### enable
|
||||
|
||||
Use _enable_ or _turn on_ consistently for activating a feature. When describing
|
||||
capability, prefer _lets you_ over _enables you_.
|
||||
|
||||
### endpoint
|
||||
|
||||
Write _endpoint_, not _end point_. Don't use _endpoint_ when the more specific
|
||||
term is _function_, _method_, or _route_.
|
||||
|
||||
### enter
|
||||
|
||||
Use _enter_ for adding text to a field. Use _type_ only when the physical act of
|
||||
typing matters.
|
||||
|
||||
### etc.
|
||||
|
||||
Avoid _etc._, _and so on_, and _and more_. Introduce a non-exhaustive list with
|
||||
_including_, _such as_, or _for example_.
|
||||
|
||||
### execute
|
||||
|
||||
Use _run_ when it has the same meaning. Keep _execute_ when it is the precise
|
||||
technical term, such as an execute permission or query execution plan.
|
||||
|
||||
### extract
|
||||
|
||||
Use _extract_ instead of _unarchive_, _uncompress_, _untar_, or _unzip_ in prose.
|
||||
Preserve literal command names.
|
||||
|
||||
## F
|
||||
|
||||
### fail over and failover
|
||||
|
||||
Use _fail over_ as a verb. Use _failover_ as a noun or adjective.
|
||||
|
||||
### filename
|
||||
|
||||
Write _filename_, not _file name_.
|
||||
|
||||
### file system
|
||||
|
||||
Write _file system_, not _filesystem_, unless the latter is part of a code item or
|
||||
official name.
|
||||
|
||||
### fill in and fill out
|
||||
|
||||
Users _fill in_ individual fields and _fill out_ an entire form.
|
||||
|
||||
### first person
|
||||
|
||||
Address the reader as _you_. Don't use singular first person (_I_, _me_, _my_, or
|
||||
_mine_); the linter reports it as an error.
|
||||
|
||||
Use _we_ only when it clearly refers to Supabase, not when it means the writer and
|
||||
reader together.
|
||||
|
||||
### foo, bar, and baz
|
||||
|
||||
Use meaningful placeholder names that help explain the example. Keep conventional
|
||||
placeholder names only when the convention itself is relevant.
|
||||
|
||||
### frontend
|
||||
|
||||
Write _frontend_, not _front-end_ or _front end_.
|
||||
|
||||
## H
|
||||
|
||||
### hardcode and hardcoded
|
||||
|
||||
Write _hardcode_ and _hardcoded_ without a hyphen.
|
||||
|
||||
### health and healthy
|
||||
|
||||
When possible, state the observable condition, such as _responding_, _available_,
|
||||
or _passing its health check_. Don't use _healthy_ when it could be ambiguous or
|
||||
anthropomorphic.
|
||||
|
||||
### higher and lower
|
||||
|
||||
For version ranges, use _later_ and _earlier_, not _higher_ and _lower_.
|
||||
|
||||
### hover
|
||||
|
||||
Use _hold the pointer over_ when the reader must wait for the interface to react.
|
||||
Use _point to_ when no waiting is required.
|
||||
|
||||
### HTTPS
|
||||
|
||||
Write _HTTPS_, not _HTTPs_.
|
||||
|
||||
## I
|
||||
|
||||
### ID
|
||||
|
||||
Write _ID_, not _Id_ or _id_, except when matching code. Use _identifier_ when it
|
||||
is clearer.
|
||||
|
||||
### impact
|
||||
|
||||
Use _impact_ as a noun. Prefer _affect_ as the verb.
|
||||
|
||||
- Recommended: The change affects performance.
|
||||
- Not recommended: The change impacts performance.
|
||||
|
||||
### index
|
||||
|
||||
Use _indexes_ as the plural in database documentation. Use _indices_ only in
|
||||
domains where it is the established term.
|
||||
|
||||
### ingest
|
||||
|
||||
Use _import_, _load_, or _copy_ for simple data movement. Use _ingest_ when the
|
||||
operation also performs substantial processing.
|
||||
|
||||
### in order to
|
||||
|
||||
Use _to_ unless _in order to_ is necessary to prevent ambiguity. The linter warns
|
||||
about _in order to_.
|
||||
|
||||
### inline
|
||||
|
||||
Write _inline_, not _in-line_.
|
||||
|
||||
### internet
|
||||
|
||||
Use lowercase _internet_ except at the beginning of a sentence.
|
||||
|
||||
## J
|
||||
|
||||
### just
|
||||
|
||||
Remove _just_ when it is filler. If it means _only_ or _previously_, use the more
|
||||
specific word. The linter warns about _just_.
|
||||
|
||||
## K
|
||||
|
||||
### key
|
||||
|
||||
Don't use _key_ to mean _important_. When referring to a technical key, identify
|
||||
the kind of key on first use.
|
||||
|
||||
### key-value pair
|
||||
|
||||
Write _key-value pair_, not _key/value pair_ or _key value pair_.
|
||||
|
||||
### kill
|
||||
|
||||
Use _stop_, _exit_, _cancel_, or _end_ in general prose. Preserve _kill_ for
|
||||
literal commands, signals, and established technical operations.
|
||||
|
||||
## L
|
||||
|
||||
### later and earlier
|
||||
|
||||
Use _later_ and _earlier_ for version ranges.
|
||||
|
||||
- Recommended: Version 2.2 or later
|
||||
- Not recommended: Version 2.2 or higher
|
||||
|
||||
### latest, new, and soon
|
||||
|
||||
Avoid time-relative descriptions that become stale. Provide a version, date, or
|
||||
specific product state instead.
|
||||
|
||||
### leverage
|
||||
|
||||
Use _use_ or a more specific verb. The linter warns about _leverage_.
|
||||
|
||||
### lifecycle
|
||||
|
||||
Write _lifecycle_, not _life cycle_ or _life-cycle_.
|
||||
|
||||
### login and log in
|
||||
|
||||
Use _login_ as a noun or adjective and _log in_ as a verb. Follow the terminology
|
||||
in the product UI when it uses _sign in_.
|
||||
|
||||
- Recommended: Open the login page, and then log in.
|
||||
- Not recommended: Login to the Dashboard.
|
||||
|
||||
## M
|
||||
|
||||
### marketing language
|
||||
|
||||
Describe measurable behavior instead of making promotional claims. The linter
|
||||
warns about:
|
||||
|
||||
- _best in class_ and _best-in-class_
|
||||
- _cutting edge_ and _cutting-edge_
|
||||
- _effortlessly_
|
||||
- _game changer_ and _game-changer_
|
||||
- _hassle free_ and _hassle-free_
|
||||
- _powerful_
|
||||
- _seamlessly_
|
||||
|
||||
### master and slave
|
||||
|
||||
Don't use _master_ and _slave_ together. Prefer terms that describe the
|
||||
relationship accurately, such as _primary and replica_, _controller and worker_,
|
||||
or _publisher and subscriber_.
|
||||
|
||||
When a literal code item uses either term, format it as code, explain it, and use
|
||||
the preferred term afterward.
|
||||
|
||||
### media type
|
||||
|
||||
Use _media type_ rather than _MIME type_. Use _content type_ when referring to the
|
||||
`Content-Type` HTTP header or when it prevents ambiguity.
|
||||
|
||||
### microservices
|
||||
|
||||
Write _microservices_, not _micro-services_.
|
||||
|
||||
### might
|
||||
|
||||
Use _might_ for possibility or an uncertain outcome.
|
||||
|
||||
### must
|
||||
|
||||
Use _must_ or _need to_ for a requirement. Don't use _must_ for a recommendation.
|
||||
|
||||
## N
|
||||
|
||||
### native
|
||||
|
||||
Use a more precise term when possible, such as _built-in_,
|
||||
_platform-specific_, or _compiled_. Don't use _native_ to describe people.
|
||||
|
||||
### numbers in product versions
|
||||
|
||||
Write an explicit comparison, such as _version 3.0 or later_. Don't use _newer_,
|
||||
_older_, _higher_, _lower_, or a trailing `+`.
|
||||
|
||||
## O
|
||||
|
||||
### OAuth 2.0
|
||||
|
||||
Write _OAuth 2.0_, not _OAuth2_, _OAuth 2_, or _Oauth_.
|
||||
|
||||
### obviously and of course
|
||||
|
||||
Remove these phrases. They can sound dismissive and don't help the reader. The
|
||||
linter warns about both.
|
||||
|
||||
### once
|
||||
|
||||
Use _after_ if that is what you mean. Use _once_ only to mean one time.
|
||||
|
||||
### on-premises
|
||||
|
||||
Write _on-premises_, not _on-premise_, _on premise_, or _on prem_.
|
||||
|
||||
## P
|
||||
|
||||
### performant
|
||||
|
||||
Use a measurable or specific description, such as _lower latency_, _uses less
|
||||
memory_, or _handles more concurrent connections_.
|
||||
|
||||
### persist
|
||||
|
||||
Avoid using _persist_ as a transitive verb.
|
||||
|
||||
- Recommended: Store the session.
|
||||
- Recommended: Make the session persistent.
|
||||
- Not recommended: Persist the session.
|
||||
|
||||
### plain text and plaintext
|
||||
|
||||
Use _plain text_ in general contexts. Use _plaintext_ in cryptography.
|
||||
|
||||
### please
|
||||
|
||||
Don't use _please_ in normal instructions. Use it only when asking permission,
|
||||
apologizing for an inconvenience, or requesting an action that primarily benefits
|
||||
Supabase. The linter warns about _please_.
|
||||
|
||||
### plugin
|
||||
|
||||
Use _plugin_ as a noun and _plug in_ as a verb.
|
||||
|
||||
### popup
|
||||
|
||||
Use the specific UI element, such as _dialog_, _menu_, or _window_. Don't use
|
||||
_popup_ or _pop-up_ as a generic noun.
|
||||
|
||||
### Postgres
|
||||
|
||||
Use _Postgres_, not _PostgreSQL_, outside code and literal third-party names. The
|
||||
linter checks this usage.
|
||||
|
||||
### powered by
|
||||
|
||||
Prefer _with_, _by_, or _through_, depending on the relationship. The linter warns
|
||||
about _powered by_.
|
||||
|
||||
### prior to and subsequent to
|
||||
|
||||
Use _before_ and _after_. The linter checks both phrases.
|
||||
|
||||
## R
|
||||
|
||||
### read-only
|
||||
|
||||
Always hyphenate _read-only_.
|
||||
|
||||
### Realtime
|
||||
|
||||
Capitalize _Realtime_ when referring to the Supabase product. Use lowercase
|
||||
_real-time_ as an adjective with its ordinary meaning.
|
||||
|
||||
### repository
|
||||
|
||||
Prefer _repository_ in documentation prose. _Repo_ is acceptable in informal
|
||||
contributor instructions and when space is constrained.
|
||||
|
||||
### retry
|
||||
|
||||
Use _retry_ as a verb or noun. Write around _retriable_, _retryable_, _triable_,
|
||||
and _tryable_ when practical.
|
||||
|
||||
### run time and runtime
|
||||
|
||||
Use _runtime_ for an execution environment. Use _run time_ for the time when a
|
||||
program runs or the duration of a run.
|
||||
|
||||
## S
|
||||
|
||||
### sanity check
|
||||
|
||||
Use _preliminary check_, _confidence check_, or a description of what the check
|
||||
validates.
|
||||
|
||||
### screenshot
|
||||
|
||||
Use _screenshot_ as a noun. Use _take a screenshot_, not _screenshot_ as a verb.
|
||||
Redact secrets and personal information from screenshots.
|
||||
|
||||
### select
|
||||
|
||||
Use _select_ for choosing an item, selecting text, or marking a checkbox. Preserve
|
||||
the exact UI label in bold.
|
||||
|
||||
### sensitive and confidential
|
||||
|
||||
_Sensitive data_ is data whose disclosure might cause harm. _Confidential data_ is
|
||||
protected against unauthorized access. Use the term that describes the relevant
|
||||
risk or control.
|
||||
|
||||
### setup and set up
|
||||
|
||||
Use _setup_ as a noun or adjective and _set up_ as a verb.
|
||||
|
||||
- Recommended: Complete the setup to set up authentication.
|
||||
- Not recommended: Setup authentication.
|
||||
|
||||
### singular they
|
||||
|
||||
Use _they_, _them_, and _their_ as gender-neutral singular pronouns. Don't use
|
||||
_s/he_, _he/she_, _(s)he_, or _him/her_. The linter reports these forms as errors.
|
||||
|
||||
### slang abbreviations
|
||||
|
||||
Don't use internet slang in documentation. The linter warns about _tl;dr_, _ymmv_,
|
||||
_rtfm_, _imo_, and _fwiw_.
|
||||
|
||||
### spin up
|
||||
|
||||
Use _create_ or _start_ unless you are literally describing a spinning disk.
|
||||
|
||||
### SQL
|
||||
|
||||
Write _a SQL query_, not _an SQL query_. Use lowercase SQL keywords in code
|
||||
examples unless uppercase is required by the surrounding convention.
|
||||
|
||||
### SSH
|
||||
|
||||
Don't use _SSH_ or `ssh` as a verb.
|
||||
|
||||
- Recommended: Connect to the server by using SSH.
|
||||
- Recommended: Use the `ssh` command.
|
||||
- Not recommended: SSH into the server.
|
||||
|
||||
### startup and start up
|
||||
|
||||
Use _startup_ as a noun or adjective and _start up_ as a verb.
|
||||
|
||||
### Supabase
|
||||
|
||||
Capitalize _Supabase_ outside code. Use _Supabase Platform_ with both words
|
||||
capitalized. Match literal package names, commands, URLs, and code.
|
||||
|
||||
## T
|
||||
|
||||
### table name
|
||||
|
||||
Write _table name_ as two words. Format a specific table name as code.
|
||||
|
||||
### target
|
||||
|
||||
Avoid using _target_ as a verb for people. Use _intended for_, _designed for_, or
|
||||
another description of the audience.
|
||||
|
||||
### terminate
|
||||
|
||||
Use _stop_, _exit_, _cancel_, or _end_ unless _terminate_ has a specific technical
|
||||
meaning in the documented context.
|
||||
|
||||
### third party and third-party
|
||||
|
||||
Use _third party_ as a noun and _third-party_ as an adjective. Don't abbreviate
|
||||
either form with `3rd`.
|
||||
|
||||
### this and that
|
||||
|
||||
Add a noun after _this_ or _that_ when the reference could be unclear.
|
||||
|
||||
- Recommended: This setting controls connection pooling.
|
||||
- Not recommended: This controls connection pooling.
|
||||
|
||||
### timeout and time out
|
||||
|
||||
Use _timeout_ as a noun or adjective and _time out_ as a verb.
|
||||
|
||||
### timestamp
|
||||
|
||||
Write _timestamp_, not _time stamp_.
|
||||
|
||||
### time zone and time-zone
|
||||
|
||||
Use _time zone_ as a noun and _time-zone_ as an adjective.
|
||||
|
||||
### toggles
|
||||
|
||||
Users _enable_ and _disable_ features with toggles. Match and bold the visible
|
||||
label. Don't instruct the reader to _click the toggle_ when the intended state can
|
||||
be stated directly.
|
||||
|
||||
## U
|
||||
|
||||
### UI
|
||||
|
||||
Use the specific interface or page name when possible. Use _UI_ only when
|
||||
discussing a user interface as a general concept.
|
||||
|
||||
Match visible UI labels exactly and format them in bold. Describe the element with
|
||||
the correct noun when it improves clarity, such as _the **Connect** button_ or
|
||||
_the **Database password** field_.
|
||||
|
||||
### URL
|
||||
|
||||
Use _URL_, not _web address_, when writing for developers. Use descriptive link
|
||||
text rather than exposing a URL unless the URL itself is the subject.
|
||||
|
||||
### user
|
||||
|
||||
Address the reader as _you_. Use _user_ for a person who uses the software that
|
||||
the reader is building or administering.
|
||||
|
||||
### utilize
|
||||
|
||||
Use _use_. Use _utilization_ only when referring to the measured proportion of a
|
||||
resource in use. The linter warns about forms of _utilize_ and _utilise_.
|
||||
|
||||
## V
|
||||
|
||||
### vague verbs
|
||||
|
||||
Describe the concrete action. The linter suggests:
|
||||
|
||||
- _view and resolve errors_ instead of _handle errors_
|
||||
- _create, edit, or delete tables_ instead of _manage tables_
|
||||
- _query and update data_ instead of _work with data_
|
||||
|
||||
Choose a different precise verb if the suggested replacement doesn't match the
|
||||
actual operation.
|
||||
|
||||
### versus
|
||||
|
||||
Write _versus_ in prose, not _vs._ Use `vs` only when it is part of a literal name
|
||||
or when space is constrained.
|
||||
|
||||
## W
|
||||
|
||||
### web
|
||||
|
||||
Use lowercase _web_. Use the capitalization established by formal names such as
|
||||
_WebAssembly_.
|
||||
|
||||
### we
|
||||
|
||||
Don't use _we_ to mean the writer and reader together. Use _you_ for the reader.
|
||||
_We_ is acceptable when it unambiguously means Supabase.
|
||||
|
||||
### while
|
||||
|
||||
Use _while_ for events that occur at the same time. Use _although_ or _whereas_
|
||||
for contrast. Use _while_, not _whilst_; the linter checks _whilst_.
|
||||
|
||||
### will and would
|
||||
|
||||
Use present tense for current product behavior. Use _will_ for an actual future
|
||||
event, not a predictable result. Replace _would_ with _can_ when describing
|
||||
capability.
|
||||
|
||||
### workload
|
||||
|
||||
Use a more specific term, such as _app_, _service_, _database_, or _job_, when the
|
||||
meaning is known. If _workload_ is the established technical term, define its
|
||||
scope on first use.
|
||||
|
||||
## Y
|
||||
|
||||
### you
|
||||
|
||||
Address the reader as _you_. Use _user_ only for a person who uses the software
|
||||
that the reader is developing or administering.
|
||||
|
||||
## Lint-enforced phrase groups
|
||||
|
||||
The alphabetical entries explain the intent behind the rules. This section mirrors
|
||||
the exact terminology checks configured in
|
||||
`supa-mdx-lint/Rule004ExcludeWords`. Update this section when those rules change.
|
||||
|
||||
### Filler
|
||||
|
||||
The linter warns about _actually_, _easily_, _easy_, _just_, _let's_,
|
||||
_obviously_, _of course_, _please_, _quickly_, _simple_, _simply_, and
|
||||
_that's it_. Remove the term or state the intended meaning directly.
|
||||
|
||||
### Marketing language
|
||||
|
||||
The linter warns about _best in class_, _best-in-class_, _cutting edge_,
|
||||
_cutting-edge_, _effortlessly_, _game changer_, _game-changer_, _hassle free_,
|
||||
_hassle-free_, _powerful_, and _seamlessly_. Describe specific behavior or
|
||||
measurable results instead.
|
||||
|
||||
### Vague verbs
|
||||
|
||||
The linter suggests _view and resolve errors_ for _handle errors_, _create, edit,
|
||||
or delete tables_ for _manage tables_, and _query and update data_ for _work with
|
||||
data_. Use a different precise replacement when the suggestion doesn't match the
|
||||
operation.
|
||||
|
||||
### Apologies
|
||||
|
||||
The linter warns about _oops_ and _sorry_. State what happened directly. Apologize
|
||||
only when an apology is genuinely useful to the reader.
|
||||
|
||||
### First person
|
||||
|
||||
The linter reports _I_, _I'm_, _me_, _my_, and _mine_ as errors. Address the
|
||||
reader as _you_ and use an explicit noun for other actors.
|
||||
|
||||
### Gender-neutral pronouns
|
||||
|
||||
The linter reports _s/he_, _he/she_, _(s)he_, and _him/her_ as errors. Use the
|
||||
singular _they_ or rewrite the sentence.
|
||||
|
||||
### Inclusive language
|
||||
|
||||
The linter reports these terms as errors:
|
||||
|
||||
- _mankind_: use _humankind_ or _people_
|
||||
- _manmade_: use _manufactured_, _artificial_, or _synthetic_
|
||||
- _middleman_: use _intermediary_
|
||||
- _blacklist_: use _denylist_ or a more precise term
|
||||
- _whitelist_: use _allowlist_ or a more precise term
|
||||
|
||||
### Abbreviations
|
||||
|
||||
The linter corrects _eg._ and _eg_ to _e.g._. It replaces _i.e._, _ie._, and
|
||||
_ie_ with _that is_. Prefer _for example_ and _that is_ in prose when space
|
||||
allows.
|
||||
|
||||
### Powered by
|
||||
|
||||
The linter warns about _powered by_. Use _with_, _by_, or _through_, depending on
|
||||
the relationship.
|
||||
|
||||
### Preferred usage
|
||||
|
||||
The linter suggests:
|
||||
|
||||
- _Postgres_ for _PostgreSQL_
|
||||
- _concurrent connections_ for _concurrent clients_
|
||||
- _use_ for _utilize_ and _utilise_
|
||||
- _uses_ for _utilizes_ and _utilises_
|
||||
- _using_ for _utilizing_ and _utilising_
|
||||
|
||||
### Direct, concise language
|
||||
|
||||
The linter warns about these phrases:
|
||||
|
||||
- _aforementioned_: name the item
|
||||
- _amongst_: use _among_
|
||||
- _endeavor_ or _endeavour_: use _try_
|
||||
- _facilitate_: use _help_ or describe the action
|
||||
- _for the purpose of_: use _to_
|
||||
- _in order to_: use _to_
|
||||
- _leverage_: use _use_ or a more precise verb
|
||||
- _prior to_: use _before_
|
||||
- _subsequent to_: use _after_
|
||||
- _whilst_: use _while_
|
||||
|
||||
### Internet slang
|
||||
|
||||
The linter warns about _tl;dr_, _ymmv_, _rtfm_, _imo_, and _fwiw_. Write out the
|
||||
meaning or remove the aside.
|
||||
|
||||
## Attribution
|
||||
|
||||
Portions of this word list are modifications based on work created and shared by
|
||||
Google and used according to the terms of the
|
||||
[Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/).
|
||||
See the
|
||||
[Google developer documentation style guide word list](https://developers.google.com/style/word-list)
|
||||
for the original work. Supabase-specific guidance and adaptations are maintained
|
||||
in this repository.
|
||||
@@ -0,0 +1,5 @@
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'content-type',
|
||||
}
|
||||
Loaded 100 of 1894 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user