mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
Merge branch 'master' into chore/function-page-design
This commit is contained in:
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).
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -112,7 +112,8 @@ next-env.d.ts
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
.vscode
|
||||
.vscode/*
|
||||
!.vscode/content-listing.code-snippets
|
||||
.idea
|
||||
.vercel
|
||||
|
||||
|
||||
@@ -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
@@ -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}\" />"]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 +0,0 @@
|
||||
apps/docs/public/.well-known/security.txt
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
...
|
||||
|
||||
@@ -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
Reference in new issue
Block a user