Merge branch 'master' into chore/function-page-design

This commit is contained in:
Saxon Fletcher committed 2026-07-13 11:55:20 +10:00
commit 0a733ecf4e
3766 files changed
+133177 -74148

No files matched your search

@@ -0,0 +1,226 @@
---
name: clickhouse-logs-queries
description: >-
Write, review, and migrate Supabase logs queries against the ClickHouse-backed
`logs` table (the `logs.all.otel` analytics endpoint). Use this whenever a task
involves Logs Explorer SQL, the `log_attributes` map, querying a log `source`
(edge_logs, postgres_logs, auth_logs, etc.), translating an old BigQuery
`cross join unnest(metadata)` logs query to ClickHouse, or wiring analytics log
SQL in `apps/studio/data/logs` and `apps/studio/components/interfaces/Settings/Logs`.
Reach for it even when the user just says "logs query", "Logs Explorer", or
pastes a BigQuery logs query to convert, not only when they name ClickHouse.
---
# Querying Supabase logs (ClickHouse)
Supabase logs live in a single ClickHouse `logs` table, served by the
`logs.all.otel` analytics endpoint. Every log line from every part of the stack
is one row in this table, tagged by a `source` column. This replaces the older
BigQuery model, where each service had its own table and fields were reached
through `cross join unnest(metadata)`.
Two kinds of work use this skill, and they share the same SQL model:
1. **Writing or reviewing a logs query** (in the Logs Explorer or anywhere a raw
ClickHouse logs query is needed). Start here in this file.
2. **Wiring a logs query in the Studio codebase** (branded analytics SQL, the
endpoint picker, the OTEL query builders). Read
[references/codebase-integration.md](references/codebase-integration.md).
If you are converting an existing BigQuery logs query, read
[references/bigquery-migration.md](references/bigquery-migration.md) for the full
translation table.
## The logs table
Each row has a small set of real columns. Everything specific to a service lives
in `log_attributes`.
| Column | Type | Notes |
| ---------------- | --------------------- | ----------------------------------------------------- |
| `id` | `String` | Unique log identifier. |
| `timestamp` | `DateTime64` (UTC) | When the log was produced. Order/compare it directly. |
| `event_message` | `String` | The raw log line. |
| `severity_text` | `String` | Log level, when the source sets one. |
| `source` | `String` | The service the log came from. Always filter on this. |
| `log_attributes` | `Map(String, String)` | Structured per-source fields, keyed by a dotted path. |
`timestamp` is formatted like `2026-06-22T09:34:06.215000` (ISO 8601, microsecond
precision, no trailing `Z`). In the Logs Explorer the selected time range is
applied for you, so you rarely need to write a `timestamp` filter by hand.
A minimal, well-formed query. Lead with a comment naming the query, filter by
`source`, and always `limit`:
```sql
-- recent edge requests
select timestamp, event_message
from logs
where source = 'edge_logs'
order by timestamp desc
limit 100;
```
## Sources
`source` selects the service. The common ones:
- `edge_logs` — API gateway requests and responses
- `postgres_logs` — database statements and errors (also where pg_cron logs live)
- `auth_logs` — authentication and authorization activity
- `function_edge_logs` — edge function requests and responses
- `function_logs` — `console` output from inside edge functions
- `storage_logs` — object upload and retrieval activity
- `realtime_logs` — Realtime client connections
- `postgrest_logs`, `supavisor_logs`, `pgbouncer_logs` — mostly `id`, `timestamp`, `event_message`
The Logs Explorer **Field Reference** drawer lists every source and the fields it
actually sets. When in doubt about a key, discover it from real data rather than
guessing (see below).
## Reading fields from log_attributes
`log_attributes` maps a string key to a string value. Read a field with bracket
access. There are no unnesting joins:
```sql
select
log_attributes['request.method'] as method,
log_attributes['request.path'] as path,
log_attributes['response.status_code'] as status
from logs
where source = 'edge_logs'
```
The key keeps the dotted path that BigQuery expressed through nested structs, with
the `metadata` root dropped: BigQuery `metadata.request.method` becomes
`log_attributes['request.method']`. Keep the full prefix — `request.cf.country`
is `log_attributes['request.cf.country']`, not `log_attributes['cf.country']`.
Common keys by source:
- `edge_logs`: `request.method`, `request.path`, `request.search`, `response.status_code`, `identifier`
- `postgres_logs`: `parsed.error_severity`, `parsed.detail`, `parsed.hint`, `parsed.query`, `identifier`
- `auth_logs`: `level`, `status`, `path`, `msg`, `error`
- `function_edge_logs`: `response.status_code`, `request.method`, `request.pathname`, `function_id`, `execution_id`, `execution_time_ms`
- `function_logs`: `event_type`, `function_id`, `execution_id`, `level`
### Numeric fields are strings
Map values are always strings. To compare or aggregate a numeric field, wrap it in
`toInt32OrZero`, which returns `0` for missing or non-numeric values so it never
errors on partial data:
```sql
select count() as server_errors
from logs
where source = 'edge_logs'
and toInt32OrZero(log_attributes['response.status_code']) between 500 and 599
```
### Discover the keys a source sets
Read `mapKeys` from recent rows rather than guessing key names:
```sql
select arrayJoin(mapKeys(log_attributes)) as key, count() as n
from logs
where source = 'postgres_logs'
group by key
order by n desc
limit 100;
```
`arrayJoin(mapKeys(...))` flattens the map keys into one row per key so you can
rank them by frequency. (The Studio codebase does exactly this for the Field
Reference drawer and to feed real keys to the AI rewrite.)
## ClickHouse vs BigQuery functions
These are the substitutions that trip people up most:
| Need | BigQuery | ClickHouse |
| ------------------ | ----------------------------- | -------------------------------------------- |
| Count rows | `count(*)` | `count()` |
| Regex match | `regexp_contains(x, 'p')` | `match(x, 'p')` |
| Substring match | `x like '%p%'` | `x ilike '%p%'` (case-insensitive) or `like` |
| Numeric coercion | `cast(x as int64)` | `toInt32OrZero(x)` |
| Read the timestamp | `cast(timestamp as datetime)` | `timestamp` (use the column directly) |
| Map keys | n/a (used unnest) | `mapKeys(log_attributes)` |
The `logs.all.otel` analytics endpoint (and the Logs Explorer on top of it)
rejects `count(*)` and `select *` — use `count()` and list the columns you need.
(Raw ClickHouse supports both; this is a constraint of the logs query surface.)
## Best practices
These keep queries correct and cheap. Log tables are large; an unbounded scan
reads far more data than you need.
- **Start every query with an identifying comment** (e.g. `-- errors since last deploy`). It labels the query in logs and review, and makes each of several queries in a file easy to tell apart.
- **Always include a `LIMIT`.** Even for aggregates while you iterate.
- **Always query `from logs where source = '...'`.** There is no per-service table (no `edge_logs`, `postgres_logs`, etc. table) — there is one `logs` table, and `source` scopes it to a service. Filtering by `source` is required, not just an optimization.
- **Keep the time range tight.** A smaller window returns results faster.
- **Filter on the real columns** (`source`, `timestamp`) before reaching into
`log_attributes`.
- **Order by `timestamp desc`** to see the most recent logs first.
- **Use `count()`**, not `count(*)` or `select *`.
## Worked examples
Requests by status code:
```sql
select
toInt32OrZero(log_attributes['response.status_code']) as status,
count() as count
from logs
where source = 'edge_logs'
group by status
order by count desc
limit 50
```
Auth errors:
```sql
select timestamp, event_message, log_attributes['msg'] as message
from logs
where source = 'auth_logs'
and log_attributes['level'] in ('error', 'fatal')
order by timestamp desc
limit 100
```
Search the raw message:
```sql
select timestamp, event_message
from logs
where source = 'postgres_logs'
and event_message ilike '%deadlock%'
order by timestamp desc
limit 100
```
Postgres errors grouped by severity (the canonical unnest-to-map conversion):
```sql
select log_attributes['parsed.error_severity'] as severity, count() as count
from logs
where source = 'postgres_logs'
and log_attributes['parsed.error_severity'] in ('ERROR', 'FATAL', 'PANIC')
group by severity
order by count desc
limit 100
```
## When the user pastes a BigQuery query
Convert it rather than running it as-is. The mechanical steps (drop the
per-service table for `from logs where source = ...`, remove every
`cross join unnest(...)`, rewrite unnest-alias columns as `log_attributes['...']`
lookups, swap the functions above) are spelled out with a full before/after in
[references/bigquery-migration.md](references/bigquery-migration.md). The Logs
Explorer also has a built-in **Rewrite to ClickHouse** action that does this with
AI; point users to it for one-off conversions in the dashboard.
@@ -0,0 +1,93 @@
# Migrating a BigQuery logs query to ClickHouse
The old logs engine gave each service its own table and exposed structured fields
through repeated `cross join unnest(...)` over a nested `metadata` column. The
ClickHouse engine has one `logs` table and a flat `log_attributes` map. Conversion
is mechanical once you internalize the mapping.
## The five steps
1. **Replace the table with `logs` plus a `source` filter.** The old table name is
the `source` value: `from postgres_logs as t` becomes
`from logs where source = 'postgres_logs'`. Never select from a per-service
table name (`postgres_logs`, `edge_logs`, ...) on the ClickHouse engine.
- Exception: pg_cron logs live under `source = 'postgres_logs'`.
2. **Remove every unnest join.** Delete `cross join unnest(metadata) as m`,
`cross join unnest(m.parsed) as p`, `left join unnest(...) on true`, and so on.
They have no equivalent — the data is already flat in the map.
3. **Rewrite every unnest-alias column as a map lookup.** A field taken off
`unnest(metadata)` becomes `log_attributes['field']`. A field off a nested
struct like `unnest(m.parsed)` becomes `log_attributes['parsed.field']`: keep
the struct name as a dotted prefix, drop the `metadata` root and every alias.
4. **Wrap numeric fields in `toInt32OrZero(...)`** before comparing or aggregating
them — map values are strings.
5. **Swap BigQuery functions for ClickHouse ones** (see the table below).
Preserve the original select list, filters, group by, order by, and limit intent
throughout.
## Function substitutions
| Need | BigQuery | ClickHouse |
| ---------------- | ----------------------------- | --------------------------- |
| Count rows | `count(*)` | `count()` |
| Regex match | `regexp_contains(x, 'p')` | `match(x, 'p')` |
| Substring match | `x like '%p%'` | `x ilike '%p%'` or `like` |
| Numeric coercion | `cast(x as int64)` | `toInt32OrZero(x)` |
| Timestamp value | `cast(timestamp as datetime)` | `timestamp` |
| All columns | `select *` | list the columns explicitly |
## Full before/after
BigQuery:
```sql
select count(t.timestamp) as count, p.error_severity
from
postgres_logs as t
cross join unnest(metadata) as m
cross join unnest(m.parsed) as p
where p.error_severity in ('ERROR', 'FATAL', 'PANIC')
group by p.error_severity
order by count desc
limit 100;
```
ClickHouse:
```sql
select count() as count, log_attributes['parsed.error_severity'] as error_severity
from logs
where source = 'postgres_logs'
and log_attributes['parsed.error_severity'] in ('ERROR', 'FATAL', 'PANIC')
group by log_attributes['parsed.error_severity']
order by count desc
limit 100
```
Notice: `count(t.timestamp)` became `count()`, the two unnest joins are gone,
`p.error_severity` became `log_attributes['parsed.error_severity']`, and the
`from`/`where` targets the single table.
## Getting the keys right
The most common conversion mistake is dropping a dotted prefix — writing
`log_attributes['x_real_ip']` when the real key is
`log_attributes['request.headers.x_real_ip']`, or `log_attributes['cf.country']`
instead of `log_attributes['request.cf.country']`. Do not invent or shorten keys.
When unsure, discover the real keys from data:
```sql
select arrayJoin(mapKeys(log_attributes)) as key, count() as n
from logs
where source = 'edge_logs'
group by key
order by n desc
limit 200;
```
Then map each old nested path to the matching key exactly as it appears.
@@ -0,0 +1,96 @@
# Wiring a logs query in the Studio codebase
This covers writing analytics log SQL inside `apps/studio` so it is safe, routed
to the right endpoint, and consistent with the existing OTEL builders. Read this
when you are editing TypeScript that builds or runs a logs query, not when you are
just writing a query in the Logs Explorer UI.
## Branded SQL: never concatenate user input
All analytics log SQL must be a `SafeLogSqlFragment`, built with the helpers in
`apps/studio/data/logs/safe-analytics-sql.ts`. This is enforced by eslint, and the
branding is what keeps interpolated values from becoming injection. The key
exports:
- `safeSql\`...\``— a tagged template that only accepts`SafeLogSqlFragment`interpolations. Plain strings (and Postgres-branded`SafeSqlFragment`) are
rejected at compile time, so you cannot accidentally drop a raw value in.
- `analyticsLiteral(value)` — turns a string/number/boolean into a safely escaped
literal fragment (single quotes and backslashes are escaped). Use it for every
dynamic value, especially the `source`.
- `joinSqlFragments(fragments, separator)` — joins already-safe fragments with a
fixed structural separator (`' and '`, `', '`, etc.).
- `keyword(value, allowed)` — resolves a value against a compile-time allow-list of
fragments (e.g. an `AND`/`OR` operator). Returns the allow-listed fragment, never
the raw input.
- `quotedIdent(value)` — backtick-quotes a dotted identifier path after validating
each segment.
There is intentionally no exported "raw" escape hatch. Compose with `safeSql` plus
these helpers.
```ts
import { analyticsLiteral, safeSql } from 'data/logs/safe-analytics-sql'
const source = 'edge_logs'
const sql = safeSql`
select timestamp, event_message
from logs
where source = ${analyticsLiteral(source)}
order by timestamp desc
limit 100
`
```
## Pick the endpoint and builder by flag
The ClickHouse path is gated by the `otelLegacyLogs` PostHog flag
(`useFlag('otelLegacyLogs')` from `common`). Keep the BigQuery path working when
the flag is off. Two helpers in `apps/studio/data/logs/logs-endpoint.ts` express
the split:
- `logsAllEndpointUrl(useOtel)` — returns the `logs.all.otel` endpoint when
`useOtel`, otherwise the legacy `logs.all` endpoint.
- `pickLogsQueryBuilder(useOtel, otel, bq)` — picks between an OTEL builder and a
BigQuery builder while preserving the type.
```ts
const useOtel = useFlag('otelLegacyLogs')
const builder = pickLogsQueryBuilder(useOtel, genDefaultQueryOtel, genDefaultQuery)
const endpoint = logsAllEndpointUrl(useOtel)
// include { otel: useOtel } in the React Query key so the two paths cache separately
```
Run the fragment through `executeAnalyticsSql` (`apps/studio/data/logs/execute-analytics-sql.ts`)
against that endpoint.
## Follow the existing OTEL builders
When you need a new query shape, mirror the generators in
`apps/studio/components/interfaces/Settings/Logs/Logs.utils.otel.ts` rather than
inventing a parallel style. They already encode the conventions:
- `genDefaultQueryOtel`, `genCountQueryOtel`, `genChartQueryOtel`,
`genSingleLogQueryOtel` — the row/count/chart/single-log builders. They select
the real columns plus source-specific `log_attributes[...]` lookups aliased to
the leaf names the renderers expect.
- `mapOtelPreviewRow`, `mapOtelSingleLogToLegacy`, `otelTimestampToMicros` — the
JS normalization layer. `timestamp` must end up a microsecond number for the
pagination cursor and the renderers, so reuse `otelTimestampToMicros` rather
than parsing the ISO string yourself.
The map-key discovery hook (`apps/studio/data/logs/otel-log-keys-query.ts`,
`fetchOtelLogKeys` / `useOtelLogKeysQuery`) is the canonical example of a small,
correctly-branded OTEL query — read it before writing a new one.
## Checklist
- [ ] Every dynamic value goes through `analyticsLiteral` (or another sanitizer),
never string concatenation.
- [ ] The query filters by `source` and includes a `LIMIT`.
- [ ] Numeric `log_attributes` values are wrapped in `toInt32OrZero`.
- [ ] The endpoint and builder are chosen with `logsAllEndpointUrl` /
`pickLogsQueryBuilder` off `useFlag('otelLegacyLogs')`.
- [ ] The React Query key distinguishes the OTEL and BigQuery paths.
- [ ] `timestamp` is normalized to micros for any row consumed by the table/cursor.
- [ ] There is a unit test asserting the generated SQL string (see
`Logs.utils.otel.test.ts` and `safe-analytics-sql.test.ts` for the pattern).
+1 -1
View File
@@ -88,7 +88,7 @@ test.describe.configure({ mode: 'serial' })
3. **`getByText` with exact match** - Good for unique text
```typescript
page.getByText('Data API Access', { exact: true })
page.getByText('Data API access', { exact: true })
```
4. **`locator` with CSS** - Use sparingly, more fragile
@@ -99,7 +99,7 @@ response: () => HttpResponse.json<MyResponse>({}, { status: 201 })
### 3. Submit buttons in Sheets/Modals need `fireEvent.click`
The convention `<Button form={FORM_ID} htmlType="submit" />` (button
The convention `<Button form={FORM_ID} type="submit" />` (button
outside the form, associated by id) doesn't reliably trigger submission
under `userEvent.click` in jsdom. Use `fireEvent.click` for the submit
button. Continue to use `userEvent.type` for inputs.
@@ -125,3 +125,9 @@ Forms in sheets:
- `layout="horizontal"` for wider sheets
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
## Copy
Source of truth: `apps/design-system/content/docs/copywriting.mdx` — sentence case, title case, proper nouns, voice and tone.
When changing visible copy, grep `e2e/studio/` for the old string.
+92
View File
@@ -0,0 +1,92 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
# Don't inherit organization-level settings (they're tuned for other repos);
# this config is self-contained and unset values use CodeRabbit defaults.
inheritance: false
# Enrich linked issues with related code and potential solutions during review.
issue_enrichment:
auto_enrich:
enabled: true
reviews:
# Skip machine-generated / vendored files (mirrors .prettierignore). Keeps
# reviews focused on hand-written code and preserves rate-limit budget on
# large codegen diffs.
path_filters:
- '!pnpm-lock.yaml'
- '!packages/api-types/types/**' # generated API types (api.d.ts, platform.d.ts)
- '!supabase/functions/common/database-types.ts' # generated by `pnpm generate:types`
- '!**/routeTree.gen.ts' # TanStack Router generated
- '!**/__generated__/**'
- '!apps/docs/features/docs/generated/**'
- '!apps/www/.generated/**'
- '!apps/design-system/__registry__/**'
- '!apps/ui-library/__registry__/**'
- '!apps/ui-library/public/r/**' # registry output
- '!packages/icons/__registry__/**'
- '!packages/icons/src/icons/**' # generated icon components
# Targeted, path-scoped review guidance, version-controlled alongside the code.
path_instructions:
- path: 'packages/common/telemetry-constants.ts'
instructions: |
Strictly enforce event naming: [object]_[verb] in snake_case. Only approved
verbs: opened, clicked, submitted, created, removed, updated, retrieved,
intended, evaluated, added, enabled, disabled, copied, exposed, failed,
converted. Properties must be camelCase for new events (match existing
convention when adding to existing events). Flag any usage of
useSendEventMutation. Verify @group Events and @source JSDoc tags are
accurate. Check that new interfaces are added to the TelemetryEvent union type.
- path: 'apps/studio/components/**/!(*.test).tsx' # production components only, not tests
instructions: |
Only suggest adding PostHog event tracking (via useTrack from
lib/telemetry/track, [object]_[verb] snake_case) when a new user-facing
interaction is growth-relevant: e.g. first-use of a feature, onboarding steps,
project/org creation, upgrade/billing actions, enabling or disabling a product
feature, or any action that signals activation or retention. Do not suggest
tracking for: passive views, page loads, UI-only state changes (e.g. expanding
a panel, switching tabs in a settings page), developer/internal tooling
interactions, or interactions clearly unrelated to product adoption.
- path: 'apps/studio/pages/**'
instructions: |
Studio is mid-migration from the Next.js pages router (apps/studio/pages/**)
to TanStack Start (apps/studio/routes/**). Both runtimes ship side-by-side, so
every URL served from pages/** has a mirror in routes/**. See
apps/studio/TANSTACK_MIGRATION.md for the full route map and strategy.
Leave a comment reminding the author to check whether this change needs to be
mirrored into the corresponding apps/studio/routes/** file so the two builds
don't silently drift:
- Most route files re-export the page's default export (Path A), so pure
page-body edits propagate automatically — no mirror needed.
- A mirror IS needed when the change touches something the route file
duplicates rather than imports: getLayout / layout wrapping, page title or
other props the route encodes as staticData, withAuth / auth gating, or the
route/redirect path itself.
- A brand-new page under pages/** needs a matching new route under routes/**
(and a checklist entry in apps/studio/TANSTACK_MIGRATION.md).
- Do NOT suggest deleting the pages/** file — the Next file stays load-bearing
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.
# Applies our internal engineering skills (.claude/skills/) as CodeRabbit review
# guidelines. The skills are the single source of truth — they are consumed
# directly, with no copy of their content elsewhere.
#
# `applyTo` decouples where a guideline file lives from the code it governs.
# Without it, CodeRabbit scopes a guideline file to its own directory and below;
# our skills live in .claude/skills/, which contains no code, so they would never
# reach apps/studio. `applyTo` points them at the right paths instead.
knowledge_base:
code_guidelines:
filePatterns:
# Studio code conventions — React/TS, UI patterns, composition, data fetching, errors
- files: '.claude/skills/{studio-best-practices,studio-ui-patterns,vercel-composition-patterns,studio-queries,studio-error-handling}/SKILL.md'
applyTo: 'apps/studio/**/*.{ts,tsx}'
# Studio unit / component test conventions
- files: '.claude/skills/{studio-testing,studio-mock-api-tests}/SKILL.md'
applyTo: 'apps/studio/**/*.test.{ts,tsx}'
# Studio end-to-end (Playwright) test conventions
- files: '.claude/skills/studio-e2e-tests/SKILL.md'
applyTo: 'e2e/studio/**/*.spec.ts'
+3 -1
View File
@@ -8,6 +8,8 @@
/apps/studio/ @supabase/Dashboard
/apps/docs/ @supabase/docs
/apps/www/ @supabase/marketing
/apps/www/public/images/blog @supabase/marketing
/apps/www/lib/redirects.js
@@ -19,4 +21,4 @@
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
/apps/studio/pages/new/index.tsx @supabase/security
/packages/shared-data/compute-disk-limits.ts @supabase/infra
/packages/shared-data/compute-disk-limits.ts @supabase/platform
+1
View File
@@ -54,6 +54,7 @@ Path-specific rules in `.github/instructions/`:
- **Error Handling**: `studio-error-handling.instructions.md` — error classification, `ErrorMatcher` usage
- **E2E Tests**: `studio-e2e-tests.instructions.md` — selector priority, anti-patterns (`waitForTimeout`, `force: true`)
- **Composition Patterns**: `studio-composition-patterns.instructions.md` — avoid boolean props, use compound components
- **UI Copy**: `studio-copy.instructions.md` → `apps/design-system/content/docs/copywriting.mdx`
- **shadcn/Radix Components**: `studio-shadcn-components.instructions.md` — accessibility handled by primitives, do not flag
- **Keyboard Shortcuts**: `studio-shortcuts.instructions.md` — shortcut registry pattern, search-input escape handler, when to flag missing coverage
@@ -0,0 +1,13 @@
---
applyTo: 'apps/studio/**'
---
# Studio UI Copy
All comments are **advisory**.
**Source of truth:** `apps/design-system/content/docs/copywriting.mdx` — read it before writing or reviewing user-facing Studio strings.
## Agent checklist (not in the design doc)
- When changing visible copy, grep `e2e/studio/` and `.github/instructions/` for the old string.
@@ -23,7 +23,7 @@ All comments are **advisory**.
3. **`getByText` with exact match** — good for unique text
```typescript
page.getByText('Data API Access', { exact: true })
page.getByText('Data API access', { exact: true })
```
4. **`locator` with CSS** — use sparingly, more fragile
+5
View File
@@ -4,6 +4,11 @@ documentation:
- changed-files:
- any-glob-to-any-file: 'apps/docs/**/*'
# Add 'self-hosted' to any change in the self-hosting docs
self-hosted:
- changed-files:
- any-glob-to-any-file: 'apps/docs/content/guides/self-hosting/**/*'
# Add 'api-deploy-required' to any change in packages/api-types/types
api-deploy-required:
- changed-files:
+36 -2
View File
@@ -15,8 +15,42 @@ permissions:
contents: read
jobs:
# Cheap, always-on gate: verifies the eval harness can reach the MCP server
# (its one real tool, search_docs). Runs on every push/PR — no OpenAI, no full
# eval suite — so a broken MCP connection is caught early with an actionable
# message. The eval job depends on this, so evals never run against a broken
# MCP connection.
preflight:
name: Eval MCP preflight
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Preflight — MCP connectivity
run: cd apps/studio && pnpm evals:preflight
eval:
name: Run evals
needs: preflight
if: github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-evals') && github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
timeout-minutes: 20
@@ -43,8 +77,8 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
+7
View File
@@ -55,6 +55,13 @@ jobs:
working-directory: apps/docs/spec
run: make download.tsdoc.v2
- name: Generate Dart reference dump
# Dart has no upstream TypeDoc dump; its gitignored dump is generated
# from the committed `supabase_dart_v2.yml` so the reference-content
# snapshot test has something to walk.
working-directory: apps/docs
run: pnpm run codegen:references:dart
- name: Run tests
run: |
touch .env
+34 -10
View File
@@ -17,15 +17,22 @@ jobs:
strategy:
fail-fast: false
matrix:
framework: [next, tanstack]
shardIndex: [1, 2]
shardTotal: [2]
outputs:
tests_ran: ${{ steps.filter.outputs.studio == 'true' }}
permissions:
contents: write
contents: read
id-token: write
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Consumed by `apps/studio/scripts/dispatch.js` to pick which framework
# build/start scripts run. `next` keeps the existing pages-router flow;
# `tanstack` exercises the Vite/TanStack-Start migration in parallel
# so we catch regressions on either side per-PR.
STUDIO_FRAMEWORK: ${{ matrix.framework }}
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
@@ -79,9 +86,17 @@ jobs:
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-
- name: Reset supabase
if: steps.filter.outputs.studio == 'true'
run: rm -rf supabase && pnpm exec supabase init && mkdir supabase/functions
# Authenticate with AWS ECR to avoid rate limiting
- name: configure aws credentials
if: steps.filter.outputs.studio == 'true' && !github.event.pull_request.head.repo.fork
uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0
with:
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
aws-region: us-east-1
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
if: steps.filter.outputs.studio == 'true' && !github.event.pull_request.head.repo.fork
with:
registry: public.ecr.aws
- name: Pre-start diagnostics
run: |
@@ -101,7 +116,12 @@ jobs:
- name: Build studio
if: steps.filter.outputs.studio == 'true'
run: SKIP_ASSET_UPLOAD=1 NODE_ENV=test NODE_OPTIONS="--max-old-space-size=4096" pnpm run build:studio
# `MODE=test` so the Vite build (TanStack) loads `.env.test` for env
# resolution; Next ignores it but reads `NODE_ENV=test` for the same
# purpose. Vite's `vite.config.ts` overrides the bundle NODE_ENV to
# 'production' regardless of input, mirroring Next's build behaviour
# so vitest-only constants (e.g. `API_URL`) don't bleed in here.
run: SKIP_ASSET_UPLOAD=1 NODE_ENV=test MODE=test NODE_OPTIONS="--max-old-space-size=4096" pnpm run build:studio
- name: 🚀 Run Playwright tests against local studio build
if: steps.filter.outputs.studio == 'true'
@@ -112,7 +132,7 @@ jobs:
if: always() && steps.filter.outputs.studio == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: blob-report-${{ matrix.shardIndex }}
name: blob-report-${{ matrix.framework }}-${{ matrix.shardIndex }}
path: e2e/studio/blob-report
retention-days: 7
@@ -123,11 +143,15 @@ jobs:
exit 1
merge-reports:
name: 'E2E reports'
name: 'E2E reports (${{ matrix.framework }})'
# Merge reports after playwright-tests, even if some shards have failed
if: ${{ !cancelled() && needs.test.outputs.tests_ran == 'true' }}
needs: [test]
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
framework: [next, tanstack]
permissions:
contents: write
pull-requests: write
@@ -144,7 +168,7 @@ jobs:
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v 5.0.0
with:
path: e2e/studio/blob-report
pattern: blob-report-*
pattern: blob-report-${{ matrix.framework }}-*
merge-multiple: true
- name: Merge Playwright reports
@@ -152,7 +176,7 @@ jobs:
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-artifacts
name: playwright-artifacts-${{ matrix.framework }}
path: |
e2e/studio/playwright-report/
e2e/studio/test-results/
@@ -163,7 +187,7 @@ jobs:
uses: daun/playwright-report-comment@be9e270edd5ad86038604d3caa84a819a6ff6fed # v3.10.0
with:
report-file: e2e/studio/test-results/test-results.json
comment-title: '🎭 Playwright Test Results'
comment-title: '🎭 Playwright Test Results (${{ matrix.framework }})'
merge-results:
name: 'E2E results'
+8
View File
@@ -40,5 +40,13 @@ jobs:
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Validate exports
run: |
pnpm --filter ui-patterns gen:exports
if ! git diff --ignore-space-at-eol --exit-code --quiet packages/ui-patterns/package.json; then
echo "Detected uncommitted changes after build. Run pnpm --filter ui-patterns gen:exports to fix it.";
git diff;
exit 1;
fi
- name: Run tests
run: pnpm run test:ui-patterns
+2 -1
View File
@@ -112,7 +112,8 @@ next-env.d.ts
# DynamoDB Local files
.dynamodb/
.vscode
.vscode/*
!.vscode/content-listing.code-snippets
.idea
.vercel
+5
View File
@@ -21,7 +21,12 @@ apps/studio/public
apps/**/.turbo
apps/docs/CONTRIBUTING.md
apps/docs/__generated__
# Generated by apps/docs/internals/generate-markdown-manifest.ts
apps/docs/lib/markdown-manifest.ts
apps/design-system/__registry__
# TanStack Router auto-generated route tree; the file header explicitly
# says to exclude it from formatters.
apps/studio/routeTree.gen.ts
packages/icons/__registry__
packages/icons/src/icons/*.ts
apps/ui-library/__registry__
+28
View File
@@ -0,0 +1,28 @@
{
"Content listing data export": {
"prefix": "cl-data",
"scope": "typescript",
"description": "ContentListingGroup export for overview listing blocks",
"body": [
"export const ${1:topic}${2:Section}: ContentListingGroup = {",
" id: '${3:topic-section}',",
" heading: '${4:Section heading}',",
" description: '${5:Optional intro sentence}',",
" type: '${6|grid,list|}',",
" items: [",
" {",
" title: '${7:Link title}',",
" href: '${8:/guides/...}',",
" description: '${9:Short description}',",
" },",
" ],",
"}"
]
},
"Content listing inline MDX": {
"prefix": "cl-inline",
"scope": "markdown,mdx",
"description": "Inline ContentListings component in a guide MDX file",
"body": ["<ContentListings id=\"${1:topic-section}\" />"]
}
}
-4
View File
@@ -2,10 +2,6 @@
Thank you for contributing to Supabase! We’re a big, exciting open source project and we’d love to have you contribute! Here’s some resources and guidance to help you get started:
[1. Getting Started](#getting-started)
[2. Issues](#issues)
[3. Pull Requests](#pull-requests)
## Getting Started
To ensure a positive and inclusive environment, please read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md) before contributing. For help setting up the code in this repo, please follow our [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) file. For the [docs](https://supabase.com/docs) site, follow this [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide.
+5 -7
View File
@@ -1,7 +1,6 @@
# Developing Supabase
- [Developing Supabase](#developing-supabase)
- [Getting started](#getting-started)
- [Install dependencies](#install-dependencies)
- [Local development](#local-development)
@@ -34,11 +33,11 @@ To ensure a positive and inclusive environment, please read our [code of conduct
You will need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
- [Git](https://git-scm.com/)
- [Node.js](https://nodejs.org) version as documented in [.nvmrc](./.nvmrc)
- [pnpm](https://pnpm.io/) version 9.x.x or higher
- [make](https://www.gnu.org/software/make/) or the equivalent to `build-essentials` for your OS
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
- [**Git**](https://git-scm.com/)
- [**Node.js**](https://nodejs.org) (version as documented in [`.nvmrc`](./.nvmrc))
- [**pnpm**](https://pnpm.io/) (version as documented in the `packageManager` field in [`package.json`](./package.json))
- [**make**](https://www.gnu.org/software/make/) (or the equivalent to `build-essentials` for your OS)
- [**Docker**](https://docs.docker.com/get-docker/) (to run studio locally)
## Local development
@@ -120,7 +119,6 @@ For example:
1. `cd apps/studio`: move to the `studio` workspace.
2. `pnpm add react`: installs `react` into `studio` workspace.
---
## Running Docker for Supabase Studio
-1
View File
@@ -1 +0,0 @@
apps/docs/public/.well-known/security.txt
+6 -6
View File
@@ -9,10 +9,10 @@ First, make a copy of _.env.local.example_ and name it _env.local_. Then install
```bash
cd apps/design-system
pnpm i
pnpm dev:full
pnpm dev
```
The `dev:full` command runs both the Next.js development server and Contentlayer concurrently, which is recommended for most development workflows.
The `dev` command runs both the Next.js development server and Contentlayer concurrently, which is recommended for most development workflows.
### Alternative commands
@@ -20,10 +20,10 @@ You can also run the development server and content watcher separately:
```bash
# Run only the Next.js development server
pnpm dev
pnpm dev:next
# Run only the content watcher (in a separate terminal shell)
pnpm content:dev
pnpm dev:content
```
Or run the development server from the root directory:
@@ -39,14 +39,14 @@ To run both the development server and content watcher from the root directory,
pnpm dev:design-system
# Run the content watcher (in a separate terminal shell)
pnpm --filter=design-system content:dev
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:full` command automatically watches for changes to MDX files with hot reload. If you're running the `pnpm dev` separately, you'll need to run `pnpm content:dev` in a separate terminal shell to watch for content changes.
The `dev` command automatically watches for changes to MDX files with hot reload. If you're running the `pnpm dev:next` separately, you'll need to run `pnpm dev:content` in a separate terminal shell to watch for content changes.
### Adding components
@@ -1,7 +1,7 @@
'use client'
import { BarChart2, ExternalLink } from 'lucide-react'
import { Badge } from 'ui'
import { Badge, CriticalIcon } from 'ui'
import {
Chart,
ChartActions,
@@ -44,6 +44,35 @@ export default function ChartComposedStates() {
</ChartCard>
</Chart>
<Chart isErrored={true}>
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="This is a tooltip">Response Errors</ChartTitle>
<ChartActions actions={actions} />
</ChartHeader>
<ChartContent
isEmpty={true}
errorState={
<ChartEmptyState
icon={<CriticalIcon />}
title="Some error happened"
description="Error happened why trying to fetch data. Please try again later."
/>
}
emptyState={
<ChartEmptyState
icon={<BarChart2 size={20} />}
title="No data to show"
description="It may take up to 24 hours for data to refresh"
/>
}
loadingState={<ChartLoadingState />}
>
My chart here...
</ChartContent>
</ChartCard>
</Chart>
<Chart>
<ChartCard>
<ChartHeader>
+99
View File
@@ -159,6 +159,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"admonition-button-split": {
name: "admonition-button-split",
type: "components:example",
registryDependencies: ["admonition","button","dropdown-menu"],
component: React.lazy(() => import("@/registry/default/example/admonition-button-split")),
source: "",
files: ["registry/default/example/admonition-button-split.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"admonition-description-only": {
name: "admonition-description-only",
type: "components:example",
@@ -368,6 +379,72 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"breadcrumb-demo": {
name: "breadcrumb-demo",
type: "components:example",
registryDependencies: ["breadcrumb"],
component: React.lazy(() => import("@/registry/default/example/breadcrumb-demo")),
source: "",
files: ["registry/default/example/breadcrumb-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"breadcrumb-separator": {
name: "breadcrumb-separator",
type: "components:example",
registryDependencies: ["breadcrumb"],
component: React.lazy(() => import("@/registry/default/example/breadcrumb-separator")),
source: "",
files: ["registry/default/example/breadcrumb-separator.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"breadcrumb-dropdown": {
name: "breadcrumb-dropdown",
type: "components:example",
registryDependencies: ["breadcrumb","dropdown-menu"],
component: React.lazy(() => import("@/registry/default/example/breadcrumb-dropdown")),
source: "",
files: ["registry/default/example/breadcrumb-dropdown.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"breadcrumb-ellipsis": {
name: "breadcrumb-ellipsis",
type: "components:example",
registryDependencies: ["breadcrumb"],
component: React.lazy(() => import("@/registry/default/example/breadcrumb-ellipsis")),
source: "",
files: ["registry/default/example/breadcrumb-ellipsis.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"breadcrumb-link": {
name: "breadcrumb-link",
type: "components:example",
registryDependencies: ["breadcrumb"],
component: React.lazy(() => import("@/registry/default/example/breadcrumb-link")),
source: "",
files: ["registry/default/example/breadcrumb-link.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"breadcrumb-responsive": {
name: "breadcrumb-responsive",
type: "components:example",
registryDependencies: ["breadcrumb","button","drawer","dropdown-menu"],
component: React.lazy(() => import("@/registry/default/example/breadcrumb-responsive")),
source: "",
files: ["registry/default/example/breadcrumb-responsive.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"button-demo": {
name: "button-demo",
type: "components:example",
@@ -511,6 +588,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"button-split-dropdown": {
name: "button-split-dropdown",
type: "components:example",
registryDependencies: ["button","dropdown-menu"],
component: React.lazy(() => import("@/registry/default/example/button-split-dropdown")),
source: "",
files: ["registry/default/example/button-split-dropdown.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"calendar-demo": {
name: "calendar-demo",
type: "components:example",
@@ -533,6 +621,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"calendar-disabled-days-demo": {
name: "calendar-disabled-days-demo",
type: "components:example",
registryDependencies: ["calendar","form","popover"],
component: React.lazy(() => import("@/registry/default/example/calendar-disabled-days-demo")),
source: "",
files: ["registry/default/example/calendar-disabled-days-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"single-value-field-array-demo": {
name: "single-value-field-array-demo",
type: "components:example",
@@ -35,7 +35,7 @@ export function CodeBlockWrapper({
)}
>
<CollapsibleTrigger asChild>
<Button type="secondary" className="h-8 text-xs">
<Button variant="secondary" className="h-8 text-xs">
{isOpened ? 'Collapse' : expandButtonTitle}
</Button>
</CollapsibleTrigger>
@@ -94,10 +94,10 @@ export function CodeFragment({
)}
>
{showGrid && (
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,hsla(var(--foreground-default)/0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
)}
{showDottedGrid && (
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
)}
<div className="z-10 relative">{ComponentPreview}</div>
</div>
@@ -54,7 +54,7 @@ export function CommandMenu({ ...props }: DialogProps) {
return (
<>
<Button
type="outline"
variant="outline"
className={cn(
`lg:flex hidden relative h-8 w-full justify-start rounded-lg bg-background text-sm font-normal text-foreground-muted shadow-none sm:pr-12 md:w-40 lg:w-64 hover:border-foreground-muted hover:bg-surface-100 hover:text-foreground-lighter
`
@@ -69,7 +69,7 @@ export function CommandMenu({ ...props }: DialogProps) {
</kbd>
</Button>
<Button
type="text"
variant="text"
size="tiny"
className="px-1 group lg:hidden"
onClick={() => setOpen(true)}
@@ -78,7 +78,7 @@ export function CommandMenu({ ...props }: DialogProps) {
<CommandDialog open={open} onOpenChange={setOpen}>
<DialogTitle className="sr-only">Search Design System...</DialogTitle>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandList className="max-h-[300px]">
<CommandEmpty>No results found.</CommandEmpty>
{docsConfig.sidebarNav.map((group) => (
<CommandGroup key={group.title} heading={group.title}>
@@ -1,13 +1,7 @@
'use client'
import * as React from 'react'
import {
cn,
Tabs_Shadcn_ as Tabs,
TabsContent_Shadcn_ as TabsContent,
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
} from 'ui'
import { cn, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
import {
// CopyButton,
@@ -101,10 +101,10 @@ export function ComponentPreview({
)}
>
{showGrid && (
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,hsla(var(--foreground-default)/0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
)}
{showDottedGrid && (
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
)}
<div className="z-10 relative">{ComponentPreview}</div>
{/* <div className="preview-grid-background"></div> */}
@@ -125,7 +125,7 @@ export function ComponentPreview({
<Button
className="rounded-full"
onClick={() => setExpandState(!expand)}
type="default"
variant="default"
icon={<Expand className="text-foreground-lighter" />}
>
{expand ? 'Collapse code' : 'Expand code'}
@@ -146,10 +146,10 @@ export function ComponentPreview({
})}
>
{showGrid && (
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,hsla(var(--foreground-default)/0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
<div className="pointer-events-none absolute h-full w-full bg-[linear-gradient(to_right,oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-size-[24px_24px]"></div>
)}
{showDottedGrid && (
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(hsla(var(--foreground-default)/0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
<div className="z-0 pointer-events-none absolute h-full w-full bg-[radial-gradient(oklch(from_var(--foreground-default)_l_c_h_/_0.02)_1px,transparent_1px)] bg-size-[16px_16px] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
)}
<div className="z-10 relative">{ComponentPreview}</div>
</div>
@@ -49,7 +49,7 @@ export function CopyButton({
return (
<Button
size="small"
type="outline"
variant="outline"
className={cn(
'relative z-10 h-6 w-6 text-foreground-muted hover:bg-surface-100 hover:text-foreground p-0',
className
@@ -106,7 +106,7 @@ export function CopyWithClassNames({
<DropdownMenuTrigger asChild>
<Button
size="small"
type="outline"
variant="outline"
className={cn(
'relative z-10 h-6 w-6 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50',
className
@@ -16,10 +16,10 @@ import {
AlertTitle,
AspectRatio,
cn,
Tabs_Shadcn_ as Tabs,
TabsContent_Shadcn_ as TabsContent,
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
@@ -111,7 +111,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
<div className="flex items-center gap-2 justify-end">
{doc.links?.doc && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -128,7 +128,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
)}
{doc.links?.api && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -205,7 +205,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
<div className="flex items-center gap-2 justify-end">
{doc.links?.doc && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -222,7 +222,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
)}
{doc.links?.api && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -268,7 +268,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
<div className="flex items-center gap-2 justify-end">
{doc.links?.doc && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -279,7 +279,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
)}
{doc.links?.api && (
<Button
type="outline"
variant="outline"
className="rounded-full"
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
>
@@ -3,7 +3,7 @@
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
import SVG from 'react-inlinesvg'
import { RadioGroup, RadioGroupLargeItem, singleThemes, Theme } from 'ui'
import { RadioGroup, RadioGroupLargeItem, singleThemes } from 'ui'
const ThemeSettings = () => {
const [mounted, setMounted] = useState(false)
@@ -33,7 +33,7 @@ const ThemeSettings = () => {
value={theme}
className="flex flex-wrap gap-3"
>
{singleThemes.map((theme: Theme) => (
{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`} />
</RadioGroupLargeItem>
@@ -13,7 +13,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
singleThemes,
Theme,
} from 'ui'
const ThemeSwitcherDropdown = () => {
@@ -40,7 +39,7 @@ const ThemeSwitcherDropdown = () => {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="text"
variant="text"
size="tiny"
className="px-1 group"
icon={
@@ -59,7 +58,7 @@ const ThemeSwitcherDropdown = () => {
value={theme}
onValueChange={(themeValue) => setTheme(themeValue)}
>
{singleThemes.map((theme: Theme) => (
{singleThemes.map((theme) => (
<DropdownMenuRadioItem key={theme.value} value={theme.value}>
{theme.name}
</DropdownMenuRadioItem>
@@ -18,7 +18,7 @@ export const TopNavigation = () => {
<div className="max-w-site w-full flex flex-row items-center gap-6 mx-auto md:px-6 px-4 justify-between">
<div className="flex items-center gap-2">
<Button
type="text"
variant="text"
size="tiny"
className="px-1 md:hidden"
onClick={toggle}
@@ -48,7 +48,7 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
} from 'ui'
```
```tsx
@@ -111,7 +111,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from "ui"
...
@@ -139,7 +139,7 @@ We provide a `<BreadcrumbEllipsis />` component to show a collapsed state when t
<ComponentPreview name="breadcrumb-ellipsis" className="[&_.preview]:p-2" />
```tsx showLineNumbers {1,9}
import { BreadcrumbEllipsis } from "@/components/ui/breadcrumb"
import { BreadcrumbEllipsis } from "ui"
...
@@ -14,11 +14,9 @@ import { Button } from '@/components/ui/button'
```
```tsx
<Button type="outline">Button</Button>
<Button variant="outline">Button</Button>
```
_It is likely that the `type` prop will be changed to `variant` in the future._
## Link
You can use the `buttonVariants` helper to create a link that looks like a button.
@@ -47,9 +45,9 @@ Use the `size` prop to determine the size of the button.
<ComponentPreview name="button-sizes" />
### Types
### Variants
These are all the different `type` variations.
These are all the different `variant` variations.
#### Primary
@@ -61,15 +59,15 @@ Used for data insertion actions, confirming purchases, strong positive actions.
Used for opening dialogs, navigating to pages, and other non CRUD actions.
This `type` will probably be the most used button type.
It will probably be changed to be the default type in future.
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.
For destructive or side effect actions, use the `destructive` or `warning` type.
For destructive or side effect actions, use the `destructive` or `warning` variant.
<ComponentPreview name="button-secondary" />
@@ -83,7 +81,7 @@ Used for actions that might have a side effect, but not as serious as a destruct
Used for actions that will have a serious destructive side effect, like deleting data.
prop `type` will probably be changed to `destructive` in the future.
prop `variant` will probably be changed to `destructive` in the future.
<ComponentPreview name="button-destructive" />
@@ -97,7 +95,7 @@ Used for secondary actions, or actions that are not as important as the primary
Used for actions that are not as important as the primary action, or for actions that are not as important as the primary action.
prop `type` will probably be changed to `ghost` in the future.
prop `variant` will probably be changed to `ghost` in the future.
<ComponentPreview name="button-ghost" />
@@ -107,7 +105,7 @@ Used for actions that are not as important as the primary action, or for actions
<ComponentPreview name="button-link" />
### Only an Icon
### Only an icon
Displaying only an Icon in a button.
@@ -117,12 +115,28 @@ Displaying only an Icon in a button.
<ComponentPreview name="button-icon" />
### As Child
### As child
Supports slot behavior with `asChild` prop.
<ComponentPreview name="button-as-child" />
### Split with dropdown
Pair a button with a chevron `DropdownMenu` trigger when there are variations of the same action, or alternative ways to accomplish the same goal. The default or most likely option should be used on the exposed button.
When secondary actions are related but distinct—not alternatives to the primary action—display the primary action as a button and place the rest in an overflow menu instead. See [Table multiple actions](./table#multiple-actions).
<ComponentPreview name="button-split-dropdown" peekCode />
The shared middle border is the tricky part. Do **not** use `border-l-0` on the chevron button — that drops the divider on hover/focus. Instead:
- Primary: `rounded-r-none` and `hover:z-10` so its border stacks above the chevron on hover.
- Chevron trigger: `rounded-l-none`, `shrink-0`, `px-[4px] py-[5px]`, and `-ml-px` to overlap the adjacent border by one pixel.
- Chevron trigger only: `aria-label` describing the menu (the icon is decorative).
Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions, also use `flex w-full @lg:w-auto` with `flex-1 @lg:flex-none` on the primary when `layout="responsive"`.
## Accessibility
[Keyboard focus](../accessibility#focus-management) is automatically handled:
@@ -79,3 +79,9 @@ You can use the `<Calendar>` component to build a date picker. See the [Date Pic
### Form
<ComponentPreview name="calendar-form" />
### Calendar with disabled days
Shows a calendar with disabled days for selection. The current month starts mid week.
<ComponentPreview name="calendar-disabled-days-demo" />
@@ -10,7 +10,7 @@ source:
## Installation
The Date Picker is built using a composition of the `<Popover />` and the `<Calendar />` components.
The Date Picker is built using a composition of the `<Popover />`, `<Button />` and `<Calendar />` components.
See installation instructions for the [Popover](/docs/components/popover#installation) and the [Calendar](/docs/components/calendar#installation) components.
@@ -20,41 +20,42 @@ See installation instructions for the [Popover](/docs/components/popover#install
'use client'
import { format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { Button, Calendar } from 'ui'
import {
DatePicker,
DatePickerButton,
DatePickerContent,
DatePickerTrigger,
} from 'ui-patterns/DatePicker'
export function DatePickerDemo() {
const [date, setDate] = React.useState<Date>()
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
className={cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground'
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
<DatePicker>
<DatePickerTrigger asChild>
<DatePickerButton variant="outline" className="w-[280px]">
{date ? format(date, 'PPP') : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
</DatePickerButton>
</DatePickerTrigger>
<DatePickerContent>
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
</PopoverContent>
</Popover>
</DatePickerContent>
</DatePicker>
)
}
```
See the [React DayPicker](https://react-day-picker.js.org) documentation for more information.
## API Reference
- `<DatePicker />` accepts the same props as [the `<Popover />` component](./popover).
- `<DatePickerTrigger />` accepts the same props as [the `<PopoverTrigger />` component](./popover#api-reference).
- `<DatePickerContent />` accepts the same props as [the `<PopoverContent />` component](./popover#api-reference).
- `<DatePickerButton />` accepts the same props as [the `<Button />` component](./button) and an optional `isInvalid` prop to visually indicate an invalid date.
## Examples
### Date Picker
@@ -46,6 +46,18 @@ Style the color of the button based upon the _context_ of the Admonition, not it
Only ever use the `primary` (green) button `type` on a `default` Admonition. Even then—given that Admonition is an isolated callout—the `primary` button `type` should rarely be used.
### Split button with dropdown
When a callout offers alternative ways to accomplish the same goal, use a [split button](../components/button#split-with-dropdown) instead of crowding the callout with multiple buttons. For related but distinct actions, use an overflow menu. See [Table multiple actions](../components/table#multiple-actions).
<ComponentPreview
name="admonition-button-split"
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]"
peekCode
/>
In responsive Admonitions, wrap the pair in `flex w-full @lg:w-auto`, give the primary `flex-1 @lg:flex-none`, and keep the chevron `shrink-0`.
## Examples
<ComponentPreview
@@ -53,7 +65,7 @@ Only ever use the `primary` (green) button `type` on a `default` Admonition. Eve
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]"
/>
## Responsive
### Responsive
Resize your browser to see the button(s) change `layout` based on the Admonition’s width.
@@ -118,7 +118,7 @@ Use the `actions` prop to add any actions like a `<Button />`.
title="No functions found"
description="Create your first serverless function to get started."
actions={
<Button type="default" onClick={createAction}>
<Button variant="default" onClick={createAction}>
Create Function
</Button>
}
@@ -24,7 +24,7 @@ import { PageBreadcrumbs, PageBreadcrumbsActions } from 'ui-patterns/PageBreadcr
<PageBreadcrumbs
actions={
<PageBreadcrumbsActions>
<Button type="primary" size="tiny">
<Button variant="primary" size="tiny">
Create
</Button>
</PageBreadcrumbsActions>
@@ -44,7 +44,7 @@ Compound header for page context: optional icon, title, description, and aside a
<PageHeaderDescription>Manage email templates for your project.</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
<Button type="primary" size="tiny">
<Button variant="primary" size="tiny">
Create template
</Button>
</PageHeaderAside>
@@ -29,7 +29,7 @@ export default function TextConfirmDialogDemo() {
return (
<>
<Button type="danger" onClick={() => setVisible(true)}>
<Button variant="danger" onClick={() => setVisible(true)}>
Show Text Confirm Dialog
</Button>
@@ -116,7 +116,7 @@ const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
<Sheet open={visible} onOpenChange={handleOpenChange}>
...
<Button type="default" onClick={confirmOnClose}>
<Button variant="default" onClick={confirmOnClose}>
Cancel
</Button>
...
+6 -5
View File
@@ -5,20 +5,20 @@
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --turbopack --port 3003",
"dev:full": "concurrently \"pnpm dev\" \"pnpm content:dev\"",
"dev": "run-p --race dev:*",
"dev:next": "next dev --turbopack --port 3003",
"dev:content": "contentlayer2 dev",
"build": "pnpm run content:build && pnpm run build:registry && next build --turbopack",
"build:registry": "tsx ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache",
"start": "next start",
"lint": "eslint .",
"content:dev": "contentlayer2 dev",
"content:build": "contentlayer2 build",
"clean": "rimraf node_modules .next .turbo",
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@hookform/resolvers": "^3.1.1",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-table": "catalog:",
"contentlayer2": "0.4.6",
"common": "workspace:*",
"date-fns": "^2.30.0",
@@ -57,15 +57,16 @@
"@types/lodash.template": "4.5.0",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"concurrently": "^8.2.2",
"config": "workspace:*",
"mdast-util-toc": "^6.1.1",
"npm-run-all": "^4.1.5",
"postcss": "catalog:",
"rimraf": "^4.1.3",
"shiki": "^1.1.7",
"tailwindcss": "catalog:",
"tsconfig": "workspace:*",
"tsx": "catalog:",
"@typescript/native": "catalog:",
"typescript": "catalog:",
"unist-builder": "3.0.0"
}
@@ -1,7 +1,7 @@
'use client'
import { BarChart2, ExternalLink } from 'lucide-react'
import { Badge } from 'ui'
import { Badge, CriticalIcon } from 'ui'
import {
Chart,
ChartActions,
@@ -44,6 +44,35 @@ export default function ChartComposedStates() {
</ChartCard>
</Chart>
<Chart isErrored={true}>
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="This is a tooltip">Response Errors</ChartTitle>
<ChartActions actions={actions} />
</ChartHeader>
<ChartContent
isEmpty={true}
errorState={
<ChartEmptyState
icon={<CriticalIcon />}
title="Some error happened"
description="Error happened why trying to fetch data. Please try again later."
/>
}
emptyState={
<ChartEmptyState
icon={<BarChart2 size={20} />}
title="No data to show"
description="It may take up to 24 hours for data to refresh"
/>
}
loadingState={<ChartLoadingState />}
>
My chart here...
</ChartContent>
</ChartCard>
</Chart>
<Chart>
<ChartCard>
<ChartHeader>
@@ -0,0 +1,60 @@
import { ChevronDown } from 'lucide-react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
export default function AdmonitionButtonSplitDemo() {
return (
<Admonition
type="default"
layout="responsive"
title="Set up custom SMTP to edit templates"
description="Emails will be sent using the default templates. Set up custom SMTP to edit their subject and body."
actions={
<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"
>
Set up SMTP
</Button>
<DropdownMenu>
<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"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem>
<div className="flex flex-col gap-y-0.5">
<p className="block text-foreground">Upgrade to Pro</p>
<p className="block text-foreground-lighter text-balance">
Customize templates while using Supabase’s email service
</p>
</div>
</DropdownMenuItem>
<DropdownMenuItem>
<div className="flex flex-col gap-y-0.5">
<p className="block text-foreground">Configure Send Email hook</p>
<p className="block text-foreground-lighter text-balance">
Send auth emails through your own workflow
</p>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
}
/>
)
}
@@ -10,14 +10,14 @@ 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 type="default">Set up SMTP</Button>}
actions={<Button variant="default">Set up SMTP</Button>}
/>
<Admonition
type="destructive"
layout="horizontal"
title="Deleting this organization will also remove its projects"
description="Make sure you have made a backup of your projects if you want to keep your data."
actions={<Button type="danger">Delete organization</Button>}
actions={<Button variant="danger">Delete organization</Button>}
/>
</div>
)
@@ -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 type="default">OAuth Server Settings</Button>}
actions={<Button variant="default">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 type="default">Go to Compute and Disk</Button>}
actions={<Button variant="default">Go to Compute and Disk</Button>}
/>
)
}
@@ -36,7 +36,7 @@ export default function AlertDialogAsyncError() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="outline">Show Alert Dialog</Button>
<Button variant="outline">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -19,7 +19,7 @@ export default function AlertDialogAsync() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="outline">Show Alert Dialog</Button>
<Button variant="outline">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -14,7 +14,7 @@ export default function AlertDialogCloseOnly() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="outline">Show Alert Dialog</Button>
<Button variant="outline">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -15,7 +15,7 @@ export default function AlertDialogDemo() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="outline">Show Alert Dialog</Button>
<Button variant="outline">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -15,7 +15,7 @@ export default function AlertDialogDestructive() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="danger">Show Alert Dialog</Button>
<Button variant="danger">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -15,7 +15,7 @@ export default function AlertDialogWarning() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="warning">Show Alert Dialog</Button>
<Button variant="warning">Show Alert Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
@@ -0,0 +1,28 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from 'ui'
export default function BreadcrumbDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/components">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -0,0 +1,50 @@
import { ChevronDown, Slash } from 'lucide-react'
import Link from 'next/link'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
export default function BreadcrumbDropdownDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/">Home</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<Slash />
</BreadcrumbSeparator>
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5">
Components
<ChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>Documentation</DropdownMenuItem>
<DropdownMenuItem>Themes</DropdownMenuItem>
<DropdownMenuItem>GitHub</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
<BreadcrumbSeparator>
<Slash />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -0,0 +1,38 @@
import Link from 'next/link'
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from 'ui'
export default function BreadcrumbEllipsisDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/">Home</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/docs/components">Components</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -0,0 +1,33 @@
import Link from 'next/link'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from 'ui'
export default function BreadcrumbLinkDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/">Home</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/components">Components</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -0,0 +1,137 @@
'use client'
import Link from 'next/link'
import * as React from 'react'
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false)
React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches)
}
const result = matchMedia(query)
result.addEventListener('change', onChange)
setValue(result.matches)
return () => result.removeEventListener('change', onChange)
}, [query])
return value
}
const items = [
{ href: '#', label: 'Home' },
{ href: '#', label: 'Documentation' },
{ href: '#', label: 'Build Your Application' },
{ href: '#', label: 'Data Fetching' },
{ label: 'Caching and Revalidating' },
]
const ITEMS_TO_DISPLAY = 3
export default function BreadcrumbResponsiveDemo() {
const [open, setOpen] = React.useState(false)
const isDesktop = useMediaQuery('(min-width: 768px)')
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={items[0].href ?? '/'}>{items[0].label}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
{items.length > ITEMS_TO_DISPLAY ? (
<>
<BreadcrumbItem>
{isDesktop ? (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger className="flex items-center gap-1" aria-label="Toggle menu">
<BreadcrumbEllipsis className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{items.slice(1, -2).map((item, index) => (
<DropdownMenuItem key={index} asChild>
<Link href={item.href ? item.href : '#'}>{item.label}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger aria-label="Toggle Menu">
<BreadcrumbEllipsis className="h-4 w-4" />
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>Navigate to</DrawerTitle>
<DrawerDescription>Select a page to navigate to.</DrawerDescription>
</DrawerHeader>
<div className="grid gap-1 px-4">
{items.slice(1, -2).map((item, index) => (
<Link
key={index}
href={item.href ? item.href : '#'}
className="py-1 text-sm"
>
{item.label}
</Link>
))}
</div>
<DrawerFooter className="pt-4">
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
</BreadcrumbItem>
<BreadcrumbSeparator />
</>
) : null}
{items.slice(-ITEMS_TO_DISPLAY + 1).map((item, index) => (
<React.Fragment key={index}>
{index > 0 ? <BreadcrumbSeparator /> : null}
<BreadcrumbItem>
{item.href ? (
<BreadcrumbLink asChild className="max-w-20 truncate md:max-w-none">
<Link href={item.href}>{item.label}</Link>
</BreadcrumbLink>
) : (
<BreadcrumbPage className="max-w-20 truncate md:max-w-none">
{item.label}
</BreadcrumbPage>
)}
</BreadcrumbItem>
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -0,0 +1,38 @@
import { Slash } from 'lucide-react'
import Link from 'next/link'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from 'ui'
export default function BreadcrumbSeparatorDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/">Home</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<Slash />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/components">Components</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>
<Slash />
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonDemo() {
return (
<div className="flex gap-3">
<Button type="default">Button rest</Button>
<Button type="default" loading>
<Button variant="default">Button rest</Button>
<Button variant="default" loading>
Button loading
</Button>
<Button type="default" icon={<Mail />}>
<Button variant="default" icon={<Mail />}>
Button icon
</Button>
<Button type="default" iconRight={<Mail />}>
<Button variant="default" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonDemo() {
return (
<div className="flex gap-3">
<Button type="primary">Button rest</Button>
<Button type="primary" loading>
<Button variant="primary">Button rest</Button>
<Button variant="primary" loading>
Button loading
</Button>
<Button type="primary" icon={<Mail />}>
<Button variant="primary" icon={<Mail />}>
Button icon
</Button>
<Button type="primary" iconRight={<Mail />}>
<Button variant="primary" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonDestructive() {
return (
<div className="flex gap-3">
<Button type="danger">Button rest</Button>
<Button type="danger" loading>
<Button variant="danger">Button rest</Button>
<Button variant="danger" loading>
Button loading
</Button>
<Button type="danger" icon={<Mail />}>
<Button variant="danger" icon={<Mail />}>
Button icon
</Button>
<Button type="danger" iconRight={<Mail />}>
<Button variant="danger" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonGhost() {
return (
<div className="flex gap-3">
<Button type="text">Button rest</Button>
<Button type="text" loading>
<Button variant="text">Button rest</Button>
<Button variant="text" loading>
Button loading
</Button>
<Button type="text" icon={<Mail />}>
<Button variant="text" icon={<Mail />}>
Button icon
</Button>
<Button type="text" iconRight={<Mail />}>
<Button variant="text" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -2,5 +2,5 @@ import { ChevronRight } from 'lucide-react'
import { Button } from 'ui'
export default function ButtonIcon() {
return <Button type="outline" icon={<ChevronRight className="h-4 w-4" />}></Button>
return <Button variant="outline" icon={<ChevronRight className="h-4 w-4" />}></Button>
}
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonLink() {
return (
<div className="flex gap-3">
<Button type="link">Button rest</Button>
<Button type="link" loading>
<Button variant="link">Button rest</Button>
<Button variant="link" loading>
Button loading
</Button>
<Button type="link" icon={<Mail />}>
<Button variant="link" icon={<Mail />}>
Button icon
</Button>
<Button type="link" iconRight={<Mail />}>
<Button variant="link" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonOutline() {
return (
<div className="flex gap-3">
<Button type="outline">Button rest</Button>
<Button type="outline" loading>
<Button variant="outline">Button rest</Button>
<Button variant="outline" loading>
Button loading
</Button>
<Button type="outline" icon={<Mail />}>
<Button variant="outline" icon={<Mail />}>
Button icon
</Button>
<Button type="outline" iconRight={<Mail />}>
<Button variant="outline" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonSecondary() {
return (
<div className="flex gap-3">
<Button type="secondary">Button rest</Button>
<Button type="secondary" loading>
<Button variant="secondary">Button rest</Button>
<Button variant="secondary" loading>
Button loading
</Button>
<Button type="secondary" icon={<Mail />}>
<Button variant="secondary" icon={<Mail />}>
Button icon
</Button>
<Button type="secondary" iconRight={<Mail />}>
<Button variant="secondary" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -4,19 +4,19 @@ import { Button } from 'ui'
export default function ButtonDemo() {
return (
<div className="flex gap-3">
<Button type="default" size="tiny">
<Button variant="default" size="tiny">
Tiny Button
</Button>
<Button type="default" size="small">
<Button variant="default" size="small">
Small button
</Button>
<Button type="default" size="medium">
<Button variant="default" size="medium">
Medium button
</Button>
<Button type="default" size="large">
<Button variant="default" size="large">
Large button
</Button>
<Button type="default" size="xlarge">
<Button variant="default" size="xlarge">
Huge button
</Button>
</div>
@@ -0,0 +1,37 @@
import { ChevronDown } from 'lucide-react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
export default function ButtonSplitDropdownDemo() {
return (
<div className="flex w-fit">
<Button type="button" variant="default" className="rounded-r-none hover:z-10">
Primary action
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="default"
aria-label="More actions"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem>Secondary action</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:text-destructive">
Destructive action
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
@@ -4,14 +4,14 @@ import { Button } from 'ui'
export default function ButtonDemo() {
return (
<div className="flex gap-3">
<Button type="warning">Button rest</Button>
<Button type="warning" loading>
<Button variant="warning">Button rest</Button>
<Button variant="warning" loading>
Button loading
</Button>
<Button type="warning" icon={<Mail />}>
<Button variant="warning" icon={<Mail />}>
Button icon
</Button>
<Button type="warning" iconRight={<Mail />}>
<Button variant="warning" iconRight={<Mail />}>
Button iconRight
</Button>
</div>
@@ -0,0 +1,25 @@
'use client'
import * as React from 'react'
import { Calendar } from 'ui'
// The dates are specifically chosen to show a month which starts mid week.
const latestDate = new Date(2026, 6, 10)
const earliestDate = new Date(2026, 6, 7)
export default function CalendarMidWeekDemo() {
const [date, setDate] = React.useState<Date | undefined>(earliestDate)
return (
<Calendar
mode="single"
required={true}
selected={date}
onSelect={setDate}
defaultMonth={latestDate}
startMonth={earliestDate}
endMonth={latestDate}
disabled={[{ before: earliestDate }, { after: latestDate }]}
/>
)
}
@@ -57,7 +57,7 @@ export default function CalendarForm() {
<PopoverTrigger asChild>
<FormControl>
<Button
type={'default'}
variant="default"
size="small"
className={cn(
'w-[240px] justify-start',
@@ -84,7 +84,7 @@ export default function CalendarForm() {
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -57,7 +57,7 @@ export default function CalendarForm() {
<PopoverTrigger asChild>
<FormControl>
<Button
type={'outline'}
variant="outline"
className={cn(
'w-[240px] pl-3 text-left font-normal',
!field.value && 'text-muted-foreground'
@@ -83,7 +83,7 @@ export default function CalendarForm() {
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -112,7 +112,7 @@ export default function CheckboxReactHookFormMultiple() {
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -59,7 +59,7 @@ export default function CheckboxReactHookFormSingle() {
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -12,7 +12,7 @@ export default function CollapsibleDemo() {
<div className="flex items-center justify-between space-x-4 px-4">
<h4 className="text-sm font-semibold">@peduarte starred 3 repositories</h4>
<CollapsibleTrigger asChild>
<Button type="outline" size="medium" className="w-9 p-0">
<Button variant="outline" size="medium" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
@@ -48,7 +48,7 @@ export default function ComboboxDemo() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="default"
variant="default"
role="combobox"
size={'small'}
aria-expanded={open}
@@ -49,7 +49,7 @@ export default function ComboboxDropdownMenu() {
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
type="text"
variant="text"
size="tiny"
icon={<MoreHorizontal className="text-foreground-muted" />}
></Button>
@@ -73,7 +73,7 @@ export default function ComboboxForm() {
<PopoverTrigger asChild>
<FormControl>
<Button
type="default"
variant="default"
role="combobox"
className={cn(
'w-[200px] justify-between',
@@ -128,7 +128,7 @@ export default function ComboboxForm() {
</FormItem>
)}
/>
<Button htmlType="submit" type="secondary" size="small">
<Button type="submit" variant="secondary" size="small">
Submit
</Button>
</form>
@@ -69,7 +69,7 @@ export default function ComboboxPopover() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="default"
variant="default"
size="small"
className="w-[150px] justify-start rounded-full"
icon={
@@ -58,7 +58,7 @@ export default function ComboBoxResponsive() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="default"
variant="default"
size="small"
className="w-[150px] justify-start"
icon={!selectedStatus && <Plus className="text-foreground-muted" />}
@@ -76,7 +76,7 @@ export default function ComboBoxResponsive() {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button type="outline" className="w-[150px] justify-start">
<Button variant="outline" className="w-[150px] justify-start">
{selectedStatus ? <>{selectedStatus.label}</> : <>+ Set status</>}
</Button>
</DrawerTrigger>
@@ -24,7 +24,7 @@ export default function ConfirmationModalDemo() {
return (
<>
<Button type="default" onClick={() => setVisible(!visible)}>
<Button variant="default" onClick={() => setVisible(!visible)}>
Show Confirmation Modal
</Button>
<ConfirmationModal
@@ -7,15 +7,15 @@ export default function CopyButtonVerbs() {
<div className="flex flex-row gap-16">
<div className="flex flex-col gap-4">
<span className="text-xs text-foreground-muted">Bad Example</span>
<Button type="primary">Table creation</Button>
<Button type="primary">Save action</Button>
<Button type="danger">Bucket deletion</Button>
<Button variant="primary">Table creation</Button>
<Button variant="primary">Save action</Button>
<Button variant="danger">Bucket deletion</Button>
</div>
<div className="flex flex-col gap-4">
<span className="text-xs text-foreground-muted">Good Example</span>
<Button type="primary">Create table</Button>
<Button type="primary">Save changes</Button>
<Button type="danger">Delete bucket</Button>
<Button variant="primary">Create table</Button>
<Button variant="primary">Save changes</Button>
<Button variant="danger">Delete bucket</Button>
</div>
</div>
)
@@ -18,10 +18,10 @@ export default function CopyConfirmations() {
</div>
</div>
<div className="flex gap-2 justify-end">
<Button type="default" size="tiny">
<Button variant="default" size="tiny">
Cancel
</Button>
<Button type="danger" size="tiny">
<Button variant="danger" size="tiny">
Delete
</Button>
</div>
@@ -40,10 +40,10 @@ export default function CopyConfirmations() {
</div>
</div>
<div className="flex gap-2 justify-end">
<Button type="default" size="tiny">
<Button variant="default" size="tiny">
Cancel
</Button>
<Button type="danger" size="tiny">
<Button variant="danger" size="tiny">
Delete project
</Button>
</div>
@@ -2,7 +2,7 @@
import { Key } from 'lucide-react'
import { Button } from 'ui'
import { EmptyStatePresentational } from 'ui-patterns'
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
export default function CopyEmptyStates() {
return (
@@ -25,7 +25,7 @@ export default function CopyEmptyStates() {
title="No API keys"
description="Generate a key to connect your application."
>
<Button type="primary">Generate key</Button>
<Button variant="primary">Generate key</Button>
</EmptyStatePresentational>
</div>
</div>
@@ -8,13 +8,13 @@ export default function CopyLoadingStates() {
<div className="flex flex-col gap-4">
<span className="text-xs text-foreground-muted">Bad Example</span>
<div className="flex flex-col gap-4">
<Button type="primary" loading>
<Button variant="primary" loading>
Please wait...
</Button>
<Button type="primary" loading>
<Button variant="primary" loading>
Loading...
</Button>
<Button type="danger" loading>
<Button variant="danger" loading>
Processing...
</Button>
</div>
@@ -22,13 +22,13 @@ export default function CopyLoadingStates() {
<div className="flex flex-col gap-4">
<span className="text-xs text-foreground-muted">Good Example</span>
<div className="flex flex-col gap-4">
<Button type="primary" loading>
<Button variant="primary" loading>
Creating table...
</Button>
<Button type="primary" loading>
<Button variant="primary" loading>
Loading schema...
</Button>
<Button type="danger" loading>
<Button variant="danger" loading>
Deleting project...
</Button>
</div>
@@ -13,7 +13,7 @@ export default function CopyTooltips() {
<Tooltip>
<TooltipTrigger asChild>
<Button
type="text"
variant="text"
className="flex items-center px-1.5"
icon={<Info size={20} strokeWidth={1.5} />}
aria-label="More info"
@@ -33,7 +33,7 @@ export default function CopyTooltips() {
<Tooltip>
<TooltipTrigger asChild>
<Button
type="text"
variant="text"
className="flex items-center px-1.5"
icon={<Info size={20} strokeWidth={1.5} />}
aria-label="More info"
@@ -139,7 +139,7 @@ export const columns: ColumnDef<Payment>[] = [
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1.5" icon={<MoreVertical />} />
<Button variant="default" 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 type="default" className="ml-auto" size="tiny" iconRight={<ChevronDown />}>
<Button variant="default" className="ml-auto" size="tiny" iconRight={<ChevronDown />}>
Columns
</Button>
</DropdownMenuTrigger>
@@ -303,7 +303,7 @@ export default function DataTableDemo() {
</div>
<div className="space-x-2">
<Button
type="default"
variant="default"
size="tiny"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
@@ -311,7 +311,7 @@ export default function DataTableDemo() {
Previous
</Button>
<Button
type="default"
variant="default"
size="tiny"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
@@ -1,32 +1,28 @@
'use client'
import { format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-react'
import * as React from 'react'
import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from 'ui'
import { cn } from '@/lib/utils'
import { Calendar } from 'ui'
import {
DatePicker,
DatePickerButton,
DatePickerContent,
DatePickerTrigger,
} from 'ui-patterns/DatePicker'
export default function DatePickerDemo() {
const [date, setDate] = React.useState<Date>()
return (
<Popover>
<PopoverTrigger asChild>
<Button
type={'outline'}
className={cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground'
)}
icon={<CalendarIcon className="h-4 w-4" />}
>
<DatePicker>
<DatePickerTrigger asChild>
<DatePickerButton variant="outline" className="w-[280px]">
{date ? format(date, 'PPP') : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
</DatePickerButton>
</DatePickerTrigger>
<DatePickerContent>
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
</PopoverContent>
</Popover>
</DatePickerContent>
</DatePicker>
)
}
@@ -2,27 +2,26 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { format } from 'date-fns'
import { CalendarIcon } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Calendar,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Popover,
PopoverContent,
PopoverTrigger,
} from 'ui'
import {
DatePicker,
DatePickerButton,
DatePickerContent,
DatePickerTrigger,
} from 'ui-patterns/DatePicker'
import { z } from 'zod'
import { cn } from '@/lib/utils'
const FormSchema = z.object({
dob: z.date({
required_error: 'A date of birth is required.',
@@ -50,25 +49,16 @@ export default function DatePickerForm() {
<FormField
control={form.control}
name="dob"
render={({ field }) => (
render={({ field, fieldState }) => (
<FormItem className="flex flex-col">
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
type={'outline'}
className={cn(
'w-[240px] pl-3 text-left font-normal',
!field.value && 'text-muted-foreground'
)}
icon={<CalendarIcon className="h-4 w-4 opacity-50" />}
>
{field.value ? format(field.value, 'PPP') : <span>Pick a date</span>}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<DatePicker>
<DatePickerTrigger asChild>
<DatePickerButton isInvalid={fieldState.invalid}>
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
</DatePickerButton>
</DatePickerTrigger>
<DatePickerContent>
<Calendar
mode="single"
selected={field.value}
@@ -76,14 +66,14 @@ export default function DatePickerForm() {
disabled={(date) => date > new Date() || date < new Date('1900-01-01')}
initialFocus
/>
</PopoverContent>
</Popover>
</DatePickerContent>
</DatePicker>
<FormDescription>Your date of birth is used to calculate your age.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button htmlType="submit">Submit</Button>
<Button type="submit">Submit</Button>
</form>
</Form>
)
@@ -1,41 +1,26 @@
'use client'
import { addDays, format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-react'
import * as React from 'react'
import { Calendar, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'ui'
import {
Button,
Calendar,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from 'ui'
import { cn } from '@/lib/utils'
DatePicker,
DatePickerButton,
DatePickerContent,
DatePickerTrigger,
} from 'ui-patterns/DatePicker'
export default function DatePickerWithPresets() {
const [date, setDate] = React.useState<Date>()
return (
<Popover>
<PopoverTrigger asChild>
<Button
type={'outline'}
className={cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground'
)}
icon={<CalendarIcon className="h-4 w-4" />}
>
<DatePicker>
<DatePickerTrigger asChild>
<DatePickerButton variant="outline" className="w-[280px]">
{date ? format(date, 'PPP') : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-auto flex-col space-y-2 p-2">
</DatePickerButton>
</DatePickerTrigger>
<DatePickerContent className="flex w-auto flex-col space-y-2 p-2">
<Select onValueChange={(value) => setDate(addDays(new Date(), parseInt(value)))}>
<SelectTrigger>
<SelectValue placeholder="Select" />
@@ -50,7 +35,7 @@ export default function DatePickerWithPresets() {
<div className="rounded-md border">
<Calendar mode="single" selected={date} onSelect={setDate} />
</div>
</PopoverContent>
</Popover>
</DatePickerContent>
</DatePicker>
)
}
@@ -1,10 +1,15 @@
'use client'
import { addDays, format } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-react'
import * as React from 'react'
import { DateRange } from 'react-day-picker'
import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from 'ui'
import { Calendar } from 'ui'
import {
DatePicker,
DatePickerButton,
DatePickerContent,
DatePickerTrigger,
} from 'ui-patterns/DatePicker'
import { cn } from '@/lib/utils'
@@ -16,17 +21,9 @@ export default function DatePickerWithRange({ className }: React.HTMLAttributes<
return (
<div className={cn('grid gap-2', className)}>
<Popover>
<PopoverTrigger asChild>
<Button
id="date"
type={'outline'}
className={cn(
'w-[300px] justify-start text-left font-normal',
!date && 'text-muted-foreground'
)}
icon={<CalendarIcon className="h-4 w-4" />}
>
<DatePicker>
<DatePickerTrigger asChild>
<DatePickerButton variant="outline" className="w-[300px]">
{date?.from ? (
date.to ? (
<>
@@ -38,9 +35,9 @@ export default function DatePickerWithRange({ className }: React.HTMLAttributes<
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
</DatePickerButton>
</DatePickerTrigger>
<DatePickerContent>
<Calendar
initialFocus
mode="range"
@@ -49,8 +46,8 @@ export default function DatePickerWithRange({ className }: React.HTMLAttributes<
onSelect={setDate}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
</DatePickerContent>
</DatePicker>
</div>
)
}
@@ -17,7 +17,7 @@ export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button type="default">Edit profile</Button>
<Button variant="default">Edit profile</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]" centered={false}>
<DialogHeader>
@@ -40,7 +40,7 @@ export default function DialogDemo() {
</div>
</DialogSection>
<DialogFooter>
<Button htmlType="submit">Save changes</Button>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -19,7 +19,7 @@ export default function DialogCloseButton() {
return (
<Dialog>
<DialogTrigger asChild>
<Button type="default">Share</Button>
<Button variant="default">Share</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
@@ -35,7 +35,7 @@ export default function DialogCloseButton() {
</Label>
<Input id="link" defaultValue="https://ui.shadcn.com/docs/installation" readOnly />
</div>
<Button htmlType="submit" size="small" type="secondary" className="px-3">
<Button type="submit" size="small" variant="secondary" className="px-3">
<span className="sr-only">Copy</span>
<Copy className="h-4 w-4" />
</Button>
@@ -43,7 +43,7 @@ export default function DialogCloseButton() {
</DialogSection>
<DialogFooter className="sm:justify-start">
<DialogClose asChild>
<Button type="default" htmlType="button">
<Button variant="default" type="button">
Custom Close Button
</Button>
</DialogClose>
@@ -17,7 +17,7 @@ export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button type="default">Show Dialog</Button>
<Button variant="default">Show Dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
Loaded 100 of 3766 files, more files were not shown because too many files have changed in this diff. Show more