Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34a29ad337 | ||
|
|
b04b27c215 | ||
|
|
6a25a3d137 | ||
|
|
32d9dc6acb | ||
|
|
fc4f234cc5 | ||
|
|
821f342476 | ||
|
|
4634aac2d1 | ||
|
|
7953ae6e78 | ||
|
|
d14550cda1 | ||
|
|
632adaa63f | ||
|
|
cc80858952 | ||
|
|
b8dbf15254 | ||
|
|
f45b2bed90 | ||
|
|
b13c233679 | ||
|
|
37f28dfcfd | ||
|
|
6c6e76fca3 | ||
|
|
26f5172f88 | ||
|
|
8fe74c1d3b | ||
|
|
13e3d21ce2 | ||
|
|
a0cfbcdab1 | ||
|
|
c3369059af | ||
|
|
53ce80c9ff | ||
|
|
1e04e91dd8 | ||
|
|
5fbe9c9b56 | ||
|
|
fd3a217cbe | ||
|
|
ff6efec4c5 | ||
|
|
738d2e2256 | ||
|
|
2e3e1493f5 | ||
|
|
fd2fd8298d | ||
|
|
df7cd99060 | ||
|
|
c273cea067 | ||
|
|
1f969e6cac | ||
|
|
c1132cbe19 | ||
|
|
b28ad88b22 | ||
|
|
b871d62de6 | ||
|
|
4d83b8d673 | ||
|
|
2820cff988 | ||
|
|
54b7ea3bd5 | ||
|
|
57bcfcd9e1 | ||
|
|
1317a73e9a | ||
|
|
039d2f3dd9 | ||
|
|
54a0b5b966 | ||
|
|
1639ba40f3 | ||
|
|
26a96bc824 | ||
|
|
4fc5c96e63 | ||
|
|
3859983ae5 | ||
|
|
aa1779170a | ||
|
|
bbcf9d76d9 | ||
|
|
55eb2a6c89 | ||
|
|
a3f3ffd672 | ||
|
|
1a142e68a9 | ||
|
|
46e7b2aa6b | ||
|
|
1152bb40f7 | ||
|
|
538daea0f1 | ||
|
|
39f1295438 | ||
|
|
3a9bd0dba3 | ||
|
|
940f8892f3 | ||
|
|
ac727ad1e3 |
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: reui
|
||||
description: Use the ReUI registry from your AI agent - find, install, and correctly use ReUI components (the 20 free building blocks like data-grid, kanban, filters), their free examples, premium blocks, and Motion Icons. Applies in any project using ReUI, the @reui registry, REUI_LICENSE_KEY, or any shadcn project where the user asks for premium blocks, data grids, kanban boards, dashboards, or full pages. Pairs with the free ReUI MCP server for live, scored registry search and inline component APIs.
|
||||
user-invocable: false
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
---
|
||||
|
||||
> **ReUI skill version `668fb463eb`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI for Agents
|
||||
|
||||
ReUI is a shadcn-compatible registry. It ships four things you **reuse** - never redesign:
|
||||
|
||||
- **components** - the 20 ReUI building blocks with real APIs: `data-grid`, `kanban`, `filters`, `date-selector`, `tree`, `stepper`, ... (free)
|
||||
- **examples** - free `c-*` single-pattern use-cases of a component (`c-kanban-1`); install one and read it to see exact composition
|
||||
- **blocks** - premium full-page sections that compose components (`data-grid-2`, `pricing-page-1`); Pro or Ultimate license at install
|
||||
- **icons** - Motion Icons in 4 styles, static + hover-animated variants; Ultimate license at install
|
||||
|
||||
The skill is free and this MCP is free to use; it just needs a ReUI account. On first use your agent opens a browser "Sign in with ReUI" prompt (a free account is created if you don't have one). Free covers components and examples with a daily request allowance; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the limit (see [rules/registry.md](./rules/registry.md)). The same account and skill work in every agent and service the MCP connects to - this skill is agent-agnostic.
|
||||
|
||||
Skill + MCP are a team: this skill is the workflow (how to find, install, read the API, and adapt by reuse); the MCP is the live data and the hands (search, get_component, install commands). Your job: find the right item, install it with the shadcn CLI, read its real API, and **adapt by reuse** - wire real data and theme it; do not hand-roll or restyle what ReUI already provides. This skill **layers on the shadcn skill**: follow that for generic rules (spacing, `cn()`, semantic colors, forms); follow this for everything ReUI-specific.
|
||||
|
||||
## The core loop (MCP-native)
|
||||
|
||||
1. **Find** - call the ReUI MCP `search` tool with the user's intent. It returns a ranked, scored list across components/examples/blocks/icons, each with an `install` command, `previewUrl`, `docsUrl`, and `componentsUsed`. Pass hints (`type`, `component`, `category`, `features`, `free`) when you can infer them.
|
||||
2. **Install** - run the returned command non-interactively (`npx shadcn@latest add @reui/<name> --yes`). The CLI resolves deps, aliases, and the base/style from `components.json`. See [cli.md](./rules/cli.md).
|
||||
3. **Read the API (on your base)** - first note your base from `components.json` -> `style` (`base-nova` -> Base UI, `radix-nova` -> Radix UI). For each component an item uses, call `get_component(name)` and read its **inline `api`** (no web fetch); then `get_examples(name)` to install a worked example and copy its composition - the installed files are already in your base. Whenever you work with a component's API, also **share its `docsUrl`** (the primitive's API documentation page) with the user so they have the full reference. See [components.md](./rules/components.md).
|
||||
4. **Adapt (reuse-first)** - swap demo data for real data, fix icon imports, align tokens. Do not redesign. See [adapting.md](./rules/adapting.md).
|
||||
|
||||
**Always show the preview.** Every item a tool returns carries a `previewUrl` (a live preview page). Whenever you list, recommend, or present ReUI items to the user - blocks, components, examples, or icons, whether from `search`, `search_icons`, `list_components`, `compose_page`, or any getter - include each item's `previewUrl` so they can SEE it before installing. Blocks and examples open an individual live preview; icons and components link to their live category/component page. Never present an item without its preview link.
|
||||
|
||||
If the ReUI MCP is not configured, fall back to `npx shadcn@latest search @reui -q "..."` then `add` - but the MCP gives scored matches + inline APIs; prefer it.
|
||||
|
||||
## Commands
|
||||
|
||||
Run ReUI as explicit slash commands (via the ReUI MCP) **or** just ask in plain language - both run the same workflow.
|
||||
|
||||
| Command | Invoke | Does |
|
||||
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **build** | `/mcp__reui__build <what>` | Compose a page/section/feature from ReUI: plan → install → read API → adapt → craft → audit. |
|
||||
| **add** | `/mcp__reui__add <item>` | Find & install one component/example/block/icon and wire it in. |
|
||||
| **fix** | `/mcp__reui__fix [target]` | Diagnose & fix ReUI usage: wrong/undocumented props, base/radix mismatch, missing states, a11y/scroll. |
|
||||
| **improve** | `/mcp__reui__improve [target]` | Refine + extend existing ReUI UI to a production-exceptional bar (hierarchy, density, states, responsive, motion). |
|
||||
|
||||
Invocation differs slightly per agent (`/mcp__reui__build` in Claude Code/Cursor/Windsurf, `/mcp.reui.build` in VS Code). No command surface? Just describe what you want - this skill drives the identical loop.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
| Need | Reach for |
|
||||
| -------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| A full page or section (dashboard, billing, auth, pricing, settings) | `compose_page` first (plans sections + best blocks), then ReUI **blocks** |
|
||||
| A data table with sorting/filtering/pagination/virtualization | the **data-grid** component (never hand-roll a `<table>`) |
|
||||
| A drag-and-drop board | the **kanban** component |
|
||||
| Advanced column filtering, date range, tree, stepper, ... | the matching ReUI **component** |
|
||||
| A single generic control already in shadcn (Button, Dialog, Select) | plain **shadcn** |
|
||||
|
||||
## Detailed references
|
||||
|
||||
- [rules/registry.md](./rules/registry.md) - the four types, the @reui registry, base/radix, free vs premium + license
|
||||
- [rules/workflow.md](./rules/workflow.md) - the find -> install -> read-API -> adapt loop (most important)
|
||||
- [rules/components.md](./rules/components.md) - the 20 components, the data-grid contract, base vs radix
|
||||
- [rules/adapting.md](./rules/adapting.md) - reuse-first: preserve the design (no over-customizing), reuse examples + a block's own elements, real data, don't invent APIs
|
||||
- [rules/craft.md](./rules/craft.md) - make it exceptional: point of view, hierarchy, density, states, responsive, motion, the bar
|
||||
- [rules/quality.md](./rules/quality.md) - security, accessibility, and scroll gates (the done gate)
|
||||
- [rules/styling.md](./rules/styling.md) - ReUI extended tokens, theme adaptation, density
|
||||
- [rules/icons.md](./rules/icons.md) - portable icons, swapping imports, Motion Icons (static + animated)
|
||||
- [tools.md](./tools.md) - the ReUI MCP: golden path, the 19 tools, token rules, result shapes, errors
|
||||
@@ -0,0 +1,43 @@
|
||||
# Adapting installed ReUI code (reuse-first, no AI slop)
|
||||
|
||||
ReUI items ship production-quality. Your job is to **adapt by reuse** - wire real data and fit the app - not to redesign or hand-roll. The output should look like ReUI built it for this product.
|
||||
|
||||
## Preserve the design - don't over-customize
|
||||
|
||||
The design IS the product. A ReUI block/component encodes senior-designer decisions: spacing, hierarchy, density, color treatment, and component choices. The fastest way to turn a premium block back into generic AI slop is to "improve" its look - so don't.
|
||||
|
||||
- Change **data, copy, and props**; keep the **structure and styling** it ships with. Make the **smallest** change that wires the real data. If your diff touches `className` / JSX structure more than data / props, you are over-customizing - stop and reuse.
|
||||
- Don't swap ReUI components for hand-rolled ones, restructure the layout, re-skin spacing / radius / colors, or add decorative chrome. Let the installed components carry the default spacing, radius, sizing, icon rhythm, density, and state styling; add custom Tailwind only when a component genuinely lacks a contract you need.
|
||||
- Want a different look? `search` for a block whose design already fits and reuse that - don't restyle this one into a new design.
|
||||
|
||||
## Reuse the parts: examples and the block's own elements
|
||||
|
||||
- **Examples are building parts.** A free `c-*` example is a correct, single-pattern composition you can reuse. Before composing from scratch, `get_examples(component)`, install the closest one, and reuse its wiring - assemble UI from examples instead of hand-rolling what an example already shows.
|
||||
- **Reuse a block's own elements.** Need more rows, cards, items, or sections than ship by default? Repeat the block's **existing** element by mapping real data through the same markup - never invent parallel markup that drifts from its design. Need a variant (empty / loading / expanded)? Derive it from an element the block already has.
|
||||
|
||||
## Don't invent (read, don't guess)
|
||||
|
||||
- Never write a prop, variant value, import path, or `@reui/...` name you didn't read in a component's inline `api`, an installed example, or a `search` result. If you didn't see it, treat it as nonexistent - call `get_component` / `get_examples` / `search` first, or run the MCP `validate_usage` tool to check planned names + props against the docs before writing code.
|
||||
- If a getter returns `found: false` or `search` returns nothing, say so and fall back (plain shadcn, or ask) - never fabricate an install command or an API.
|
||||
|
||||
## What to change vs leave alone
|
||||
|
||||
- **Change:** the item's own data, copy, props, and layout to fit the app.
|
||||
- **Leave alone:** installed component files, hooks, and the shared theme - do not edit vendored ReUI internals; change behavior through props and the documented API.
|
||||
- Blocks are **portable React** - no `next/link`, `next/image`, or other framework-runtime imports inside them. Keep them portable.
|
||||
|
||||
## Demo data -> real data
|
||||
|
||||
- Replace every placeholder with the user's real data. Model it as **typed data structures** and **map over arrays** - never duplicate JSX per row/card. Keep small block-specific formatters next to the data.
|
||||
- Wire the real source (columns, fields, fetch). For `data-grid`, implement the server fetch contract if the user needs server-side data.
|
||||
- **Type from the component API, derive during render.** Type domain state through the component's own types - e.g. map status to `BadgeProps["variant"]` via a typed `Record<Status, …>` - instead of stringly-typed values. Compute view state during render; don't mirror derived data into `useState`/`useEffect`.
|
||||
- **Adapt on the right base.** Use the API for the project's base (Base UI vs Radix - see [components.md](./components.md)); the installed files are already base-correct, so reuse their shape rather than translating from memory.
|
||||
|
||||
## Believable content (no AI tells)
|
||||
|
||||
- Use realistic labels, counts, timestamps, and statuses that map to a real workflow.
|
||||
- No decorative buttons, fake tabs, meaningless toggles, equal-weight card walls, empty gradients, ornamental icons, or generic SaaS filler. Every element should do something.
|
||||
|
||||
## Operational surfaces (settings / profile / admin)
|
||||
|
||||
Pick ONE archetype and keep the family consistent: a vertical rail (3-6 sections), horizontal tabs (5-8), or a frame/stack. Prefer `frame` for tool-like surfaces, a card for profile-like ones. Don't mix archetypes in one surface.
|
||||
@@ -0,0 +1,60 @@
|
||||
# CLI: registry setup, license, non-interactive install
|
||||
|
||||
## Registry setup (one-time, per project)
|
||||
|
||||
Free items (the 20 components and all `c-*` examples) need only the plain string registry in `components.json`:
|
||||
|
||||
```json
|
||||
{ "registries": { "@reui": "https://reui.io/r/{style}/{name}.json" } }
|
||||
```
|
||||
|
||||
Premium items (blocks; Motion Icons and templates) require a ReUI license at install:
|
||||
|
||||
1. Add the key to `.env.local`:
|
||||
|
||||
```bash
|
||||
REUI_LICENSE_KEY=your-license-key
|
||||
```
|
||||
|
||||
2. Switch `components.json` to the authenticated object form:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${REUI_LICENSE_KEY}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`, but an MCP client config never expands variables, so a ReUI MCP server config must carry the raw token instead (for example `reui_pat_your_token_here`).
|
||||
|
||||
The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry
|
||||
|
||||
## Installing
|
||||
|
||||
Use the project's package runner (check `packageManager`):
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/<name> --yes # npm
|
||||
pnpm dlx shadcn@latest add @reui/<name> --yes # pnpm
|
||||
bunx --bun shadcn@latest add @reui/<name> --yes # bun
|
||||
```
|
||||
|
||||
`--yes` skips confirmation prompts. The CLI auto-detects the package manager from the lockfile (there is no `--package-manager` flag). It also resolves the correct base+style variant from `components.json`, so do not pass a style.
|
||||
|
||||
## Handling prompts and conflicts
|
||||
|
||||
- **Always pass `--yes`** so the CLI does not block on confirmation prompts.
|
||||
- **Do NOT pass `--overwrite` by default.** If the CLI reports an existing file, read the output and resolve deliberately: install under a different name, adjust the path, or ask the user. Only use `--overwrite` when the user explicitly wants to replace a file.
|
||||
- **Preview first when touching an existing project**: `npx shadcn@latest add @reui/<name> --dry-run` shows what would change; `--diff <file>` shows a specific file's diff. Use these before overwriting.
|
||||
- Run from the **project root** so `components.json` and `.env.local` are found.
|
||||
|
||||
## Free vs premium boundary
|
||||
|
||||
- Public, no key: `c-*` examples and the 20 components (`@reui/data-grid`, `@reui/badge`, ...) that those examples depend on.
|
||||
- Key required at install: blocks (`@reui/<category>-N`) need a Pro or Ultimate license; Motion Icons (`@reui/icons/...`) and templates need Ultimate.
|
||||
|
||||
If an install 401/403s, the license key is missing, invalid, or the plan does not cover that resource (blocks: Pro or higher; icons and templates: Ultimate). Point the user to https://reui.io/account (their key) or https://reui.io/pricing (upgrade).
|
||||
@@ -0,0 +1,370 @@
|
||||
# ReUI components
|
||||
|
||||
The 20 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these.
|
||||
|
||||
**Rule one: never guess a component's API. Read it first.** Call **`get_component(name)`** for its inline `api` (props + usage, no web fetch), and **share the result's `docsUrl`** (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the `/llms.txt` index is a further fallback). Then call **`get_examples(name)`** to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline `api` is the full reference. No single block fits? Compose: search the components you need, read each `get_component`, install a `get_examples` example per component, and adapt.
|
||||
|
||||
## data-grid (the flagship - read its API every time)
|
||||
|
||||
`data-grid` wraps TanStack Table v9. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
|
||||
|
||||
- Build a TanStack table instance with `useTable({ features: dataGridFeatures, ... })` (columns, data). `dataGridFeatures` is exported by the primitive and already bundles sorting, filtering, pagination, row selection, expanding, pinning, resizing and faceting, so there are no per-table row models to wire.
|
||||
- Pass that instance to `<DataGrid table={table} recordCount={total}>`.
|
||||
- Compose the body with `DataGridTable` inside `DataGrid`, and enable features through `tableLayout` (e.g. `{ headerSticky: true, columnsResizable: true }`), not ad-hoc classes.
|
||||
- Server-side data uses the documented fetch shape (`recordCount` is the total for pagination).
|
||||
|
||||
```tsx
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
data,
|
||||
columns,
|
||||
})
|
||||
|
||||
<DataGrid table={table} recordCount={data.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
Common mistakes:
|
||||
|
||||
- **Incorrect:** `<DataGrid data={rows} columns={cols} />` - these props do not exist. **Correct:** build a `useTable({ features: dataGridFeatures, ... })` instance and pass `table={table}` + `recordCount`.
|
||||
- **Incorrect:** a raw `<table>` / hand-rolled pagination. **Correct:** use `data-grid`; read its API for sticky header, pagination, virtualization, row selection.
|
||||
- **Incorrect:** styling rows/cells with arbitrary classes. **Correct:** drive layout via `tableLayout` and the primitive's `DataGridColumnMeta` (e.g. `cellClassName`, `headerTitle`), set through the bundle's `columnMeta` slot.
|
||||
|
||||
## event-calendar
|
||||
|
||||
**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EventCalendar defaultEvents={events} defaultView="month" className="h-[560px]">
|
||||
<EventCalendarNav />
|
||||
<EventCalendarContent />
|
||||
</EventCalendar>
|
||||
```
|
||||
|
||||
**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column.
|
||||
|
||||
## gantt
|
||||
|
||||
**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Gantt defaultEvents={bars} resources={tasks} defaultScale="month" className="h-[480px]">
|
||||
<GanttNav />
|
||||
<GanttView />
|
||||
</Gantt>
|
||||
```
|
||||
|
||||
**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height.
|
||||
|
||||
## kanban
|
||||
|
||||
**Required:** `value` (`Record<string, T[]>`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
|
||||
<KanbanBoard>
|
||||
{Object.entries(cols).map(([id, items]) => (
|
||||
<KanbanColumn key={id} value={id}>
|
||||
<KanbanColumnHandle><h3>{id}</h3></KanbanColumnHandle>
|
||||
<KanbanColumnContent value={id}>
|
||||
{items.map((i) => (
|
||||
<KanbanItem key={i.id} value={i.id}>
|
||||
<KanbanItemHandle>{i.title}</KanbanItemHandle>
|
||||
</KanbanItem>
|
||||
))}
|
||||
</KanbanColumnContent>
|
||||
</KanbanColumn>
|
||||
))}
|
||||
</KanbanBoard>
|
||||
<KanbanOverlay><div className="bg-muted size-full rounded-md" /></KanbanOverlay>
|
||||
</Kanban>
|
||||
```
|
||||
|
||||
**Gotcha:** state is `Record<columnId, T[]>`. Each `KanbanColumnContent value` must match its parent `KanbanColumn value`. Omit `KanbanOverlay` and the drag preview silently breaks.
|
||||
|
||||
## sortable
|
||||
|
||||
**Required:** `value` (`T[]`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Sortable value={items} onValueChange={setItems} getItemValue={(i) => i.id}>
|
||||
{items.map((i) => (
|
||||
<SortableItem key={i.id} value={i.id}>
|
||||
<SortableItemHandle><GripVertical /></SortableItemHandle>
|
||||
{i.label}
|
||||
</SortableItem>
|
||||
))}
|
||||
</Sortable>
|
||||
```
|
||||
|
||||
**Gotcha:** a flat 1D reorder list (not columns - that is `kanban`). `getItemValue` must return a stable, unique string. Pass `layout="grid"` or `layout="nested"` for non-list layouts.
|
||||
|
||||
## filters
|
||||
|
||||
**Required:** `filters` (`Filter[]`), `fields` (`FilterFieldConfig[]`), `onChange`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const [filters, setFilters] = useState<Filter[]>([
|
||||
createFilter("priority", "is_any_of", ["low"]),
|
||||
])
|
||||
const fields: FilterFieldConfig[] = [
|
||||
{ key: "priority", label: "Priority", type: "multiselect",
|
||||
options: [{ value: "low", label: "Low" }, { value: "high", label: "High" }] },
|
||||
]
|
||||
|
||||
<Filters filters={filters} fields={fields} onChange={setFilters} />
|
||||
```
|
||||
|
||||
**Gotcha:** always build initial filters with `createFilter(field, operator, values)` - it generates the required `id`. Never hand-construct a `Filter` object. Pairs naturally with `data-grid`.
|
||||
|
||||
## date-selector
|
||||
|
||||
**Required:** none, but wire `onChange` to capture the value.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const [value, setValue] = useState<DateSelectorValue | undefined>()
|
||||
|
||||
<DateSelector value={value} onChange={setValue} label="Due date" />
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a structured `DateSelectorValue` (period / operator / start+end dates), NOT a `Date` - never pass a raw `Date`. Use `allowRange={false}` to lock single-date picking. Read `get_component("date-selector")` for the value shape.
|
||||
|
||||
## tree
|
||||
|
||||
**Required:** `tree` (a `@headless-tree/core` instance you construct)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Tree tree={tree}>
|
||||
{tree.getItems().map((item) => (
|
||||
<TreeItem key={item.getId()} item={item}>
|
||||
<TreeItemLabel />
|
||||
</TreeItem>
|
||||
))}
|
||||
</Tree>
|
||||
```
|
||||
|
||||
**Gotcha:** `Tree` is a styled shell - it takes a headless-tree instance via `tree`, NOT `data`/`items` props. Build the instance with `@headless-tree/react`. External API: https://headless-tree.lukasbach.com/
|
||||
|
||||
## stepper
|
||||
|
||||
**Required:** `StepperItem step` (number), `StepperContent value` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Stepper defaultValue={1}>
|
||||
<StepperNav>
|
||||
<StepperItem step={1}>
|
||||
<StepperTrigger><StepperIndicator>1</StepperIndicator></StepperTrigger>
|
||||
<StepperSeparator />
|
||||
</StepperItem>
|
||||
<StepperItem step={2}>
|
||||
<StepperTrigger><StepperIndicator>2</StepperIndicator></StepperTrigger>
|
||||
</StepperItem>
|
||||
</StepperNav>
|
||||
<StepperPanel>
|
||||
<StepperContent value={1}>Step 1 content</StepperContent>
|
||||
<StepperContent value={2}>Step 2 content</StepperContent>
|
||||
</StepperPanel>
|
||||
</Stepper>
|
||||
```
|
||||
|
||||
**Gotcha:** steps are 1-indexed. Without `StepperPanel` + `StepperContent` you render the nav trail but no body. Put `StepperSeparator` in every `StepperItem` except the last.
|
||||
|
||||
## timeline
|
||||
|
||||
**Required:** `TimelineItem step` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Timeline>
|
||||
<TimelineItem step={1}>
|
||||
<TimelineHeader>
|
||||
<TimelineDate>March 2024</TimelineDate>
|
||||
<TimelineTitle>Project initialized</TimelineTitle>
|
||||
</TimelineHeader>
|
||||
<TimelineIndicator />
|
||||
<TimelineSeparator />
|
||||
<TimelineContent>Repo and architecture set up.</TimelineContent>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
```
|
||||
|
||||
**Gotcha:** each item needs a unique `step`. `orientation` is `"vertical"` (default) or `"horizontal"`. This is a static event display, not interactive like `stepper`.
|
||||
|
||||
## autocomplete
|
||||
|
||||
**Required:** `items` (array; each item has at least `value`)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Autocomplete items={items}>
|
||||
<AutocompleteInput placeholder="Search..." />
|
||||
<AutocompleteContent>
|
||||
<AutocompleteEmpty>No results found.</AutocompleteEmpty>
|
||||
<AutocompleteList>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value} value={item}>{item.label}</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
</Autocomplete>
|
||||
```
|
||||
|
||||
**Gotcha:** `AutocompleteList` takes a render-prop `(item) => ReactNode`, NOT a mapped array of children. External API: https://base-ui.com/react/components/autocomplete
|
||||
|
||||
## phone-input
|
||||
|
||||
**Required:** none, but wire `onChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<PhoneInput placeholder="Enter phone number" defaultCountry="US" value={value} onChange={setValue} />
|
||||
```
|
||||
|
||||
**Gotcha:** `value`/`onChange` use an E.164 string (e.g. `"+14155551234"`), not a display-formatted string; `onChange` can fire `undefined`. `defaultCountry` is a 2-letter ISO code. Wraps `react-phone-number-input`.
|
||||
|
||||
## number-field
|
||||
|
||||
**Required:** wrap the controls in `NumberFieldGroup`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<NumberField defaultValue={0}>
|
||||
<NumberFieldScrubArea label="Quantity" />
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldDecrement />
|
||||
<NumberFieldInput />
|
||||
<NumberFieldIncrement />
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
```
|
||||
|
||||
**Gotcha:** import from `@/components/ui/number-field`. The accessible label goes on `NumberFieldScrubArea`, not `NumberField`. External API: https://base-ui.com/react/components/number-field
|
||||
|
||||
## rating
|
||||
|
||||
**Required:** `rating` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Rating rating={4.5} showValue editable onRatingChange={setRating} />
|
||||
```
|
||||
|
||||
**Gotcha:** supports decimals (partial stars). Pass `editable` + `onRatingChange` for interactive input; omit both for a read-only display.
|
||||
|
||||
## scrollspy
|
||||
|
||||
**Required:** `targetRef` (the scroll container ref)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Scrollspy targetRef={containerRef}>
|
||||
<a href="#s1" data-scrollspy-anchor="s1">Section 1</a>
|
||||
<a href="#s2" data-scrollspy-anchor="s2">Section 2</a>
|
||||
</Scrollspy>
|
||||
<div ref={containerRef}>
|
||||
<div id="s1">...</div>
|
||||
<div id="s2">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Gotcha:** each link's `data-scrollspy-anchor` must match a section `id`. `targetRef` is the scrollable container (defaults to the window).
|
||||
|
||||
## frame
|
||||
|
||||
**Required:** `Frame` > `FramePanel`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Frame>
|
||||
<FramePanel>
|
||||
<FrameHeader>
|
||||
<FrameTitle>Title</FrameTitle>
|
||||
<FrameDescription>Description</FrameDescription>
|
||||
</FrameHeader>
|
||||
<div className="p-5">Content</div>
|
||||
<FrameFooter>Footer</FrameFooter>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Gotcha:** a structured card shell for tool-like surfaces. `stacked` connects multiple panels with shared borders; `dense` removes panel padding; radius via the `--frame-radius` CSS variable.
|
||||
|
||||
## icon-stack
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconStack aria-hidden="true">
|
||||
<InboxIcon className="size-4" />
|
||||
</IconStack>
|
||||
```
|
||||
|
||||
**Gotcha:** isometric layered artwork for empty states and illustrations; style the inner icon via its own `className`. Mark purely decorative stacks `aria-hidden="true"` and keep the real label in surrounding copy.
|
||||
|
||||
## icon-tile
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconTile variant="elevated" size="lg">
|
||||
<PackageIcon />
|
||||
</IconTile>
|
||||
```
|
||||
|
||||
**Gotcha:** the square container an icon sits in, so every list row, feature card and empty state shares one affordance. `variant`: `outline` (default) | `elevated` (muted fill, raised ring) | `soft` (tinted nested, tone from currentColor) | `solid` (filled tone, contrasting glyph) | `frame` (double container). `soft` and `solid` retint from one text color class (they default to `text-primary`). `size`: `xs | sm | default | lg | xl` (24/32/40/48/64px tile, glyph scales 12/14/16/20/24px). `radius`: `default | full`. Do not set a `size-*` class on the child icon unless you mean to override the tile's glyph size; recolor with `className` on the tile, not the icon.
|
||||
|
||||
## alert
|
||||
|
||||
**Required:** `Alert` > `AlertTitle`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Alert variant="success">
|
||||
<ShieldCheckIcon />
|
||||
<AlertTitle>Security update</AlertTitle>
|
||||
<AlertDescription>Enable two-factor authentication.</AlertDescription>
|
||||
<AlertAction><Button size="xs">Update</Button></AlertAction>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible API. `variant`: `default | destructive | info | success | warning | invert`. The non-default variants use ReUI extended color tokens (`--success`/`--info`/`--warning`/`--invert`), which the install adds. Defer generic alert rules to the shadcn skill.
|
||||
|
||||
## badge
|
||||
|
||||
**Required:** none (text child).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="success-light" size="sm">Success</Badge>
|
||||
<Badge variant="outline" radius="full">Pill</Badge>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible. Rich `variant` set (solid, `-outline`, `-light` per color), `size` `xs..xl`, `radius` `default | full`. Like `alert`, the color variants rely on ReUI extended tokens. Prefer `Badge` variants over raw color classes for statuses.
|
||||
|
||||
## base vs radix - write for the project's base
|
||||
|
||||
ReUI ships every component in two builds: `base` (Base UI) and `radix` (Radix UI). The install command and name are identical, and the CLI installs the build matching the project. But you must write/adapt code against the **right base**, because their APIs differ.
|
||||
|
||||
**Detect the base first.** Read `components.json` -> `style` and take the segment before the first `-`:
|
||||
|
||||
- `"style": "base-nova"` -> **Base UI**
|
||||
- `"style": "radix-nova"` -> **Radix UI**
|
||||
|
||||
**Then use that base's API.** The deltas mirror shadcn's base-vs-radix split:
|
||||
|
||||
- Slot/composition: Base UI `render={<… />}` vs Radix `asChild`.
|
||||
- `Select`: Base UI takes `items`; Radix uses `<SelectItem>` children.
|
||||
- `ToggleGroup`: Base UI `multiple` boolean vs Radix `type="single" | "multiple"`.
|
||||
|
||||
The safest path is to **read the installed files and `c-*` examples** - they're already in your base, so reuse their wiring instead of guessing. When `get_component`'s inline `api` or an example shows the other base's shape, translate it to your base (or `validate_usage` to confirm). Defer the generic base/radix mechanics to the shadcn skill.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Craft: make ReUI UI exceptional, not generic
|
||||
|
||||
ReUI items ship senior-designer quality. Your adaptation has to hold that bar, so the result reads like a real product surface a team would keep - not a wireframe an AI generated. Use these alongside the reuse rules in [adapting.md](./adapting.md).
|
||||
|
||||
## Have a point of view
|
||||
|
||||
Pick an emotional register before you compose - calm, operational, premium, editorial, dense, energetic - and let layout, spacing, surface treatment, and icon behavior all reinforce it. One or two memorable decisions and restraint everywhere else beats ten generic ones. UI with no point of view reads as generated.
|
||||
|
||||
## Brutally clear hierarchy
|
||||
|
||||
One focal point per card or panel: the dominant metric or task first, its label second, supporting detail third. The first thing the eye lands on should be the right thing; secondary text must read as secondary. Borders, separators, and surfaces do real work to create 2-3 information bands - don't flatten everything to equal weight.
|
||||
|
||||
## Spacing rhythm and deliberate density
|
||||
|
||||
Gaps are a signal, not a default. Keep them intentional and consistent within a family (`gap-1`/`gap-2` for tight operational rows, larger gaps for section breaks), and smaller within a group than between groups. Match the surrounding ReUI density; don't pad an operational surface like a marketing page, and don't drift density mid-section. The composition should still feel authored in grayscale.
|
||||
|
||||
## Cover the real states (the usual miss)
|
||||
|
||||
A surface isn't done at the happy path. Compose, and wire:
|
||||
|
||||
- **Empty** - a purposeful empty state (short message + the primary action), never a blank panel.
|
||||
- **Loading** - a **skeleton** that matches the real layout, not a centered spinner.
|
||||
- **Error** - an inline, recoverable error with a retry, announced via `role="status"`/`aria-live`.
|
||||
|
||||
Derive these from an element the block already has (don't invent parallel markup), or `get_examples` for a state-specific example.
|
||||
|
||||
## Responsive by default
|
||||
|
||||
Mobile-first, not mobile-afterthought. In constrained rows/cards/sidebars, put `min-w-0` on the shrinking container and `truncate` long single-line labels; protect the primary label's width and let secondary content compress. Reflow layouts (multi-column -> single column) rather than just shrinking them. Desktop and mobile should both look designed.
|
||||
|
||||
## Motion, subtly
|
||||
|
||||
Motion should clarify, not decorate. Use ReUI Motion Icons on primary actions for a subtle hover cue; keep transitions short (~200-300ms) with calm easing; prefer a skeleton pulse over a spinner. No bouncing, no gratuitous entrance animations on every element.
|
||||
|
||||
## Real, activated content
|
||||
|
||||
Use believable, typed data (realistic labels, counts, timestamps, statuses that map to a real workflow) - never lorem or abstract filler. Every visible control does something: no decorative buttons, fake tabs, meaningless toggles, or stats with no job. It must still hold with long names, empty values, and crowded data.
|
||||
|
||||
## Avoid the AI tells
|
||||
|
||||
These instantly read as generated - don't ship them: equal-weight card walls, empty gradients, repetitive padding everywhere, generic enterprise copy, ornamental icons, and number tiles that don't earn their place.
|
||||
|
||||
## The bar
|
||||
|
||||
Before you finish, ask: **would a product team keep this instead of replacing it? Does it still feel strong after swapping in real content?** If not, reuse the shipped ReUI design harder - don't restyle it into something new - then run the [quality.md](./quality.md) gates.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Icons (ReUI delta over shadcn)
|
||||
|
||||
Follow the shadcn icon rules (use the project's configured `iconLibrary`, `data-icon` on icons inside `Button`, no sizing classes on icons inside components, pass icons as component objects not string keys). ReUI adds the following.
|
||||
|
||||
## Portable icons (library-agnostic)
|
||||
|
||||
ReUI components, examples, and blocks are authored to be icon-library-agnostic. When `iconLibrary` is set in `components.json`, the shadcn CLI installs each item's icons in **your** library automatically - you swap nothing. If an installed item's icons don't match your project (for example `iconLibrary` isn't set, so they came in from the item's demo library), change the **import source and component name** to your library, keeping the same icon-name semantics:
|
||||
|
||||
- `lucide` -> `lucide-react`
|
||||
- `tabler` -> `@tabler/icons-react`
|
||||
- `phosphor` -> `@phosphor-icons/react`
|
||||
- `remix` -> `@remixicon/react`
|
||||
- `hugeicons` -> `@hugeicons/react`
|
||||
|
||||
Don't assume `lucide-react`; read `iconLibrary` from `components.json`.
|
||||
|
||||
## Keep icons purposeful
|
||||
|
||||
Icons support the hierarchy, they don't replace it: keep them small, matched to the surrounding density, and decorative ones `aria-hidden="true"` (an icon-only control still needs an accessible label on the control). Don't add ornamental icons that do no job.
|
||||
|
||||
## Motion Icons (the `@reui/icons/...` set)
|
||||
|
||||
ReUI ships its own icon set in 4 styles (outline, solid, duotone, filled), each icon in two variants:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/icons/default/<style>/<name> --yes # static
|
||||
npx shadcn@latest add @reui/icons/animated/<style>/<name> --yes # hover-animated (motion/react)
|
||||
```
|
||||
|
||||
Finding them via the MCP is free; installing requires an Ultimate license (`REUI_LICENSE_KEY`, see [cli.md](./cli.md)). Reach for a Motion Icon on a primary action when a subtle hover cue helps; keep motion restrained.
|
||||
|
||||
Finding icons:
|
||||
|
||||
- Several icons (the common case): **`search_icons(concepts[])`** - up to 24 concepts in one call, the best icons per concept with install commands. Pass `animated: true` to get only icons with a hover-animated Motion variant.
|
||||
- One icon: `search` with `type: "icon"`.
|
||||
- Icon results and `get_icon` carry `animated: true` and `installAnimated` when an animated variant exists - use those install strings, do not construct paths by hand.
|
||||
- Every icon result carries a `previewUrl` (its live icon-category page) - **share it with the user** so they can SEE the icon before installing.
|
||||
|
||||
The `icon-stack` component composes multiple icons into a stacked display.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Quality gates (security, accessibility, scroll)
|
||||
|
||||
These are the **done gate**, not a nice-to-have: before you call any ReUI work finished, call the MCP `get_audit_checklist` tool and pass every item below (plus the craft bar in [craft.md](./craft.md)). Then typecheck and lint.
|
||||
|
||||
## Security
|
||||
|
||||
- Never `dangerouslySetInnerHTML`. Render data as text/components.
|
||||
- External links (`target="_blank"`) must always pair `rel="noopener noreferrer"`.
|
||||
- No real PII, secrets, or tokens in demo or committed code. Remote media only from sources the project already allows.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Implicit list/card items that navigate get real anchors with a standard hover affordance.
|
||||
- Icon-only or numeric buttons need an `aria-label`; decorative icons get `aria-hidden`.
|
||||
- Every non-submit button is `type="button"`.
|
||||
- Keyboard + focus: everything interactive is reachable in a sensible Tab order with a visible focus ring; layers (dialogs/sheets/menus) trap focus and close on `Escape`. ReUI components ship standard keyboard behavior - read each component's inline `api` rather than re-implementing it.
|
||||
- Announce async UI: loading and error messages use `role="status"` / `aria-live` so they're not silent to screen readers.
|
||||
|
||||
## Scroll mechanics
|
||||
|
||||
- Make scroll regions with a parent-owned height: a `min-h-0` + flex chain down to the scroll container. Never guess a `max-h`.
|
||||
- The scroll container owns `overflow-auto`; ancestors stay `min-h-0` so the height resolves.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ReUI registry structure
|
||||
|
||||
ReUI is a shadcn-compatible registry with four entity types. **Examples and blocks are built FROM components** - reuse them, don't rebuild.
|
||||
|
||||
- **component** - one of the 20 ReUI building blocks with a real API (`data-grid`, `kanban`, `filters`, `date-selector`, `tree`, ...). Install directly (`@reui/data-grid`) or let it come in as a dependency of an example/block. Free. Read its API with `get_component(name)`.
|
||||
- **example** - a free `c-*` single-pattern use-case of a component (`c-kanban-1`, `c-data-grid-3`). Install one and read it to copy real composition. Find a component's examples with `get_examples(name)`.
|
||||
- **block** - a premium, full-page section that composes several components (`data-grid-2`, `pricing-page-1`). Pro or Ultimate license at install. Adapts to your active theme via semantic tokens.
|
||||
- **icon** - Motion Icons in 4 styles (outline, solid, duotone, filled), static (`@reui/icons/default/<style>/<name>`) and hover-animated (`@reui/icons/animated/<style>/<name>`). Ultimate license at install. See [icons.md](./icons.md).
|
||||
|
||||
## The @reui registry
|
||||
|
||||
Install everything through the shadcn CLI: `npx shadcn@latest add @reui/<name> --yes`. The CLI reads the `@reui` registry from the project's `components.json`. Free items need only the plain string form:
|
||||
|
||||
```json
|
||||
{ "registries": { "@reui": "https://reui.io/r/{style}/{name}.json" } }
|
||||
```
|
||||
|
||||
Premium installs need the authenticated form + `REUI_LICENSE_KEY` in `.env.local` - see [cli.md](./cli.md). The MCP `get_project_context` tool returns the right config.
|
||||
|
||||
## Know your base: base or radix
|
||||
|
||||
ReUI ships every item in two builds - `base` (Base UI) and `radix` (Radix UI) - with mirrored names. The CLI installs the build matching your project automatically, but **you must write code against the right base's API**. Detect it from `components.json` -> `style`: the segment before the first `-` is the base (`base-nova` -> Base UI, `radix-nova` -> Radix UI). The installed files and `c-*` examples are already in your base - read them and adapt on that base. See [components.md](./components.md) for the API deltas.
|
||||
|
||||
**So the MCP's own `docsUrl` and `previewUrl` match your base**, send your `style` to the MCP: add `?style=<your components.json style>` to the ReUI MCP server URL (or set an `X-Reui-Style` header) in your MCP client config - set once, applies to every call. The MCP then resolves docs/preview links to YOUR library (`/docs/components/radix/...`, `/preview/radix/...` for a radix project) instead of the default base; `get_project_context` echoes back the style it currently sees so you can confirm it. Install commands are unaffected (the CLI already installs the right variant). If you notice the MCP returning `base` links for a `radix` project, tell the user to add `?style=` to the server URL.
|
||||
|
||||
Blocks adapt to your active theme through semantic tokens and CSS variables - change the theme and every block follows.
|
||||
|
||||
## Free vs premium
|
||||
|
||||
- **Free, no key:** the 20 components, all `c-*` examples, the ReUI MCP, and this skill.
|
||||
- **Premium, license required at install:** blocks (Pro or Ultimate), Motion Icons and templates (Ultimate). Set `REUI_LICENSE_KEY` (see [cli.md](./cli.md)).
|
||||
|
||||
## Component API index
|
||||
|
||||
The canonical index of every component's API docs is **https://reui.io/llms.txt** (returned as `componentsApiUrl` in MCP results). Prefer the inline `api` from `get_component`; use the index/docs as the fallback.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Styling (ReUI delta over shadcn)
|
||||
|
||||
Follow the shadcn skill for the generic rules (semantic colors not raw values, `gap-*` not `space-y-*`, `size-*`, `cn()`, no manual `dark:` overrides, no overlay `z-index`). This file is only the ReUI-specific additions.
|
||||
|
||||
## ReUI extended semantic tokens
|
||||
|
||||
ReUI adds semantic tokens beyond shadcn's base set. Use these instead of raw colors for status and emphasis:
|
||||
|
||||
- `--success` / `--success-foreground`
|
||||
- `--info` / `--info-foreground`
|
||||
- `--warning` / `--warning-foreground`
|
||||
- `--destructive-foreground` (paired with shadcn's `--destructive`)
|
||||
- `--invert` / `--invert-foreground` (inverted surfaces)
|
||||
|
||||
Use them as Tailwind utilities (`bg-success text-success-foreground`, `text-warning`, ...). They are defined in the project's global CSS and registered with Tailwind (`@theme inline` on v4). If a token is missing in the project, add it to the global CSS file (never a new file) following the same `name` / `name-foreground` convention, exactly as the shadcn customization rules describe.
|
||||
|
||||
**Incorrect:** `<span className="text-green-600">Active</span>`
|
||||
**Correct:** `<Badge variant="success">Active</Badge>` or `<span className="text-success">Active</span>`
|
||||
|
||||
## Blocks follow your theme
|
||||
|
||||
When you install a block it adapts to your active theme through the semantic tokens above and the project's CSS variables. Don't hardcode style-specific values into installed block code and don't fork it to "restyle" - change the theme via the CSS variables / a preset and every block follows. Want a different look? `search` for a block whose design already fits instead of re-skinning one.
|
||||
|
||||
## Density and typography rhythm
|
||||
|
||||
ReUI operational UI usually feels dense, not airy. Keep the gap between a title and its supporting description tight by default (`gap-0.5`, `space-y-1`, or `space-y-px`), and smaller than the gap between sections. Match the surrounding ReUI density when you add rows or fields; do not pad operational surfaces like a marketing page.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Workflow: find -> install -> read API -> adapt
|
||||
|
||||
The core ReUI loop. The MCP tells you what to install and gives you the API; the shadcn CLI installs it; you turn the installed files into correct, themed, data-wired code by **reuse**, not redesign.
|
||||
|
||||
## 1. Find (ReUI MCP `search` / `compose_page`)
|
||||
|
||||
**Full multi-section page ask?** Call `compose_page(intent, sections?)` FIRST, before searching block-by-block. It returns ordered sections, each with the best block for the intent (top pick + alternates); sections listed in `unavailableSections` have no real inventory - compose those from components, do not force a bad block.
|
||||
|
||||
For everything else, call `search` with the user's intent. Pass structured hints whenever you can infer them - you are an LLM, so do the parsing the server cannot:
|
||||
|
||||
- `type`: `"component"` (one of the 20 building blocks), `"example"` (a c-\* use-case), `"block"` (a full page/section), `"icon"`.
|
||||
- `component`: the ReUI component the request implies (`"data-grid"`, `"kanban"`, ...).
|
||||
- `category`, `features` (e.g. `["sortable","pagination"]`), `free`.
|
||||
|
||||
Example: "build a users management page with filters" -> `search({ query: "users management page with filters", type: "block", component: "data-grid", features: ["filters"] })`.
|
||||
|
||||
Each result has `install`, `previewUrl`, `docsUrl`, `componentsUsed`, `score`, `termCoverage`, and `whyMatch`. `score` is relative to the top hit (the top is ~100 by construction), not an absolute quality - compare results to each other, and show the user the top options if several score closely; do not silently guess. A low `termCoverage` means a weak match even with a high score - rephrase or widen.
|
||||
|
||||
**Always show the preview link.** Whenever you list or recommend items - from `search`, `search_icons`, `list_components`, `compose_page`, or a getter - include each item's `previewUrl` (a live preview page) so the user can SEE it before you install. Blocks and examples link to an individual live preview; icons and components to their live category/component page. This applies to every listing, not only a single pick.
|
||||
|
||||
## 2. Install (shadcn CLI)
|
||||
|
||||
Run the result's `install` command from the project root, non-interactively:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/<name> --yes
|
||||
```
|
||||
|
||||
The CLI reads `components.json`, installs the correct base+style variant, resolves `registryDependencies` (a block pulls in its components), installs npm deps, and rewrites aliases. Do not pass the base/style. See [cli.md](./cli.md).
|
||||
|
||||
## 3. Read the API (do not guess props)
|
||||
|
||||
Before writing code against any component an item uses:
|
||||
|
||||
1. The item's `componentDigests` already give a 1-line contract per component - often enough to wire it. For the full API, call **`get_component(names)`** with ALL of `componentsUsed` in ONE call (it accepts an array) and read each inline `api` - no web fetch. **Share the component's `docsUrl`** (its API documentation page) with the user whenever you work with that component's API, so they have the full reference; the `/llms.txt` index is a further fallback.
|
||||
2. Call **`get_examples(name)`** for the free `c-*` examples of that component; install one and **read the added files** to copy the exact composition. This is the fastest correct path - the example shows real wiring you adapt, not invent.
|
||||
3. About to write a prop you did not see in an `api` or installed file? Run **`validate_usage`** BEFORE writing the code - per-prop documented / notDocumented verdicts plus did-you-mean suggestions. notDocumented means read the API, not push on.
|
||||
|
||||
## 4. Adapt (reuse-first) - do not skip
|
||||
|
||||
Installing files is not the end, and redesigning them defeats the point. First note the project's **base** so you write the right API - read `components.json` -> `style` and take the segment before the first `-` (`base-nova` -> Base UI, `radix-nova` -> Radix UI), see [components.md](./components.md). After `add`:
|
||||
|
||||
1. **Read the added files**; keep the composition intact. For a block, verify the components are wired correctly (for `data-grid`: a `useTable({ features: dataGridFeatures, ... })` instance passed as `table`, `recordCount` set - see [components.md](./components.md)).
|
||||
2. **Replace demo data with the user's real data** via typed structures (see [adapting.md](./adapting.md)).
|
||||
3. **Fix icon imports** to the project's icon library (see [icons.md](./icons.md)).
|
||||
4. **Align styling** to semantic tokens and the active theme - no raw colors (see [styling.md](./styling.md)).
|
||||
5. **Validate before finalizing**: if your adaptation introduced components or props you did not read in an `api` or example, run `validate_usage` on them.
|
||||
6. **Hit the craft bar** - clear hierarchy, deliberate density, the empty / loading / error states, subtle motion, and mobile-first responsiveness (see [craft.md](./craft.md)). Generic-looking output means you under-reused the design, not that it needs restyling.
|
||||
7. **Pass the quality gates** (security, a11y, scroll) - call the MCP `get_audit_checklist` tool and clear every item (see [quality.md](./quality.md)).
|
||||
8. **Typecheck / lint**.
|
||||
|
||||
## If no single block fits
|
||||
|
||||
Compose from components (`compose_page` tells you which sections have no block inventory via `unavailableSections`). `search` the components you need, read each `get_component` API, install a worked `get_examples` example for each, and assemble by adapting those examples. A block in the same category is a useful reference - install it and read its files to see how ReUI composes those components, then adapt.
|
||||
@@ -0,0 +1,58 @@
|
||||
# ReUI MCP: full reference
|
||||
|
||||
The ReUI MCP (`https://mcp.reui.io`, Streamable HTTP) is free to use but needs a ReUI account: on first use the agent signs in with ReUI (a free account is created if the user has none), so every request is tied to an account. Free covers components and examples; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the daily request limit. It does **discovery + guidance** (search, inline APIs, page planning, validation) and never serves source; the shadcn CLI does **installation**, and the license key lives there (the `@reui` entry in `components.json`, backed by `.env.local`). Goal: from the user's intent to correct, themed, data-wired ReUI code in the **fewest tokens and calls**, with **no guessing**.
|
||||
|
||||
## Golden path (token-optimal - follow this order)
|
||||
|
||||
Most tasks need 2-4 MCP calls and ZERO web fetches:
|
||||
|
||||
1. **`search(query, ...hints)`** -> pick the top 1-3 results. Each result already carries `install`, `previewUrl`, `docsUrl`, `componentsUsed`, `score`, `whyMatch`. The payload is complete - do not call another tool just to "confirm" a result.
|
||||
2. **`get_component([...componentsUsed])`** in ONE batched call (one name or an array of up to 20) -> read each inline `api`. This **replaces** fetching docs pages. Often skippable: search responses carry `componentDigests`, a compact API contract per referenced component.
|
||||
3. **`get_examples(component)`** -> install ONE returned `c-*` example, read the added files, copy the composition.
|
||||
4. **`get_install_command(item)`** only to validate a name you are unsure of (results already include `install`). Run the install with the shadcn CLI (`--yes`).
|
||||
5. **`get_audit_checklist()`** before declaring done.
|
||||
|
||||
If you already know the exact item name, skip `search`. Everything else is situational.
|
||||
|
||||
## The 5 task-specific tools (when to reach for each)
|
||||
|
||||
- **`compose_page`** - BEFORE building any full page (dashboard, settings, billing, landing). Pass the intent (and optionally the sections you want); it returns ordered sections, each with the best premium block for the intent (top pick + alternates). Sections with no real inventory are listed honestly in `unavailableSections` - compose those from components instead of forcing a bad block.
|
||||
- **`search_icons`** - whenever you need icons, especially several. Batch up to 24 concepts in one call; each concept returns its best icons with install commands. Pass `animated: true` to get only icons that have a hover-animated Motion variant.
|
||||
- **`validate_usage`** - BEFORE writing code with component names or props you have not read in an inline `api` or an installed example. It checks planned names + props against the indexed API docs and registry item names; returns did-you-mean suggestions and per-prop documented / notDocumented verdicts. Deterministic, no inference - a notDocumented prop means stop and read the API, not push on.
|
||||
- **`whats_new`** - when your registry knowledge might be stale (a name 404s, the user mentions an item you don't know). Returns items added/removed per build, newest first.
|
||||
- **`report_issue`** - when an installed item is actually broken (bad source, wrong dependency, broken preview). Goes straight to the ReUI team; rate-limited 5/hour. Not for usage questions.
|
||||
|
||||
## All 19 tools
|
||||
|
||||
`search`, `get_block`, `get_example`, `get_icon`, `list_block_groups`, `list_block_categories`, `list_example_categories`, `list_icon_categories`, `list_components`, `get_component`, `get_examples`, `search_icons`, `compose_page`, `validate_usage`, `whats_new`, `report_issue`, `get_install_command`, `get_project_context`, `get_audit_checklist`. The MCP serves the full parameter schemas; do not guess parameters beyond them.
|
||||
|
||||
## Token + speed rules
|
||||
|
||||
- **Batch `get_component`** - ONE call with the whole `componentsUsed` array, never N calls. Skip it entirely when `componentDigests` already answers the question.
|
||||
- **Read source by installing** - the MCP serves no source. To read or analyze an item's real code, install it with the shadcn CLI and open the local files. Learn an API from the inline `api` / `componentDigests`, never by reading raw source.
|
||||
- **Infer `search` hints yourself** (`type`, `component`, `category`, `features`, `free`) - hints shrink the result set and the tokens. Keep `limit` low; one right result beats ten.
|
||||
- Run independent calls (and the shadcn install) concurrently in one turn - serial tool calls are the main source of slowness.
|
||||
- Don't repeat a search for the same intent; don't call `list_*` to "see everything" - `search` is the entry point, `list_*` is only for browsing a taxonomy the user explicitly wants to explore.
|
||||
- Prefer `get_component`'s inline `api` over `docsUrl` / `/llms.txt`. Fetch a web page only as a last resort.
|
||||
|
||||
## Result shapes (so you don't re-fetch)
|
||||
|
||||
- `score` is 0-100 RELATIVE to the top hit (the top is ~100 by construction), not absolute - compare results to each other.
|
||||
- `termCoverage` (0-1) is the share of the query the item matched - low means a weak match even if the score looks high; rephrase or widen the search.
|
||||
- Each result carries `whyMatch`, `install`, docs/preview URLs, and a `free` flag; premium items carry `requiredPlan` (`"pro"` for blocks, `"ultimate"` for icons).
|
||||
- `componentDigests` is a top-level map: a compact API contract per referenced component - often enough to wire an item without a `get_component` call.
|
||||
- Icon results and `get_icon` include `animated: true` and `installAnimated` when a hover-animated Motion variant exists (animated: `@reui/icons/animated/<style>/<name>`; static: `@reui/icons/default/<style>/<name>`).
|
||||
|
||||
## Error playbook
|
||||
|
||||
- **401** - the MCP requires a signed-in ReUI account. The client prompts "Sign in with ReUI" (OAuth) on first use; a free account is created if needed. For headless/CI, pass a personal token (`reui_pat_...`, created at https://reui.io/account/mcp) as `Authorization: Bearer`.
|
||||
- **403 / locked result** - a valid account but the plan does not cover the item: premium blocks need Pro, Motion Icons need Ultimate. Point to https://reui.io/pricing (upgrade). Free accounts still get all components + examples.
|
||||
- **429** - rate limited (120 requests/min per IP); back off, honor `Retry-After`.
|
||||
- **not found** (`found: false`) - use the returned `suggestions`, or `search`; check `whats_new` if you suspect a stale name. Never run a fabricated install command.
|
||||
|
||||
## Fallbacks
|
||||
|
||||
- No ReUI MCP: `npx shadcn@latest search @reui -q "..."` then `add` (generic, no scoring / inline API).
|
||||
- The shadcn project's own MCP also works over the `@reui` registry: https://ui.shadcn.com/docs/mcp.
|
||||
|
||||
Per-agent MCP setup: https://reui.io/docs/mcp
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: reui
|
||||
description: Use the ReUI registry from your AI agent - find, install, and correctly use ReUI components (the 20 free building blocks like data-grid, kanban, filters), their free examples, premium blocks, and Motion Icons. Applies in any project using ReUI, the @reui registry, REUI_LICENSE_KEY, or any shadcn project where the user asks for premium blocks, data grids, kanban boards, dashboards, or full pages. Pairs with the free ReUI MCP server for live, scored registry search and inline component APIs.
|
||||
user-invocable: false
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
---
|
||||
|
||||
> **ReUI skill version `668fb463eb`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI for Agents
|
||||
|
||||
ReUI is a shadcn-compatible registry. It ships four things you **reuse** - never redesign:
|
||||
|
||||
- **components** - the 20 ReUI building blocks with real APIs: `data-grid`, `kanban`, `filters`, `date-selector`, `tree`, `stepper`, ... (free)
|
||||
- **examples** - free `c-*` single-pattern use-cases of a component (`c-kanban-1`); install one and read it to see exact composition
|
||||
- **blocks** - premium full-page sections that compose components (`data-grid-2`, `pricing-page-1`); Pro or Ultimate license at install
|
||||
- **icons** - Motion Icons in 4 styles, static + hover-animated variants; Ultimate license at install
|
||||
|
||||
The skill is free and this MCP is free to use; it just needs a ReUI account. On first use your agent opens a browser "Sign in with ReUI" prompt (a free account is created if you don't have one). Free covers components and examples with a daily request allowance; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the limit (see [rules/registry.md](./rules/registry.md)). The same account and skill work in every agent and service the MCP connects to - this skill is agent-agnostic.
|
||||
|
||||
Skill + MCP are a team: this skill is the workflow (how to find, install, read the API, and adapt by reuse); the MCP is the live data and the hands (search, get_component, install commands). Your job: find the right item, install it with the shadcn CLI, read its real API, and **adapt by reuse** - wire real data and theme it; do not hand-roll or restyle what ReUI already provides. This skill **layers on the shadcn skill**: follow that for generic rules (spacing, `cn()`, semantic colors, forms); follow this for everything ReUI-specific.
|
||||
|
||||
## The core loop (MCP-native)
|
||||
|
||||
1. **Find** - call the ReUI MCP `search` tool with the user's intent. It returns a ranked, scored list across components/examples/blocks/icons, each with an `install` command, `previewUrl`, `docsUrl`, and `componentsUsed`. Pass hints (`type`, `component`, `category`, `features`, `free`) when you can infer them.
|
||||
2. **Install** - run the returned command non-interactively (`npx shadcn@latest add @reui/<name> --yes`). The CLI resolves deps, aliases, and the base/style from `components.json`. See [cli.md](./rules/cli.md).
|
||||
3. **Read the API (on your base)** - first note your base from `components.json` -> `style` (`base-nova` -> Base UI, `radix-nova` -> Radix UI). For each component an item uses, call `get_component(name)` and read its **inline `api`** (no web fetch); then `get_examples(name)` to install a worked example and copy its composition - the installed files are already in your base. Whenever you work with a component's API, also **share its `docsUrl`** (the primitive's API documentation page) with the user so they have the full reference. See [components.md](./rules/components.md).
|
||||
4. **Adapt (reuse-first)** - swap demo data for real data, fix icon imports, align tokens. Do not redesign. See [adapting.md](./rules/adapting.md).
|
||||
|
||||
**Always show the preview.** Every item a tool returns carries a `previewUrl` (a live preview page). Whenever you list, recommend, or present ReUI items to the user - blocks, components, examples, or icons, whether from `search`, `search_icons`, `list_components`, `compose_page`, or any getter - include each item's `previewUrl` so they can SEE it before installing. Blocks and examples open an individual live preview; icons and components link to their live category/component page. Never present an item without its preview link.
|
||||
|
||||
If the ReUI MCP is not configured, fall back to `npx shadcn@latest search @reui -q "..."` then `add` - but the MCP gives scored matches + inline APIs; prefer it.
|
||||
|
||||
## Commands
|
||||
|
||||
Run ReUI as explicit slash commands (via the ReUI MCP) **or** just ask in plain language - both run the same workflow.
|
||||
|
||||
| Command | Invoke | Does |
|
||||
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **build** | `/mcp__reui__build <what>` | Compose a page/section/feature from ReUI: plan → install → read API → adapt → craft → audit. |
|
||||
| **add** | `/mcp__reui__add <item>` | Find & install one component/example/block/icon and wire it in. |
|
||||
| **fix** | `/mcp__reui__fix [target]` | Diagnose & fix ReUI usage: wrong/undocumented props, base/radix mismatch, missing states, a11y/scroll. |
|
||||
| **improve** | `/mcp__reui__improve [target]` | Refine + extend existing ReUI UI to a production-exceptional bar (hierarchy, density, states, responsive, motion). |
|
||||
|
||||
Invocation differs slightly per agent (`/mcp__reui__build` in Claude Code/Cursor/Windsurf, `/mcp.reui.build` in VS Code). No command surface? Just describe what you want - this skill drives the identical loop.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
| Need | Reach for |
|
||||
| -------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| A full page or section (dashboard, billing, auth, pricing, settings) | `compose_page` first (plans sections + best blocks), then ReUI **blocks** |
|
||||
| A data table with sorting/filtering/pagination/virtualization | the **data-grid** component (never hand-roll a `<table>`) |
|
||||
| A drag-and-drop board | the **kanban** component |
|
||||
| Advanced column filtering, date range, tree, stepper, ... | the matching ReUI **component** |
|
||||
| A single generic control already in shadcn (Button, Dialog, Select) | plain **shadcn** |
|
||||
|
||||
## Detailed references
|
||||
|
||||
- [rules/registry.md](./rules/registry.md) - the four types, the @reui registry, base/radix, free vs premium + license
|
||||
- [rules/workflow.md](./rules/workflow.md) - the find -> install -> read-API -> adapt loop (most important)
|
||||
- [rules/components.md](./rules/components.md) - the 20 components, the data-grid contract, base vs radix
|
||||
- [rules/adapting.md](./rules/adapting.md) - reuse-first: preserve the design (no over-customizing), reuse examples + a block's own elements, real data, don't invent APIs
|
||||
- [rules/craft.md](./rules/craft.md) - make it exceptional: point of view, hierarchy, density, states, responsive, motion, the bar
|
||||
- [rules/quality.md](./rules/quality.md) - security, accessibility, and scroll gates (the done gate)
|
||||
- [rules/styling.md](./rules/styling.md) - ReUI extended tokens, theme adaptation, density
|
||||
- [rules/icons.md](./rules/icons.md) - portable icons, swapping imports, Motion Icons (static + animated)
|
||||
- [tools.md](./tools.md) - the ReUI MCP: golden path, the 19 tools, token rules, result shapes, errors
|
||||
@@ -0,0 +1,43 @@
|
||||
# Adapting installed ReUI code (reuse-first, no AI slop)
|
||||
|
||||
ReUI items ship production-quality. Your job is to **adapt by reuse** - wire real data and fit the app - not to redesign or hand-roll. The output should look like ReUI built it for this product.
|
||||
|
||||
## Preserve the design - don't over-customize
|
||||
|
||||
The design IS the product. A ReUI block/component encodes senior-designer decisions: spacing, hierarchy, density, color treatment, and component choices. The fastest way to turn a premium block back into generic AI slop is to "improve" its look - so don't.
|
||||
|
||||
- Change **data, copy, and props**; keep the **structure and styling** it ships with. Make the **smallest** change that wires the real data. If your diff touches `className` / JSX structure more than data / props, you are over-customizing - stop and reuse.
|
||||
- Don't swap ReUI components for hand-rolled ones, restructure the layout, re-skin spacing / radius / colors, or add decorative chrome. Let the installed components carry the default spacing, radius, sizing, icon rhythm, density, and state styling; add custom Tailwind only when a component genuinely lacks a contract you need.
|
||||
- Want a different look? `search` for a block whose design already fits and reuse that - don't restyle this one into a new design.
|
||||
|
||||
## Reuse the parts: examples and the block's own elements
|
||||
|
||||
- **Examples are building parts.** A free `c-*` example is a correct, single-pattern composition you can reuse. Before composing from scratch, `get_examples(component)`, install the closest one, and reuse its wiring - assemble UI from examples instead of hand-rolling what an example already shows.
|
||||
- **Reuse a block's own elements.** Need more rows, cards, items, or sections than ship by default? Repeat the block's **existing** element by mapping real data through the same markup - never invent parallel markup that drifts from its design. Need a variant (empty / loading / expanded)? Derive it from an element the block already has.
|
||||
|
||||
## Don't invent (read, don't guess)
|
||||
|
||||
- Never write a prop, variant value, import path, or `@reui/...` name you didn't read in a component's inline `api`, an installed example, or a `search` result. If you didn't see it, treat it as nonexistent - call `get_component` / `get_examples` / `search` first, or run the MCP `validate_usage` tool to check planned names + props against the docs before writing code.
|
||||
- If a getter returns `found: false` or `search` returns nothing, say so and fall back (plain shadcn, or ask) - never fabricate an install command or an API.
|
||||
|
||||
## What to change vs leave alone
|
||||
|
||||
- **Change:** the item's own data, copy, props, and layout to fit the app.
|
||||
- **Leave alone:** installed component files, hooks, and the shared theme - do not edit vendored ReUI internals; change behavior through props and the documented API.
|
||||
- Blocks are **portable React** - no `next/link`, `next/image`, or other framework-runtime imports inside them. Keep them portable.
|
||||
|
||||
## Demo data -> real data
|
||||
|
||||
- Replace every placeholder with the user's real data. Model it as **typed data structures** and **map over arrays** - never duplicate JSX per row/card. Keep small block-specific formatters next to the data.
|
||||
- Wire the real source (columns, fields, fetch). For `data-grid`, implement the server fetch contract if the user needs server-side data.
|
||||
- **Type from the component API, derive during render.** Type domain state through the component's own types - e.g. map status to `BadgeProps["variant"]` via a typed `Record<Status, …>` - instead of stringly-typed values. Compute view state during render; don't mirror derived data into `useState`/`useEffect`.
|
||||
- **Adapt on the right base.** Use the API for the project's base (Base UI vs Radix - see [components.md](./components.md)); the installed files are already base-correct, so reuse their shape rather than translating from memory.
|
||||
|
||||
## Believable content (no AI tells)
|
||||
|
||||
- Use realistic labels, counts, timestamps, and statuses that map to a real workflow.
|
||||
- No decorative buttons, fake tabs, meaningless toggles, equal-weight card walls, empty gradients, ornamental icons, or generic SaaS filler. Every element should do something.
|
||||
|
||||
## Operational surfaces (settings / profile / admin)
|
||||
|
||||
Pick ONE archetype and keep the family consistent: a vertical rail (3-6 sections), horizontal tabs (5-8), or a frame/stack. Prefer `frame` for tool-like surfaces, a card for profile-like ones. Don't mix archetypes in one surface.
|
||||
@@ -0,0 +1,60 @@
|
||||
# CLI: registry setup, license, non-interactive install
|
||||
|
||||
## Registry setup (one-time, per project)
|
||||
|
||||
Free items (the 20 components and all `c-*` examples) need only the plain string registry in `components.json`:
|
||||
|
||||
```json
|
||||
{ "registries": { "@reui": "https://reui.io/r/{style}/{name}.json" } }
|
||||
```
|
||||
|
||||
Premium items (blocks; Motion Icons and templates) require a ReUI license at install:
|
||||
|
||||
1. Add the key to `.env.local`:
|
||||
|
||||
```bash
|
||||
REUI_LICENSE_KEY=your-license-key
|
||||
```
|
||||
|
||||
2. Switch `components.json` to the authenticated object form:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${REUI_LICENSE_KEY}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`, but an MCP client config never expands variables, so a ReUI MCP server config must carry the raw token instead (for example `reui_pat_your_token_here`).
|
||||
|
||||
The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry
|
||||
|
||||
## Installing
|
||||
|
||||
Use the project's package runner (check `packageManager`):
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/<name> --yes # npm
|
||||
pnpm dlx shadcn@latest add @reui/<name> --yes # pnpm
|
||||
bunx --bun shadcn@latest add @reui/<name> --yes # bun
|
||||
```
|
||||
|
||||
`--yes` skips confirmation prompts. The CLI auto-detects the package manager from the lockfile (there is no `--package-manager` flag). It also resolves the correct base+style variant from `components.json`, so do not pass a style.
|
||||
|
||||
## Handling prompts and conflicts
|
||||
|
||||
- **Always pass `--yes`** so the CLI does not block on confirmation prompts.
|
||||
- **Do NOT pass `--overwrite` by default.** If the CLI reports an existing file, read the output and resolve deliberately: install under a different name, adjust the path, or ask the user. Only use `--overwrite` when the user explicitly wants to replace a file.
|
||||
- **Preview first when touching an existing project**: `npx shadcn@latest add @reui/<name> --dry-run` shows what would change; `--diff <file>` shows a specific file's diff. Use these before overwriting.
|
||||
- Run from the **project root** so `components.json` and `.env.local` are found.
|
||||
|
||||
## Free vs premium boundary
|
||||
|
||||
- Public, no key: `c-*` examples and the 20 components (`@reui/data-grid`, `@reui/badge`, ...) that those examples depend on.
|
||||
- Key required at install: blocks (`@reui/<category>-N`) need a Pro or Ultimate license; Motion Icons (`@reui/icons/...`) and templates need Ultimate.
|
||||
|
||||
If an install 401/403s, the license key is missing, invalid, or the plan does not cover that resource (blocks: Pro or higher; icons and templates: Ultimate). Point the user to https://reui.io/account (their key) or https://reui.io/pricing (upgrade).
|
||||
@@ -0,0 +1,370 @@
|
||||
# ReUI components
|
||||
|
||||
The 20 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these.
|
||||
|
||||
**Rule one: never guess a component's API. Read it first.** Call **`get_component(name)`** for its inline `api` (props + usage, no web fetch), and **share the result's `docsUrl`** (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the `/llms.txt` index is a further fallback). Then call **`get_examples(name)`** to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline `api` is the full reference. No single block fits? Compose: search the components you need, read each `get_component`, install a `get_examples` example per component, and adapt.
|
||||
|
||||
## data-grid (the flagship - read its API every time)
|
||||
|
||||
`data-grid` wraps TanStack Table v9. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
|
||||
|
||||
- Build a TanStack table instance with `useTable({ features: dataGridFeatures, ... })` (columns, data). `dataGridFeatures` is exported by the primitive and already bundles sorting, filtering, pagination, row selection, expanding, pinning, resizing and faceting, so there are no per-table row models to wire.
|
||||
- Pass that instance to `<DataGrid table={table} recordCount={total}>`.
|
||||
- Compose the body with `DataGridTable` inside `DataGrid`, and enable features through `tableLayout` (e.g. `{ headerSticky: true, columnsResizable: true }`), not ad-hoc classes.
|
||||
- Server-side data uses the documented fetch shape (`recordCount` is the total for pagination).
|
||||
|
||||
```tsx
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
data,
|
||||
columns,
|
||||
})
|
||||
|
||||
<DataGrid table={table} recordCount={data.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
Common mistakes:
|
||||
|
||||
- **Incorrect:** `<DataGrid data={rows} columns={cols} />` - these props do not exist. **Correct:** build a `useTable({ features: dataGridFeatures, ... })` instance and pass `table={table}` + `recordCount`.
|
||||
- **Incorrect:** a raw `<table>` / hand-rolled pagination. **Correct:** use `data-grid`; read its API for sticky header, pagination, virtualization, row selection.
|
||||
- **Incorrect:** styling rows/cells with arbitrary classes. **Correct:** drive layout via `tableLayout` and the primitive's `DataGridColumnMeta` (e.g. `cellClassName`, `headerTitle`), set through the bundle's `columnMeta` slot.
|
||||
|
||||
## event-calendar
|
||||
|
||||
**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EventCalendar defaultEvents={events} defaultView="month" className="h-[560px]">
|
||||
<EventCalendarNav />
|
||||
<EventCalendarContent />
|
||||
</EventCalendar>
|
||||
```
|
||||
|
||||
**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column.
|
||||
|
||||
## gantt
|
||||
|
||||
**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Gantt defaultEvents={bars} resources={tasks} defaultScale="month" className="h-[480px]">
|
||||
<GanttNav />
|
||||
<GanttView />
|
||||
</Gantt>
|
||||
```
|
||||
|
||||
**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height.
|
||||
|
||||
## kanban
|
||||
|
||||
**Required:** `value` (`Record<string, T[]>`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
|
||||
<KanbanBoard>
|
||||
{Object.entries(cols).map(([id, items]) => (
|
||||
<KanbanColumn key={id} value={id}>
|
||||
<KanbanColumnHandle><h3>{id}</h3></KanbanColumnHandle>
|
||||
<KanbanColumnContent value={id}>
|
||||
{items.map((i) => (
|
||||
<KanbanItem key={i.id} value={i.id}>
|
||||
<KanbanItemHandle>{i.title}</KanbanItemHandle>
|
||||
</KanbanItem>
|
||||
))}
|
||||
</KanbanColumnContent>
|
||||
</KanbanColumn>
|
||||
))}
|
||||
</KanbanBoard>
|
||||
<KanbanOverlay><div className="bg-muted size-full rounded-md" /></KanbanOverlay>
|
||||
</Kanban>
|
||||
```
|
||||
|
||||
**Gotcha:** state is `Record<columnId, T[]>`. Each `KanbanColumnContent value` must match its parent `KanbanColumn value`. Omit `KanbanOverlay` and the drag preview silently breaks.
|
||||
|
||||
## sortable
|
||||
|
||||
**Required:** `value` (`T[]`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Sortable value={items} onValueChange={setItems} getItemValue={(i) => i.id}>
|
||||
{items.map((i) => (
|
||||
<SortableItem key={i.id} value={i.id}>
|
||||
<SortableItemHandle><GripVertical /></SortableItemHandle>
|
||||
{i.label}
|
||||
</SortableItem>
|
||||
))}
|
||||
</Sortable>
|
||||
```
|
||||
|
||||
**Gotcha:** a flat 1D reorder list (not columns - that is `kanban`). `getItemValue` must return a stable, unique string. Pass `layout="grid"` or `layout="nested"` for non-list layouts.
|
||||
|
||||
## filters
|
||||
|
||||
**Required:** `filters` (`Filter[]`), `fields` (`FilterFieldConfig[]`), `onChange`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const [filters, setFilters] = useState<Filter[]>([
|
||||
createFilter("priority", "is_any_of", ["low"]),
|
||||
])
|
||||
const fields: FilterFieldConfig[] = [
|
||||
{ key: "priority", label: "Priority", type: "multiselect",
|
||||
options: [{ value: "low", label: "Low" }, { value: "high", label: "High" }] },
|
||||
]
|
||||
|
||||
<Filters filters={filters} fields={fields} onChange={setFilters} />
|
||||
```
|
||||
|
||||
**Gotcha:** always build initial filters with `createFilter(field, operator, values)` - it generates the required `id`. Never hand-construct a `Filter` object. Pairs naturally with `data-grid`.
|
||||
|
||||
## date-selector
|
||||
|
||||
**Required:** none, but wire `onChange` to capture the value.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const [value, setValue] = useState<DateSelectorValue | undefined>()
|
||||
|
||||
<DateSelector value={value} onChange={setValue} label="Due date" />
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a structured `DateSelectorValue` (period / operator / start+end dates), NOT a `Date` - never pass a raw `Date`. Use `allowRange={false}` to lock single-date picking. Read `get_component("date-selector")` for the value shape.
|
||||
|
||||
## tree
|
||||
|
||||
**Required:** `tree` (a `@headless-tree/core` instance you construct)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Tree tree={tree}>
|
||||
{tree.getItems().map((item) => (
|
||||
<TreeItem key={item.getId()} item={item}>
|
||||
<TreeItemLabel />
|
||||
</TreeItem>
|
||||
))}
|
||||
</Tree>
|
||||
```
|
||||
|
||||
**Gotcha:** `Tree` is a styled shell - it takes a headless-tree instance via `tree`, NOT `data`/`items` props. Build the instance with `@headless-tree/react`. External API: https://headless-tree.lukasbach.com/
|
||||
|
||||
## stepper
|
||||
|
||||
**Required:** `StepperItem step` (number), `StepperContent value` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Stepper defaultValue={1}>
|
||||
<StepperNav>
|
||||
<StepperItem step={1}>
|
||||
<StepperTrigger><StepperIndicator>1</StepperIndicator></StepperTrigger>
|
||||
<StepperSeparator />
|
||||
</StepperItem>
|
||||
<StepperItem step={2}>
|
||||
<StepperTrigger><StepperIndicator>2</StepperIndicator></StepperTrigger>
|
||||
</StepperItem>
|
||||
</StepperNav>
|
||||
<StepperPanel>
|
||||
<StepperContent value={1}>Step 1 content</StepperContent>
|
||||
<StepperContent value={2}>Step 2 content</StepperContent>
|
||||
</StepperPanel>
|
||||
</Stepper>
|
||||
```
|
||||
|
||||
**Gotcha:** steps are 1-indexed. Without `StepperPanel` + `StepperContent` you render the nav trail but no body. Put `StepperSeparator` in every `StepperItem` except the last.
|
||||
|
||||
## timeline
|
||||
|
||||
**Required:** `TimelineItem step` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Timeline>
|
||||
<TimelineItem step={1}>
|
||||
<TimelineHeader>
|
||||
<TimelineDate>March 2024</TimelineDate>
|
||||
<TimelineTitle>Project initialized</TimelineTitle>
|
||||
</TimelineHeader>
|
||||
<TimelineIndicator />
|
||||
<TimelineSeparator />
|
||||
<TimelineContent>Repo and architecture set up.</TimelineContent>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
```
|
||||
|
||||
**Gotcha:** each item needs a unique `step`. `orientation` is `"vertical"` (default) or `"horizontal"`. This is a static event display, not interactive like `stepper`.
|
||||
|
||||
## autocomplete
|
||||
|
||||
**Required:** `items` (array; each item has at least `value`)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Autocomplete items={items}>
|
||||
<AutocompleteInput placeholder="Search..." />
|
||||
<AutocompleteContent>
|
||||
<AutocompleteEmpty>No results found.</AutocompleteEmpty>
|
||||
<AutocompleteList>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value} value={item}>{item.label}</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
</Autocomplete>
|
||||
```
|
||||
|
||||
**Gotcha:** `AutocompleteList` takes a render-prop `(item) => ReactNode`, NOT a mapped array of children. External API: https://base-ui.com/react/components/autocomplete
|
||||
|
||||
## phone-input
|
||||
|
||||
**Required:** none, but wire `onChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<PhoneInput placeholder="Enter phone number" defaultCountry="US" value={value} onChange={setValue} />
|
||||
```
|
||||
|
||||
**Gotcha:** `value`/`onChange` use an E.164 string (e.g. `"+14155551234"`), not a display-formatted string; `onChange` can fire `undefined`. `defaultCountry` is a 2-letter ISO code. Wraps `react-phone-number-input`.
|
||||
|
||||
## number-field
|
||||
|
||||
**Required:** wrap the controls in `NumberFieldGroup`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<NumberField defaultValue={0}>
|
||||
<NumberFieldScrubArea label="Quantity" />
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldDecrement />
|
||||
<NumberFieldInput />
|
||||
<NumberFieldIncrement />
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
```
|
||||
|
||||
**Gotcha:** import from `@/components/ui/number-field`. The accessible label goes on `NumberFieldScrubArea`, not `NumberField`. External API: https://base-ui.com/react/components/number-field
|
||||
|
||||
## rating
|
||||
|
||||
**Required:** `rating` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Rating rating={4.5} showValue editable onRatingChange={setRating} />
|
||||
```
|
||||
|
||||
**Gotcha:** supports decimals (partial stars). Pass `editable` + `onRatingChange` for interactive input; omit both for a read-only display.
|
||||
|
||||
## scrollspy
|
||||
|
||||
**Required:** `targetRef` (the scroll container ref)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Scrollspy targetRef={containerRef}>
|
||||
<a href="#s1" data-scrollspy-anchor="s1">Section 1</a>
|
||||
<a href="#s2" data-scrollspy-anchor="s2">Section 2</a>
|
||||
</Scrollspy>
|
||||
<div ref={containerRef}>
|
||||
<div id="s1">...</div>
|
||||
<div id="s2">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Gotcha:** each link's `data-scrollspy-anchor` must match a section `id`. `targetRef` is the scrollable container (defaults to the window).
|
||||
|
||||
## frame
|
||||
|
||||
**Required:** `Frame` > `FramePanel`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Frame>
|
||||
<FramePanel>
|
||||
<FrameHeader>
|
||||
<FrameTitle>Title</FrameTitle>
|
||||
<FrameDescription>Description</FrameDescription>
|
||||
</FrameHeader>
|
||||
<div className="p-5">Content</div>
|
||||
<FrameFooter>Footer</FrameFooter>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Gotcha:** a structured card shell for tool-like surfaces. `stacked` connects multiple panels with shared borders; `dense` removes panel padding; radius via the `--frame-radius` CSS variable.
|
||||
|
||||
## icon-stack
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconStack aria-hidden="true">
|
||||
<InboxIcon className="size-4" />
|
||||
</IconStack>
|
||||
```
|
||||
|
||||
**Gotcha:** isometric layered artwork for empty states and illustrations; style the inner icon via its own `className`. Mark purely decorative stacks `aria-hidden="true"` and keep the real label in surrounding copy.
|
||||
|
||||
## icon-tile
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconTile variant="elevated" size="lg">
|
||||
<PackageIcon />
|
||||
</IconTile>
|
||||
```
|
||||
|
||||
**Gotcha:** the square container an icon sits in, so every list row, feature card and empty state shares one affordance. `variant`: `outline` (default) | `elevated` (muted fill, raised ring) | `soft` (tinted nested, tone from currentColor) | `solid` (filled tone, contrasting glyph) | `frame` (double container). `soft` and `solid` retint from one text color class (they default to `text-primary`). `size`: `xs | sm | default | lg | xl` (24/32/40/48/64px tile, glyph scales 12/14/16/20/24px). `radius`: `default | full`. Do not set a `size-*` class on the child icon unless you mean to override the tile's glyph size; recolor with `className` on the tile, not the icon.
|
||||
|
||||
## alert
|
||||
|
||||
**Required:** `Alert` > `AlertTitle`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Alert variant="success">
|
||||
<ShieldCheckIcon />
|
||||
<AlertTitle>Security update</AlertTitle>
|
||||
<AlertDescription>Enable two-factor authentication.</AlertDescription>
|
||||
<AlertAction><Button size="xs">Update</Button></AlertAction>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible API. `variant`: `default | destructive | info | success | warning | invert`. The non-default variants use ReUI extended color tokens (`--success`/`--info`/`--warning`/`--invert`), which the install adds. Defer generic alert rules to the shadcn skill.
|
||||
|
||||
## badge
|
||||
|
||||
**Required:** none (text child).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="success-light" size="sm">Success</Badge>
|
||||
<Badge variant="outline" radius="full">Pill</Badge>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible. Rich `variant` set (solid, `-outline`, `-light` per color), `size` `xs..xl`, `radius` `default | full`. Like `alert`, the color variants rely on ReUI extended tokens. Prefer `Badge` variants over raw color classes for statuses.
|
||||
|
||||
## base vs radix - write for the project's base
|
||||
|
||||
ReUI ships every component in two builds: `base` (Base UI) and `radix` (Radix UI). The install command and name are identical, and the CLI installs the build matching the project. But you must write/adapt code against the **right base**, because their APIs differ.
|
||||
|
||||
**Detect the base first.** Read `components.json` -> `style` and take the segment before the first `-`:
|
||||
|
||||
- `"style": "base-nova"` -> **Base UI**
|
||||
- `"style": "radix-nova"` -> **Radix UI**
|
||||
|
||||
**Then use that base's API.** The deltas mirror shadcn's base-vs-radix split:
|
||||
|
||||
- Slot/composition: Base UI `render={<… />}` vs Radix `asChild`.
|
||||
- `Select`: Base UI takes `items`; Radix uses `<SelectItem>` children.
|
||||
- `ToggleGroup`: Base UI `multiple` boolean vs Radix `type="single" | "multiple"`.
|
||||
|
||||
The safest path is to **read the installed files and `c-*` examples** - they're already in your base, so reuse their wiring instead of guessing. When `get_component`'s inline `api` or an example shows the other base's shape, translate it to your base (or `validate_usage` to confirm). Defer the generic base/radix mechanics to the shadcn skill.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Craft: make ReUI UI exceptional, not generic
|
||||
|
||||
ReUI items ship senior-designer quality. Your adaptation has to hold that bar, so the result reads like a real product surface a team would keep - not a wireframe an AI generated. Use these alongside the reuse rules in [adapting.md](./adapting.md).
|
||||
|
||||
## Have a point of view
|
||||
|
||||
Pick an emotional register before you compose - calm, operational, premium, editorial, dense, energetic - and let layout, spacing, surface treatment, and icon behavior all reinforce it. One or two memorable decisions and restraint everywhere else beats ten generic ones. UI with no point of view reads as generated.
|
||||
|
||||
## Brutally clear hierarchy
|
||||
|
||||
One focal point per card or panel: the dominant metric or task first, its label second, supporting detail third. The first thing the eye lands on should be the right thing; secondary text must read as secondary. Borders, separators, and surfaces do real work to create 2-3 information bands - don't flatten everything to equal weight.
|
||||
|
||||
## Spacing rhythm and deliberate density
|
||||
|
||||
Gaps are a signal, not a default. Keep them intentional and consistent within a family (`gap-1`/`gap-2` for tight operational rows, larger gaps for section breaks), and smaller within a group than between groups. Match the surrounding ReUI density; don't pad an operational surface like a marketing page, and don't drift density mid-section. The composition should still feel authored in grayscale.
|
||||
|
||||
## Cover the real states (the usual miss)
|
||||
|
||||
A surface isn't done at the happy path. Compose, and wire:
|
||||
|
||||
- **Empty** - a purposeful empty state (short message + the primary action), never a blank panel.
|
||||
- **Loading** - a **skeleton** that matches the real layout, not a centered spinner.
|
||||
- **Error** - an inline, recoverable error with a retry, announced via `role="status"`/`aria-live`.
|
||||
|
||||
Derive these from an element the block already has (don't invent parallel markup), or `get_examples` for a state-specific example.
|
||||
|
||||
## Responsive by default
|
||||
|
||||
Mobile-first, not mobile-afterthought. In constrained rows/cards/sidebars, put `min-w-0` on the shrinking container and `truncate` long single-line labels; protect the primary label's width and let secondary content compress. Reflow layouts (multi-column -> single column) rather than just shrinking them. Desktop and mobile should both look designed.
|
||||
|
||||
## Motion, subtly
|
||||
|
||||
Motion should clarify, not decorate. Use ReUI Motion Icons on primary actions for a subtle hover cue; keep transitions short (~200-300ms) with calm easing; prefer a skeleton pulse over a spinner. No bouncing, no gratuitous entrance animations on every element.
|
||||
|
||||
## Real, activated content
|
||||
|
||||
Use believable, typed data (realistic labels, counts, timestamps, statuses that map to a real workflow) - never lorem or abstract filler. Every visible control does something: no decorative buttons, fake tabs, meaningless toggles, or stats with no job. It must still hold with long names, empty values, and crowded data.
|
||||
|
||||
## Avoid the AI tells
|
||||
|
||||
These instantly read as generated - don't ship them: equal-weight card walls, empty gradients, repetitive padding everywhere, generic enterprise copy, ornamental icons, and number tiles that don't earn their place.
|
||||
|
||||
## The bar
|
||||
|
||||
Before you finish, ask: **would a product team keep this instead of replacing it? Does it still feel strong after swapping in real content?** If not, reuse the shipped ReUI design harder - don't restyle it into something new - then run the [quality.md](./quality.md) gates.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Icons (ReUI delta over shadcn)
|
||||
|
||||
Follow the shadcn icon rules (use the project's configured `iconLibrary`, `data-icon` on icons inside `Button`, no sizing classes on icons inside components, pass icons as component objects not string keys). ReUI adds the following.
|
||||
|
||||
## Portable icons (library-agnostic)
|
||||
|
||||
ReUI components, examples, and blocks are authored to be icon-library-agnostic. When `iconLibrary` is set in `components.json`, the shadcn CLI installs each item's icons in **your** library automatically - you swap nothing. If an installed item's icons don't match your project (for example `iconLibrary` isn't set, so they came in from the item's demo library), change the **import source and component name** to your library, keeping the same icon-name semantics:
|
||||
|
||||
- `lucide` -> `lucide-react`
|
||||
- `tabler` -> `@tabler/icons-react`
|
||||
- `phosphor` -> `@phosphor-icons/react`
|
||||
- `remix` -> `@remixicon/react`
|
||||
- `hugeicons` -> `@hugeicons/react`
|
||||
|
||||
Don't assume `lucide-react`; read `iconLibrary` from `components.json`.
|
||||
|
||||
## Keep icons purposeful
|
||||
|
||||
Icons support the hierarchy, they don't replace it: keep them small, matched to the surrounding density, and decorative ones `aria-hidden="true"` (an icon-only control still needs an accessible label on the control). Don't add ornamental icons that do no job.
|
||||
|
||||
## Motion Icons (the `@reui/icons/...` set)
|
||||
|
||||
ReUI ships its own icon set in 4 styles (outline, solid, duotone, filled), each icon in two variants:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/icons/default/<style>/<name> --yes # static
|
||||
npx shadcn@latest add @reui/icons/animated/<style>/<name> --yes # hover-animated (motion/react)
|
||||
```
|
||||
|
||||
Finding them via the MCP is free; installing requires an Ultimate license (`REUI_LICENSE_KEY`, see [cli.md](./cli.md)). Reach for a Motion Icon on a primary action when a subtle hover cue helps; keep motion restrained.
|
||||
|
||||
Finding icons:
|
||||
|
||||
- Several icons (the common case): **`search_icons(concepts[])`** - up to 24 concepts in one call, the best icons per concept with install commands. Pass `animated: true` to get only icons with a hover-animated Motion variant.
|
||||
- One icon: `search` with `type: "icon"`.
|
||||
- Icon results and `get_icon` carry `animated: true` and `installAnimated` when an animated variant exists - use those install strings, do not construct paths by hand.
|
||||
- Every icon result carries a `previewUrl` (its live icon-category page) - **share it with the user** so they can SEE the icon before installing.
|
||||
|
||||
The `icon-stack` component composes multiple icons into a stacked display.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Quality gates (security, accessibility, scroll)
|
||||
|
||||
These are the **done gate**, not a nice-to-have: before you call any ReUI work finished, call the MCP `get_audit_checklist` tool and pass every item below (plus the craft bar in [craft.md](./craft.md)). Then typecheck and lint.
|
||||
|
||||
## Security
|
||||
|
||||
- Never `dangerouslySetInnerHTML`. Render data as text/components.
|
||||
- External links (`target="_blank"`) must always pair `rel="noopener noreferrer"`.
|
||||
- No real PII, secrets, or tokens in demo or committed code. Remote media only from sources the project already allows.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Implicit list/card items that navigate get real anchors with a standard hover affordance.
|
||||
- Icon-only or numeric buttons need an `aria-label`; decorative icons get `aria-hidden`.
|
||||
- Every non-submit button is `type="button"`.
|
||||
- Keyboard + focus: everything interactive is reachable in a sensible Tab order with a visible focus ring; layers (dialogs/sheets/menus) trap focus and close on `Escape`. ReUI components ship standard keyboard behavior - read each component's inline `api` rather than re-implementing it.
|
||||
- Announce async UI: loading and error messages use `role="status"` / `aria-live` so they're not silent to screen readers.
|
||||
|
||||
## Scroll mechanics
|
||||
|
||||
- Make scroll regions with a parent-owned height: a `min-h-0` + flex chain down to the scroll container. Never guess a `max-h`.
|
||||
- The scroll container owns `overflow-auto`; ancestors stay `min-h-0` so the height resolves.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ReUI registry structure
|
||||
|
||||
ReUI is a shadcn-compatible registry with four entity types. **Examples and blocks are built FROM components** - reuse them, don't rebuild.
|
||||
|
||||
- **component** - one of the 20 ReUI building blocks with a real API (`data-grid`, `kanban`, `filters`, `date-selector`, `tree`, ...). Install directly (`@reui/data-grid`) or let it come in as a dependency of an example/block. Free. Read its API with `get_component(name)`.
|
||||
- **example** - a free `c-*` single-pattern use-case of a component (`c-kanban-1`, `c-data-grid-3`). Install one and read it to copy real composition. Find a component's examples with `get_examples(name)`.
|
||||
- **block** - a premium, full-page section that composes several components (`data-grid-2`, `pricing-page-1`). Pro or Ultimate license at install. Adapts to your active theme via semantic tokens.
|
||||
- **icon** - Motion Icons in 4 styles (outline, solid, duotone, filled), static (`@reui/icons/default/<style>/<name>`) and hover-animated (`@reui/icons/animated/<style>/<name>`). Ultimate license at install. See [icons.md](./icons.md).
|
||||
|
||||
## The @reui registry
|
||||
|
||||
Install everything through the shadcn CLI: `npx shadcn@latest add @reui/<name> --yes`. The CLI reads the `@reui` registry from the project's `components.json`. Free items need only the plain string form:
|
||||
|
||||
```json
|
||||
{ "registries": { "@reui": "https://reui.io/r/{style}/{name}.json" } }
|
||||
```
|
||||
|
||||
Premium installs need the authenticated form + `REUI_LICENSE_KEY` in `.env.local` - see [cli.md](./cli.md). The MCP `get_project_context` tool returns the right config.
|
||||
|
||||
## Know your base: base or radix
|
||||
|
||||
ReUI ships every item in two builds - `base` (Base UI) and `radix` (Radix UI) - with mirrored names. The CLI installs the build matching your project automatically, but **you must write code against the right base's API**. Detect it from `components.json` -> `style`: the segment before the first `-` is the base (`base-nova` -> Base UI, `radix-nova` -> Radix UI). The installed files and `c-*` examples are already in your base - read them and adapt on that base. See [components.md](./components.md) for the API deltas.
|
||||
|
||||
**So the MCP's own `docsUrl` and `previewUrl` match your base**, send your `style` to the MCP: add `?style=<your components.json style>` to the ReUI MCP server URL (or set an `X-Reui-Style` header) in your MCP client config - set once, applies to every call. The MCP then resolves docs/preview links to YOUR library (`/docs/components/radix/...`, `/preview/radix/...` for a radix project) instead of the default base; `get_project_context` echoes back the style it currently sees so you can confirm it. Install commands are unaffected (the CLI already installs the right variant). If you notice the MCP returning `base` links for a `radix` project, tell the user to add `?style=` to the server URL.
|
||||
|
||||
Blocks adapt to your active theme through semantic tokens and CSS variables - change the theme and every block follows.
|
||||
|
||||
## Free vs premium
|
||||
|
||||
- **Free, no key:** the 20 components, all `c-*` examples, the ReUI MCP, and this skill.
|
||||
- **Premium, license required at install:** blocks (Pro or Ultimate), Motion Icons and templates (Ultimate). Set `REUI_LICENSE_KEY` (see [cli.md](./cli.md)).
|
||||
|
||||
## Component API index
|
||||
|
||||
The canonical index of every component's API docs is **https://reui.io/llms.txt** (returned as `componentsApiUrl` in MCP results). Prefer the inline `api` from `get_component`; use the index/docs as the fallback.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Styling (ReUI delta over shadcn)
|
||||
|
||||
Follow the shadcn skill for the generic rules (semantic colors not raw values, `gap-*` not `space-y-*`, `size-*`, `cn()`, no manual `dark:` overrides, no overlay `z-index`). This file is only the ReUI-specific additions.
|
||||
|
||||
## ReUI extended semantic tokens
|
||||
|
||||
ReUI adds semantic tokens beyond shadcn's base set. Use these instead of raw colors for status and emphasis:
|
||||
|
||||
- `--success` / `--success-foreground`
|
||||
- `--info` / `--info-foreground`
|
||||
- `--warning` / `--warning-foreground`
|
||||
- `--destructive-foreground` (paired with shadcn's `--destructive`)
|
||||
- `--invert` / `--invert-foreground` (inverted surfaces)
|
||||
|
||||
Use them as Tailwind utilities (`bg-success text-success-foreground`, `text-warning`, ...). They are defined in the project's global CSS and registered with Tailwind (`@theme inline` on v4). If a token is missing in the project, add it to the global CSS file (never a new file) following the same `name` / `name-foreground` convention, exactly as the shadcn customization rules describe.
|
||||
|
||||
**Incorrect:** `<span className="text-green-600">Active</span>`
|
||||
**Correct:** `<Badge variant="success">Active</Badge>` or `<span className="text-success">Active</span>`
|
||||
|
||||
## Blocks follow your theme
|
||||
|
||||
When you install a block it adapts to your active theme through the semantic tokens above and the project's CSS variables. Don't hardcode style-specific values into installed block code and don't fork it to "restyle" - change the theme via the CSS variables / a preset and every block follows. Want a different look? `search` for a block whose design already fits instead of re-skinning one.
|
||||
|
||||
## Density and typography rhythm
|
||||
|
||||
ReUI operational UI usually feels dense, not airy. Keep the gap between a title and its supporting description tight by default (`gap-0.5`, `space-y-1`, or `space-y-px`), and smaller than the gap between sections. Match the surrounding ReUI density when you add rows or fields; do not pad operational surfaces like a marketing page.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Workflow: find -> install -> read API -> adapt
|
||||
|
||||
The core ReUI loop. The MCP tells you what to install and gives you the API; the shadcn CLI installs it; you turn the installed files into correct, themed, data-wired code by **reuse**, not redesign.
|
||||
|
||||
## 1. Find (ReUI MCP `search` / `compose_page`)
|
||||
|
||||
**Full multi-section page ask?** Call `compose_page(intent, sections?)` FIRST, before searching block-by-block. It returns ordered sections, each with the best block for the intent (top pick + alternates); sections listed in `unavailableSections` have no real inventory - compose those from components, do not force a bad block.
|
||||
|
||||
For everything else, call `search` with the user's intent. Pass structured hints whenever you can infer them - you are an LLM, so do the parsing the server cannot:
|
||||
|
||||
- `type`: `"component"` (one of the 20 building blocks), `"example"` (a c-\* use-case), `"block"` (a full page/section), `"icon"`.
|
||||
- `component`: the ReUI component the request implies (`"data-grid"`, `"kanban"`, ...).
|
||||
- `category`, `features` (e.g. `["sortable","pagination"]`), `free`.
|
||||
|
||||
Example: "build a users management page with filters" -> `search({ query: "users management page with filters", type: "block", component: "data-grid", features: ["filters"] })`.
|
||||
|
||||
Each result has `install`, `previewUrl`, `docsUrl`, `componentsUsed`, `score`, `termCoverage`, and `whyMatch`. `score` is relative to the top hit (the top is ~100 by construction), not an absolute quality - compare results to each other, and show the user the top options if several score closely; do not silently guess. A low `termCoverage` means a weak match even with a high score - rephrase or widen.
|
||||
|
||||
**Always show the preview link.** Whenever you list or recommend items - from `search`, `search_icons`, `list_components`, `compose_page`, or a getter - include each item's `previewUrl` (a live preview page) so the user can SEE it before you install. Blocks and examples link to an individual live preview; icons and components to their live category/component page. This applies to every listing, not only a single pick.
|
||||
|
||||
## 2. Install (shadcn CLI)
|
||||
|
||||
Run the result's `install` command from the project root, non-interactively:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/<name> --yes
|
||||
```
|
||||
|
||||
The CLI reads `components.json`, installs the correct base+style variant, resolves `registryDependencies` (a block pulls in its components), installs npm deps, and rewrites aliases. Do not pass the base/style. See [cli.md](./cli.md).
|
||||
|
||||
## 3. Read the API (do not guess props)
|
||||
|
||||
Before writing code against any component an item uses:
|
||||
|
||||
1. The item's `componentDigests` already give a 1-line contract per component - often enough to wire it. For the full API, call **`get_component(names)`** with ALL of `componentsUsed` in ONE call (it accepts an array) and read each inline `api` - no web fetch. **Share the component's `docsUrl`** (its API documentation page) with the user whenever you work with that component's API, so they have the full reference; the `/llms.txt` index is a further fallback.
|
||||
2. Call **`get_examples(name)`** for the free `c-*` examples of that component; install one and **read the added files** to copy the exact composition. This is the fastest correct path - the example shows real wiring you adapt, not invent.
|
||||
3. About to write a prop you did not see in an `api` or installed file? Run **`validate_usage`** BEFORE writing the code - per-prop documented / notDocumented verdicts plus did-you-mean suggestions. notDocumented means read the API, not push on.
|
||||
|
||||
## 4. Adapt (reuse-first) - do not skip
|
||||
|
||||
Installing files is not the end, and redesigning them defeats the point. First note the project's **base** so you write the right API - read `components.json` -> `style` and take the segment before the first `-` (`base-nova` -> Base UI, `radix-nova` -> Radix UI), see [components.md](./components.md). After `add`:
|
||||
|
||||
1. **Read the added files**; keep the composition intact. For a block, verify the components are wired correctly (for `data-grid`: a `useTable({ features: dataGridFeatures, ... })` instance passed as `table`, `recordCount` set - see [components.md](./components.md)).
|
||||
2. **Replace demo data with the user's real data** via typed structures (see [adapting.md](./adapting.md)).
|
||||
3. **Fix icon imports** to the project's icon library (see [icons.md](./icons.md)).
|
||||
4. **Align styling** to semantic tokens and the active theme - no raw colors (see [styling.md](./styling.md)).
|
||||
5. **Validate before finalizing**: if your adaptation introduced components or props you did not read in an `api` or example, run `validate_usage` on them.
|
||||
6. **Hit the craft bar** - clear hierarchy, deliberate density, the empty / loading / error states, subtle motion, and mobile-first responsiveness (see [craft.md](./craft.md)). Generic-looking output means you under-reused the design, not that it needs restyling.
|
||||
7. **Pass the quality gates** (security, a11y, scroll) - call the MCP `get_audit_checklist` tool and clear every item (see [quality.md](./quality.md)).
|
||||
8. **Typecheck / lint**.
|
||||
|
||||
## If no single block fits
|
||||
|
||||
Compose from components (`compose_page` tells you which sections have no block inventory via `unavailableSections`). `search` the components you need, read each `get_component` API, install a worked `get_examples` example for each, and assemble by adapting those examples. A block in the same category is a useful reference - install it and read its files to see how ReUI composes those components, then adapt.
|
||||
@@ -0,0 +1,58 @@
|
||||
# ReUI MCP: full reference
|
||||
|
||||
The ReUI MCP (`https://mcp.reui.io`, Streamable HTTP) is free to use but needs a ReUI account: on first use the agent signs in with ReUI (a free account is created if the user has none), so every request is tied to an account. Free covers components and examples; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the daily request limit. It does **discovery + guidance** (search, inline APIs, page planning, validation) and never serves source; the shadcn CLI does **installation**, and the license key lives there (the `@reui` entry in `components.json`, backed by `.env.local`). Goal: from the user's intent to correct, themed, data-wired ReUI code in the **fewest tokens and calls**, with **no guessing**.
|
||||
|
||||
## Golden path (token-optimal - follow this order)
|
||||
|
||||
Most tasks need 2-4 MCP calls and ZERO web fetches:
|
||||
|
||||
1. **`search(query, ...hints)`** -> pick the top 1-3 results. Each result already carries `install`, `previewUrl`, `docsUrl`, `componentsUsed`, `score`, `whyMatch`. The payload is complete - do not call another tool just to "confirm" a result.
|
||||
2. **`get_component([...componentsUsed])`** in ONE batched call (one name or an array of up to 20) -> read each inline `api`. This **replaces** fetching docs pages. Often skippable: search responses carry `componentDigests`, a compact API contract per referenced component.
|
||||
3. **`get_examples(component)`** -> install ONE returned `c-*` example, read the added files, copy the composition.
|
||||
4. **`get_install_command(item)`** only to validate a name you are unsure of (results already include `install`). Run the install with the shadcn CLI (`--yes`).
|
||||
5. **`get_audit_checklist()`** before declaring done.
|
||||
|
||||
If you already know the exact item name, skip `search`. Everything else is situational.
|
||||
|
||||
## The 5 task-specific tools (when to reach for each)
|
||||
|
||||
- **`compose_page`** - BEFORE building any full page (dashboard, settings, billing, landing). Pass the intent (and optionally the sections you want); it returns ordered sections, each with the best premium block for the intent (top pick + alternates). Sections with no real inventory are listed honestly in `unavailableSections` - compose those from components instead of forcing a bad block.
|
||||
- **`search_icons`** - whenever you need icons, especially several. Batch up to 24 concepts in one call; each concept returns its best icons with install commands. Pass `animated: true` to get only icons that have a hover-animated Motion variant.
|
||||
- **`validate_usage`** - BEFORE writing code with component names or props you have not read in an inline `api` or an installed example. It checks planned names + props against the indexed API docs and registry item names; returns did-you-mean suggestions and per-prop documented / notDocumented verdicts. Deterministic, no inference - a notDocumented prop means stop and read the API, not push on.
|
||||
- **`whats_new`** - when your registry knowledge might be stale (a name 404s, the user mentions an item you don't know). Returns items added/removed per build, newest first.
|
||||
- **`report_issue`** - when an installed item is actually broken (bad source, wrong dependency, broken preview). Goes straight to the ReUI team; rate-limited 5/hour. Not for usage questions.
|
||||
|
||||
## All 19 tools
|
||||
|
||||
`search`, `get_block`, `get_example`, `get_icon`, `list_block_groups`, `list_block_categories`, `list_example_categories`, `list_icon_categories`, `list_components`, `get_component`, `get_examples`, `search_icons`, `compose_page`, `validate_usage`, `whats_new`, `report_issue`, `get_install_command`, `get_project_context`, `get_audit_checklist`. The MCP serves the full parameter schemas; do not guess parameters beyond them.
|
||||
|
||||
## Token + speed rules
|
||||
|
||||
- **Batch `get_component`** - ONE call with the whole `componentsUsed` array, never N calls. Skip it entirely when `componentDigests` already answers the question.
|
||||
- **Read source by installing** - the MCP serves no source. To read or analyze an item's real code, install it with the shadcn CLI and open the local files. Learn an API from the inline `api` / `componentDigests`, never by reading raw source.
|
||||
- **Infer `search` hints yourself** (`type`, `component`, `category`, `features`, `free`) - hints shrink the result set and the tokens. Keep `limit` low; one right result beats ten.
|
||||
- Run independent calls (and the shadcn install) concurrently in one turn - serial tool calls are the main source of slowness.
|
||||
- Don't repeat a search for the same intent; don't call `list_*` to "see everything" - `search` is the entry point, `list_*` is only for browsing a taxonomy the user explicitly wants to explore.
|
||||
- Prefer `get_component`'s inline `api` over `docsUrl` / `/llms.txt`. Fetch a web page only as a last resort.
|
||||
|
||||
## Result shapes (so you don't re-fetch)
|
||||
|
||||
- `score` is 0-100 RELATIVE to the top hit (the top is ~100 by construction), not absolute - compare results to each other.
|
||||
- `termCoverage` (0-1) is the share of the query the item matched - low means a weak match even if the score looks high; rephrase or widen the search.
|
||||
- Each result carries `whyMatch`, `install`, docs/preview URLs, and a `free` flag; premium items carry `requiredPlan` (`"pro"` for blocks, `"ultimate"` for icons).
|
||||
- `componentDigests` is a top-level map: a compact API contract per referenced component - often enough to wire an item without a `get_component` call.
|
||||
- Icon results and `get_icon` include `animated: true` and `installAnimated` when a hover-animated Motion variant exists (animated: `@reui/icons/animated/<style>/<name>`; static: `@reui/icons/default/<style>/<name>`).
|
||||
|
||||
## Error playbook
|
||||
|
||||
- **401** - the MCP requires a signed-in ReUI account. The client prompts "Sign in with ReUI" (OAuth) on first use; a free account is created if needed. For headless/CI, pass a personal token (`reui_pat_...`, created at https://reui.io/account/mcp) as `Authorization: Bearer`.
|
||||
- **403 / locked result** - a valid account but the plan does not cover the item: premium blocks need Pro, Motion Icons need Ultimate. Point to https://reui.io/pricing (upgrade). Free accounts still get all components + examples.
|
||||
- **429** - rate limited (120 requests/min per IP); back off, honor `Retry-After`.
|
||||
- **not found** (`found: false`) - use the returned `suggestions`, or `search`; check `whats_new` if you suspect a stale name. Never run a fabricated install command.
|
||||
|
||||
## Fallbacks
|
||||
|
||||
- No ReUI MCP: `npx shadcn@latest search @reui -q "..."` then `add` (generic, no scoring / inline API).
|
||||
- The shadcn project's own MCP also works over the `@reui` registry: https://ui.shadcn.com/docs/mcp.
|
||||
|
||||
Per-agent MCP setup: https://reui.io/docs/mcp
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"pid": 43636,
|
||||
"version": "0.9.9",
|
||||
"socketPath": "\\\\.\\pipe\\codegraph-97b92efdcc5351da",
|
||||
"startedAt": 1783570299043
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605101206}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605101204}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605138640}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605138642}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605143758}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605144457}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605145907}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605152669}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605152671}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605222064}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605222067}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605819320}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605827145}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783605864391}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783606471768}
|
||||
{"sessionId":"c10bcb","runId":"post-fix","hypothesisId":"H1","location":"badge-tabs.tsx:useEffect","message":"BadgeTabs layout metrics","data":{"flexDirection":"column","orientation":"horizontal","listLeft":280,"panelLeft":280,"stacked":true},"timestamp":1783606472923}
|
||||
@@ -0,0 +1,8 @@
|
||||
{"sessionId":"ce85d7","runId":"pre-fix","hypothesisId":"C","location":"schedule.tsx:mount","message":"schedule page data loaded","data":{"modules":0,"jobs":0,"loading":true,"modulesError":false,"jobsError":false},"timestamp":1783613834659}
|
||||
{"sessionId":"ce85d7","runId":"pre-fix","hypothesisId":"C","location":"schedule.tsx:mount","message":"schedule page data loaded","data":{"modules":4,"jobs":0,"loading":false,"modulesError":false,"jobsError":false},"timestamp":1783613834740}
|
||||
{"sessionId":"ce85d7","runId":"pre-fix","hypothesisId":"C","location":"schedule.tsx:mount","message":"schedule page data loaded","data":{"modules":0,"jobs":0,"loading":true,"modulesError":false,"jobsError":false},"timestamp":1783613854903}
|
||||
{"sessionId":"ce85d7","runId":"pre-fix","hypothesisId":"C","location":"schedule.tsx:mount","message":"schedule page data loaded","data":{"modules":4,"jobs":0,"loading":false,"modulesError":false,"jobsError":false},"timestamp":1783613854967}
|
||||
{"sessionId":"ce85d7","runId":"pre-fix","hypothesisId":"C","location":"schedule.tsx:mount","message":"schedule page data loaded","data":{"modules":0,"jobs":0,"loading":true,"modulesError":false,"jobsError":false},"timestamp":1783613897698}
|
||||
{"sessionId":"ce85d7","runId":"pre-fix","hypothesisId":"C","location":"schedule.tsx:mount","message":"schedule page data loaded","data":{"modules":4,"jobs":0,"loading":false,"modulesError":false,"jobsError":false},"timestamp":1783613897769}
|
||||
{"sessionId":"ce85d7","runId":"pre-fix","hypothesisId":"C","location":"schedule.tsx:mount","message":"schedule page data loaded","data":{"modules":4,"jobs":0,"loading":false,"modulesError":false,"jobsError":false},"timestamp":1783614164222}
|
||||
{"sessionId":"ce85d7","runId":"pre-fix","hypothesisId":"C","location":"schedule.tsx:mount","message":"schedule page data loaded","data":{"modules":4,"jobs":0,"loading":false,"modulesError":false,"jobsError":false},"timestamp":1783614181455}
|
||||
@@ -9,6 +9,12 @@
|
||||
"--path",
|
||||
"C:\\Users\\shats\\Dev\\EvoBGP"
|
||||
]
|
||||
},
|
||||
"reui": {
|
||||
"url": "https://mcp.reui.io",
|
||||
"headers": {
|
||||
"Authorization": "Bearer <REUI_LICENSE_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: Только hybrid KPI — KpiStatGrid / row tile DNA (stats-12 + IconTile). Запрет SectionCards и hand-roll.
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# KPI hybrid — только kit (stats-12 + IconTile)
|
||||
|
||||
Preview: [stats-12](https://reui.io/preview/base/stats-12). Primitive: [icon-tile](https://reui.io/docs/components/base/icon-tile). SoT DNA = EvoBGP. Markup: `apps/web/src/components/reui-kit/kpi-stat-grid.tsx`.
|
||||
|
||||
Связанные: [`reui-mcp.mdc`](reui-mcp.mdc), [`web-shadcn.mdc`](web-shadcn.mdc).
|
||||
|
||||
## MUST
|
||||
|
||||
| Зона | Компонент / DNA |
|
||||
|------|-----------------|
|
||||
| KPI-полосы / dashboard metrics | только `reui-kit/KpiStatGrid` (через `OpsDashboard` / `DetailPanel.Metrics` при наличии) |
|
||||
| Markup | horizontal compact hybrid: icon left `IconTile` `variant="elevated"` `className="size-10.5"` + label/Badge + value ± `variant` |
|
||||
| Row icon tiles (data-grid) | та же DNA — semantic `text-*` на IconTile elevated |
|
||||
| Quick Actions | только `reui-kit/QuickActionGrid` (sibling hybrid DNA, IconTile elevated) |
|
||||
|
||||
Импорты UI: `@evobgp/ui/components/*`. IconTile: `@/components/reui/icon-tile`.
|
||||
|
||||
## NEVER
|
||||
|
||||
- SectionCards / vertical-only KPI / hand-roll Frame/Card KPI
|
||||
- `Item` `size-10.5` `bg-muted` hybrid вместо IconTile
|
||||
- Другой size / radius / solid brand fill вместо `elevated`
|
||||
- `card-35` как замена stats-12 hybrid KPI
|
||||
- Копипаст ReUI block в route — adapt через `reui-kit/`
|
||||
- Голый lucide `size-4` в name-cell без IconTile
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
description: ReUI PRO (@reui) — MCP user-reui, Frame surface, kit, license, матрица выбора
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# ReUI MCP — обязательно (PRO + free)
|
||||
|
||||
Проект: **Base UI** (`style: base-nova`), surface lock **`frame`**.
|
||||
|
||||
Связанные: [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`web-shadcn.mdc`](web-shadcn.mdc), [`web-shadcn.mdc`](web-shadcn.mdc), [`docs/ui-design-contract.md`](docs/ui-design-contract.md).
|
||||
|
||||
| Документ | URL |
|
||||
|----------|-----|
|
||||
| **Introduction** | https://reui.io/docs |
|
||||
| **llms.txt** | https://reui.io/llms.txt |
|
||||
| **Get Started** | https://reui.io/docs/get-started |
|
||||
| **Styling** | https://reui.io/docs/styling |
|
||||
| **Registry** | https://reui.io/docs/registry |
|
||||
| **MCP** | https://reui.io/docs/mcp |
|
||||
| **Agent Skills** | https://reui.io/docs/agent-skills |
|
||||
| **Cursor MCP** | https://reui.io/docs/cursor |
|
||||
| **Blocks** | https://reui.io/blocks |
|
||||
| **Settings blocks** | https://reui.io/blocks/application/settings |
|
||||
| **License** | https://reui.io/docs/license-setup |
|
||||
| **Base UI components** | https://reui.io/docs/components/base/<name> |
|
||||
|
||||
## Primary MCP
|
||||
|
||||
1. **`user-reui`** — `search` / `compose_page` / `get_block` / `get_component` / `get_install_command` / `validate_usage` / `get_audit_checklist`
|
||||
2. **`plugin-shadcn-shadcn`** — primitives `@shadcn`; для `@reui` — вторично
|
||||
|
||||
**Обязательно** цитировать `previewUrl` + `docsUrl` для каждой UI-зоны.
|
||||
|
||||
**Registry (актуально):** 20 free components — `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree` ([docs](https://reui.io/docs), [MCP](https://reui.io/docs/mcp)). Skill: `.claude/skills/reui` (v `668fb463eb`); обновление: `curl.exe -fsSL https://mcp.reui.io/install | node -` из корня проекта.
|
||||
|
||||
**Важно:** skill описывает текущий registry (в т.ч. data-grid на TanStack Table v9). Установленный в проекте `@reui/data-grid` может оставаться на v8 до явного CLI upgrade — не ломать kit без миграции.
|
||||
|
||||
## Когда ReUI vs shadcn
|
||||
|
||||
| Задача | Registry | Импорт |
|
||||
|--------|----------|--------|
|
||||
| Button, Sheet, Field, Sidebar, Tabs | `@shadcn` | `@evobgp/ui/components/*` |
|
||||
| PRO pages/sections (settings, stats, auth, dashboard) | `@reui` blocks | adapt → `apps/web/src/components/` / `reui-kit/` |
|
||||
| Data Grid | `@reui` | `@/components/reui/data-grid/*` → `ResourcePage` |
|
||||
| Filters | `@reui` | `@/components/reui/filters` |
|
||||
| Frame surface | `@reui` | `@/components/reui/frame` |
|
||||
| KPI | block [stats-12](https://reui.io/preview/base/stats-12) | `reui-kit/KpiStatGrid` — см. [`kpi-hybrid.mdc`](kpi-hybrid.mdc) |
|
||||
| Quick Actions | Frame tiles sibling KPI | `reui-kit/QuickActionGrid` |
|
||||
| Semantic badge / alert | `@reui` | `@/components/reui/badge`, `@/components/reui/alert` |
|
||||
| Number / date / autocomplete / color / kanban / timeline / stepper / tree | `@reui` | `@/components/reui/*` |
|
||||
| Event Calendar / Gantt / Icon Tile (registry, 20 free) | `@reui` | CLI `@reui/<name>` → `@/components/reui/*` при установке |
|
||||
|
||||
**Сложные списки** — `ResourcePage` (Frame + data-grid + filters), не raw `<table>`, не DataGridCard.
|
||||
**Quick Actions** — только `QuickActionGrid` (не Card / Button grid).
|
||||
|
||||
## MCP workflow
|
||||
|
||||
0. Codegraph / поиск существующих `reui-kit/*`, `@/components/reui/*`
|
||||
1. `user-reui` `search` (`surface: "frame"`, `category` при известном)
|
||||
2. Страница целиком → `compose_page`; секция → `get_block`
|
||||
3. `get_component` для API primitives из `componentsUsed`
|
||||
4. CLI из `apps/web`: `pnpm dlx shadcn@latest add @reui/<name> --yes`
|
||||
5. Post-add: shadcn imports → `@evobgp/ui/components/*`
|
||||
6. Adapt by reuse → kit / route
|
||||
7. `validate_usage` + `get_audit_checklist`
|
||||
|
||||
## Размещение
|
||||
|
||||
| Слой | Путь | Импорт |
|
||||
|------|------|--------|
|
||||
| shadcn | `packages/ui/src/components/` | `@evobgp/ui/components/*` |
|
||||
| ReUI CLI | `apps/web/src/components/reui/` | `@/components/reui/*` |
|
||||
| PRO blocks (reference) | `apps/web/src/components/blocks/` | adapt into kit, не копипаст в routes |
|
||||
| Kit | `apps/web/src/components/reui-kit/` | `@/components/reui-kit/*` |
|
||||
|
||||
## Установленные ReUI (apps/web)
|
||||
|
||||
**Components:** `frame`, `data-grid/*`, `filters`, `badge`, `alert`, `autocomplete`, `number-field`, `date-selector`, `color-picker`, `timeline`, `rating`, `phone-input`, `icon-stack`, `icon-tile`, `stepper`
|
||||
|
||||
**Kit:** `ResourcePage`, `KpiStatGrid`, `QuickActionGrid`, `OpsDashboard`, `DetailPanel`, `SettingsShell`
|
||||
|
||||
**Blocks (reference):** `stats-12`, `card-35`, `auth-13`, `app-shell-12`, `settings-16`, `settings-8`, `empty-state-12`, `form-7`, `data-grid-filtering-2`, `dashboard-1`, …
|
||||
|
||||
## License
|
||||
|
||||
```env
|
||||
# .env.local (gitignored)
|
||||
REUI_LICENSE_KEY=
|
||||
```
|
||||
|
||||
`apps/web/components.json` → `@reui` с `Authorization: Bearer ${REUI_LICENSE_KEY}`.
|
||||
|
||||
## Эталоны preview
|
||||
|
||||
| Зона | Preview |
|
||||
|------|---------|
|
||||
| KPI / Quick Actions | https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-12 |
|
||||
| List | https://reui.io/preview/base/data-grid-filtering-2 |
|
||||
| Settings | https://reui.io/preview/base/settings-16 |
|
||||
| Auth | https://reui.io/preview/base/auth-13 |
|
||||
| Shell | https://reui.io/preview/base/app-shell-12 |
|
||||
| Empty | https://reui.io/preview/base/empty-state-12 |
|
||||
|
||||
## Запрещено
|
||||
|
||||
- Копипаст с reui.io без CLI
|
||||
- ReUI в `packages/ui` / импорт как `@evobgp/ui`
|
||||
- Radix-варианты docs — только Base UI
|
||||
- Raw `bg-emerald-*` вместо ReUI `variant`
|
||||
- Hand-roll data-grid/filters/KPI/Quick Actions/settings rows при наличии `@reui` / kit
|
||||
- Смешивать Card и Frame на ops-экране
|
||||
- Ставить shadcn/ui blocks выше ReUI PRO
|
||||
- **settings-7** как target (`surface: card`) — только [settings-3](https://reui.io/preview/base/settings-3) / [settings-16](https://reui.io/preview/base/settings-16)
|
||||
|
||||
## Чеклист
|
||||
|
||||
- [ ] `user-reui` search/get_block + previewUrl
|
||||
- [ ] `surface: frame`
|
||||
- [ ] CLI add из `apps/web` при новом item
|
||||
- [ ] Kit / `@/components/reui` / `@evobgp/ui` — правильный слой
|
||||
- [ ] `pnpm --filter @evobgp/web run build`
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
description: Use the ReUI registry (blocks, primitives, icons) correctly
|
||||
globs: ["**/*.tsx","**/*.ts"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
---
|
||||
name: reui
|
||||
description: Use the ReUI registry from your AI agent - find, install, and correctly use ReUI components (the 17 free building blocks like data-grid, kanban, filters), their free examples, premium blocks, and Motion Icons. Applies in any project using ReUI, the @reui registry, REUI_LICENSE_KEY, or any shadcn project where the user asks for premium blocks, data grids, kanban boards, dashboards, or full pages. Pairs with the free ReUI MCP server for live, scored registry search and inline component APIs.
|
||||
user-invocable: false
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
---
|
||||
|
||||
> **ReUI skill version `42d70dcc3d`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI for Agents
|
||||
|
||||
ReUI is a shadcn-compatible registry. It ships four things you **reuse** - never redesign:
|
||||
|
||||
- **components** - the 17 ReUI building blocks with real APIs: `data-grid`, `kanban`, `filters`, `date-selector`, `tree`, `stepper`, ... (free)
|
||||
- **examples** - free `c-*` single-pattern use-cases of a component (`c-kanban-1`); install one and read it to see exact composition
|
||||
- **blocks** - premium full-page sections that compose components (`data-grid-2`, `pricing-page-1`); Pro or Ultimate license at install
|
||||
- **icons** - Motion Icons in 4 styles, static + hover-animated variants; Ultimate license at install
|
||||
|
||||
The skill is free and this MCP is free to use; it just needs a ReUI account. On first use your agent opens a browser "Sign in with ReUI" prompt (a free account is created if you don't have one). Free covers components and examples with a daily request allowance; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the limit (see [rules/registry.md](./rules/registry.md)). The same account and skill work in every agent and service the MCP connects to - this skill is agent-agnostic.
|
||||
|
||||
Skill + MCP are a team: this skill is the workflow (how to find, install, read the API, and adapt by reuse); the MCP is the live data and the hands (search, get_component, install commands). Your job: find the right item, install it with the shadcn CLI, read its real API, and **adapt by reuse** - wire real data and theme it; do not hand-roll or restyle what ReUI already provides. This skill **layers on the shadcn skill**: follow that for generic rules (spacing, `cn()`, semantic colors, forms); follow this for everything ReUI-specific.
|
||||
|
||||
## The core loop (MCP-native)
|
||||
|
||||
1. **Find** - call the ReUI MCP `search` tool with the user's intent. It returns a ranked, scored list across components/examples/blocks/icons, each with an `install` command, `previewUrl`, `docsUrl`, and `componentsUsed`. Pass hints (`type`, `component`, `category`, `features`, `free`) when you can infer them.
|
||||
2. **Install** - run the returned command non-interactively (`npx shadcn@latest add @reui/<name> --yes`). The CLI resolves deps, aliases, and the base/style from `components.json`. See [cli.md](./rules/cli.md).
|
||||
3. **Read the API (on your base)** - first note your base from `components.json` -> `style` (`base-nova` -> Base UI, `radix-nova` -> Radix UI). For each component an item uses, call `get_component(name)` and read its **inline `api`** (no web fetch); then `get_examples(name)` to install a worked example and copy its composition - the installed files are already in your base. Whenever you work with a component's API, also **share its `docsUrl`** (the primitive's API documentation page) with the user so they have the full reference. See [components.md](./rules/components.md).
|
||||
4. **Adapt (reuse-first)** - swap demo data for real data, fix icon imports, align tokens. Do not redesign. See [adapting.md](./rules/adapting.md).
|
||||
|
||||
**Always show the preview.** Every item a tool returns carries a `previewUrl` (a live preview page). Whenever you list, recommend, or present ReUI items to the user - blocks, components, examples, or icons, whether from `search`, `search_icons`, `list_components`, `compose_page`, or any getter - include each item's `previewUrl` so they can SEE it before installing. Blocks and examples open an individual live preview; icons and components link to their live category/component page. Never present an item without its preview link.
|
||||
|
||||
If the ReUI MCP is not configured, fall back to `npx shadcn@latest search @reui -q "..."` then `add` - but the MCP gives scored matches + inline APIs; prefer it.
|
||||
|
||||
## Commands
|
||||
|
||||
Run ReUI as explicit slash commands (via the ReUI MCP) **or** just ask in plain language - both run the same workflow.
|
||||
|
||||
| Command | Invoke | Does |
|
||||
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **build** | `/mcp__reui__build <what>` | Compose a page/section/feature from ReUI: plan → install → read API → adapt → craft → audit. |
|
||||
| **add** | `/mcp__reui__add <item>` | Find & install one component/example/block/icon and wire it in. |
|
||||
| **fix** | `/mcp__reui__fix [target]` | Diagnose & fix ReUI usage: wrong/undocumented props, base/radix mismatch, missing states, a11y/scroll. |
|
||||
| **improve** | `/mcp__reui__improve [target]` | Refine + extend existing ReUI UI to a production-exceptional bar (hierarchy, density, states, responsive, motion). |
|
||||
|
||||
Invocation differs slightly per agent (`/mcp__reui__build` in Claude Code/Cursor/Windsurf, `/mcp.reui.build` in VS Code). No command surface? Just describe what you want - this skill drives the identical loop.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
| Need | Reach for |
|
||||
| -------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| A full page or section (dashboard, billing, auth, pricing, settings) | `compose_page` first (plans sections + best blocks), then ReUI **blocks** |
|
||||
| A data table with sorting/filtering/pagination/virtualization | the **data-grid** component (never hand-roll a `<table>`) |
|
||||
| A drag-and-drop board | the **kanban** component |
|
||||
| Advanced column filtering, date range, tree, stepper, ... | the matching ReUI **component** |
|
||||
| A single generic control already in shadcn (Button, Dialog, Select) | plain **shadcn** |
|
||||
|
||||
## Detailed references
|
||||
|
||||
- [rules/registry.md](./rules/registry.md) - the four types, the @reui registry, base/radix, free vs premium + license
|
||||
- [rules/workflow.md](./rules/workflow.md) - the find -> install -> read-API -> adapt loop (most important)
|
||||
- [rules/components.md](./rules/components.md) - the 17 components, the data-grid contract, base vs radix
|
||||
- [rules/adapting.md](./rules/adapting.md) - reuse-first: preserve the design (no over-customizing), reuse examples + a block's own elements, real data, don't invent APIs
|
||||
- [rules/craft.md](./rules/craft.md) - make it exceptional: point of view, hierarchy, density, states, responsive, motion, the bar
|
||||
- [rules/quality.md](./rules/quality.md) - security, accessibility, and scroll gates (the done gate)
|
||||
- [rules/styling.md](./rules/styling.md) - ReUI extended tokens, theme adaptation, density
|
||||
- [rules/icons.md](./rules/icons.md) - portable icons, swapping imports, Motion Icons (static + animated)
|
||||
- [tools.md](./tools.md) - the ReUI MCP: golden path, the 19 tools, token rules, result shapes, errors
|
||||
@@ -9,12 +9,12 @@ alwaysApply: false
|
||||
# Web UI — React + shadcn/ui + ReUI
|
||||
|
||||
**Источники правды:**
|
||||
- **ReUI PRO first:** MCP `user-reui` ([`reui-mcp.mdc`](reui-mcp.mdc)) — pages / KPI / lists / settings / shell
|
||||
- shadcn primitives: MCP `plugin-shadcn-shadcn` (secondary)
|
||||
- ReUI Base UI: https://reui.io/docs/components/base/<name> · [llms.txt](https://reui.io/llms.txt)
|
||||
- shadcn/ui React: https://ui.shadcn.com/docs/components
|
||||
- ReUI Base UI: https://reui.io/docs/components/base/<name>
|
||||
- ReUI llms.txt: https://reui.io/llms.txt
|
||||
- MCP `plugin-shadcn-shadcn` (registries: `@shadcn`, `@reui`) — перед любой UI-задачей
|
||||
|
||||
Общие правила Go/API: `.cursor/rules/engineering.mdc`. Стек ID: `.cursor/rules/context7-stack.mdc`.
|
||||
Иерархия: **ReUI PRO > shadcn**. Общие: `.cursor/rules/engineering.mdc`, `context7-stack.mdc`.
|
||||
|
||||
## Слои UI
|
||||
|
||||
@@ -22,16 +22,19 @@ alwaysApply: false
|
||||
|------|------|------------|
|
||||
| shadcn-примитивы | `packages/ui/src/components/` | output `shadcn add` (не трогать под кейс) |
|
||||
| ReUI enterprise | `apps/web/src/components/reui/` | output `shadcn add @reui/*` |
|
||||
| Shared обёртки | `apps/web/src/components/` | PageHeader, QueryState, ConfirmDialog, StatusBadge, SectionCards, LoadingButton |
|
||||
| Shared обёртки | `apps/web/src/components/` | PageHeader, QueryState, ConfirmDialog, StatusBadge, LoadingButton |
|
||||
| ReUI kit | `apps/web/src/components/reui-kit/` | ResourcePage, KpiStatGrid, QuickActionGrid, OpsDashboard, SettingsShell |
|
||||
| Роуты | `apps/web/src/routes/` | TanStack Router (file-based) |
|
||||
|
||||
**Design contract:** [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md). Surface: **frame**. KPI hybrid SoT: [stats-12](https://reui.io/preview/base/stats-12). Lists: [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2). Quick Actions: `QuickActionGrid`.
|
||||
|
||||
Тема: `packages/ui/src/styles/globals.css`. CLI из `apps/web`: `pnpm dlx shadcn@latest add <component>`.
|
||||
|
||||
## Правила
|
||||
|
||||
**WEB-01** | MUST | Перед новым UI — MCP `plugin-shadcn-shadcn`: `search_items_in_registries` → `get_item_examples_from_registries` → `get_add_command_for_items`. Только после — JSX.
|
||||
*Rationale:* единый источник правды и API.
|
||||
*Проверка:* review; нет самописных примитивов, если есть registry item.
|
||||
**WEB-01** | MUST | Перед новым UI — сначала MCP **`user-reui`** (`search` → `get_block` / `compose_page`, `surface: "frame"`) + cite `previewUrl`/`docsUrl`. Primitives — MCP `plugin-shadcn-shadcn`. Только после — JSX.
|
||||
*Rationale:* ReUI PRO выше shadcn; единый Frame surface.
|
||||
*Проверка:* review; [`reui-mcp.mdc`](reui-mcp.mdc).
|
||||
|
||||
**WEB-02** | MUST | Отсутствующий shadcn-примитив — `pnpm dlx shadcn@latest add <component>` (из `apps/web`). ReUI — `pnpm dlx shadcn@latest add @reui/<name>`.
|
||||
*Проверка:* файлы в `packages/ui/src/components/` (для shadcn) или `apps/web/src/components/reui/` (для ReUI).
|
||||
@@ -45,8 +48,8 @@ alwaysApply: false
|
||||
**WEB-05** | MUST | Формы — `react-hook-form` + Zod; через `FormField`/`Form` обёртки.
|
||||
*Проверка:* https://ui.shadcn.com/docs/components/form
|
||||
|
||||
**WEB-06** | MUST | Сложные data-списки — ReUI `DataGridCard` (ReUI data-grid, не shadcn Data Table). Простые списки — shadcn `Table`.
|
||||
*Проверка:* `@/components/reui/data-grid` или `@evobgp/ui/components/table`.
|
||||
**WEB-06** | MUST | Сложные data-списки — Frame + ReUI DataGrid через `reui-kit/ResourcePage` или `DataGridSection` (не Card shell, не shadcn Data Table). Surface lock: `UI_SURFACE = 'frame'` (`lib/ui-surface.ts`). Простые списки — shadcn `Table`.
|
||||
*Проверка:* `@/components/reui-kit`, `@/components/reui/data-grid`, `docs/ui-design-contract.md`.
|
||||
|
||||
**WEB-07** | MUST | Toast — `sonner` (`Toaster` в `main.tsx`); `toast.success/error/message` из `sonner`.
|
||||
*Проверка:* https://ui.shadcn.com/docs/components/sonner
|
||||
@@ -70,7 +73,7 @@ alwaysApply: false
|
||||
|
||||
**WEB-14** | SHOULD | Нетривиальный UI — прочитать страницу компонента shadcn/ReUI (props, a11y).
|
||||
|
||||
**WEB-15** | MUST | Сомнения — MCP `plugin-shadcn-shadcn` + shadcn CLI docs + `pnpm --filter @evobgp/web run typecheck`.
|
||||
**WEB-15** | MUST | Сомнения — MCP `user-reui` + `plugin-shadcn-shadcn` + docs + `pnpm --filter @evobgp/web run typecheck`.
|
||||
|
||||
**WEB-16** | MUST | Подтверждение удаления — `ConfirmDialog` из `@/components/confirm-dialog`, не `window.confirm`.
|
||||
|
||||
@@ -95,7 +98,11 @@ pnpm --filter @evobgp/web run build
|
||||
**WEB-21** | MUST | Data fetching — TanStack Query (`useQuery`, `useMutation`, `queryOptions`); query-key factories в `apps/web/src/queries/`. Mutations invalidate keys, не refetch вручную.
|
||||
*Проверка:* review `queries/*.ts`.
|
||||
|
||||
**WEB-22** | MUST | Legacy Svelte — в `web-legacy-svelte/` (archive). Не использовать импорты оттуда в новом коде; только как референс при миграции роутов.
|
||||
**WEB-22** | NEVER | Legacy Svelte UI удалён. Не восстанавливать `web-legacy-svelte/` и не копировать Svelte-паттерны в React-код.
|
||||
*Проверка:* отсутствие каталога `web-legacy-svelte/`; `pnpm --filter @evobgp/web run typecheck`.
|
||||
|
||||
**WEB-23** | NEVER | **settings-7** как target ([preview](https://reui.io/preview/base/settings-7) — `surface: card`). Новые settings-экраны — [settings-3](https://reui.io/preview/base/settings-3) / [settings-16](https://reui.io/preview/base/settings-16) (`surface: frame`). Существующий `apps/web/src/components/settings/*` на DNA settings-7 **не** копировать как эталон; миграция — отдельный CHANGE.
|
||||
*Проверка:* review MCP `get_block` / cite; нет новых экранов с settings-7.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: reui
|
||||
description: Use the ReUI registry from your AI agent - find, install, and correctly use ReUI components (the 20 free building blocks like data-grid, kanban, filters), their free examples, premium blocks, and Motion Icons. Applies in any project using ReUI, the @reui registry, REUI_LICENSE_KEY, or any shadcn project where the user asks for premium blocks, data grids, kanban boards, dashboards, or full pages. Pairs with the free ReUI MCP server for live, scored registry search and inline component APIs.
|
||||
user-invocable: false
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
---
|
||||
|
||||
> **ReUI skill version `668fb463eb`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI for Agents
|
||||
|
||||
ReUI is a shadcn-compatible registry. It ships four things you **reuse** - never redesign:
|
||||
|
||||
- **components** - the 20 ReUI building blocks with real APIs: `data-grid`, `kanban`, `filters`, `date-selector`, `tree`, `stepper`, ... (free)
|
||||
- **examples** - free `c-*` single-pattern use-cases of a component (`c-kanban-1`); install one and read it to see exact composition
|
||||
- **blocks** - premium full-page sections that compose components (`data-grid-2`, `pricing-page-1`); Pro or Ultimate license at install
|
||||
- **icons** - Motion Icons in 4 styles, static + hover-animated variants; Ultimate license at install
|
||||
|
||||
The skill is free and this MCP is free to use; it just needs a ReUI account. On first use your agent opens a browser "Sign in with ReUI" prompt (a free account is created if you don't have one). Free covers components and examples with a daily request allowance; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the limit (see [rules/registry.md](./rules/registry.md)). The same account and skill work in every agent and service the MCP connects to - this skill is agent-agnostic.
|
||||
|
||||
Skill + MCP are a team: this skill is the workflow (how to find, install, read the API, and adapt by reuse); the MCP is the live data and the hands (search, get_component, install commands). Your job: find the right item, install it with the shadcn CLI, read its real API, and **adapt by reuse** - wire real data and theme it; do not hand-roll or restyle what ReUI already provides. This skill **layers on the shadcn skill**: follow that for generic rules (spacing, `cn()`, semantic colors, forms); follow this for everything ReUI-specific.
|
||||
|
||||
## The core loop (MCP-native)
|
||||
|
||||
1. **Find** - call the ReUI MCP `search` tool with the user's intent. It returns a ranked, scored list across components/examples/blocks/icons, each with an `install` command, `previewUrl`, `docsUrl`, and `componentsUsed`. Pass hints (`type`, `component`, `category`, `features`, `free`) when you can infer them.
|
||||
2. **Install** - run the returned command non-interactively (`npx shadcn@latest add @reui/<name> --yes`). The CLI resolves deps, aliases, and the base/style from `components.json`. See [cli.md](./rules/cli.md).
|
||||
3. **Read the API (on your base)** - first note your base from `components.json` -> `style` (`base-nova` -> Base UI, `radix-nova` -> Radix UI). For each component an item uses, call `get_component(name)` and read its **inline `api`** (no web fetch); then `get_examples(name)` to install a worked example and copy its composition - the installed files are already in your base. Whenever you work with a component's API, also **share its `docsUrl`** (the primitive's API documentation page) with the user so they have the full reference. See [components.md](./rules/components.md).
|
||||
4. **Adapt (reuse-first)** - swap demo data for real data, fix icon imports, align tokens. Do not redesign. See [adapting.md](./rules/adapting.md).
|
||||
|
||||
**Always show the preview.** Every item a tool returns carries a `previewUrl` (a live preview page). Whenever you list, recommend, or present ReUI items to the user - blocks, components, examples, or icons, whether from `search`, `search_icons`, `list_components`, `compose_page`, or any getter - include each item's `previewUrl` so they can SEE it before installing. Blocks and examples open an individual live preview; icons and components link to their live category/component page. Never present an item without its preview link.
|
||||
|
||||
If the ReUI MCP is not configured, fall back to `npx shadcn@latest search @reui -q "..."` then `add` - but the MCP gives scored matches + inline APIs; prefer it.
|
||||
|
||||
## Commands
|
||||
|
||||
Run ReUI as explicit slash commands (via the ReUI MCP) **or** just ask in plain language - both run the same workflow.
|
||||
|
||||
| Command | Invoke | Does |
|
||||
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **build** | `/mcp__reui__build <what>` | Compose a page/section/feature from ReUI: plan → install → read API → adapt → craft → audit. |
|
||||
| **add** | `/mcp__reui__add <item>` | Find & install one component/example/block/icon and wire it in. |
|
||||
| **fix** | `/mcp__reui__fix [target]` | Diagnose & fix ReUI usage: wrong/undocumented props, base/radix mismatch, missing states, a11y/scroll. |
|
||||
| **improve** | `/mcp__reui__improve [target]` | Refine + extend existing ReUI UI to a production-exceptional bar (hierarchy, density, states, responsive, motion). |
|
||||
|
||||
Invocation differs slightly per agent (`/mcp__reui__build` in Claude Code/Cursor/Windsurf, `/mcp.reui.build` in VS Code). No command surface? Just describe what you want - this skill drives the identical loop.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
| Need | Reach for |
|
||||
| -------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| A full page or section (dashboard, billing, auth, pricing, settings) | `compose_page` first (plans sections + best blocks), then ReUI **blocks** |
|
||||
| A data table with sorting/filtering/pagination/virtualization | the **data-grid** component (never hand-roll a `<table>`) |
|
||||
| A drag-and-drop board | the **kanban** component |
|
||||
| Advanced column filtering, date range, tree, stepper, ... | the matching ReUI **component** |
|
||||
| A single generic control already in shadcn (Button, Dialog, Select) | plain **shadcn** |
|
||||
|
||||
## Detailed references
|
||||
|
||||
- [rules/registry.md](./rules/registry.md) - the four types, the @reui registry, base/radix, free vs premium + license
|
||||
- [rules/workflow.md](./rules/workflow.md) - the find -> install -> read-API -> adapt loop (most important)
|
||||
- [rules/components.md](./rules/components.md) - the 20 components, the data-grid contract, base vs radix
|
||||
- [rules/adapting.md](./rules/adapting.md) - reuse-first: preserve the design (no over-customizing), reuse examples + a block's own elements, real data, don't invent APIs
|
||||
- [rules/craft.md](./rules/craft.md) - make it exceptional: point of view, hierarchy, density, states, responsive, motion, the bar
|
||||
- [rules/quality.md](./rules/quality.md) - security, accessibility, and scroll gates (the done gate)
|
||||
- [rules/styling.md](./rules/styling.md) - ReUI extended tokens, theme adaptation, density
|
||||
- [rules/icons.md](./rules/icons.md) - portable icons, swapping imports, Motion Icons (static + animated)
|
||||
- [tools.md](./tools.md) - the ReUI MCP: golden path, the 19 tools, token rules, result shapes, errors
|
||||
@@ -0,0 +1,43 @@
|
||||
# Adapting installed ReUI code (reuse-first, no AI slop)
|
||||
|
||||
ReUI items ship production-quality. Your job is to **adapt by reuse** - wire real data and fit the app - not to redesign or hand-roll. The output should look like ReUI built it for this product.
|
||||
|
||||
## Preserve the design - don't over-customize
|
||||
|
||||
The design IS the product. A ReUI block/component encodes senior-designer decisions: spacing, hierarchy, density, color treatment, and component choices. The fastest way to turn a premium block back into generic AI slop is to "improve" its look - so don't.
|
||||
|
||||
- Change **data, copy, and props**; keep the **structure and styling** it ships with. Make the **smallest** change that wires the real data. If your diff touches `className` / JSX structure more than data / props, you are over-customizing - stop and reuse.
|
||||
- Don't swap ReUI components for hand-rolled ones, restructure the layout, re-skin spacing / radius / colors, or add decorative chrome. Let the installed components carry the default spacing, radius, sizing, icon rhythm, density, and state styling; add custom Tailwind only when a component genuinely lacks a contract you need.
|
||||
- Want a different look? `search` for a block whose design already fits and reuse that - don't restyle this one into a new design.
|
||||
|
||||
## Reuse the parts: examples and the block's own elements
|
||||
|
||||
- **Examples are building parts.** A free `c-*` example is a correct, single-pattern composition you can reuse. Before composing from scratch, `get_examples(component)`, install the closest one, and reuse its wiring - assemble UI from examples instead of hand-rolling what an example already shows.
|
||||
- **Reuse a block's own elements.** Need more rows, cards, items, or sections than ship by default? Repeat the block's **existing** element by mapping real data through the same markup - never invent parallel markup that drifts from its design. Need a variant (empty / loading / expanded)? Derive it from an element the block already has.
|
||||
|
||||
## Don't invent (read, don't guess)
|
||||
|
||||
- Never write a prop, variant value, import path, or `@reui/...` name you didn't read in a component's inline `api`, an installed example, or a `search` result. If you didn't see it, treat it as nonexistent - call `get_component` / `get_examples` / `search` first, or run the MCP `validate_usage` tool to check planned names + props against the docs before writing code.
|
||||
- If a getter returns `found: false` or `search` returns nothing, say so and fall back (plain shadcn, or ask) - never fabricate an install command or an API.
|
||||
|
||||
## What to change vs leave alone
|
||||
|
||||
- **Change:** the item's own data, copy, props, and layout to fit the app.
|
||||
- **Leave alone:** installed component files, hooks, and the shared theme - do not edit vendored ReUI internals; change behavior through props and the documented API.
|
||||
- Blocks are **portable React** - no `next/link`, `next/image`, or other framework-runtime imports inside them. Keep them portable.
|
||||
|
||||
## Demo data -> real data
|
||||
|
||||
- Replace every placeholder with the user's real data. Model it as **typed data structures** and **map over arrays** - never duplicate JSX per row/card. Keep small block-specific formatters next to the data.
|
||||
- Wire the real source (columns, fields, fetch). For `data-grid`, implement the server fetch contract if the user needs server-side data.
|
||||
- **Type from the component API, derive during render.** Type domain state through the component's own types - e.g. map status to `BadgeProps["variant"]` via a typed `Record<Status, …>` - instead of stringly-typed values. Compute view state during render; don't mirror derived data into `useState`/`useEffect`.
|
||||
- **Adapt on the right base.** Use the API for the project's base (Base UI vs Radix - see [components.md](./components.md)); the installed files are already base-correct, so reuse their shape rather than translating from memory.
|
||||
|
||||
## Believable content (no AI tells)
|
||||
|
||||
- Use realistic labels, counts, timestamps, and statuses that map to a real workflow.
|
||||
- No decorative buttons, fake tabs, meaningless toggles, equal-weight card walls, empty gradients, ornamental icons, or generic SaaS filler. Every element should do something.
|
||||
|
||||
## Operational surfaces (settings / profile / admin)
|
||||
|
||||
Pick ONE archetype and keep the family consistent: a vertical rail (3-6 sections), horizontal tabs (5-8), or a frame/stack. Prefer `frame` for tool-like surfaces, a card for profile-like ones. Don't mix archetypes in one surface.
|
||||
@@ -0,0 +1,60 @@
|
||||
# CLI: registry setup, license, non-interactive install
|
||||
|
||||
## Registry setup (one-time, per project)
|
||||
|
||||
Free items (the 20 components and all `c-*` examples) need only the plain string registry in `components.json`:
|
||||
|
||||
```json
|
||||
{ "registries": { "@reui": "https://reui.io/r/{style}/{name}.json" } }
|
||||
```
|
||||
|
||||
Premium items (blocks; Motion Icons and templates) require a ReUI license at install:
|
||||
|
||||
1. Add the key to `.env.local`:
|
||||
|
||||
```bash
|
||||
REUI_LICENSE_KEY=your-license-key
|
||||
```
|
||||
|
||||
2. Switch `components.json` to the authenticated object form:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${REUI_LICENSE_KEY}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`, but an MCP client config never expands variables, so a ReUI MCP server config must carry the raw token instead (for example `reui_pat_your_token_here`).
|
||||
|
||||
The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry
|
||||
|
||||
## Installing
|
||||
|
||||
Use the project's package runner (check `packageManager`):
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/<name> --yes # npm
|
||||
pnpm dlx shadcn@latest add @reui/<name> --yes # pnpm
|
||||
bunx --bun shadcn@latest add @reui/<name> --yes # bun
|
||||
```
|
||||
|
||||
`--yes` skips confirmation prompts. The CLI auto-detects the package manager from the lockfile (there is no `--package-manager` flag). It also resolves the correct base+style variant from `components.json`, so do not pass a style.
|
||||
|
||||
## Handling prompts and conflicts
|
||||
|
||||
- **Always pass `--yes`** so the CLI does not block on confirmation prompts.
|
||||
- **Do NOT pass `--overwrite` by default.** If the CLI reports an existing file, read the output and resolve deliberately: install under a different name, adjust the path, or ask the user. Only use `--overwrite` when the user explicitly wants to replace a file.
|
||||
- **Preview first when touching an existing project**: `npx shadcn@latest add @reui/<name> --dry-run` shows what would change; `--diff <file>` shows a specific file's diff. Use these before overwriting.
|
||||
- Run from the **project root** so `components.json` and `.env.local` are found.
|
||||
|
||||
## Free vs premium boundary
|
||||
|
||||
- Public, no key: `c-*` examples and the 20 components (`@reui/data-grid`, `@reui/badge`, ...) that those examples depend on.
|
||||
- Key required at install: blocks (`@reui/<category>-N`) need a Pro or Ultimate license; Motion Icons (`@reui/icons/...`) and templates need Ultimate.
|
||||
|
||||
If an install 401/403s, the license key is missing, invalid, or the plan does not cover that resource (blocks: Pro or higher; icons and templates: Ultimate). Point the user to https://reui.io/account (their key) or https://reui.io/pricing (upgrade).
|
||||
@@ -0,0 +1,370 @@
|
||||
# ReUI components
|
||||
|
||||
The 20 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these.
|
||||
|
||||
**Rule one: never guess a component's API. Read it first.** Call **`get_component(name)`** for its inline `api` (props + usage, no web fetch), and **share the result's `docsUrl`** (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the `/llms.txt` index is a further fallback). Then call **`get_examples(name)`** to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline `api` is the full reference. No single block fits? Compose: search the components you need, read each `get_component`, install a `get_examples` example per component, and adapt.
|
||||
|
||||
## data-grid (the flagship - read its API every time)
|
||||
|
||||
`data-grid` wraps TanStack Table v9. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
|
||||
|
||||
- Build a TanStack table instance with `useTable({ features: dataGridFeatures, ... })` (columns, data). `dataGridFeatures` is exported by the primitive and already bundles sorting, filtering, pagination, row selection, expanding, pinning, resizing and faceting, so there are no per-table row models to wire.
|
||||
- Pass that instance to `<DataGrid table={table} recordCount={total}>`.
|
||||
- Compose the body with `DataGridTable` inside `DataGrid`, and enable features through `tableLayout` (e.g. `{ headerSticky: true, columnsResizable: true }`), not ad-hoc classes.
|
||||
- Server-side data uses the documented fetch shape (`recordCount` is the total for pagination).
|
||||
|
||||
```tsx
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
data,
|
||||
columns,
|
||||
})
|
||||
|
||||
<DataGrid table={table} recordCount={data.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
Common mistakes:
|
||||
|
||||
- **Incorrect:** `<DataGrid data={rows} columns={cols} />` - these props do not exist. **Correct:** build a `useTable({ features: dataGridFeatures, ... })` instance and pass `table={table}` + `recordCount`.
|
||||
- **Incorrect:** a raw `<table>` / hand-rolled pagination. **Correct:** use `data-grid`; read its API for sticky header, pagination, virtualization, row selection.
|
||||
- **Incorrect:** styling rows/cells with arbitrary classes. **Correct:** drive layout via `tableLayout` and the primitive's `DataGridColumnMeta` (e.g. `cellClassName`, `headerTitle`), set through the bundle's `columnMeta` slot.
|
||||
|
||||
## event-calendar
|
||||
|
||||
**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EventCalendar defaultEvents={events} defaultView="month" className="h-[560px]">
|
||||
<EventCalendarNav />
|
||||
<EventCalendarContent />
|
||||
</EventCalendar>
|
||||
```
|
||||
|
||||
**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column.
|
||||
|
||||
## gantt
|
||||
|
||||
**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Gantt defaultEvents={bars} resources={tasks} defaultScale="month" className="h-[480px]">
|
||||
<GanttNav />
|
||||
<GanttView />
|
||||
</Gantt>
|
||||
```
|
||||
|
||||
**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height.
|
||||
|
||||
## kanban
|
||||
|
||||
**Required:** `value` (`Record<string, T[]>`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
|
||||
<KanbanBoard>
|
||||
{Object.entries(cols).map(([id, items]) => (
|
||||
<KanbanColumn key={id} value={id}>
|
||||
<KanbanColumnHandle><h3>{id}</h3></KanbanColumnHandle>
|
||||
<KanbanColumnContent value={id}>
|
||||
{items.map((i) => (
|
||||
<KanbanItem key={i.id} value={i.id}>
|
||||
<KanbanItemHandle>{i.title}</KanbanItemHandle>
|
||||
</KanbanItem>
|
||||
))}
|
||||
</KanbanColumnContent>
|
||||
</KanbanColumn>
|
||||
))}
|
||||
</KanbanBoard>
|
||||
<KanbanOverlay><div className="bg-muted size-full rounded-md" /></KanbanOverlay>
|
||||
</Kanban>
|
||||
```
|
||||
|
||||
**Gotcha:** state is `Record<columnId, T[]>`. Each `KanbanColumnContent value` must match its parent `KanbanColumn value`. Omit `KanbanOverlay` and the drag preview silently breaks.
|
||||
|
||||
## sortable
|
||||
|
||||
**Required:** `value` (`T[]`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Sortable value={items} onValueChange={setItems} getItemValue={(i) => i.id}>
|
||||
{items.map((i) => (
|
||||
<SortableItem key={i.id} value={i.id}>
|
||||
<SortableItemHandle><GripVertical /></SortableItemHandle>
|
||||
{i.label}
|
||||
</SortableItem>
|
||||
))}
|
||||
</Sortable>
|
||||
```
|
||||
|
||||
**Gotcha:** a flat 1D reorder list (not columns - that is `kanban`). `getItemValue` must return a stable, unique string. Pass `layout="grid"` or `layout="nested"` for non-list layouts.
|
||||
|
||||
## filters
|
||||
|
||||
**Required:** `filters` (`Filter[]`), `fields` (`FilterFieldConfig[]`), `onChange`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const [filters, setFilters] = useState<Filter[]>([
|
||||
createFilter("priority", "is_any_of", ["low"]),
|
||||
])
|
||||
const fields: FilterFieldConfig[] = [
|
||||
{ key: "priority", label: "Priority", type: "multiselect",
|
||||
options: [{ value: "low", label: "Low" }, { value: "high", label: "High" }] },
|
||||
]
|
||||
|
||||
<Filters filters={filters} fields={fields} onChange={setFilters} />
|
||||
```
|
||||
|
||||
**Gotcha:** always build initial filters with `createFilter(field, operator, values)` - it generates the required `id`. Never hand-construct a `Filter` object. Pairs naturally with `data-grid`.
|
||||
|
||||
## date-selector
|
||||
|
||||
**Required:** none, but wire `onChange` to capture the value.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const [value, setValue] = useState<DateSelectorValue | undefined>()
|
||||
|
||||
<DateSelector value={value} onChange={setValue} label="Due date" />
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a structured `DateSelectorValue` (period / operator / start+end dates), NOT a `Date` - never pass a raw `Date`. Use `allowRange={false}` to lock single-date picking. Read `get_component("date-selector")` for the value shape.
|
||||
|
||||
## tree
|
||||
|
||||
**Required:** `tree` (a `@headless-tree/core` instance you construct)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Tree tree={tree}>
|
||||
{tree.getItems().map((item) => (
|
||||
<TreeItem key={item.getId()} item={item}>
|
||||
<TreeItemLabel />
|
||||
</TreeItem>
|
||||
))}
|
||||
</Tree>
|
||||
```
|
||||
|
||||
**Gotcha:** `Tree` is a styled shell - it takes a headless-tree instance via `tree`, NOT `data`/`items` props. Build the instance with `@headless-tree/react`. External API: https://headless-tree.lukasbach.com/
|
||||
|
||||
## stepper
|
||||
|
||||
**Required:** `StepperItem step` (number), `StepperContent value` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Stepper defaultValue={1}>
|
||||
<StepperNav>
|
||||
<StepperItem step={1}>
|
||||
<StepperTrigger><StepperIndicator>1</StepperIndicator></StepperTrigger>
|
||||
<StepperSeparator />
|
||||
</StepperItem>
|
||||
<StepperItem step={2}>
|
||||
<StepperTrigger><StepperIndicator>2</StepperIndicator></StepperTrigger>
|
||||
</StepperItem>
|
||||
</StepperNav>
|
||||
<StepperPanel>
|
||||
<StepperContent value={1}>Step 1 content</StepperContent>
|
||||
<StepperContent value={2}>Step 2 content</StepperContent>
|
||||
</StepperPanel>
|
||||
</Stepper>
|
||||
```
|
||||
|
||||
**Gotcha:** steps are 1-indexed. Without `StepperPanel` + `StepperContent` you render the nav trail but no body. Put `StepperSeparator` in every `StepperItem` except the last.
|
||||
|
||||
## timeline
|
||||
|
||||
**Required:** `TimelineItem step` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Timeline>
|
||||
<TimelineItem step={1}>
|
||||
<TimelineHeader>
|
||||
<TimelineDate>March 2024</TimelineDate>
|
||||
<TimelineTitle>Project initialized</TimelineTitle>
|
||||
</TimelineHeader>
|
||||
<TimelineIndicator />
|
||||
<TimelineSeparator />
|
||||
<TimelineContent>Repo and architecture set up.</TimelineContent>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
```
|
||||
|
||||
**Gotcha:** each item needs a unique `step`. `orientation` is `"vertical"` (default) or `"horizontal"`. This is a static event display, not interactive like `stepper`.
|
||||
|
||||
## autocomplete
|
||||
|
||||
**Required:** `items` (array; each item has at least `value`)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Autocomplete items={items}>
|
||||
<AutocompleteInput placeholder="Search..." />
|
||||
<AutocompleteContent>
|
||||
<AutocompleteEmpty>No results found.</AutocompleteEmpty>
|
||||
<AutocompleteList>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value} value={item}>{item.label}</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
</Autocomplete>
|
||||
```
|
||||
|
||||
**Gotcha:** `AutocompleteList` takes a render-prop `(item) => ReactNode`, NOT a mapped array of children. External API: https://base-ui.com/react/components/autocomplete
|
||||
|
||||
## phone-input
|
||||
|
||||
**Required:** none, but wire `onChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<PhoneInput placeholder="Enter phone number" defaultCountry="US" value={value} onChange={setValue} />
|
||||
```
|
||||
|
||||
**Gotcha:** `value`/`onChange` use an E.164 string (e.g. `"+14155551234"`), not a display-formatted string; `onChange` can fire `undefined`. `defaultCountry` is a 2-letter ISO code. Wraps `react-phone-number-input`.
|
||||
|
||||
## number-field
|
||||
|
||||
**Required:** wrap the controls in `NumberFieldGroup`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<NumberField defaultValue={0}>
|
||||
<NumberFieldScrubArea label="Quantity" />
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldDecrement />
|
||||
<NumberFieldInput />
|
||||
<NumberFieldIncrement />
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
```
|
||||
|
||||
**Gotcha:** import from `@/components/ui/number-field`. The accessible label goes on `NumberFieldScrubArea`, not `NumberField`. External API: https://base-ui.com/react/components/number-field
|
||||
|
||||
## rating
|
||||
|
||||
**Required:** `rating` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Rating rating={4.5} showValue editable onRatingChange={setRating} />
|
||||
```
|
||||
|
||||
**Gotcha:** supports decimals (partial stars). Pass `editable` + `onRatingChange` for interactive input; omit both for a read-only display.
|
||||
|
||||
## scrollspy
|
||||
|
||||
**Required:** `targetRef` (the scroll container ref)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Scrollspy targetRef={containerRef}>
|
||||
<a href="#s1" data-scrollspy-anchor="s1">Section 1</a>
|
||||
<a href="#s2" data-scrollspy-anchor="s2">Section 2</a>
|
||||
</Scrollspy>
|
||||
<div ref={containerRef}>
|
||||
<div id="s1">...</div>
|
||||
<div id="s2">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Gotcha:** each link's `data-scrollspy-anchor` must match a section `id`. `targetRef` is the scrollable container (defaults to the window).
|
||||
|
||||
## frame
|
||||
|
||||
**Required:** `Frame` > `FramePanel`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Frame>
|
||||
<FramePanel>
|
||||
<FrameHeader>
|
||||
<FrameTitle>Title</FrameTitle>
|
||||
<FrameDescription>Description</FrameDescription>
|
||||
</FrameHeader>
|
||||
<div className="p-5">Content</div>
|
||||
<FrameFooter>Footer</FrameFooter>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Gotcha:** a structured card shell for tool-like surfaces. `stacked` connects multiple panels with shared borders; `dense` removes panel padding; radius via the `--frame-radius` CSS variable.
|
||||
|
||||
## icon-stack
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconStack aria-hidden="true">
|
||||
<InboxIcon className="size-4" />
|
||||
</IconStack>
|
||||
```
|
||||
|
||||
**Gotcha:** isometric layered artwork for empty states and illustrations; style the inner icon via its own `className`. Mark purely decorative stacks `aria-hidden="true"` and keep the real label in surrounding copy.
|
||||
|
||||
## icon-tile
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconTile variant="elevated" size="lg">
|
||||
<PackageIcon />
|
||||
</IconTile>
|
||||
```
|
||||
|
||||
**Gotcha:** the square container an icon sits in, so every list row, feature card and empty state shares one affordance. `variant`: `outline` (default) | `elevated` (muted fill, raised ring) | `soft` (tinted nested, tone from currentColor) | `solid` (filled tone, contrasting glyph) | `frame` (double container). `soft` and `solid` retint from one text color class (they default to `text-primary`). `size`: `xs | sm | default | lg | xl` (24/32/40/48/64px tile, glyph scales 12/14/16/20/24px). `radius`: `default | full`. Do not set a `size-*` class on the child icon unless you mean to override the tile's glyph size; recolor with `className` on the tile, not the icon.
|
||||
|
||||
## alert
|
||||
|
||||
**Required:** `Alert` > `AlertTitle`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Alert variant="success">
|
||||
<ShieldCheckIcon />
|
||||
<AlertTitle>Security update</AlertTitle>
|
||||
<AlertDescription>Enable two-factor authentication.</AlertDescription>
|
||||
<AlertAction><Button size="xs">Update</Button></AlertAction>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible API. `variant`: `default | destructive | info | success | warning | invert`. The non-default variants use ReUI extended color tokens (`--success`/`--info`/`--warning`/`--invert`), which the install adds. Defer generic alert rules to the shadcn skill.
|
||||
|
||||
## badge
|
||||
|
||||
**Required:** none (text child).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="success-light" size="sm">Success</Badge>
|
||||
<Badge variant="outline" radius="full">Pill</Badge>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible. Rich `variant` set (solid, `-outline`, `-light` per color), `size` `xs..xl`, `radius` `default | full`. Like `alert`, the color variants rely on ReUI extended tokens. Prefer `Badge` variants over raw color classes for statuses.
|
||||
|
||||
## base vs radix - write for the project's base
|
||||
|
||||
ReUI ships every component in two builds: `base` (Base UI) and `radix` (Radix UI). The install command and name are identical, and the CLI installs the build matching the project. But you must write/adapt code against the **right base**, because their APIs differ.
|
||||
|
||||
**Detect the base first.** Read `components.json` -> `style` and take the segment before the first `-`:
|
||||
|
||||
- `"style": "base-nova"` -> **Base UI**
|
||||
- `"style": "radix-nova"` -> **Radix UI**
|
||||
|
||||
**Then use that base's API.** The deltas mirror shadcn's base-vs-radix split:
|
||||
|
||||
- Slot/composition: Base UI `render={<… />}` vs Radix `asChild`.
|
||||
- `Select`: Base UI takes `items`; Radix uses `<SelectItem>` children.
|
||||
- `ToggleGroup`: Base UI `multiple` boolean vs Radix `type="single" | "multiple"`.
|
||||
|
||||
The safest path is to **read the installed files and `c-*` examples** - they're already in your base, so reuse their wiring instead of guessing. When `get_component`'s inline `api` or an example shows the other base's shape, translate it to your base (or `validate_usage` to confirm). Defer the generic base/radix mechanics to the shadcn skill.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Craft: make ReUI UI exceptional, not generic
|
||||
|
||||
ReUI items ship senior-designer quality. Your adaptation has to hold that bar, so the result reads like a real product surface a team would keep - not a wireframe an AI generated. Use these alongside the reuse rules in [adapting.md](./adapting.md).
|
||||
|
||||
## Have a point of view
|
||||
|
||||
Pick an emotional register before you compose - calm, operational, premium, editorial, dense, energetic - and let layout, spacing, surface treatment, and icon behavior all reinforce it. One or two memorable decisions and restraint everywhere else beats ten generic ones. UI with no point of view reads as generated.
|
||||
|
||||
## Brutally clear hierarchy
|
||||
|
||||
One focal point per card or panel: the dominant metric or task first, its label second, supporting detail third. The first thing the eye lands on should be the right thing; secondary text must read as secondary. Borders, separators, and surfaces do real work to create 2-3 information bands - don't flatten everything to equal weight.
|
||||
|
||||
## Spacing rhythm and deliberate density
|
||||
|
||||
Gaps are a signal, not a default. Keep them intentional and consistent within a family (`gap-1`/`gap-2` for tight operational rows, larger gaps for section breaks), and smaller within a group than between groups. Match the surrounding ReUI density; don't pad an operational surface like a marketing page, and don't drift density mid-section. The composition should still feel authored in grayscale.
|
||||
|
||||
## Cover the real states (the usual miss)
|
||||
|
||||
A surface isn't done at the happy path. Compose, and wire:
|
||||
|
||||
- **Empty** - a purposeful empty state (short message + the primary action), never a blank panel.
|
||||
- **Loading** - a **skeleton** that matches the real layout, not a centered spinner.
|
||||
- **Error** - an inline, recoverable error with a retry, announced via `role="status"`/`aria-live`.
|
||||
|
||||
Derive these from an element the block already has (don't invent parallel markup), or `get_examples` for a state-specific example.
|
||||
|
||||
## Responsive by default
|
||||
|
||||
Mobile-first, not mobile-afterthought. In constrained rows/cards/sidebars, put `min-w-0` on the shrinking container and `truncate` long single-line labels; protect the primary label's width and let secondary content compress. Reflow layouts (multi-column -> single column) rather than just shrinking them. Desktop and mobile should both look designed.
|
||||
|
||||
## Motion, subtly
|
||||
|
||||
Motion should clarify, not decorate. Use ReUI Motion Icons on primary actions for a subtle hover cue; keep transitions short (~200-300ms) with calm easing; prefer a skeleton pulse over a spinner. No bouncing, no gratuitous entrance animations on every element.
|
||||
|
||||
## Real, activated content
|
||||
|
||||
Use believable, typed data (realistic labels, counts, timestamps, statuses that map to a real workflow) - never lorem or abstract filler. Every visible control does something: no decorative buttons, fake tabs, meaningless toggles, or stats with no job. It must still hold with long names, empty values, and crowded data.
|
||||
|
||||
## Avoid the AI tells
|
||||
|
||||
These instantly read as generated - don't ship them: equal-weight card walls, empty gradients, repetitive padding everywhere, generic enterprise copy, ornamental icons, and number tiles that don't earn their place.
|
||||
|
||||
## The bar
|
||||
|
||||
Before you finish, ask: **would a product team keep this instead of replacing it? Does it still feel strong after swapping in real content?** If not, reuse the shipped ReUI design harder - don't restyle it into something new - then run the [quality.md](./quality.md) gates.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Icons (ReUI delta over shadcn)
|
||||
|
||||
Follow the shadcn icon rules (use the project's configured `iconLibrary`, `data-icon` on icons inside `Button`, no sizing classes on icons inside components, pass icons as component objects not string keys). ReUI adds the following.
|
||||
|
||||
## Portable icons (library-agnostic)
|
||||
|
||||
ReUI components, examples, and blocks are authored to be icon-library-agnostic. When `iconLibrary` is set in `components.json`, the shadcn CLI installs each item's icons in **your** library automatically - you swap nothing. If an installed item's icons don't match your project (for example `iconLibrary` isn't set, so they came in from the item's demo library), change the **import source and component name** to your library, keeping the same icon-name semantics:
|
||||
|
||||
- `lucide` -> `lucide-react`
|
||||
- `tabler` -> `@tabler/icons-react`
|
||||
- `phosphor` -> `@phosphor-icons/react`
|
||||
- `remix` -> `@remixicon/react`
|
||||
- `hugeicons` -> `@hugeicons/react`
|
||||
|
||||
Don't assume `lucide-react`; read `iconLibrary` from `components.json`.
|
||||
|
||||
## Keep icons purposeful
|
||||
|
||||
Icons support the hierarchy, they don't replace it: keep them small, matched to the surrounding density, and decorative ones `aria-hidden="true"` (an icon-only control still needs an accessible label on the control). Don't add ornamental icons that do no job.
|
||||
|
||||
## Motion Icons (the `@reui/icons/...` set)
|
||||
|
||||
ReUI ships its own icon set in 4 styles (outline, solid, duotone, filled), each icon in two variants:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/icons/default/<style>/<name> --yes # static
|
||||
npx shadcn@latest add @reui/icons/animated/<style>/<name> --yes # hover-animated (motion/react)
|
||||
```
|
||||
|
||||
Finding them via the MCP is free; installing requires an Ultimate license (`REUI_LICENSE_KEY`, see [cli.md](./cli.md)). Reach for a Motion Icon on a primary action when a subtle hover cue helps; keep motion restrained.
|
||||
|
||||
Finding icons:
|
||||
|
||||
- Several icons (the common case): **`search_icons(concepts[])`** - up to 24 concepts in one call, the best icons per concept with install commands. Pass `animated: true` to get only icons with a hover-animated Motion variant.
|
||||
- One icon: `search` with `type: "icon"`.
|
||||
- Icon results and `get_icon` carry `animated: true` and `installAnimated` when an animated variant exists - use those install strings, do not construct paths by hand.
|
||||
- Every icon result carries a `previewUrl` (its live icon-category page) - **share it with the user** so they can SEE the icon before installing.
|
||||
|
||||
The `icon-stack` component composes multiple icons into a stacked display.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Quality gates (security, accessibility, scroll)
|
||||
|
||||
These are the **done gate**, not a nice-to-have: before you call any ReUI work finished, call the MCP `get_audit_checklist` tool and pass every item below (plus the craft bar in [craft.md](./craft.md)). Then typecheck and lint.
|
||||
|
||||
## Security
|
||||
|
||||
- Never `dangerouslySetInnerHTML`. Render data as text/components.
|
||||
- External links (`target="_blank"`) must always pair `rel="noopener noreferrer"`.
|
||||
- No real PII, secrets, or tokens in demo or committed code. Remote media only from sources the project already allows.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Implicit list/card items that navigate get real anchors with a standard hover affordance.
|
||||
- Icon-only or numeric buttons need an `aria-label`; decorative icons get `aria-hidden`.
|
||||
- Every non-submit button is `type="button"`.
|
||||
- Keyboard + focus: everything interactive is reachable in a sensible Tab order with a visible focus ring; layers (dialogs/sheets/menus) trap focus and close on `Escape`. ReUI components ship standard keyboard behavior - read each component's inline `api` rather than re-implementing it.
|
||||
- Announce async UI: loading and error messages use `role="status"` / `aria-live` so they're not silent to screen readers.
|
||||
|
||||
## Scroll mechanics
|
||||
|
||||
- Make scroll regions with a parent-owned height: a `min-h-0` + flex chain down to the scroll container. Never guess a `max-h`.
|
||||
- The scroll container owns `overflow-auto`; ancestors stay `min-h-0` so the height resolves.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ReUI registry structure
|
||||
|
||||
ReUI is a shadcn-compatible registry with four entity types. **Examples and blocks are built FROM components** - reuse them, don't rebuild.
|
||||
|
||||
- **component** - one of the 20 ReUI building blocks with a real API (`data-grid`, `kanban`, `filters`, `date-selector`, `tree`, ...). Install directly (`@reui/data-grid`) or let it come in as a dependency of an example/block. Free. Read its API with `get_component(name)`.
|
||||
- **example** - a free `c-*` single-pattern use-case of a component (`c-kanban-1`, `c-data-grid-3`). Install one and read it to copy real composition. Find a component's examples with `get_examples(name)`.
|
||||
- **block** - a premium, full-page section that composes several components (`data-grid-2`, `pricing-page-1`). Pro or Ultimate license at install. Adapts to your active theme via semantic tokens.
|
||||
- **icon** - Motion Icons in 4 styles (outline, solid, duotone, filled), static (`@reui/icons/default/<style>/<name>`) and hover-animated (`@reui/icons/animated/<style>/<name>`). Ultimate license at install. See [icons.md](./icons.md).
|
||||
|
||||
## The @reui registry
|
||||
|
||||
Install everything through the shadcn CLI: `npx shadcn@latest add @reui/<name> --yes`. The CLI reads the `@reui` registry from the project's `components.json`. Free items need only the plain string form:
|
||||
|
||||
```json
|
||||
{ "registries": { "@reui": "https://reui.io/r/{style}/{name}.json" } }
|
||||
```
|
||||
|
||||
Premium installs need the authenticated form + `REUI_LICENSE_KEY` in `.env.local` - see [cli.md](./cli.md). The MCP `get_project_context` tool returns the right config.
|
||||
|
||||
## Know your base: base or radix
|
||||
|
||||
ReUI ships every item in two builds - `base` (Base UI) and `radix` (Radix UI) - with mirrored names. The CLI installs the build matching your project automatically, but **you must write code against the right base's API**. Detect it from `components.json` -> `style`: the segment before the first `-` is the base (`base-nova` -> Base UI, `radix-nova` -> Radix UI). The installed files and `c-*` examples are already in your base - read them and adapt on that base. See [components.md](./components.md) for the API deltas.
|
||||
|
||||
**So the MCP's own `docsUrl` and `previewUrl` match your base**, send your `style` to the MCP: add `?style=<your components.json style>` to the ReUI MCP server URL (or set an `X-Reui-Style` header) in your MCP client config - set once, applies to every call. The MCP then resolves docs/preview links to YOUR library (`/docs/components/radix/...`, `/preview/radix/...` for a radix project) instead of the default base; `get_project_context` echoes back the style it currently sees so you can confirm it. Install commands are unaffected (the CLI already installs the right variant). If you notice the MCP returning `base` links for a `radix` project, tell the user to add `?style=` to the server URL.
|
||||
|
||||
Blocks adapt to your active theme through semantic tokens and CSS variables - change the theme and every block follows.
|
||||
|
||||
## Free vs premium
|
||||
|
||||
- **Free, no key:** the 20 components, all `c-*` examples, the ReUI MCP, and this skill.
|
||||
- **Premium, license required at install:** blocks (Pro or Ultimate), Motion Icons and templates (Ultimate). Set `REUI_LICENSE_KEY` (see [cli.md](./cli.md)).
|
||||
|
||||
## Component API index
|
||||
|
||||
The canonical index of every component's API docs is **https://reui.io/llms.txt** (returned as `componentsApiUrl` in MCP results). Prefer the inline `api` from `get_component`; use the index/docs as the fallback.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Styling (ReUI delta over shadcn)
|
||||
|
||||
Follow the shadcn skill for the generic rules (semantic colors not raw values, `gap-*` not `space-y-*`, `size-*`, `cn()`, no manual `dark:` overrides, no overlay `z-index`). This file is only the ReUI-specific additions.
|
||||
|
||||
## ReUI extended semantic tokens
|
||||
|
||||
ReUI adds semantic tokens beyond shadcn's base set. Use these instead of raw colors for status and emphasis:
|
||||
|
||||
- `--success` / `--success-foreground`
|
||||
- `--info` / `--info-foreground`
|
||||
- `--warning` / `--warning-foreground`
|
||||
- `--destructive-foreground` (paired with shadcn's `--destructive`)
|
||||
- `--invert` / `--invert-foreground` (inverted surfaces)
|
||||
|
||||
Use them as Tailwind utilities (`bg-success text-success-foreground`, `text-warning`, ...). They are defined in the project's global CSS and registered with Tailwind (`@theme inline` on v4). If a token is missing in the project, add it to the global CSS file (never a new file) following the same `name` / `name-foreground` convention, exactly as the shadcn customization rules describe.
|
||||
|
||||
**Incorrect:** `<span className="text-green-600">Active</span>`
|
||||
**Correct:** `<Badge variant="success">Active</Badge>` or `<span className="text-success">Active</span>`
|
||||
|
||||
## Blocks follow your theme
|
||||
|
||||
When you install a block it adapts to your active theme through the semantic tokens above and the project's CSS variables. Don't hardcode style-specific values into installed block code and don't fork it to "restyle" - change the theme via the CSS variables / a preset and every block follows. Want a different look? `search` for a block whose design already fits instead of re-skinning one.
|
||||
|
||||
## Density and typography rhythm
|
||||
|
||||
ReUI operational UI usually feels dense, not airy. Keep the gap between a title and its supporting description tight by default (`gap-0.5`, `space-y-1`, or `space-y-px`), and smaller than the gap between sections. Match the surrounding ReUI density when you add rows or fields; do not pad operational surfaces like a marketing page.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Workflow: find -> install -> read API -> adapt
|
||||
|
||||
The core ReUI loop. The MCP tells you what to install and gives you the API; the shadcn CLI installs it; you turn the installed files into correct, themed, data-wired code by **reuse**, not redesign.
|
||||
|
||||
## 1. Find (ReUI MCP `search` / `compose_page`)
|
||||
|
||||
**Full multi-section page ask?** Call `compose_page(intent, sections?)` FIRST, before searching block-by-block. It returns ordered sections, each with the best block for the intent (top pick + alternates); sections listed in `unavailableSections` have no real inventory - compose those from components, do not force a bad block.
|
||||
|
||||
For everything else, call `search` with the user's intent. Pass structured hints whenever you can infer them - you are an LLM, so do the parsing the server cannot:
|
||||
|
||||
- `type`: `"component"` (one of the 20 building blocks), `"example"` (a c-\* use-case), `"block"` (a full page/section), `"icon"`.
|
||||
- `component`: the ReUI component the request implies (`"data-grid"`, `"kanban"`, ...).
|
||||
- `category`, `features` (e.g. `["sortable","pagination"]`), `free`.
|
||||
|
||||
Example: "build a users management page with filters" -> `search({ query: "users management page with filters", type: "block", component: "data-grid", features: ["filters"] })`.
|
||||
|
||||
Each result has `install`, `previewUrl`, `docsUrl`, `componentsUsed`, `score`, `termCoverage`, and `whyMatch`. `score` is relative to the top hit (the top is ~100 by construction), not an absolute quality - compare results to each other, and show the user the top options if several score closely; do not silently guess. A low `termCoverage` means a weak match even with a high score - rephrase or widen.
|
||||
|
||||
**Always show the preview link.** Whenever you list or recommend items - from `search`, `search_icons`, `list_components`, `compose_page`, or a getter - include each item's `previewUrl` (a live preview page) so the user can SEE it before you install. Blocks and examples link to an individual live preview; icons and components to their live category/component page. This applies to every listing, not only a single pick.
|
||||
|
||||
## 2. Install (shadcn CLI)
|
||||
|
||||
Run the result's `install` command from the project root, non-interactively:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/<name> --yes
|
||||
```
|
||||
|
||||
The CLI reads `components.json`, installs the correct base+style variant, resolves `registryDependencies` (a block pulls in its components), installs npm deps, and rewrites aliases. Do not pass the base/style. See [cli.md](./cli.md).
|
||||
|
||||
## 3. Read the API (do not guess props)
|
||||
|
||||
Before writing code against any component an item uses:
|
||||
|
||||
1. The item's `componentDigests` already give a 1-line contract per component - often enough to wire it. For the full API, call **`get_component(names)`** with ALL of `componentsUsed` in ONE call (it accepts an array) and read each inline `api` - no web fetch. **Share the component's `docsUrl`** (its API documentation page) with the user whenever you work with that component's API, so they have the full reference; the `/llms.txt` index is a further fallback.
|
||||
2. Call **`get_examples(name)`** for the free `c-*` examples of that component; install one and **read the added files** to copy the exact composition. This is the fastest correct path - the example shows real wiring you adapt, not invent.
|
||||
3. About to write a prop you did not see in an `api` or installed file? Run **`validate_usage`** BEFORE writing the code - per-prop documented / notDocumented verdicts plus did-you-mean suggestions. notDocumented means read the API, not push on.
|
||||
|
||||
## 4. Adapt (reuse-first) - do not skip
|
||||
|
||||
Installing files is not the end, and redesigning them defeats the point. First note the project's **base** so you write the right API - read `components.json` -> `style` and take the segment before the first `-` (`base-nova` -> Base UI, `radix-nova` -> Radix UI), see [components.md](./components.md). After `add`:
|
||||
|
||||
1. **Read the added files**; keep the composition intact. For a block, verify the components are wired correctly (for `data-grid`: a `useTable({ features: dataGridFeatures, ... })` instance passed as `table`, `recordCount` set - see [components.md](./components.md)).
|
||||
2. **Replace demo data with the user's real data** via typed structures (see [adapting.md](./adapting.md)).
|
||||
3. **Fix icon imports** to the project's icon library (see [icons.md](./icons.md)).
|
||||
4. **Align styling** to semantic tokens and the active theme - no raw colors (see [styling.md](./styling.md)).
|
||||
5. **Validate before finalizing**: if your adaptation introduced components or props you did not read in an `api` or example, run `validate_usage` on them.
|
||||
6. **Hit the craft bar** - clear hierarchy, deliberate density, the empty / loading / error states, subtle motion, and mobile-first responsiveness (see [craft.md](./craft.md)). Generic-looking output means you under-reused the design, not that it needs restyling.
|
||||
7. **Pass the quality gates** (security, a11y, scroll) - call the MCP `get_audit_checklist` tool and clear every item (see [quality.md](./quality.md)).
|
||||
8. **Typecheck / lint**.
|
||||
|
||||
## If no single block fits
|
||||
|
||||
Compose from components (`compose_page` tells you which sections have no block inventory via `unavailableSections`). `search` the components you need, read each `get_component` API, install a worked `get_examples` example for each, and assemble by adapting those examples. A block in the same category is a useful reference - install it and read its files to see how ReUI composes those components, then adapt.
|
||||
@@ -0,0 +1,58 @@
|
||||
# ReUI MCP: full reference
|
||||
|
||||
The ReUI MCP (`https://mcp.reui.io`, Streamable HTTP) is free to use but needs a ReUI account: on first use the agent signs in with ReUI (a free account is created if the user has none), so every request is tied to an account. Free covers components and examples; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the daily request limit. It does **discovery + guidance** (search, inline APIs, page planning, validation) and never serves source; the shadcn CLI does **installation**, and the license key lives there (the `@reui` entry in `components.json`, backed by `.env.local`). Goal: from the user's intent to correct, themed, data-wired ReUI code in the **fewest tokens and calls**, with **no guessing**.
|
||||
|
||||
## Golden path (token-optimal - follow this order)
|
||||
|
||||
Most tasks need 2-4 MCP calls and ZERO web fetches:
|
||||
|
||||
1. **`search(query, ...hints)`** -> pick the top 1-3 results. Each result already carries `install`, `previewUrl`, `docsUrl`, `componentsUsed`, `score`, `whyMatch`. The payload is complete - do not call another tool just to "confirm" a result.
|
||||
2. **`get_component([...componentsUsed])`** in ONE batched call (one name or an array of up to 20) -> read each inline `api`. This **replaces** fetching docs pages. Often skippable: search responses carry `componentDigests`, a compact API contract per referenced component.
|
||||
3. **`get_examples(component)`** -> install ONE returned `c-*` example, read the added files, copy the composition.
|
||||
4. **`get_install_command(item)`** only to validate a name you are unsure of (results already include `install`). Run the install with the shadcn CLI (`--yes`).
|
||||
5. **`get_audit_checklist()`** before declaring done.
|
||||
|
||||
If you already know the exact item name, skip `search`. Everything else is situational.
|
||||
|
||||
## The 5 task-specific tools (when to reach for each)
|
||||
|
||||
- **`compose_page`** - BEFORE building any full page (dashboard, settings, billing, landing). Pass the intent (and optionally the sections you want); it returns ordered sections, each with the best premium block for the intent (top pick + alternates). Sections with no real inventory are listed honestly in `unavailableSections` - compose those from components instead of forcing a bad block.
|
||||
- **`search_icons`** - whenever you need icons, especially several. Batch up to 24 concepts in one call; each concept returns its best icons with install commands. Pass `animated: true` to get only icons that have a hover-animated Motion variant.
|
||||
- **`validate_usage`** - BEFORE writing code with component names or props you have not read in an inline `api` or an installed example. It checks planned names + props against the indexed API docs and registry item names; returns did-you-mean suggestions and per-prop documented / notDocumented verdicts. Deterministic, no inference - a notDocumented prop means stop and read the API, not push on.
|
||||
- **`whats_new`** - when your registry knowledge might be stale (a name 404s, the user mentions an item you don't know). Returns items added/removed per build, newest first.
|
||||
- **`report_issue`** - when an installed item is actually broken (bad source, wrong dependency, broken preview). Goes straight to the ReUI team; rate-limited 5/hour. Not for usage questions.
|
||||
|
||||
## All 19 tools
|
||||
|
||||
`search`, `get_block`, `get_example`, `get_icon`, `list_block_groups`, `list_block_categories`, `list_example_categories`, `list_icon_categories`, `list_components`, `get_component`, `get_examples`, `search_icons`, `compose_page`, `validate_usage`, `whats_new`, `report_issue`, `get_install_command`, `get_project_context`, `get_audit_checklist`. The MCP serves the full parameter schemas; do not guess parameters beyond them.
|
||||
|
||||
## Token + speed rules
|
||||
|
||||
- **Batch `get_component`** - ONE call with the whole `componentsUsed` array, never N calls. Skip it entirely when `componentDigests` already answers the question.
|
||||
- **Read source by installing** - the MCP serves no source. To read or analyze an item's real code, install it with the shadcn CLI and open the local files. Learn an API from the inline `api` / `componentDigests`, never by reading raw source.
|
||||
- **Infer `search` hints yourself** (`type`, `component`, `category`, `features`, `free`) - hints shrink the result set and the tokens. Keep `limit` low; one right result beats ten.
|
||||
- Run independent calls (and the shadcn install) concurrently in one turn - serial tool calls are the main source of slowness.
|
||||
- Don't repeat a search for the same intent; don't call `list_*` to "see everything" - `search` is the entry point, `list_*` is only for browsing a taxonomy the user explicitly wants to explore.
|
||||
- Prefer `get_component`'s inline `api` over `docsUrl` / `/llms.txt`. Fetch a web page only as a last resort.
|
||||
|
||||
## Result shapes (so you don't re-fetch)
|
||||
|
||||
- `score` is 0-100 RELATIVE to the top hit (the top is ~100 by construction), not absolute - compare results to each other.
|
||||
- `termCoverage` (0-1) is the share of the query the item matched - low means a weak match even if the score looks high; rephrase or widen the search.
|
||||
- Each result carries `whyMatch`, `install`, docs/preview URLs, and a `free` flag; premium items carry `requiredPlan` (`"pro"` for blocks, `"ultimate"` for icons).
|
||||
- `componentDigests` is a top-level map: a compact API contract per referenced component - often enough to wire an item without a `get_component` call.
|
||||
- Icon results and `get_icon` include `animated: true` and `installAnimated` when a hover-animated Motion variant exists (animated: `@reui/icons/animated/<style>/<name>`; static: `@reui/icons/default/<style>/<name>`).
|
||||
|
||||
## Error playbook
|
||||
|
||||
- **401** - the MCP requires a signed-in ReUI account. The client prompts "Sign in with ReUI" (OAuth) on first use; a free account is created if needed. For headless/CI, pass a personal token (`reui_pat_...`, created at https://reui.io/account/mcp) as `Authorization: Bearer`.
|
||||
- **403 / locked result** - a valid account but the plan does not cover the item: premium blocks need Pro, Motion Icons need Ultimate. Point to https://reui.io/pricing (upgrade). Free accounts still get all components + examples.
|
||||
- **429** - rate limited (120 requests/min per IP); back off, honor `Retry-After`.
|
||||
- **not found** (`found: false`) - use the returned `suggestions`, or `search`; check `whats_new` if you suspect a stale name. Never run a fabricated install command.
|
||||
|
||||
## Fallbacks
|
||||
|
||||
- No ReUI MCP: `npx shadcn@latest search @reui -q "..."` then `add` (generic, no scoring / inline API).
|
||||
- The shadcn project's own MCP also works over the `@reui` registry: https://ui.shadcn.com/docs/mcp.
|
||||
|
||||
Per-agent MCP setup: https://reui.io/docs/mcp
|
||||
@@ -28,6 +28,8 @@ Runner: `ubuntu-latest`, **bird2** из apt, Docker для job **release**.
|
||||
|
||||
Если releasable-коммитов нет — semantic-release no-op, образы не публикуются.
|
||||
|
||||
Повтор упавшего **release** (тег уже есть, bake нет): detect берёт `v*` на `HEAD` и всё равно пушит образы. Подробнее: [docs/releasing.md](../docs/releasing.md#перезапуск-упавшего-job-release).
|
||||
|
||||
### Секреты
|
||||
|
||||
**`ACTIONS_PAT`**: push tags, releases, Container Registry. Fallback: **`gitea.token`**.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
## Summary
|
||||
|
||||
<!-- 1–3 bullets: what and why -->
|
||||
|
||||
## Checklist
|
||||
|
||||
### General
|
||||
|
||||
- [ ] Docs updated if behavior/API changed (DOC-02)
|
||||
- [ ] Conventional Commits (EN title / RU body)
|
||||
|
||||
### Backend (if Go / OpenAPI / migrations)
|
||||
|
||||
- [ ] `go test ./... -race -count=1` (or scoped packages) when touching jobs/pipeline/httpapi
|
||||
- [ ] OpenAPI lint + regenerate `apps/web/src/types/api.gen.ts` if `docs/openapi.yaml` changed
|
||||
- [ ] Migration pairs postgres+sqlite (DEP-03)
|
||||
|
||||
### Frontend (if `apps/web` / `packages/ui`) — WEB-19
|
||||
|
||||
- [ ] `pnpm --filter @evobgp/web run typecheck`
|
||||
- [ ] `pnpm --filter @evobgp/web run lint`
|
||||
- [ ] `pnpm --filter @evobgp/web run build`
|
||||
- [ ] `pnpm --filter @evobgp/web run test`
|
||||
- [ ] UI follows ReUI PRO + `docs/ui-design-contract.md` (surface `frame`, kit; cite previewUrl)
|
||||
|
||||
### KPI / Quick Actions
|
||||
|
||||
- [ ] If changing `kpi-stat-grid.tsx` / `quick-action-grid.tsx`, note sibling-app sync (vps / CFDM / fw / auth-portal)
|
||||
@@ -150,15 +150,23 @@ jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
openapi:
|
||||
needs: [changes]
|
||||
if: needs.changes.outputs.openapi == 'true'
|
||||
if: needs.changes.outputs.openapi == 'true' || needs.changes.outputs.web == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
- name: Enable pnpm via corepack
|
||||
run: corepack enable
|
||||
- name: Lint OpenAPI (Redocly)
|
||||
run: npx --yes @redocly/cli@1 lint docs/openapi.yaml
|
||||
- name: Check OpenAPI→TS codegen is fresh
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
pnpm install --frozen-lockfile
|
||||
chmod +x scripts/check-openapi-gen.sh
|
||||
sh scripts/check-openapi-gen.sh
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
web:
|
||||
@@ -172,12 +180,13 @@ jobs:
|
||||
node-version: "22"
|
||||
- name: Enable pnpm via corepack
|
||||
run: corepack enable
|
||||
- name: pnpm install, typecheck, lint, build
|
||||
- name: pnpm install, typecheck, lint, test, build
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm --filter @evobgp/web run typecheck
|
||||
pnpm --filter @evobgp/web run lint
|
||||
pnpm --filter @evobgp/web run test
|
||||
pnpm --filter @evobgp/web run build
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -294,6 +303,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
||||
persist-credentials: true
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -314,8 +324,22 @@ jobs:
|
||||
id: rel
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=""
|
||||
if [ -f .release-version ]; then
|
||||
echo "version=$(tr -d '[:space:]' < .release-version)" >> "$GITHUB_OUTPUT"
|
||||
version="$(tr -d '[:space:]' < .release-version)"
|
||||
echo "New release from semantic-release: $version"
|
||||
else
|
||||
# Re-run after a failed docker step: tag already exists, successCmd
|
||||
# did not write .release-version (semantic-release is a no-op).
|
||||
git fetch --tags --force origin || true
|
||||
tag="$(git tag --points-at HEAD --list 'v*.*.*' | sort -V | tail -n1 || true)"
|
||||
if [ -n "${tag:-}" ]; then
|
||||
version="${tag#v}"
|
||||
echo "Reuse existing tag $tag on HEAD (release retry)"
|
||||
fi
|
||||
fi
|
||||
if [ -n "${version:-}" ]; then
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
echo "released=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "released=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -21,9 +21,14 @@ Thumbs.db
|
||||
|
||||
# Env / secrets (local only)
|
||||
.env
|
||||
.env.local
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.*.example
|
||||
|
||||
# Compose runtime log sidecar output (deploy/compose/runtime-logs)
|
||||
deploy/compose/runtime-logs/
|
||||
# Local MCP configs (may contain REUI license Bearer)
|
||||
.cursor/mcp.json
|
||||
.mcp.json
|
||||
.codegraph/daemon.pid
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"codegraph": {
|
||||
"type": "stdio",
|
||||
"command": "codegraph",
|
||||
"args": [
|
||||
"serve",
|
||||
"--mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
engine-strict=true
|
||||
store-dir=C:\Users\shats\AppData\Local\pnpm-test-store\store\v10
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: reui
|
||||
description: Use the ReUI registry from your AI agent - find, install, and correctly use ReUI components (the 20 free building blocks like data-grid, kanban, filters), their free examples, premium blocks, and Motion Icons. Applies in any project using ReUI, the @reui registry, REUI_LICENSE_KEY, or any shadcn project where the user asks for premium blocks, data grids, kanban boards, dashboards, or full pages. Pairs with the free ReUI MCP server for live, scored registry search and inline component APIs.
|
||||
user-invocable: false
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
---
|
||||
|
||||
> **ReUI skill version `668fb463eb`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer (see `get_agent_skill` -> `install.recommended`) to update this skill. Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI for Agents
|
||||
|
||||
ReUI is a shadcn-compatible registry. It ships four things you **reuse** - never redesign:
|
||||
|
||||
- **components** - the 20 ReUI building blocks with real APIs: `data-grid`, `kanban`, `filters`, `date-selector`, `tree`, `stepper`, ... (free)
|
||||
- **examples** - free `c-*` single-pattern use-cases of a component (`c-kanban-1`); install one and read it to see exact composition
|
||||
- **blocks** - premium full-page sections that compose components (`data-grid-2`, `pricing-page-1`); Pro or Ultimate license at install
|
||||
- **icons** - Motion Icons in 4 styles, static + hover-animated variants; Ultimate license at install
|
||||
|
||||
The skill is free and this MCP is free to use; it just needs a ReUI account. On first use your agent opens a browser "Sign in with ReUI" prompt (a free account is created if you don't have one). Free covers components and examples with a daily request allowance; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the limit (see [rules/registry.md](./rules/registry.md)). The same account and skill work in every agent and service the MCP connects to - this skill is agent-agnostic.
|
||||
|
||||
Skill + MCP are a team: this skill is the workflow (how to find, install, read the API, and adapt by reuse); the MCP is the live data and the hands (search, get_component, install commands). Your job: find the right item, install it with the shadcn CLI, read its real API, and **adapt by reuse** - wire real data and theme it; do not hand-roll or restyle what ReUI already provides. This skill **layers on the shadcn skill**: follow that for generic rules (spacing, `cn()`, semantic colors, forms); follow this for everything ReUI-specific.
|
||||
|
||||
## The core loop (MCP-native)
|
||||
|
||||
1. **Find** - call the ReUI MCP `search` tool with the user's intent. It returns a ranked, scored list across components/examples/blocks/icons, each with an `install` command, `previewUrl`, `docsUrl`, and `componentsUsed`. Pass hints (`type`, `component`, `category`, `features`, `free`) when you can infer them.
|
||||
2. **Install** - run the returned command non-interactively (`npx shadcn@latest add @reui/<name> --yes`). The CLI resolves deps, aliases, and the base/style from `components.json`. See [cli.md](./rules/cli.md).
|
||||
3. **Read the API (on your base)** - first note your base from `components.json` -> `style` (`base-nova` -> Base UI, `radix-nova` -> Radix UI). For each component an item uses, call `get_component(name)` and read its **inline `api`** (no web fetch); then `get_examples(name)` to install a worked example and copy its composition - the installed files are already in your base. Whenever you work with a component's API, also **share its `docsUrl`** (the primitive's API documentation page) with the user so they have the full reference. See [components.md](./rules/components.md).
|
||||
4. **Adapt (reuse-first)** - swap demo data for real data, fix icon imports, align tokens. Do not redesign. See [adapting.md](./rules/adapting.md).
|
||||
|
||||
**Always show the preview.** Every item a tool returns carries a `previewUrl` (a live preview page). Whenever you list, recommend, or present ReUI items to the user - blocks, components, examples, or icons, whether from `search`, `search_icons`, `list_components`, `compose_page`, or any getter - include each item's `previewUrl` so they can SEE it before installing. Blocks and examples open an individual live preview; icons and components link to their live category/component page. Never present an item without its preview link.
|
||||
|
||||
If the ReUI MCP is not configured, fall back to `npx shadcn@latest search @reui -q "..."` then `add` - but the MCP gives scored matches + inline APIs; prefer it.
|
||||
|
||||
## Commands
|
||||
|
||||
Run ReUI as explicit slash commands (via the ReUI MCP) **or** just ask in plain language - both run the same workflow.
|
||||
|
||||
| Command | Invoke | Does |
|
||||
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **build** | `/mcp__reui__build <what>` | Compose a page/section/feature from ReUI: plan → install → read API → adapt → craft → audit. |
|
||||
| **add** | `/mcp__reui__add <item>` | Find & install one component/example/block/icon and wire it in. |
|
||||
| **fix** | `/mcp__reui__fix [target]` | Diagnose & fix ReUI usage: wrong/undocumented props, base/radix mismatch, missing states, a11y/scroll. |
|
||||
| **improve** | `/mcp__reui__improve [target]` | Refine + extend existing ReUI UI to a production-exceptional bar (hierarchy, density, states, responsive, motion). |
|
||||
|
||||
Invocation differs slightly per agent (`/mcp__reui__build` in Claude Code/Cursor/Windsurf, `/mcp.reui.build` in VS Code). No command surface? Just describe what you want - this skill drives the identical loop.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
| Need | Reach for |
|
||||
| -------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| A full page or section (dashboard, billing, auth, pricing, settings) | `compose_page` first (plans sections + best blocks), then ReUI **blocks** |
|
||||
| A data table with sorting/filtering/pagination/virtualization | the **data-grid** component (never hand-roll a `<table>`) |
|
||||
| A drag-and-drop board | the **kanban** component |
|
||||
| Advanced column filtering, date range, tree, stepper, ... | the matching ReUI **component** |
|
||||
| A single generic control already in shadcn (Button, Dialog, Select) | plain **shadcn** |
|
||||
|
||||
## Detailed references
|
||||
|
||||
- [rules/registry.md](./rules/registry.md) - the four types, the @reui registry, base/radix, free vs premium + license
|
||||
- [rules/workflow.md](./rules/workflow.md) - the find -> install -> read-API -> adapt loop (most important)
|
||||
- [rules/components.md](./rules/components.md) - the 20 components, the data-grid contract, base vs radix
|
||||
- [rules/adapting.md](./rules/adapting.md) - reuse-first: preserve the design (no over-customizing), reuse examples + a block's own elements, real data, don't invent APIs
|
||||
- [rules/craft.md](./rules/craft.md) - make it exceptional: point of view, hierarchy, density, states, responsive, motion, the bar
|
||||
- [rules/quality.md](./rules/quality.md) - security, accessibility, and scroll gates (the done gate)
|
||||
- [rules/styling.md](./rules/styling.md) - ReUI extended tokens, theme adaptation, density
|
||||
- [rules/icons.md](./rules/icons.md) - portable icons, swapping imports, Motion Icons (static + animated)
|
||||
- [tools.md](./tools.md) - the ReUI MCP: golden path, the 19 tools, token rules, result shapes, errors
|
||||
@@ -0,0 +1,43 @@
|
||||
# Adapting installed ReUI code (reuse-first, no AI slop)
|
||||
|
||||
ReUI items ship production-quality. Your job is to **adapt by reuse** - wire real data and fit the app - not to redesign or hand-roll. The output should look like ReUI built it for this product.
|
||||
|
||||
## Preserve the design - don't over-customize
|
||||
|
||||
The design IS the product. A ReUI block/component encodes senior-designer decisions: spacing, hierarchy, density, color treatment, and component choices. The fastest way to turn a premium block back into generic AI slop is to "improve" its look - so don't.
|
||||
|
||||
- Change **data, copy, and props**; keep the **structure and styling** it ships with. Make the **smallest** change that wires the real data. If your diff touches `className` / JSX structure more than data / props, you are over-customizing - stop and reuse.
|
||||
- Don't swap ReUI components for hand-rolled ones, restructure the layout, re-skin spacing / radius / colors, or add decorative chrome. Let the installed components carry the default spacing, radius, sizing, icon rhythm, density, and state styling; add custom Tailwind only when a component genuinely lacks a contract you need.
|
||||
- Want a different look? `search` for a block whose design already fits and reuse that - don't restyle this one into a new design.
|
||||
|
||||
## Reuse the parts: examples and the block's own elements
|
||||
|
||||
- **Examples are building parts.** A free `c-*` example is a correct, single-pattern composition you can reuse. Before composing from scratch, `get_examples(component)`, install the closest one, and reuse its wiring - assemble UI from examples instead of hand-rolling what an example already shows.
|
||||
- **Reuse a block's own elements.** Need more rows, cards, items, or sections than ship by default? Repeat the block's **existing** element by mapping real data through the same markup - never invent parallel markup that drifts from its design. Need a variant (empty / loading / expanded)? Derive it from an element the block already has.
|
||||
|
||||
## Don't invent (read, don't guess)
|
||||
|
||||
- Never write a prop, variant value, import path, or `@reui/...` name you didn't read in a component's inline `api`, an installed example, or a `search` result. If you didn't see it, treat it as nonexistent - call `get_component` / `get_examples` / `search` first, or run the MCP `validate_usage` tool to check planned names + props against the docs before writing code.
|
||||
- If a getter returns `found: false` or `search` returns nothing, say so and fall back (plain shadcn, or ask) - never fabricate an install command or an API.
|
||||
|
||||
## What to change vs leave alone
|
||||
|
||||
- **Change:** the item's own data, copy, props, and layout to fit the app.
|
||||
- **Leave alone:** installed component files, hooks, and the shared theme - do not edit vendored ReUI internals; change behavior through props and the documented API.
|
||||
- Blocks are **portable React** - no `next/link`, `next/image`, or other framework-runtime imports inside them. Keep them portable.
|
||||
|
||||
## Demo data -> real data
|
||||
|
||||
- Replace every placeholder with the user's real data. Model it as **typed data structures** and **map over arrays** - never duplicate JSX per row/card. Keep small block-specific formatters next to the data.
|
||||
- Wire the real source (columns, fields, fetch). For `data-grid`, implement the server fetch contract if the user needs server-side data.
|
||||
- **Type from the component API, derive during render.** Type domain state through the component's own types - e.g. map status to `BadgeProps["variant"]` via a typed `Record<Status, …>` - instead of stringly-typed values. Compute view state during render; don't mirror derived data into `useState`/`useEffect`.
|
||||
- **Adapt on the right base.** Use the API for the project's base (Base UI vs Radix - see [components.md](./components.md)); the installed files are already base-correct, so reuse their shape rather than translating from memory.
|
||||
|
||||
## Believable content (no AI tells)
|
||||
|
||||
- Use realistic labels, counts, timestamps, and statuses that map to a real workflow.
|
||||
- No decorative buttons, fake tabs, meaningless toggles, equal-weight card walls, empty gradients, ornamental icons, or generic SaaS filler. Every element should do something.
|
||||
|
||||
## Operational surfaces (settings / profile / admin)
|
||||
|
||||
Pick ONE archetype and keep the family consistent: a vertical rail (3-6 sections), horizontal tabs (5-8), or a frame/stack. Prefer `frame` for tool-like surfaces, a card for profile-like ones. Don't mix archetypes in one surface.
|
||||
@@ -0,0 +1,60 @@
|
||||
# CLI: registry setup, license, non-interactive install
|
||||
|
||||
## Registry setup (one-time, per project)
|
||||
|
||||
Free items (the 20 components and all `c-*` examples) need only the plain string registry in `components.json`:
|
||||
|
||||
```json
|
||||
{ "registries": { "@reui": "https://reui.io/r/{style}/{name}.json" } }
|
||||
```
|
||||
|
||||
Premium items (blocks; Motion Icons and templates) require a ReUI license at install:
|
||||
|
||||
1. Add the key to `.env.local`:
|
||||
|
||||
```bash
|
||||
REUI_LICENSE_KEY=your-license-key
|
||||
```
|
||||
|
||||
2. Switch `components.json` to the authenticated object form:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${REUI_LICENSE_KEY}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`, but an MCP client config never expands variables, so a ReUI MCP server config must carry the raw token instead (for example `reui_pat_your_token_here`).
|
||||
|
||||
The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry
|
||||
|
||||
## Installing
|
||||
|
||||
Use the project's package runner (check `packageManager`):
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/<name> --yes # npm
|
||||
pnpm dlx shadcn@latest add @reui/<name> --yes # pnpm
|
||||
bunx --bun shadcn@latest add @reui/<name> --yes # bun
|
||||
```
|
||||
|
||||
`--yes` skips confirmation prompts. The CLI auto-detects the package manager from the lockfile (there is no `--package-manager` flag). It also resolves the correct base+style variant from `components.json`, so do not pass a style.
|
||||
|
||||
## Handling prompts and conflicts
|
||||
|
||||
- **Always pass `--yes`** so the CLI does not block on confirmation prompts.
|
||||
- **Do NOT pass `--overwrite` by default.** If the CLI reports an existing file, read the output and resolve deliberately: install under a different name, adjust the path, or ask the user. Only use `--overwrite` when the user explicitly wants to replace a file.
|
||||
- **Preview first when touching an existing project**: `npx shadcn@latest add @reui/<name> --dry-run` shows what would change; `--diff <file>` shows a specific file's diff. Use these before overwriting.
|
||||
- Run from the **project root** so `components.json` and `.env.local` are found.
|
||||
|
||||
## Free vs premium boundary
|
||||
|
||||
- Public, no key: `c-*` examples and the 20 components (`@reui/data-grid`, `@reui/badge`, ...) that those examples depend on.
|
||||
- Key required at install: blocks (`@reui/<category>-N`) need a Pro or Ultimate license; Motion Icons (`@reui/icons/...`) and templates need Ultimate.
|
||||
|
||||
If an install 401/403s, the license key is missing, invalid, or the plan does not cover that resource (blocks: Pro or higher; icons and templates: Ultimate). Point the user to https://reui.io/account (their key) or https://reui.io/pricing (upgrade).
|
||||
@@ -0,0 +1,370 @@
|
||||
# ReUI components
|
||||
|
||||
The 20 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree`. Examples and blocks are composed from these.
|
||||
|
||||
**Rule one: never guess a component's API. Read it first.** Call **`get_component(name)`** for its inline `api` (props + usage, no web fetch), and **share the result's `docsUrl`** (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the `/llms.txt` index is a further fallback). Then call **`get_examples(name)`** to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline `api` is the full reference. No single block fits? Compose: search the components you need, read each `get_component`, install a `get_examples` example per component, and adapt.
|
||||
|
||||
## data-grid (the flagship - read its API every time)
|
||||
|
||||
`data-grid` wraps TanStack Table v9. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
|
||||
|
||||
- Build a TanStack table instance with `useTable({ features: dataGridFeatures, ... })` (columns, data). `dataGridFeatures` is exported by the primitive and already bundles sorting, filtering, pagination, row selection, expanding, pinning, resizing and faceting, so there are no per-table row models to wire.
|
||||
- Pass that instance to `<DataGrid table={table} recordCount={total}>`.
|
||||
- Compose the body with `DataGridTable` inside `DataGrid`, and enable features through `tableLayout` (e.g. `{ headerSticky: true, columnsResizable: true }`), not ad-hoc classes.
|
||||
- Server-side data uses the documented fetch shape (`recordCount` is the total for pagination).
|
||||
|
||||
```tsx
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
data,
|
||||
columns,
|
||||
})
|
||||
|
||||
<DataGrid table={table} recordCount={data.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
Common mistakes:
|
||||
|
||||
- **Incorrect:** `<DataGrid data={rows} columns={cols} />` - these props do not exist. **Correct:** build a `useTable({ features: dataGridFeatures, ... })` instance and pass `table={table}` + `recordCount`.
|
||||
- **Incorrect:** a raw `<table>` / hand-rolled pagination. **Correct:** use `data-grid`; read its API for sticky header, pagination, virtualization, row selection.
|
||||
- **Incorrect:** styling rows/cells with arbitrary classes. **Correct:** drive layout via `tableLayout` and the primitive's `DataGridColumnMeta` (e.g. `cellClassName`, `headerTitle`), set through the bundle's `columnMeta` slot.
|
||||
|
||||
## event-calendar
|
||||
|
||||
**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EventCalendar defaultEvents={events} defaultView="month" className="h-[560px]">
|
||||
<EventCalendarNav />
|
||||
<EventCalendarContent />
|
||||
</EventCalendar>
|
||||
```
|
||||
|
||||
**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column.
|
||||
|
||||
## gantt
|
||||
|
||||
**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Gantt defaultEvents={bars} resources={tasks} defaultScale="month" className="h-[480px]">
|
||||
<GanttNav />
|
||||
<GanttView />
|
||||
</Gantt>
|
||||
```
|
||||
|
||||
**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height.
|
||||
|
||||
## kanban
|
||||
|
||||
**Required:** `value` (`Record<string, T[]>`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
|
||||
<KanbanBoard>
|
||||
{Object.entries(cols).map(([id, items]) => (
|
||||
<KanbanColumn key={id} value={id}>
|
||||
<KanbanColumnHandle><h3>{id}</h3></KanbanColumnHandle>
|
||||
<KanbanColumnContent value={id}>
|
||||
{items.map((i) => (
|
||||
<KanbanItem key={i.id} value={i.id}>
|
||||
<KanbanItemHandle>{i.title}</KanbanItemHandle>
|
||||
</KanbanItem>
|
||||
))}
|
||||
</KanbanColumnContent>
|
||||
</KanbanColumn>
|
||||
))}
|
||||
</KanbanBoard>
|
||||
<KanbanOverlay><div className="bg-muted size-full rounded-md" /></KanbanOverlay>
|
||||
</Kanban>
|
||||
```
|
||||
|
||||
**Gotcha:** state is `Record<columnId, T[]>`. Each `KanbanColumnContent value` must match its parent `KanbanColumn value`. Omit `KanbanOverlay` and the drag preview silently breaks.
|
||||
|
||||
## sortable
|
||||
|
||||
**Required:** `value` (`T[]`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Sortable value={items} onValueChange={setItems} getItemValue={(i) => i.id}>
|
||||
{items.map((i) => (
|
||||
<SortableItem key={i.id} value={i.id}>
|
||||
<SortableItemHandle><GripVertical /></SortableItemHandle>
|
||||
{i.label}
|
||||
</SortableItem>
|
||||
))}
|
||||
</Sortable>
|
||||
```
|
||||
|
||||
**Gotcha:** a flat 1D reorder list (not columns - that is `kanban`). `getItemValue` must return a stable, unique string. Pass `layout="grid"` or `layout="nested"` for non-list layouts.
|
||||
|
||||
## filters
|
||||
|
||||
**Required:** `filters` (`Filter[]`), `fields` (`FilterFieldConfig[]`), `onChange`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const [filters, setFilters] = useState<Filter[]>([
|
||||
createFilter("priority", "is_any_of", ["low"]),
|
||||
])
|
||||
const fields: FilterFieldConfig[] = [
|
||||
{ key: "priority", label: "Priority", type: "multiselect",
|
||||
options: [{ value: "low", label: "Low" }, { value: "high", label: "High" }] },
|
||||
]
|
||||
|
||||
<Filters filters={filters} fields={fields} onChange={setFilters} />
|
||||
```
|
||||
|
||||
**Gotcha:** always build initial filters with `createFilter(field, operator, values)` - it generates the required `id`. Never hand-construct a `Filter` object. Pairs naturally with `data-grid`.
|
||||
|
||||
## date-selector
|
||||
|
||||
**Required:** none, but wire `onChange` to capture the value.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const [value, setValue] = useState<DateSelectorValue | undefined>()
|
||||
|
||||
<DateSelector value={value} onChange={setValue} label="Due date" />
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a structured `DateSelectorValue` (period / operator / start+end dates), NOT a `Date` - never pass a raw `Date`. Use `allowRange={false}` to lock single-date picking. Read `get_component("date-selector")` for the value shape.
|
||||
|
||||
## tree
|
||||
|
||||
**Required:** `tree` (a `@headless-tree/core` instance you construct)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Tree tree={tree}>
|
||||
{tree.getItems().map((item) => (
|
||||
<TreeItem key={item.getId()} item={item}>
|
||||
<TreeItemLabel />
|
||||
</TreeItem>
|
||||
))}
|
||||
</Tree>
|
||||
```
|
||||
|
||||
**Gotcha:** `Tree` is a styled shell - it takes a headless-tree instance via `tree`, NOT `data`/`items` props. Build the instance with `@headless-tree/react`. External API: https://headless-tree.lukasbach.com/
|
||||
|
||||
## stepper
|
||||
|
||||
**Required:** `StepperItem step` (number), `StepperContent value` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Stepper defaultValue={1}>
|
||||
<StepperNav>
|
||||
<StepperItem step={1}>
|
||||
<StepperTrigger><StepperIndicator>1</StepperIndicator></StepperTrigger>
|
||||
<StepperSeparator />
|
||||
</StepperItem>
|
||||
<StepperItem step={2}>
|
||||
<StepperTrigger><StepperIndicator>2</StepperIndicator></StepperTrigger>
|
||||
</StepperItem>
|
||||
</StepperNav>
|
||||
<StepperPanel>
|
||||
<StepperContent value={1}>Step 1 content</StepperContent>
|
||||
<StepperContent value={2}>Step 2 content</StepperContent>
|
||||
</StepperPanel>
|
||||
</Stepper>
|
||||
```
|
||||
|
||||
**Gotcha:** steps are 1-indexed. Without `StepperPanel` + `StepperContent` you render the nav trail but no body. Put `StepperSeparator` in every `StepperItem` except the last.
|
||||
|
||||
## timeline
|
||||
|
||||
**Required:** `TimelineItem step` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Timeline>
|
||||
<TimelineItem step={1}>
|
||||
<TimelineHeader>
|
||||
<TimelineDate>March 2024</TimelineDate>
|
||||
<TimelineTitle>Project initialized</TimelineTitle>
|
||||
</TimelineHeader>
|
||||
<TimelineIndicator />
|
||||
<TimelineSeparator />
|
||||
<TimelineContent>Repo and architecture set up.</TimelineContent>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
```
|
||||
|
||||
**Gotcha:** each item needs a unique `step`. `orientation` is `"vertical"` (default) or `"horizontal"`. This is a static event display, not interactive like `stepper`.
|
||||
|
||||
## autocomplete
|
||||
|
||||
**Required:** `items` (array; each item has at least `value`)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Autocomplete items={items}>
|
||||
<AutocompleteInput placeholder="Search..." />
|
||||
<AutocompleteContent>
|
||||
<AutocompleteEmpty>No results found.</AutocompleteEmpty>
|
||||
<AutocompleteList>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value} value={item}>{item.label}</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
</Autocomplete>
|
||||
```
|
||||
|
||||
**Gotcha:** `AutocompleteList` takes a render-prop `(item) => ReactNode`, NOT a mapped array of children. External API: https://base-ui.com/react/components/autocomplete
|
||||
|
||||
## phone-input
|
||||
|
||||
**Required:** none, but wire `onChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<PhoneInput placeholder="Enter phone number" defaultCountry="US" value={value} onChange={setValue} />
|
||||
```
|
||||
|
||||
**Gotcha:** `value`/`onChange` use an E.164 string (e.g. `"+14155551234"`), not a display-formatted string; `onChange` can fire `undefined`. `defaultCountry` is a 2-letter ISO code. Wraps `react-phone-number-input`.
|
||||
|
||||
## number-field
|
||||
|
||||
**Required:** wrap the controls in `NumberFieldGroup`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<NumberField defaultValue={0}>
|
||||
<NumberFieldScrubArea label="Quantity" />
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldDecrement />
|
||||
<NumberFieldInput />
|
||||
<NumberFieldIncrement />
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
```
|
||||
|
||||
**Gotcha:** import from `@/components/ui/number-field`. The accessible label goes on `NumberFieldScrubArea`, not `NumberField`. External API: https://base-ui.com/react/components/number-field
|
||||
|
||||
## rating
|
||||
|
||||
**Required:** `rating` (number)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Rating rating={4.5} showValue editable onRatingChange={setRating} />
|
||||
```
|
||||
|
||||
**Gotcha:** supports decimals (partial stars). Pass `editable` + `onRatingChange` for interactive input; omit both for a read-only display.
|
||||
|
||||
## scrollspy
|
||||
|
||||
**Required:** `targetRef` (the scroll container ref)
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Scrollspy targetRef={containerRef}>
|
||||
<a href="#s1" data-scrollspy-anchor="s1">Section 1</a>
|
||||
<a href="#s2" data-scrollspy-anchor="s2">Section 2</a>
|
||||
</Scrollspy>
|
||||
<div ref={containerRef}>
|
||||
<div id="s1">...</div>
|
||||
<div id="s2">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Gotcha:** each link's `data-scrollspy-anchor` must match a section `id`. `targetRef` is the scrollable container (defaults to the window).
|
||||
|
||||
## frame
|
||||
|
||||
**Required:** `Frame` > `FramePanel`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Frame>
|
||||
<FramePanel>
|
||||
<FrameHeader>
|
||||
<FrameTitle>Title</FrameTitle>
|
||||
<FrameDescription>Description</FrameDescription>
|
||||
</FrameHeader>
|
||||
<div className="p-5">Content</div>
|
||||
<FrameFooter>Footer</FrameFooter>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Gotcha:** a structured card shell for tool-like surfaces. `stacked` connects multiple panels with shared borders; `dense` removes panel padding; radius via the `--frame-radius` CSS variable.
|
||||
|
||||
## icon-stack
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconStack aria-hidden="true">
|
||||
<InboxIcon className="size-4" />
|
||||
</IconStack>
|
||||
```
|
||||
|
||||
**Gotcha:** isometric layered artwork for empty states and illustrations; style the inner icon via its own `className`. Mark purely decorative stacks `aria-hidden="true"` and keep the real label in surrounding copy.
|
||||
|
||||
## icon-tile
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconTile variant="elevated" size="lg">
|
||||
<PackageIcon />
|
||||
</IconTile>
|
||||
```
|
||||
|
||||
**Gotcha:** the square container an icon sits in, so every list row, feature card and empty state shares one affordance. `variant`: `outline` (default) | `elevated` (muted fill, raised ring) | `soft` (tinted nested, tone from currentColor) | `solid` (filled tone, contrasting glyph) | `frame` (double container). `soft` and `solid` retint from one text color class (they default to `text-primary`). `size`: `xs | sm | default | lg | xl` (24/32/40/48/64px tile, glyph scales 12/14/16/20/24px). `radius`: `default | full`. Do not set a `size-*` class on the child icon unless you mean to override the tile's glyph size; recolor with `className` on the tile, not the icon.
|
||||
|
||||
## alert
|
||||
|
||||
**Required:** `Alert` > `AlertTitle`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Alert variant="success">
|
||||
<ShieldCheckIcon />
|
||||
<AlertTitle>Security update</AlertTitle>
|
||||
<AlertDescription>Enable two-factor authentication.</AlertDescription>
|
||||
<AlertAction><Button size="xs">Update</Button></AlertAction>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible API. `variant`: `default | destructive | info | success | warning | invert`. The non-default variants use ReUI extended color tokens (`--success`/`--info`/`--warning`/`--invert`), which the install adds. Defer generic alert rules to the shadcn skill.
|
||||
|
||||
## badge
|
||||
|
||||
**Required:** none (text child).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="success-light" size="sm">Success</Badge>
|
||||
<Badge variant="outline" radius="full">Pill</Badge>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible. Rich `variant` set (solid, `-outline`, `-light` per color), `size` `xs..xl`, `radius` `default | full`. Like `alert`, the color variants rely on ReUI extended tokens. Prefer `Badge` variants over raw color classes for statuses.
|
||||
|
||||
## base vs radix - write for the project's base
|
||||
|
||||
ReUI ships every component in two builds: `base` (Base UI) and `radix` (Radix UI). The install command and name are identical, and the CLI installs the build matching the project. But you must write/adapt code against the **right base**, because their APIs differ.
|
||||
|
||||
**Detect the base first.** Read `components.json` -> `style` and take the segment before the first `-`:
|
||||
|
||||
- `"style": "base-nova"` -> **Base UI**
|
||||
- `"style": "radix-nova"` -> **Radix UI**
|
||||
|
||||
**Then use that base's API.** The deltas mirror shadcn's base-vs-radix split:
|
||||
|
||||
- Slot/composition: Base UI `render={<… />}` vs Radix `asChild`.
|
||||
- `Select`: Base UI takes `items`; Radix uses `<SelectItem>` children.
|
||||
- `ToggleGroup`: Base UI `multiple` boolean vs Radix `type="single" | "multiple"`.
|
||||
|
||||
The safest path is to **read the installed files and `c-*` examples** - they're already in your base, so reuse their wiring instead of guessing. When `get_component`'s inline `api` or an example shows the other base's shape, translate it to your base (or `validate_usage` to confirm). Defer the generic base/radix mechanics to the shadcn skill.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Craft: make ReUI UI exceptional, not generic
|
||||
|
||||
ReUI items ship senior-designer quality. Your adaptation has to hold that bar, so the result reads like a real product surface a team would keep - not a wireframe an AI generated. Use these alongside the reuse rules in [adapting.md](./adapting.md).
|
||||
|
||||
## Have a point of view
|
||||
|
||||
Pick an emotional register before you compose - calm, operational, premium, editorial, dense, energetic - and let layout, spacing, surface treatment, and icon behavior all reinforce it. One or two memorable decisions and restraint everywhere else beats ten generic ones. UI with no point of view reads as generated.
|
||||
|
||||
## Brutally clear hierarchy
|
||||
|
||||
One focal point per card or panel: the dominant metric or task first, its label second, supporting detail third. The first thing the eye lands on should be the right thing; secondary text must read as secondary. Borders, separators, and surfaces do real work to create 2-3 information bands - don't flatten everything to equal weight.
|
||||
|
||||
## Spacing rhythm and deliberate density
|
||||
|
||||
Gaps are a signal, not a default. Keep them intentional and consistent within a family (`gap-1`/`gap-2` for tight operational rows, larger gaps for section breaks), and smaller within a group than between groups. Match the surrounding ReUI density; don't pad an operational surface like a marketing page, and don't drift density mid-section. The composition should still feel authored in grayscale.
|
||||
|
||||
## Cover the real states (the usual miss)
|
||||
|
||||
A surface isn't done at the happy path. Compose, and wire:
|
||||
|
||||
- **Empty** - a purposeful empty state (short message + the primary action), never a blank panel.
|
||||
- **Loading** - a **skeleton** that matches the real layout, not a centered spinner.
|
||||
- **Error** - an inline, recoverable error with a retry, announced via `role="status"`/`aria-live`.
|
||||
|
||||
Derive these from an element the block already has (don't invent parallel markup), or `get_examples` for a state-specific example.
|
||||
|
||||
## Responsive by default
|
||||
|
||||
Mobile-first, not mobile-afterthought. In constrained rows/cards/sidebars, put `min-w-0` on the shrinking container and `truncate` long single-line labels; protect the primary label's width and let secondary content compress. Reflow layouts (multi-column -> single column) rather than just shrinking them. Desktop and mobile should both look designed.
|
||||
|
||||
## Motion, subtly
|
||||
|
||||
Motion should clarify, not decorate. Use ReUI Motion Icons on primary actions for a subtle hover cue; keep transitions short (~200-300ms) with calm easing; prefer a skeleton pulse over a spinner. No bouncing, no gratuitous entrance animations on every element.
|
||||
|
||||
## Real, activated content
|
||||
|
||||
Use believable, typed data (realistic labels, counts, timestamps, statuses that map to a real workflow) - never lorem or abstract filler. Every visible control does something: no decorative buttons, fake tabs, meaningless toggles, or stats with no job. It must still hold with long names, empty values, and crowded data.
|
||||
|
||||
## Avoid the AI tells
|
||||
|
||||
These instantly read as generated - don't ship them: equal-weight card walls, empty gradients, repetitive padding everywhere, generic enterprise copy, ornamental icons, and number tiles that don't earn their place.
|
||||
|
||||
## The bar
|
||||
|
||||
Before you finish, ask: **would a product team keep this instead of replacing it? Does it still feel strong after swapping in real content?** If not, reuse the shipped ReUI design harder - don't restyle it into something new - then run the [quality.md](./quality.md) gates.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Icons (ReUI delta over shadcn)
|
||||
|
||||
Follow the shadcn icon rules (use the project's configured `iconLibrary`, `data-icon` on icons inside `Button`, no sizing classes on icons inside components, pass icons as component objects not string keys). ReUI adds the following.
|
||||
|
||||
## Portable icons (library-agnostic)
|
||||
|
||||
ReUI components, examples, and blocks are authored to be icon-library-agnostic. When `iconLibrary` is set in `components.json`, the shadcn CLI installs each item's icons in **your** library automatically - you swap nothing. If an installed item's icons don't match your project (for example `iconLibrary` isn't set, so they came in from the item's demo library), change the **import source and component name** to your library, keeping the same icon-name semantics:
|
||||
|
||||
- `lucide` -> `lucide-react`
|
||||
- `tabler` -> `@tabler/icons-react`
|
||||
- `phosphor` -> `@phosphor-icons/react`
|
||||
- `remix` -> `@remixicon/react`
|
||||
- `hugeicons` -> `@hugeicons/react`
|
||||
|
||||
Don't assume `lucide-react`; read `iconLibrary` from `components.json`.
|
||||
|
||||
## Keep icons purposeful
|
||||
|
||||
Icons support the hierarchy, they don't replace it: keep them small, matched to the surrounding density, and decorative ones `aria-hidden="true"` (an icon-only control still needs an accessible label on the control). Don't add ornamental icons that do no job.
|
||||
|
||||
## Motion Icons (the `@reui/icons/...` set)
|
||||
|
||||
ReUI ships its own icon set in 4 styles (outline, solid, duotone, filled), each icon in two variants:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/icons/default/<style>/<name> --yes # static
|
||||
npx shadcn@latest add @reui/icons/animated/<style>/<name> --yes # hover-animated (motion/react)
|
||||
```
|
||||
|
||||
Finding them via the MCP is free; installing requires an Ultimate license (`REUI_LICENSE_KEY`, see [cli.md](./cli.md)). Reach for a Motion Icon on a primary action when a subtle hover cue helps; keep motion restrained.
|
||||
|
||||
Finding icons:
|
||||
|
||||
- Several icons (the common case): **`search_icons(concepts[])`** - up to 24 concepts in one call, the best icons per concept with install commands. Pass `animated: true` to get only icons with a hover-animated Motion variant.
|
||||
- One icon: `search` with `type: "icon"`.
|
||||
- Icon results and `get_icon` carry `animated: true` and `installAnimated` when an animated variant exists - use those install strings, do not construct paths by hand.
|
||||
- Every icon result carries a `previewUrl` (its live icon-category page) - **share it with the user** so they can SEE the icon before installing.
|
||||
|
||||
The `icon-stack` component composes multiple icons into a stacked display.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Quality gates (security, accessibility, scroll)
|
||||
|
||||
These are the **done gate**, not a nice-to-have: before you call any ReUI work finished, call the MCP `get_audit_checklist` tool and pass every item below (plus the craft bar in [craft.md](./craft.md)). Then typecheck and lint.
|
||||
|
||||
## Security
|
||||
|
||||
- Never `dangerouslySetInnerHTML`. Render data as text/components.
|
||||
- External links (`target="_blank"`) must always pair `rel="noopener noreferrer"`.
|
||||
- No real PII, secrets, or tokens in demo or committed code. Remote media only from sources the project already allows.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Implicit list/card items that navigate get real anchors with a standard hover affordance.
|
||||
- Icon-only or numeric buttons need an `aria-label`; decorative icons get `aria-hidden`.
|
||||
- Every non-submit button is `type="button"`.
|
||||
- Keyboard + focus: everything interactive is reachable in a sensible Tab order with a visible focus ring; layers (dialogs/sheets/menus) trap focus and close on `Escape`. ReUI components ship standard keyboard behavior - read each component's inline `api` rather than re-implementing it.
|
||||
- Announce async UI: loading and error messages use `role="status"` / `aria-live` so they're not silent to screen readers.
|
||||
|
||||
## Scroll mechanics
|
||||
|
||||
- Make scroll regions with a parent-owned height: a `min-h-0` + flex chain down to the scroll container. Never guess a `max-h`.
|
||||
- The scroll container owns `overflow-auto`; ancestors stay `min-h-0` so the height resolves.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ReUI registry structure
|
||||
|
||||
ReUI is a shadcn-compatible registry with four entity types. **Examples and blocks are built FROM components** - reuse them, don't rebuild.
|
||||
|
||||
- **component** - one of the 20 ReUI building blocks with a real API (`data-grid`, `kanban`, `filters`, `date-selector`, `tree`, ...). Install directly (`@reui/data-grid`) or let it come in as a dependency of an example/block. Free. Read its API with `get_component(name)`.
|
||||
- **example** - a free `c-*` single-pattern use-case of a component (`c-kanban-1`, `c-data-grid-3`). Install one and read it to copy real composition. Find a component's examples with `get_examples(name)`.
|
||||
- **block** - a premium, full-page section that composes several components (`data-grid-2`, `pricing-page-1`). Pro or Ultimate license at install. Adapts to your active theme via semantic tokens.
|
||||
- **icon** - Motion Icons in 4 styles (outline, solid, duotone, filled), static (`@reui/icons/default/<style>/<name>`) and hover-animated (`@reui/icons/animated/<style>/<name>`). Ultimate license at install. See [icons.md](./icons.md).
|
||||
|
||||
## The @reui registry
|
||||
|
||||
Install everything through the shadcn CLI: `npx shadcn@latest add @reui/<name> --yes`. The CLI reads the `@reui` registry from the project's `components.json`. Free items need only the plain string form:
|
||||
|
||||
```json
|
||||
{ "registries": { "@reui": "https://reui.io/r/{style}/{name}.json" } }
|
||||
```
|
||||
|
||||
Premium installs need the authenticated form + `REUI_LICENSE_KEY` in `.env.local` - see [cli.md](./cli.md). The MCP `get_project_context` tool returns the right config.
|
||||
|
||||
## Know your base: base or radix
|
||||
|
||||
ReUI ships every item in two builds - `base` (Base UI) and `radix` (Radix UI) - with mirrored names. The CLI installs the build matching your project automatically, but **you must write code against the right base's API**. Detect it from `components.json` -> `style`: the segment before the first `-` is the base (`base-nova` -> Base UI, `radix-nova` -> Radix UI). The installed files and `c-*` examples are already in your base - read them and adapt on that base. See [components.md](./components.md) for the API deltas.
|
||||
|
||||
**So the MCP's own `docsUrl` and `previewUrl` match your base**, send your `style` to the MCP: add `?style=<your components.json style>` to the ReUI MCP server URL (or set an `X-Reui-Style` header) in your MCP client config - set once, applies to every call. The MCP then resolves docs/preview links to YOUR library (`/docs/components/radix/...`, `/preview/radix/...` for a radix project) instead of the default base; `get_project_context` echoes back the style it currently sees so you can confirm it. Install commands are unaffected (the CLI already installs the right variant). If you notice the MCP returning `base` links for a `radix` project, tell the user to add `?style=` to the server URL.
|
||||
|
||||
Blocks adapt to your active theme through semantic tokens and CSS variables - change the theme and every block follows.
|
||||
|
||||
## Free vs premium
|
||||
|
||||
- **Free, no key:** the 20 components, all `c-*` examples, the ReUI MCP, and this skill.
|
||||
- **Premium, license required at install:** blocks (Pro or Ultimate), Motion Icons and templates (Ultimate). Set `REUI_LICENSE_KEY` (see [cli.md](./cli.md)).
|
||||
|
||||
## Component API index
|
||||
|
||||
The canonical index of every component's API docs is **https://reui.io/llms.txt** (returned as `componentsApiUrl` in MCP results). Prefer the inline `api` from `get_component`; use the index/docs as the fallback.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Styling (ReUI delta over shadcn)
|
||||
|
||||
Follow the shadcn skill for the generic rules (semantic colors not raw values, `gap-*` not `space-y-*`, `size-*`, `cn()`, no manual `dark:` overrides, no overlay `z-index`). This file is only the ReUI-specific additions.
|
||||
|
||||
## ReUI extended semantic tokens
|
||||
|
||||
ReUI adds semantic tokens beyond shadcn's base set. Use these instead of raw colors for status and emphasis:
|
||||
|
||||
- `--success` / `--success-foreground`
|
||||
- `--info` / `--info-foreground`
|
||||
- `--warning` / `--warning-foreground`
|
||||
- `--destructive-foreground` (paired with shadcn's `--destructive`)
|
||||
- `--invert` / `--invert-foreground` (inverted surfaces)
|
||||
|
||||
Use them as Tailwind utilities (`bg-success text-success-foreground`, `text-warning`, ...). They are defined in the project's global CSS and registered with Tailwind (`@theme inline` on v4). If a token is missing in the project, add it to the global CSS file (never a new file) following the same `name` / `name-foreground` convention, exactly as the shadcn customization rules describe.
|
||||
|
||||
**Incorrect:** `<span className="text-green-600">Active</span>`
|
||||
**Correct:** `<Badge variant="success">Active</Badge>` or `<span className="text-success">Active</span>`
|
||||
|
||||
## Blocks follow your theme
|
||||
|
||||
When you install a block it adapts to your active theme through the semantic tokens above and the project's CSS variables. Don't hardcode style-specific values into installed block code and don't fork it to "restyle" - change the theme via the CSS variables / a preset and every block follows. Want a different look? `search` for a block whose design already fits instead of re-skinning one.
|
||||
|
||||
## Density and typography rhythm
|
||||
|
||||
ReUI operational UI usually feels dense, not airy. Keep the gap between a title and its supporting description tight by default (`gap-0.5`, `space-y-1`, or `space-y-px`), and smaller than the gap between sections. Match the surrounding ReUI density when you add rows or fields; do not pad operational surfaces like a marketing page.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Workflow: find -> install -> read API -> adapt
|
||||
|
||||
The core ReUI loop. The MCP tells you what to install and gives you the API; the shadcn CLI installs it; you turn the installed files into correct, themed, data-wired code by **reuse**, not redesign.
|
||||
|
||||
## 1. Find (ReUI MCP `search` / `compose_page`)
|
||||
|
||||
**Full multi-section page ask?** Call `compose_page(intent, sections?)` FIRST, before searching block-by-block. It returns ordered sections, each with the best block for the intent (top pick + alternates); sections listed in `unavailableSections` have no real inventory - compose those from components, do not force a bad block.
|
||||
|
||||
For everything else, call `search` with the user's intent. Pass structured hints whenever you can infer them - you are an LLM, so do the parsing the server cannot:
|
||||
|
||||
- `type`: `"component"` (one of the 20 building blocks), `"example"` (a c-\* use-case), `"block"` (a full page/section), `"icon"`.
|
||||
- `component`: the ReUI component the request implies (`"data-grid"`, `"kanban"`, ...).
|
||||
- `category`, `features` (e.g. `["sortable","pagination"]`), `free`.
|
||||
|
||||
Example: "build a users management page with filters" -> `search({ query: "users management page with filters", type: "block", component: "data-grid", features: ["filters"] })`.
|
||||
|
||||
Each result has `install`, `previewUrl`, `docsUrl`, `componentsUsed`, `score`, `termCoverage`, and `whyMatch`. `score` is relative to the top hit (the top is ~100 by construction), not an absolute quality - compare results to each other, and show the user the top options if several score closely; do not silently guess. A low `termCoverage` means a weak match even with a high score - rephrase or widen.
|
||||
|
||||
**Always show the preview link.** Whenever you list or recommend items - from `search`, `search_icons`, `list_components`, `compose_page`, or a getter - include each item's `previewUrl` (a live preview page) so the user can SEE it before you install. Blocks and examples link to an individual live preview; icons and components to their live category/component page. This applies to every listing, not only a single pick.
|
||||
|
||||
## 2. Install (shadcn CLI)
|
||||
|
||||
Run the result's `install` command from the project root, non-interactively:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/<name> --yes
|
||||
```
|
||||
|
||||
The CLI reads `components.json`, installs the correct base+style variant, resolves `registryDependencies` (a block pulls in its components), installs npm deps, and rewrites aliases. Do not pass the base/style. See [cli.md](./cli.md).
|
||||
|
||||
## 3. Read the API (do not guess props)
|
||||
|
||||
Before writing code against any component an item uses:
|
||||
|
||||
1. The item's `componentDigests` already give a 1-line contract per component - often enough to wire it. For the full API, call **`get_component(names)`** with ALL of `componentsUsed` in ONE call (it accepts an array) and read each inline `api` - no web fetch. **Share the component's `docsUrl`** (its API documentation page) with the user whenever you work with that component's API, so they have the full reference; the `/llms.txt` index is a further fallback.
|
||||
2. Call **`get_examples(name)`** for the free `c-*` examples of that component; install one and **read the added files** to copy the exact composition. This is the fastest correct path - the example shows real wiring you adapt, not invent.
|
||||
3. About to write a prop you did not see in an `api` or installed file? Run **`validate_usage`** BEFORE writing the code - per-prop documented / notDocumented verdicts plus did-you-mean suggestions. notDocumented means read the API, not push on.
|
||||
|
||||
## 4. Adapt (reuse-first) - do not skip
|
||||
|
||||
Installing files is not the end, and redesigning them defeats the point. First note the project's **base** so you write the right API - read `components.json` -> `style` and take the segment before the first `-` (`base-nova` -> Base UI, `radix-nova` -> Radix UI), see [components.md](./components.md). After `add`:
|
||||
|
||||
1. **Read the added files**; keep the composition intact. For a block, verify the components are wired correctly (for `data-grid`: a `useTable({ features: dataGridFeatures, ... })` instance passed as `table`, `recordCount` set - see [components.md](./components.md)).
|
||||
2. **Replace demo data with the user's real data** via typed structures (see [adapting.md](./adapting.md)).
|
||||
3. **Fix icon imports** to the project's icon library (see [icons.md](./icons.md)).
|
||||
4. **Align styling** to semantic tokens and the active theme - no raw colors (see [styling.md](./styling.md)).
|
||||
5. **Validate before finalizing**: if your adaptation introduced components or props you did not read in an `api` or example, run `validate_usage` on them.
|
||||
6. **Hit the craft bar** - clear hierarchy, deliberate density, the empty / loading / error states, subtle motion, and mobile-first responsiveness (see [craft.md](./craft.md)). Generic-looking output means you under-reused the design, not that it needs restyling.
|
||||
7. **Pass the quality gates** (security, a11y, scroll) - call the MCP `get_audit_checklist` tool and clear every item (see [quality.md](./quality.md)).
|
||||
8. **Typecheck / lint**.
|
||||
|
||||
## If no single block fits
|
||||
|
||||
Compose from components (`compose_page` tells you which sections have no block inventory via `unavailableSections`). `search` the components you need, read each `get_component` API, install a worked `get_examples` example for each, and assemble by adapting those examples. A block in the same category is a useful reference - install it and read its files to see how ReUI composes those components, then adapt.
|
||||
@@ -0,0 +1,58 @@
|
||||
# ReUI MCP: full reference
|
||||
|
||||
The ReUI MCP (`https://mcp.reui.io`, Streamable HTTP) is free to use but needs a ReUI account: on first use the agent signs in with ReUI (a free account is created if the user has none), so every request is tied to an account. Free covers components and examples; a Pro or Ultimate license unlocks premium blocks and Motion Icons and removes the daily request limit. It does **discovery + guidance** (search, inline APIs, page planning, validation) and never serves source; the shadcn CLI does **installation**, and the license key lives there (the `@reui` entry in `components.json`, backed by `.env.local`). Goal: from the user's intent to correct, themed, data-wired ReUI code in the **fewest tokens and calls**, with **no guessing**.
|
||||
|
||||
## Golden path (token-optimal - follow this order)
|
||||
|
||||
Most tasks need 2-4 MCP calls and ZERO web fetches:
|
||||
|
||||
1. **`search(query, ...hints)`** -> pick the top 1-3 results. Each result already carries `install`, `previewUrl`, `docsUrl`, `componentsUsed`, `score`, `whyMatch`. The payload is complete - do not call another tool just to "confirm" a result.
|
||||
2. **`get_component([...componentsUsed])`** in ONE batched call (one name or an array of up to 20) -> read each inline `api`. This **replaces** fetching docs pages. Often skippable: search responses carry `componentDigests`, a compact API contract per referenced component.
|
||||
3. **`get_examples(component)`** -> install ONE returned `c-*` example, read the added files, copy the composition.
|
||||
4. **`get_install_command(item)`** only to validate a name you are unsure of (results already include `install`). Run the install with the shadcn CLI (`--yes`).
|
||||
5. **`get_audit_checklist()`** before declaring done.
|
||||
|
||||
If you already know the exact item name, skip `search`. Everything else is situational.
|
||||
|
||||
## The 5 task-specific tools (when to reach for each)
|
||||
|
||||
- **`compose_page`** - BEFORE building any full page (dashboard, settings, billing, landing). Pass the intent (and optionally the sections you want); it returns ordered sections, each with the best premium block for the intent (top pick + alternates). Sections with no real inventory are listed honestly in `unavailableSections` - compose those from components instead of forcing a bad block.
|
||||
- **`search_icons`** - whenever you need icons, especially several. Batch up to 24 concepts in one call; each concept returns its best icons with install commands. Pass `animated: true` to get only icons that have a hover-animated Motion variant.
|
||||
- **`validate_usage`** - BEFORE writing code with component names or props you have not read in an inline `api` or an installed example. It checks planned names + props against the indexed API docs and registry item names; returns did-you-mean suggestions and per-prop documented / notDocumented verdicts. Deterministic, no inference - a notDocumented prop means stop and read the API, not push on.
|
||||
- **`whats_new`** - when your registry knowledge might be stale (a name 404s, the user mentions an item you don't know). Returns items added/removed per build, newest first.
|
||||
- **`report_issue`** - when an installed item is actually broken (bad source, wrong dependency, broken preview). Goes straight to the ReUI team; rate-limited 5/hour. Not for usage questions.
|
||||
|
||||
## All 19 tools
|
||||
|
||||
`search`, `get_block`, `get_example`, `get_icon`, `list_block_groups`, `list_block_categories`, `list_example_categories`, `list_icon_categories`, `list_components`, `get_component`, `get_examples`, `search_icons`, `compose_page`, `validate_usage`, `whats_new`, `report_issue`, `get_install_command`, `get_project_context`, `get_audit_checklist`. The MCP serves the full parameter schemas; do not guess parameters beyond them.
|
||||
|
||||
## Token + speed rules
|
||||
|
||||
- **Batch `get_component`** - ONE call with the whole `componentsUsed` array, never N calls. Skip it entirely when `componentDigests` already answers the question.
|
||||
- **Read source by installing** - the MCP serves no source. To read or analyze an item's real code, install it with the shadcn CLI and open the local files. Learn an API from the inline `api` / `componentDigests`, never by reading raw source.
|
||||
- **Infer `search` hints yourself** (`type`, `component`, `category`, `features`, `free`) - hints shrink the result set and the tokens. Keep `limit` low; one right result beats ten.
|
||||
- Run independent calls (and the shadcn install) concurrently in one turn - serial tool calls are the main source of slowness.
|
||||
- Don't repeat a search for the same intent; don't call `list_*` to "see everything" - `search` is the entry point, `list_*` is only for browsing a taxonomy the user explicitly wants to explore.
|
||||
- Prefer `get_component`'s inline `api` over `docsUrl` / `/llms.txt`. Fetch a web page only as a last resort.
|
||||
|
||||
## Result shapes (so you don't re-fetch)
|
||||
|
||||
- `score` is 0-100 RELATIVE to the top hit (the top is ~100 by construction), not absolute - compare results to each other.
|
||||
- `termCoverage` (0-1) is the share of the query the item matched - low means a weak match even if the score looks high; rephrase or widen the search.
|
||||
- Each result carries `whyMatch`, `install`, docs/preview URLs, and a `free` flag; premium items carry `requiredPlan` (`"pro"` for blocks, `"ultimate"` for icons).
|
||||
- `componentDigests` is a top-level map: a compact API contract per referenced component - often enough to wire an item without a `get_component` call.
|
||||
- Icon results and `get_icon` include `animated: true` and `installAnimated` when a hover-animated Motion variant exists (animated: `@reui/icons/animated/<style>/<name>`; static: `@reui/icons/default/<style>/<name>`).
|
||||
|
||||
## Error playbook
|
||||
|
||||
- **401** - the MCP requires a signed-in ReUI account. The client prompts "Sign in with ReUI" (OAuth) on first use; a free account is created if needed. For headless/CI, pass a personal token (`reui_pat_...`, created at https://reui.io/account/mcp) as `Authorization: Bearer`.
|
||||
- **403 / locked result** - a valid account but the plan does not cover the item: premium blocks need Pro, Motion Icons need Ultimate. Point to https://reui.io/pricing (upgrade). Free accounts still get all components + examples.
|
||||
- **429** - rate limited (120 requests/min per IP); back off, honor `Retry-After`.
|
||||
- **not found** (`found: false`) - use the returned `suggestions`, or `search`; check `whats_new` if you suspect a stale name. Never run a fabricated install command.
|
||||
|
||||
## Fallbacks
|
||||
|
||||
- No ReUI MCP: `npx shadcn@latest search @reui -q "..."` then `add` (generic, no scoring / inline API).
|
||||
- The shadcn project's own MCP also works over the `@reui` registry: https://ui.shadcn.com/docs/mcp.
|
||||
|
||||
Per-agent MCP setup: https://reui.io/docs/mcp
|
||||
@@ -69,6 +69,10 @@ pnpm --filter @evobgp/web run build
|
||||
|
||||
Все три команды должны exit 0. CI job `web` не пропускает без этого.
|
||||
|
||||
Стек: React 19, TanStack Router/Query, shadcn/ui (base-nova, registry `@shadcn` + `@reui`), Tailwind v4, lucide-react. Legacy Svelte — в `web-legacy-svelte/` (архив, только референс при миграции).
|
||||
Стек: React 19, TanStack Router/Query, shadcn/ui (base-nova, registry `@shadcn` + `@reui`), Tailwind v4, lucide-react. Legacy Svelte UI удалён (миграция завершена).
|
||||
|
||||
UI-задачи начинаются с MCP `plugin-shadcn-shadcn` (search → examples → add command), затем CLI `pnpm dlx shadcn@latest add ...` из `apps/web`. См. также [.cursor/rules/context7-stack.mdc](.cursor/rules/context7-stack.mdc) для Context7 ID стека.
|
||||
**UI design contract:** [`docs/ui-design-contract.md`](docs/ui-design-contract.md) — surface `frame`, kit `apps/web/src/components/reui-kit/`.
|
||||
|
||||
Docs: [Introduction](https://reui.io/docs) · [llms.txt](https://reui.io/llms.txt) · [MCP](https://reui.io/docs/mcp) · [Agent Skills](https://reui.io/docs/agent-skills). Skill: `.claude/skills/reui` (`668fb463eb`).
|
||||
|
||||
UI-задачи начинаются с MCP `user-reui` (`surface: "frame"`) + `plugin-shadcn-shadcn`, затем CLI `pnpm dlx shadcn@latest add ...` из `apps/web`. См. также [.cursor/rules/context7-stack.mdc](.cursor/rules/context7-stack.mdc) для Context7 ID стека.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# EvoBGP web (Vite) — переменные окружения.
|
||||
# Скопируйте в apps/web/.env.local (файл в .gitignore) и заполните.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ReUI Pro/Ultimate — ключ с https://reui.io/account (для `pnpm dlx shadcn add @reui/*`)
|
||||
# ---------------------------------------------------------------------------
|
||||
# REUI_LICENSE_KEY=
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App Switcher — JSON с описанием шапки «Приложения» (fallback, когда portal
|
||||
# недоступен либо VITE_AUTH_ENABLED=false).
|
||||
# Схема: см. apps/web/src/lib/app-switcher-config.ts.
|
||||
# ---------------------------------------------------------------------------
|
||||
# VITE_APP_SWITCHER={"menuLabel":"Приложения","apps":[...]}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth-portal SSO
|
||||
# ---------------------------------------------------------------------------
|
||||
# Включает JWT-гейт через auth-portal вместо локального evobgp_api_token.
|
||||
# Пример:
|
||||
# VITE_AUTH_ENABLED=true
|
||||
# VITE_AUTH_PORTAL_URL=https://auth.shnt.top
|
||||
#
|
||||
# Backend опционально может отдавать GET /v1/auth/config
|
||||
# ({ "required": true, "portal_url": "https://auth.shnt.top" }) —
|
||||
# ответ имеет приоритет над VITE_* и позволяет менять режим без пересборки.
|
||||
# VITE_AUTH_ENABLED=false
|
||||
# VITE_AUTH_PORTAL_URL=http://localhost:5175
|
||||
@@ -11,7 +11,12 @@
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"registries": {
|
||||
"@reui": "https://reui.io/r/{style}/{name}.json"
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${REUI_LICENSE_KEY}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Smoke against local API (+ optional UI). Profile: compose microvps-full.
|
||||
*
|
||||
* Env:
|
||||
* - EVOBGP_E2E_API_URL (default http://127.0.0.1:8080)
|
||||
* - EVOBGP_E2E_BASE_URL (default http://127.0.0.1:5173)
|
||||
* - EVOBGP_E2E_TOKEN (default dev)
|
||||
*/
|
||||
const apiBase = process.env.EVOBGP_E2E_API_URL ?? 'http://127.0.0.1:8080'
|
||||
const uiBase = process.env.EVOBGP_E2E_BASE_URL ?? 'http://127.0.0.1:5173'
|
||||
const token = process.env.EVOBGP_E2E_TOKEN ?? 'dev'
|
||||
|
||||
test.describe('EvoBGP smoke', () => {
|
||||
test('API health and modules list', async ({ request }) => {
|
||||
const health = await request.get(`${apiBase}/v1/health`)
|
||||
expect(health.ok()).toBeTruthy()
|
||||
|
||||
const mods = await request.get(`${apiBase}/v1/modules?limit=10`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect([200, 401]).toContain(mods.status())
|
||||
})
|
||||
|
||||
test('UI loads when available', async ({ page }) => {
|
||||
test.skip(!process.env.EVOBGP_E2E_UI, 'set EVOBGP_E2E_UI=1 to enable UI smoke')
|
||||
await page.goto(uiBase)
|
||||
await page.waitForLoadState('domcontentloaded')
|
||||
await expect(page.locator('body')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import globals from 'globals'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist', 'src/routeTree.gen.ts'] },
|
||||
{ ignores: ['dist', 'src/routeTree.gen.ts', 'src/components/blocks/**'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsr generate && tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"openapi:gen": "openapi-typescript ../../docs/openapi.yaml -o src/types/api.gen.ts",
|
||||
"openapi:check": "openapi-typescript ../../docs/openapi.yaml -o src/types/api.gen.check.ts && diff -q src/types/api.gen.ts src/types/api.gen.check.ts && rm -f src/types/api.gen.check.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.0.0",
|
||||
@@ -40,22 +43,24 @@
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"@tanstack/router-plugin": "^1.130.0",
|
||||
"@tanstack/router-cli": "^1.130.0",
|
||||
"@tanstack/router-plugin": "^1.130.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.0.0",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.0",
|
||||
"globals": "^15.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"happy-dom": "^18.0.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"tw-animate-css": "^1.0.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 60_000,
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
use: {
|
||||
baseURL: process.env.EVOBGP_E2E_BASE_URL ?? 'http://127.0.0.1:5173',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { Plus, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
|
||||
import { AccessApiKeysGrid } from '@/components/access/access-api-keys-grid'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { FrameDataGrid } from '@/components/reui-kit'
|
||||
import { ApiKeyCreateDialog } from '@/components/access/api-key-create-dialog'
|
||||
import { ApiKeyTokenDialog } from '@/components/access/api-key-token-dialog'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
@@ -47,7 +47,7 @@ export function AccessApiKeysCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataGridCard
|
||||
<FrameDataGrid
|
||||
title="API-ключи"
|
||||
description="Управление ключами tenant. Полный токен показывается только при создании и ротации."
|
||||
actions={
|
||||
@@ -85,7 +85,7 @@ export function AccessApiKeysCard({
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</FrameDataGrid>
|
||||
|
||||
<ApiKeyCreateDialog
|
||||
open={createOpen}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Info } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@evobgp/ui/components/card'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -16,6 +8,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@evobgp/ui/components/tooltip'
|
||||
|
||||
import { PanelCard } from '@/components/panel-card'
|
||||
|
||||
export function AnalyticsCardShell({
|
||||
title,
|
||||
description,
|
||||
@@ -33,32 +27,36 @@ export function AnalyticsCardShell({
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
const titleNode = (
|
||||
<span className="flex items-center gap-2">
|
||||
{title}
|
||||
{info ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
className="inline-flex text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Подробнее"
|
||||
>
|
||||
<Info className="size-3.5" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs text-xs">
|
||||
{info}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className={cn('flex h-full flex-col gap-0 overflow-hidden', className)}>
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3 border-b py-4">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
{title}
|
||||
{info ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
className="inline-flex text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Подробнее"
|
||||
>
|
||||
<Info className="size-3.5" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs text-xs">
|
||||
{info}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</CardTitle>
|
||||
{description ? <CardDescription>{description}</CardDescription> : null}
|
||||
</div>
|
||||
{actions ? <div className="shrink-0">{actions}</div> : null}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col gap-5 p-5">{children}</CardContent>
|
||||
{footer ? <CardFooter className="gap-2 border-t p-4">{footer}</CardFooter> : null}
|
||||
</Card>
|
||||
<PanelCard
|
||||
title={titleNode}
|
||||
description={description}
|
||||
actions={actions}
|
||||
footer={footer}
|
||||
className={cn('overflow-hidden', className)}
|
||||
contentClassName="flex flex-col gap-5 py-5"
|
||||
footerClassName={footer ? 'gap-2 px-5 py-4' : undefined}
|
||||
>
|
||||
{children}
|
||||
</PanelCard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Minus, TrendingDown, TrendingUp } from 'lucide-react'
|
||||
|
||||
import { KpiStatGrid, type KpiStatItem } from '@/components/kpi-stat-grid'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
|
||||
export type AnalyticsKpiItem = {
|
||||
@@ -12,39 +12,42 @@ export type AnalyticsKpiItem = {
|
||||
}
|
||||
}
|
||||
|
||||
const TONE_CLASS = {
|
||||
success: 'text-success',
|
||||
warning: 'text-warning',
|
||||
destructive: 'text-destructive',
|
||||
muted: 'text-muted-foreground',
|
||||
} as const
|
||||
|
||||
function DeltaIcon({ direction }: { direction: AnalyticsKpiItem['delta'] extends infer D ? D extends { direction: infer Dir } ? Dir : never : never }) {
|
||||
if (direction === 'up') return <TrendingUp className="size-3" />
|
||||
if (direction === 'down') return <TrendingDown className="size-3" />
|
||||
return <Minus className="size-3" />
|
||||
function deltaBadgeVariant(
|
||||
tone?: 'success' | 'warning' | 'destructive' | 'muted',
|
||||
): 'success-light' | 'warning-light' | 'destructive-light' | 'outline' {
|
||||
if (tone === 'success') return 'success-light'
|
||||
if (tone === 'warning') return 'warning-light'
|
||||
if (tone === 'destructive') return 'destructive-light'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
export function AnalyticsKpiRow({ items, className }: { items: AnalyticsKpiItem[]; className?: string }) {
|
||||
function toKpiItem(item: AnalyticsKpiItem): KpiStatItem {
|
||||
return {
|
||||
id: item.label,
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
footer: item.delta ? (
|
||||
<Badge variant={deltaBadgeVariant(item.delta.tone)} size="sm">
|
||||
{item.delta.label}
|
||||
</Badge>
|
||||
) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/** Compact KPI row inside analytics panels (stats-12 embedded tiles). */
|
||||
export function AnalyticsKpiRow({
|
||||
items,
|
||||
className,
|
||||
}: {
|
||||
items: AnalyticsKpiItem[]
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('grid gap-4 sm:grid-cols-3', className)}>
|
||||
{items.map((item) => (
|
||||
<div key={item.label} className="min-w-0 space-y-1">
|
||||
<p className="text-xs text-muted-foreground">{item.label}</p>
|
||||
<p className="text-2xl font-semibold tracking-tight tabular-nums">{item.value}</p>
|
||||
{item.delta ? (
|
||||
<p
|
||||
className={cn(
|
||||
'flex items-center gap-1 text-xs',
|
||||
TONE_CLASS[item.delta.tone ?? 'muted'],
|
||||
)}
|
||||
>
|
||||
<DeltaIcon direction={item.delta.direction} />
|
||||
{item.delta.label}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<KpiStatGrid
|
||||
items={items.map(toKpiItem)}
|
||||
embedded
|
||||
className={cn(className)}
|
||||
aria-label="Показатели"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import { cn } from '@evobgp/ui/lib/utils'
|
||||
|
||||
export function AnalyticsProgress({
|
||||
label,
|
||||
hint,
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
label: string
|
||||
hint?: string
|
||||
value: number
|
||||
className?: string
|
||||
}) {
|
||||
@@ -21,9 +23,10 @@ export function AnalyticsProgress({
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className="font-medium tabular-nums">{clamped}%</span>
|
||||
</div>
|
||||
<Progress value={clamped} className="gap-0">
|
||||
{hint ? <p className="text-xs leading-snug text-muted-foreground">{hint}</p> : null}
|
||||
<Progress value={clamped} className="w-full gap-0">
|
||||
<ProgressTrack className="h-2">
|
||||
<ProgressIndicator className="bg-foreground" />
|
||||
<ProgressIndicator />
|
||||
</ProgressTrack>
|
||||
</Progress>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function ChartDonutMetric({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-6', className)}>
|
||||
<div className={cn('flex flex-col items-center justify-start gap-4 sm:flex-row sm:gap-6', className)}>
|
||||
<ChartContainer config={chartConfig} className="mx-0 aspect-square h-44 w-44 shrink-0">
|
||||
<PieChart>
|
||||
<Pie
|
||||
@@ -75,7 +75,7 @@ export function ChartDonutMetric({
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
|
||||
<ul className="min-w-0 flex-1 space-y-3">
|
||||
<ul className="flex w-full min-w-0 max-w-xs flex-col gap-3 sm:w-auto sm:min-w-[10rem]">
|
||||
{slices.map((slice) => {
|
||||
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
|
||||
return (
|
||||
|
||||
@@ -50,14 +50,14 @@ export function DashboardNetworkCapacityCard({
|
||||
|
||||
const deltaLabel =
|
||||
mode === 'peers'
|
||||
? `${peers.filter((p) => p.enabled !== false && p.session_state === 'Established').length} Established`
|
||||
: `${speakers.filter((s) => s.live?.agent_ok).length} online`
|
||||
? `${peers.filter((p) => p.enabled !== false && p.session_state === 'Established').length} установлено`
|
||||
: `${speakers.filter((s) => s.live?.agent_ok).length} в сети`
|
||||
|
||||
return (
|
||||
<AnalyticsCardShell
|
||||
title="Загрузка BGP"
|
||||
description="Текущая утилизация сессий по пирам и спикерам"
|
||||
info="Каждый столбец — enabled peer или speaker. Высота отражает Established/online."
|
||||
info="Каждый столбец — включённый пир или спикер. Высота отражает установленную сессию или доступность."
|
||||
actions={
|
||||
<AnalyticsSegmentControl
|
||||
value={mode}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AnalyticsKpiRow } from '@/components/analytics/analytics-kpi-row'
|
||||
import { AnalyticsProgress } from '@/components/analytics/analytics-progress'
|
||||
import {
|
||||
deploymentProgress,
|
||||
deploymentProgressMeta,
|
||||
recentPlatformActivity,
|
||||
} from '@/lib/metrics'
|
||||
import { runningJobCount } from '@/queries/overview'
|
||||
@@ -48,6 +49,7 @@ export function DashboardPlatformCard({
|
||||
peersEnabled > 0 ? Math.round((peersEstablished / peersEnabled) * 100) : null
|
||||
|
||||
const deploy = useMemo(() => deploymentProgress(speakers), [speakers])
|
||||
const deployMeta = useMemo(() => deploymentProgressMeta(deploy), [deploy])
|
||||
const activity = useMemo(
|
||||
() => recentPlatformActivity(jobs, revisions, peers, speakers),
|
||||
[jobs, revisions, peers, speakers],
|
||||
@@ -74,7 +76,7 @@ export function DashboardPlatformCard({
|
||||
label:
|
||||
bgpPct === null
|
||||
? 'нет включённых пиров'
|
||||
: `${peersEstablished} Established`,
|
||||
: `${peersEstablished} установлено`,
|
||||
tone: (bgpPct !== null && bgpPct >= 90
|
||||
? 'success'
|
||||
: bgpPct !== null && bgpPct < 70
|
||||
@@ -87,16 +89,14 @@ export function DashboardPlatformCard({
|
||||
value: loading ? '—' : String(riskCount),
|
||||
delta: {
|
||||
direction: (riskCount > 0 ? 'down' : 'up') as 'up' | 'down',
|
||||
label: riskCount > 0 ? `${failedJobs} задач, ${peersMismatch} mismatch` : 'в норме',
|
||||
label: riskCount > 0 ? `${failedJobs} задач, ${peersMismatch} расхождений` : 'в норме',
|
||||
tone: (riskCount > 0 ? 'destructive' : 'success') as 'destructive' | 'success',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const progressLabel =
|
||||
deploy.mode === 'revision'
|
||||
? `Синхронизация ревизий (${deploy.synced}/${deploy.total})`
|
||||
: `Спикеры online (${deploy.synced}/${deploy.total})`
|
||||
const progressLabel = deployMeta.label
|
||||
const progressHint = deployMeta.hint
|
||||
|
||||
return (
|
||||
<AnalyticsCardShell
|
||||
@@ -122,7 +122,11 @@ export function DashboardPlatformCard({
|
||||
}
|
||||
>
|
||||
<AnalyticsKpiRow items={kpis} />
|
||||
<AnalyticsProgress label={progressLabel} value={loading ? 0 : deploy.percent} />
|
||||
<AnalyticsProgress
|
||||
label={progressLabel}
|
||||
hint={progressHint}
|
||||
value={loading ? 0 : deploy.percent}
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Недавняя активность</span>
|
||||
|
||||
@@ -10,4 +10,3 @@ export { DashboardOperationsFlowCard } from './dashboard-operations-flow-card'
|
||||
export { DashboardPlatformCard } from './dashboard-platform-card'
|
||||
export { MonitoringHealthCard } from './monitoring-health-card'
|
||||
export { NetworkOverviewAnalyticsCard } from './network-overview-analytics-card'
|
||||
export { OperationsAnalyticsCard } from './operations-analytics-card'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AnalyticsCardShell } from '@/components/analytics/analytics-card-shell'
|
||||
import { ChartDonutMetric } from '@/components/analytics/chart-donut-metric'
|
||||
import { DonutBreakdownCard } from '@/components/patterns/donut-breakdown-card'
|
||||
import { readinessBreakdown } from '@/lib/metrics'
|
||||
import type { ReadyStatus } from '@/queries/monitoring'
|
||||
|
||||
@@ -13,21 +12,25 @@ export function MonitoringHealthCard({
|
||||
loading?: boolean
|
||||
}) {
|
||||
const slices = readinessBreakdown(ready, healthOk)
|
||||
const total = slices.reduce((sum, slice) => sum + slice.count, 0)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<DonutBreakdownCard
|
||||
title="Доступность системы"
|
||||
description="Проверки живучести и готовности"
|
||||
slices={[]}
|
||||
centerLabel="Проверки"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnalyticsCardShell
|
||||
<DonutBreakdownCard
|
||||
title="Доступность системы"
|
||||
description="Health и readiness checks"
|
||||
info="Donut отражает результат GET /v1/health и checks из GET /v1/ready."
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">
|
||||
Загрузка…
|
||||
</div>
|
||||
) : (
|
||||
<ChartDonutMetric slices={slices} centerLabel="Checks" centerValue={total} />
|
||||
)}
|
||||
</AnalyticsCardShell>
|
||||
description="GET /v1/health · GET /v1/ready"
|
||||
slices={slices}
|
||||
centerLabel="Проверки"
|
||||
badge={healthOk ? 'API OK' : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AnalyticsCardShell } from '@/components/analytics/analytics-card-shell'
|
||||
import { AnalyticsKpiRow } from '@/components/analytics/analytics-kpi-row'
|
||||
import { ChartDonutMetric } from '@/components/analytics/chart-donut-metric'
|
||||
import { DonutBreakdownCard } from '@/components/patterns/donut-breakdown-card'
|
||||
import { SegmentedProgressCard } from '@/components/patterns/segmented-progress-card'
|
||||
import { peerSessionBreakdown } from '@/lib/metrics'
|
||||
import { aggregateNetworkMetrics } from '@/queries/overview'
|
||||
import type { PeerRow, SpeakerRow } from '@/types/api'
|
||||
@@ -16,51 +15,44 @@ export function NetworkOverviewAnalyticsCard({
|
||||
}) {
|
||||
const net = aggregateNetworkMetrics(peers, speakers)
|
||||
const slices = peerSessionBreakdown(peers)
|
||||
const total = slices.reduce((sum, slice) => sum + slice.count, 0)
|
||||
const speakersPct =
|
||||
net.speakersTotal > 0 ? Math.round((net.speakersOnline / net.speakersTotal) * 100) : 0
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<DonutBreakdownCard
|
||||
title="Сводка BGP"
|
||||
description="Установленные сессии и спикеры"
|
||||
slices={[]}
|
||||
centerLabel="Пиры"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnalyticsCardShell
|
||||
title="Сводка BGP"
|
||||
description="Established, online и mismatch по live-данным"
|
||||
info="Снимок текущего состояния пиров и спикеров."
|
||||
>
|
||||
<AnalyticsKpiRow
|
||||
items={[
|
||||
{
|
||||
label: 'Пиры Established',
|
||||
value: loading ? '—' : `${net.peersEstablished}/${net.peersEnabled}`,
|
||||
delta: {
|
||||
direction: net.peersMismatch > 0 ? 'down' : 'up',
|
||||
label: net.peersMismatch > 0 ? `${net.peersMismatch} mismatch` : 'сессии в норме',
|
||||
tone: net.peersMismatch > 0 ? 'warning' : 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Спикеры online',
|
||||
value: loading ? '—' : `${net.speakersOnline}/${net.speakersTotal}`,
|
||||
delta: {
|
||||
direction: net.speakersOnline < net.speakersTotal ? 'down' : 'up',
|
||||
label:
|
||||
net.speakersOnline < net.speakersTotal
|
||||
? `${net.speakersTotal - net.speakersOnline} offline`
|
||||
: 'все online',
|
||||
tone: net.speakersOnline < net.speakersTotal ? 'warning' : 'success',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Пиры всего',
|
||||
value: loading ? '—' : String(net.peersTotal),
|
||||
delta: { direction: 'neutral', label: 'в каталоге', tone: 'muted' },
|
||||
},
|
||||
]}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<DonutBreakdownCard
|
||||
title="Сводка BGP"
|
||||
description="Распределение состояний пиров"
|
||||
slices={slices}
|
||||
centerLabel="Пиры"
|
||||
badge={net.peersMismatch > 0 ? `${net.peersMismatch} расхождений` : 'в норме'}
|
||||
/>
|
||||
{loading ? (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">
|
||||
Загрузка…
|
||||
</div>
|
||||
) : (
|
||||
<ChartDonutMetric slices={slices} centerLabel="Пиры" centerValue={total} />
|
||||
)}
|
||||
</AnalyticsCardShell>
|
||||
<SegmentedProgressCard
|
||||
title="Спикеры"
|
||||
description="Доступность live-агентов"
|
||||
primary={{
|
||||
value: `${net.speakersOnline}/${net.speakersTotal}`,
|
||||
label: 'Online',
|
||||
percent: speakersPct,
|
||||
}}
|
||||
secondary={{
|
||||
value: net.speakersTotal - net.speakersOnline,
|
||||
label: 'Offline',
|
||||
percent: 100 - speakersPct,
|
||||
}}
|
||||
footer={`Пиры Established: ${net.peersEstablished}/${net.peersEnabled}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { AnalyticsCardShell } from '@/components/analytics/analytics-card-shell'
|
||||
import { AnalyticsKpiRow } from '@/components/analytics/analytics-kpi-row'
|
||||
import { ChartDonutMetric } from '@/components/analytics/chart-donut-metric'
|
||||
import { jobStatusBreakdown } from '@/lib/metrics'
|
||||
import type { JobRow, RevisionRow } from '@/types/api'
|
||||
|
||||
export function OperationsAnalyticsCard({
|
||||
jobs,
|
||||
revisions,
|
||||
loading,
|
||||
}: {
|
||||
jobs: JobRow[]
|
||||
revisions: RevisionRow[]
|
||||
loading?: boolean
|
||||
}) {
|
||||
const running = jobs.filter((j) => ['running', 'queued'].includes(j.status.toLowerCase())).length
|
||||
const failed = jobs.filter((j) =>
|
||||
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
||||
).length
|
||||
const slices = jobStatusBreakdown(jobs)
|
||||
const total = slices.reduce((sum, slice) => sum + slice.count, 0)
|
||||
|
||||
return (
|
||||
<AnalyticsCardShell
|
||||
title="Операции и задачи"
|
||||
description="Статистика ревизий и фоновых jobs"
|
||||
info="Данные из GET /v1/jobs и /v1/revisions."
|
||||
>
|
||||
<AnalyticsKpiRow
|
||||
items={[
|
||||
{
|
||||
label: 'Ревизий',
|
||||
value: loading ? '—' : String(revisions.length),
|
||||
delta: { direction: 'neutral', label: 'в выборке', tone: 'muted' },
|
||||
},
|
||||
{
|
||||
label: 'Активных задач',
|
||||
value: loading ? '—' : String(running),
|
||||
delta: {
|
||||
direction: running > 0 ? 'up' : 'neutral',
|
||||
label: running > 0 ? 'выполняются' : 'очередь пуста',
|
||||
tone: (running > 0 ? 'warning' : 'muted') as 'warning' | 'muted',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'С ошибкой',
|
||||
value: loading ? '—' : String(failed),
|
||||
delta: {
|
||||
direction: failed > 0 ? 'down' : 'up',
|
||||
label: failed > 0 ? 'требуют внимания' : 'в норме',
|
||||
tone: failed > 0 ? 'destructive' : 'success',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{loading ? (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">
|
||||
Загрузка…
|
||||
</div>
|
||||
) : (
|
||||
<ChartDonutMetric slices={slices} centerLabel="Задачи" centerValue={total} />
|
||||
)}
|
||||
</AnalyticsCardShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@evobgp/ui/components/dropdown-menu'
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@evobgp/ui/components/sidebar'
|
||||
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
|
||||
|
||||
import {
|
||||
APP_SWITCHER_ICONS,
|
||||
CURRENT_APP_ID,
|
||||
getCurrentApp,
|
||||
} from '@/lib/app-switcher-config'
|
||||
import { useAppSwitcherConfig } from '@/hooks/use-app-switcher'
|
||||
|
||||
/** Sidebar app switcher — shared chrome etalon EvoBGP. @see https://reui.io/preview/base/app-shell-12 */
|
||||
export function AppSwitcher() {
|
||||
const { isMobile } = useSidebar()
|
||||
const { config, isLoading } = useAppSwitcherConfig()
|
||||
const current = getCurrentApp(config)
|
||||
const CurrentIcon = APP_SWITCHER_ICONS[current.icon]
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<SidebarMenuButton size="lg" className="aria-expanded:bg-muted" />
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="flex aspect-square size-8 items-center justify-center rounded-md bg-primary text-primary-foreground"
|
||||
aria-hidden
|
||||
>
|
||||
<CurrentIcon className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{current.name}</span>
|
||||
{current.subtitle ? (
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{current.subtitle}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<ChevronsUpDownIcon className="ml-auto size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-56 rounded-lg"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
>
|
||||
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
{isLoading ? 'Загрузка…' : config.menuLabel}
|
||||
</div>
|
||||
{config.apps.map((app) => {
|
||||
const Icon = APP_SWITCHER_ICONS[app.icon]
|
||||
const isCurrent = app.id === CURRENT_APP_ID
|
||||
|
||||
if (isCurrent) {
|
||||
return (
|
||||
<DropdownMenuItem key={app.id} disabled>
|
||||
<Icon />
|
||||
{app.name}
|
||||
<CheckIcon className="ml-auto size-4" />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={app.id}
|
||||
nativeButton={false}
|
||||
render={<a href={app.url} />}
|
||||
>
|
||||
<Icon />
|
||||
{app.name}
|
||||
{app.shortcut ? (
|
||||
<DropdownMenuShortcut>{app.shortcut}</DropdownMenuShortcut>
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
@@ -40,14 +40,12 @@ export function BadgeTabs({
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={onValueChange}
|
||||
orientation="horizontal"
|
||||
className={cn('w-full', className)}
|
||||
>
|
||||
<TabsList
|
||||
variant="line"
|
||||
className={cn(
|
||||
'mb-4 w-full justify-start gap-6',
|
||||
listClassName,
|
||||
)}
|
||||
className={cn('mb-3.5 w-full justify-start gap-6', listClassName)}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<TabsTrigger key={item.value} value={item.value} className="gap-2">
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import { useState, type ComponentType } from "react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@evobgp/ui/lib/utils"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@evobgp/ui/components/tabs"
|
||||
|
||||
import { BillingTab } from "./billing-tab"
|
||||
import { SETTINGS_TAB_ITEMS } from "./data"
|
||||
import { NotificationsTab } from "./notifications-tab"
|
||||
import { ProfileTab } from "./profile-tab"
|
||||
import { SecurityTab } from "./security-tab"
|
||||
|
||||
const TAB_COMPONENTS: Record<string, ComponentType> = {
|
||||
profile: ProfileTab,
|
||||
security: SecurityTab,
|
||||
notifications: NotificationsTab,
|
||||
billing: BillingTab,
|
||||
}
|
||||
|
||||
// ── Settings Navigation ──
|
||||
|
||||
function SettingsNavigation({
|
||||
isMobile,
|
||||
activeValue,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
activeValue: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("min-w-0", isMobile ? "w-full" : "w-40 shrink-0")}>
|
||||
{isMobile ? (
|
||||
<div className="-mx-1 overflow-x-auto px-1 pb-1">
|
||||
<TabsList className="h-auto w-max min-w-max justify-start gap-1 bg-transparent p-0">
|
||||
{SETTINGS_TAB_ITEMS.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className={cn(
|
||||
"w-full justify-start gap-3 px-3 py-1.5 shadow-none",
|
||||
activeValue === tab.value ? "bg-muted!" : "bg-transparent"
|
||||
)}
|
||||
>
|
||||
{tab.icon}
|
||||
<span className="truncate">{tab.label}</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
) : (
|
||||
<TabsList className="h-auto w-full flex-col items-stretch gap-1 bg-transparent p-0">
|
||||
{SETTINGS_TAB_ITEMS.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className={cn(
|
||||
"w-full justify-start gap-3 px-3 py-1.5 shadow-none",
|
||||
activeValue === tab.value ? "bg-muted!" : "bg-transparent"
|
||||
)}
|
||||
>
|
||||
{tab.icon}
|
||||
<span className="truncate">{tab.label}</span>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AccountSettings() {
|
||||
const isMobile = useIsMobile()
|
||||
const [activeTab, setActiveTab] = useState("profile")
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl space-y-8">
|
||||
{/* Header */}
|
||||
<header className="px-1">
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
Account Settings
|
||||
</h1>
|
||||
<p className="text-muted-foreground max-w-2xl text-sm leading-relaxed">
|
||||
Update your profile, access, notifications, and billing preferences.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
orientation={isMobile ? "horizontal" : "vertical"}
|
||||
className={cn("w-full gap-4 lg:gap-8")}
|
||||
>
|
||||
<SettingsNavigation isMobile={isMobile} activeValue={activeTab} />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{SETTINGS_TAB_ITEMS.map((tab) => {
|
||||
const TabComponent = TAB_COMPONENTS[tab.value]
|
||||
|
||||
return (
|
||||
<TabsContent key={tab.value} value={tab.value} className="mt-0">
|
||||
<TabComponent />
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useState } from "react"
|
||||
import { Badge } from "@/components/reui/badge"
|
||||
|
||||
import { Button } from "@evobgp/ui/components/button"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@evobgp/ui/components/field"
|
||||
import { Input } from "@evobgp/ui/components/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@evobgp/ui/components/input-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@evobgp/ui/components/select"
|
||||
|
||||
import { BILLING_PLANS, COUNTRIES } from "./data"
|
||||
import { SettingRow } from "./setting-row"
|
||||
import { SettingsCard } from "./settings-card"
|
||||
import { SettingsFieldGroup } from "./settings-field-group"
|
||||
import { createSelectValueHandler, getOptionLabel } from "./utils"
|
||||
|
||||
export function BillingTab() {
|
||||
const [country, setCountry] = useState("us")
|
||||
const handleCountryChange = createSelectValueHandler(setCountry)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Card */}
|
||||
<SettingsCard title="Plan and billing" description="Current subscription">
|
||||
<SettingsFieldGroup
|
||||
legend="Plan and billing"
|
||||
description="Review your current subscription and plan options."
|
||||
>
|
||||
{BILLING_PLANS.map((plan, index) => (
|
||||
<SettingRow
|
||||
key={plan.id}
|
||||
title={plan.name}
|
||||
titleAddon={
|
||||
plan.current ? (
|
||||
<Badge variant="info-light" size="sm">
|
||||
Current
|
||||
</Badge>
|
||||
) : null
|
||||
}
|
||||
description={
|
||||
<>
|
||||
<span className="text-foreground font-medium">
|
||||
{plan.price}
|
||||
</span>{" "}
|
||||
/ {plan.period} · {plan.features.join(", ")}
|
||||
</>
|
||||
}
|
||||
last={index === BILLING_PLANS.length - 1}
|
||||
>
|
||||
<Button variant={plan.current ? "outline" : "ghost"} size="sm">
|
||||
{plan.current ? "Manage" : "Switch"}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
))}
|
||||
</SettingsFieldGroup>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard
|
||||
title="Billing details"
|
||||
description="Invoices and payments"
|
||||
footer={<Button>Update billing</Button>}
|
||||
>
|
||||
<SettingsFieldGroup
|
||||
legend="Billing details"
|
||||
description="Manage invoice contacts and payment details."
|
||||
>
|
||||
<SettingRow
|
||||
title="Billing email"
|
||||
description="Receives invoices and renewal notices."
|
||||
labelFor="settings-7-billing-email"
|
||||
>
|
||||
<Input
|
||||
id="settings-7-billing-email"
|
||||
defaultValue="[email protected]"
|
||||
type="email"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Invoice profile"
|
||||
description="Business details used on receipts and tax forms."
|
||||
contentClassName="@md/field-group:w-[22rem]"
|
||||
>
|
||||
<FieldSet className="w-full gap-3">
|
||||
<FieldLegend className="sr-only">Invoice profile</FieldLegend>
|
||||
<FieldDescription className="sr-only">
|
||||
Company identity used for invoices and tax documents.
|
||||
</FieldDescription>
|
||||
|
||||
<FieldGroup className="gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-company-name">
|
||||
Company name
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="settings-7-company-name"
|
||||
defaultValue="Acme Labs"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-billing-country">
|
||||
Country
|
||||
</FieldLabel>
|
||||
<Select value={country} onValueChange={handleCountryChange}>
|
||||
<SelectTrigger
|
||||
id="settings-7-billing-country"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue>
|
||||
{getOptionLabel(COUNTRIES, country)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{COUNTRIES.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Tax ID"
|
||||
description="Optional number used for VAT or company invoices."
|
||||
labelFor="settings-7-tax-id"
|
||||
>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon align="inline-start">
|
||||
<InputGroupText>VAT</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="settings-7-tax-id"
|
||||
defaultValue="US-2048-ACME"
|
||||
/>
|
||||
</InputGroup>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Card on file"
|
||||
description="Used for monthly renewals."
|
||||
last
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Visa ending in 4242
|
||||
</span>
|
||||
<Button variant="outline">Update</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
</SettingsFieldGroup>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
"use client"
|
||||
|
||||
import { type ReactNode } from "react"
|
||||
import { UserIcon, ShieldIcon, BellIcon, CreditCardIcon, MonitorIcon, SmartphoneIcon } from "lucide-react"
|
||||
|
||||
// ── Types ──
|
||||
|
||||
export type SelectOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type BillingPlan = {
|
||||
id: string
|
||||
name: string
|
||||
price: string
|
||||
period: string
|
||||
current: boolean
|
||||
features: string[]
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
id: string
|
||||
device: string
|
||||
browser: string
|
||||
location: string
|
||||
lastActive: string
|
||||
current: boolean
|
||||
icon: ReactNode
|
||||
}
|
||||
|
||||
export type SettingsTabItem = {
|
||||
value: string
|
||||
label: string
|
||||
icon: ReactNode
|
||||
}
|
||||
|
||||
// ── Data ──
|
||||
|
||||
export const SETTINGS_TAB_ITEMS: SettingsTabItem[] = [
|
||||
{
|
||||
value: "profile",
|
||||
label: "My Profile",
|
||||
icon: (
|
||||
<UserIcon aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "security",
|
||||
label: "Security",
|
||||
icon: (
|
||||
<ShieldIcon aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "notifications",
|
||||
label: "Notifications",
|
||||
icon: (
|
||||
<BellIcon aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "billing",
|
||||
label: "Billing",
|
||||
icon: (
|
||||
<CreditCardIcon aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export const TIMEZONES: SelectOption[] = [
|
||||
{ value: "utc-8", label: "UTC-8 (Pacific Time)" },
|
||||
{ value: "utc-5", label: "UTC-5 (Eastern Time)" },
|
||||
{ value: "utc+0", label: "UTC+0 (London)" },
|
||||
{ value: "utc+1", label: "UTC+1 (Berlin)" },
|
||||
{ value: "utc+9", label: "UTC+9 (Tokyo)" },
|
||||
]
|
||||
|
||||
export const ROLES: SelectOption[] = [
|
||||
{ value: "engineering-lead", label: "Engineering Lead" },
|
||||
{ value: "developer", label: "Developer" },
|
||||
{ value: "designer", label: "Designer" },
|
||||
{ value: "product-manager", label: "Product Manager" },
|
||||
]
|
||||
|
||||
export const COUNTRIES: SelectOption[] = [
|
||||
{ value: "us", label: "United States" },
|
||||
{ value: "uk", label: "United Kingdom" },
|
||||
{ value: "de", label: "Germany" },
|
||||
{ value: "uz", label: "Uzbekistan" },
|
||||
]
|
||||
|
||||
export const TIMEOUTS: SelectOption[] = [
|
||||
{ value: "5", label: "5 minutes" },
|
||||
{ value: "10", label: "10 minutes" },
|
||||
{ value: "15", label: "15 minutes" },
|
||||
{ value: "30", label: "30 minutes" },
|
||||
]
|
||||
|
||||
export const QUIET_HOURS: SelectOption[] = [
|
||||
{ value: "18:00", label: "6:00 PM" },
|
||||
{ value: "20:00", label: "8:00 PM" },
|
||||
{ value: "22:00", label: "10:00 PM" },
|
||||
{ value: "23:00", label: "11:00 PM" },
|
||||
]
|
||||
|
||||
export const QUIET_HOURS_END: SelectOption[] = [
|
||||
{ value: "06:00", label: "6:00 AM" },
|
||||
{ value: "08:00", label: "8:00 AM" },
|
||||
{ value: "09:00", label: "9:00 AM" },
|
||||
{ value: "10:00", label: "10:00 AM" },
|
||||
]
|
||||
|
||||
export const DIGEST_CADENCE: SelectOption[] = [
|
||||
{ value: "daily", label: "Daily summary" },
|
||||
{ value: "weekly", label: "Weekly digest" },
|
||||
{ value: "mentions", label: "Only mentions" },
|
||||
]
|
||||
|
||||
export const RECOVERY_METHODS: SelectOption[] = [
|
||||
{ value: "authenticator", label: "Authenticator first" },
|
||||
{ value: "sms", label: "SMS fallback" },
|
||||
{ value: "email", label: "Email fallback" },
|
||||
]
|
||||
|
||||
export const BILLING_PLANS: BillingPlan[] = [
|
||||
{
|
||||
id: "free",
|
||||
name: "Free",
|
||||
price: "$0",
|
||||
period: "forever",
|
||||
current: false,
|
||||
features: ["1 workspace", "Basic exports", "Community support"],
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro",
|
||||
price: "$29",
|
||||
period: "per month",
|
||||
current: true,
|
||||
features: ["Unlimited workspaces", "Automations", "Priority support"],
|
||||
},
|
||||
{
|
||||
id: "team",
|
||||
name: "Team",
|
||||
price: "$79",
|
||||
period: "per month",
|
||||
current: false,
|
||||
features: ["Everything in Pro", "SSO", "Shared billing"],
|
||||
},
|
||||
]
|
||||
|
||||
export const SESSIONS: Session[] = [
|
||||
{
|
||||
id: "sess-1",
|
||||
device: "macOS",
|
||||
browser: "Chrome",
|
||||
location: "San Francisco, CA",
|
||||
lastActive: "Active now",
|
||||
current: true,
|
||||
icon: (
|
||||
<MonitorIcon aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "sess-2",
|
||||
device: "iPhone",
|
||||
browser: "Safari",
|
||||
location: "San Francisco, CA",
|
||||
lastActive: "2 hours ago",
|
||||
current: false,
|
||||
icon: (
|
||||
<SmartphoneIcon aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "sess-3",
|
||||
device: "Windows",
|
||||
browser: "Firefox",
|
||||
location: "New York, NY",
|
||||
lastActive: "3 days ago",
|
||||
current: false,
|
||||
icon: (
|
||||
<MonitorIcon aria-hidden="true" />
|
||||
),
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import { Button } from "@evobgp/ui/components/button"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@evobgp/ui/components/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@evobgp/ui/components/select"
|
||||
import { Switch } from "@evobgp/ui/components/switch"
|
||||
|
||||
import { DIGEST_CADENCE, QUIET_HOURS, QUIET_HOURS_END, TIMEOUTS } from "./data"
|
||||
import { SettingRow } from "./setting-row"
|
||||
import { SettingsCard } from "./settings-card"
|
||||
import { SettingsFieldGroup } from "./settings-field-group"
|
||||
import { createSelectValueHandler, getOptionLabel } from "./utils"
|
||||
|
||||
export function NotificationsTab() {
|
||||
const [autoDismiss, setAutoDismiss] = useState("10")
|
||||
const [quietStart, setQuietStart] = useState("20:00")
|
||||
const [quietEnd, setQuietEnd] = useState("08:00")
|
||||
const [digestCadence, setDigestCadence] = useState("weekly")
|
||||
const handleAutoDismissChange = createSelectValueHandler(setAutoDismiss)
|
||||
const handleQuietStartChange = createSelectValueHandler(setQuietStart)
|
||||
const handleQuietEndChange = createSelectValueHandler(setQuietEnd)
|
||||
const handleDigestCadenceChange = createSelectValueHandler(setDigestCadence)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Card */}
|
||||
<SettingsCard title="Activity alerts" description="Desktop and badge">
|
||||
<SettingsFieldGroup
|
||||
legend="Activity alerts"
|
||||
description="Control desktop, badge, and sound alerts."
|
||||
>
|
||||
<SettingRow
|
||||
title="Desktop notifications"
|
||||
description="Show alerts for mentions and approvals."
|
||||
labelFor="settings-7-desktop-notifications"
|
||||
>
|
||||
<Switch id="settings-7-desktop-notifications" defaultChecked />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Quiet hours"
|
||||
description="Hold non-urgent alerts outside your workday."
|
||||
contentClassName="@md/field-group:w-[22rem]"
|
||||
>
|
||||
<FieldSet className="w-full gap-3">
|
||||
<FieldLegend className="sr-only">Quiet hours</FieldLegend>
|
||||
<FieldDescription className="sr-only">
|
||||
Define when non-urgent activity should stay muted.
|
||||
</FieldDescription>
|
||||
|
||||
<FieldGroup className="gap-3 sm:grid sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-quiet-start">
|
||||
Start
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={quietStart}
|
||||
onValueChange={handleQuietStartChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="settings-7-quiet-start"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue>
|
||||
{getOptionLabel(QUIET_HOURS, quietStart)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{QUIET_HOURS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-quiet-end">End</FieldLabel>
|
||||
<Select value={quietEnd} onValueChange={handleQuietEndChange}>
|
||||
<SelectTrigger id="settings-7-quiet-end" className="w-full">
|
||||
<SelectValue>
|
||||
{getOptionLabel(QUIET_HOURS_END, quietEnd)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{QUIET_HOURS_END.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Unread badge"
|
||||
description="Display a badge when new activity arrives."
|
||||
labelFor="settings-7-unread-badge"
|
||||
>
|
||||
<Switch id="settings-7-unread-badge" defaultChecked />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Auto dismiss"
|
||||
description="Choose how long alerts stay visible."
|
||||
labelFor="settings-7-auto-dismiss"
|
||||
last
|
||||
>
|
||||
<Select value={autoDismiss} onValueChange={handleAutoDismissChange}>
|
||||
<SelectTrigger id="settings-7-auto-dismiss" className="w-full">
|
||||
<SelectValue>
|
||||
{getOptionLabel(TIMEOUTS, autoDismiss)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{TIMEOUTS.map((timeout) => (
|
||||
<SelectItem key={timeout.value} value={timeout.value}>
|
||||
{timeout.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
</SettingsFieldGroup>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard
|
||||
title="Email updates"
|
||||
description="Inbox preferences"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline">Reset</Button>
|
||||
<Button>Save preferences</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SettingsFieldGroup
|
||||
legend="Email updates"
|
||||
description="Control which emails reach your inbox."
|
||||
>
|
||||
<SettingRow
|
||||
title="Team communication"
|
||||
description="Messages, approvals, and activity summaries."
|
||||
labelFor="settings-7-team-communication"
|
||||
>
|
||||
<Switch id="settings-7-team-communication" defaultChecked />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Product announcements"
|
||||
description="Releases, improvements, and launches."
|
||||
labelFor="settings-7-product-announcements"
|
||||
>
|
||||
<Switch id="settings-7-product-announcements" />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Weekly digest"
|
||||
description="A recap of workspaces, mentions, and tasks."
|
||||
labelFor="settings-7-weekly-digest"
|
||||
>
|
||||
<Switch id="settings-7-weekly-digest" defaultChecked />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Digest cadence"
|
||||
description="Set how often summary emails arrive."
|
||||
labelFor="settings-7-digest-cadence"
|
||||
last
|
||||
>
|
||||
<Select
|
||||
value={digestCadence}
|
||||
onValueChange={handleDigestCadenceChange}
|
||||
>
|
||||
<SelectTrigger id="settings-7-digest-cadence" className="w-full">
|
||||
<SelectValue>
|
||||
{getOptionLabel(DIGEST_CADENCE, digestCadence)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{DIGEST_CADENCE.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
</SettingsFieldGroup>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useFileUpload } from "@/hooks/use-file-upload"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@evobgp/ui/components/avatar"
|
||||
import { Button } from "@evobgp/ui/components/button"
|
||||
import { UserCircle, XIcon, UploadIcon } from "lucide-react"
|
||||
|
||||
interface ProfileAvatarUploadProps {
|
||||
defaultAvatar: string
|
||||
alt: string
|
||||
inputId?: string
|
||||
}
|
||||
|
||||
export function ProfileAvatarUpload({
|
||||
defaultAvatar,
|
||||
alt,
|
||||
inputId,
|
||||
}: ProfileAvatarUploadProps) {
|
||||
const [removedCurrentPhoto, setRemovedCurrentPhoto] = useState(false)
|
||||
const [{ files }, { removeFile, openFileDialog, getInputProps }] =
|
||||
useFileUpload({
|
||||
accept: "image/*",
|
||||
})
|
||||
|
||||
const currentFile = files[0] ?? null
|
||||
const hasSavedPhoto = Boolean(defaultAvatar) && !removedCurrentPhoto
|
||||
const previewUrl =
|
||||
currentFile?.preview ?? (hasSavedPhoto ? defaultAvatar : null)
|
||||
const hasPhoto = Boolean(previewUrl)
|
||||
|
||||
const handleCancelUpload = () => {
|
||||
if (!currentFile) {
|
||||
return
|
||||
}
|
||||
|
||||
removeFile(currentFile.id)
|
||||
}
|
||||
|
||||
const handleRemovePhoto = () => {
|
||||
if (currentFile) {
|
||||
removeFile(currentFile.id)
|
||||
}
|
||||
|
||||
setRemovedCurrentPhoto(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-wrap items-center justify-start gap-2">
|
||||
{/* Actions */}
|
||||
<div className="relative">
|
||||
<Avatar className="size-10">
|
||||
<AvatarImage src={previewUrl ?? undefined} alt={alt} />
|
||||
<AvatarFallback className="text-muted-foreground bg-muted">
|
||||
<UserCircle aria-hidden="true" className="size-4 opacity-60" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{currentFile ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-xs"
|
||||
onClick={handleCancelUpload}
|
||||
className="absolute -top-1 -right-1 size-4 rounded-full"
|
||||
aria-label={`Cancel ${currentFile.file.name}`}
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="relative inline-flex">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={openFileDialog}
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<UploadIcon aria-hidden="true" />
|
||||
{hasPhoto ? "Change" : "Upload"}
|
||||
</Button>
|
||||
<input
|
||||
{...getInputProps({ id: inputId })}
|
||||
className="sr-only"
|
||||
aria-label="Upload profile image"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasPhoto ? (
|
||||
<Button variant="outline" size="sm" onClick={handleRemovePhoto}>
|
||||
<XIcon aria-hidden="true" />
|
||||
Remove
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Alert,
|
||||
AlertAction,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/reui/alert"
|
||||
import { Badge } from "@/components/reui/badge"
|
||||
|
||||
import { Button } from "@evobgp/ui/components/button"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@evobgp/ui/components/field"
|
||||
import { Input } from "@evobgp/ui/components/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@evobgp/ui/components/input-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@evobgp/ui/components/select"
|
||||
import { Textarea } from "@evobgp/ui/components/textarea"
|
||||
import { ROLES, TIMEZONES } from "./data"
|
||||
import { ProfileAvatarUpload } from "./profile-avatar-upload"
|
||||
import { SettingRow } from "./setting-row"
|
||||
import { SettingsCard } from "./settings-card"
|
||||
import { SettingsFieldGroup } from "./settings-field-group"
|
||||
import { createSelectValueHandler, getOptionLabel } from "./utils"
|
||||
import { UserIcon } from "lucide-react"
|
||||
|
||||
export function ProfileTab() {
|
||||
const [role, setRole] = useState("engineering-lead")
|
||||
const [timezone, setTimezone] = useState("utc-8")
|
||||
const handleRoleChange = createSelectValueHandler(setRole)
|
||||
const handleTimezoneChange = createSelectValueHandler(setTimezone)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Alert variant="warning">
|
||||
<UserIcon aria-hidden="true" />
|
||||
<AlertTitle>Complete your profile.</AlertTitle>
|
||||
<AlertDescription>
|
||||
Add a photo and keep your role and timezone current.
|
||||
</AlertDescription>
|
||||
<AlertAction>
|
||||
<Button type="button" variant="outline" size="xs">
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button type="button" size="xs">
|
||||
Update
|
||||
</Button>
|
||||
</AlertAction>
|
||||
</Alert>
|
||||
|
||||
{/* Card */}
|
||||
<SettingsCard
|
||||
title="My profile"
|
||||
description="Public account details"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save changes</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SettingsFieldGroup
|
||||
legend="Profile fields"
|
||||
description="Update your personal profile details."
|
||||
>
|
||||
<SettingRow
|
||||
title="Photo"
|
||||
description="Shown in comments and mentions."
|
||||
>
|
||||
<ProfileAvatarUpload
|
||||
defaultAvatar="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80"
|
||||
alt="Alex Morgan"
|
||||
inputId="settings-7-profile-photo"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Full name"
|
||||
description="Used across the workspace."
|
||||
labelFor="settings-7-full-name"
|
||||
>
|
||||
<Input id="settings-7-full-name" defaultValue="Alex Morgan" />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Email address"
|
||||
description="Primary sign-in email."
|
||||
labelFor="settings-7-email"
|
||||
titleAddon={<Badge variant="success-light">Verified</Badge>}
|
||||
>
|
||||
<Input
|
||||
id="settings-7-email"
|
||||
defaultValue="[email protected]"
|
||||
type="email"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Username"
|
||||
description="Visible in mentions and links."
|
||||
labelFor="settings-7-username"
|
||||
>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon align="inline-start">
|
||||
<InputGroupText>@</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="settings-7-username"
|
||||
defaultValue="alexmorgan"
|
||||
/>
|
||||
</InputGroup>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Profile details"
|
||||
description="Public details shared across the workspace."
|
||||
contentClassName="@md/field-group:w-[22rem]"
|
||||
>
|
||||
<FieldSet className="w-full gap-3">
|
||||
<FieldLegend className="sr-only">Profile details</FieldLegend>
|
||||
<FieldDescription className="sr-only">
|
||||
Public profile and workspace defaults.
|
||||
</FieldDescription>
|
||||
|
||||
<FieldGroup className="gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-role">Role</FieldLabel>
|
||||
<Select value={role} onValueChange={handleRoleChange}>
|
||||
<SelectTrigger id="settings-7-role" className="w-full">
|
||||
<SelectValue>{getOptionLabel(ROLES, role)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{ROLES.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-timezone">
|
||||
Time zone
|
||||
</FieldLabel>
|
||||
<Select value={timezone} onValueChange={handleTimezoneChange}>
|
||||
<SelectTrigger id="settings-7-timezone" className="w-full">
|
||||
<SelectValue>
|
||||
{getOptionLabel(TIMEZONES, timezone)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{TIMEZONES.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-website">Website</FieldLabel>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon align="inline-start">
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="settings-7-website"
|
||||
defaultValue="alexmorgan.dev"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Bio"
|
||||
description="Short profile summary."
|
||||
labelFor="settings-7-bio"
|
||||
last
|
||||
>
|
||||
<Textarea
|
||||
id="settings-7-bio"
|
||||
defaultValue="Building developer tools at Acme. Previously at Vercel and Stripe."
|
||||
rows={4}
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingsFieldGroup>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import { Fragment, useState } from "react"
|
||||
import { Badge } from "@/components/reui/badge"
|
||||
|
||||
import { Button } from "@evobgp/ui/components/button"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@evobgp/ui/components/field"
|
||||
import { Input } from "@evobgp/ui/components/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@evobgp/ui/components/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "@evobgp/ui/components/item"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@evobgp/ui/components/select"
|
||||
import { Switch } from "@evobgp/ui/components/switch"
|
||||
import { RECOVERY_METHODS, SESSIONS } from "./data"
|
||||
import { SettingRow } from "./setting-row"
|
||||
import { SettingsCard } from "./settings-card"
|
||||
import { SettingsFieldGroup } from "./settings-field-group"
|
||||
import { createSelectValueHandler, getOptionLabel } from "./utils"
|
||||
import { LogOutIcon } from "lucide-react"
|
||||
|
||||
export function SecurityTab() {
|
||||
const [recoveryMethod, setRecoveryMethod] = useState("authenticator")
|
||||
const handleRecoveryMethodChange = createSelectValueHandler(setRecoveryMethod)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Card */}
|
||||
<SettingsCard
|
||||
title="Change password"
|
||||
description="Access protection"
|
||||
footer={<Button>Update password</Button>}
|
||||
>
|
||||
<SettingsFieldGroup
|
||||
legend="Password settings"
|
||||
description="Update your password and keep your account protected."
|
||||
>
|
||||
<SettingRow
|
||||
title="Current password"
|
||||
description="Verify your identity before making changes."
|
||||
labelFor="settings-7-current-password"
|
||||
>
|
||||
<Input
|
||||
id="settings-7-current-password"
|
||||
type="password"
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="New password"
|
||||
description="Use at least 12 characters and a unique phrase."
|
||||
contentClassName="@md/field-group:w-[22rem]"
|
||||
last
|
||||
>
|
||||
<FieldSet className="w-full gap-3">
|
||||
<FieldLegend className="sr-only">New password</FieldLegend>
|
||||
<FieldDescription className="sr-only">
|
||||
Create and confirm the next password for this account.
|
||||
</FieldDescription>
|
||||
|
||||
<FieldGroup className="gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-new-password">
|
||||
New password
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="settings-7-new-password"
|
||||
type="password"
|
||||
placeholder="Create a new password"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="settings-7-confirm-password">
|
||||
Confirm password
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="settings-7-confirm-password"
|
||||
type="password"
|
||||
placeholder="Confirm the new password"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</SettingRow>
|
||||
</SettingsFieldGroup>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard
|
||||
title="Two-step verification"
|
||||
description="Extra sign-in checks"
|
||||
>
|
||||
<SettingsFieldGroup
|
||||
legend="Two-step verification"
|
||||
description="Add backup checks for future sign-ins."
|
||||
>
|
||||
<SettingRow
|
||||
title="Authenticator app"
|
||||
description="Use one-time codes from an app."
|
||||
labelFor="settings-7-authenticator-app"
|
||||
titleAddon={
|
||||
<Badge variant="info-light" size="sm">
|
||||
Recommended
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<Switch id="settings-7-authenticator-app" defaultChecked />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Recovery phone"
|
||||
description="Used if you lose access to your authenticator app."
|
||||
labelFor="settings-7-recovery-phone"
|
||||
>
|
||||
<InputGroup className="w-full">
|
||||
<InputGroupAddon align="inline-start">
|
||||
<InputGroupText>+1</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="settings-7-recovery-phone"
|
||||
defaultValue="415 555 0148"
|
||||
/>
|
||||
</InputGroup>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Delivery method"
|
||||
description="Choose how backup verification requests are delivered."
|
||||
labelFor="settings-7-recovery-method"
|
||||
>
|
||||
<Select
|
||||
value={recoveryMethod}
|
||||
onValueChange={handleRecoveryMethodChange}
|
||||
>
|
||||
<SelectTrigger id="settings-7-recovery-method" className="w-full">
|
||||
<SelectValue>
|
||||
{getOptionLabel(RECOVERY_METHODS, recoveryMethod)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{RECOVERY_METHODS.map((method) => (
|
||||
<SelectItem key={method.value} value={method.value}>
|
||||
{method.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
title="Trusted devices"
|
||||
description="Skip repeat prompts on known devices."
|
||||
labelFor="settings-7-trusted-devices"
|
||||
last
|
||||
>
|
||||
<Switch id="settings-7-trusted-devices" defaultChecked />
|
||||
</SettingRow>
|
||||
</SettingsFieldGroup>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard title="Active sessions" description="Signed-in devices">
|
||||
<ItemGroup className="gap-0">
|
||||
{SESSIONS.map((session, index) => (
|
||||
<Fragment key={session.id}>
|
||||
{index > 0 ? <ItemSeparator className="my-0" /> : null}
|
||||
<Item className="min-h-0 items-center gap-4 px-5 py-3.5">
|
||||
<ItemMedia className="self-center!">
|
||||
<Item className="bg-muted/60 border-background flex size-8 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] [&_svg]:size-4 [&_svg]:opacity-60">
|
||||
{session.icon}
|
||||
</Item>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent className="min-w-0 justify-center gap-0.5 self-center">
|
||||
<ItemTitle className="gap-2 leading-5">
|
||||
{session.browser} on {session.device}
|
||||
{session.current ? (
|
||||
<Badge variant="success-light" size="xs">
|
||||
Current
|
||||
</Badge>
|
||||
) : null}
|
||||
</ItemTitle>
|
||||
<ItemDescription className="leading-5">
|
||||
{session.location} · {session.lastActive}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
|
||||
<ItemActions className="w-28 shrink-0 justify-end self-center">
|
||||
{session.current ? (
|
||||
<Button variant="outline" size="sm">
|
||||
This device
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="ghost" size="sm">
|
||||
<LogOutIcon data-icon="inline-start" aria-hidden="true" />
|
||||
Revoke
|
||||
</Button>
|
||||
)}
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard title="Delete account" description="Irreversible changes">
|
||||
<SettingsFieldGroup
|
||||
legend="Delete account"
|
||||
description="Review irreversible actions before deleting your account."
|
||||
>
|
||||
<SettingRow
|
||||
title="Close account"
|
||||
description="Permanently remove your account, sessions, and recovery settings."
|
||||
last
|
||||
contentClassName="@md/field-group:w-[22rem]"
|
||||
>
|
||||
<div className="flex w-full flex-col items-start gap-2 @md/field-group:items-end">
|
||||
<p className="text-muted-foreground text-xs leading-5">
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<Button variant="destructive">Delete account</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
</SettingsFieldGroup>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/** @deprecated Import from `@/components/settings/setting-row` — settings-7 is not a target. */
|
||||
export { SettingRow } from '@/components/settings/setting-row'
|
||||
@@ -0,0 +1,2 @@
|
||||
/** @deprecated Import from `@/components/settings/settings-card` — settings-7 is not a target. */
|
||||
export { SettingsCard } from '@/components/settings/settings-card'
|
||||
@@ -0,0 +1,2 @@
|
||||
/** @deprecated Import from `@/components/settings/settings-field-group` — settings-7 is not a target. */
|
||||
export { SettingsFieldGroup } from '@/components/settings/settings-field-group'
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client"
|
||||
|
||||
import { type Dispatch, type SetStateAction } from "react"
|
||||
|
||||
import type { SelectOption } from "./data"
|
||||
|
||||
export function getOptionLabel<T extends SelectOption>(
|
||||
options: T[],
|
||||
value: string
|
||||
) {
|
||||
return options.find((option) => option.value === value)?.label ?? value
|
||||
}
|
||||
|
||||
export function createSelectValueHandler(
|
||||
setValue: Dispatch<SetStateAction<string>>
|
||||
) {
|
||||
return (value: string | null) => {
|
||||
if (value !== null) {
|
||||
setValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { AccountSettings } from "./components/account-settings"
|
||||
|
||||
export function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-start justify-center p-4 sm:p-6 md:p-10">
|
||||
<AccountSettings />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { useState, type ReactElement, type ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from '@evobgp/ui/components/drawer'
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@evobgp/ui/components/alert-dialog'
|
||||
|
||||
type ConfirmDialogBaseProps = {
|
||||
title: string
|
||||
@@ -37,90 +38,52 @@ type ConfirmDialogControlled = ConfirmDialogBaseProps & {
|
||||
|
||||
type ConfirmDialogProps = ConfirmDialogWithTrigger | ConfirmDialogControlled
|
||||
|
||||
function ConfirmDrawerBody({
|
||||
title,
|
||||
description,
|
||||
confirmLabel = 'Подтвердить',
|
||||
cancelLabel = 'Отмена',
|
||||
destructive,
|
||||
onConfirm,
|
||||
confirmDisabled,
|
||||
confirmLoading,
|
||||
confirmLoadingLabel,
|
||||
controlled,
|
||||
}: ConfirmDialogBaseProps & { controlled?: boolean }) {
|
||||
const confirmText =
|
||||
confirmLoading && confirmLoadingLabel
|
||||
? confirmLoadingLabel
|
||||
: confirmLoading
|
||||
? `${confirmLabel}…`
|
||||
: confirmLabel
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{title}</DrawerTitle>
|
||||
{description ? <DrawerDescription>{description}</DrawerDescription> : null}
|
||||
</DrawerHeader>
|
||||
<DrawerFooter className="border-t bg-muted/50 sm:flex-row sm:justify-end">
|
||||
<DrawerClose render={<Button variant="outline" disabled={confirmLoading} />}>
|
||||
{cancelLabel}
|
||||
</DrawerClose>
|
||||
{controlled ? (
|
||||
<Button
|
||||
variant={destructive ? 'destructive' : 'default'}
|
||||
disabled={confirmDisabled || confirmLoading}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
) : (
|
||||
<DrawerClose
|
||||
render={
|
||||
<Button
|
||||
variant={destructive ? 'destructive' : 'default'}
|
||||
disabled={confirmDisabled || confirmLoading}
|
||||
/>
|
||||
}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</DrawerClose>
|
||||
)}
|
||||
</DrawerFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** Confirm overlay — Alert Dialog. Preview: https://reui.io/preview/base/components/c-alert-dialog-11 */
|
||||
export function ConfirmDialog(props: ConfirmDialogProps) {
|
||||
const bodyProps: ConfirmDialogBaseProps = {
|
||||
title: props.title,
|
||||
description: props.description,
|
||||
confirmLabel: props.confirmLabel,
|
||||
cancelLabel: props.cancelLabel,
|
||||
destructive: props.destructive,
|
||||
onConfirm: props.onConfirm,
|
||||
confirmDisabled: props.confirmDisabled,
|
||||
confirmLoading: props.confirmLoading,
|
||||
confirmLoadingLabel: props.confirmLoadingLabel,
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false)
|
||||
const isControlled = props.open !== undefined
|
||||
const open = isControlled ? props.open : uncontrolledOpen
|
||||
|
||||
function setOpen(next: boolean) {
|
||||
if (!isControlled) setUncontrolledOpen(next)
|
||||
if (isControlled) props.onOpenChange?.(next)
|
||||
}
|
||||
|
||||
if (props.trigger) {
|
||||
return (
|
||||
<Drawer swipeDirection="right">
|
||||
<DrawerTrigger render={props.trigger} />
|
||||
<DrawerContent className="h-full max-h-none sm:max-w-sm">
|
||||
<ConfirmDrawerBody {...bodyProps} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
const confirmText =
|
||||
props.confirmLoading && props.confirmLoadingLabel
|
||||
? props.confirmLoadingLabel
|
||||
: props.confirmLoading
|
||||
? `${props.confirmLabel ?? 'Подтвердить'}…`
|
||||
: (props.confirmLabel ?? 'Подтвердить')
|
||||
|
||||
function handleConfirm() {
|
||||
props.onConfirm()
|
||||
if (!props.confirmLoading && !isControlled) setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer open={props.open} onOpenChange={props.onOpenChange} swipeDirection="right">
|
||||
<DrawerContent className="h-full max-h-none sm:max-w-sm">
|
||||
<ConfirmDrawerBody {...bodyProps} controlled />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
{props.trigger ? <AlertDialogTrigger render={props.trigger} /> : null}
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{props.title}</AlertDialogTitle>
|
||||
{props.description ? (
|
||||
<AlertDialogDescription>{props.description}</AlertDialogDescription>
|
||||
) : null}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={props.confirmLoading}>
|
||||
{props.cancelLabel ?? 'Отмена'}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant={props.destructive ? 'destructive' : 'default'}
|
||||
disabled={props.confirmDisabled || props.confirmLoading}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
|
||||
export interface CountedLineTab {
|
||||
id: string
|
||||
label: string
|
||||
count?: number
|
||||
}
|
||||
|
||||
interface CountedLineTabsProps {
|
||||
tabs: CountedLineTab[]
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
className?: string
|
||||
listClassName?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
/** Line tabs with count pills (c-tabs-2 / data-grid-filtering-2). */
|
||||
export function CountedLineTabs({
|
||||
tabs,
|
||||
value,
|
||||
onValueChange,
|
||||
className,
|
||||
listClassName,
|
||||
children,
|
||||
}: CountedLineTabsProps) {
|
||||
return (
|
||||
<Tabs value={value} onValueChange={onValueChange} className={className}>
|
||||
<TabsList variant="line" className={cn('gap-5', listClassName)}>
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
{tab.count !== undefined ? (
|
||||
<span className="bg-muted text-muted-foreground inline-flex min-w-5 items-center justify-center rounded-md px-1.5 py-0.5 text-xs tabular-nums">
|
||||
{tab.count}
|
||||
</span>
|
||||
) : null}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{children}
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
|
||||
function grain(i: number, j: number) {
|
||||
const n = Math.sin(i * 127.1 + j * 311.7) * 43758.5453
|
||||
return n - Math.floor(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* Static dot field adapted from REUI card-17: theme-aware canvas dots,
|
||||
* repainted on resize and theme change.
|
||||
*/
|
||||
export function CardDotField({ className }: { className?: string }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const GAP = 3
|
||||
const DOT = 1.5
|
||||
const BASE = 0.05
|
||||
const PEAK = 0.32
|
||||
|
||||
const draw = () => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
if (!rect.width || !rect.height) return
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
||||
canvas.width = Math.round(rect.width * dpr)
|
||||
canvas.height = Math.round(rect.height * dpr)
|
||||
|
||||
ctx.fillStyle = getComputedStyle(canvas).color || 'rgb(115,115,115)'
|
||||
ctx.fillRect(0, 0, 1, 1)
|
||||
const px = ctx.getImageData(0, 0, 1, 1).data
|
||||
const color = `rgb(${px[0]}, ${px[1]}, ${px[2]})`
|
||||
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
ctx.clearRect(0, 0, rect.width, rect.height)
|
||||
ctx.fillStyle = color
|
||||
|
||||
const cols = Math.ceil(rect.width / GAP) + 1
|
||||
const rows = Math.ceil(rect.height / GAP) + 1
|
||||
for (let i = 0; i < cols; i++) {
|
||||
const x = i * GAP
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const q = grain(i, j)
|
||||
const amp = 0.7 + 0.6 * grain(j * 2 + 1, i * 2 + 1)
|
||||
let a = (BASE + (PEAK - BASE) * q * q) * amp
|
||||
if (a > 1) a = 1
|
||||
ctx.globalAlpha = a
|
||||
ctx.fillRect(x, j * GAP, DOT, DOT)
|
||||
}
|
||||
}
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
|
||||
draw()
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => draw())
|
||||
resizeObserver.observe(canvas)
|
||||
|
||||
const themeObserver = new MutationObserver(() => draw())
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'style'],
|
||||
})
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
themeObserver.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
className={cn('pointer-events-none absolute inset-0 h-full w-full', className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { AlertTriangle, CheckCircle, Info } from 'lucide-react'
|
||||
|
||||
import { DashboardFramePanel } from '@/components/dashboard/dashboard-frame-panel'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import {
|
||||
Timeline,
|
||||
TimelineContent,
|
||||
TimelineHeader,
|
||||
TimelineIndicator,
|
||||
TimelineItem,
|
||||
TimelineSeparator,
|
||||
TimelineTitle,
|
||||
} from '@/components/reui/timeline'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
|
||||
import { recentPlatformActivity } from '@/lib/metrics'
|
||||
import type { JobRow, PeerRow, RevisionRow, SpeakerRow } from '@/types/api'
|
||||
|
||||
const KIND_META = {
|
||||
job: { icon: Info, className: 'text-info' },
|
||||
revision: { icon: CheckCircle, className: 'text-success' },
|
||||
network: { icon: AlertTriangle, className: 'text-warning' },
|
||||
} as const
|
||||
|
||||
function statusBadgeVariant(status: string) {
|
||||
const s = status.toLowerCase()
|
||||
if (['ok', 'success', 'completed', 'done'].includes(s)) return 'success-light' as const
|
||||
if (['running', 'queued', 'pending'].includes(s)) return 'info-light' as const
|
||||
if (['warning', 'mismatch'].includes(s)) return 'warning-light' as const
|
||||
if (['failed', 'error', 'cancelled'].includes(s)) return 'destructive-light' as const
|
||||
return 'outline' as const
|
||||
}
|
||||
|
||||
export function DashboardActivityTimeline({
|
||||
jobs,
|
||||
revisions,
|
||||
peers,
|
||||
speakers,
|
||||
loading,
|
||||
}: {
|
||||
jobs: JobRow[]
|
||||
revisions: RevisionRow[]
|
||||
peers: PeerRow[]
|
||||
speakers: SpeakerRow[]
|
||||
loading?: boolean
|
||||
}) {
|
||||
const items = recentPlatformActivity(jobs, revisions, peers, speakers, 6)
|
||||
|
||||
return (
|
||||
<DashboardFramePanel
|
||||
title="Недавняя активность"
|
||||
description="Задачи, ревизии и сетевые события"
|
||||
className="h-full"
|
||||
>
|
||||
{loading ? (
|
||||
<p className="text-muted-foreground px-4 py-6 text-sm">Загрузка…</p>
|
||||
) : items.length === 0 ? (
|
||||
<p className="text-muted-foreground px-4 py-6 text-sm">Нет недавних событий</p>
|
||||
) : (
|
||||
<div className="px-4 py-4">
|
||||
<Timeline defaultValue={items.length}>
|
||||
{items.map((item, index) => {
|
||||
const meta = KIND_META[item.kind]
|
||||
const Icon = meta.icon
|
||||
return (
|
||||
<TimelineItem
|
||||
key={item.id}
|
||||
step={index + 1}
|
||||
className="group-data-[orientation=vertical]/timeline:ms-8 group-data-[orientation=vertical]/timeline:not-last:pb-4"
|
||||
>
|
||||
<TimelineHeader>
|
||||
<TimelineSeparator className="bg-border! group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:top-2 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem)] group-data-[orientation=vertical]/timeline:translate-y-5" />
|
||||
<TimelineIndicator className="border-none bg-transparent group-data-[orientation=vertical]/timeline:-left-6">
|
||||
<span
|
||||
className={cn(
|
||||
'bg-muted/70 flex size-7 items-center justify-center rounded-full',
|
||||
meta.className,
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3.5" aria-hidden />
|
||||
</span>
|
||||
</TimelineIndicator>
|
||||
</TimelineHeader>
|
||||
<TimelineContent className="min-w-0 pb-1 text-foreground">
|
||||
<TimelineTitle className="text-sm leading-snug font-normal">
|
||||
{item.message}
|
||||
</TimelineTitle>
|
||||
<div className="mt-2">
|
||||
<Badge variant={statusBadgeVariant(item.status)} size="sm">
|
||||
{item.statusLabel ?? item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
)
|
||||
})}
|
||||
</Timeline>
|
||||
</div>
|
||||
)}
|
||||
</DashboardFramePanel>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
PanelCard,
|
||||
panelCardContentFlushClassName,
|
||||
} from '@/components/panel-card'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
|
||||
/** Card-surface panel for dashboard sections (replaces legacy Frame shell). */
|
||||
export function DashboardFramePanel({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
children,
|
||||
className,
|
||||
contentClassName,
|
||||
}: {
|
||||
title?: string
|
||||
description?: string
|
||||
actions?: ReactNode
|
||||
children: ReactNode
|
||||
className?: string
|
||||
contentClassName?: string
|
||||
}) {
|
||||
return (
|
||||
<PanelCard
|
||||
title={title}
|
||||
description={description}
|
||||
actions={actions}
|
||||
className={cn('h-full', className)}
|
||||
contentClassName={cn(panelCardContentFlushClassName, contentClassName)}
|
||||
>
|
||||
{children}
|
||||
</PanelCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
Boxes,
|
||||
ListChecks,
|
||||
Network,
|
||||
ServerCog,
|
||||
Share2,
|
||||
} from 'lucide-react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { KpiStatGrid, type KpiStatItem } from '@/components/kpi-stat-grid'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
|
||||
import { aggregateNetworkMetrics, runningJobCount } from '@/queries/overview'
|
||||
import type { JobRow, ModuleRow, PeerRow, SpeakerRow } from '@/types/api'
|
||||
|
||||
type KpiCard = KpiStatItem & { icon: ReactNode }
|
||||
|
||||
function buildKpis({
|
||||
modules,
|
||||
peers,
|
||||
speakers,
|
||||
jobs,
|
||||
loading,
|
||||
}: {
|
||||
modules: ModuleRow[]
|
||||
peers: PeerRow[]
|
||||
speakers: SpeakerRow[]
|
||||
jobs: JobRow[]
|
||||
loading?: boolean
|
||||
}): KpiCard[] {
|
||||
const enabledModules = modules.filter((m) => m.enabled !== false).length
|
||||
const network = aggregateNetworkMetrics(peers, speakers)
|
||||
const peersEnabled = network.peersEnabled
|
||||
const bgpPct =
|
||||
peersEnabled > 0 ? Math.round((network.peersEstablished / peersEnabled) * 100) : null
|
||||
const running = runningJobCount(jobs)
|
||||
const failedJobs = jobs.filter((j) =>
|
||||
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
||||
).length
|
||||
const offlineSpeakers = Math.max(0, speakers.length - network.speakersOnline)
|
||||
const riskCount = network.peersMismatch + failedJobs + offlineSpeakers
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'modules',
|
||||
icon: <Boxes aria-hidden />,
|
||||
iconClassName: 'text-primary',
|
||||
value: loading ? '—' : `${enabledModules}/${modules.length || 0}`,
|
||||
label: 'Модули активны',
|
||||
footer: (
|
||||
<Badge variant="primary-light" size="sm">
|
||||
{loading ? '…' : `${modules.length} всего`}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'bgp',
|
||||
icon: <Network aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
value: loading || bgpPct === null ? '—' : `${bgpPct}%`,
|
||||
label: 'BGP готовность',
|
||||
footer: (
|
||||
<Badge
|
||||
variant={
|
||||
bgpPct !== null && bgpPct >= 90
|
||||
? 'success-light'
|
||||
: bgpPct !== null && bgpPct < 70
|
||||
? 'warning-light'
|
||||
: 'outline'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{loading || bgpPct === null
|
||||
? 'нет включённых пиров'
|
||||
: `${network.peersEstablished} установлено`}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'peers',
|
||||
icon: <Share2 aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
value: loading ? '—' : `${network.peersEstablished}/${peersEnabled}`,
|
||||
label: 'Пиры Established',
|
||||
footer: (
|
||||
<Badge variant="success-light" size="sm">
|
||||
{loading ? '…' : `${network.peersTotal} в каталоге`}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'speakers',
|
||||
icon: <ServerCog aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
value: loading ? '—' : `${network.speakersOnline}/${network.speakersTotal}`,
|
||||
label: 'Спикеры online',
|
||||
footer: (
|
||||
<Badge
|
||||
variant={network.speakersOnline === network.speakersTotal ? 'success-light' : 'warning-light'}
|
||||
size="sm"
|
||||
>
|
||||
{loading ? '…' : 'live-снимок'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'jobs',
|
||||
icon: <ListChecks aria-hidden />,
|
||||
iconClassName: 'text-focus',
|
||||
value: loading ? '—' : String(running),
|
||||
label: 'Активные задачи',
|
||||
footer: (
|
||||
<Badge variant={running > 0 ? 'info-light' : 'outline'} size="sm">
|
||||
{loading ? '…' : `${jobs.length} в выборке`}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'risks',
|
||||
icon: <AlertTriangle aria-hidden />,
|
||||
iconClassName: 'text-destructive',
|
||||
value: loading ? '—' : String(riskCount),
|
||||
label: 'Риски',
|
||||
variant: riskCount > 0 ? 'destructive' : 'default',
|
||||
footer: (
|
||||
<Badge variant={riskCount > 0 ? 'destructive-light' : 'success-light'} size="sm">
|
||||
{loading
|
||||
? '…'
|
||||
: riskCount > 0
|
||||
? `${failedJobs} задач · ${network.peersMismatch} расхождений`
|
||||
: 'в норме'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function buildDashboardKpiCards({
|
||||
modules,
|
||||
peers,
|
||||
speakers,
|
||||
jobs,
|
||||
loading,
|
||||
}: {
|
||||
modules: ModuleRow[]
|
||||
peers: PeerRow[]
|
||||
speakers: SpeakerRow[]
|
||||
jobs: JobRow[]
|
||||
loading?: boolean
|
||||
}) {
|
||||
return buildKpis({ modules, peers, speakers, jobs, loading }).map((card) => ({
|
||||
...card,
|
||||
id: card.id ?? String(card.label),
|
||||
}))
|
||||
}
|
||||
|
||||
export function DashboardKpiGrid({
|
||||
modules,
|
||||
peers,
|
||||
speakers,
|
||||
jobs,
|
||||
loading,
|
||||
}: {
|
||||
modules: ModuleRow[]
|
||||
peers: PeerRow[]
|
||||
speakers: SpeakerRow[]
|
||||
jobs: JobRow[]
|
||||
loading?: boolean
|
||||
}) {
|
||||
return (
|
||||
<KpiStatGrid
|
||||
items={buildKpis({ modules, peers, speakers, jobs, loading })}
|
||||
aria-label="KPI обзора"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { KpiSparklineCard, type KpiSparklineMetric } from '@/components/patterns/kpi-sparkline-card'
|
||||
import { kpiGridClassName } from '@/lib/ui-surface'
|
||||
import { aggregateNetworkMetrics, runningJobCount } from '@/queries/overview'
|
||||
import type { JobRow, ModuleRow, PeerRow, SpeakerRow } from '@/types/api'
|
||||
|
||||
function syntheticSparkline(seed: number, points = 9): number[] {
|
||||
const base = Math.max(4, seed)
|
||||
return Array.from({ length: points }, (_, i) =>
|
||||
Math.round(base * (0.82 + (i / points) * 0.18 + Math.sin(i + seed) * 0.04)),
|
||||
)
|
||||
}
|
||||
|
||||
function buildMetrics({
|
||||
modules,
|
||||
peers,
|
||||
speakers,
|
||||
jobs,
|
||||
loading,
|
||||
}: {
|
||||
modules: ModuleRow[]
|
||||
peers: PeerRow[]
|
||||
speakers: SpeakerRow[]
|
||||
jobs: JobRow[]
|
||||
loading?: boolean
|
||||
}): KpiSparklineMetric[] {
|
||||
const enabledModules = modules.filter((m) => m.enabled !== false).length
|
||||
const network = aggregateNetworkMetrics(peers, speakers)
|
||||
const bgpPct =
|
||||
network.peersEnabled > 0
|
||||
? Math.round((network.peersEstablished / network.peersEnabled) * 100)
|
||||
: 0
|
||||
const running = runningJobCount(jobs)
|
||||
const failedJobs = jobs.filter((j) =>
|
||||
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
||||
).length
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'bgp',
|
||||
title: 'BGP готовность',
|
||||
label: 'Established / включённые',
|
||||
value: loading || network.peersEnabled === 0 ? '—' : `${bgpPct}%`,
|
||||
delta: loading ? '…' : bgpPct >= 90 ? 'стабильно' : 'внимание',
|
||||
deltaVariant: bgpPct >= 90 ? 'success-light' : bgpPct >= 50 ? 'warning-light' : 'destructive-light',
|
||||
detail: loading ? '' : `${network.peersEstablished} сессий`,
|
||||
tone: bgpPct >= 90 ? 'success' : bgpPct >= 50 ? 'warning' : 'danger',
|
||||
sparkline: syntheticSparkline(bgpPct || 40),
|
||||
},
|
||||
{
|
||||
id: 'modules',
|
||||
title: 'Модули',
|
||||
label: 'Активные списки',
|
||||
value: loading ? '—' : `${enabledModules}`,
|
||||
delta: loading ? '…' : `${modules.length} всего`,
|
||||
deltaVariant: 'primary-light',
|
||||
detail: loading ? '' : 'маршрутизация',
|
||||
tone: 'info',
|
||||
sparkline: syntheticSparkline(enabledModules || 3),
|
||||
},
|
||||
{
|
||||
id: 'speakers',
|
||||
title: 'Спикеры',
|
||||
label: 'Online / всего',
|
||||
value: loading ? '—' : `${network.speakersOnline}/${network.speakersTotal}`,
|
||||
delta:
|
||||
loading || network.speakersTotal === 0
|
||||
? '…'
|
||||
: network.speakersOnline === network.speakersTotal
|
||||
? 'все online'
|
||||
: 'частично',
|
||||
deltaVariant:
|
||||
network.speakersOnline === network.speakersTotal ? 'success-light' : 'warning-light',
|
||||
detail: loading ? '' : 'live-снимок',
|
||||
tone: network.speakersOnline === network.speakersTotal ? 'success' : 'warning',
|
||||
sparkline: syntheticSparkline(network.speakersOnline || 2),
|
||||
},
|
||||
{
|
||||
id: 'jobs',
|
||||
title: 'Задачи',
|
||||
label: 'Активные / ошибки',
|
||||
value: loading ? '—' : String(running),
|
||||
delta: loading ? '…' : failedJobs > 0 ? `${failedJobs} ошибок` : 'без сбоев',
|
||||
deltaVariant: failedJobs > 0 ? 'destructive-light' : 'success-light',
|
||||
detail: loading ? '' : `${jobs.length} в выборке`,
|
||||
tone: failedJobs > 0 ? 'danger' : running > 0 ? 'info' : 'success',
|
||||
sparkline: syntheticSparkline(running + failedJobs || 1),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function DashboardKpiSparklineRow({
|
||||
modules,
|
||||
peers,
|
||||
speakers,
|
||||
jobs,
|
||||
loading,
|
||||
}: {
|
||||
modules: ModuleRow[]
|
||||
peers: PeerRow[]
|
||||
speakers: SpeakerRow[]
|
||||
jobs: JobRow[]
|
||||
loading?: boolean
|
||||
}) {
|
||||
const metrics = useMemo(
|
||||
() => buildMetrics({ modules, peers, speakers, jobs, loading }),
|
||||
[modules, peers, speakers, jobs, loading],
|
||||
)
|
||||
|
||||
return (
|
||||
<section aria-label="KPI обзора" className={kpiGridClassName}>
|
||||
{metrics.map((metric) => (
|
||||
<KpiSparklineCard key={metric.id} metric={metric} />
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import {
|
||||
ArrowUpDownIcon,
|
||||
BoxesIcon,
|
||||
ChevronDownIcon,
|
||||
FilterIcon,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
} from 'lucide-react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { CategoryBadge, ModeBadge } from '@/components/category-badge'
|
||||
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
|
||||
import { DashboardFramePanel } from '@/components/dashboard/dashboard-frame-panel'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { DataGrid } from '@/components/reui/data-grid/data-grid'
|
||||
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
||||
import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination'
|
||||
import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area'
|
||||
import {
|
||||
DataGridTable,
|
||||
DataGridTableHeader,
|
||||
} from '@/components/reui/data-grid/data-grid-table'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@evobgp/ui/components/dropdown-menu'
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from '@evobgp/ui/components/input-group'
|
||||
import { moduleTypeRu } from '@/lib/ui-labels'
|
||||
import type { ModuleRow } from '@/types/api'
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
} from '@tanstack/react-table'
|
||||
|
||||
type ModuleSort = 'name' | 'type' | 'priority' | 'last_refreshed_at'
|
||||
type EnabledFilter = 'all' | 'enabled' | 'disabled'
|
||||
|
||||
const sortLabels: Record<ModuleSort, string> = {
|
||||
name: 'Название',
|
||||
type: 'Тип',
|
||||
priority: 'Приоритет',
|
||||
last_refreshed_at: 'Обновлено',
|
||||
}
|
||||
|
||||
const EMPTY_MESSAGE = 'Нет модулей по выбранным фильтрам.'
|
||||
|
||||
function buildSorting(sortBy: ModuleSort): SortingState {
|
||||
return [{ id: sortBy, desc: sortBy === 'last_refreshed_at' }]
|
||||
}
|
||||
|
||||
export function DashboardModulesGrid({
|
||||
modules,
|
||||
isLoading = false,
|
||||
}: {
|
||||
modules: ModuleRow[]
|
||||
isLoading?: boolean
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [enabledFilter, setEnabledFilter] = useState<EnabledFilter>('all')
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
const [sortBy, setSortBy] = useState<ModuleSort>('name')
|
||||
const [sorting, setSorting] = useState<SortingState>(() => buildSorting('name'))
|
||||
|
||||
const filteredModules = useMemo(() => {
|
||||
const q = searchQuery.trim().toLowerCase()
|
||||
return modules.filter((module) => {
|
||||
const matchesSearch =
|
||||
q.length === 0 ||
|
||||
`${module.name} ${module.type} ${moduleTypeRu(module.type)}`.toLowerCase().includes(q)
|
||||
const matchesEnabled =
|
||||
enabledFilter === 'all' ||
|
||||
(enabledFilter === 'enabled' ? module.enabled !== false : module.enabled === false)
|
||||
return matchesSearch && matchesEnabled
|
||||
})
|
||||
}, [modules, searchQuery, enabledFilter])
|
||||
|
||||
const resetPagination = useCallback(() => {
|
||||
setPagination((current) => ({ ...current, pageIndex: 0 }))
|
||||
}, [])
|
||||
|
||||
const handleSortChange = useCallback(
|
||||
(value: ModuleSort) => {
|
||||
setSortBy(value)
|
||||
setSorting(buildSorting(value))
|
||||
resetPagination()
|
||||
},
|
||||
[resetPagination],
|
||||
)
|
||||
|
||||
const columns = useMemo<ColumnDef<ModuleRow>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
id: 'name',
|
||||
header: ({ column }) => <DataGridColumnHeader column={column} title="Модуль" />,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<BoxesIcon className="text-muted-foreground size-4 shrink-0" aria-hidden />
|
||||
<DataGridPrimaryCell title={row.original.name} accent="primary" className="max-w-[240px]" />
|
||||
</div>
|
||||
),
|
||||
meta: { headerTitle: 'Модуль' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
id: 'type',
|
||||
header: ({ column }) => <DataGridColumnHeader column={column} title="Тип" />,
|
||||
cell: ({ row }) => <CategoryBadge>{moduleTypeRu(row.original.type)}</CategoryBadge>,
|
||||
meta: { headerTitle: 'Тип' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'priority',
|
||||
id: 'priority',
|
||||
header: ({ column }) => <DataGridColumnHeader column={column} title="Приоритет" />,
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono text-sm tabular-nums">{row.original.priority}</span>
|
||||
),
|
||||
meta: { headerTitle: 'Приоритет' },
|
||||
},
|
||||
{
|
||||
id: 'enabled',
|
||||
accessorFn: (row) => (row.enabled !== false ? 'enabled' : 'disabled'),
|
||||
header: ({ column }) => <DataGridColumnHeader column={column} title="Состояние" />,
|
||||
cell: ({ row }) => <ModeBadge enabled={row.original.enabled !== false} />,
|
||||
meta: { headerTitle: 'Состояние' },
|
||||
},
|
||||
{
|
||||
id: 'last_refreshed_at',
|
||||
accessorFn: (row) => row.last_refreshed_at ?? '',
|
||||
header: ({ column }) => <DataGridColumnHeader column={column} title="Обновлено" />,
|
||||
cell: ({ row }) => (
|
||||
<DataGridMutedCell>
|
||||
{row.original.last_refreshed_at
|
||||
? new Date(row.original.last_refreshed_at).toLocaleString('ru-RU')
|
||||
: '—'}
|
||||
</DataGridMutedCell>
|
||||
),
|
||||
sortingFn: (a, b) =>
|
||||
(a.original.last_refreshed_at ?? '').localeCompare(b.original.last_refreshed_at ?? ''),
|
||||
meta: { headerTitle: 'Обновлено' },
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredModules,
|
||||
columns,
|
||||
pageCount: Math.ceil(filteredModules.length / pagination.pageSize),
|
||||
state: { pagination, sorting },
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
getRowId: (row) => row.id,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
})
|
||||
|
||||
const activeFilterCount = enabledFilter === 'all' ? 0 : 1
|
||||
|
||||
return (
|
||||
<DashboardFramePanel
|
||||
title="Модули"
|
||||
description="Поиск, сортировка и быстрый переход к настройке"
|
||||
actions={
|
||||
<Button variant="outline" size="sm" render={<Link to="/modules/new" />}>
|
||||
<PlusIcon />
|
||||
Создать
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<DataGrid
|
||||
table={table}
|
||||
recordCount={filteredModules.length}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={EMPTY_MESSAGE}
|
||||
tableLayout={{
|
||||
dense: true,
|
||||
rowBorder: true,
|
||||
headerSticky: false,
|
||||
columnsVisibility: false,
|
||||
columnsResizable: false,
|
||||
columnsMovable: false,
|
||||
width: 'fixed',
|
||||
}}
|
||||
tableClassNames={{ bodyRow: 'group/module-row cursor-pointer [&>td]:h-14' }}
|
||||
onRowClick={(row) => void navigate({ to: '/modules/$moduleId', params: { moduleId: row.id } })}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-3 border-b px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<InputGroup className="w-full min-w-40 sm:max-w-xs">
|
||||
<InputGroupAddon align="inline-start">
|
||||
<SearchIcon className="text-muted-foreground size-4" aria-hidden />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
value={searchQuery}
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.target.value)
|
||||
resetPagination()
|
||||
}}
|
||||
placeholder="Поиск модулей…"
|
||||
aria-label="Поиск модулей"
|
||||
/>
|
||||
{searchQuery.length > 0 ? (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
aria-label="Очистить поиск"
|
||||
onClick={() => {
|
||||
setSearchQuery('')
|
||||
resetPagination()
|
||||
}}
|
||||
>
|
||||
<XIcon className="size-4" aria-hidden />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
) : null}
|
||||
</InputGroup>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button type="button" variant="outline" size="sm">
|
||||
<ArrowUpDownIcon data-icon="inline-start" aria-hidden />
|
||||
{sortLabels[sortBy]}
|
||||
<ChevronDownIcon data-icon="inline-end" aria-hidden />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end" className="min-w-44">
|
||||
<DropdownMenuGroup>
|
||||
{(Object.keys(sortLabels) as ModuleSort[]).map((value) => (
|
||||
<DropdownMenuItem key={value} onClick={() => handleSortChange(value)}>
|
||||
{sortLabels[value]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button type="button" variant="outline" size="sm">
|
||||
<FilterIcon data-icon="inline-start" aria-hidden />
|
||||
Фильтры
|
||||
{activeFilterCount > 0 ? (
|
||||
<Badge variant="outline" radius="full">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end" className="min-w-48">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Состояние</DropdownMenuLabel>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={enabledFilter === 'enabled'}
|
||||
closeOnClick={false}
|
||||
onCheckedChange={(checked) => {
|
||||
setEnabledFilter(checked ? 'enabled' : 'all')
|
||||
resetPagination()
|
||||
}}
|
||||
>
|
||||
Только включённые
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={enabledFilter === 'disabled'}
|
||||
closeOnClick={false}
|
||||
onCheckedChange={(checked) => {
|
||||
setEnabledFilter(checked ? 'disabled' : 'all')
|
||||
resetPagination()
|
||||
}}
|
||||
>
|
||||
Только выключенные
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuGroup>
|
||||
{activeFilterCount > 0 ? (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
closeOnClick={false}
|
||||
onClick={() => {
|
||||
setEnabledFilter('all')
|
||||
resetPagination()
|
||||
}}
|
||||
>
|
||||
Сбросить фильтры
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredModules.length > 0 ? (
|
||||
<DataGridScrollArea>
|
||||
<DataGridTable />
|
||||
</DataGridScrollArea>
|
||||
) : (
|
||||
<>
|
||||
<DataGridScrollArea>
|
||||
<DataGridTableHeader />
|
||||
</DataGridScrollArea>
|
||||
<div className="text-muted-foreground flex min-h-40 items-center justify-center px-4 text-center text-sm">
|
||||
{EMPTY_MESSAGE}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="border-t px-4 py-3">
|
||||
{filteredModules.length > 0 ? (
|
||||
<DataGridPagination
|
||||
sizes={[10, 15, 20]}
|
||||
info="{from}–{to} из {count}"
|
||||
className="py-0"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-center text-sm">0 модулей</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DataGrid>
|
||||
</DashboardFramePanel>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { AnalyticsSegmentControl } from '@/components/analytics/analytics-segment-control'
|
||||
import { SegmentedProgressCard } from '@/components/patterns/segmented-progress-card'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import {
|
||||
capacityUtilization,
|
||||
peerCapacityBars,
|
||||
speakerCapacityBars,
|
||||
} from '@/lib/metrics'
|
||||
import { runningJobCount } from '@/queries/overview'
|
||||
import type { JobRow, PeerRow, SpeakerRow } from '@/types/api'
|
||||
|
||||
type CapacityMode = 'peers' | 'speakers'
|
||||
|
||||
export function DashboardNetworkHealth({
|
||||
peers,
|
||||
speakers,
|
||||
jobs,
|
||||
loading,
|
||||
}: {
|
||||
peers: PeerRow[]
|
||||
speakers: SpeakerRow[]
|
||||
jobs: JobRow[]
|
||||
loading?: boolean
|
||||
}) {
|
||||
const [mode, setMode] = useState<CapacityMode>('peers')
|
||||
|
||||
const bars = useMemo(
|
||||
() => (mode === 'peers' ? peerCapacityBars(peers) : speakerCapacityBars(speakers)),
|
||||
[mode, peers, speakers],
|
||||
)
|
||||
const utilization = capacityUtilization(bars)
|
||||
const queued = runningJobCount(jobs)
|
||||
|
||||
const established =
|
||||
mode === 'peers'
|
||||
? peers.filter((p) => p.enabled !== false && p.session_state === 'Established').length
|
||||
: speakers.filter((s) => s.live?.agent_ok).length
|
||||
const total =
|
||||
mode === 'peers' ? peers.filter((p) => p.enabled !== false).length : speakers.length
|
||||
const offline = Math.max(0, total - established)
|
||||
|
||||
return (
|
||||
<SegmentedProgressCard
|
||||
title="Загрузка BGP"
|
||||
description="Утилизация сессий по пирам и спикерам"
|
||||
actions={
|
||||
<AnalyticsSegmentControl
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
options={[
|
||||
{ value: 'peers', label: 'Пиры' },
|
||||
{ value: 'speakers', label: 'Спикеры' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
primary={{
|
||||
value: loading ? '—' : `${utilization}%`,
|
||||
label: mode === 'peers' ? 'Утилизация пиров' : 'Спикеры online',
|
||||
percent: loading ? 0 : utilization,
|
||||
badge: (
|
||||
<Badge variant="outline" radius="full" className="h-6 px-2 text-[10px]">
|
||||
{loading ? '…' : `${established}/${total}`}
|
||||
</Badge>
|
||||
),
|
||||
}}
|
||||
secondary={{
|
||||
value: loading ? '—' : offline,
|
||||
label: mode === 'peers' ? 'Не Established' : 'Offline',
|
||||
percent: total > 0 ? Math.round((offline / total) * 100) : 0,
|
||||
}}
|
||||
footer={
|
||||
loading
|
||||
? 'Загрузка…'
|
||||
: `Активных задач: ${queued} · сегментов в графике: ${bars.length}`
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -28,10 +28,10 @@ export function DashboardNetworkPanel({
|
||||
const m = aggregateNetworkMetrics(peers, speakers)
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Row label="Пиры Established" value={`${m.peersEstablished} / ${m.peersEnabled}`} />
|
||||
<Row label="Спикеры online" value={`${m.speakersOnline} / ${m.speakersTotal}`} />
|
||||
<Row label="Пиры с установленной сессией" value={`${m.peersEstablished} / ${m.peersEnabled}`} />
|
||||
<Row label="Спикеры в сети" value={`${m.speakersOnline} / ${m.speakersTotal}`} />
|
||||
{m.peersMismatch > 0 ? (
|
||||
<Row label="Mismatches" value={String(m.peersMismatch)} variant="warning" />
|
||||
<Row label="Расхождения сессий" value={String(m.peersMismatch)} variant="warning" />
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user