mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
Merge branch 'master' into guillaumefaas/sdk-1724-create-blog-post-for-v800-release-and-migration
This commit is contained in:
2599 files changed
+75081
-28619
No files matched your search
@@ -0,0 +1,27 @@
|
||||
---
|
||||
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. Run `pnpm api:codegen` against a running local API environment. It fetches all three local OpenAPI specs and updates the committed files.
|
||||
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.
|
||||
|
||||
## 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 currently observational, not a required merge check. The `api-deploy-required` label is informational only. Still run the local verifier before requesting review and treat a failed CI verification as production drift that must be resolved.
|
||||
@@ -2,73 +2,220 @@
|
||||
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 lint, 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.
|
||||
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 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.
|
||||
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 shared pitfalls in [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md) rather than duplicating them 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 CONTRIBUTING.md. 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 the Procedures section of CONTRIBUTING.md. Start each step with an imperative verb, keep one action or a closely related set per step, present 7 ± 2 steps per chunk, and group anything longer into named phases or smaller procedures.
|
||||
- Apply the inline rules in CONTRIBUTING.md for admonitions, emphasis, links, lists, and the "Styling, formatting, and grammar" section.
|
||||
- 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.
|
||||
- The shared pitfalls in [`common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md): timelessness, internal planning context in shipped MDX, redundancy, single-item lists, and admonition restatement.
|
||||
- Terminology that doesn't match [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md). Crawl the list for 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 the **Mixed information types**, **Navigation**, and **Cross-references and glue** guidance in the Guides section of [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md).
|
||||
|
||||
**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
|
||||
|
||||
Each one is **procedural** (the reader performs actions), **contextual** (the reader needs to understand something before acting), or **reference** (the reader looks something up).
|
||||
|
||||
**Classify by what the reader is doing, not by what the section is about.** Subject matter is the trap: on a page about tables every section is "about tables", so grouping by topic produces one task-named bucket that quietly collects the background as well. A reader opens a section on schemas to understand something, not to do something, so it is context no matter how much it is about tables.
|
||||
|
||||
**A section serving two classes gets split, not filed under the larger half.** Give the new half a heading, keep the heading text of the half that stays, and cross-reference the two. One cross-reference costs less than a reader hunting for the half they need.
|
||||
|
||||
### 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: a short conceptual opener when the page serves newcomers, then procedures, then context, then reference. The action path runs uninterrupted and the background sits after it.
|
||||
|
||||
A guide about database tables settled here:
|
||||
|
||||
```
|
||||
## What is a table? <- short conceptual opener
|
||||
## Creating and managing tables <- procedures
|
||||
### Creating tables
|
||||
### Securing your tables
|
||||
### Loading data
|
||||
### Joining tables with foreign keys
|
||||
## How tables are organized <- context
|
||||
### Primary keys
|
||||
### Relationships between tables
|
||||
### Schemas
|
||||
## Reference
|
||||
### Data types
|
||||
```
|
||||
|
||||
"Joining tables with foreign keys" held both classes. The steps kept the heading and stayed in the procedures group; the idea of a relational database moved to "Relationships between tables" in the context group.
|
||||
|
||||
### 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 CONTRIBUTING.md and 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.
|
||||
|
||||
**Lint and format.** 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: `pnpm lint:mdx`, 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:**
|
||||
|
||||
- Mixed information types, navigation, and glue: the Guides section of [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md)
|
||||
- Procedure format: the Procedures section of [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md)
|
||||
- Terminology: [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md)
|
||||
|
||||
**Sibling skills:**
|
||||
|
||||
- Pitfalls and drafting mechanics: [`common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md), [`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,7 +17,7 @@ _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).
|
||||
@@ -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`).
|
||||
@@ -69,6 +69,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 apps/docs/CONTRIBUTING.md and
|
||||
apps/docs/WORD_LIST.md. 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 +125,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
|
||||
|
||||
@@ -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
|
||||
@@ -1,19 +1,47 @@
|
||||
## 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 references [WORD_LIST](https://github.com/supabase/supabase/blob/master/apps/docs/WORD_LIST.md) and the docs [CONTRIBUTING](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide
|
||||
@@ -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 }}
|
||||
|
||||
@@ -22,5 +22,5 @@ jobs:
|
||||
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.',
|
||||
body: 'The `api-deploy-required` label was auto-applied to this PR because it updates the API types. The `Verify production API types` check reports whether the committed types match production; it is currently observational and does not block merging.',
|
||||
})
|
||||
@@ -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)"
|
||||
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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -5,12 +5,22 @@ on:
|
||||
branches: ['master']
|
||||
paths:
|
||||
- 'apps/www/**/*.ts*'
|
||||
- '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,6 +43,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
apps/www
|
||||
apps/docs/content/guides
|
||||
packages
|
||||
supabase
|
||||
patches
|
||||
|
||||
@@ -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
|
||||
@@ -1,3 +1,4 @@
|
||||
^./i18n
|
||||
^./packages/api-types
|
||||
^./apps/www/lib/redirects.js
|
||||
^./apps/www/lib/redirects.js
|
||||
^./apps/studio/public/*
|
||||
@@ -7,6 +7,7 @@ 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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -125,7 +125,6 @@ export function ComponentPreview({
|
||||
<Button
|
||||
className="rounded-full"
|
||||
onClick={() => setExpandState(!expand)}
|
||||
variant="default"
|
||||
icon={<Expand className="text-foreground-lighter" />}
|
||||
>
|
||||
{expand ? 'Collapse code' : 'Expand code'}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,13 +15,14 @@ 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.
|
||||
|
||||
@@ -59,16 +60,17 @@ focus-visible:ring-offset-background
|
||||
Uses `outline` (not `ring`) so it paints reliably on interactive `<tr>`s. Tailwind `ring` is `box-shadow`, which browsers often skip on `display: table-row` (notably Safari). Do not put `focus-ring` or raw `ring-*` on a `<tr>`, and do not add `outline-hidden` alongside `focus-inset`. `outline-hidden` sets `outline-style: none` and will hide the indicator.
|
||||
|
||||
```txt
|
||||
outline: 2px solid transparent
|
||||
outline-offset: -2px
|
||||
transition-property: color, background-color, border-color, ...
|
||||
|
||||
&:focus-visible {
|
||||
outline-style: solid
|
||||
outline-width: 2px
|
||||
outline-offset: -2px
|
||||
outline-color: var(--ring)
|
||||
border-radius: var(--radius-md)
|
||||
}
|
||||
```
|
||||
|
||||
`outline-hidden` is always on (not `focus-visible:`-prefixed) so mouse click does not show the browser’s default outline; the focus indicator replaces it for keyboard focus only.
|
||||
`focus-ring` keeps `outline-hidden` always on so mouse clicks do not show the browser’s default outline. `focus-inset` reserves a transparent outline instead. Its transition property list deliberately excludes outline properties so the keyboard focus indicator appears immediately, even when a call site uses `transition-all`.
|
||||
|
||||
Rules:
|
||||
|
||||
@@ -124,6 +126,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.
|
||||
|
||||
@@ -9,10 +9,18 @@ The shorthand utility classes below simplify our full color palette by providing
|
||||
|
||||
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. On light-theme
|
||||
surfaces it meets the 4.5:1 WCAG AA requirement for normal text. Use `bg-brand-default` /
|
||||
`border-brand-default` when you need the canonical bright Supabase green as a fill or border.
|
||||
|
||||
<Colors definition={'text'} />
|
||||
|
||||
## Background
|
||||
|
||||
Use `bg-brand-default` when the canonical Supabase green is required as a fill. The same
|
||||
`brand-default` suffix applies to borders and other non-text utilities, such as
|
||||
`border-brand-default`.
|
||||
|
||||
<Colors definition={'background'} />
|
||||
|
||||
### App backgrounds
|
||||
|
||||
@@ -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.
|
||||
@@ -143,8 +138,22 @@ Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions
|
||||
[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
|
||||
```
|
||||
|
||||
@@ -19,18 +19,21 @@ See installation instructions for the [Popover](/docs/components/popover#install
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { Check } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
ComboboxTrigger,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
CommandList,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from 'ui'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const frameworks = [
|
||||
@@ -63,17 +66,15 @@ export function ComboboxDemo() {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
<ComboboxTrigger
|
||||
aria-expanded={open}
|
||||
className="w-[200px] justify-between"
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
className={cn('w-[200px]', !value && 'text-foreground-lighter')}
|
||||
>
|
||||
{value
|
||||
? frameworks.find((framework) => framework.value === value)?.label
|
||||
: 'Select framework...'}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</ComboboxTrigger>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
|
||||
@@ -41,6 +41,12 @@ import {
|
||||
|
||||
<ComponentPreview name="multi-select-disabled" />
|
||||
|
||||
### Without trigger icon
|
||||
|
||||
Set `showIcon={false}` to hide the trigger icon.
|
||||
|
||||
<ComponentPreview name="multi-select-without-icon" />
|
||||
|
||||
### Badge Combo Box
|
||||
|
||||
Use `MultiSelectorInput`to add a search input.
|
||||
@@ -54,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" />
|
||||
|
||||
|
||||
@@ -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,9 @@ 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` is independent from `bg-brand-default`, `border-brand-default`, and other non-text brand utilities. Its light-mode value has at least 4.5:1 contrast against the light surfaces used by the apps, meeting WCAG AA for normal text.
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const rawBasePath = process.env.NEXT_PUBLIC_BASE_PATH || 'design-system'
|
||||
|
||||
export const BASE_PATH = rawBasePath.startsWith('/') ? rawBasePath : `/${rawBasePath}`
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -52,11 +52,11 @@ export default function ComposedChartBasic() {
|
||||
},
|
||||
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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { Check } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Button,
|
||||
ComboboxTrigger,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
@@ -47,18 +47,15 @@ export default function ComboboxDemo() {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
role="combobox"
|
||||
size={'small'}
|
||||
<ComboboxTrigger
|
||||
aria-expanded={open}
|
||||
className="w-[200px] justify-between"
|
||||
iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
className={cn('w-[200px]', !value && 'text-foreground-lighter')}
|
||||
>
|
||||
{value
|
||||
? frameworks.find((framework) => framework.value === value)?.label
|
||||
: 'Select framework...'}
|
||||
</Button>
|
||||
</ComboboxTrigger>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { Check } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
ComboboxTrigger,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
@@ -46,6 +48,7 @@ const FormSchema = z.object({
|
||||
})
|
||||
|
||||
export default function ComboboxForm() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
@@ -69,28 +72,18 @@ export default function ComboboxForm() {
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Language</FormLabel>
|
||||
<Popover>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="default"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'w-[200px] justify-between',
|
||||
!field.value && 'text-muted-foreground'
|
||||
)}
|
||||
size="small"
|
||||
iconRight={
|
||||
<ChevronsUpDown
|
||||
className="ml-2 h-4 w-4 shrink-0 opacity-50"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
}
|
||||
<ComboboxTrigger
|
||||
aria-expanded={open}
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
className={cn('w-[200px]', !field.value && 'text-foreground-lighter')}
|
||||
>
|
||||
{field.value
|
||||
? languages.find((language) => language.value === field.value)?.label
|
||||
: 'Select language'}
|
||||
</Button>
|
||||
</ComboboxTrigger>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
@@ -105,6 +98,7 @@ export default function ComboboxForm() {
|
||||
key={language.value}
|
||||
onSelect={() => {
|
||||
form.setValue('language', language.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SignOutButton,
|
||||
SupabaseLogo,
|
||||
} from './connect-interstitial-shared'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
|
||||
/** Stand-in uploaded OAuth icon: checked-in solid-colour bitmap (not a real brand). */
|
||||
function UploadedAppLogo() {
|
||||
@@ -15,7 +16,7 @@ function UploadedAppLogo() {
|
||||
<LogoBox className="border-black/10 bg-white">
|
||||
<img
|
||||
alt="Acme"
|
||||
src={`${process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'}/img/icons/acme-oauth-icon.png`}
|
||||
src={`${BASE_PATH}/img/icons/acme-oauth-icon.png`}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</LogoBox>
|
||||
|
||||
@@ -133,5 +133,5 @@ export function InterstitialActionError({ error }: { error?: React.ReactNode })
|
||||
}
|
||||
|
||||
export function SignOutButton() {
|
||||
return <Button variant="default" icon={<LogOut />} className="px-2" aria-label="Sign out" />
|
||||
return <Button icon={<LogOut />} className="px-2" aria-label="Sign out" />
|
||||
}
|
||||
@@ -18,9 +18,7 @@ export default function CopyConfirmations() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="default" size="tiny">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="tiny">Cancel</Button>
|
||||
<Button variant="danger" size="tiny">
|
||||
Delete
|
||||
</Button>
|
||||
@@ -40,9 +38,7 @@ export default function CopyConfirmations() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="default" size="tiny">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="tiny">Cancel</Button>
|
||||
<Button variant="danger" size="tiny">
|
||||
Delete project
|
||||
</Button>
|
||||
|
||||
@@ -139,7 +139,7 @@ export const columns: ColumnDef<Payment>[] = [
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" className="px-1.5" icon={<MoreVertical />} />
|
||||
<Button className="px-1.5" icon={<MoreVertical />} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="max-w-48">
|
||||
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)}>
|
||||
@@ -200,7 +200,7 @@ export default function DataTableDemo() {
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" className="ml-auto" size="tiny" iconRight={<ChevronDown />}>
|
||||
<Button className="ml-auto" size="tiny" iconRight={<ChevronDown />}>
|
||||
Columns
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -303,19 +303,13 @@ export default function DataTableDemo() {
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="tiny"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="tiny"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<Button size="tiny" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Button,
|
||||
Calendar,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
@@ -53,11 +54,13 @@ export default function DatePickerForm() {
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date of birth</FormLabel>
|
||||
<DatePicker>
|
||||
<DatePickerTrigger asChild>
|
||||
<DatePickerButton isInvalid={fieldState.invalid}>
|
||||
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
|
||||
</DatePickerButton>
|
||||
</DatePickerTrigger>
|
||||
<FormControl>
|
||||
<DatePickerTrigger asChild>
|
||||
<DatePickerButton isInvalid={fieldState.invalid}>
|
||||
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
|
||||
</DatePickerButton>
|
||||
</DatePickerTrigger>
|
||||
</FormControl>
|
||||
<DatePickerContent>
|
||||
<Calendar
|
||||
mode="single"
|
||||
@@ -73,7 +76,9 @@ export default function DatePickerForm() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function DialogDemo() {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default">Edit profile</Button>
|
||||
<Button>Edit profile</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]" centered={false}>
|
||||
<DialogHeader>
|
||||
@@ -40,7 +40,9 @@ export default function DialogDemo() {
|
||||
</div>
|
||||
</DialogSection>
|
||||
<DialogFooter>
|
||||
<Button type="submit">Save changes</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Save changes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function DialogCloseButton() {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default">Share</Button>
|
||||
<Button>Share</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
@@ -43,9 +43,7 @@ export default function DialogCloseButton() {
|
||||
</DialogSection>
|
||||
<DialogFooter className="sm:justify-start">
|
||||
<DialogClose asChild>
|
||||
<Button variant="default" type="button">
|
||||
Custom Close Button
|
||||
</Button>
|
||||
<Button type="button">Custom Close Button</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function DialogDemo() {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default">Show Dialog</Button>
|
||||
<Button>Show Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
@@ -36,7 +36,7 @@ export default function DialogDemo() {
|
||||
</div>
|
||||
</DialogSection>
|
||||
<DialogFooter>
|
||||
<Button>Save changes</Button>
|
||||
<Button variant="primary">Save changes</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { CirclePause } from 'lucide-react'
|
||||
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
|
||||
const UNAVAILABLE_REASON = 'Pausing is unavailable on High Availability projects'
|
||||
|
||||
export default function DisabledFocusable() {
|
||||
const unavailable = true
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
icon={<CirclePause />}
|
||||
disabled={unavailable}
|
||||
focusableWhenDisabled
|
||||
>
|
||||
Pause project
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{UNAVAILABLE_REASON}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { CirclePause } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
|
||||
const UNAVAILABLE_REASON = 'Pausing is unavailable on High Availability projects'
|
||||
|
||||
export default function DisabledUnavailableWithNotice() {
|
||||
const unavailable = true
|
||||
|
||||
return (
|
||||
<Card className="max-w-lg">
|
||||
<CardHeader className="border-b-0">
|
||||
<CardTitle>Pause project</CardTitle>
|
||||
</CardHeader>
|
||||
<Admonition
|
||||
type="default"
|
||||
layout="horizontal"
|
||||
title="High Availability project"
|
||||
description="Some infrastructure actions are unavailable on High Availability projects."
|
||||
className="mb-0 rounded-none border-x-0"
|
||||
/>
|
||||
<CardContent className="flex justify-end pt-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
icon={<CirclePause />}
|
||||
disabled={unavailable}
|
||||
focusableWhenDisabled
|
||||
>
|
||||
Pause project
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{UNAVAILABLE_REASON}</TooltipContent>
|
||||
</Tooltip>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -67,9 +67,7 @@ export default function DrawerDemo() {
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="default" size="small">
|
||||
Open Drawer
|
||||
</Button>
|
||||
<Button size="small">Open Drawer</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
@@ -121,7 +119,7 @@ export default function DrawerDemo() {
|
||||
</div>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button>Submit</Button>
|
||||
<Button variant="primary">Submit</Button>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
|
||||
@@ -81,7 +81,9 @@ function ProfileForm({ className }: React.ComponentProps<'form'>) {
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input id="username" defaultValue="@shadcn" />
|
||||
</div>
|
||||
<Button type="submit">Save changes</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Save changes
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export default function EmptyStateMissingRoute() {
|
||||
title="Unable to find bucket"
|
||||
description={`${bucketId ? `The bucket “${bucketId}”` : 'This bucket'} doesn’t seem to exist.`}
|
||||
>
|
||||
<Button asChild variant="default" className="mt-2">
|
||||
<Button asChild className="mt-2">
|
||||
<Link
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function EmptyStatePresentationalIcon() {
|
||||
title="Add a provider"
|
||||
description="Use third-party authentication systems to access your project."
|
||||
>
|
||||
<Button size="tiny" variant="default" icon={<Plus size={14} />}>
|
||||
<Button size="tiny" icon={<Plus size={14} />}>
|
||||
Add provider
|
||||
</Button>
|
||||
</EmptyStatePresentational>
|
||||
|
||||
@@ -113,10 +113,10 @@ export default function FieldDemo() {
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="button" variant="default">
|
||||
Cancel
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<Button type="button">Cancel</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function FieldResponsive() {
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="button" variant="default">
|
||||
Cancel
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<Button type="button">Cancel</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
|
||||
@@ -25,9 +25,7 @@ function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-end gap-2 py-3 px-4 border-t">
|
||||
<Button variant="default" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
|
||||
@@ -340,7 +340,6 @@ export default function FormPatternsPageLayout() {
|
||||
</Button>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button
|
||||
variant="default"
|
||||
size="tiny"
|
||||
icon={<Upload size={14} />}
|
||||
onClick={() => uploadButtonRef.current?.click()}
|
||||
@@ -349,7 +348,6 @@ export default function FormPatternsPageLayout() {
|
||||
</Button>
|
||||
{logoUrl && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="tiny"
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => {
|
||||
@@ -454,7 +452,6 @@ export default function FormPatternsPageLayout() {
|
||||
{file.name}
|
||||
</span>
|
||||
<Button
|
||||
variant="default"
|
||||
size="tiny"
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => {
|
||||
@@ -613,14 +610,16 @@ export default function FormPatternsPageLayout() {
|
||||
values={field.value}
|
||||
size="small"
|
||||
>
|
||||
<MultiSelectorTrigger
|
||||
mode="inline-combobox"
|
||||
label="Select options..."
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
deletableBadge
|
||||
className="w-full"
|
||||
/>
|
||||
<FormControl>
|
||||
<MultiSelectorTrigger
|
||||
mode="inline-combobox"
|
||||
label="Select options..."
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
deletableBadge
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="public">public</MultiSelectorItem>
|
||||
@@ -675,23 +674,23 @@ export default function FormPatternsPageLayout() {
|
||||
label="Date Picker"
|
||||
description="Date selection with calendar popover"
|
||||
>
|
||||
<FormControl>
|
||||
<DatePicker>
|
||||
<DatePicker>
|
||||
<FormControl>
|
||||
<DatePickerTrigger asChild>
|
||||
<DatePickerButton block isInvalid={fieldState.invalid}>
|
||||
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
|
||||
</DatePickerButton>
|
||||
</DatePickerTrigger>
|
||||
<DatePickerContent>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
initialFocus
|
||||
/>
|
||||
</DatePickerContent>
|
||||
</DatePicker>
|
||||
</FormControl>
|
||||
</FormControl>
|
||||
<DatePickerContent>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
initialFocus
|
||||
/>
|
||||
</DatePickerContent>
|
||||
</DatePicker>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
@@ -758,24 +757,17 @@ export default function FormPatternsPageLayout() {
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-end">
|
||||
<Button
|
||||
variant="default"
|
||||
icon={<ExternalLink size={14} />}
|
||||
onClick={() => console.log('Action performed')}
|
||||
>
|
||||
View documentation
|
||||
</Button>
|
||||
<Button variant="default" onClick={() => console.log('Reset action')}>
|
||||
Reset API key
|
||||
</Button>
|
||||
<Button onClick={() => console.log('Reset action')}>Reset API key</Button>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{form.formState.isDirty && (
|
||||
<Button variant="default" onClick={() => form.reset()}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{form.formState.isDirty && <Button onClick={() => form.reset()}>Cancel</Button>}
|
||||
<Button variant="primary" type="submit" disabled={!form.formState.isDirty}>
|
||||
Save changes
|
||||
</Button>
|
||||
|
||||
@@ -338,7 +338,6 @@ export default function FormPatternsSidePanel() {
|
||||
</Button>
|
||||
{logoUrl && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="tiny"
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => {
|
||||
@@ -445,7 +444,6 @@ export default function FormPatternsSidePanel() {
|
||||
{file.name}
|
||||
</span>
|
||||
<Button
|
||||
variant="default"
|
||||
size="tiny"
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => {
|
||||
@@ -610,14 +608,16 @@ export default function FormPatternsSidePanel() {
|
||||
size="small"
|
||||
className="w-full"
|
||||
>
|
||||
<MultiSelectorTrigger
|
||||
mode="inline-combobox"
|
||||
label="Select options..."
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
deletableBadge
|
||||
className="w-full"
|
||||
/>
|
||||
<FormControl>
|
||||
<MultiSelectorTrigger
|
||||
mode="inline-combobox"
|
||||
label="Select options..."
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
deletableBadge
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="public">public</MultiSelectorItem>
|
||||
@@ -677,23 +677,23 @@ export default function FormPatternsSidePanel() {
|
||||
label="Date Picker"
|
||||
description="Date selection with calendar popover"
|
||||
>
|
||||
<FormControl className="col-span-6">
|
||||
<DatePicker>
|
||||
<DatePicker>
|
||||
<FormControl className="col-span-6">
|
||||
<DatePickerTrigger asChild>
|
||||
<DatePickerButton block isInvalid={fieldState.invalid}>
|
||||
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
|
||||
</DatePickerButton>
|
||||
</DatePickerTrigger>
|
||||
<DatePickerContent>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
initialFocus
|
||||
/>
|
||||
</DatePickerContent>
|
||||
</DatePicker>
|
||||
</FormControl>
|
||||
</FormControl>
|
||||
<DatePickerContent>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
initialFocus
|
||||
/>
|
||||
</DatePickerContent>
|
||||
</DatePicker>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
@@ -770,15 +770,12 @@ export default function FormPatternsSidePanel() {
|
||||
>
|
||||
<div className="col-span-6 flex gap-2 items-center">
|
||||
<Button
|
||||
variant="default"
|
||||
icon={<ExternalLink size={14} />}
|
||||
onClick={() => console.log('Action performed')}
|
||||
>
|
||||
View documentation
|
||||
</Button>
|
||||
<Button variant="default" onClick={() => console.log('Reset action')}>
|
||||
Reset API key
|
||||
</Button>
|
||||
<Button onClick={() => console.log('Reset action')}>Reset API key</Button>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
</SheetSection>
|
||||
@@ -786,7 +783,6 @@ export default function FormPatternsSidePanel() {
|
||||
</Form>
|
||||
<SheetFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
form.reset()
|
||||
setOpen(false)
|
||||
|
||||
@@ -33,11 +33,7 @@ export default function InnerSideMenuEmpty() {
|
||||
title="No functions found"
|
||||
description="Create your first serverless function to get started."
|
||||
illustration={<div className="text-4xl">🚀</div>}
|
||||
actions={
|
||||
<Button variant="default" onClick={() => setHasItems(true)}>
|
||||
Create Function
|
||||
</Button>
|
||||
}
|
||||
actions={<Button onClick={() => setHasItems(true)}>Create Function</Button>}
|
||||
/>
|
||||
</InnerSideMenuCollapsibleContent>
|
||||
</InnerSideMenuCollapsible>
|
||||
@@ -61,11 +57,7 @@ export default function InnerSideMenuEmpty() {
|
||||
/>
|
||||
</figure>
|
||||
}
|
||||
actions={
|
||||
<Button variant="default" onClick={() => setHasItems(true)}>
|
||||
Create Function
|
||||
</Button>
|
||||
}
|
||||
actions={<Button onClick={() => setHasItems(true)}>Create Function</Button>}
|
||||
/>
|
||||
</InnerSideMenuCollapsibleContent>
|
||||
</InnerSideMenuCollapsible>
|
||||
|
||||
@@ -71,7 +71,9 @@ export default function InputOTPForm() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
|
||||
@@ -11,10 +11,7 @@ export default function KeyboardShortcutDemo() {
|
||||
<div className="flex w-full max-w-2xl flex-col gap-6">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button iconRight={<KeyboardShortcut keys={['Meta', 'S']} variant="inline" />}>Save</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
iconRight={<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />}
|
||||
>
|
||||
<Button iconRight={<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />}>
|
||||
Run query
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,7 @@ export default function KeyboardShortcutInline() {
|
||||
<div className="flex w-full max-w-xl flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button iconRight={<KeyboardShortcut keys={['Meta', 'S']} variant="inline" />}>Save</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
iconRight={<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />}
|
||||
>
|
||||
<Button iconRight={<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Minus, Plus } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
@@ -15,31 +17,42 @@ export default function MultiSelectDemo() {
|
||||
'Date',
|
||||
'Elderberrie',
|
||||
])
|
||||
const [limit, setLimit] = useState(3)
|
||||
|
||||
return (
|
||||
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
|
||||
<MultiSelectorTrigger
|
||||
className="w-72"
|
||||
label="Select fruits"
|
||||
persistLabel
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
deletableBadge={false}
|
||||
/>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="tiny" onClick={() => setLimit((value) => value - 1)} disabled={limit < 1}>
|
||||
<Minus size={12} />
|
||||
</Button>
|
||||
<span className="text-sm font-semibold text-foreground/90">Limit: {limit}</span>
|
||||
<Button size="tiny" onClick={() => setLimit((value) => value + 1)}>
|
||||
<Plus size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
|
||||
<MultiSelectorTrigger
|
||||
className="w-72"
|
||||
label="Select fruits"
|
||||
badgeLimit={limit}
|
||||
wrapBadges
|
||||
deletableBadge={false}
|
||||
/>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -16,18 +16,13 @@ export default function MultiSelectDemo() {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="default"
|
||||
onClick={() => setLimit(limit - 1)}
|
||||
disabled={limit < 1}
|
||||
>
|
||||
<Button size="tiny" onClick={() => setLimit(limit - 1)} disabled={limit < 1}>
|
||||
<Minus size={12} />
|
||||
</Button>
|
||||
<span className="text-sm text-foreground/90 peer-checked:line-through font-semibold hover:cursor-pointer">
|
||||
Limit: {limit}
|
||||
</span>
|
||||
<Button size="tiny" variant="default" onClick={() => setLimit(limit + 1)}>
|
||||
<Button size="tiny" onClick={() => setLimit(limit + 1)}>
|
||||
<Plus size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ export default function MultiSelectDemo() {
|
||||
label="Select fruits"
|
||||
deletableBadge
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
/>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
|
||||
@@ -25,12 +25,7 @@ export default function MultiSelectDemo() {
|
||||
|
||||
return (
|
||||
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
|
||||
<MultiSelectorTrigger
|
||||
className="w-72"
|
||||
label="Select fruits"
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
/>
|
||||
<MultiSelectorTrigger className="w-72" label="Select fruits" badgeLimit="wrap" />
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
{fruits.map(({ value, isDisabled }) => (
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function MultiSelectDemo() {
|
||||
<div>
|
||||
<Label htmlFor="fruits">Fruits</Label>
|
||||
<MultiSelector id="fruits" values={selectedValues} onValuesChange={setSelectedValues}>
|
||||
<MultiSelectorTrigger label="Select fruits" badgeLimit="wrap" showIcon={false} />
|
||||
<MultiSelectorTrigger label="Select fruits" badgeLimit="wrap" />
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
{fruits.map(({ value, isDisabled }) => (
|
||||
@@ -63,7 +63,7 @@ export default function MultiSelectDemo() {
|
||||
</div>
|
||||
</DialogSection>
|
||||
<DialogFooter>
|
||||
<Button>Save changes</Button>
|
||||
<Button variant="primary">Save changes</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -18,7 +18,6 @@ export default function MultiSelectDemo() {
|
||||
label="Select fruits"
|
||||
deletableBadge
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
/>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorItem,
|
||||
MultiSelectorList,
|
||||
MultiSelectorTrigger,
|
||||
} from 'ui-patterns/multi-select'
|
||||
|
||||
export default function MultiSelectWithoutIcon() {
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([])
|
||||
|
||||
return (
|
||||
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
|
||||
<MultiSelectorTrigger className="w-72" label="Select fruits" showIcon={false} />
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
|
||||
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
)
|
||||
}
|
||||
@@ -27,9 +27,7 @@ export default function PageHeaderDemo() {
|
||||
</PageHeaderDescription>
|
||||
</PageHeaderSummary>
|
||||
<PageHeaderAside>
|
||||
<Button variant="default" size="small">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button size="small">Secondary</Button>
|
||||
<Button variant="primary" size="small">
|
||||
Deploy Function
|
||||
</Button>
|
||||
|
||||
@@ -197,7 +197,7 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
|
||||
layout="horizontal"
|
||||
className="mb-4"
|
||||
actions={
|
||||
<Button variant="default" size="tiny" onClick={onNavigateToSmtp}>
|
||||
<Button size="tiny" onClick={onNavigateToSmtp}>
|
||||
Set up SMTP
|
||||
</Button>
|
||||
}
|
||||
@@ -283,9 +283,7 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
|
||||
})}
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{notificationsForm.formState.isDirty && (
|
||||
<Button variant="default" onClick={() => notificationsForm.reset()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => notificationsForm.reset()}>Cancel</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -495,11 +493,7 @@ function SmtpPage() {
|
||||
)}
|
||||
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{form.formState.isDirty && (
|
||||
<Button variant="default" onClick={() => form.reset()}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{form.formState.isDirty && <Button onClick={() => form.reset()}>Cancel</Button>}
|
||||
<Button variant="primary" type="submit" disabled={!form.formState.isDirty}>
|
||||
Save changes
|
||||
</Button>
|
||||
|
||||
@@ -82,9 +82,7 @@ export default function PageLayoutDetail() {
|
||||
<p className="text-sm">March 15, 2024</p>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<Button variant="default" size="small">
|
||||
Change Plan
|
||||
</Button>
|
||||
<Button size="small">Change Plan</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -114,9 +112,7 @@ export default function PageLayoutDetail() {
|
||||
<p className="text-sm">$234.50</p>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<Button variant="default" size="small">
|
||||
Configure Limits
|
||||
</Button>
|
||||
<Button size="small">Configure Limits</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -146,9 +142,7 @@ export default function PageLayoutDetail() {
|
||||
<p className="text-sm">12/2025</p>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<Button variant="default" size="small">
|
||||
Update Payment Method
|
||||
</Button>
|
||||
<Button size="small">Update Payment Method</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -234,9 +234,7 @@ export default function PageLayoutEdgeFunction() {
|
||||
<PageBreadcrumbs
|
||||
actions={
|
||||
<PageBreadcrumbsActions>
|
||||
<Button variant="default" size="tiny">
|
||||
Test
|
||||
</Button>
|
||||
<Button size="tiny">Test</Button>
|
||||
<Button variant="primary" size="tiny">
|
||||
Deploy
|
||||
</Button>
|
||||
@@ -426,7 +424,7 @@ function OverviewPage() {
|
||||
<PageSectionTitle>Errors since last deploy</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
<PageSectionAside>
|
||||
<Button variant="default" size="tiny" icon={<ExternalLink size={14} />}>
|
||||
<Button size="tiny" icon={<ExternalLink size={14} />}>
|
||||
View logs
|
||||
</Button>
|
||||
</PageSectionAside>
|
||||
@@ -437,7 +435,7 @@ function OverviewPage() {
|
||||
<Check
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="mt-0.5 shrink-0 text-brand"
|
||||
className="mt-0.5 shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div>
|
||||
@@ -704,7 +702,7 @@ function CodePage() {
|
||||
<h3 className="text-sm font-normal font-mono uppercase text-lighter tracking-wide">
|
||||
Files
|
||||
</h3>
|
||||
<Button size="tiny" variant="default" icon={<Plus size={14} />} onClick={addNewFile}>
|
||||
<Button size="tiny" icon={<Plus size={14} />} onClick={addNewFile}>
|
||||
Add File
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -20,9 +20,7 @@ export default function PageLayoutFullWidth() {
|
||||
<PageBreadcrumbs
|
||||
actions={
|
||||
<PageBreadcrumbsActions>
|
||||
<Button variant="default" size="tiny">
|
||||
Docs
|
||||
</Button>
|
||||
<Button size="tiny">Docs</Button>
|
||||
</PageBreadcrumbsActions>
|
||||
}
|
||||
>
|
||||
|
||||
Loaded 100 of 2599 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user