Compare commits

..
2 Commits
Author SHA1 Message Date
Denozordec 3a9bd0dba3 feat: add reui configuration for enhanced API integration
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m0s
CI / go (push) Successful in 1m14s
CI / bird2 (push) Successful in 19s
CI / release (push) Successful in 4m22s
Updated .mcp.json and .cursor/mcp.json to include reui configuration for HTTP API integration, specifying the URL and necessary headers. Modified components.json to enhance the @reui registry with authorization headers, improving security and access to resources.
2026-07-09 18:02:47 +07:00
Denozordec 940f8892f3 refactor: enhance UI components with PanelCard integration and improved layout consistency
CI / changes (push) Successful in 13s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m6s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m17s
Updated multiple components to utilize the new PanelCard for better organization and presentation of content. Refactored DataGridShell, DataGridCard, and various analytics components to streamline layouts and enhance user experience. Adjusted styles for consistency across components, including pagination and toolbar elements, ensuring a cohesive interface throughout the application.
2026-07-09 17:23:17 +07:00
95 changed files with 3770 additions and 368 deletions
+67
View File
@@ -0,0 +1,67 @@
---
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 `0e224b0281`.** 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
+43
View File
@@ -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.
+58
View File
@@ -0,0 +1,58 @@
# CLI: registry setup, license, non-interactive install
## Registry setup (one-time, per project)
Free items (the 17 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 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 17 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).
+330
View File
@@ -0,0 +1,330 @@
# ReUI components
The 17 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `filters`, `frame`, `icon-stack`, `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 v8. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
- Build a TanStack table instance with `useReactTable(...)` (columns, data, the feature models you need: sorting, pagination, row selection).
- 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 = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
// add sorting/pagination/selection models per the API
})
<DataGrid table={table} recordCount={data.length}>
<DataGridTable />
</DataGrid>
```
Common mistakes:
- **Incorrect:** `<DataGrid data={rows} columns={cols} />` - these props do not exist. **Correct:** build a `useReactTable` 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 documented `ColumnMeta` (e.g. `cellClassName`, `headerTitle`).
## 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.
## 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.
+45
View File
@@ -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.
+39
View File
@@ -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.
+22
View File
@@ -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.
+35
View File
@@ -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 17 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 17 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.
+26
View File
@@ -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.
+54
View File
@@ -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 17 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 `useReactTable` 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.
+58
View File
@@ -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
+67
View File
@@ -0,0 +1,67 @@
---
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 `0e224b0281`.** 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
+43
View File
@@ -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.
+58
View File
@@ -0,0 +1,58 @@
# CLI: registry setup, license, non-interactive install
## Registry setup (one-time, per project)
Free items (the 17 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 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 17 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).
+330
View File
@@ -0,0 +1,330 @@
# ReUI components
The 17 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `filters`, `frame`, `icon-stack`, `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 v8. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
- Build a TanStack table instance with `useReactTable(...)` (columns, data, the feature models you need: sorting, pagination, row selection).
- 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 = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
// add sorting/pagination/selection models per the API
})
<DataGrid table={table} recordCount={data.length}>
<DataGridTable />
</DataGrid>
```
Common mistakes:
- **Incorrect:** `<DataGrid data={rows} columns={cols} />` - these props do not exist. **Correct:** build a `useReactTable` 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 documented `ColumnMeta` (e.g. `cellClassName`, `headerTitle`).
## 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.
## 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.
+45
View File
@@ -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.
+39
View File
@@ -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.
+22
View File
@@ -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.
+35
View File
@@ -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 17 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 17 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.
+26
View File
@@ -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.
+54
View File
@@ -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 17 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 `useReactTable` 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.
+58
View File
@@ -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
+6
View File
@@ -9,6 +9,12 @@
"--path",
"C:\\Users\\shats\\Dev\\EvoBGP"
]
},
"reui": {
"url": "https://mcp.reui.io/api/mcp?style=base-nova",
"headers": {
"X-Reui-Style": "base-nova"
}
}
}
}
+73
View File
@@ -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 `0e224b0281`.** 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
+67
View File
@@ -0,0 +1,67 @@
---
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 `0e224b0281`.** 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
+43
View File
@@ -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.
+58
View File
@@ -0,0 +1,58 @@
# CLI: registry setup, license, non-interactive install
## Registry setup (one-time, per project)
Free items (the 17 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 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 17 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).
+330
View File
@@ -0,0 +1,330 @@
# ReUI components
The 17 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `filters`, `frame`, `icon-stack`, `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 v8. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
- Build a TanStack table instance with `useReactTable(...)` (columns, data, the feature models you need: sorting, pagination, row selection).
- 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 = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
// add sorting/pagination/selection models per the API
})
<DataGrid table={table} recordCount={data.length}>
<DataGridTable />
</DataGrid>
```
Common mistakes:
- **Incorrect:** `<DataGrid data={rows} columns={cols} />` - these props do not exist. **Correct:** build a `useReactTable` 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 documented `ColumnMeta` (e.g. `cellClassName`, `headerTitle`).
## 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.
## 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.
+45
View File
@@ -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.
+39
View File
@@ -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.
+22
View File
@@ -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.
+35
View File
@@ -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 17 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 17 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.
+26
View File
@@ -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.
+54
View File
@@ -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 17 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 `useReactTable` 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.
+58
View File
@@ -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
+4
View File
@@ -7,6 +7,10 @@
"serve",
"--mcp"
]
},
"reui": {
"type": "http",
"url": "https://mcp.reui.io/api/mcp"
}
}
}
+67
View File
@@ -0,0 +1,67 @@
---
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 `0e224b0281`.** 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
+43
View File
@@ -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.
+58
View File
@@ -0,0 +1,58 @@
# CLI: registry setup, license, non-interactive install
## Registry setup (one-time, per project)
Free items (the 17 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 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 17 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).
+330
View File
@@ -0,0 +1,330 @@
# ReUI components
The 17 ReUI building blocks: `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `filters`, `frame`, `icon-stack`, `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 v8. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
- Build a TanStack table instance with `useReactTable(...)` (columns, data, the feature models you need: sorting, pagination, row selection).
- 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 = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
// add sorting/pagination/selection models per the API
})
<DataGrid table={table} recordCount={data.length}>
<DataGridTable />
</DataGrid>
```
Common mistakes:
- **Incorrect:** `<DataGrid data={rows} columns={cols} />` - these props do not exist. **Correct:** build a `useReactTable` 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 documented `ColumnMeta` (e.g. `cellClassName`, `headerTitle`).
## 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.
## 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.
+45
View File
@@ -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.
+39
View File
@@ -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.
+22
View File
@@ -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.
+35
View File
@@ -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 17 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 17 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.
+26
View File
@@ -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.
+54
View File
@@ -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 17 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 `useReactTable` 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.
+58
View File
@@ -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
+6 -1
View File
@@ -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",
@@ -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 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-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>
)
}
@@ -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>
@@ -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>
@@ -18,15 +18,15 @@ export function MonitoringHealthCard({
return (
<AnalyticsCardShell
title="Доступность системы"
description="Health и readiness checks"
info="Donut отражает результат GET /v1/health и checks из GET /v1/ready."
description="Проверки живучести и готовности"
info="Диаграмма отражает результат GET /v1/health и проверок из 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} />
<ChartDonutMetric slices={slices} centerLabel="Проверки" centerValue={total} />
)}
</AnalyticsCardShell>
)
@@ -21,29 +21,29 @@ export function NetworkOverviewAnalyticsCard({
return (
<AnalyticsCardShell
title="Сводка BGP"
description="Established, online и mismatch по live-данным"
description="Установленные сессии, доступность спикеров и расхождения по live-данным"
info="Снимок текущего состояния пиров и спикеров."
>
<AnalyticsKpiRow
items={[
{
label: 'Пиры Established',
label: 'Пиры с установленной сессией',
value: loading ? '—' : `${net.peersEstablished}/${net.peersEnabled}`,
delta: {
direction: net.peersMismatch > 0 ? 'down' : 'up',
label: net.peersMismatch > 0 ? `${net.peersMismatch} mismatch` : 'сессии в норме',
label: net.peersMismatch > 0 ? `${net.peersMismatch} расхождений` : 'сессии в норме',
tone: net.peersMismatch > 0 ? 'warning' : 'success',
},
},
{
label: 'Спикеры online',
label: 'Спикеры в сети',
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',
? `${net.speakersTotal - net.speakersOnline} не в сети`
: 'все в сети',
tone: net.speakersOnline < net.speakersTotal ? 'warning' : 'success',
},
},
@@ -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>
)
@@ -2,18 +2,17 @@ import { Link } from '@tanstack/react-router'
import { Gauge, Network, Play, Plus, Share2, Tags } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button'
import { CardFooter } from '@evobgp/ui/components/card'
export function DashboardQuickActions() {
return (
<CardFooter className="flex flex-wrap gap-2 border-t-0 bg-transparent">
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" type="button" render={<Link to="/modules" />}>
<Plus className="size-4" />
Создать модуль
</Button>
<Button variant="outline" size="sm" type="button" render={<Link to="/directories" />}>
<Tags className="size-4" />
Добавить community
Добавить BGP-сообщество
</Button>
<Button variant="outline" size="sm" type="button" render={<Link to="/network" search={{ tab: 'overview' }} />}>
<Network className="size-4" />
@@ -30,12 +29,12 @@ export function DashboardQuickActions() {
render={<Link to="/operations" search={{ tab: 'revisions' }} />}
>
<Play className="size-4" />
Деплой (Apply)
Деплой
</Button>
<Button variant="outline" size="sm" type="button" render={<Link to="/monitoring" search={{ tab: 'system' }} />}>
<Gauge className="size-4" />
Мониторинг
</Button>
</CardFooter>
</div>
)
}
@@ -6,6 +6,7 @@ import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { DATA_GRID_DENSE_LAYOUT } from '@/lib/data-grid-defaults'
import { jobKindRu } from '@/lib/ui-labels'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { JobRow } from '@/types/api'
@@ -27,7 +28,7 @@ export function DashboardRecentJobsGrid({
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => (
<DataGridPrimaryCell
title={row.original.kind}
title={jobKindRu(row.original.kind)}
accent="mono"
subtitle={
row.original.meta?.module_id
@@ -55,7 +56,7 @@ export function DashboardRecentJobsGrid({
const moduleName = row.meta?.module_id
? (nameById.get(String(row.meta.module_id)) ?? '')
: ''
return `${row.kind} ${row.status} ${moduleName}`
return `${jobKindRu(row.kind)} ${row.status} ${moduleName}`
},
getRowId: (row) => row.job_id,
pageSize: 8,
+18 -17
View File
@@ -1,14 +1,17 @@
import type { ReactNode } from 'react'
import type { Table } from '@tanstack/react-table'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { cn } from '@evobgp/ui/lib/utils'
import { DataGridToolbar } from '@/components/data-grid-toolbar'
import { PanelCard, panelCardContentFlushClassName, panelCardFooterClassName, panelCardInsetClassName } from '@/components/panel-card'
import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination'
import { DataGrid, DataGridContainer } from '@/components/reui/data-grid/data-grid'
import { DataGridTable } from '@/components/reui/data-grid/data-grid-table'
import {
DATA_GRID_MESSAGES_RU,
DATA_GRID_PAGINATION_RU,
DATA_GRID_TABLE_CLASS_NAMES,
DATA_GRID_TABLE_LAYOUT,
} from '@/lib/data-grid-defaults'
@@ -38,16 +41,18 @@ export function DataGridShell<TData extends object>({
table={table}
recordCount={recordCount}
isLoading={isLoading}
emptyMessage={emptyMessage}
emptyMessage={emptyMessage ?? DATA_GRID_MESSAGES_RU.emptyMessage}
loadingMessage={DATA_GRID_MESSAGES_RU.loadingMessage}
tableLayout={tableLayout}
tableClassNames={DATA_GRID_TABLE_CLASS_NAMES}
className={className}
onRowClick={onRowClick}
>
<DataGridContainer>
<DataGridContainer border={false}>
<DataGridTable />
</DataGridContainer>
{showPagination ? (
<div className="border-t px-4 py-3">
<div className={cn(panelCardFooterClassName, panelCardInsetClassName)}>
<DataGridPagination {...DATA_GRID_PAGINATION_RU} />
</div>
) : null}
@@ -64,20 +69,16 @@ interface DataGridCardProps {
}
export function DataGridCard({ title, description, actions, children, className }: DataGridCardProps) {
const hasHeader = Boolean(title || description || actions)
return (
<Card className={className ?? 'gap-0'}>
{hasHeader ? (
<CardHeader className="flex flex-row items-center justify-between border-b py-3">
<div className="flex flex-col gap-0.5">
{title ? <CardTitle className="text-base">{title}</CardTitle> : null}
{description ? <CardDescription>{description}</CardDescription> : null}
</div>
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
</CardHeader>
) : null}
<CardContent className="p-0">{children}</CardContent>
</Card>
<PanelCard
title={title}
description={description}
actions={actions}
className={className}
contentClassName={panelCardContentFlushClassName}
>
{children}
</PanelCard>
)
}
@@ -9,6 +9,9 @@ import {
} from '@evobgp/ui/components/input-group'
import { ListFilterIcon, SearchIcon, XIcon } from 'lucide-react'
import { panelCardInsetClassName } from '@/components/panel-card'
import { cn } from '@evobgp/ui/lib/utils'
interface DataGridToolbarProps {
searchValue: string
onSearchChange: (value: string) => void
@@ -28,7 +31,7 @@ export function DataGridToolbar({
className,
}: DataGridToolbarProps) {
return (
<div className={`flex flex-wrap items-center gap-2 border-b px-4 py-3 ${className ?? ''}`}>
<div className={cn('flex flex-wrap items-center gap-3 border-b', panelCardInsetClassName, className)}>
<Field className="min-w-[200px] flex-1">
<InputGroup>
<InputGroupAddon align="inline-start">
@@ -60,7 +60,7 @@ export function FirewallClientsGrid({
{
id: 'last_seen_at',
accessorFn: (row) => row.last_seen_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Last seen" />,
header: ({ column }) => <DataGridColumnHeader column={column} title="Последняя активность" />,
cell: ({ row }) => (
<DataGridMutedCell>{row.original.last_seen_at?.slice(0, 19) ?? '—'}</DataGridMutedCell>
),
@@ -69,12 +69,12 @@ export function FirewallClientsGrid({
const bv = b.original.last_seen_at ?? ''
return av.localeCompare(bv)
},
meta: { headerTitle: 'Last seen' },
meta: { headerTitle: 'Последняя активность' },
},
{
id: 'apply',
enableSorting: false,
header: 'Apply',
header: 'Применение',
cell: ({ row }) => {
const c = row.original
return (
@@ -84,7 +84,7 @@ export function FirewallClientsGrid({
</span>
)
},
meta: { headerTitle: 'Apply' },
meta: { headerTitle: 'Применение' },
},
{
id: 'packets',
+3 -3
View File
@@ -56,7 +56,7 @@ interface NavGroup {
const NAV_GROUPS: NavGroup[] = [
{
label: 'Обзор',
items: [{ to: '/dashboard', label: 'Dashboard', icon: LayoutDashboard }],
items: [{ to: '/dashboard', label: 'Панель', icon: LayoutDashboard }],
},
{
label: 'Маршрутизация',
@@ -70,7 +70,7 @@ const NAV_GROUPS: NavGroup[] = [
label: 'Операции',
items: [
{ to: '/operations', label: 'Операции', icon: Cog },
{ to: '/firewall', label: 'Firewall', icon: Shield },
{ to: '/firewall', label: 'Файрвол', icon: Shield },
{ to: '/schedule', label: 'Задачи', icon: ListChecks },
{ to: '/monitoring', label: 'Мониторинг', icon: Activity },
],
@@ -111,7 +111,7 @@ export function AppShell({ children }: { children: ReactNode }) {
</div>
<div className="flex flex-col overflow-hidden group-data-[collapsible=icon]:hidden">
<span className="truncate text-sm font-semibold">EvoBGP</span>
<span className="truncate text-xs text-muted-foreground">Control Plane</span>
<span className="truncate text-xs text-muted-foreground">Плоскость управления</span>
</div>
</div>
</SidebarHeader>
@@ -8,6 +8,7 @@ import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-c
import { DataGridSection } from '@/components/data-grid-shell'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { moduleTypeRu } from '@/lib/ui-labels'
import type { ModuleRow } from '@/types/api'
export function ModulesListGrid({
@@ -35,7 +36,7 @@ export function ModulesListGrid({
{
accessorKey: 'type',
header: ({ column }) => <DataGridColumnHeader column={column} title="Тип" />,
cell: ({ row }) => <CategoryBadge>{row.original.type}</CategoryBadge>,
cell: ({ row }) => <CategoryBadge>{moduleTypeRu(row.original.type)}</CategoryBadge>,
meta: { headerTitle: 'Тип' },
},
{
@@ -7,6 +7,7 @@ import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { jobStatusRu, readyCheckRu } from '@/lib/ui-labels'
import type { ReadyStatus } from '@/queries/monitoring'
const READY_CHECK_ICONS: Record<string, typeof Database> = {
@@ -36,19 +37,19 @@ export function MonitoringReadyGrid({
const rows: ReadyCheckRow[] = [
{
id: 'liveness',
label: 'Liveness',
label: 'Живучесть',
subtitle: '/v1/health',
icon: HeartPulse,
status: health?.ok ? 'ok' : 'error',
statusLabel: health?.ok ? 'OK' : 'Ошибка',
statusLabel: health?.ok ? 'В норме' : 'Ошибка',
},
{
id: 'readiness',
label: 'Readiness',
label: 'Готовность',
subtitle: '/v1/ready',
icon: ShieldCheck,
status: ready.status === 'ok' ? 'ok' : 'warning',
statusLabel: ready.status ?? '—',
statusLabel: ready.status === 'ok' ? 'Готов' : jobStatusRu(ready.status ?? 'pending'),
},
]
for (const key of Object.keys(checks)) {
@@ -56,10 +57,10 @@ export function MonitoringReadyGrid({
const ok = typeof value === 'boolean' ? value : value?.ok !== false
rows.push({
id: key,
label: key,
label: readyCheckRu(key),
icon: READY_CHECK_ICONS[key] ?? ListTodo,
status: ok ? 'ok' : 'error',
statusLabel: ok ? 'OK' : 'Ошибка',
statusLabel: ok ? 'В норме' : 'Ошибка',
})
}
return rows
@@ -7,6 +7,7 @@ import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { bgpSessionStateRu } from '@/lib/ui-labels'
import type { PeerRow } from '@/types/api'
export function NetworkPeersGrid({
@@ -33,11 +34,11 @@ export function NetworkPeersGrid({
},
{
accessorKey: 'neighbor',
header: ({ column }) => <DataGridColumnHeader column={column} title="Neighbor" />,
header: ({ column }) => <DataGridColumnHeader column={column} title="Адрес соседа" />,
cell: ({ row }) => (
<DataGridPrimaryCell title={row.original.neighbor} accent="mono" />
),
meta: { headerTitle: 'Neighbor' },
meta: { headerTitle: 'Адрес соседа' },
},
{
accessorKey: 'remote_asn',
@@ -52,9 +53,12 @@ export function NetworkPeersGrid({
header: ({ column }) => <DataGridColumnHeader column={column} title="Состояние" />,
cell: ({ row }) => (
<div className="flex items-center gap-1">
<StatusBadge status={row.original.session_state} />
<StatusBadge
status={row.original.session_state ?? '—'}
label={bgpSessionStateRu(row.original.session_state)}
/>
{row.original.session_mismatch ? (
<CategoryBadge tone="warning">mismatch</CategoryBadge>
<CategoryBadge tone="warning">расхождение</CategoryBadge>
) : null}
</div>
),
@@ -8,6 +8,7 @@ import { StatusBadge } from '@/components/status-badge'
import { Badge } from '@/components/reui/badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { speakerOnlineLabel } from '@/lib/ui-labels'
import type { SpeakerRow } from '@/types/api'
export function NetworkSpeakersGrid({
@@ -21,11 +22,11 @@ export function NetworkSpeakersGrid({
() => [
{
accessorKey: 'endpoint',
header: ({ column }) => <DataGridColumnHeader column={column} title="Endpoint" />,
header: ({ column }) => <DataGridColumnHeader column={column} title="Конечная точка" />,
cell: ({ row }) => (
<DataGridPrimaryCell title={row.original.endpoint} accent="mono" />
),
meta: { headerTitle: 'Endpoint' },
meta: { headerTitle: 'Конечная точка' },
},
{
accessorKey: 'role',
@@ -36,14 +37,14 @@ export function NetworkSpeakersGrid({
{
id: 'agent',
enableSorting: false,
header: 'Agent',
header: 'Агент',
cell: ({ row }) => {
const live = row.original.live
if (live?.agent_ok === true) return <StatusBadge status="ok" label="online" />
if (live?.agent_ok === false) return <StatusBadge status="error" label="offline" />
if (live?.agent_ok === true) return <StatusBadge status="ok" label={speakerOnlineLabel(true)} />
if (live?.agent_ok === false) return <StatusBadge status="error" label={speakerOnlineLabel(false)} />
return <Badge variant="outline" size="sm" radius="full"></Badge>
},
meta: { headerTitle: 'Agent' },
meta: { headerTitle: 'Агент' },
},
{
id: 'bgp',
@@ -109,7 +109,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
}
>
<div className="flex flex-col gap-2">
<Label htmlFor="speaker-endpoint">Endpoint</Label>
<Label htmlFor="speaker-endpoint">Конечная точка</Label>
<Input
id="speaker-endpoint"
placeholder="https://node.example.com:8443"
@@ -121,14 +121,14 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
id="speaker-role"
label="Роль"
items={[
{ value: 'replica', label: 'replica' },
{ value: 'master', label: 'master (CP)' },
{ value: 'replica', label: 'Реплика' },
{ value: 'master', label: 'Мастер (CP)' },
]}
value={role}
onValueChange={(v) => setRole(v ?? 'replica')}
/>
<div className="flex flex-col gap-2">
<Label htmlFor="speaker-agent-domain">Agent domain</Label>
<Label htmlFor="speaker-agent-domain">Домен агента</Label>
<Input
id="speaker-agent-domain"
placeholder="bird-agent.example.com"
@@ -137,7 +137,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="speaker-node-ipv4">Node IPv4</Label>
<Label htmlFor="speaker-node-ipv4">IPv4 ноды</Label>
<Input
id="speaker-node-ipv4"
placeholder="203.0.113.10"
@@ -11,6 +11,7 @@ import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { apiMutate } from '@/lib/api-client'
import { jobKindRu } from '@/lib/ui-labels'
import type { JobRow } from '@/types/api'
import type { QueryClient } from '@tanstack/react-query'
@@ -41,7 +42,7 @@ export function OperationsJobsGrid({
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => (
<DataGridPrimaryCell
title={row.original.kind}
title={jobKindRu(row.original.kind)}
accent="mono"
subtitle={
row.original.meta?.module_id
@@ -119,7 +120,7 @@ export function OperationsJobsGrid({
const moduleName = row.meta?.module_id
? (nameById.get(String(row.meta.module_id)) ?? String(row.meta.module_id))
: ''
return `${row.kind} ${row.status} ${row.job_id} ${moduleName}`
return `${jobKindRu(row.kind)} ${row.status} ${row.job_id} ${moduleName}`
},
getRowId: (row) => row.job_id,
})
+66
View File
@@ -0,0 +1,66 @@
import type { ReactNode } from 'react'
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@evobgp/ui/components/card'
import { cn } from '@evobgp/ui/lib/utils'
/** REUI c-data-grid-19: card shell with default spacing and flush grid body. */
export const panelCardClassName = 'gap-0 has-data-[slot=card-footer]:pb-0'
export const panelCardHeaderClassName = 'border-b'
export const panelCardContentFlushClassName = 'px-0'
/** Horizontal inset for toolbar / pagination rows (matches edgeCell px-5). */
export const panelCardInsetClassName = 'px-5 py-3'
export const panelCardFooterClassName = 'border-t bg-transparent'
interface PanelCardProps {
title?: ReactNode
description?: ReactNode
actions?: ReactNode
footer?: ReactNode
children?: ReactNode
className?: string
headerClassName?: string
contentClassName?: string
footerClassName?: string
size?: 'default' | 'sm'
}
export function PanelCard({
title,
description,
actions,
footer,
children,
className,
headerClassName,
contentClassName,
footerClassName,
size = 'default',
}: PanelCardProps) {
const hasHeader = Boolean(title || description || actions)
return (
<Card size={size} className={cn(panelCardClassName, className)}>
{hasHeader ? (
<CardHeader className={cn(panelCardHeaderClassName, headerClassName)}>
{title ? <CardTitle>{title}</CardTitle> : null}
{description ? <CardDescription>{description}</CardDescription> : null}
{actions ? <CardAction>{actions}</CardAction> : null}
</CardHeader>
) : null}
{children != null && children !== false ? (
<CardContent className={contentClassName}>{children}</CardContent>
) : null}
{footer ? (
<CardFooter className={cn(panelCardFooterClassName, footerClassName)}>{footer}</CardFooter>
) : null}
</Card>
)
}
@@ -140,12 +140,12 @@ function DataGridPagination(props: DataGridPaginationProps): React.JSX.Element {
mergedProps?.className
)}
>
<div className="order-2 flex flex-wrap items-center space-x-2.5 pb-2.5 sm:order-1 sm:pb-0">
<div className="order-2 flex flex-wrap items-center gap-2 pb-2.5 sm:order-1 sm:pb-0">
{isLoading ? (
mergedProps?.sizesSkeleton
) : (
<>
<div className="text-muted-foreground text-sm">
<div className="shrink-0 text-sm text-muted-foreground whitespace-nowrap">
{mergedProps.rowsPerPageLabel}
</div>
<SelectMenu
@@ -156,10 +156,10 @@ function DataGridPagination(props: DataGridPaginationProps): React.JSX.Element {
})) ?? []
}
value={`${pageSize}`}
triggerClassName="w-14"
triggerClassName="min-w-20 w-auto tabular-nums"
size="sm"
side="top"
contentClassName="min-w-18"
contentClassName="min-w-20"
onValueChange={(value) => {
if (!value) return
table.setPageSize(Number(value))
@@ -21,6 +21,7 @@ import {
DataGridTableViewport,
getDataGridTableRowSections,
} from "@/components/reui/data-grid/data-grid-table"
import { DATA_GRID_MESSAGES_RU } from "@/lib/data-grid-defaults"
import { flexRender, HeaderGroup, Row, Table } from "@tanstack/react-table"
import {
useVirtualizer,
@@ -304,9 +305,9 @@ function DataGridTableVirtual<TData>({
const isVirtualizationEnabled = virtualizerOptions?.enabled !== false
const loadingMoreMessage =
props.fetchingMoreMessage || props.loadingMessage || "Loading..."
props.fetchingMoreMessage || props.loadingMessage || DATA_GRID_MESSAGES_RU.loadingMessage
const allRowsLoadedMessage =
props.allRowsLoadedMessage || "All records loaded"
props.allRowsLoadedMessage || DATA_GRID_MESSAGES_RU.allRecordsLoadedMessage
const handleViewportRef = useCallback((node: HTMLDivElement | null) => {
setViewportElements({
@@ -28,6 +28,7 @@ import { cva } from "class-variance-authority"
import { cn } from "@evobgp/ui/lib/utils"
import { Checkbox } from "@evobgp/ui/components/checkbox"
import { Spinner } from "@evobgp/ui/components/spinner"
import { DATA_GRID_MESSAGES_RU } from "@/lib/data-grid-defaults"
const headerCellSpacingVariants = cva("", {
variants: {
@@ -1098,7 +1099,7 @@ function DataGridTableEmpty() {
colSpan={Math.max(visibleColumnCount, 1)}
className="text-muted-foreground text-sm py-6 text-center"
>
{props.emptyMessage || "No data available"}
{props.emptyMessage || DATA_GRID_MESSAGES_RU.emptyMessage}
</td>
</tr>
)
@@ -1111,7 +1112,7 @@ function DataGridTableLoader() {
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="text-muted-foreground bg-card rounded-lg text-sm flex items-center gap-2 border px-4 py-2 leading-none font-medium">
<Spinner className="size-5 opacity-60" />
{props.loadingMessage || "Loading..."}
{props.loadingMessage || DATA_GRID_MESSAGES_RU.loadingMessage}
</div>
</div>
)
@@ -1123,7 +1124,7 @@ function DataGridTableRowPin<TData>({ row }: { row: Row<TData> }) {
return (
<button
type="button"
aria-label={isPinned ? "Unpin row" : "Pin row"}
aria-label={isPinned ? DATA_GRID_MESSAGES_RU.unpinRowLabel : DATA_GRID_MESSAGES_RU.pinRowLabel}
onClick={() => {
if (isPinned) {
row.pin(false)
@@ -1179,7 +1180,7 @@ function DataGridTableRowSelect<TData>({ row }: { row: Row<TData> }) {
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
aria-label={DATA_GRID_MESSAGES_RU.selectRowLabel}
className="align-[inherit]"
/>
</>
@@ -1198,7 +1199,7 @@ function DataGridTableRowSelectAll() {
indeterminate={isSomeSelected && !isAllSelected}
disabled={isLoading || recordCount === 0}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
aria-label={DATA_GRID_MESSAGES_RU.selectAllLabel}
className="align-[inherit]"
/>
)
@@ -1249,7 +1250,7 @@ function DataGridTableBodyRows<TData>({ table }: { table: Table<TData> }) {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{props.loadingMessage || "Loading..."}
{props.loadingMessage || DATA_GRID_MESSAGES_RU.loadingMessage}
</div>
</td>
</tr>
@@ -6,6 +6,7 @@ import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { jobKindRu } from '@/lib/ui-labels'
import type { JobRow } from '@/types/api'
export function ScheduleJobsGrid({
@@ -20,7 +21,7 @@ export function ScheduleJobsGrid({
{
accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => <DataGridPrimaryCell title={row.original.kind} accent="mono" />,
cell: ({ row }) => <DataGridPrimaryCell title={jobKindRu(row.original.kind)} accent="mono" />,
meta: { headerTitle: 'Вид' },
},
{
@@ -79,7 +80,7 @@ export function ScheduleJobsGrid({
const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({
data: items,
columns,
getSearchText: (row) => `${row.kind} ${row.status} ${row.error ?? ''}`,
getSearchText: (row) => `${jobKindRu(row.kind)} ${row.status} ${row.error ?? ''}`,
getRowId: (row) => row.job_id,
})
@@ -8,6 +8,7 @@ import { DataGridSection } from '@/components/data-grid-shell'
import { LoadingButton } from '@/components/loading-button'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { moduleTypeRu } from '@/lib/ui-labels'
import type { ModuleRow } from '@/types/api'
export function ScheduleModulesGrid({
@@ -32,7 +33,7 @@ export function ScheduleModulesGrid({
{
accessorKey: 'type',
header: ({ column }) => <DataGridColumnHeader column={column} title="Тип" />,
cell: ({ row }) => <CategoryBadge>{row.original.type}</CategoryBadge>,
cell: ({ row }) => <CategoryBadge>{moduleTypeRu(row.original.type)}</CategoryBadge>,
meta: { headerTitle: 'Тип' },
},
{
+2 -1
View File
@@ -42,7 +42,7 @@ export function SectionCards({ items, className }: { items: SectionCardItem[]; c
{items.map((item, idx) => {
const clickable = Boolean(item.onClick)
const content = (
<CardContent className="flex items-start gap-2.5 px-3 py-2.5">
<CardContent className="flex items-start gap-3 px-4 py-3">
{item.icon ? (
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted/60 text-muted-foreground">
{item.icon}
@@ -80,6 +80,7 @@ export function SectionCards({ items, className }: { items: SectionCardItem[]; c
return (
<Card
key={typeof item.label === 'string' ? item.label : idx}
size="sm"
className={cn(
'gap-0',
VARIANT_CLASS[item.variant ?? 'default'],
+7 -5
View File
@@ -1,6 +1,8 @@
import { Card, CardContent } from '@evobgp/ui/components/card'
import { Skeleton } from '@evobgp/ui/components/skeleton'
import { cn } from '@evobgp/ui/lib/utils'
import { panelCardInsetClassName } from '@/components/panel-card'
import { SectionCards } from './section-cards'
export function SectionCardsSkeleton({ count = 4 }: { count?: number }) {
@@ -19,7 +21,7 @@ export function AnalyticsDashboardSkeleton() {
return (
<div className="grid gap-4 lg:grid-cols-3 lg:items-start">
<Card className="gap-0">
<CardContent className="space-y-4 p-5">
<CardContent className="space-y-5 py-5">
<Skeleton className="h-4 w-40" />
<div className="grid gap-4 sm:grid-cols-3">
{Array.from({ length: 3 }).map((_, i) => (
@@ -31,14 +33,14 @@ export function AnalyticsDashboardSkeleton() {
</CardContent>
</Card>
<Card className="gap-0">
<CardContent className="space-y-4 p-5">
<CardContent className="space-y-5 py-5">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-10 w-24" />
<Skeleton className="h-36 w-full" />
</CardContent>
</Card>
<Card className="gap-0">
<CardContent className="space-y-4 p-5">
<CardContent className="space-y-5 py-5">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-44 w-full" />
</CardContent>
@@ -52,13 +54,13 @@ export function TableSkeleton({ rows = 6, cols = 4 }: { rows?: number; cols?: nu
<Card className="gap-0">
<CardContent className="p-0">
<div className="flex flex-col">
<div className="flex gap-2 border-b p-3">
<div className={cn('flex gap-2 border-b', panelCardInsetClassName)}>
{Array.from({ length: cols }).map((_, i) => (
<Skeleton className="h-4 flex-1" key={`h-${i}`} />
))}
</div>
{Array.from({ length: rows }).map((_, r) => (
<div className="flex gap-2 border-b p-3" key={`r-${r}`}>
<div className={cn('flex gap-2 border-b', panelCardInsetClassName)} key={`r-${r}`}>
{Array.from({ length: cols }).map((_, c) => (
<Skeleton className="h-4 flex-1" key={`c-${r}-${c}`} />
))}
+2 -1
View File
@@ -3,6 +3,7 @@ import type { ComponentProps } from 'react'
import { cn } from '@evobgp/ui/lib/utils'
import { Badge } from '@/components/reui/badge'
import { jobStatusRu } from '@/lib/ui-labels'
type BadgeVariant = NonNullable<ComponentProps<typeof Badge>['variant']>
@@ -63,7 +64,7 @@ export function StatusBadge({
<div className={cn('flex flex-col gap-0.5', className)}>
<Badge variant={variant} size="sm" radius="full" className="gap-1.5">
<span className={cn('size-1.5 shrink-0 rounded-full', dotColor)} aria-hidden />
{label ?? status}
{label ?? jobStatusRu(status)}
</Badge>
{hint ? <span className="text-xs text-muted-foreground">{hint}</span> : null}
</div>
+6 -4
View File
@@ -1,10 +1,12 @@
import type { ApiKeyRole } from '@/types/api'
import { apiKeyRoleRu } from '@/lib/ui-labels'
export const API_KEY_ROLE_ITEMS: ReadonlyArray<{ value: ApiKeyRole; label: string }> = [
{ value: 'viewer', label: 'viewer — только чтение' },
{ value: 'editor', label: 'editor — CRUD без apply' },
{ value: 'operator', label: 'operator — полный доступ' },
{ value: 'node', label: 'node — только API ноды' },
{ value: 'viewer', label: `${apiKeyRoleRu('viewer')} — только чтение` },
{ value: 'editor', label: `${apiKeyRoleRu('editor')} — CRUD без применения` },
{ value: 'operator', label: `${apiKeyRoleRu('operator')} — полный доступ` },
{ value: 'node', label: `${apiKeyRoleRu('node')} — только API ноды` },
]
export function apiKeyRoleLabel(role: ApiKeyRole): string {
+17 -1
View File
@@ -10,11 +10,16 @@ import {
import type { DataGridProps } from '@/components/reui/data-grid/data-grid'
export const DATA_GRID_TABLE_LAYOUT: NonNullable<DataGridProps<object>['tableLayout']> = {
dense: true,
dense: false,
headerSticky: true,
rowBorder: true,
}
/** REUI c-data-grid-19 edge cell padding. */
export const DATA_GRID_TABLE_CLASS_NAMES: NonNullable<DataGridProps<object>['tableClassNames']> = {
edgeCell: 'px-5',
}
export const DATA_GRID_PAGINATION_RU = {
sizes: [10, 25, 50] as number[],
sizesLabel: 'Показать',
@@ -25,6 +30,17 @@ export const DATA_GRID_PAGINATION_RU = {
nextPageLabel: 'Следующая страница',
}
export const DATA_GRID_MESSAGES_RU = {
emptyMessage: 'Нет данных',
loadingMessage: 'Загрузка…',
fetchingMoreMessage: 'Загрузка…',
selectAllLabel: 'Выбрать все',
selectRowLabel: 'Выбрать строку',
pinRowLabel: 'Закрепить строку',
unpinRowLabel: 'Открепить строку',
allRecordsLoadedMessage: 'Все записи загружены',
}
export const DATA_GRID_DENSE_LAYOUT: NonNullable<DataGridProps<object>['tableLayout']> = {
...DATA_GRID_TABLE_LAYOUT,
dense: true,
@@ -30,3 +30,25 @@ export function deploymentProgress(speakers: SpeakerRow[]): DeploymentProgress {
mode: 'online',
}
}
export function deploymentProgressMeta(deploy: DeploymentProgress): {
label: string
hint: string
} {
if (deploy.total === 0) {
return {
label: 'Деплой на спикерах',
hint: 'Нет зарегистрированных BIRD-спикеров',
}
}
if (deploy.mode === 'revision') {
return {
label: `Применена ревизия (${deploy.synced} из ${deploy.total} спикеров)`,
hint: 'Доля спикеров, на которых последняя опубликованная ревизия уже применена',
}
}
return {
label: `Спикеры в сети (${deploy.synced} из ${deploy.total})`,
hint: 'Ревизии ещё не публиковались — показана доступность агента на нодах',
}
}
@@ -14,7 +14,7 @@ export function peerSessionBreakdown(peers: PeerRow[]): BreakdownSlice[] {
if (established > 0) {
slices.push({
key: 'established',
label: 'Established',
label: 'Установлена',
count: established,
color: 'var(--color-chart-2)',
})
@@ -22,7 +22,7 @@ export function peerSessionBreakdown(peers: PeerRow[]): BreakdownSlice[] {
if (pending > 0) {
slices.push({
key: 'pending',
label: 'Не Established',
label: 'Не установлена',
count: pending,
color: 'var(--color-warning)',
})
@@ -35,7 +35,7 @@ export function readinessBreakdown(
const slices: BreakdownSlice[] = [
{
key: 'health',
label: 'Health OK',
label: 'API доступен',
count: 1,
color: 'var(--color-chart-2)',
},
@@ -44,7 +44,7 @@ export function readinessBreakdown(
if (okCount > 0) {
slices.push({
key: 'checks-ok',
label: 'Checks OK',
label: 'Проверки в норме',
count: okCount,
color: 'var(--color-chart-1)',
})
@@ -52,7 +52,7 @@ export function readinessBreakdown(
if (failCount > 0) {
slices.push({
key: 'checks-fail',
label: 'Checks fail',
label: 'Ошибки проверок',
count: failCount,
color: 'var(--color-warning)',
})
@@ -61,7 +61,7 @@ export function readinessBreakdown(
if (slices.length === 1 && okCount === 0 && failCount === 0) {
slices.push({
key: 'ready',
label: ready?.status === 'ok' ? 'Ready' : 'Ready pending',
label: ready?.status === 'ok' ? 'Готов' : 'Ожидает готовности',
count: 1,
color: 'var(--color-chart-4)',
})
@@ -1,17 +1,11 @@
import type { JobRow, PeerRow, RevisionRow, SpeakerRow } from '@/types/api'
import { jobKindRu, jobStatusRu } from '@/lib/ui-labels'
import type { PlatformActivityItem } from './types'
const JOB_KIND_RU: Record<string, string> = {
module_refresh: 'Обновление модуля',
apply: 'Применение конфигурации',
rollback: 'Откат ревизии',
bird_reload: 'Перезагрузка BIRD',
}
function jobMessage(job: JobRow): string {
const kind = JOB_KIND_RU[job.kind] ?? job.kind
return `${kind} · ${job.status}`
return `${jobKindRu(job.kind)} · ${jobStatusRu(job.status)}`
}
export function recentPlatformActivity(
@@ -45,7 +39,7 @@ export function recentPlatformActivity(
for (const peer of peers.filter((p) => p.session_mismatch).slice(0, 2)) {
items.push({
id: `peer-${peer.id}`,
message: `Mismatch сессии: ${peer.name ?? peer.neighbor}`,
message: `Расхождение сессии: ${peer.name ?? peer.neighbor}`,
status: 'mismatch',
kind: 'network',
})
+94
View File
@@ -27,3 +27,97 @@ export function moduleTypeRu(type: string): string {
return type
}
}
const JOB_KIND_RU: Record<string, string> = {
module_refresh: 'Обновление модуля',
apply: 'Применение конфигурации',
rollback: 'Откат ревизии',
bird_reload: 'Перезагрузка BIRD',
}
export function jobKindRu(kind: string): string {
return JOB_KIND_RU[kind] ?? kind
}
const JOB_STATUS_RU: Record<string, string> = {
queued: 'В очереди',
running: 'Выполняется',
succeeded: 'Успешно',
failed: 'Ошибка',
error: 'Ошибка',
cancelled: 'Отменена',
canceled: 'Отменена',
pending: 'Ожидает',
approved: 'Одобрен',
revoked: 'Отозван',
active: 'Активен',
ok: 'В норме',
mismatch: 'Расхождение',
established: 'Установлена',
healthy: 'В норме',
warning: 'Предупреждение',
stale: 'Устарело',
overdue: 'Просрочено',
paused: 'Приостановлен',
disabled: 'Выключен',
archived: 'В архиве',
block: 'block',
accept: 'accept',
}
export function jobStatusRu(status: string): string {
return JOB_STATUS_RU[status.toLowerCase()] ?? status
}
export function bgpSessionStateRu(state: string | null | undefined): string {
if (!state) return '—'
if (state === 'Established') return 'Установлена'
return state
}
export function speakerOnlineLabel(agentOk: boolean | undefined): string {
if (agentOk === true) return 'В сети'
if (agentOk === false) return 'Не в сети'
return '—'
}
export function firewallClientStatusRu(status: string): string {
switch (status) {
case 'pending':
return 'Ожидает'
case 'approved':
return 'Одобрен'
case 'revoked':
return 'Отозван'
default:
return status
}
}
export function apiKeyRoleRu(role: string): string {
switch (role) {
case 'viewer':
return 'Наблюдатель'
case 'editor':
return 'Редактор'
case 'operator':
return 'Оператор'
case 'node':
return 'Нода'
default:
return role
}
}
export function readyCheckRu(key: string): string {
switch (key) {
case 'postgres':
return 'PostgreSQL'
case 'store':
return 'Хранилище'
case 'jobs':
return 'Очередь задач'
default:
return key
}
}
+15 -21
View File
@@ -4,7 +4,7 @@ import { KeyRound, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-react'
import { useMemo } from 'react'
import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { PanelCard } from '@/components/panel-card'
import { AccessApiKeysCard } from '@/components/access/access-api-keys-card'
import { PageHeader } from '@/components/page-header'
@@ -79,12 +79,11 @@ function AccessComponent() {
/>
{session ? (
<Card>
<CardHeader className="border-b py-3">
<CardTitle className="text-base">Текущая сессия</CardTitle>
<CardDescription>Tenant и роль ключа, с которым открыта панель.</CardDescription>
</CardHeader>
<CardContent className="grid gap-3 p-4 text-sm sm:grid-cols-2">
<PanelCard
title="Текущая сессия"
description="Tenant и роль ключа, с которым открыта панель."
contentClassName="grid gap-3 py-4 text-sm sm:grid-cols-2"
>
<div>
<p className="text-muted-foreground">Tenant</p>
<p className="break-all font-mono text-xs">{session.tenant_id}</p>
@@ -93,11 +92,9 @@ function AccessComponent() {
<p className="text-muted-foreground">Роль</p>
<p className="font-mono">{session.role}</p>
</div>
</CardContent>
</Card>
</PanelCard>
) : (
<Card>
<CardContent className="py-6 text-sm text-muted-foreground">
<PanelCard contentClassName="py-4 text-sm text-muted-foreground">
Не удалось определить сессию. Укажите токен в{' '}
<Link to="/settings" className="text-primary underline-offset-4 hover:underline">
настройках
@@ -106,8 +103,7 @@ function AccessComponent() {
{sessionQuery.isError && sessionQuery.error instanceof Error ? (
<span className="mt-2 block text-destructive">{sessionQuery.error.message}</span>
) : null}
</CardContent>
</Card>
</PanelCard>
)}
{isOperator ? (
@@ -126,14 +122,12 @@ function AccessComponent() {
/>
</>
) : session ? (
<Card>
<CardContent className="py-6 text-sm text-muted-foreground">
Управление API-ключами доступно только роли <strong>operator</strong>. Текущая роль:{' '}
<span className="font-mono">{session.role}</span>. Для выдачи ключей войдите с
operator-ключом или создайте ключ через API / переменную{' '}
<code className="text-xs">EVOBGP_API_KEYS</code>.
</CardContent>
</Card>
<PanelCard contentClassName="py-4 text-sm text-muted-foreground">
Управление API-ключами доступно только роли <strong>operator</strong>. Текущая роль:{' '}
<span className="font-mono">{session.role}</span>. Для выдачи ключей войдите с
operator-ключом или создайте ключ через API / переменную{' '}
<code className="text-xs">EVOBGP_API_KEYS</code>.
</PanelCard>
) : null}
</div>
)
+7 -13
View File
@@ -4,12 +4,7 @@ import { RefreshCw } from 'lucide-react'
import { useState } from 'react'
import { Button } from '@evobgp/ui/components/button'
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from '@evobgp/ui/components/card'
import { PanelCard } from '@/components/panel-card'
import { Skeleton } from '@evobgp/ui/components/skeleton'
import {
@@ -133,13 +128,12 @@ function DashboardComponent() {
</DataGridCard>
</div>
<Card className="gap-0">
<CardHeader className="border-b py-3">
<CardTitle className="text-base">Быстрые действия</CardTitle>
<CardDescription>Частые переходы к настройке и деплою</CardDescription>
</CardHeader>
<DashboardQuickActions />
</Card>
<PanelCard
title="Быстрые действия"
description="Частые переходы к настройке и деплою"
footer={<DashboardQuickActions />}
footerClassName="gap-2 px-5 py-4"
/>
</div>
)
}
+19 -19
View File
@@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { PanelCard } from '@/components/panel-card'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
@@ -92,7 +92,7 @@ function FirewallPage() {
toast.error(
installCtx?.bundle_seed_configured === false
? 'На CP не задан EVOBGP_BUNDLE_SEED_HEX'
: 'Bundle seed недоступен (нужна роль operator)',
: 'Seed бандла недоступен (нужна роль оператора)',
)
return
}
@@ -107,7 +107,7 @@ function FirewallPage() {
return (
<div className="flex flex-col gap-6">
<PageHeader
title="Firewall blocklist"
title="Файрвол: blocklist"
description="Linux-серверы: синхронизация CIDR по policy block/accept"
actions={
<Button
@@ -125,15 +125,16 @@ function FirewallPage() {
}
/>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="size-5" />
<PanelCard
title={
<span className="flex items-center gap-2">
<Shield className="size-4" />
Установка на сервер
</CardTitle>
<CardDescription>One-liner для root на целевом Linux (bash, curl). После enroll approve в «Запросы».</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
</span>
}
description="Команда для root на целевом Linux (bash, curl). После регистрации — одобрите клиента во вкладке «Запросы»."
contentClassName="flex flex-col gap-4 py-4"
>
<div className="grid gap-4 sm:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="fw-name">Имя сервера</Label>
@@ -144,7 +145,7 @@ function FirewallPage() {
<Input id="fw-url" value={cpUrl} onChange={(e) => setCpUrl(e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="fw-seed">Bundle seed</Label>
<Label htmlFor="fw-seed">Seed бандла</Label>
<Input
id="fw-seed"
type="password"
@@ -155,10 +156,10 @@ function FirewallPage() {
/>
<p className="text-muted-foreground text-xs">
{installCtxQ.isLoading
? 'Загрузка из control plane…'
? 'Загрузка с плоскости управления…'
: installCtx?.bundle_seed_configured
? 'Из переменной EVOBGP_BUNDLE_SEED_HEX на CP (docker compose / .env)'
: 'На CP не задан EVOBGP_BUNDLE_SEED_HEX — enroll невозможен'}
: 'На CP не задан EVOBGP_BUNDLE_SEED_HEX — регистрация невозможна'}
</p>
</div>
</div>
@@ -167,8 +168,7 @@ function FirewallPage() {
<Copy />
Копировать команду
</Button>
</CardContent>
</Card>
</PanelCard>
<BadgeTabs
defaultValue="clients"
@@ -184,7 +184,7 @@ function FirewallPage() {
]}
>
<TabsContent value="clients" className="mt-0">
<DataGridCard title="Клиенты" description="Активные Linux-серверы с синхронизацией blocklist">
<DataGridCard title="Клиенты" description="Активные Linux-серверы с синхронизацией списка блокировок">
<QueryState
data={clientsQ.data}
isLoading={clientsQ.isLoading}
@@ -246,7 +246,7 @@ function FirewallPage() {
<TabsContent value="requests" className="mt-0">
<DataGridCard
title="Запросы"
description="Pending enroll — одобрите или отклоните новые клиенты"
description="Запросы на регистрацию — одобрите или отклоните новые клиенты"
>
<QueryState
data={clientsQ.data}
@@ -264,7 +264,7 @@ function FirewallPage() {
onReject={(id) => deleteClient.mutate(id)}
approvePending={approve.isPending}
rejectPending={deleteClient.isPending}
emptyTitle="Нет pending-запросов"
emptyTitle="Нет ожидающих запросов"
/>
)}
</QueryState>
+50 -47
View File
@@ -3,11 +3,12 @@ import { useQuery } from '@tanstack/react-query'
import { Activity, AlertTriangle, Bird, RefreshCw } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { PanelCard } from '@/components/panel-card'
import { Separator } from '@evobgp/ui/components/separator'
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
import { DataGridCard } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { jobKindRu } from '@/lib/ui-labels'
import {
DashboardOperationsFlowCard,
MonitoringHealthCard,
@@ -139,15 +140,16 @@ function MonitoringComponent() {
</QueryState>
</DataGridCard>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<PanelCard
title={
<span className="flex items-center gap-2">
<Bird className="size-4" />
BGP на API-хосте
</CardTitle>
<CardDescription>GET /v1/bird/status</CardDescription>
</CardHeader>
<CardContent>
</span>
}
description="GET /v1/bird/status"
contentClassName="py-4"
>
<QueryState
data={birdQ.data}
isLoading={birdQ.isLoading}
@@ -158,20 +160,20 @@ function MonitoringComponent() {
>
{(bird) => <BirdSummary bird={bird} />}
</QueryState>
</CardContent>
</Card>
</PanelCard>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<PanelCard
title={
<span className="flex items-center gap-2">
<Activity className="size-4" />
Задачи
</CardTitle>
<CardDescription>Последние 100 задач · GET /v1/jobs</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
</span>
}
description="Последние 100 задач · GET /v1/jobs"
contentClassName="space-y-4 py-4"
>
<div className="flex flex-wrap gap-4 text-sm">
<Metric label="Активных" value={jobs.filter((j) => j.status === 'running' || j.status === 'queued').length} />
<Metric
@@ -189,7 +191,7 @@ function MonitoringComponent() {
{failedJobs.map((job) => (
<li key={job.job_id} className="rounded-lg border px-3 py-2 text-sm">
<div className="flex items-start justify-between gap-2">
<p className="font-medium">{job.kind}</p>
<p className="font-medium">{jobKindRu(job.kind)}</p>
<StatusBadge status={job.status} />
</div>
{job.error ? (
@@ -207,18 +209,18 @@ function MonitoringComponent() {
Критичных сбоев в последних 100 задачах нет.
</p>
)}
</CardContent>
</Card>
</PanelCard>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<PanelCard
title={
<span className="flex items-center gap-2">
<AlertTriangle className="size-4 text-muted-foreground" />
Что проверять при деградации
</CardTitle>
<CardDescription>Короткая шпаргалка для triage</CardDescription>
</CardHeader>
<CardContent>
</span>
}
description="Краткая шпаргалка для первичной диагностики"
contentClassName="py-4"
>
<ul className="space-y-3 text-sm text-muted-foreground">
<li>
<span className="font-medium text-foreground">API недоступен.</span> Если{' '}
@@ -226,9 +228,9 @@ function MonitoringComponent() {
его логи.
</li>
<li>
<span className="font-medium text-foreground">Readiness не «Готов».</span> Сначала{' '}
<span className="font-medium text-foreground">Готовность не «Готов».</span> Сначала{' '}
<code className="text-xs">postgres</code>, затем <code className="text-xs">store</code>{' '}
и <code className="text-xs">jobs</code> в checks.
и <code className="text-xs">jobs</code> в проверках.
</li>
<li>
<span className="font-medium text-foreground">Низкий ratio BGP.</span> Проверьте{' '}
@@ -236,36 +238,37 @@ function MonitoringComponent() {
</li>
<li>
<span className="font-medium text-foreground">Ошибки задач.</span> Откройте Операции и
проверьте последние неуспешные jobs.
проверьте последние неуспешные задачи.
</li>
</ul>
</CardContent>
</Card>
</PanelCard>
</div>
</TabsContent>
<TabsContent value="postgres" className="mt-0">
<Card>
<CardHeader>
<CardTitle className="text-base">PostgreSQL</CardTitle>
<CardDescription>
<PanelCard
title="PostgreSQL"
description={
<>
Статус соединения и пул. PostgreSQL отображается в readiness-проверке на вкладке «Система»
(check <code className="text-xs">postgres</code>).
</CardDescription>
</CardHeader>
</Card>
</>
}
contentClassName="py-4"
/>
</TabsContent>
<TabsContent value="runtime-logs" className="mt-0">
<Card>
<CardHeader>
<CardTitle className="text-base">Файловые логи</CardTitle>
<CardDescription>
<PanelCard
title="Файловые логи"
description={
<>
Логи API и pipeline настраиваются переменной <code className="text-xs">EVOBGP_LOG_*</code> и
управляются в tenant-settings.
</CardDescription>
</CardHeader>
</Card>
</>
}
contentClassName="py-4"
/>
</TabsContent>
</BadgeTabs>
</div>
@@ -301,7 +304,7 @@ function BirdSummary({ bird }: { bird: import('@/types/api').BirdStatus }) {
return (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Established / total</span>
<span className="text-muted-foreground">Установлено / всего</span>
<span className="font-medium tabular-nums">
{bird.bgp_established} / {bird.bgp_sessions_total}
{ratio !== null ? <span className="text-muted-foreground"> ({ratio}%)</span> : null}
+16 -19
View File
@@ -1,7 +1,7 @@
import { createFileRoute, useSearch } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { PanelCard } from '@/components/panel-card'
import { RefreshCw } from 'lucide-react'
import {
@@ -72,7 +72,7 @@ function NetworkComponent() {
{ value: 'overview', label: 'Обзор' },
{ value: 'peers', label: 'Пиры', count: peers.length },
{ value: 'speakers', label: 'Спикеры', count: speakers.length, badgeVariant: 'info-light' },
{ value: 'control-plane', label: 'Control plane' },
{ value: 'control-plane', label: 'Плоскость управления' },
]}
>
<TabsContent value="overview" className="mt-0">
@@ -89,12 +89,12 @@ function NetworkComponent() {
loading={overviewLoading}
/>
</div>
<Card className="mt-4">
<CardHeader className="border-b py-3">
<CardTitle className="text-base">BIRD (control plane)</CardTitle>
<CardDescription>Статус birdc на хосте API</CardDescription>
</CardHeader>
<CardContent className="p-4">
<PanelCard
className="mt-4"
title="BIRD (control plane)"
description="Статус birdc на хосте API"
contentClassName="py-4"
>
<QueryState
data={birdQ.data}
isLoading={birdQ.isLoading}
@@ -105,8 +105,7 @@ function NetworkComponent() {
>
{(bird) => <BirdSummary bird={bird} />}
</QueryState>
</CardContent>
</Card>
</PanelCard>
</TabsContent>
<TabsContent value="peers" className="mt-0">
@@ -131,15 +130,13 @@ function NetworkComponent() {
</TabsContent>
<TabsContent value="control-plane" className="mt-0">
<Card>
<CardHeader className="border-b py-3">
<CardTitle className="text-base">Настройки Control Plane (BIRD)</CardTitle>
<CardDescription>Конфигурация tenant-level в разделе «Настройки BIRD»</CardDescription>
</CardHeader>
<CardContent className="p-4 text-sm text-muted-foreground">
См. раздел «Настройки BIRD».
</CardContent>
</Card>
<PanelCard
title="Настройки Control Plane (BIRD)"
description="Конфигурация tenant-level — в разделе «Настройки BIRD»"
contentClassName="py-4 text-sm text-muted-foreground"
>
См. раздел «Настройки BIRD».
</PanelCard>
</TabsContent>
</BadgeTabs>
</div>
+7 -12
View File
@@ -5,7 +5,7 @@ import { toast } from 'sonner'
import { useState, useMemo } from 'react'
import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { PanelCard } from '@/components/panel-card'
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
import { DataGridCard } from '@/components/data-grid-shell'
import { OperationsAnalyticsCard } from '@/components/analytics'
@@ -97,22 +97,22 @@ function OperationsComponent() {
<ConfirmDialog
trigger={
<Button variant="default" size="sm" disabled={applyMutation.isPending}>
Apply
Применить
</Button>
}
title="Применить конфигурацию на всех спикерах?"
description="Текущая конфигурация будет применена на всех BIRD-агентах. Требуется роль operator."
description="Текущая конфигурация будет применена на всех BIRD-агентах. Требуется роль оператора."
confirmLabel="Применить"
onConfirm={() => applyMutation.mutate()}
/>
<ConfirmDialog
trigger={
<Button variant="outline" size="sm" disabled={birdReloadMutation.isPending}>
BIRD reload
Перезагрузка BIRD
</Button>
}
title="Перезагрузить BIRD?"
description="BIRD перезагрузит конфигурацию. Требуется роль operator."
description="BIRD перезагрузит конфигурацию. Требуется роль оператора."
confirmLabel="Перезагрузить"
onConfirm={() => birdReloadMutation.mutate()}
/>
@@ -202,11 +202,7 @@ function DiffTab({ revisions }: { revisions: import('@/types/api').RevisionRow[]
)
return (
<Card>
<CardHeader className="border-b py-3">
<CardTitle className="text-base">Сравнение ревизий</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4 p-4">
<PanelCard title="Сравнение ревизий" contentClassName="flex flex-col gap-4 py-4">
<div className="flex flex-wrap items-end gap-3">
<div className="flex w-full max-w-xs flex-col gap-1">
<span className="text-xs text-muted-foreground">Ревизия A</span>
@@ -242,8 +238,7 @@ function DiffTab({ revisions }: { revisions: import('@/types/api').RevisionRow[]
>
{(diff) => <DiffView diff={diff} />}
</QueryState>
</CardContent>
</Card>
</PanelCard>
)
}
+2 -2
View File
@@ -39,7 +39,7 @@ function ScheduleComponent() {
const items: SectionCardItem[] = [
{ label: 'Всего задач', value: jobs.length, icon: <ListTodo className="size-4" />, hint: 'в выборке' },
{ label: 'В работе', value: running, icon: <Clock className="size-4" />, hint: 'queued и running' },
{ label: 'В работе', value: running, icon: <Clock className="size-4" />, hint: 'в очереди и выполняются' },
{
label: 'С ошибкой',
value: failed,
@@ -90,7 +90,7 @@ function ScheduleComponent() {
<DataGridCard
title="Модули"
description="Расписание обновления и ручной запуск ingest"
description="Расписание обновления и ручной запуск обновления"
>
<QueryState
data={modules}
+13 -22
View File
@@ -2,7 +2,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { PanelCard } from '@/components/panel-card'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
@@ -69,15 +69,11 @@ function SettingsComponent() {
description="Параметры интерфейса и подключения браузера к API."
/>
<Card>
<CardHeader>
<CardTitle>Подключение к API</CardTitle>
<CardDescription>
Токен хранится только в этом браузере (localStorage). Управление ключами tenant в
разделе «Права доступа».
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<PanelCard
title="Подключение к API"
description="Токен хранится только в этом браузере (localStorage). Управление ключами tenant — в разделе «Права доступа»."
contentClassName="flex flex-col gap-4 py-4"
>
<div className="flex flex-col gap-2">
<Label htmlFor="token">Токен для запросов</Label>
<Input
@@ -113,17 +109,13 @@ function SettingsComponent() {
<code className="text-xs">EVOBGP_SEED_DEMO</code> 0) и запущенный API.
</p>
) : null}
</CardContent>
</Card>
</PanelCard>
<Card>
<CardHeader>
<CardTitle>Оформление</CardTitle>
<CardDescription>
Тема интерфейса. Быстрый переключатель также доступен в боковой панели.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2">
<PanelCard
title="Оформление"
description="Тема интерфейса. Быстрый переключатель также доступен в боковой панели."
contentClassName="flex flex-col gap-2 py-4"
>
<SelectField
id="theme-select"
label="Тема"
@@ -133,8 +125,7 @@ function SettingsComponent() {
triggerClassName="max-w-xs"
onValueChange={(v) => v && setTheme(v)}
/>
</CardContent>
</Card>
</PanelCard>
</div>
)
}
+27 -31
View File
@@ -4,7 +4,7 @@ import { Save } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { PanelCard } from '@/components/panel-card'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
@@ -42,8 +42,8 @@ const RUNTIME_LOGS_ENABLED_ITEMS = [
] as const
const RUNTIME_LOGS_MODE_ITEMS = [
{ value: 'truncate', label: 'truncate — обнулить' },
{ value: 'delete', label: 'delete — удалить файл' },
{ value: 'truncate', label: 'обнулить (truncate)' },
{ value: 'delete', label: 'удалить файл (delete)' },
] as const
const BIRD_LABELS: Record<BirdSettingKey, string> = {
@@ -99,8 +99,8 @@ function TenantSettingsComponent() {
return (
<div className="flex flex-col gap-6">
<PageHeader
title="Параметры tenant"
description="Параметры control plane для текущего tenant (API /v1/settings)"
title="Параметры арендатора"
description="Параметры плоскости управления для текущего арендатора (API /v1/settings)"
/>
<BadgeTabs
@@ -118,15 +118,16 @@ function TenantSettingsComponent() {
]}
>
<TabsContent value="bird" className="mt-0">
<Card>
<CardHeader>
<CardTitle>BIRD control plane</CardTitle>
<CardDescription>
<PanelCard
title="BIRD control plane"
description={
<>
Глобальные параметры BIRD для pipeline refresh/apply. Сохранение через{' '}
<code className="text-xs">PATCH /v1/settings</code> (роль operator).
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
</>
}
contentClassName="space-y-4 py-4"
>
<QueryState
data={partitioned}
isLoading={settingsQ.isLoading}
@@ -158,17 +159,15 @@ function TenantSettingsComponent() {
</div>
)}
</QueryState>
</CardContent>
</Card>
</PanelCard>
</TabsContent>
<TabsContent value="revision" className="mt-0">
<Card>
<CardHeader>
<CardTitle>Ревизии</CardTitle>
<CardDescription>Время хранения ревизий в БД</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<PanelCard
title="Ревизии"
description="Время хранения ревизий в БД"
contentClassName="space-y-4 py-4"
>
<QueryState
data={partitioned}
isLoading={settingsQ.isLoading}
@@ -205,17 +204,15 @@ function TenantSettingsComponent() {
</div>
)}
</QueryState>
</CardContent>
</Card>
</PanelCard>
</TabsContent>
<TabsContent value="runtime-logs" className="mt-0">
<Card>
<CardHeader>
<CardTitle>Файловые логи</CardTitle>
<CardDescription>Автоматическая очистка логов</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<PanelCard
title="Файловые логи"
description="Автоматическая очистка логов"
contentClassName="space-y-4 py-4"
>
<QueryState
data={partitioned}
isLoading={settingsQ.isLoading}
@@ -296,14 +293,13 @@ function TenantSettingsComponent() {
</div>
)}
</QueryState>
</CardContent>
</Card>
</PanelCard>
</TabsContent>
<TabsContent value="additional" className="mt-0">
<DataGridCard
title="Дополнительные параметры"
description="Параметры вне стандартных групп (readonly — изменяются только через API)"
description="Параметры вне стандартных групп (только чтение — изменяются через API)"
>
<QueryState
data={partitioned?.additional ?? []}
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"reui": {
"type": "remote",
"url": "https://mcp.reui.io/api/mcp",
"enabled": true
}
}
}
+30 -23
View File
@@ -1,33 +1,13 @@
import * as React from "react"
import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
import { cn } from "@evobgp/ui/lib/utils"
function Progress({
className,
children,
value,
...props
}: ProgressPrimitive.Root.Props) {
return (
<ProgressPrimitive.Root
value={value}
data-slot="progress"
className={cn("flex flex-wrap gap-3", className)}
{...props}
>
{children}
<ProgressTrack>
<ProgressIndicator />
</ProgressTrack>
</ProgressPrimitive.Root>
)
}
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
return (
<ProgressPrimitive.Track
className={cn(
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
"relative flex h-1 w-full items-center overflow-hidden rounded-full bg-muted",
className
)}
data-slot="progress-track"
@@ -43,12 +23,39 @@ function ProgressIndicator({
return (
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className={cn("h-full bg-primary transition-all", className)}
className={cn("h-full rounded-full bg-primary transition-all", className)}
{...props}
/>
)
}
function Progress({
className,
children,
value,
...props
}: ProgressPrimitive.Root.Props) {
const hasCustomTrack = React.Children.toArray(children).some(
(child) => React.isValidElement(child) && child.type === ProgressTrack,
)
return (
<ProgressPrimitive.Root
value={value}
data-slot="progress"
className={cn("flex flex-wrap gap-3", className)}
{...props}
>
{children}
{!hasCustomTrack ? (
<ProgressTrack>
<ProgressIndicator />
</ProgressTrack>
) : null}
</ProgressPrimitive.Root>
)
}
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
return (
<ProgressPrimitive.Label