Merge branch 'master' into autovacuum_docs

This commit is contained in:
TheOtherBrian1 authored and GitHub committed 2026-10-07 14:48:29 -04:00
commit a1cf5ebfc0
3448 files changed
+147385 -78060

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
---
name: api-types
description: Maintain Supabase API types. Use when changing generated API type declarations, OpenAPI schemas, or investigating API type deployment drift.
---
# API types
The generated API contract has three specs: API v1, API v2, and Platform. Their committed outputs are `packages/api-types/types/api-v1.d.ts`, `packages/api-types/types/api-v2.d.ts`, and `packages/api-types/types/platform.d.ts`.
## Update types
1. Make the API/schema change and ensure it is deployed to production before relying on a type PR. Production is the merge-gate source of truth.
2. Update the committed types, either:
- Run `pnpm api:codegen` against a running local API environment. It fetches all three local OpenAPI specs and updates the committed files.
- Or, if you don't have a local API environment running, run `pnpm api:codegen:prod`. It fetches the three production OpenAPI specs directly and overwrites the committed files with them (no diffing — it always writes).
3. Inspect and commit only the intended generated type changes.
4. Run `pnpm api:verify-types`. It fetches the three production OpenAPI specs, regenerates types with the repository tooling, and compares them with the committed files.
Complete the update only when `pnpm api:verify-types` passes after the production deployment is available. If you used `api:codegen:prod`, this should already pass since the committed files came straight from production.
## Interpret verification
- A pass means the committed generated declarations match all three production specs at the time of the check.
- A mismatch means production and the committed files differ. If the API is not deployed, deploy it and rerun the check. If production is correct, regenerate and review the changed files.
- A fetch failure means the production schema endpoint could not be read; fix or retry the endpoint before treating the result as a type mismatch.
## Pull requests
The `Verify production API types` CI job runs when `packages/api-types/types/**` changes and performs the same production comparison. It is a required merge check. Run the local verifier before requesting review and treat a failed CI verification as production drift that must be resolved.
+1 -1
View File
@@ -86,7 +86,7 @@ at hand — they cite each other where context matters.
| [`reference/llm-agent-surface.md`](./reference/llm-agent-surface.md) | Audience routing, `llms.txt`, content negotiation, bulk exports. |
| [`reference/llm-agent-parity.md`](./reference/llm-agent-parity.md) | HTML↔markdown fidelity (e.g. AI prompts), search caveat, agent onboarding guides, in-flux wiring. |
| [`reference/federated-docs.md`](./reference/federated-docs.md) | How docs pulls markdown from external repos at build time. Routes, `pageMap`, remark/rehype plugins, link transforms, known failure modes. |
| [`reference/ci-and-lint.md`](./reference/ci-and-lint.md) | GitHub Actions on every PR — `docs_lint`, `Docs Tests`, typecheck, prettier, Vercel preview gate. Where to add a check before creating a new one. |
| [`reference/ci-and-lint.md`](./reference/ci-and-lint.md) | GitHub Actions on every PR — `Docs Tests`, typecheck, prettier, Vercel preview gate. Where to add a check before creating a new one. |
| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI → reference generation, including scoped PAT permission tables; why not to swap in Scalar/Redoc. |
| [`reference/graphql-endpoint.md`](./reference/graphql-endpoint.md) | The `/api/graphql` endpoint under `apps/docs/resources/` — per-query folder layout, `rootSchema.ts`, connection/field utils, and the steps to add a new top-level query. |
| [`reference/search-embeddings.md`](./reference/search-embeddings.md) | The `scripts/search/` embeddings pipeline behind `searchDocs` — content sources, processing flow, change detection, and the `page` / `page_section` tables. |
@@ -46,7 +46,6 @@ building parallel ones.
| Code samples in MDX | `$CodeSample` directive |
| Build steps | `prebuild` / `postbuild` chain in `apps/docs/package.json` |
| CI checks | Existing workflows under `.github/workflows/`. See [`ci-and-lint.md`](./ci-and-lint.md). |
| Lint rules for MDX content | `supa-mdx-lint` configuration — extend it, don't add a new lint job |
If something close to what you need already exists, **the default is to
extend it**, not to build alongside.
@@ -90,8 +89,8 @@ Antipatterns to avoid:
covers the pattern — compose at the call site instead.
- New custom build steps that run alongside the existing `prebuild` /
`postbuild` chain when a hook already exists.
- A new CI workflow when `docs_lint`, `Docs Tests`, or the existing
typecheck/prettier jobs could absorb the check. See
- A new CI workflow when `Docs Tests` or the existing typecheck/prettier jobs
could absorb the check. See
[`ci-and-lint.md`](./ci-and-lint.md).
- A new content vocabulary (custom front-matter block, novel MDX directive,
new YAML schema) when a React component + partial would express the same
@@ -194,15 +194,14 @@ markdown string to substitute.
See [`ci-and-lint.md`](./ci-and-lint.md) for the full CI surface. Local
commands:
| Tool | Where | What it catches |
| --------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------- |
| `pnpm test:local:unwatch <path>` (from `apps/docs`) | per-test | Vitest suite for `lib/` and `data/` schemas; needs local Supabase + DB reset first — see `apps/docs/AGENTS.md` |
| `pnpm format` | repo root | Prettier — run before opening a PR |
| `pnpm lint --filter=docs` | repo root | ESLint over `apps/docs` |
| `pnpm typecheck` | repo root | TS across packages |
| `pnpm build --filter=docs` | repo root | Includes markdown generation; failures here block release |
| `pnpm lint:mdx` | `apps/docs` | MDX content lint (whole `content/` tree) |
| Typos check (`.github/workflows/avoid-typos.yml`) | CI only | `runner / misspell` job at error severity — no local command; fix flagged words before merge |
| Tool | Where | What it catches |
| --------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------- |
| `pnpm test:local:unwatch <path>` (from `apps/docs`) | per-test | Vitest suite for `lib/` and `data/` schemas; needs local Supabase + DB reset first — see `apps/docs/AGENTS.md` |
| `pnpm format` | repo root | Prettier — run before opening a PR |
| `pnpm lint --filter=docs` | repo root | ESLint over `apps/docs` |
| `pnpm typecheck` | repo root | TS across packages |
| `pnpm build --filter=docs` | repo root | Includes markdown generation; failures here block release |
| Typos check (`.github/workflows/avoid-typos.yml`) | CI only | `runner / misspell` job at error severity — no local command; fix flagged words before merge |
Before adding a custom lint job, check whether the existing one can absorb
the check (see [`adding-features.md`](./adding-features.md) "Reuse
@@ -12,8 +12,6 @@ Before adding a new lint job or CI check, **scan this list first** — see
```
PR opened / updated
│
├── docs_lint (MDX/content linting; required)
├── docs_lint_comment_external (posts results as PR comments for external PRs)
├── Docs Tests (pnpm test:docs on relevant docs code/spec changes)
├── TypeScript & Lint (tsc + eslint)
├── Prettier (format check)
@@ -29,12 +27,7 @@ Merge to master
Workflows run in parallel against any PR touching the repo:
### 1. `docs_lint`
Primary docs-specific CI check. MDX/content linting for the docs. **Required
check before merging.** Backed by `supa-mdx-lint`.
### 2. Docs Tests (`docs-tests.yml`)
### 1. Docs Tests (`docs-tests.yml`)
Triggered on relevant docs code/spec changes, including the generated scoped
PAT partials and their shared permission catalog. Runs on a Blacksmith 4-vCPU
@@ -49,29 +42,24 @@ Ubuntu runner with concurrency controls to cancel stale builds.
- Run `pnpm run test:docs` (with dummy GitHub OAuth env vars to prevent local
Supabase startup errors).
### 3. TypeScript & Lint (`typecheck.yml`)
### 2. TypeScript & Lint (`typecheck.yml`)
TypeScript type checking and ESLint across the monorepo.
### 4. Prettier (`prettier.yml`)
### 3. Prettier (`prettier.yml`)
Format checking across the whole repo including `apps/docs`.
### 5. `docs_lint_comment_external`
Companion to `docs_lint` that posts lint results as PR comments for external
contributors.
### 6. Authorize Vercel Deploys
### 4. Authorize Vercel Deploys
Gates Vercel preview deployment on a GitHub-side check first. Prevents
arbitrary forks from triggering Vercel builds.
### 7. reviewdog
### 5. reviewdog
Inline code review annotations via reviewdog.
### 8. Validate pull request
### 6. Validate pull request
PR metadata validation (title format, labels, etc.).
@@ -102,8 +90,9 @@ builds/deploys the Next.js site to their CDN.
Before adding a new GitHub Actions workflow:
1. **Can `docs_lint` absorb it?** — most MDX/content checks belong inside
`supa-mdx-lint` configuration, not as a new workflow.
1. **Is it a prose or terminology check?** — it belongs in the authoring
skills, not in a workflow. The repo ran a blocking MDX linter and retired
it.
2. **Can `Docs Tests` absorb it?** — TypeScript / vitest checks for new
functionality fit here.
3. **Is it cross-cutting?** — typecheck, prettier, and reviewdog already
+172 -41
View File
@@ -2,73 +2,204 @@
name: edit-the-docs
description: >-
Restructure, reorder, and improve existing Supabase docs pages under
apps/docs — clarity, connective text, section grouping, and brevity.
Use when asked to edit, reorganize, restructure, tighten prose, or add
glue between sections on a page that already exists. Not for net-new
feature drafts (use write-the-docs) or PR triage/verification (use
review-the-docs).
apps/docs: clarity, connective text, section grouping, and brevity.
Use when asked to edit, reorganize, restructure, tighten prose, add glue
between sections, or split a page edit into stacked PRs. Not for net-new
feature drafts, which belong to write-the-docs, and not for PR triage or
verification, which belong to review-the-docs.
---
# Edit the docs
Improves **existing** Supabase docs pages: structure, order, connective text,
and clarity. Distinct from [`write-the-docs`](../write-the-docs/SKILL.md)
(draft net-new or product-grounded rewrites from intent + code) and
[`review-the-docs`](../review-the-docs/SKILL.md) (lint, build, PR triage).
Improves **existing** Supabase docs pages: structure, order, connective text, and clarity.
**Not this skill:** [`write-the-docs`](../write-the-docs/SKILL.md) drafts net-new content or product-grounded rewrites from intent and code. [`review-the-docs`](../review-the-docs/SKILL.md) covers build and PR triage.
**Output is one pull request, with one change type per commit.** A reviewer reads the style diff apart from the structure diff without holding several PRs in their head. Split into a stack of PRs only when the requester asks for one, or approves the split you offer because the diff turned out large. Phase 0 covers when to raise it, and [reference/stacked-prs.md](reference/stacked-prs.md) covers the mechanics.
## Core rules
1. **Read before you rewrite.** Open the target page and nearby pages of the same type. Name the reader's goal and the page type (explainer, guide, tutorial, troubleshooting) before moving sections.
2. **Improve structure and clarity; don't invent product truth.** Preserve behavior claims, UI labels, and positioning unless you verify a change against code or product intent. Accuracy gaps or missing net-new content belong with [`write-the-docs`](../write-the-docs/SKILL.md) / [`pm-the-docs`](../pm-the-docs/SKILL.md), not silent invention here.
3. **Follow CONTRIBUTING.md and WORD_LIST.md** for voice, terminology, and formatting. See [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) and [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md).
4. **Prefer brevity.** Prefer broad strokes when mechanical detail doesn't help the reader's task. Cut redundancy; don't over-explain.
5. **Reuse sibling skills.** IA/architecture via [`ask-the-docs`](../ask-the-docs/SKILL.md); validation and self-review via [`review-the-docs`](../review-the-docs/SKILL.md). Shared pitfalls live in [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md) — apply them, don't duplicate them.
1. **Read before you rewrite.** Open the target page and nearby pages of the same type. Name the reader's goal and the page type before moving sections.
2. **Don't invent product truth. Verify it, in its own PR.** Style and structure work preserves behavior claims, UI labels, and positioning as written. Correcting a claim is PR 3 work, and adding one belongs to the additions branches above it. Those PRs follow [`write-the-docs`](../write-the-docs/SKILL.md) grounding rules: read the code, separate shipped behavior from product intent, and flag what you inferred.
3. **Follow the [style guide](../../../apps/docs/style-guide/README.md)** for voice, terminology, structure, and components.
4. **Prefer brevity.** Use broad strokes when mechanical detail doesn't help the reader's task. Cut redundancy. Don't over-explain.
5. **Reuse sibling skills.** Get IA and architecture from [`ask-the-docs`](../ask-the-docs/SKILL.md). Get validation and self-review from [`review-the-docs`](../review-the-docs/SKILL.md). Apply the style guide's [timeless documentation](../../../apps/docs/style-guide/03-page-structure.md#write-timeless-documentation) rather than duplicating it here.
6. **One change type per diff.** A diff that mixes reworded prose with moved sections is unreviewable, because the reader can't tell a move from a rewrite. Separate them by commit in a single PR, or by branch in a stack.
## Phase 1 — Diagnose
## Phase 0: Size and split
1. Identify the document type per CONTRIBUTING.md (explainer, tutorial, guide, reference, or troubleshooting).
1. Identify the document type per [`03-page-structure.md`](../../../apps/docs/style-guide/03-page-structure.md#document-types). The types are explainer, tutorial, guide, reference, and troubleshooting.
2. State the reader's goal and prerequisites in one or two lines.
3. Note structural problems: mixed information types interrupting a procedure, missing intro navigation on a long page, weak transitions, redundancy, or over-explained mechanics.
4. Summarize the diagnosis to the requester before large moves when the restructure would change how the page is read.
4. Sort the diagnosis into the buckets below. **Drop any bucket that comes back empty, and say so.** Style, structure, and technical revision take one commit or branch each. Additions take as many as the content needs, so the edit has no fixed size. A style edit plus a structural edit is the common shape, because most pages that need restructuring are already correct. Two buckets is a complete result, not a truncated one.
5. Know where the edit ends. **The edit is only the buckets that have content.** Any bucket you drop is beyond the edit, and a later request for that change type is a new request. That includes one you raise yourself. Name it, keep the work in progress clean, and ask whether it belongs in this edit, in a separate ticket, or nowhere. Absorbing it into a bucket that's already open is what turns an edit into a rewrite.
6. Size the edit. **When it comes out large, offer a stack. Don't choose one.** One PR with each bucket as its own commit is the output unless the requester approves a split. Raise the question when both hold:
- The edit rewrites prose and moves sections, or it corrects a technical claim.
- It runs over roughly 150 changed lines.
## Phase 2 — Restructure
Say how large the diff is and propose the branches. Name the trade in the ask: a stack gives a reviewer clean per-change-type diffs, and it also means no PR page shows the whole edit, so reading it end to end costs them an extra command. Their reviewers pay that cost, so it's their call. **No answer means one PR.**
Apply the **Mixed information types**, **Navigation**, and **Cross-references and glue** guidance in [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) (Guides section), summarized here:
7. Summarize the diagnosis and the proposed split to the requester, and **wait for confirmation before creating any branch.** Name which buckets are empty and why. When nobody is available to confirm, record the diagnosis in the PR body and ship one PR.
1. 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.
2. 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.
3. 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.
4. Move and regroup first; preserve meaning. Don't silently rewrite facts while restructuring.
**When the diff outgrows the estimate mid-edit, stop and offer the split then.** A size call made at diagnosis can be wrong by the time the style pass lands. Say how large it got and ask. Splitting unasked is the failure here, and so is carrying on quietly because you already have an answer.
## Phase 3 — Edit for clarity
**The sections below are named for the stacked case.** In a single PR they're commits, in the same order and under the same rules.
## PR 1: Style
Inline changes only. Nothing in this PR moves a line from one place to another.
**Rewrite:**
- Use second person, present tense, short paragraphs, and ordered steps for sequential actions.
- Cut restated points and mechanical over-explanation.
- Apply [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md): timelessness, no internal planning context in shipped MDX, redundancy, single-item lists, admonition restatement.
- Search [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md) when introducing or revising technical terms and UI actions.
- Keep code samples executable in their stated context; mark intentionally omitted code. Prefer partials under `apps/docs/content/_partials/` over copied blocks.
- Put procedures in procedure format. Per-step formatting is in [`02-elements.md`](../../../apps/docs/style-guide/02-elements.md#procedures); how many steps to present at once and when to split into phases is in [`03-page-structure.md`](../../../apps/docs/style-guide/03-page-structure.md#chunking).
- Apply [`02-elements.md`](../../../apps/docs/style-guide/02-elements.md) for admonitions, emphasis, links, and lists, and [`01-voice-and-tone.md`](../../../apps/docs/style-guide/01-voice-and-tone.md) for person, tense, and sentence-level grammar.
- Keep code samples executable in their stated context, and mark intentionally omitted code. Prefer partials under `apps/docs/content/_partials/` over copied blocks.
- **Check the alt text on every image, and open the image to do it.** Alt text on an existing page usually names the topic rather than describing the picture, and a topic name is what the nearby heading already says. Describe what a reader who can't see it would need: the labeled parts, the relationships between them, and any values the diagram carries. This is a rewrite of existing text, so it belongs in this PR.
## Phase 4 — Validate
**Cut:**
Before handoff:
- Restated points and mechanical over-explanation.
- Time-anchored language and unshipped features: [timeless documentation](../../../apps/docs/style-guide/03-page-structure.md#write-timeless-documentation). Internal planning context in shipped MDX: [keep internal context out](../../../apps/docs/style-guide/03-page-structure.md#keep-internal-context-out). Repeated points: [brevity](../../../apps/docs/style-guide/01-voice-and-tone.md#brevity). Single-item lists and restated admonitions: [`02-elements.md`](../../../apps/docs/style-guide/02-elements.md).
- Terminology that doesn't match [`WORD_LIST.md`](../../../apps/docs/style-guide/WORD_LIST.md). Run both passes from [Use with an AI agent](../../../apps/docs/style-guide/WORD_LIST.md#use-with-an-ai-agent): [Phrase groups](../../../apps/docs/style-guide/WORD_LIST.md#phrase-groups) gives you every literal term list in one read, then `grep '^### ' WORD_LIST.md` and open only the entries matching words on the page. Cover terms already on the page, not only the ones you introduce. An existing page is where nonconforming terminology accumulates.
- [ ] Section groups follow information type; procedures aren't interrupted by long context
- [ ] Intro navigation present only when the page needs it; links resolve
## PR 2: Structure
Apply [`03-page-structure.md`](../../../apps/docs/style-guide/03-page-structure.md) — section grouping, navigation, and glue. This section is the procedure for doing that on an existing page; the rules live in the guide.
**Work in this order, and settle the outline before you move a line.** A restructure invalidates every branch above it in the stack, so each revision costs a full restack, and a restack is where content gets dropped in conflict resolution. Reworking the shape twice costs far more than getting it right once.
### 1. Lock the headings other code links to
Grep the whole repo for `#<slug>` against every heading on the page, not just `apps/docs/content`. Studio renders Docs buttons that deep-link into guide anchors, and `apps/www` links into them too. Those are the matches that break a button in the product rather than a link between two pages.
Write the matched heading texts down. For the rest of this PR they are immutable. **Moving a section preserves its slug, and so does changing its level. Only renaming breaks it.** That is what makes an aggressive regroup safe.
### 2. Classify every substantial section
Classify each one against the information types in [`03-page-structure.md`](../../../apps/docs/style-guide/03-page-structure.md#information-types), then collapse them into the three buckets you'll group by: **procedural** (Procedure), **contextual** (Concept and Process), and **reference** (Structure and Fact). A principle or a fact usually rides along in the section it qualifies rather than getting one of its own.
The guide has the two rules that decide the hard cases: classify by what the reader is doing rather than what the section is about, and split a section that serves two types instead of filing it under the larger half.
Write the bucket for every section down before moving anything. A page where every section lands in one bucket is a page that wasn't really classified.
### 3. Write the target outline before touching the file
Produce the whole heading tree, with levels, and check it against the locked list from step 1. Put it in front of the requester along with the Phase 0 diagnosis. The outline is the artifact that gets revised, not the page.
Order the groups per [`03-page-structure.md`](../../../apps/docs/style-guide/03-page-structure.md#grouping-sections), which has the order that works and a worked outline. A short concept opener can precede the procedure group; keep that group uninterrupted, and put the remaining background after it.
When a section held two types and you split it in step 2, the half that keeps the original heading text stays in its original group, so the locked anchor from step 1 survives. The new half takes a new heading and moves to the group its type belongs to.
### 4. Move, then add the glue the new shape needs
Move and regroup, and preserve meaning. Don't silently rewrite facts while restructuring. A pure set of moves is what makes this PR reviewable, so call out in the PR body any deletion that isn't a move.
Then:
- Add a short introduction linking each major group and saying when to use it. Skip it when a short page is already easy to scan.
- Add a group introduction, a transition where the information type changes, and an outcome after a procedure. Don't link every adjacent section.
- Put sections covering the same topic under a shared heading.
### 5. When the page itself should split
When a topic outgrows the page, give it its own page rather than its own group. Navigation that overflows the sidebar is one signal. A section carrying its own subsections several levels deep, sharing nothing with the rest of the page but a single word, is another.
Update every navigation entry, repoint every inbound anchor, and cross-reference the new page. Confirm the nav-registration mechanism through [`ask-the-docs`](../ask-the-docs/SKILL.md) rather than assuming it.
### 6. Before you submit
Re-run the step 1 grep. Every locked heading text is still present, at whatever level it ended up.
**A move that only reads correctly once new content exists isn't a PR 2 move.** It belongs to the branch that adds the content. Leave the section where it is, and say in the PR body which move you deferred and what it is waiting on. Otherwise PR 2 stops standing on its own, and a stack merged partway leaves the page reading worse than before.
If nothing needs to move, PR 2 doesn't exist. A page can be well organized and still need a style pass. Drop the branch and say the structure held up.
## PR 3: Technical revision
Validate the truth of the content and correct what's wrong.
**Change a claim only when leaving it would produce a wrong outcome.** A reader following the page would hit an error, get a different result than the page promises, or decide on a fact that isn't true. That's the test.
**Leave it alone otherwise.** Don't open PR 3 for imprecise but harmless phrasing, a claim you'd have worded differently, an accurate detail that isn't the newest way to do it, or a stale-looking value you can't verify against code. The last one is a note to the author, not an edit.
**An external rule isn't a wrong outcome by itself.** A best-practices rule that a reader would never hit as a failure doesn't clear the gate, however high the rule's stated impact. Weigh what the reader experiences against the page, not how the rule is ranked.
**PR 3 corrects what's on the page. A missing safeguard is an absence, and absences are additions.** When the fix is to add something the page never had, it belongs above this branch, not in it. This is the line that keeps a verification pass from quietly becoming a rewrite.
When a claim does fail the test, verify before you change it, per Phase 1 of [`write-the-docs`](../write-the-docs/SKILL.md):
- Read the implementation. Prefer the diff of a linked `supabase/supabase` PR over a general codebase read.
- Where code and product intent disagree, code wins for behavior claims. Flag the mismatch.
- Flag anything you inferred in the PR description, not in the MDX.
**Run the snippets when the page has them.** Offer [`test-the-docs`](../test-the-docs/SKILL.md) before you start, and don't run it unasked. A snippet that fails in the sandbox is the most direct evidence a claim fails the wrong-outcome test, because the reader hits the same error. Attach the verification report to the PR body. If the author declines, record the artifacts as deferred and carry on with the code read. If the sandbox fails for an environmental reason, that's a deferral rather than a result — retry it before the branch merges.
**Run every fence in document order, not only one path.** The reader pastes top to bottom, so that order is the claim. Snippets that each work alone can still fail as a sequence, by re-creating an object an earlier one made or by depending on one no fence ever creates. Nothing in a code read surfaces that, and it's the failure a reader hits first.
Testing covers procedural content only. Claims that nothing executes, such as limits, defaults, and positioning, still need the code read above.
**A branch above can change the answer.** The test is applied to the page as it stands, so a claim that passes inspection here can become wrong once an additions branch contradicts it. That correction belongs to the branch that creates the conflict, not back down here. Say so when you leave the claim, so the later change reads as intended rather than as a missed finding.
**If every finding fails the test, PR 3 is empty.** Say what you checked and what you're deliberately leaving, then drop the branch. An empty PR 3 means verified and fine, not skipped. A technical concern raised later in the stack is then a new request, per the boundary rule in Phase 0.
## PR 4+: Additions, on request only
**Additions sit on top of the stack, so they stay out of the edit.** New content is a different job from editing what's already there. Keeping it on its own branches is what stops an edit from turning into a rewrite halfway through.
**Additions take as many branches as the content needs.** Split them by diff size so each branch stays reviewable, and name each branch for what it adds rather than for its position in the stack. One branch is right when the additions are one topic and a small diff.
**Don't scope these branches from the diagnosis.** Additions are empty by default. Don't propose them because the page looks thin.
**A tracked request is the request.** An assigned ticket or issue that asks for new content has already made the ask, so treat it as scoped and get on with it. The rule forbids inventing additions yourself. It doesn't ask you to wait for someone to repeat a request that's already written down.
**Route mid-edit requests up here instead.** When the author asks for new content while you're on an earlier branch, or when you spot a gap yourself, say it's additions material and keep the current branch clean. Then ask whether they want it in this stack, in a separate ticket, or not at all. Naming it is how you keep the conversation from reopening PR 1.
Once it's scoped:
- Crawl reader feedback for candidate gaps. Linear is an internal Supabase tool, preferred when available and not required for open-source contributors.
- Ground additions the same way as PR 3. Read the code before making a behavior claim, and flag what you inferred.
- **Run every new runnable snippet through [`test-the-docs`](../test-the-docs/SKILL.md) before it ships.** New content is where an untested snippet is likeliest to be wrong, because nothing has ever executed it.
- Strip internal business context before the draft ships: PRD intent, roadmap speculation, and ticket discussion. It belongs in the PR description, not in the MDX.
## Validate each PR
Run this per change type, before you submit the commit or branch that carries it, not once at the end:
- [ ] The diff contains only this change type
- [ ] Section groups follow information type, and procedures aren't interrupted by long context
- [ ] Intro navigation is present only when the page needs it, and links resolve
- [ ] Connective text is selective, not link spam
- [ ] Voice matches CONTRIBUTING.md / WORD_LIST.md
- [ ] Voice matches [`01-voice-and-tone.md`](../../../apps/docs/style-guide/01-voice-and-tone.md) and terminology matches [`WORD_LIST.md`](../../../apps/docs/style-guide/WORD_LIST.md)
- [ ] Every image has alt text that describes the image, checked against the image itself
- [ ] No invented behavior or positioning
- [ ] Shared pitfalls checklist considered
Mechanics (anchors, lint, format): follow [`write-the-docs/reference/drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md). Before renaming or rewording headings, grep for `#<old-anchor-slug>` under `apps/docs/content` and update matches.
**Anchors.** PR 2 step 1 builds the locked-heading list and step 6 re-checks it. Any branch that renames or rewords a heading clears the same gate.
Then run [`review-the-docs`](../review-the-docs/SKILL.md) local self-review (`pnpm lint:mdx`, and `pnpm build:guides-markdown` when guides/explainers/tutorials changed).
**Frontmatter `title`.** It follows the same sentence-case rule as a heading. Renaming it moves a navigation label and a search entry, not just a line of prose, so it clears this same gate and lands in PR 2 rather than PR 1.
**Format and build.** Follow [`write-the-docs/reference/drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md). Then run the [`review-the-docs`](../review-the-docs/SKILL.md) local self-review, plus `pnpm build:guides-markdown` when a guide, explainer, or tutorial changed.
`build:guides-markdown` writes `apps/docs/public/markdown/manifest.json`, which the repo tracks and commits as `[]`. Discard that file before committing. It's a build artifact, not part of the edit.
## Additional resources
- Structure SoT: [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) (mixed types, navigation, glue)
- Structure ops: [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) — Guides: Mixed information types, Navigation, Cross-references and glue
- Pitfalls: [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md)
- Mechanics: [`write-the-docs/reference/drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md)
- Architecture/IA: [`ask-the-docs`](../ask-the-docs/SKILL.md)
**Stacking:**
- Mechanics and `gh stack` commands: [reference/stacked-prs.md](reference/stacked-prs.md)
- Bottom-up stack review: [`review-the-docs`](../review-the-docs/SKILL.md)
**Style and structure:**
- Style guide entry point: [`style-guide/README.md`](../../../apps/docs/style-guide/README.md)
- Section grouping, navigation, glue, and chunking: [`03-page-structure.md`](../../../apps/docs/style-guide/03-page-structure.md)
- Procedure format and other components: [`02-elements.md`](../../../apps/docs/style-guide/02-elements.md)
- Terminology: [`WORD_LIST.md`](../../../apps/docs/style-guide/WORD_LIST.md)
**Sibling skills:**
- Drafting mechanics: [`drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md)
- Runnable verification: [`test-the-docs`](../test-the-docs/SKILL.md)
- Architecture and IA: [`ask-the-docs`](../ask-the-docs/SKILL.md)
- Net-new drafts: [`write-the-docs`](../write-the-docs/SKILL.md)
- Review: [`review-the-docs`](../review-the-docs/SKILL.md)
@@ -0,0 +1,74 @@
# Stacked PRs for a page edit
Mechanics for shipping the [`edit-the-docs`](../SKILL.md) buckets as a stack. **A stack is the exception, and the requester approves it.** Phase 0 covers when to offer one. This file covers how to build and submit it once they agree.
## Branch names
One branch per change type, bottom to top:
| PR | Branch |
| --- | ---------------------------- |
| 1 | `docs/<page>-style` |
| 2 | `docs/<page>-structure` |
| 3 | `docs/<page>-technical` |
| 4+ | `docs/<page>-<what-it-adds>` |
The first three names are fixed, because there's one of each. **Additions get one branch per topic, named for the content it adds:** `docs/tables-rls` and `docs/tables-datatypes`, not `docs/tables-additions-1` and `-2`. Use `docs/<page>-additions` when a single branch carries all of them.
**Create only the branches whose buckets have content.** Two branches is the common shape once an edit clears the gate. `gh stack init` takes however many you pass it.
**Use a category prefix and a short second segment.** Don't prefix a branch with an author name, even when a tracker suggests that format.
**Get every name right before you submit.** Renaming a branch that already has an open PR closes the PR rather than retargeting it, and a closed PR whose head ref is gone can't be reopened. Recovering costs the PR number and its CI history.
## Build the stack with gh stack
Never chain `gh pr create --base <previous-branch>`. That produces correct base branches but no GitHub stack. There's no stack number and no stack UI, so reviewers see several unrelated-looking PRs instead of one series.
1. `gh stack init <bottom> <middle> <top>` adopts existing branches, bottom to top. This is local only and makes no remote change.
2. `gh stack view` confirms the structure and shows the mapped PR for each branch.
3. `gh stack submit --auto` pushes and registers the stack on GitHub. Use `--auto` in a non-interactive session, where the editor can't open. New PRs are created as drafts unless you pass `--open`.
**Check the titles after submitting.** `submit` can title a PR from its branch name rather than its commit subject. Fix any that came out wrong with `gh pr edit <pr> --title`.
**Safe to re-run on PRs that already exist.** `submit` reports each one "up to date" and reuses it, so PR numbers, descriptions, and creation timestamps survive.
**Draft state doesn't reliably survive.** `--open` marks existing PRs ready for review, not just new ones, and a resubmit has been observed taking drafts out of draft without it. Check the draft state of every PR after submitting, and set it back with `gh pr ready --undo` if it moved.
**Other commands.** `gh stack link <pr> <pr> <pr>` registers the GitHub stack without local tracking. `gh stack unstack` removes a stack. The extension is `github/gh-stack`.
## Reading the stack as a whole
No PR page shows the whole edit, so a reviewer who wants it in one view needs the command:
```bash
git diff master...<top-branch> -- <path>
```
`gh stack view` lists the branches in order, so it gives you the top one. Put the command in the bottom PR's body. Without it the reviewer reconstructs the edit branch by branch, and that cost is why Phase 0 defaults to a single PR.
## Restacking after a change low in the stack
`gh stack rebase` replays every branch above the one you changed. Where a lower branch moved content that an upper branch also edited, git raises a conflict whose two sides are "the new structure" and "the old content being re-added". Resolving toward the new structure is usually right, and it silently drops the upper branch's edit along with the stale copy.
**Assume that happened. Audit rather than read the diff.** Before pushing, grep each branch for a marker of every change it is supposed to carry:
```bash
git show <branch>:<path> | grep -c '<marker>'
```
One marker per change, checked against the count you expect. A restructure large enough to conflict is large enough that reading the diff will not catch a missing paragraph.
Restore anything missing as a new commit on the branch that owns it, then rebase again. Don't fold it into a neighboring branch to avoid a second rebase; that breaks the one-change-type-per-PR rule the stack exists for.
## Merge order
Merge bottom-up: `master`, then PR 1, then PR 2, then PR 3, then each additions branch in stack order. This is the model [`review-the-docs`](../../review-the-docs/SKILL.md) uses to review a stack, so the authoring and review sides share one vocabulary.
## PR bodies
Each body states which change type the PR carries and what it leaves to the PRs above it. That tells a reviewer the diff is narrow on purpose. Reworded prose isn't missing from the structure PR, it already landed below.
Carry forward anything you flagged while working: inferred claims from PR 3, gaps you named but didn't fill, and stale values you couldn't verify. Those belong in the description, not in the MDX.
For general PR-body mechanics, see [`create-pull-request`](https://github.com/supabase/docs-agent-skills/blob/main/.claude/skills/create-pull-request/SKILL.md).
@@ -17,10 +17,10 @@ _Self-serve first ([agent skills](../../../../apps/docs/CONTRIBUTING.md#ai-agent
- The **why** is explicit: a reader learns what problem this solves and when to reach for it, not only the steps.
- The content **type is deliberate** and consistent within the page.
- **Audience and prerequisites** are stated up front.
- **Examples are runnable and have been tested** (commands, code, expected result) — verify with `/test-the-docs` against a Docker-isolated local stack, not production.
- **Examples are runnable and have been tested** (commands, code, expected result)
- **Correct stage** like GA is stated; limitations are named honestly.
- The page **lives in the right place** in the IA and links to and from related pages.
- Terminology and formatting match existing docs (and style guide once it lands).
- Terminology and formatting match the [style guide](../../../../apps/docs/style-guide/README.md).
### 1. Frame
@@ -48,8 +48,6 @@ _Skill:_ `/write-the-docs` to draft net-new content grounded in Linear and the c
- [ ] E: Contribute technical depth and verify accuracy (APIs, limits, edge cases)
- [ ] P: Call out the current stage inline and any known limitations
When the work is improving an existing page (restructure, reorder, connective text, brevity) rather than authoring net-new content, use `/edit-the-docs` instead of `/write-the-docs`.
### 4. Self-review against the bar
_Skills:_ `/review-the-docs` for [local self-review](../../review-the-docs/SKILL.md#local-self-review-no-open-pr) before opening the PR; `/test-the-docs` to run snippets and produce a verification report.
@@ -81,4 +79,4 @@ _Skill:_ `/review-the-docs` to triage, classify, verify the build, and report.
## Resources
Skills for this checklist: [AI agent skills for docs authoring](../../../../apps/docs/CONTRIBUTING.md#ai-agent-skills-for-docs-authoring) (`/pm-the-docs`, `/ask-the-docs`, `/write-the-docs`, `/edit-the-docs`, `/test-the-docs`, `/review-the-docs`).
Skills for this checklist: [AI agent skills for docs authoring](../../../../apps/docs/CONTRIBUTING.md#ai-agent-skills-for-docs-authoring) (`/pm-the-docs`, `/ask-the-docs`, `/write-the-docs`, `/test-the-docs`, `/review-the-docs`).
+1 -1
View File
@@ -170,7 +170,7 @@ buttons.
must read one of them in the owner, or the next refetch will discard array
edits. (Good examples:
`components/interfaces/Settings/Database/ConnectionLogging.tsx`,
`components/interfaces/Storage/EditBucketModal.tsx`.)
`components/interfaces/Storage/FilesBuckets/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
+26 -22
View File
@@ -59,9 +59,6 @@ git diff --name-only master...HEAD
3. **Run type-specific checks** from the matching sections below on the current branch (no checkout step). Typical commands:
```bash
# Content / tutorial MDX (lints the whole content/ tree; no per-file scoping)
cd apps/docs && pnpm lint:mdx
# Pipeline / schema handler
cd apps/docs && pnpm build:guides-markdown
# inspect public/markdown/guides/ for affected pages
@@ -109,17 +106,17 @@ Inspect changed files from `gh pr view` or:
gh pr diff <number> --repo supabase/supabase --name-only
```
| PR type | Path signals | Primary skill section |
| --------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| **Markdown-schema handler** | `apps/docs/internals/markdown-schema/`, `generate-guides-markdown.ts` | [Schema handler review](#schema-handler-review) |
| **Pipeline / internals** | `apps/docs/internals/` (not just one new handler) | [Pipeline review](#pipeline-review) |
| **Content-only MDX** | `apps/docs/content/**` only | [Content review](#content-review) |
| **Tutorial / quickstart** | `apps/docs/content/guides/**/tutorials/`, `quickstarts/`, plus `examples/` | [Tutorial review](#tutorial-review) → also `work-linear-issue` |
| **Example app only** | `examples/**` without matching MDX | [Example review](#example-review) |
| **Studio ↔ docs links** | `apps/studio/**` | [Studio review](#studio-review) |
| **Docs UI / components** | `apps/docs/components/`, `apps/docs/features/` (no pipeline) | [Component review](#component-review) |
| **Docs tooling** | `.agents/skills/`, `apps/docs/AGENTS.md`, `apps/docs/CONTRIBUTING.md`, `apps/docs/DEVELOPERS.md` | [Docs tooling review](#docs-tooling-review) |
| **Mixed** | Multiple path groups above | Run each applicable section; note overlap |
| PR type | Path signals | Primary skill section |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Markdown-schema handler** | `apps/docs/internals/markdown-schema/`, `generate-guides-markdown.ts` | [Schema handler review](#schema-handler-review) |
| **Pipeline / internals** | `apps/docs/internals/` (not just one new handler) | [Pipeline review](#pipeline-review) |
| **Content-only MDX** | `apps/docs/content/**` only | [Content review](#content-review) |
| **Tutorial / quickstart** | `apps/docs/content/guides/**/tutorials/`, `quickstarts/`, plus `examples/` | [Tutorial review](#tutorial-review) → also `work-linear-issue` |
| **Example app only** | `examples/**` without matching MDX | [Example review](#example-review) |
| **Studio ↔ docs links** | `apps/studio/**` | [Studio review](#studio-review) |
| **Docs UI / components** | `apps/docs/components/`, `apps/docs/features/` (no pipeline) | [Component review](#component-review) |
| **Docs tooling** | `.agents/skills/`, `apps/docs/AGENTS.md`, `apps/docs/CONTRIBUTING.md`, `apps/docs/DEVELOPERS.md`, `apps/docs/style-guide/` | [Docs tooling review](#docs-tooling-review) |
| **Mixed** | Multiple path groups above | Run each applicable section; note overlap |
When a PR spans types (e.g. schema handler + component refactor), run **all** matching sections.
@@ -205,13 +202,22 @@ Verify both guides and reference output when `generate-reference-markdown.ts` or
MDX prose, partials, navigation — no pipeline or example changes.
```bash
cd apps/docs
pnpm lint:mdx # lints the whole content/ tree; filter the output to your changed paths
```
Check the prose against the [style guide](../../../apps/docs/style-guide/README.md) yourself. No CI or local
check covers style or terminology. CodeRabbit reviews style, terminology, and
structure on `apps/docs/content/**/*.mdx`, but only once the PR is open.
Check [`WORD_LIST.md`](../../../apps/docs/style-guide/WORD_LIST.md) against the finished page last, using the
two-pass protocol in [Use with an AI agent](../../../apps/docs/style-guide/WORD_LIST.md#use-with-an-ai-agent): [Phrase
groups](../../../apps/docs/style-guide/WORD_LIST.md#phrase-groups) for the literal term lists, then `grep '^### '
WORD_LIST.md` and read only the entries matching words on the page. Include terms
the author didn't introduce.
Checklist:
- [ ] Voice follows [`01-voice-and-tone.md`](../../../apps/docs/style-guide/01-voice-and-tone.md)
- [ ] Section grouping and chunking follow [`03-page-structure.md`](../../../apps/docs/style-guide/03-page-structure.md)
- [ ] Components follow [`02-elements.md`](../../../apps/docs/style-guide/02-elements.md)
- [ ] Terminology matches [`WORD_LIST.md`](../../../apps/docs/style-guide/WORD_LIST.md)
- [ ] Frontmatter valid (`title`, `description` where required)
- [ ] Internal links resolve (`/docs/guides/...`, not broken anchors)
- [ ] `$CodeSample` paths match existing example directories
@@ -228,9 +234,6 @@ Compare PR preview URL (from Vercel/deployment comment) against production for v
Tutorial MDX plus matching example app. **Read [`work-linear-issue`](https://github.com/supabase/docs-agent-skills/blob/main/.claude/skills/work-linear-issue/SKILL.md)** for full platform E2E — review is not complete without it when auth flows are involved.
```bash
# MDX lint
cd apps/docs && pnpm lint:mdx # then check output for content/guides/getting-started/tutorials/<path>
# Example build (from work-linear-issue)
cd examples/<example-dir>
npm install && npm run build
@@ -298,7 +301,8 @@ Checklist:
- [ ] `.claude/skills` is still a single Git symlink to `../.agents/skills` — no per-skill symlinks or copies under `.claude/`
- [ ] Cross-skill links resolve: relative for in-repo skills; absolute `docs-agent-skills` URLs only for skills that remain in that private repo
- [ ] No personal vault paths, Obsidian references, or private-process-only instructions
- [ ] `apps/docs/CONTRIBUTING.md` / `DEVELOPERS.md` pointers match skill names and checklist stages
- [ ] `apps/docs/CONTRIBUTING.md` / `DEVELOPERS.md` / `AGENTS.md` pointers match skill names and checklist stages
- [ ] A style rule added to a skill belongs in `apps/docs/style-guide/` instead, with the skill pointing at it
- [ ] Reference files under a skill stay near the ~250-line guideline (split if bloated)
```bash
+3 -3
View File
@@ -5,14 +5,14 @@ description: >-
sandbox (runner container + local Supabase stack via `supabase start`). Use
after Draft or during Self-review when asked to test the docs, fact-check
CLI/SQL/code samples, or produce a verification report for a docs PR.
Complements review-the-docs lint/build checks; does not replace them.
Complements review-the-docs build and review checks; does not replace them.
---
# Test the docs
Runs procedural docs content **inside disposable containers**, not on the host shell and not against production. Produces a verification report for the PR body / self-review note.
For lint, markdown rebuilds, example-app triage, and PR review, use [`review-the-docs`](../review-the-docs/SKILL.md). For Frame/Shape and cross-repo product lookup, use [`pm-the-docs`](../pm-the-docs/SKILL.md).
For markdown rebuilds, example-app triage, and PR review, use [`review-the-docs`](../review-the-docs/SKILL.md). For Frame/Shape and cross-repo product lookup, use [`pm-the-docs`](../pm-the-docs/SKILL.md).
## When to invoke
@@ -92,5 +92,5 @@ Write a verification report per [reference/verification-report.md](reference/ver
## Related skills
- [`write-the-docs`](../write-the-docs/SKILL.md) — Draft; hands off here before PR
- [`review-the-docs`](../review-the-docs/SKILL.md) — lint/build/classify; consumes verification report
- [`review-the-docs`](../review-the-docs/SKILL.md) — build/classify; consumes verification report
- [`pm-the-docs`](../pm-the-docs/SKILL.md) — Frame/Shape; universe for cross-repo product lookup
+15 -15
View File
@@ -3,7 +3,7 @@ name: write-the-docs
description: >-
Draft new or updated Supabase docs content for a feature or launch,
grounded in product intent (Linear when available), a read of the actual
code, and the docs style guide once one exists. Use when asked to write
code, and the docs style guide. Use when asked to write
docs for a new feature, a product launch, or a Linear ticket that needs
net-new content rather than a bug fix. Not for implementing existing docs
bug reports — use work-linear-issue for that. Not for restructuring
@@ -18,7 +18,7 @@ Drafts net-new Supabase docs content (or product-grounded rewrites) for a featur
1. **Gather before drafting.** Never draft from a ticket title alone. Pull all four inputs below first; a thin gather phase produces a draft that's wrong about how the feature actually works.
2. **Separate confirmed behavior from product intent from inference.** Code tells you what the feature does today. Linear/PRD/PRFAQ (or prior Frame/Shape output) tells you what it's meant to do and how it should be positioned. Anything you had to guess, flag explicitly rather than stating it as fact.
3. **Follow CONTRIBUTING.md and WORD_LIST.md for voice, terminology, and formatting only, never for content accuracy.** These are a style reference, not a source of truth: rule 2's Linear+code read is what governs what the page actually says. Don't silently invent voice/structure rules either; name the nearest existing-page precedent you followed instead (see [reference/style-fallback.md](reference/style-fallback.md)).
3. **Follow the [style guide](../../../apps/docs/style-guide/README.md) for writing conventions, including voice, terminology, formatting, page structure, and elements. Never follow it for content accuracy.** It is a style reference, not a source of truth: rule 2's Linear+code read is what governs what the page actually says. Don't invent voice or structure rules. When the guide and Google's developer documentation style guide both come up short, say so in the handoff rather than copying whatever the nearest page happens to do.
4. **Reuse, don't duplicate.** For docs-app architecture/placement questions, use [`ask-the-docs`](../ask-the-docs/SKILL.md) and [`audit-docs-ia`](https://github.com/supabase/docs-agent-skills/blob/main/.claude/skills/audit-docs-ia/SKILL.md) rather than re-deriving that knowledge here.
5. **Know what you're actually drafting.** Not everything that looks like "docs for a feature" is a hand-written page — see the content-type gate below before you start writing. If the ask is restructure, reorder, connective text, or clarity on an existing page (no new product story), use [`edit-the-docs`](../edit-the-docs/SKILL.md) instead.
@@ -26,7 +26,7 @@ Drafts net-new Supabase docs content (or product-grounded rewrites) for a featur
Four inputs, read in this sequence (sequence, not priority; Linear remains the product-intent source and code remains the behavior source per rule 2 and Phase 1 step 3):
1. **Style guide — voice/terminology reference.** Start with [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) and [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md) for voice, structure, and terminology. If those don't cover the case, fall back to the nearest comparable existing page under `apps/docs/content/` and say explicitly: _"no dedicated style guide yet — following the precedent of `<page>`."_ See [reference/style-fallback.md](reference/style-fallback.md).
1. **Style guide — voice/terminology reference.** Read [`style-guide/README.md`](../../../apps/docs/style-guide/README.md) and follow the step that matches what you're drafting. Check [`WORD_LIST.md`](../../../apps/docs/style-guide/WORD_LIST.md) for the terms you plan to introduce. The guide's closing section says what to consult when it's silent; follow that rather than matching a neighbouring page.
2. **Linear — the ticket and its product context.** Linear is an internal Supabase tool: preferred when available, not required for open-source contributors. When a Linear issue is available, pull it, then its parent project/initiative description too (PRD, PRFAQ, RFC, or initiative narrative) and any PM comments. Product framing/positioning language usually lives one level up from the ticket, in the parent project or initiative description rather than the ticket body. Distinguish scope the ticket actually commits to from aspirational language in the PRD. If there is no Linear issue and no prior Frame/Shape product-intent output, stop drafting: ask internal authors for a Linear URL, otherwise hand off to [`pm-the-docs`](../pm-the-docs/SKILL.md) (Frame) and [`ask-the-docs`](../ask-the-docs/SKILL.md) when Shape/IA is unsettled. Resume only after product intent exists — never invent positioning, and never run Frame/Shape inside this Draft skill.
3. **Code.** Read the actual implementation before writing a single behavior claim — the PRD describes intent, the code describes what shipped. Check the Linear issue/project first for a linked `supabase/supabase` PR — its diff and description are the most precise "what actually shipped" source, more precise than a general codebase read. If no PR is linked, locate the feature directly in `supabase/supabase` (or the product's own repo), and apply [`ask-the-docs`](../ask-the-docs/SKILL.md)'s reuse/minimalism lens: understand what exists before describing it. When behavior spans services (CLI, Auth, migrations, platform, …), follow [`pm-the-docs`](../pm-the-docs/SKILL.md) → [universe-lookup](../pm-the-docs/reference/universe-lookup.md) **capability gate** (universe when accessible, else OSS public search / linked repos — not `ask-the-docs`). If code and PRD disagree, the code wins for behavior claims — flag the mismatch rather than silently picking one.
4. **Whatever else the author supplies.** Screenshots, example projects, related pages, Slack threads, a specific voice sample. Screenshots are for more than general context — use them to verify the _exact_ button/menu/field labels before writing instructional steps that reference them; a mismatched UI label is one of the easiest, most avoidable errors in a draft. Ask for these when the feature's user-facing shape is still unclear after 1–3, rather than guessing.
@@ -49,18 +49,16 @@ When in doubt, ask `ask-the-docs` rather than guessing — this classification i
- Place the page using existing IA precedent; for a placement call that isn't obvious, consult [`audit-docs-ia`](https://github.com/supabase/docs-agent-skills/blob/main/.claude/skills/audit-docs-ia/SKILL.md)'s nav/IA knowledge rather than guessing a nav slot.
- **Wire it into navigation, not just onto disk.** Placement (which section) and nav enablement (whether it actually shows up) are separate — confirm the current nav-registration mechanism via `ask-the-docs`/`audit-docs-ia` rather than assuming a page is discoverable just because the file exists in the right folder.
- Ground every behavior claim in Phase 1's code read (the linked PR when there is one); ground every "why this matters" framing in Linear/PM context or prior Frame/Shape output; mark inferred material inline (e.g. an HTML comment or a flagged line in the handoff summary) so a reviewer can find it fast.
- **Write for timelessness.** Prefer documenting what exists now over promising future features. See [reference/common-pitfalls.md](reference/common-pitfalls.md#2-timeless-documentation).
- **Keep it concise and avoid redundancy.** See [reference/common-pitfalls.md](reference/common-pitfalls.md#4-redundancy-and-over-explanation).
- **Prefer paragraphs over single-item lists.** See [reference/common-pitfalls.md](reference/common-pitfalls.md#5-single-item-lists).
- **Write timeless documentation, cut redundancy, and prefer a paragraph to a single-item list.** See [timeless documentation](../../../apps/docs/style-guide/03-page-structure.md#write-timeless-documentation), [brevity](../../../apps/docs/style-guide/01-voice-and-tone.md#brevity), and [lists](../../../apps/docs/style-guide/02-elements.md#lists).
- **Strip internal business context before the final draft.** HTML comments flagging PRD intent, roadmap speculation, internal ticket discussions, or "gap-fill" notes must be removed from MDX before handoff. Open-source docs shouldn't expose internal planning. Flag assumptions and open questions for reviewers in the PR description instead, not in the shipped content.
- Search [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md) when introducing or reviewing technical terms, UI actions, abbreviations, and potentially ambiguous language during drafting. This targeted search supplements, but does not replace, the full-file compliance check in Phase 2.5.
- Search [`WORD_LIST.md`](../../../apps/docs/style-guide/WORD_LIST.md) when introducing or reviewing technical terms, UI actions, abbreviations, and potentially ambiguous language during drafting. Use the two-pass protocol in [Use with an AI agent](../../../apps/docs/style-guide/WORD_LIST.md#use-with-an-ai-agent) rather than reading the file end to end.
- Reuse repeated content through `apps/docs/content/_partials/` instead of copying it. For nav wiring, partials, and file placement, see [`ask-the-docs`](../ask-the-docs/SKILL.md)'s `app-map.md` and `federated-docs.md`.
## Phase 2.5 — Review checklist
Before handing off, confirm:
- [ ] CONTRIBUTING.md / WORD_LIST.md followed, or precedent page named explicitly
- [ ] Style guide followed, or the gap named explicitly in the handoff
- [ ] Every behavior claim traces to the code read (ideally the linked PR), not just the PRD
- [ ] Every "why it matters" / positioning line traces to Linear/PM context or prior Frame/Shape output, not invented
- [ ] Inferred or assumed material is flagged, not stated as fact
@@ -75,28 +73,30 @@ Before handing off, confirm:
### Compliance checklist
Re-read [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) and [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md) in full before handoff, not just the sections searched during drafting. When a dedicated style guide lands in the repo, extend this checklist to cover it too.
Before handoff, run both passes from [Use with an AI agent](../../../apps/docs/style-guide/WORD_LIST.md#use-with-an-ai-agent): read [Phrase groups](../../../apps/docs/style-guide/WORD_LIST.md#phrase-groups) for the literal term lists, then `grep '^### ' WORD_LIST.md` and read only the entries matching words on the page. Cover terms you didn't introduce, not just the ones you searched while drafting. Then check the draft against each numbered file in the [style guide](../../../apps/docs/style-guide/README.md).
- [ ] Parentheses used only for acronyms or `(Optional)`, not prose asides
- [ ] Bold, italics, and code used only for their distinct purposes (UI labels, must-not-miss terms), not for visual emphasis alone
- [ ] No dash-based asides where a direct sentence reads better
- [ ] Terminology matches `WORD_LIST.md` (including any terms flagged as imprecise, not just spelling/capitalization)
- [ ] Headings, admonitions, and links follow CONTRIBUTING.md's "Styling, formatting, and grammar" and "Components and elements" sections
- [ ] Terminology matches [`WORD_LIST.md`](../../../apps/docs/style-guide/WORD_LIST.md) (including any terms flagged as imprecise, not just spelling/capitalization)
- [ ] Voice, tense, and brevity follow [`01-voice-and-tone.md`](../../../apps/docs/style-guide/01-voice-and-tone.md)
- [ ] Document type, section grouping, and chunking follow [`03-page-structure.md`](../../../apps/docs/style-guide/03-page-structure.md)
- [ ] Admonitions, headings, links, and other components follow [`02-elements.md`](../../../apps/docs/style-guide/02-elements.md)
## Phase 3 — Handoff
This skill stops at a reviewable draft. It does not open worktrees or PRs itself:
- **Offer** [`test-the-docs`](../test-the-docs/SKILL.md) when the draft includes runnable procedural snippets. Ask before starting verification. Gate prerequisites **per artifact class** (Docker Compose stack profile for DB/API artifacts; examples profile / Node in-runner for `example-app`). If declined, or a required prerequisite for that class is missing, record `deferred` for those artifacts only and continue. When accepted, attach the verification report to the PR body / self-review note.
- Then run [`review-the-docs`](../review-the-docs/SKILL.md) local self-review (lint/build/classify).
- Then run [`review-the-docs`](../review-the-docs/SKILL.md) local self-review (build/classify).
- Hand off to [`create-pull-request`](https://github.com/supabase/docs-agent-skills/blob/main/.claude/skills/create-pull-request/SKILL.md) (and [`work-linear-issue`](https://github.com/supabase/docs-agent-skills/blob/main/.claude/skills/work-linear-issue/SKILL.md) if the ticket needs a full worktree+PR flow) for the actual PR mechanics. Carry the Phase 1/2 flagged-assumptions list forward explicitly into that handoff — it belongs in the PR description (e.g. a "needs review" section) so a reviewer sees it, not just as an inline comment buried in the draft.
- If the feature is UI-driven and the PR will need screenshots/GIFs, flag [`proof-it-works`](https://github.com/supabase/docs-agent-skills/blob/main/.claude/skills/proof-it-works/SKILL.md) as the next step rather than capturing evidence here.
- Before opening the PR, run [`review-the-docs`](../review-the-docs/SKILL.md) local self-review: `pnpm lint:mdx`, `pnpm build:guides-markdown` where applicable, and anchor checks per [reference/drafting-mechanics.md](reference/drafting-mechanics.md).
- Before opening the PR, run [`review-the-docs`](../review-the-docs/SKILL.md) local self-review: `pnpm build:guides-markdown` where applicable, and anchor checks per [reference/drafting-mechanics.md](reference/drafting-mechanics.md).
## Additional resources
- Style / terminology: [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md), [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md), [reference/style-fallback.md](reference/style-fallback.md)
- Common pitfalls to avoid: [reference/common-pitfalls.md](reference/common-pitfalls.md)
- Style: [`style-guide/README.md`](../../../apps/docs/style-guide/README.md) routes to the file for each level
- Repo mechanics (nav wiring, partials, reference pipeline): [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md)
- Drafting mechanics: [reference/drafting-mechanics.md](reference/drafting-mechanics.md)
- Content-type gate detail: [reference/content-type-gate.md](reference/content-type-gate.md)
- Existing-page restructure/clarity: [`edit-the-docs`](../edit-the-docs/SKILL.md)
@@ -1,94 +0,0 @@
# Common pitfalls in docs drafts
Patterns to watch for when drafting docs, based on review feedback. These are guidelines, not absolute rules — use judgment based on context.
## 1. Internal planning context in open-source docs
**Principle:** Supabase docs are open source. Internal planning context, unshipped features, and business intent shouldn't be visible in the public repo.
**What to watch for:**
- HTML comments referencing PRDs, roadmaps, or internal ticket discussions
- "Gap-fill" notes about what's planned but not shipped
- Internal product strategy or positioning discussions
- References to any project-management or ticketing system (internal or a contributor's own tools)
**Where to put internal context instead:**
- PR description (for review-time context)
- The team's project-management tool (for product/PM handoff)
- Internal docs (for roadmap tracking)
## 2. Timeless documentation
**Principle:** Prefer documenting what exists now over promising future features ([Google's timeless documentation](https://developers.google.com/style/timeless-documentation)). Future promises become stale.
**Common patterns to watch for:**
- "Coming soon" / "will be available" / "once finalized"
- "This page is a placeholder"
- "Being rolled out gradually" without concrete eligibility criteria
**Better alternatives:**
- Document what exists today
- Wait to publish until the feature is complete
- If phased rollout is real, be specific: "Available to organizations on Pro and Enterprise plans"
**Context matters:** Changelog and roadmap content naturally references the future — this guidance applies primarily to feature documentation.
## 3. Placeholder pages
**Principle:** Generally avoid shipping pages that explicitly say "This is a placeholder" or "More details coming soon."
**Alternatives:**
- Wait to publish until content is ready
- If navigation structure requires it, link to external resources that are complete
- Ship minimal but useful content (what's true today) rather than promises
**Valid exceptions:** Navigation structure needs, federated docs where the placeholder provides context and links out, cross-references where the page existing (even minimal) provides value.
## 4. Redundancy and over-explanation
**Principle:** Avoid restating the same point in multiple ways. Prefer brevity — sometimes broad strokes help more than mechanical detail.
**Common patterns:**
- Multiple ways of saying the same thing: "It's free" + "You won't be billed" + "No charge"
- Admonition stating a risk, then body text restating it verbatim
- Adjacent sentences that rephrase each other
- Over-explaining mechanical steps or implementation detail that doesn't help the reader complete the task
**How to catch it:** If you can remove a sentence without losing information, it's probably redundant. If a procedure works with less setup explanation, prefer the shorter path.
## 5. Single-item lists
**Principle:** Prefer paragraphs over single-item bullet lists.
**Why:** Single-item lists can signal incomplete content.
**Valid exceptions:** Layout consistency across sections, future expansion expected, or when the item needs special visual emphasis.
## 6. Restating admonition content
**Principle:** Admonitions and body text should cover distinct points, not repeat each other.
**What to watch for:** An admonition stating a risk/limitation, then the next paragraph restating it verbatim.
## Summary checklist
Before submitting a draft, check:
- [ ] No HTML comments with internal PRD/roadmap/ticket context in MDX
- [ ] Future promises minimized where appropriate (timeless documentation)
- [ ] Placeholder pages avoided where possible
- [ ] No unnecessary redundancy or mechanical over-explanation
- [ ] Single-item lists avoided unless there's a reason
- [ ] Admonitions and body text cover distinct points
**Note:** These are guidelines based on review feedback, not absolute rules. Use judgment based on content type, context, and the specific documentation needs. The goal is clearer, more maintainable docs, not rigid adherence to formatting rules.
## Style guide consolidation
This content is style guidance, not skill-specific process. It belongs in a shared, human-and-agent-readable style guide rather than only inside this skill. When a dedicated style guide exists in the repo, fold this content into it and replace this file with a pointer, the same pattern [reference/style-fallback.md](style-fallback.md) already uses for CONTRIBUTING.md/WORD_LIST.md.
@@ -1,8 +1,10 @@
# Drafting mechanics
Mechanics that come up during drafting but aren't worth duplicating from
`CONTRIBUTING.md` or `ask-the-docs`. For nav wiring, partials, and file
placement, see `ask-the-docs`'s `app-map.md` and `federated-docs.md`.
Mechanics that come up during drafting but aren't worth duplicating from the
style guide or `ask-the-docs`. For style rules, see
[`apps/docs/style-guide/`](../../../../apps/docs/style-guide/README.md). For nav
wiring, partials, and file placement, see `ask-the-docs`'s `app-map.md` and
`federated-docs.md`.
## Link paths
@@ -29,18 +31,15 @@ independent of its wording, pin it with a custom anchor, for example
From `apps/docs`:
```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.
`pnpm build:guides-markdown` only applies to guides, explainers, and tutorials.
From the repository root, run `pnpm format` to apply Prettier to changed MDX
files. This enforces repo-wide formatting rules, including lowercase SQL
keyword casing in code samples.
Treat `supa-mdx-lint` replacements as suggestions when context matters. Rewrite
the sentence instead of applying a replacement that changes its technical
meaning.
Check terminology against [`WORD_LIST.md`](../../../../apps/docs/style-guide/WORD_LIST.md)
yourself. Treat its replacements as suggestions when context matters: rewrite the
sentence instead of applying one that changes its technical meaning.
@@ -1,22 +0,0 @@
# Style guide fallback
There is no separate published style guide yet beyond what already lives in
this repo. Use the public sources below, in order.
This fallback governs voice, formatting, and terminology only. It's not a
source of truth for behavior or product framing (that's the Gather phase's
Linear + code read, per SKILL.md rule 3).
1. Read [`apps/docs/CONTRIBUTING.md`](../../../../apps/docs/CONTRIBUTING.md)
for authoring conventions (voice, structure, document types).
2. Read [`apps/docs/WORD_LIST.md`](../../../../apps/docs/WORD_LIST.md) for
preferred spelling, capitalization, and terminology.
3. If those don't cover the case, find the nearest comparable existing page
under `apps/docs/content/` (same product area, similar content type —
reference vs. guide vs. quickstart) and follow its voice, heading
structure, and code-sample conventions.
4. State which page you followed as precedent in the handoff summary — e.g.
"no dedicated style guide yet — following the precedent of
`guides/storage/uploads.mdx`."
5. When a dedicated style guide is added to the repo later, prefer it over
precedent-matching and drop this fallback step.
+14 -1
View File
@@ -10,6 +10,7 @@ issue_enrichment:
enabled: true
reviews:
high_level_summary: false
# Skip machine-generated / vendored files (mirrors .prettierignore). Keeps
# reviews focused on hand-written code and preserves rate-limit budget on
# large codegen diffs.
@@ -34,7 +35,7 @@ reviews:
Strictly enforce event naming: [object]_[verb] in snake_case. Only approved
verbs: opened, clicked, submitted, created, removed, updated, retrieved,
intended, evaluated, added, enabled, disabled, copied, exposed, failed,
converted. Properties must be camelCase for new events (match existing
converted, completed. Properties must be camelCase for new events (match existing
convention when adding to existing events). Flag any usage of
useSendEventMutation. Verify @group Events and @source JSDoc tags are
accurate. Check that new interfaces are added to the TelemetryEvent union type.
@@ -69,6 +70,14 @@ reviews:
for both runtimes until the final cleanup pass (tracked in FE-3106).
Keep this a reminder to verify, not a hard blocker: if no mirror is required,
say so briefly rather than forcing a change.
- path: 'apps/docs/content/**/*.mdx'
instructions: |
Flag style, terminology, and structure issues as usual. When a page has two or
more of them, add one comment pointing the author at the `/write-the-docs` skill
for new content or `/edit-the-docs` for an existing page (canonical files in
`.agents/skills/`); both apply the docs style guide in
apps/docs/style-guide/. Skip that pointer on a single issue, so it stays a
signal that the author isn't using the skills rather than boilerplate.
- path: '{apps,packages}/**/*.{tsx,jsx,css,mdx}'
instructions: |
When reviewing UI changes, flag these accessibility gaps. Comments are
@@ -117,6 +126,10 @@ reviews:
"read more", or "learn more" when it does not describe the
destination. Skip if aria-label or wrapping context already names
where the link goes.
- Color contrast: flag non-large informative text below 4.5:1 and large informative
text (at least 24px regular or 18.5px bold) below 3:1. Skip logotypes and
decorative text. Treat expressive text at 40px or larger as advisory rather
than blocking.
# Applies our internal engineering skills (.agents/skills/) as CodeRabbit review
# guidelines. The skills are the single source of truth — they are consumed
+3
View File
@@ -22,4 +22,7 @@
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
/apps/studio/pages/new/index.tsx @supabase/security
/apps/studio/lib/ai/ @supabase/ai
/apps/studio/pages/api/ai/ @supabase/ai
/packages/shared-data/compute-disk-limits.ts @supabase/platform
+19
View File
@@ -6,3 +6,22 @@ updates:
interval: 'weekly'
cooldown:
default-days: 7
# `pnpm-workspace.yaml`'s `minimumReleaseAge: 4320` (3 days) rejects any
# dependency version younger than 3 days old during `pnpm install`. Without
# a cooldown, Dependabot proposes the newest release the moment it's
# published, so its PRs are structurally guaranteed to fail CI/Vercel until
# the proposed version happens to age past the pnpm gate on its own. This
# cooldown holds Dependabot's proposals back until they've already cleared
# (with a one-day margin for scheduling/CI latency) pnpm's minimum release
# age, so the version pnpm sees is always old enough to be accepted.
- package-ecosystem: 'npm'
directories:
- '/'
- '/apps/*'
- '/packages/*'
- '/blocks/*'
- '/e2e/*'
schedule:
interval: 'weekly'
cooldown:
default-days: 4
-5
View File
@@ -8,8 +8,3 @@ documentation:
self-hosted:
- changed-files:
- any-glob-to-any-file: 'apps/docs/content/guides/self-hosting/**/*'
# Add 'api-deploy-required' to any change in packages/api-types/types
api-deploy-required:
- changed-files:
- any-glob-to-any-file: 'packages/api-types/types/**'
+40 -13
View File
@@ -1,19 +1,46 @@
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file.
## Problem
YES/NO
Describe what went wrong or what need emerged to justify this bug fix, feature, or change.
Link any relevant issues here.
## What kind of change does this PR introduce?
Bug fix, feature, docs update, ...
## What is the current behavior?
Please link any relevant issues here.
## What is the new behavior?
## Solution
Provide a brief description of the change and the key choices you made when architecting the solution.
Feel free to include screenshots if it includes visual changes.
## Additional context
<!--
## Preview links
Add any other context or screenshots.
If relevant, include links to changed pages for easy review access.
Copy the preview base URL from the Vercel bot comment on this PR. Use the following table as an example template.
| Site | Live | Preview | Search for |
| -------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------- |
| WWW | [/blog/your-post](https://supabase.com/blog/your-post) | [/blog/your-post](https://zone-www-dot-com-git-branch-name-supabase.vercel.app/blog/your-post) | unique phrase from the change |
| Docs | [/docs/guides/your-page](https://supabase.com/docs/guides/your-page) | [/docs/guides/your-page](https://docs-git-branch-name-supabase.vercel.app/docs/guides/your-page) | unique phrase from the change |
| Studio | [/dashboard](https://supabase.com/dashboard) | [/dashboard](https://studio-git-branch-name-supabase.vercel.app/dashboard) | unique phrase from the change |
| Design system | [/design-system](https://supabase.com/design-system) | [/design-system](https://design-system-git-branch-name-supabase.vercel.app/design-system) | unique phrase from the change |
| UI library | [/library](https://supabase.com/library) | [/library](https://ui-library-git-branch-name-supabase.vercel.app/library) | unique phrase from the change |
| Knowledge base | [/kb/guides/your-page](https://supabase.com/kb/guides/your-page) | [/kb/guides/your-page](https://kb-git-branch-name-supabase.vercel.app/kb/guides/your-page) | unique phrase from the change |
-->
<!-- ## Additional context
Optionally add any other context or screenshots.
-->
## Review instructions
Provide a clear numbered procedure that the PR reviewer can walk through.
1. For example, `Open the live and preview links side-by-side.`
2. For example, `See the issue is fixed.`
## Checklist
Check all before review:
- [ ] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [ ] If I wrote a new docs topic or edited an existing topic, I used the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs [style guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide)
+5
View File
@@ -129,6 +129,10 @@ jobs:
# Vercel skips the docs preview when a PR only changes the harness
# (e2e/docs, workflow), so wait for a preview only when apps/docs changed.
# When apps/docs changed earlier in the PR but not in the head commit,
# Vercel skips the head build as not affected and its URL serves a
# placeholder page; the script then uses the newest READY preview from
# an earlier commit of the PR, which serves the same docs content.
#
# Vercel's GitHub App stopped writing GitHub Deployment objects on
# 2026-02-17 (broken app auth), so vercel/wait-for-deployment-action
@@ -146,6 +150,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
VERCEL_STATUS_CONTEXT: 'Vercel – docs'
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
@@ -1,67 +0,0 @@
name: docs_lint_comment_external
# This is a continuation of ./docs-lint-v2.yml, to write comments on external
# PRs.
#
# SECURITY:
# This workflow runs with write permissions, in the context of code from an
# external PR. This is safe because no external code is executed. The
# stringified Markdown output from the linter (downloaded as an artifact) is
# directly written as the body of a PR comment.
on:
workflow_run:
workflows: [docs_lint]
types:
- completed
permissions:
pull-requests: write
jobs:
comment_on_pr:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'failure'
steps:
- id: download_artifact
name: 'Download artifact'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }}
});
const matchingArtifact = artifacts?.data?.artifacts?.find(
(artifact) => artifact.name == 'lint_results'
);
if (matchingArtifact) {
core.setOutput('contains_results', 'true')
const download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchingArtifact.id,
archive_format: 'zip',
});
const fs = require('fs');
fs.writeFileSync('${{ github.workspace }}/lint_results.zip', Buffer.from(download.data));
}
- id: unzip_results
name: Unzip results file
if: steps.download_artifact.outputs.contains_results == 'true'
run: unzip lint_results.zip
- name: 'Comment on PR'
if: steps.download_artifact.outputs.contains_results == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const fs = require('fs');
const prNumber = Number(fs.readFileSync('./pr_number.txt'));
const lintResults = fs.readFileSync('./lint_results.txt', 'utf8');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: lintResults
});
@@ -1,62 +0,0 @@
name: '[Docs] Lint v2 (scheduled)'
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
permissions:
contents: write
pull-requests: write
jobs:
lint-all:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: true
sparse-checkout: |
supa-mdx-lint.config.toml
supa-mdx-lint
apps/docs/content
- name: cache cargo
id: cache-cargo
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: da6838d8d6898f28ec9ab432353b2707db9df8f5
- name: install linter
if: steps.cache-cargo.outputs.cache-hit != 'true'
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev da6838d8d6898f28ec9ab432353b2707db9df8f5
- name: run linter
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
supa-mdx-lint apps/docs/content || {
echo "Linter failed, attempting to fix errors..."
git config --global user.name 'github-docs-bot'
git config --global user.email 'github-docs-bot@supabase.com'
BRANCH_NAME="bot/docs-lint-fixes"
EXISTING_BRANCH=$(git ls-remote --heads origin $BRANCH_NAME)
if [[ -n "$EXISTING_BRANCH" ]]; then
git push origin --delete $BRANCH_NAME
fi
git checkout -b $BRANCH_NAME
supa-mdx-lint apps/docs/content --fix || FIX_FAILED=1
git add .
git commit -m '[bot] fix lint errors' || true
git push origin $BRANCH_NAME
gh pr create --title '[bot] fix lint errors' --body 'This PR fixes lint errors in the documentation.' --head $BRANCH_NAME
if [ "${FIX_FAILED:-0}" -eq 1 ]; then
echo "Fix did not correct all errors."
exit 1
fi
}
-108
View File
@@ -1,108 +0,0 @@
name: docs_lint
# Runs the docs linter on PRs that edit docs content.
# There are two branches of this workflow for internal and external PRs, due
# to the security design of GitHub Actions.
#
# Internal PRs:
# Have write permissions, so comments are written directly by reviewdog.
#
# External PRs:
# Have read-only permissions, so lint results are uploaded as an artifact, to
# be written to the PR in a subsequent workflow_run action that has write
# permissions. See ./docs/lint-v2-comment.yml.
#
# See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
on:
pull_request:
env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
permissions:
pull-requests: write
jobs:
supa-mdx-lint:
name: supa-mdx-lint
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
sparse-checkout: |
supa-mdx-lint.config.toml
supa-mdx-lint
apps/docs/content
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
docs:
- 'apps/docs/content/**'
- 'supa-mdx-lint/**'
- 'supa-mdx-lint.config.toml'
- name: cache cargo
id: cache-cargo
if: steps.filter.outputs.docs == 'true'
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: da6838d8d6898f28ec9ab432353b2707db9df8f5
- name: install linter
if: steps.filter.outputs.docs == 'true' && steps.cache-cargo.outputs.cache-hit != 'true'
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev da6838d8d6898f28ec9ab432353b2707db9df8f5
- name: install reviewdog
if: steps.filter.outputs.docs == 'true'
uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.0
with:
reviewdog_version: v0.20.2
- name: run linter (internal)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name == github.repository
env:
BASE_REF: ${{ github.base_ref }}
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o pipefail
git diff --name-only "origin/$BASE_REF" HEAD \
| { grep -E "^apps/docs/content/" || test $? = 1; } \
| xargs -r supa-mdx-lint --format rdf \
| reviewdog -f=rdjsonl -reporter=github-pr-review -tee
- id: external_lint
name: run linter (external)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository
env:
BASE_REF: ${{ github.base_ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -o pipefail
run_lints() {
git diff --name-only "origin/$BASE_REF" HEAD \
| { grep -E "^apps/docs/content/" || test $? = 1; } \
| xargs -rx -n 1000000000 supa-mdx-lint --format markdown
}
set +e
LINT_RESULTS=$(run_lints)
LINT_EXIT_CODE=$?
set -e
echo "LINT_EXIT_CODE=$LINT_EXIT_CODE" >> $GITHUB_OUTPUT
if [[ $LINT_EXIT_CODE -ne 0 ]]; then
mkdir -p ./__github_actions__pr
echo "${{ github.event.number }}" > ./__github_actions__pr/pr_number.txt
echo "$LINT_RESULTS" > ./__github_actions__pr/lint_results.txt
fi
- name: save results as artifact (external)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository && steps.external_lint.outputs.LINT_EXIT_CODE != 0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: lint_results
path: __github_actions__pr/
- name: fail if linter fails (external)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository && steps.external_lint.outputs.LINT_EXIT_CODE != 0
run: exit 1
@@ -0,0 +1,60 @@
name: Ingest Docs Content for Search V2
on:
push:
branches:
- master
paths:
- '.github/workflows/docs-search-v2-ingest.yml'
- 'apps/docs/content/**/*.mdx'
- 'apps/docs/scripts/search_v2/**'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-prod
cancel-in-progress: false
permissions:
contents: read
jobs:
ingest:
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
SEARCH_V2_SUPABASE_PROJECT_ID: ${{ secrets.SEARCH_V2_SUPABASE_PROJECT_ID }}
SEARCH_V2_SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_V2_SUPABASE_SECRET_KEY }}
DOCS_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
DOCS_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
DOCS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
steps:
- name: Check out repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
apps/docs
packages
patches
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
- name: Download dependencies
run: pnpm install --frozen-lockfile
- name: Fetch federated content
working-directory: ./apps/docs
run: pnpm run build:federated-content
- name: Ingest content
working-directory: ./apps/docs
run: pnpm run search-v2:ingest
@@ -0,0 +1,43 @@
name: ESLint Config Tests
on:
pull_request:
branches: ['master']
paths:
- 'packages/eslint-config-supabase/**/*'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
packages/eslint-config-supabase
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: Run tests
run: pnpm --filter eslint-config-supabase run test
+3 -14
View File
@@ -2,6 +2,8 @@ name: 'Pull Request Labeler'
on:
pull_request_target:
paths:
- 'apps/docs/**/*'
jobs:
labeler:
@@ -10,17 +12,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- id: label
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')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'The `api-deploy-required` label was auto-applied to this PR because it updates the API types. Ensure that the new or updated API, if any, is deployed on production before **removing the label** and merging this PR.',
})
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
+60
View File
@@ -0,0 +1,60 @@
name: Library checks
on:
# No branch filter: a stacked pull request targets the branch below it, and
# skipping its checks until the stack reaches master defeats the point.
pull_request:
paths:
- 'apps/ui-library/**'
- 'blocks/vue/**'
- 'packages/ui/**'
- 'packages/ui-patterns/**'
- 'packages/common/**'
- 'packages/icons/**'
- 'packages/shared-data/**'
- 'packages/api-types/**'
- 'packages/config/**'
- 'packages/tsconfig/**'
- 'packages/eslint-config-supabase/**'
- 'patches/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- '.github/workflows/library-tests.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
run_install: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm --filter library test
- run: pnpm --filter library build:registry
- name: Check generated registry
run: |
registry_changes="$(git status --porcelain --untracked-files=all -- apps/ui-library/public/r apps/ui-library/__registry__)"
if [ -n "$registry_changes" ]; then
printf '%s\n' "$registry_changes"
echo 'Run pnpm --filter library build:registry and commit the generated registry files.'
exit 1
fi
- run: pnpm --filter library build
+109
View File
@@ -0,0 +1,109 @@
name: Decrease lint ratchet baselines
on:
schedule:
- cron: '0 0 * * SUN'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
decrease-baselines:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
apps/www
apps/docs
apps/design-system
apps/ui-library
apps/learn
packages
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: Generate token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Decrease ESLint ratchet baselines and open PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -eo pipefail
DEFAULT_BRANCH=${DEFAULT_BRANCH:-master}
BRANCH="bot/decrease-lint-ratchet-baselines"
git fetch origin "$DEFAULT_BRANCH" --depth=1
if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then
git fetch origin "$BRANCH":"$BRANCH" --depth=1
git switch "$BRANCH"
git reset --hard "origin/$DEFAULT_BRANCH"
else
git switch --create "$BRANCH" "origin/$DEFAULT_BRANCH"
fi
# Keep going past an app that regressed so the others still get decreased;
# the job fails at the end if any app did.
failed=""
APPS="www docs design-system ui-library learn"
for app in $APPS; do
pnpm --filter "./apps/$app" run lint:ratchet --decrease-baselines || failed="$failed $app"
done
if git diff --quiet; then
echo "No baseline updates detected."
if [ -n "$failed" ]; then
echo "::error title=Ratchet regressions::New violations on $DEFAULT_BRANCH in:$failed"
exit 1
fi
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
for app in $APPS; do git add "apps/$app/.github/eslint-rule-baselines.json"; done
git commit --message "chore: decrease lint ratchet baselines"
git -c credential.helper= push --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${BRANCH}"
pr_url=$(gh pr list --state open --head "$BRANCH" --json url --jq '.[0].url // ""' 2>/dev/null || echo "")
if [ -z "$pr_url" ]; then
gh pr create \
--title "[bot] Decrease lint ratchet baselines" \
--body "Automated weekly decrease of lint ratchet baselines." \
--base "$DEFAULT_BRANCH" \
--head "$BRANCH"
else
gh pr comment "$pr_url" --body "Updated lint ratchet baselines with the latest weekly decreases."
fi
if [ -n "$failed" ]; then
echo "::error title=Ratchet regressions::New violations on $DEFAULT_BRANCH in:$failed"
exit 1
fi
+85
View File
@@ -0,0 +1,85 @@
name: Ratchet lint checks
on:
pull_request:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
apps: ${{ steps.filter.outputs.changes }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
shared: &shared
- 'packages/**'
- 'pnpm-lock.yaml'
www:
- *shared
- 'apps/www/**'
docs:
- *shared
- 'apps/docs/**'
design-system:
- *shared
- 'apps/design-system/**'
ui-library:
- *shared
- 'apps/ui-library/**'
learn:
- *shared
- 'apps/learn/**'
ratchet:
needs: changes
if: ${{ needs.changes.outputs.apps != '[]' }}
# Uses larger hosted runner as it significantly decreases build times
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(needs.changes.outputs.apps) }}
exclude:
- app: shared
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
apps/${{ matrix.app }}
packages
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
# pnpm runs the script from the app directory, which is where the
# baselines were generated, so per-file keys line up.
- name: Run ratchet script
env:
APP: ${{ matrix.app }}
run: pnpm --filter "./apps/$APP" run lint:ratchet
@@ -47,6 +47,7 @@ jobs:
permission-pull-requests: write
- name: Decrease ESLint ratchet baselines and open PR
id: decrease-baselines
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
@@ -69,6 +70,7 @@ jobs:
if git diff --quiet; then
echo "No baseline updates detected."
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -81,11 +83,45 @@ jobs:
pr_url=$(gh pr list --state open --head "$BRANCH" --json url --jq '.[0].url // ""' 2>/dev/null || echo "")
if [ -z "$pr_url" ]; then
gh pr create \
pr_url=$(gh pr create \
--title "[bot] Decrease ESLint ratchet baselines" \
--body "Automated weekly decrease of ESLint ratchet baselines." \
--base "$DEFAULT_BRANCH" \
--head "$BRANCH"
--head "$BRANCH")
else
gh pr comment "$pr_url" --body "Updated ESLint ratchet baselines with the latest weekly decreases."
fi
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT"
- name: 'Notify #team-frontend about rules that hit a zero baseline'
if: steps.decrease-baselines.outputs.changed == 'true'
env:
PR_URL: ${{ steps.decrease-baselines.outputs.pr_url }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DASHBOARD_WEBHOOK_URL }}
run: |
set -euo pipefail
zero_rules=$(jq -r '.rules | to_entries | map(select(.value == 0 and (.key | startswith("shadcn/") | not)) | .key) | join(", ")' apps/studio/.github/eslint-rule-baselines.json)
if [ -z "$zero_rules" ]; then
echo "No rules dropped to a baseline of 0; nothing to notify."
exit 0
fi
if [ -z "${SLACK_WEBHOOK_URL:-}" ]; then
echo "::warning::SLACK_DASHBOARD_WEBHOOK_URL secret is not set; skipping Slack notification for zero-baseline rules: $zero_rules"
exit 0
fi
pr_number="${PR_URL##*/}"
text="<@U0A1BRW39PC> the weekly ratchet-baseline job just dropped these rules to 0 in <${PR_URL}|#${pr_number}>: ${zero_rules}. Can you remove them from ratchet tracking and bump their ESLint severity to \"error\"?"
payload=$(jq -n --arg text "$text" '{text: $text}')
if ! curl --fail --silent --show-error -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$payload"; then
echo "::warning::Failed to post Slack notification for zero-baseline rules: $zero_rules"
fi
@@ -6,6 +6,8 @@ on:
- master
paths:
- 'apps/studio/**'
- 'packages/**'
- 'pnpm-lock.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+4
View File
@@ -8,6 +8,8 @@ on:
branches: [master, studio]
paths:
- 'apps/studio/**'
- 'packages/common/sentry.ts'
- 'packages/common/sentry.test.ts'
- 'packages/ui/**'
- 'packages/ui-patterns/**'
- 'pnpm-lock.yaml'
@@ -53,6 +55,8 @@ jobs:
- 'packages/ui/**'
- 'packages/ui-patterns/**'
- 'apps/studio/**'
- 'packages/common/sentry.ts'
- 'packages/common/sentry.test.ts'
- 'pnpm-lock.yaml'
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
if: steps.filter.outputs.relevant == 'true'
-6
View File
@@ -17,12 +17,6 @@ jobs:
echo "PR blocked: [tag: do not merge]"
exit 1
- name: Tagged with 'api-deploy-required'
if: contains( github.event.pull_request.labels.*.name, 'api-deploy-required')
run: |
echo "PR blocked: [tag: api-deploy-required] — confirm the API is deployed in production, then remove the label."
exit 1
- name: All good
if: ${{ success() }}
run: |
@@ -0,0 +1,50 @@
name: Verify production API types
on:
pull_request:
types: [opened, reopened, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
verify-production-api-types:
runs-on: ubuntu-latest
steps:
- id: changes
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --paginate --jq '.[].filename' | grep -q '^packages/api-types/types/'; then
echo "api_types_changed=true" >> "$GITHUB_OUTPUT"
else
echo "api_types_changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Check out pull request
if: steps.changes.outputs.api_types_changed == 'true'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- name: Install pnpm
if: steps.changes.outputs.api_types_changed == 'true'
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
with:
run_install: false
- name: Set up Node.js
if: steps.changes.outputs.api_types_changed == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version-file: '.nvmrc'
cache: pnpm
- name: Install API types dependencies
if: steps.changes.outputs.api_types_changed == 'true'
run: pnpm install --frozen-lockfile --filter=api-types...
- name: Verify production API types
if: steps.changes.outputs.api_types_changed == 'true'
run: pnpm --filter=api-types run verify-production-types
+5 -1
View File
@@ -120,7 +120,10 @@ jobs:
cache: 'pnpm'
# Vercel skips the preview when only the harness changed, so wait for one
# only when apps/www changed. See scripts/waitForVercelPreview.js.
# only when apps/www changed. A head commit that leaves apps/www untouched
# gets a skipped build whose URL serves a placeholder page, so the script
# falls back to the newest READY preview from an earlier commit of the PR.
# See scripts/waitForVercelPreview.js.
- name: Wait for Vercel www preview
if: steps.scope.outputs.skip == 'false' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.changes.outputs.www_app == 'true'
id: deployment
@@ -129,6 +132,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
VERCEL_STATUS_CONTEXT: 'Vercel – zone-www-dot-com'
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
@@ -0,0 +1,60 @@
name: Partner intake form / HubSpot sync check
# Deliberately its own workflow, path-filtered to only the files this check
# actually concerns — NOT part of www-tests.yml's `pnpm test` run, which
# gates every apps/www PR. A HubSpot-side change surfaced by the scheduled
# www-partner-form-sync.yml bot PR (which touches only the generated
# snapshot) should fail *this* check, not block unrelated apps/www work.
on:
pull_request:
branches: ['master']
paths:
- 'apps/www/components/Partners/PartnerIntakeForm.fields.ts'
- 'apps/www/components/Partners/PartnerIntakeForm.sync.test.ts'
- 'apps/www/components/Partners/__generated__/partner-intake-form.hubspot.json'
- 'apps/www/lib/staticFormCrm.ts'
- 'apps/www/vitest.sync.config.ts'
- 'apps/www/package.json'
- '.github/workflows/www-partner-form-sync-check.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CI: true
jobs:
check:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
apps/www
packages
supabase
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: Run partner intake form / HubSpot sync check
run: pnpm run test:partner-form-sync
working-directory: ./apps/www
@@ -0,0 +1,78 @@
name: Sync partner intake form from HubSpot
on:
schedule:
# Run at 00:00 UTC every Monday
- cron: '0 0 * * 1'
workflow_dispatch:
permissions:
pull-requests: write
contents: write
jobs:
sync-partner-form:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: ${{ github.ref }}
sparse-checkout: |
apps/www
packages
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: Fetch the live HubSpot form and regenerate the snapshot
run: pnpm run sync:partner-form
working-directory: ./apps/www
- name: Generate token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-pull-requests: write
permission-contents: write
# Only opens/updates a PR when the regenerated snapshot actually
# differs from what's committed — i.e. only when the live HubSpot form
# has changed since the last sync. The PR's existence is the drift
# alert: PartnerIntakeForm.sync.test.ts (run by the normal
# www-tests.yml suite on this PR) fails until a human reconciles
# PartnerIntakeForm.tsx / staticFormCrm.ts with the new snapshot.
- name: Create pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
token: ${{ steps.app-token.outputs.token }}
commit-message: 'chore(www): sync partner intake form snapshot from HubSpot'
title: 'chore(www): partner intake form has changed in HubSpot'
body: |
The live "Become a Partner" HubSpot form no longer matches the
checked-in snapshot at
`apps/www/components/Partners/__generated__/partner-intake-form.hubspot.json`.
`PartnerIntakeForm.sync.test.ts` will fail on this PR until
`apps/www/components/Partners/PartnerIntakeForm.fields.ts` and
`apps/www/lib/staticFormCrm.ts` are updated by hand to match —
see the comment at the top of that test for what it checks.
branch: 'gha/auto-sync-partner-intake-form'
base: 'master'
add-paths: |
apps/www/components/Partners/__generated__/partner-intake-form.hubspot.json
+16
View File
@@ -4,13 +4,27 @@ on:
pull_request:
branches: ['master']
paths:
- '.github/workflows/www-tests.yml'
- 'apps/www/**/*.ts*'
- 'apps/www/package.json'
- 'apps/www/turbo.jsonc'
- 'scripts/upload-static-assets.sh'
- 'packages/common/sentry.ts'
- 'packages/common/sentry.test.ts'
- 'apps/www/next.config.mjs'
- 'apps/www/next.config.js'
- 'apps/www/lib/**/*.js'
- 'apps/www/lib/**/*.mjs'
- 'apps/www/content/md/**'
- 'apps/www/scripts/**/*.mjs'
- 'apps/www/internals/**/*.mjs'
- 'apps/www/_blog/**'
- 'apps/www/_alternatives/**'
- 'apps/www/_customers/**'
- 'apps/www/public/.well-known/**'
# www catalog tests check that linked docs guides exist, so guide changes
# must trigger these tests and their sources must be included in checkout.
- 'apps/docs/content/guides/**'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
@@ -33,7 +47,9 @@ jobs:
persist-credentials: false
sparse-checkout: |
apps/www
apps/docs/content/guides
packages
scripts
supabase
patches
+5
View File
@@ -7,6 +7,8 @@ tmp
*.swp
coverage
# vitest reporter output (json/junit/html/attachments)
.vitest
allure-results
allure-report
.nyc_output
@@ -166,3 +168,6 @@ keys.json
examples/**/package-lock.json
examples/**/yarn.lock
examples/**/pnpm-lock.yaml
# local-only lockfile for the mcp-server registry block
apps/ui-library/registry/default/blocks/mcp-server/supabase/functions/mcp-server/deno.lock
+2 -1
View File
@@ -1,3 +1,4 @@
^./i18n
^./packages/api-types
^./apps/www/lib/redirects.js
^./apps/www/lib/redirects.js
^./apps/studio/public/*
+3 -1
View File
@@ -7,19 +7,21 @@ apps/**/out
.context/**
# prettier-plugin-sql-cst only supports sqlite syntax
**/supabase/migrations/*.sql
**/supabase/schemas/**/*.sql
apps/www/schema.sql
apps/www/public/images/*
# Generated by apps/www/scripts/generateStaticContent.mjs (GitHub discussion bodies)
apps/www/public/changelog.md
apps/www/public/changelog/*.md
apps/www/.generated/*
# Written by apps/www/scripts/syncPartnerIntakeForm.mjs
apps/www/components/Partners/__generated__
apps/docs/**/generated/*
apps/docs/examples/*
examples/slack-clone/nextjs-slack-clone/full-schema.sql
# ignore files with custom js formatting
apps/studio/public
apps/**/.turbo
apps/docs/CONTRIBUTING.md
apps/docs/__generated__
# Generated by apps/docs/internals/generate-markdown-manifest.ts
apps/docs/lib/markdown-manifest.ts
+3 -3
View File
@@ -42,7 +42,7 @@ pnpm api:codegen # platform Management API types → packages/api-ty
## 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.
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 and the Tailwind class rules (`shadcn/*`) for `www`, `docs`, `design-system`, `ui-library`, and `learn`; 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`, `apps/docs/content/_partials/access-control/scoped_pat_*.mdx` (run `make -C apps/docs/spec generate.partials.access-control`).
@@ -60,10 +60,10 @@ Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.ge
## Skills
The skills in `.agents/skills/` are the source of truth for conventions — load the relevant ones before working, don't guess:
The skills in `.agents/skills/` are the source of truth for conventions. Load the relevant ones before working, don't guess. One exception: for docs **content** style, `apps/docs/style-guide/` is the source of truth and the docs skills are the process that applies it.
- `copywriting` — any user-facing text, anywhere in the monorepo
- `pm-the-docs` / `write-the-docs` / `edit-the-docs` / `ask-the-docs` / `review-the-docs` — anything under `apps/docs` (see `apps/docs/CONTRIBUTING.md` for the authoring skill model)
- `pm-the-docs` / `write-the-docs` / `edit-the-docs` / `ask-the-docs` / `review-the-docs` — anything under `apps/docs` (see `apps/docs/CONTRIBUTING.md` for the authoring skill model, and `apps/docs/style-guide/` for the content style rules they apply)
- `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
+4
View File
@@ -27,5 +27,9 @@ Prior to submitting your PR, please conduct the following pre-flight checks:
- Run `npm run build` locally to ensure that your code builds successfully without having to wait on us to approve Vercel Preview deploys.
- Ensure that the Prettier tests run successfully on your PR.
- If your PR changes docs content, use the docs authoring [agent skills](https://github.com/supabase/supabase/tree/master/.agents/skills). They apply the [docs style guide](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) for you.
- `/write-the-docs` to draft a new page, or `/edit-the-docs` to revise an existing page.
- `/test-the-docs` to run any snippets you added.
- `/review-the-docs` to self-review before you open the PR.
Running these before you create the PR will help reduce back and forth with the team.
+5 -5
View File
@@ -105,11 +105,11 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<tr>
<td>Flutter</td>
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-flutter</a></td>
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-dart</a></td>
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/postgrest" target="_blank" rel="noopener noreferrer">postgrest</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_auth" target="_blank" rel="noopener noreferrer">supabase_auth</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_realtime" target="_blank" rel="noopener noreferrer">supabase_realtime</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_storage" target="_blank" rel="noopener noreferrer">supabase_storage</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_functions" target="_blank" rel="noopener noreferrer">supabase_functions</a></td>
</tr>
<tr>
<td>Swift</td>
+56
View File
@@ -0,0 +1,56 @@
{
"rules": {
"shadcn/no-arbitrary-values": 42,
"shadcn/no-unknown-classes": 8,
"shadcn/no-raw-colors": 22
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"components/code-block-wrapper.tsx": 1,
"components/code-fragment.tsx": 7,
"components/color-palette.tsx": 2,
"components/command-menu.tsx": 1,
"components/component-preview.tsx": 12,
"components/mdx-components.tsx": 2,
"registry/default/example/admonition-button-split.tsx": 2,
"registry/default/example/assistant-chat-commands.tsx": 2,
"registry/default/example/button-split-dropdown.tsx": 2,
"registry/default/example/chart-tooltip-demo.tsx": 2,
"registry/default/example/command-dialog.tsx": 1,
"registry/default/example/connect-interstitial-shared.tsx": 2,
"registry/default/example/drawer-demo.tsx": 1,
"registry/default/example/page-layout-edge-function.tsx": 1,
"registry/default/example/toc-demo.tsx": 1,
"registry/default/example/toc-single-demo.tsx": 1,
"registry/default/example/typography-inline-code.tsx": 2
},
"shadcn/no-unknown-classes": {
"components/code-block-wrapper.tsx": 1,
"components/code-fragment.tsx": 1,
"components/component-preview.tsx": 1,
"components/mdx-components.tsx": 2,
"registry/default/example/data-grid-demo.tsx": 1,
"registry/default/example/data-grid-empty-state.tsx": 1,
"registry/default/example/form-patterns-sidepanel.tsx": 1
},
"shadcn/no-raw-colors": {
"components/code-block-wrapper.tsx": 2,
"components/copy-button.tsx": 3,
"registry/default/example/calendar-form.tsx": 1,
"registry/default/example/calendar-react-hook-form.tsx": 1,
"registry/default/example/checkbox-form-multiple.tsx": 1,
"registry/default/example/checkbox-form-single.tsx": 1,
"registry/default/example/combobox-form.tsx": 1,
"registry/default/example/connect-interstitial-shared.tsx": 3,
"registry/default/example/date-picker-form.tsx": 1,
"registry/default/example/input-form.tsx": 1,
"registry/default/example/input-otp-form.tsx": 1,
"registry/default/example/radio-group-card-form.tsx": 1,
"registry/default/example/radio-group-form.tsx": 1,
"registry/default/example/radio-group-stacked-form.tsx": 1,
"registry/default/example/select-form.tsx": 1,
"registry/default/example/switch-form.tsx": 1,
"registry/default/example/textarea-form.tsx": 1
}
}
}
+30 -10
View File
@@ -4,40 +4,60 @@ Design resources for building consistent user experiences at Supabase.
## Getting started
First, make a copy of _.env.local.example_ and name it _env.local_. Then install any required packages and start the development server:
From the repo root:
```bash
# Copy local env vars (sets NEXT_PUBLIC_BASE_PATH for asset URLs)
cp apps/design-system/.env.local.example apps/design-system/.env.local
# Move into the design-system app
cd apps/design-system
# Install dependencies
pnpm i
# Build the registry and Velite content, then start the dev servers
pnpm dev
```
The `dev` command generates `__registry__`, then runs the Next.js development server and Contentlayer together. That is the recommended workflow.
Or from `apps/design-system`:
```bash
# Copy local env vars (sets NEXT_PUBLIC_BASE_PATH for asset URLs)
cp .env.local.example .env.local
# Install dependencies
pnpm i
# Build the registry and Velite content, then start the dev servers
pnpm dev
```
The `dev` command builds the registry and Velite content, then runs the Next.js dev server and Velite watcher in parallel.
Open [http://localhost:3003/design-system](http://localhost:3003/design-system) in your browser to see the result.
Doc pages load compiled MDX from `.velite/codes/*.json` per document. Metadata lives in the smaller `allDocs.json` index (~367KB instead of ~27MB), so content edits only reload the changed doc's code.
### Alternative commands
You can also run the development server and content watcher separately. Generate the registry first, because `dev:next` and `dev:content` do not:
You can also run the development server and content watcher separately. Build the registry and content first, because `dev:next` and `dev:content` do not:
```bash
pnpm generate:registry
pnpm build:registry
pnpm build:content
# Run only the Next.js development server
pnpm dev:next
# Run only the content watcher (in a separate terminal shell)
# Run only the Velite content watcher (in a separate terminal shell)
pnpm dev:content
```
From the repo root, `pnpm dev:design-system` runs the same `dev` script, so it also generates `__registry__`. If you split the watchers from the root, generate first:
From the repo root, `pnpm dev:design-system` runs the same `dev` script. If you split the watchers from the root, build first:
```bash
pnpm --filter=design-system generate:registry
pnpm --filter=design-system build:registry
pnpm --filter=design-system build:content
pnpm --filter=design-system dev:next
pnpm --filter=design-system dev:content
```
Open [http://localhost:3003](http://localhost:3003) in your browser to see the result.
### Watching for MDX changes
The `dev` command watches MDX files and hot-reloads them. If you are running `pnpm dev:next` on its own, also run `pnpm dev:content` in another terminal.
@@ -64,5 +84,5 @@ Do not edit `__registry__`. `pnpm dev`, `pnpm typecheck`, and `pnpm build` gener
```bash
cd apps/design-system
pnpm generate:registry
pnpm build:registry
```
@@ -3,8 +3,10 @@ import { DocsPager, getBreadcrumbSegments } from '@/components/pager'
import { SourcePanel } from '@/components/source-panel'
import { DashboardTableOfContents } from '@/components/toc'
import { siteConfig } from '@/config/site'
import { getAllDocs, getDocBySlug, getDocMetaBySlug } from '@/lib/docs'
import { getTableOfContents } from '@/lib/toc'
import { absoluteUrl } from '@/lib/utils'
/* eslint-disable turbo/no-undeclared-env-vars */
import '@/styles/code-block-variables.css'
import '@/styles/mdx.css'
@@ -16,8 +18,6 @@ import { notFound } from 'next/navigation'
import Balancer from 'react-wrap-balancer'
import { ScrollArea, Separator } from 'ui'
import { allDocs } from '@/.velite'
interface DocPageProps {
params: Promise<{
slug: string[]
@@ -26,13 +26,7 @@ interface DocPageProps {
async function getDocFromParams({ params }: { params: { slug: string[] } }) {
const slug = params.slug?.join('/') || ''
const doc = allDocs.find((doc) => doc.slugAsParams === slug)
if (!doc) {
return null
}
return doc
return getDocMetaBySlug(slug)
}
export async function generateMetadata(props: DocPageProps): Promise<Metadata> {
@@ -71,14 +65,20 @@ export async function generateMetadata(props: DocPageProps): Promise<Metadata> {
}
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
if (process.env.NODE_ENV === 'development') {
return []
}
const allDocs = await getAllDocs()
return allDocs.map((doc) => ({
slug: doc.slugAsParams.split('/'),
slug: doc.slugAsParams ? doc.slugAsParams.split('/') : [],
}))
}
export default async function DocPage(props: DocPageProps) {
const params = await props.params
const doc = await getDocFromParams({ params })
const slug = params.slug?.join('/') || ''
const doc = await getDocBySlug(slug)
if (!doc) {
notFound()
+2 -2
View File
@@ -30,7 +30,7 @@ export default function Home() {
<Link href="/docs/icons" className="h-full flex">
<div className="p-6 gap-4 flex flex-col justify-between h-full w-full bg-surface-75 hover:bg-overlay/50 hover:border-foreground-muted cursor-pointer transition-all border rounded-md">
<div className="flex items-center justify-start min-h-[24px] gap-3 text-brand">
<div className="flex items-center justify-start min-h-[24px] gap-3 text-primary">
<Realtime className="w-5 h-5" strokeWidth={1.5} stroke="currentColor" />
<Database className="w-5 h-5 opacity-60" strokeWidth={1.5} stroke="currentColor" />
<Auth className="w-5 h-5 opacity-30" strokeWidth={1.5} stroke="currentColor" />
@@ -44,7 +44,7 @@ export default function Home() {
<Link href="/docs/theming" className="h-full flex">
<div className="p-6 gap-4 flex flex-col justify-between h-full w-full bg-surface-75 hover:bg-overlay/50 hover:border-foreground-muted cursor-pointer transition-all border rounded-md">
<div className="flex items-center justify-start min-h-[24px] text-brand">
<div className="flex items-center justify-start min-h-[24px] text-primary">
<Paintbrush className="w-6 h-6" strokeWidth={1.5} stroke="currentColor" />
</div>
<div>
+2 -4
View File
@@ -1,18 +1,16 @@
import 'react-data-grid/lib/styles.css'
import '@/styles/globals.css'
import type { Metadata, Viewport } from 'next'
import { genFaviconData } from 'common/MetaFavicons/app-router'
import type { Metadata, Viewport } from 'next'
import { Providers } from './Providers'
import { Toaster } from './toaster'
import { BASE_PATH } from '@/lib/constants'
import { inter, manrope, sourceCodePro } from '@/lib/fonts'
const className = `${inter.variable} ${manrope.variable} ${sourceCodePro.variable}`
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'
export const metadata: Metadata = {
applicationName: 'Supabase Design System',
title: 'Supabase Design System',
@@ -1,7 +1,7 @@
'use client'
import * as React from 'react'
import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger, FloatingPlate } from 'ui'
interface CodeBlockProps extends React.HTMLAttributes<HTMLDivElement> {
expandButtonTitle?: string
@@ -34,11 +34,13 @@ export function CodeBlockWrapper({
isOpened ? 'inset-x-0 bottom-0 h-12' : 'inset-0'
)}
>
<CollapsibleTrigger asChild>
<Button variant="secondary" className="h-8 text-xs">
{isOpened ? 'Collapse' : expandButtonTitle}
</Button>
</CollapsibleTrigger>
<FloatingPlate>
<CollapsibleTrigger asChild>
<Button variant="secondary" className="h-8 text-xs">
{isOpened ? 'Collapse' : expandButtonTitle}
</Button>
</CollapsibleTrigger>
</FloatingPlate>
</div>
</div>
</Collapsible>
+5 -1
View File
@@ -7,10 +7,14 @@ const color = colors['default']
const Colors = ({
definition,
classes,
}: {
definition: 'background' | 'border' | 'text' | 'colors' | 'palletes'
/** Optional subset of utility classes. Defaults to the full set for `definition`. */
classes?: string[]
}) => {
const [copiedIndex, setCopiedIndex] = useState<number | null>(null)
const items = classes ?? color[definition]
const handleCopy = async (value: string, index: number) => {
try {
@@ -63,7 +67,7 @@ const Colors = ({
return (
<>
<Grid>
{color[definition].map((x: string, i) => {
{items.map((x: string, i) => {
return (
<GridItem
key={i}
@@ -108,10 +108,6 @@ export function CommandMenu({ ...props }: DialogProps) {
<MoonIcon className="mr-2 h-4 w-4" strokeWidth={1} />
Dark
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme('classic-dark'))}>
<MoonIcon className="mr-2 h-4 w-4" strokeWidth={1} />
Classic dark
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme('system'))}>
<LaptopIcon className="mr-2 h-4 w-4" strokeWidth={1} />
System
@@ -2,7 +2,7 @@
import { ChevronRight, Expand } from 'lucide-react'
import * as React from 'react'
import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger, FloatingPlate } from 'ui'
import { Index } from '@/__registry__'
import { useConfig } from '@/hooks/use-config'
@@ -97,7 +97,7 @@ export function ComponentPreview({
<div className={cn('@container mt-4 mb-12', wideClasses)}>
<div
className={cn(
'relative rounded-tl-md rounded-tr-md border-t border-l border-r bg-studio'
'relative overflow-hidden rounded-tl-md rounded-tr-md border-t border-l border-r bg-studio'
)}
>
{showGrid && (
@@ -122,14 +122,15 @@ export function ComponentPreview({
>
{Code}
<div className="absolute bottom-0 w-full flex justify-center mb-4">
<Button
className="rounded-full"
onClick={() => setExpandState(!expand)}
variant="default"
icon={<Expand className="text-foreground-lighter" />}
>
{expand ? 'Collapse code' : 'Expand code'}
</Button>
<FloatingPlate rounded="full">
<Button
className="rounded-full"
onClick={() => setExpandState(!expand)}
icon={<Expand className="text-foreground-lighter" />}
>
{expand ? 'Collapse code' : 'Expand code'}
</Button>
</FloatingPlate>
</div>
</div>
</div>
@@ -140,7 +141,7 @@ export function ComponentPreview({
return (
<div className={cn('mt-4 mb-12', wideClasses)}>
<div
className={cn('relative bg-studio', {
className={cn('relative overflow-hidden bg-studio', {
'rounded-tl-md rounded-tr-md border-t border-l border-r': !hideCode,
'rounded-md border': hideCode,
})}
@@ -4,6 +4,8 @@ import { useTheme } from 'next-themes'
import SVG from 'react-inlinesvg'
import { cn } from 'ui'
import { BASE_PATH } from '@/lib/constants'
const HomepageSvgHandler = ({ name, className }: { name: string; className?: string }) => {
const { resolvedTheme } = useTheme()
@@ -11,7 +13,7 @@ const HomepageSvgHandler = ({ name, className }: { name: string; className?: str
<div>
<SVG
className={cn('h-32 w-auto', className)}
src={`${process.env.NEXT_PUBLIC_BASE_PATH}/img/design-system-marks/${name}--${resolvedTheme}.svg`}
src={`${BASE_PATH}/img/design-system-marks/${name}--${resolvedTheme}.svg`}
/>
</div>
)
@@ -17,6 +17,7 @@ import {
cn,
Tabs,
TabsContent,
TabsIndicator,
TabsList,
TabsTrigger,
} from 'ui'
@@ -86,7 +87,7 @@ const components = {
a: ({ className, ...props }: React.HTMLAttributes<HTMLAnchorElement>) => (
<a
className={cn(
'text-foreground underline decoration-1 decoration-foreground-muted underline-offset-4 transition-colors hover:decoration-brand hover:decoration-2',
'text-foreground underline decoration-1 decoration-foreground-muted underline-offset-4 transition-colors hover:decoration-primary hover:decoration-2',
className
)}
{...props}
@@ -226,16 +227,26 @@ const components = {
Tabs: ({ className, ...props }: React.ComponentProps<typeof Tabs>) => (
<Tabs className={cn('relative mt-6 w-full', className)} {...props} />
),
TabsList: ({ className, ...props }: React.ComponentProps<typeof TabsList>) => (
TabsList: ({ className, children, ...props }: React.ComponentProps<typeof TabsList>) => (
<TabsList
className={cn('w-full justify-start rounded-none border-b bg-transparent p-0', className)}
className={cn(
'w-full justify-start rounded-none bg-transparent p-0',
'ps-4 -ms-4 [--tab-track-inset:--spacing(4)]',
className
)}
{...props}
/>
>
{children}
<TabsIndicator />
</TabsList>
),
TabsTrigger: ({ className, ...props }: React.ComponentProps<typeof TabsTrigger>) => (
<TabsTrigger
className={cn(
'relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none',
'relative h-9 rounded-none bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none',
// The first label lines up with the surrounding content, keeping its
// padding so the focus ring sits off the glyphs
'first:-ms-4',
className
)}
{...props}
@@ -28,10 +28,11 @@ export const NavigationItem: React.FC<{ item: SidebarNavItem }> = React.memo(({
'items-center',
'h-6',
'text-sm',
'text-foreground-lighter px-6',
!isActive && 'hover:bg-surface-100 hover:text-foreground',
isActive && 'bg-surface-200 text-foreground',
'transition-all'
'px-6',
'transition-all',
isActive
? 'bg-selection text-foreground'
: 'text-foreground-light hover:bg-surface-200 hover:text-foreground'
)}
>
<div
+18 -4
View File
@@ -10,6 +10,10 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
({ doc, children, ...props }, ref) => {
const ShadcnPanel = () => {
if (doc.source?.shadcn) {
const shadcnDocsUrl = doc.slugAsParams?.startsWith('components/')
? `https://ui.shadcn.com/docs/${doc.slugAsParams}`
: 'https://ui.shadcn.com/'
return (
<div
className={cn(
@@ -46,9 +50,19 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
</svg>
<span className="hidden font-bold sm:inline-block">shadcn/ui</span>
</div>
<span className="text-foreground-light text-sm">
This component is based on ui.shadcn
</span>
<div className="flex flex-row items-center justify-between text-sm w-full">
<span className="text-foreground-light text-xs">This component uses shadcn/ui</span>
<Button
asChild
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
<Link href={shadcnDocsUrl} target="_blank" rel="noreferrer">
Docs
</Link>
</Button>
</div>
</div>
)
}
@@ -299,11 +313,11 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
return (
<div className="flex flex-col -space-y-px">
<RadixPanel />
<ShadcnPanel />
<VaulPanel />
<InputOtp />
<ReactAccesibleTreeViewPanel />
<RechartsPanel />
{/* <ShadcnPanel /> */}
</div>
)
}
@@ -5,6 +5,8 @@ import { useEffect, useState } from 'react'
import SVG from 'react-inlinesvg'
import { RadioGroup, RadioGroupLargeItem, singleThemes } from 'ui'
import { BASE_PATH } from '@/lib/constants'
const ThemeSettings = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
@@ -35,7 +37,7 @@ const ThemeSettings = () => {
>
{singleThemes.map((theme) => (
<RadioGroupLargeItem key={theme.value} value={theme.value} label={theme.name}>
<SVG src={`${process.env.NEXT_PUBLIC_BASE_PATH}/img/themes/${theme.value}.svg`} />
<SVG src={`${BASE_PATH}/img/themes/${theme.value}.svg`} />
</RadioGroupLargeItem>
))}
</RadioGroup>
@@ -15,17 +15,20 @@ Accessibility is about making an interface work for as many people as possible a
About to push some code? At a minimum, check your work against this list:
- Are interactive page elements [keyboard-focusable](#focus-management)?
- Are unavailable actions [discoverable and explained](#disabled-controls) for keyboard users?
- Are all elements announcable by a [screen reader](#screen-reader-support)?
- Are textual elements legible and scalable?
- Can I use this on a smaller and/or older device?
## Focus management
All interactive page elements should be reachable by keyboard. Given the below inconsistency between devices and browsers, add `tabIndex={0}` to all buttons, links, and non-text inputs, ideally at the component level. Consider tying the state of `tabIndex` to the `disabled` state of a component, if applicable.
All interactive page elements should be reachable by keyboard. Native buttons, links with `href`, and form inputs are keyboard accessible by default. Add `tabIndex={0}` only to bespoke interactive elements. For controls using native `disabled`, tie `tabIndex` to that state (disabled controls default to `tabIndex={-1}`). Controls that use `aria-disabled` to stay discoverable should remain at `tabIndex={0}`. See [Disabled controls](#disabled-controls).
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 one shared focus ring 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. Its color follows the theme's brighter primary hue in both themes. See [Primary and brand colors](../docs/color-usage#primary-and-brand-colors).
An editable text field can use its visible insertion caret to show focus. This can be useful for inputs embedded within a compact control, where another ring would obscure nearby elements. Check that the caret clearly identifies the active field. Read-only fields and controls without a caret still need a visible focus indicator.
### Focus ring recipe
@@ -74,8 +77,8 @@ transition-property: color, background-color, border-color, ...
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
- Never use `outline-none` / `outline-hidden` without a visible focus indicator, such as a ring, outline, or insertion caret in an editable text field
- Always use the shared color (`ring-ring` / `outline-ring`). Variants (primary, danger, warning) do not change focus color. The shared color derives from `--primary` with enough lightness to remain visible in light mode.
- 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
@@ -125,6 +128,51 @@ Some keyboard-navigable content may contain hundreds or thousands of items. Help
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.
## Disabled controls
Native `disabled` controls are removed from the tab order. When users need to focus a disabled button to discover the action or understand why it is unavailable, add `focusableWhenDisabled`.
### Focusable when disabled
Use `disabled` with `focusableWhenDisabled` when an action is unavailable for a reason that is not obvious, especially when you show a tooltip explaining why:
- Permission gates
- Plan or infrastructure restrictions
- Business rules that block an otherwise visible action
`focusableWhenDisabled` changes how the disabled state is implemented. It sets `aria-disabled="true"`, keeps the control in the tab order, and applies disabled styling without `pointer-events-none`. Guard handlers are built into [Button](components/button). See also [MDN: aria-disabled](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-disabled).
Tab to the example below with the keyboard. The disabled button stays focusable and exposes its tooltip.
<ComponentPreview name="disabled-focusable" />
Implementation checklist for focusable disabled buttons:
- Add `focusableWhenDisabled` when the reason for disabling the control is not obvious
- Pair with a tooltip when you need to explain why
- Guard `onClick` and keyboard activation (`Enter` / `Space`) (`Button` does this automatically)
- Keep `tabIndex={0}` (`Button` does this automatically)
- Do **not** use `pointer-events-none` on the control (it blocks hover and tooltips)
```tsx showLineNumbers
<Tooltip>
<TooltipTrigger asChild>
<Button disabled={unavailable} focusableWhenDisabled>
Pause project
</Button>
</TooltipTrigger>
{unavailable && <TooltipContent>{reason}</TooltipContent>}
</Tooltip>
```
In Studio, `ButtonTooltip` adds `focusableWhenDisabled` to disabled buttons with tooltip text automatically.
### Page-level context
Tooltips alone are not enough for significant restrictions. Pair focusable disabled controls with visible page context (for example: an [Admonition](fragments/admonition), empty state, or inline copy) so the reason is available even without hover or focus.
<ComponentPreview name="disabled-unavailable-with-notice" />
## Screen readers
Textual elements are supported out-of-the-box by screen readers.
@@ -5,14 +5,59 @@ description: Colors system breakdown with best practices.
The shorthand utility classes below simplify our full color palette by providing sensible, contrast-checked defaults. Use them whenever possible to ensure accessible text colors and balanced background fills.
## Primary and brand colors
Primary is the theme's functional accent. Changing `--primary-hue` updates primary text, buttons, selected controls, and focus rings. Brand is the fixed Supabase palette for product identity.
| Role | Tokens | Follows the primary hue? |
| ----------------- | ------------------------------------------------------------ | ------------------------ |
| Functional accent | `--primary`, `--primary-solid`, `--primary-bright`, `--ring` | Yes |
| Supabase identity | `brand-default`, `brand-*` | No |
### Primary ink
`--primary` is for readable UI: links, labels, radios, step dots, and other small indicators. Utilities include `text-primary`, `bg-primary`, and `border-primary`.
In light mode it is darkened to meet WCAG AA for normal text. In dark mode it stays bright so those controls remain visible on dark surfaces.
### Primary solid
`--primary-solid` is the deeper fill for primary buttons. In dark mode it is darker than `--primary` so buttons do not read as lime. In light mode it matches `--primary`, so ink and buttons share one green. Primary Button uses `bg-primary-solid` / `text-primary-solid-foreground`.
### Bright primary
`--primary-bright` gives focus rings, selected controls, and other interactive chrome a brighter version of the primary hue. It stays vivid when light-mode `--primary` is darkened for readable text. Use `bg-primary-bright`, `border-primary-bright`, and the shared `ring-ring` focus treatment.
### Brand green
`brand-default` remains the fixed Supabase green for explicitly branded surfaces.
Toggle the theme to compare the swatches below. Light mode collapses primary and primary-solid; dark mode separates bright ink from the deeper button plate:
<Colors
definition={'colors'}
classes={['bg-primary', 'bg-primary-solid', 'bg-primary-bright', 'bg-brand-default']}
/>
See also [Typography](../docs/typography) (`text-primary`) and [Accessibility](../docs/accessibility#focus-management) (shared focus ring).
## Text
Use accent text colors (e.g. text-destructive, text-warning) sparingly to avoid visual overload.
Use `text-primary` for readable branded text such as links, labels, and statuses. It meets
WCAG AA for normal text on the surfaces used by the apps: darkened in light mode, bright in
dark mode. Primary buttons use a separate solid plate in dark mode; focus chrome uses
the brighter primary hue. See [Primary and brand colors](#primary-and-brand-colors).
<Colors definition={'text'} />
## Background
Use `bg-primary-bright` for selected controls that need a vivid fill, and `bg-primary`
for smaller indicators such as radios and step dots. Use `bg-brand-default` only when
the canonical Supabase green is required. Details in [Primary and brand colors](#primary-and-brand-colors).
<Colors definition={'background'} />
### App backgrounds
@@ -32,7 +77,7 @@ We use backgrounds in 2 different ways. In the ./www and ./docs sites, we use a
<body className="bg-studio">{children}</body>
```
### Backgrounds and Surfaces
### Backgrounds and surfaces
#### `./apps/www` + `./apps/docs`
@@ -64,7 +109,7 @@ This is not to be confused with `Dialogs`, they require to use the same app back
<Colors definition={'border'} />
## Other Colors
## Other colors
These can also be accessed with `foreground`. Like `text-foreground-light`.
@@ -5,7 +5,7 @@ featured: true
component: true
---
<ComponentPreview name="button-demo" peekCode wide />
<ComponentPreview name="button-default" peekCode wide />
## Usage
@@ -47,23 +47,18 @@ Use the `size` prop to determine the size of the button.
### Variants
These are all the different `variant` variations.
#### Default
Used when no `variant` is specified. Prefer this unless another variant fits better, as below.
<ComponentPreview name="button-default" />
#### Primary
Used for data insertion actions, confirming purchases, strong positive actions.
Use sparingly for data insertion, confirming purchases, and other strong positive actions. Because it is so prominent, aim for at most one primary button in a viewport.
<ComponentPreview name="button-demo" />
#### Default
Used for opening dialogs, navigating to pages, and other non CRUD actions.
This `variant` will probably be the most used button variant.
It will probably be changed to be the default variant in future.
<ComponentPreview name="button-default" />
#### Secondary
Can be used for signaling a data or config change, but not as serious as a primary button.
@@ -105,13 +100,17 @@ Used for actions that are not as important as the primary action, or for actions
<ComponentPreview name="button-link" />
### Only an icon
### Icon-only
Displaying only an Icon in a button.
Render an [icon](./icons) in a button without accompanying text. Ensure the button is accessible by:
<Admonition type="note" title="This feature requires more support" className="mt-3">
We should update the button component to support this use case better.
</Admonition>
- Wrapping it in a [Tooltip](./tooltip) for sighted users.
- Adding an `aria-label` prop for screen readers.
- Setting its `aria-describedby` prop to `undefined` when the tooltip content repeats the label. Otherwise screen readers read the label twice.
Consider also squaring off the button container as shown in the example below. For the default `tiny` size (`h-[26px]`), use `w-6.5` so the button is square.
Compact icon-only buttons may also benefit from the `hit-area` utility. This increases their tap target slightly each side without changing the visual layout. See the [Table](./table#actions) component for more information.
<ComponentPreview name="button-icon" />
@@ -138,13 +137,61 @@ Ensure the middle border is shared rather than doubled-up. Do not use `border-l-
Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions when `layout="responsive"`: also use `flex w-full @lg:w-auto` with `flex-1 @lg:flex-none` on the primary action.
## Default fill and floating buttons
The default variant is meant to read as raised chrome on whatever surface it sits on.
- **Light:** opaque `bg-card` (a solid elevated plate). Opaque on purpose so the button can cover busy content underneath.
- **Dark:** translucent `bg-muted` (a foreground wash). That adapts to the local surface, but content can show through the fill.
Hover: light uses `hover:bg-muted`; dark uses `dark:hover:bg-accent`.
### Floating over content
If a default button is absolutely or sticky-positioned over code, tables, maps, or other busy UI, wrap it in [`FloatingPlate`](#floating-plate) so nothing bleeds through:
<ComponentPreview name="button-floating-plate" peekCode />
Keep positioning, z-index, and hover/focus reveal on the plate's `className`. Use `rounded="full"` when the child is a pill. For clusters (split toggles, parallel actions), wrap the group once.
Do not override the button fill with `bg-popover` at the callsite. The plate owns occlusion; the button stays a normal default control.
## Floating plate
`FloatingPlate` is an opaque `bg-popover` shell for floating default buttons (and small clusters).
```tsx
import { Button, FloatingPlate } from 'ui'
```
| Prop | Default | Notes |
| ----------- | ------- | --------------------------------------------------------------- |
| `rounded` | `lg` | `md`, `lg`, or `full`. Match or exceed the child button radius. |
| `className` | — | Positioning, opacity, gaps for clusters. |
See [Floating over content](#floating-over-content) for when to use it.
## Accessibility
[Keyboard focus](../accessibility#focus-management) is automatically handled:
- Enabled buttons default to `tabIndex={0}` (keyboard accessible)
- Disabled buttons default to `tabIndex={-1}` (removed from tab order)
- Buttons with native `disabled` 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.
You therefore don't need to manually set `tabIndex` for buttons using native `disabled`.
When a disabled action has a non-obvious reason and needs a tooltip, add `focusableWhenDisabled` so keyboard users can still focus the control. See [Disabled controls](../accessibility#disabled-controls).
### Focusable when disabled
Use `focusableWhenDisabled` with `disabled` when the action is blocked for a non-obvious reason and you need a tooltip or other explanation. The control stays in the tab order and uses `aria-disabled` instead of native `disabled`.
```tsx
<Button disabled focusableWhenDisabled>
Pause project
</Button>
```
In Studio, `ButtonTooltip` adds `focusableWhenDisabled` to disabled buttons with tooltip text automatically.
@@ -49,27 +49,7 @@ We do not wrap Recharts. This means you're not locked into an abstraction. When
</Callout>
Add the following colors to your CSS file in your app.
```css
@layer base {
:root {
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
```
Chart colors are already defined for every app in `packages/config/css/charts.css`, which ships through the shared Tailwind config. It provides eight categorical slots, `--chart-1` through `--chart-8`, each with a matching `-fill` token, resolved per theme. See the [Charts](/docs/ui-patterns/charts) pattern page for the palette and the rules for assigning slots.
## Your First Chart
@@ -327,25 +307,19 @@ Charts has built-in support for theming. You can use css variables (recommended)
<Steps>
<Step>Define your colors in your css file</Step>
<Step>Pick a slot from the shared palette</Step>
```css {6-7,14-15} title="globals.css"
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
// ...
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
}
```css title="packages/config/css/charts.css"
:root {
--chart-1: var(--color-brand-800);
--chart-2: var(--color-blue-900);
/* ... */
}
.dark: {
--background: 240 10% 3.9%;
--foreground: 0 0% 100%;
// ...
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
}
[data-theme*='dark'] {
--chart-1: var(--color-brand-900);
--chart-2: var(--color-blue-1100);
/* ... */
}
```
@@ -355,28 +329,18 @@ Charts has built-in support for theming. You can use css variables (recommended)
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
color: 'var(--chart-1)',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
} satisfies ChartConfig
```
<Callout className="mt-4">
We're wrapping the value in `hsl()` here because we define the colors without color space function.
This is not required. You can use full color values, such as hex, hsl or oklch.
```css
--chart-1: oklch(70% 0.227 154.59);
```
```tsx
color: "var(--chart-1)",
```
The slots are full color values, so pass them as `var(--chart-1)`. Do not wrap them in `hsl()`; that form is for bare HSL triplets and produces an invalid color here.
</Callout>
@@ -472,11 +436,11 @@ const chartConfig = {
},
chrome: {
label: 'Chrome',
color: 'hsl(var(--chart-1))',
color: 'var(--chart-1)',
},
safari: {
label: 'Safari',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
} satisfies ChartConfig
```
@@ -516,11 +480,11 @@ const chartData = [
const chartConfig = {
chrome: {
label: 'Chrome',
color: 'hsl(var(--chart-1))',
color: 'var(--chart-1)',
},
safari: {
label: 'Safari',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
} satisfies ChartConfig
```
@@ -132,3 +132,9 @@ You can create a responsive combobox by using the `<Popover />` on desktop and t
### Form
<ComponentPreview name="combobox-form" />
### Create from list
When users can create an option while choosing one, give the action a clear verb label and place it at the bottom of the list, separated from the existing options. Keep the current selection in place while they create the new option.
<ComponentPreview name="combobox-create-option" peekCode wide />
File diff suppressed because it is too large. Load diff
@@ -72,6 +72,12 @@ import { Textarea } from '@/components/ui/textarea'
<ComponentPreview name="textarea-with-button" />
### With addon
An addon places supporting content inside the same bordered field as the textarea. Wrap `InputGroupTextarea` and `InputGroupAddon` in `InputGroup`, and use `align="block-end"` to place the addon below the text.
<ComponentPreview name="textarea-with-addon" />
### Form
<ComponentPreview name="textarea-form" />
@@ -89,3 +89,37 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
### Disabled
<ComponentPreview name="toggle-group-disabled" />
## Segmented
A Supabase addition, not part of shadcn or Radix. A segmented control is a compact row of two to
four mutually exclusive options sharing one continuous track, for switching view state (Data or
Definition) or filtering a list (All, Active, Revoked). The choice takes effect immediately and is
never saved, so reach for a [Switch](../components/switch) or
[Radio Group](../components/radio-group) when the setting is persisted, and
[Tabs](../components/tabs) when the options swap whole panels of content. Items paint nothing
themselves; a single indicator slides between them, so the group always has exactly one thing
selected. Give a filter an explicit **All** segment rather than letting deselection stand for
"no filter".
<ComponentPreview name="toggle-group-segmented" />
<ComponentPreview name="toggle-group-segmented-filter" peekCode />
## Props
`ToggleGroup` forwards every prop to the underlying
[Radix Toggle Group](https://www.radix-ui.com/docs/primitives/components/toggle-group#api-reference).
These are the ones worth knowing about.
| Prop | Type | Default | Description |
| --------------- | --------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type` | `'single' \| 'multiple'` | | Required by Radix. Only a `'single'` group gets the segmented indicator; a `'multiple'` group falls back to items painting their own background. |
| `variant` | `'default' \| 'outline' \| 'segmented'` | `'default'` | Styles the root and every item, which inherit it through context. `segmented` drops the gaps and per-item borders in favor of an indicator that slides between segments, respecting `prefers-reduced-motion`. |
| `size` | `'tiny' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Item height and padding, inherited through context. `tiny` suits a dense toolbar or table footer. Segments hug their labels, so add `className="flex-1"` to each item, plus a width on the root, if you need them equal. |
| `tone` | `'text' \| 'outline' \| 'primary'` | `'text'` | Segmented only. `text` is a flat `bg-accent` fill, `outline` adds a container border and a raised bordered indicator, `primary` fills the indicator with brand. |
| `allowDeselect` | `boolean` | `true` | Whether clicking the active item in a `type="single"` group clears it, emitting `''`. Set `false` for a segmented control, which has no empty state. |
| `aria-label` | `string` | | Names what is being switched or filtered. The segments name the options, not the axis. |
`variant` and `size` are the shadcn variant axes; `segmented`, `tone` and `allowDeselect` are the
Supabase additions. `default` and `outline` are unchanged from shadcn.
@@ -50,6 +50,34 @@ export function FilterDemo() {
}
```
## Variants
The default variant joins filters into a segmented bar. Use the pill variant to show each filter as a separate rounded chip:
### Segmented
<ComponentPreview
name="filter-bar-segmented-demo"
description="Filters joined into a segmented bar."
peekCode
showDottedGrid
wide
/>
### Pill
<ComponentPreview
name="filter-bar-pill-demo"
description="Rounded chips for each filter, as used in Studio."
peekCode
showDottedGrid
wide
/>
```tsx
<FilterBar variant="pill" className="border-0 bg-transparent overflow-visible" {...props} />
```
## API Reference
### FilterProperty
@@ -60,6 +88,7 @@ interface FilterProperty {
name: string
type: 'string' | 'number' | 'date' | 'boolean'
operators: string[]
formatValue?: (value: FilterCondition['value']) => string
options?: string[] | ((search?: string) => Promise<string[]> | string[])
}
```
@@ -94,6 +123,7 @@ interface FilterCondition {
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
| actions | FilterBarAction[]? | Optional custom actions to show in the menu |
| isLoading | boolean? | If true, dims the bar while work is in progress |
| variant | 'default' \| 'pill'? | Filter appearance. Defaults to 'default' |
## Custom actions (e.g. AI)
@@ -60,16 +60,16 @@ creatable: `boolean`
<ComponentPreview name="multi-select-combobox-creatable" />
### Badge Limit
### Badge limit
badgeLimit: `number` | `"wrap"`.
`badgeLimit` prop on the `MultiSelectorTrigger` component can be used to limit the number of badges displayed.
<ComponentPreview name="multi-select-badge-limit" />
### Badge Limit="wrap"
### Wrapped badge limit
`badgeLimit` prop can also be "wrap" to wrap the badges to the next line.
Combine `badgeLimit` with `wrapBadges` to limit the number of badges and allow them to wrap onto additional lines. Use `badgeLimit="wrap"` to show and wrap every selected badge.
<ComponentPreview name="multi-select-badge-limit-wrap" />
+2 -3
View File
@@ -5,11 +5,10 @@ description: Themes used in Supabase
Design System currently takes into account varying themes.
Themes currently in development:
Available themes:
- Light
- Dark (Classic dark)
- Deep dark
- Dark
We also support a system theme, which will automatically switch between light and dark themes based on the user's system settings.
@@ -9,7 +9,12 @@ The shorthands below are composed of core [Tailwind utility classes](../docs/tai
## Shorthands
| Value | Usage |
| ------------------ | ---------------------------------------------------------------------------- |
| `text-code-inline` | Apply to a `code` element for inline code or similar custom inline content |
| `text-brand-link` | Supabase green text that meets contrast requirements in light and dark modes |
| Value | Usage |
| ------------------ | -------------------------------------------------------------------------- |
| `text-code-inline` | Apply to a `code` element for inline code or similar custom inline content |
| `text-primary` | Accessible Supabase green for readable branded text |
`text-primary` meets WCAG AA for normal text on app surfaces: darkened in light mode,
bright in dark mode. Focus rings and selected controls derive their brighter fill from
the same primary hue. See [Primary and brand colors](../docs/color-usage#primary-and-brand-colors) for when
to use each green.
@@ -23,6 +23,29 @@ Our charts use a combination of our own presentational components and [Recharts]
3. **Keep it simple**: Try to avoid abstracting the chart content too much. These components should cover most of your presentational needs.
## Color
Series colors come from eight categorical slots, `--chart-1` through `--chart-8`, defined in
`packages/config/css/charts.css`. Assign them in order and never cycle: a ninth series folds
into "Other" or becomes small multiples. Each slot has a matching `-fill` token. Slots resolve
per theme, so pass `var(--chart-n)` and never branch on light/dark in code. Adjacent slots
alternate hue families and clear colorblind separation in both themes.
Reference lines use `--chart-reference`. Headroom, idle and unused capacity use `--chart-muted`.
Directional pairs use `--chart-in` / `--chart-out` so read and write keep the same hue across
charts.
Status colors (`--chart-status-success`, `-warning`, `-destructive`, each with a `-muted` tier)
are reserved for state and always ship with an icon or label. Never use one as a series color:
amber on a neutral metric reads as a problem. Warm hues are otherwise limited to tomato, slot 5,
because no amber or yellow step is legible on the dark surface.
<ComponentPreview name="chart-palette" wide />
Every slot stacked together, to check adjacent segments stay separable in both themes.
<ComponentPreview name="chart-palette-stress" wide />
## Examples
### Basic Chart Types
@@ -199,27 +222,27 @@ type ChartConfig = {
Line chart component for displaying time-series data.
| Prop | Type | Default | Description |
| ------------------- | --------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------- |
| `data` | `ChartLineTick[]` | - | Array of data points with timestamp |
| `dataKey` | `string` | - | Key in data object to plot |
| `config` | `ChartConfig?` | - | Chart configuration for styling |
| `onLineClick` | `(datum: ChartLineTick, tooltipData?: CategoricalChartState) => void` | - | Click handler for line points |
| `DateTimeFormat` | `string` | `'MMM D, YYYY, hh:mma'` | Date format for tooltips and labels |
| `isFullHeight` | `boolean` | `false` | Whether chart should take full height |
| `className` | `string` | - | Additional CSS classes |
| `color` | `string` | `'hsl(var(--brand-default))'` | Line color |
| `hoverColor` | `string` | `'hsl(var(--brand-500))'` | Line color on hover |
| `chartHighlight` | `ChartHighlight` | - | Highlight selection configuration |
| `updateDateRange` | `(from: string, to: string) => void` | - | Callback when date range is updated via highlight |
| `highlightActions` | `ChartHighlightAction[]` | - | Actions to show when area is highlighted |
| `syncId` | `string` | - | ID to sync multiple charts |
| `showHighlightArea` | `boolean` | `true` | Whether to show highlight area |
| `cursor` | `string` | - | Cursor style (defaults to 'crosshair' if chartHighlight provided) |
| `showGrid` | `boolean` | `false` | Whether to show grid lines |
| `showYAxis` | `boolean` | `false` | Whether to show Y-axis |
| `YAxisProps` | `object` | - | Additional Y-axis props (tick, tickFormatter, width, etc.) |
| `strokeWidth` | `number` | `1.5` | Line stroke width |
| Prop | Type | Default | Description |
| ------------------- | --------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------- |
| `data` | `ChartLineTick[]` | - | Array of data points with timestamp |
| `dataKey` | `string` | - | Key in data object to plot |
| `config` | `ChartConfig?` | - | Chart configuration for styling |
| `onLineClick` | `(datum: ChartLineTick, tooltipData?: CategoricalChartState) => void` | - | Click handler for line points |
| `DateTimeFormat` | `string` | `'MMM D, YYYY, hh:mma'` | Date format for tooltips and labels |
| `isFullHeight` | `boolean` | `false` | Whether chart should take full height |
| `className` | `string` | - | Additional CSS classes |
| `color` | `string` | `'var(--primary-bright)'` | Line color |
| `hoverColor` | `string` | `'hsl(var(--brand-500))'` | Line color on hover |
| `chartHighlight` | `ChartHighlight` | - | Highlight selection configuration |
| `updateDateRange` | `(from: string, to: string) => void` | - | Callback when date range is updated via highlight |
| `highlightActions` | `ChartHighlightAction[]` | - | Actions to show when area is highlighted |
| `syncId` | `string` | - | ID to sync multiple charts |
| `showHighlightArea` | `boolean` | `true` | Whether to show highlight area |
| `cursor` | `string` | - | Cursor style (defaults to 'crosshair' if chartHighlight provided) |
| `showGrid` | `boolean` | `false` | Whether to show grid lines |
| `showYAxis` | `boolean` | `false` | Whether to show Y-axis |
| `YAxisProps` | `object` | - | Additional Y-axis props (tick, tickFormatter, width, etc.) |
| `strokeWidth` | `number` | `1.5` | Line stroke width |
#### ChartLineTick Type
@@ -252,26 +275,26 @@ Line chart component for displaying time-series data.
Bar chart component for displaying time-series data.
| Prop | Type | Default | Description |
| ------------------- | -------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------- |
| `data` | `ChartBarTick[]` | - | Array of data points with timestamp |
| `dataKey` | `string` | - | Key in data object to plot |
| `config` | `ChartConfig?` | - | Chart configuration for styling |
| `onBarClick` | `(datum: ChartBarTick, tooltipData?: CategoricalChartState) => void` | - | Click handler for bars |
| `DateTimeFormat` | `string` | `'MMM D, YYYY, hh:mma'` | Date format for tooltips and labels |
| `isFullHeight` | `boolean` | `false` | Whether chart should take full height |
| `className` | `string` | - | Additional CSS classes |
| `color` | `string` | `'hsl(var(--brand-default))'` | Bar color |
| `hoverColor` | `string` | `'hsl(var(--brand-500))'` | Bar color on hover |
| `chartHighlight` | `ChartHighlight` | - | Highlight selection configuration |
| `updateDateRange` | `(from: string, to: string) => void` | - | Callback when date range is updated via highlight |
| `highlightActions` | `ChartHighlightAction[]` | - | Actions to show when area is highlighted |
| `syncId` | `string` | - | ID to sync multiple charts |
| `showHighlightArea` | `boolean` | `true` | Whether to show highlight area |
| `cursor` | `string` | - | Cursor style (defaults to 'crosshair' if chartHighlight provided) |
| `showGrid` | `boolean` | `false` | Whether to show grid lines |
| `showYAxis` | `boolean` | `false` | Whether to show Y-axis |
| `YAxisProps` | `object` | - | Additional Y-axis props (tick, tickFormatter, width, etc.) |
| Prop | Type | Default | Description |
| ------------------- | -------------------------------------------------------------------- | ------------------------------- | ----------------------------------------------------------------- |
| `data` | `ChartBarTick[]` | - | Array of data points with timestamp |
| `dataKey` | `string` | - | Key in data object to plot |
| `config` | `ChartConfig?` | - | Chart configuration for styling |
| `onBarClick` | `(datum: ChartBarTick, tooltipData?: CategoricalChartState) => void` | - | Click handler for bars |
| `DateTimeFormat` | `string` | `'MMM D, YYYY, hh:mma'` | Date format for tooltips and labels |
| `isFullHeight` | `boolean` | `false` | Whether chart should take full height |
| `className` | `string` | - | Additional CSS classes |
| `color` | `string` | `'var(--primary-bright)'` | Bar color |
| `hoverColor` | `string` | `'var(--primary-bright-hover)'` | Bar color on hover |
| `chartHighlight` | `ChartHighlight` | - | Highlight selection configuration |
| `updateDateRange` | `(from: string, to: string) => void` | - | Callback when date range is updated via highlight |
| `highlightActions` | `ChartHighlightAction[]` | - | Actions to show when area is highlighted |
| `syncId` | `string` | - | ID to sync multiple charts |
| `showHighlightArea` | `boolean` | `true` | Whether to show highlight area |
| `cursor` | `string` | - | Cursor style (defaults to 'crosshair' if chartHighlight provided) |
| `showGrid` | `boolean` | `false` | Whether to show grid lines |
| `showYAxis` | `boolean` | `false` | Whether to show Y-axis |
| `YAxisProps` | `object` | - | Additional Y-axis props (tick, tickFormatter, width, etc.) |
#### ChartBarTick Type
+3
View File
@@ -0,0 +1,3 @@
const rawBasePath = process.env.NEXT_PUBLIC_BASE_PATH || 'design-system'
export const BASE_PATH = rawBasePath.startsWith('/') ? rawBasePath : `/${rawBasePath}`
+44
View File
@@ -0,0 +1,44 @@
import 'server-only'
/* eslint-disable turbo/no-undeclared-env-vars */
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { connection } from 'next/server'
import type { Doc as DocMeta } from '@/.velite'
export type { DocMeta }
export type Doc = DocMeta & { code: string }
const CODE_DIR = path.join(process.cwd(), '.velite/codes')
async function loadDocCode(codeId: string): Promise<string> {
const raw = await readFile(path.join(CODE_DIR, `${codeId}.json`), 'utf8')
return JSON.parse(raw) as string
}
export async function getAllDocs(): Promise<DocMeta[]> {
if (process.env.NODE_ENV === 'development') {
await connection()
}
const { allDocs } = await import('@/.velite')
return allDocs
}
export async function getDocMetaBySlug(slug: string): Promise<DocMeta | null> {
const allDocs = await getAllDocs()
return allDocs.find((doc) => doc.slugAsParams === slug) ?? null
}
export async function getDocBySlug(slug: string): Promise<Doc | null> {
const doc = await getDocMetaBySlug(slug)
if (!doc) {
return null
}
const code = await loadDocCode(doc.codeId)
return { ...doc, code }
}
+1
View File
@@ -14,6 +14,7 @@
"build": "run-p build:registry build:content && pnpm build:next",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite __registry__",
"typecheck": "run-p build:registry build:content && tsc --noEmit -p tsconfig.json"
},
+16
View File
@@ -57,4 +57,20 @@ export const charts: Registry = [
category: 'Charts',
subcategory: 'Composed',
},
{
name: 'chart-palette',
type: 'components:block',
registryDependencies: ['chart'],
files: ['block/chart-palette.tsx'],
category: 'Charts',
subcategory: 'Palette',
},
{
name: 'chart-palette-stress',
type: 'components:block',
registryDependencies: ['chart'],
files: ['block/chart-palette-stress.tsx'],
category: 'Charts',
subcategory: 'Palette',
},
]
@@ -116,11 +116,11 @@ const chartConfig = {
},
desktop: {
label: 'Desktop',
color: 'hsl(var(--chart-1))',
color: 'var(--chart-1)',
},
mobile: {
label: 'Mobile',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
} satisfies ChartConfig
@@ -48,15 +48,15 @@ export default function ComposedChartBasic() {
const chartConfig = {
standard_score: {
label: 'Standard Score',
color: 'hsl(var(--brand-default))',
color: 'var(--primary-bright)',
},
performance: {
label: 'Performance',
color: 'hsl(var(--chart-2))',
color: 'var(--chart-2)',
},
efficiency: {
label: 'Efficiency',
color: 'hsl(var(--chart-5))',
color: 'var(--chart-5)',
},
}
@@ -0,0 +1,72 @@
'use client'
import {
Chart,
ChartBar,
ChartCard,
ChartContent,
ChartHeader,
ChartTitle,
type ChartBarTick,
type ChartConfig,
} from 'ui-patterns/Chart'
const SERIES = [
{ key: 'postgres', label: 'Postgres' },
{ key: 'postgrest', label: 'PostgREST' },
{ key: 'reserved', label: 'Reserved' },
{ key: 'auth', label: 'Auth' },
{ key: 'storage', label: 'Storage' },
{ key: 'realtime', label: 'Realtime' },
{ key: 'cron', label: 'Cron' },
{ key: 'other', label: 'Other roles' },
]
const config: ChartConfig = Object.fromEntries(
SERIES.map((s, i) => [s.key, { label: s.label, color: `var(--chart-${i + 1})` }])
)
export default function ChartPaletteStress() {
const data: ChartBarTick[] = Array.from({ length: 40 }, (_, i) => {
const date = new Date()
date.setMinutes(date.getMinutes() - (40 - i) * 3)
const row: ChartBarTick = { timestamp: date.toISOString() }
const trend = Math.sin((i / 40) * Math.PI * 2)
SERIES.forEach((s, idx) => {
const phase = Math.sin(i / 3.5 + idx * 1.7)
const jitter = Math.sin(i * 2.3 + idx * 0.9) * 1.5
row[s.key] = Math.max(1, Math.round(5 + idx * 1.8 + phase * 3 + trend * 2 + jitter))
})
return row
})
return (
<div className="flex flex-col gap-6 w-8/12">
<Chart>
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="Every categorical slot on screen at once">
Client connections by role
</ChartTitle>
</ChartHeader>
<ChartContent>
<div className="h-40">
<ChartBar
data={data}
dataKey={SERIES[0].key}
dataKeys={SERIES.map((s) => s.key)}
config={config}
isStacked
isFullHeight
showGrid
showYAxis
YAxisProps={{ width: 36 }}
/>
</div>
</ChartContent>
</ChartCard>
</Chart>
</div>
)
}
@@ -0,0 +1,135 @@
import { ReactNode } from 'react'
const SLOTS = [1, 2, 3, 4, 5, 6, 7, 8]
const STATUS = [
{ name: '--chart-status-success', muted: '--chart-status-success-muted', note: 'Healthy, ok' },
{
name: '--chart-status-warning',
muted: '--chart-status-warning-muted',
note: 'Threshold breach',
},
{
name: '--chart-status-destructive',
muted: '--chart-status-destructive-muted',
note: 'Error, failure',
},
]
const DEFAULTS = [
{ name: '--chart-in', note: 'Pinned: network in, disk read' },
{ name: '--chart-out', note: 'Pinned: network out, disk write' },
{ name: '--chart-reference', note: 'Reference lines, max values' },
{ name: '--chart-muted', note: 'Headroom, idle, unused capacity' },
]
function Swatch({ token, label }: { token: string; label: string }) {
return (
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<div
className="border-default h-12 w-full rounded-md border"
style={{ background: `var(${token})` }}
/>
<span className="text-foreground-lighter text-xs">{label}</span>
</div>
)
}
function TokenCard({
title,
token,
note,
children,
}: {
title: ReactNode
token: string
note?: string
children: ReactNode
}) {
return (
<div className="border-default bg-surface-100 flex flex-col gap-4 rounded-lg border p-4">
<div className="flex flex-col gap-1">
<div className="text-foreground text-sm">{title}</div>
<code className="text-foreground-lighter break-all font-mono text-xs">{token}</code>
</div>
<div className="flex gap-3">{children}</div>
{note && <p className="text-foreground-lighter text-xs">{note}</p>}
</div>
)
}
function Section({
title,
description,
children,
className,
}: {
title: string
description: string
children: ReactNode
className: string
}) {
return (
<section className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h3 className="text-foreground text-sm">{title}</h3>
<p className="text-foreground-lighter max-w-prose text-xs">{description}</p>
</div>
<div className={className}>{children}</div>
</section>
)
}
export default function ChartPalette() {
return (
<div className="flex w-full flex-col gap-10 p-6">
<Section
title="Categorical slots"
description="Assigned in fixed order, never cycled. A ninth series folds into “Other”."
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
{SLOTS.map((n) => (
<TokenCard key={n} title={`Slot ${n}`} token={`--chart-${n}`}>
<Swatch token={`--chart-${n}`} label="Stroke" />
<Swatch token={`--chart-${n}-fill`} label="Fill" />
</TokenCard>
))}
</Section>
<Section
title="Status"
description="Reserved meaning. These point at the same tokens the rest of the UI uses and always ship with an icon or label, so state is never carried by color alone. Never assign one to a series."
className="grid grid-cols-1 gap-4 sm:grid-cols-3"
>
{STATUS.map((d) => (
<TokenCard
key={d.name}
title={d.name.replace('--chart-status-', '')}
token={d.name}
note={d.note}
>
<Swatch token={d.name} label="Base" />
<Swatch token={d.muted} label="Muted" />
</TokenCard>
))}
</Section>
<Section
title="Pinned pairs and rendering defaults"
description="Chart authors do not pick these. Reference lines and headroom are applied by the chart."
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
{DEFAULTS.map((d) => (
<TokenCard
key={d.name}
title={d.name.replace('--chart-', '')}
token={d.name}
note={d.note}
>
<Swatch token={d.name} label="Color" />
</TokenCard>
))}
</Section>
</div>
)
}
@@ -19,7 +19,6 @@ export default function AdmonitionButtonSplitDemo() {
<div className="flex w-full @lg:w-auto">
<Button
type="button"
variant="default"
className="flex-1 rounded-r-none px-3 @lg:flex-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Set up SMTP
@@ -28,7 +27,6 @@ export default function AdmonitionButtonSplitDemo() {
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="default"
aria-label="More email template editing options"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
@@ -10,7 +10,7 @@ export default function AdmonitionDemo() {
title="Set up custom SMTP"
description="You’re using the built-in email service. This service has rate limits and is not meant to be
used for production apps."
actions={<Button variant="default">Set up SMTP</Button>}
actions={<Button>Set up SMTP</Button>}
/>
<Admonition
type="destructive"
@@ -9,7 +9,7 @@ export default function AdmonitionDemo() {
title="OAuth Server is disabled"
description="Enable OAuth Server to make your project act as an identity provider for
third-party applications."
actions={<Button variant="default">OAuth Server Settings</Button>}
actions={<Button>OAuth Server Settings</Button>}
/>
)
}
@@ -8,7 +8,7 @@ export default function AdmonitionDemo() {
layout="responsive"
title="Disk management has moved"
description="Disk management is now handled alongside Project Compute on the Compute and Disk page."
actions={<Button variant="default">Go to Compute and Disk</Button>}
actions={<Button>Go to Compute and Disk</Button>}
/>
)
}
@@ -3,7 +3,7 @@ import { Button } from 'ui'
export default function ButtonAsChild() {
return (
<Button asChild>
<Button variant="primary" asChild>
<Link href="/login">Sign in</Link>
</Button>
)
@@ -0,0 +1,21 @@
import { Copy } from 'lucide-react'
import { Button, FloatingPlate } from 'ui'
export default function ButtonFloatingPlate() {
return (
<div className="relative w-full max-w-md overflow-hidden rounded-md border border-border">
<pre className="bg-surface-100 p-4 pr-16 font-mono text-xs text-foreground-light leading-relaxed">
{`select *
from projects
where status = 'ACTIVE_HEALTHY'
order by created_at desc
limit 20;`}
</pre>
<FloatingPlate className="absolute right-2 top-2">
<Button size="tiny" icon={<Copy />}>
Copy
</Button>
</FloatingPlate>
</div>
)
}
@@ -1,6 +1,21 @@
import { ChevronRight } from 'lucide-react'
import { Button } from 'ui'
import { ExternalLink } from 'lucide-react'
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
export default function ButtonIcon() {
return <Button variant="outline" icon={<ChevronRight className="h-4 w-4" />}></Button>
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
icon={<ExternalLink />}
// Match tooltip content for screen readers
aria-label="View logs"
// Tooltip repeats the label; clear describedby so screen readers don't hear it twice
// Skip this if the tooltip adds information beyond the label
aria-describedby={undefined}
></Button>
</TooltipTrigger>
<TooltipContent>View logs</TooltipContent>
</Tooltip>
)
}
@@ -2,7 +2,7 @@ import { Button } from 'ui'
export default function ButtonLoading() {
return (
<Button disabled loading>
<Button variant="primary" disabled loading>
Please wait
</Button>
)
@@ -13,7 +13,6 @@ export default function ButtonSplitDropdownDemo() {
<div className="flex w-fit">
<Button
type="button"
variant="default"
className="rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Primary action
@@ -22,7 +21,6 @@ export default function ButtonSplitDropdownDemo() {
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="default"
aria-label="More actions"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
@@ -2,5 +2,9 @@ import { Mail } from 'lucide-react'
import { Button } from 'ui'
export default function ButtonWithIcon() {
return <Button icon={<Mail className="mr-2 h-4 w-4" />}>Sign in with email</Button>
return (
<Button variant="primary" icon={<Mail className="mr-2 h-4 w-4" />}>
Sign in with email
</Button>
)
}
@@ -57,7 +57,6 @@ export default function CalendarForm() {
<PopoverTrigger asChild>
<FormControl>
<Button
variant="default"
size="small"
className={cn(
'w-[240px] justify-start',
@@ -84,7 +83,9 @@ export default function CalendarForm() {
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
<Button variant="primary" type="submit">
Submit
</Button>
</form>
</Form>
)
@@ -83,7 +83,9 @@ export default function CalendarForm() {
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
<Button variant="primary" type="submit">
Submit
</Button>
</form>
</Form>
)
@@ -32,8 +32,8 @@ export default function Component() {
<TooltipDemo
label="Page Views"
payload={[
{ name: 'Desktop', value: 186, fill: 'hsl(var(--chart-1))' },
{ name: 'Mobile', value: 80, fill: 'hsl(var(--chart-2))' },
{ name: 'Desktop', value: 186, fill: 'var(--chart-1)' },
{ name: 'Mobile', value: 80, fill: 'var(--chart-2)' },
]}
className="w-32"
/>
@@ -64,8 +64,8 @@ export default function Component() {
label="Browser"
hideLabel
payload={[
{ name: 'Chrome', value: 1286, fill: 'hsl(var(--chart-3))' },
{ name: 'Firefox', value: 1000, fill: 'hsl(var(--chart-4))' },
{ name: 'Chrome', value: 1286, fill: 'var(--chart-3)' },
{ name: 'Firefox', value: 1000, fill: 'var(--chart-4)' },
]}
indicator="dashed"
className="w-32"
@@ -74,7 +74,7 @@ export default function Component() {
<div className="hidden! md:flex!">
<TooltipDemo
label="Page Views"
payload={[{ name: 'Desktop', value: 12486, fill: 'hsl(var(--chart-3))' }]}
payload={[{ name: 'Desktop', value: 12486, fill: 'var(--chart-3)' }]}
className="w-36"
indicator="line"
/>
@@ -84,7 +84,7 @@ export default function Component() {
<TooltipDemo
label="Browser"
hideLabel
payload={[{ name: 'Chrome', value: 1286, fill: 'hsl(var(--chart-1))' }]}
payload={[{ name: 'Chrome', value: 1286, fill: 'var(--chart-1)' }]}
indicator="dot"
className="w-32"
/>
@@ -112,7 +112,9 @@ export default function CheckboxReactHookFormMultiple() {
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
<Button variant="primary" type="submit">
Submit
</Button>
</form>
</Form>
)
@@ -59,7 +59,9 @@ export default function CheckboxReactHookFormSingle() {
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
<Button variant="primary" type="submit">
Submit
</Button>
</form>
</Form>
)
@@ -4,7 +4,7 @@ import { Checkbox } from 'ui'
export default function CheckboxWithText() {
return (
<div className="items-top flex space-x-2">
<div className="items-start flex space-x-2">
<Checkbox id="terms1" />
<div className="grid gap-1.5 leading-none">
<label
@@ -0,0 +1,71 @@
'use client'
import { Check, Plus } from 'lucide-react'
import { useState } from 'react'
import {
ComboboxTrigger,
Command,
CommandGroup,
CommandItem,
CommandList,
CommandSeparator,
Popover,
PopoverContent,
PopoverTrigger,
} from 'ui'
const buckets = ['Images', 'Exports']
export default function ComboboxCreateOption() {
const [selectedBucket, setSelectedBucket] = useState('')
const [isListOpen, setIsListOpen] = useState(false)
return (
<Popover open={isListOpen} onOpenChange={setIsListOpen}>
<PopoverTrigger asChild>
<ComboboxTrigger
aria-expanded={isListOpen}
data-state={isListOpen ? 'open' : 'closed'}
className="w-[240px]"
>
{selectedBucket || 'Select a bucket'}
</ComboboxTrigger>
</PopoverTrigger>
<PopoverContent className="w-[240px] p-0" align="start">
<Command>
<CommandList>
<CommandGroup>
{buckets.map((bucket) => (
<CommandItem
key={bucket}
value={bucket}
className="cursor-pointer justify-between"
onSelect={() => {
setSelectedBucket(bucket)
setIsListOpen(false)
}}
>
{bucket}
{selectedBucket === bucket && <Check size={14} />}
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<CommandItem
className="cursor-pointer"
onSelect={() => {
setIsListOpen(false)
// Open the creation flow here without changing the selected bucket.
}}
>
<Plus size={14} className="mr-2" />
New bucket
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
@@ -69,7 +69,6 @@ export default function ComboboxPopover() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="default"
size="small"
className="w-[150px] justify-start rounded-full"
icon={
@@ -58,7 +58,6 @@ export default function ComboBoxResponsive() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="default"
size="small"
className="w-[150px] justify-start"
icon={!selectedStatus && <Plus className="text-foreground-muted" />}
@@ -24,9 +24,7 @@ export default function ConfirmationModalDemo() {
return (
<>
<Button variant="default" onClick={() => setVisible(!visible)}>
Show Confirmation Modal
</Button>
<Button onClick={() => setVisible(!visible)}>Show Confirmation Modal</Button>
<ConfirmationModal
visible={visible}
size="small"
Loaded 100 of 3448 files, more files were not shown because too many files have changed in this diff. Show more