Compare commits

...
4 Commits
Author SHA1 Message Date
Denozordec 39f1295438 refactor: update configuration and enhance skeleton components for improved UI consistency
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m5s
CI / go (push) Successful in 1m5s
CI / bird2 (push) Successful in 19s
CI / release (push) Successful in 4m22s
Modified .npmrc to set a new store directory. Updated eslint configuration to ignore additional paths. Adjusted tsconfig to exclude specific components and refined the SectionCardsSkeleton and AnalyticsDashboardSkeleton for better layout and loading states. Removed the deprecated DashboardQuickActions component to streamline the codebase.
2026-07-09 18:23:53 +07:00
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
Denozordec ac727ad1e3 refactor: update dialog components and enhance UI consistency
CI / changes (push) Successful in 13s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m5s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m20s
Refactored ConfirmDialog and FormDrawer components to improve layout and user experience. Integrated new DrawerActionsFooter for better action handling in dialogs. Updated styles for consistency across components, including adjustments to the skeletons and analytics card layouts. Removed deprecated alert components from various routes to streamline the codebase and enhance clarity in the UI.
2026-07-09 16:54:18 +07:00
169 changed files with 11997 additions and 1009 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"
}
}
}
+1 -1
View File
@@ -1 +1 @@
engine-strict=true
store-dir=C:\Users\shats\AppData\Local\pnpm-test-store\store\v10
+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 -1
View File
@@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import globals from 'globals'
export default tseslint.config(
{ ignores: ['dist', 'src/routeTree.gen.ts'] },
{ ignores: ['dist', 'src/routeTree.gen.ts', 'src/components/blocks/**'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
@@ -1,14 +1,6 @@
import type { ReactNode } from 'react'
import { Info } from 'lucide-react'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@evobgp/ui/components/card'
import { cn } from '@evobgp/ui/lib/utils'
import {
Tooltip,
@@ -16,6 +8,8 @@ import {
TooltipTrigger,
} from '@evobgp/ui/components/tooltip'
import { PanelCard } from '@/components/panel-card'
export function AnalyticsCardShell({
title,
description,
@@ -33,32 +27,36 @@ export function AnalyticsCardShell({
className?: string
children: ReactNode
}) {
const titleNode = (
<span className="flex items-center gap-2">
{title}
{info ? (
<Tooltip>
<TooltipTrigger
className="inline-flex text-muted-foreground transition-colors hover:text-foreground"
aria-label="Подробнее"
>
<Info className="size-3.5" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-xs">
{info}
</TooltipContent>
</Tooltip>
) : null}
</span>
)
return (
<Card className={cn('flex h-full flex-col gap-0 overflow-hidden', className)}>
<CardHeader className="flex flex-row items-start justify-between gap-3 border-b py-4">
<div className="min-w-0 space-y-1">
<CardTitle className="flex items-center gap-2 text-base">
{title}
{info ? (
<Tooltip>
<TooltipTrigger
className="inline-flex text-muted-foreground transition-colors hover:text-foreground"
aria-label="Подробнее"
>
<Info className="size-3.5" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-xs">
{info}
</TooltipContent>
</Tooltip>
) : null}
</CardTitle>
{description ? <CardDescription>{description}</CardDescription> : null}
</div>
{actions ? <div className="shrink-0">{actions}</div> : null}
</CardHeader>
<CardContent className="flex flex-1 flex-col gap-5 p-5">{children}</CardContent>
{footer ? <CardFooter className="gap-2 border-t p-4">{footer}</CardFooter> : null}
</Card>
<PanelCard
title={titleNode}
description={description}
actions={actions}
footer={footer}
className={cn('overflow-hidden', className)}
contentClassName="flex flex-col gap-5 py-5"
footerClassName={footer ? 'gap-2 px-5 py-4' : undefined}
>
{children}
</PanelCard>
)
}
@@ -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',
},
},
@@ -0,0 +1,15 @@
import { CardItem } from "./card-item"
import { CARDS } from "./data"
export function CardGrid() {
return (
<div className="@container w-full">
{/* Grid */}
<div className="grid gap-5 @2xl:grid-cols-3">
{CARDS.map((card) => (
<CardItem key={card.label} card={card} />
))}
</div>
</div>
)
}
@@ -0,0 +1,34 @@
import {
Frame,
FrameHeader,
FramePanel,
} from "@/components/reui/frame"
import { ICard } from "./data"
import { LinkIcon } from "lucide-react"
export function CardItem({ card }: { card: ICard }) {
return (
<Frame spacing="sm">
{/* Header */}
<FrameHeader className="px-1! py-1!">
<div className="[&_svg]:text-muted-foreground flex items-center gap-2 [&_svg]:size-4">
{card.icon}
<span className="text-foreground text-sm font-medium">
{card.label}
</span>
</div>
</FrameHeader>
{/* Content */}
<FramePanel className="space-y-3.5">
<p className="text-xs leading-relaxed">{card.description}</p>
<a
href="#"
className="text-primary inline-flex items-center gap-1 text-xs font-medium underline-offset-2 hover:underline"
>
<LinkIcon aria-hidden="true" className="size-2.5 shrink-0" />
{card.link}
</a>
</FramePanel>
</Frame>
)
}
@@ -0,0 +1,39 @@
import { type ReactNode } from "react"
import { PackageIcon, TrendingUp, MapPinIcon } from "lucide-react"
export interface ICard {
label: string
icon: ReactNode
description: string
link: string
}
export const CARDS: ICard[] = [
{
label: "Binance",
icon: (
<PackageIcon aria-hidden="true" />
),
description:
"Track trading volumes, liquidity shifts, and price movements for informed decisions",
link: "https://www.binance.com/en/markets/over..",
},
{
label: "Revenue",
icon: (
<TrendingUp aria-hidden="true" />
),
description:
"Get instant insights into earnings and cash flow performance.",
link: "https://nexo.io/earn/crypto-detailed-portfol..",
},
{
label: "Shipments",
icon: (
<MapPinIcon aria-hidden="true" />
),
description:
"Stay on top of deliveries and track shipment statuses efficiently.",
link: "https://www.educare.io/platform/analytics/e..",
},
]
@@ -0,0 +1,9 @@
import { CardGrid } from "./components/card-grid"
export function Page() {
return (
<div className="flex min-h-svh w-full max-w-5xl items-center justify-center p-6">
<CardGrid />
</div>
)
}
@@ -0,0 +1,416 @@
import { Frame, FramePanel } from "@/components/reui/frame"
import { Cell, Pie, PieChart } from "recharts"
import { cn } from "@evobgp/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@evobgp/ui/components/avatar"
import { Button } from "@evobgp/ui/components/button"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@evobgp/ui/components/chart"
import { Separator } from "@evobgp/ui/components/separator"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@evobgp/ui/components/tabs"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@evobgp/ui/components/tooltip"
import {
allocationMemberCount,
allocationMembers,
allocationPeriods,
inflowChartConfig,
inflowPeriods,
SEGMENT_COUNT,
type AllocationPeriod,
type InflowFund,
type InflowPeriod,
} from "./data"
import { InfoIcon } from "lucide-react"
const segments = Array.from({ length: SEGMENT_COUNT }, (_, index) => index)
const CHART_REVEAL_STYLE = `
@keyframes dashboard-1-flow-reveal-up {
from {
clip-path: inset(100% 0 0 0);
opacity: 0.75;
}
to {
clip-path: inset(0 0 0 0);
opacity: 1;
}
}
.dashboard-1-flow-reveal-up {
animation: dashboard-1-flow-reveal-up 680ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
.dashboard-1-flow-reveal-up {
animation: none;
clip-path: none;
opacity: 1;
}
}
`
function AllocationMeter({ period }: { period: AllocationPeriod }) {
return (
<div
aria-label={`${period.label} capacity allocation is ${period.allocation}`}
className="flex h-7 w-full items-stretch justify-between"
role="img"
>
{segments.map((segment) => (
<span
aria-hidden="true"
key={segment}
className={cn(
"h-full w-1 shrink-0 rounded-full",
segment < period.filledSegments ? "bg-success" : "bg-muted"
)}
/>
))}
</div>
)
}
function MemberStack() {
return (
<div className="flex items-center gap-2">
<AvatarGroup className="-space-x-2">
{allocationMembers.map((member) => (
<Avatar key={member.name} className="size-6">
{member.avatar ? (
<AvatarImage src={member.avatar} alt={member.name} />
) : null}
<AvatarFallback className="bg-background text-xs font-medium">
{member.initials}
</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
<span className="text-muted-foreground text-xs whitespace-nowrap">
{allocationMemberCount} Members
</span>
</div>
)
}
function AllocationChart() {
return (
<Frame className="@container h-full w-full">
<FramePanel>
<Tabs
defaultValue={allocationPeriods[0].value}
className="h-full w-full min-w-0 gap-4"
>
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-1.5">
<h2 className="text-sm font-medium">Capacity Allocation</h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className="text-muted-foreground/70 hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex shrink-0 rounded-full p-0.5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
aria-label="Capacity Allocation info"
/>
}
>
<InfoIcon className="size-3.5" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent
side="top"
className="max-w-56 px-2.5 py-1.5 text-xs leading-5"
>
Fulfillment capacity by selected period.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<TabsList>
{allocationPeriods.map((period) => (
<TabsTrigger key={period.value} value={period.value}>
{period.label}
</TabsTrigger>
))}
</TabsList>
</div>
{allocationPeriods.map((period) => (
<TabsContent
key={period.value}
value={period.value}
className="mt-0"
>
<div className="flex flex-col gap-4">
{/* Metric */}
<div className="flex flex-wrap items-baseline gap-x-2">
<span className="text-[26px] font-medium">
{period.allocation}
</span>
<span className="text-success text-xs font-medium">
{period.delta}
</span>
<span className="text-muted-foreground/70 text-xs">
{period.comparison}
</span>
</div>
{/* Chart */}
<AllocationMeter period={period} />
{/* Footer */}
<div className="mt-1 flex flex-wrap items-center justify-between gap-3">
<p>
<span className="text-muted-foreground/70 text-xs">
Queued Orders:
</span>{" "}
<span className="text-sm font-medium">
{period.exposure}
</span>
</p>
<MemberStack />
</div>
</div>
</TabsContent>
))}
</Tabs>
</FramePanel>
</Frame>
)
}
type DonutSlice =
| InflowFund
| {
key: "reserve"
name: string
amount: string
share: number
color: string
fill: string
}
function getDonutData(period: InflowPeriod) {
const trackedShare = period.funds.reduce(
(total, fund) => total + fund.share,
0
)
const reserveShare = Math.max(100 - trackedShare, 0)
return [
...period.funds,
{
key: "reserve",
name: "Reserve Capacity",
amount: "",
share: reserveShare,
color: "var(--muted)",
fill: "var(--color-reserve)",
},
] satisfies DonutSlice[]
}
function ChartTooltipFormatter(item: unknown) {
const fund = item as DonutSlice
const value = fund.key === "reserve" ? `${fund.share}%` : fund.amount
return (
<div className="flex min-w-40 items-center justify-between gap-6">
<div className="flex min-w-0 items-center gap-2">
<span
aria-hidden="true"
className="size-2.5 shrink-0 rounded-full"
style={{ backgroundColor: fund.color }}
/>
<span className="text-muted-foreground truncate">{fund.name}</span>
</div>
<span className="text-foreground font-medium tabular-nums">{value}</span>
</div>
)
}
function InfoTooltip() {
return (
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="About Decision Flow"
className="text-muted-foreground/70 -my-1"
size="icon-sm"
type="button"
variant="ghost"
>
<InfoIcon aria-hidden="true" className="text-sm" data-icon="inline-start" />
</Button>
}
/>
<TooltipContent side="top" sideOffset={8}>
<p>Tracked decisions entering fulfillment lanes.</p>
</TooltipContent>
</Tooltip>
)
}
function InflowDonut({ period }: { period: InflowPeriod }) {
const chartData = getDonutData(period)
return (
<div className="dashboard-1-flow-reveal-up relative size-[8.25rem] shrink-0">
<ChartContainer
aria-label={`Decision Flow: ${period.total} total for ${period.label}`}
className="aspect-square size-[8.25rem]"
config={inflowChartConfig}
initialDimension={{ width: 132, height: 132 }}
>
<PieChart margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<ChartTooltip
cursor={false}
wrapperStyle={{ zIndex: 30 }}
content={
<ChartTooltipContent
hideLabel
hideIndicator
formatter={(_value, _name, item) =>
ChartTooltipFormatter(item.payload)
}
/>
}
/>
<Pie
data={chartData}
dataKey="share"
endAngle={-230}
innerRadius={47}
isAnimationActive={false}
nameKey="name"
outerRadius={62}
paddingAngle={1}
cornerRadius={3}
startAngle={130}
stroke="var(--background)"
strokeWidth={2}
>
{chartData.map((item) => (
<Cell key={item.key} fill={item.fill} />
))}
</Pie>
</PieChart>
</ChartContainer>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 flex items-center justify-center"
>
<div className="bg-background/90 border-border/70 flex size-[5.25rem] flex-col items-center justify-center rounded-full border border-dashed">
<span className="text-muted-foreground/70 text-xs">Flow</span>
<span className="mt-0.5 text-sm font-semibold">{period.total}</span>
</div>
</div>
</div>
)
}
function InflowLegend({ period }: { period: InflowPeriod }) {
return (
<ul className="flex min-w-0 flex-1 flex-col">
{period.funds.map((fund, index) => (
<li key={fund.key}>
<div className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-3 py-2.5">
<div className="flex min-w-0 items-center gap-2.5">
<span
aria-hidden="true"
className="border-background size-3 shrink-0 rounded-full border-2 shadow-sm"
style={{ backgroundColor: fund.color }}
/>
<span className="text-sm font-medium">{fund.name}</span>
</div>
<span className="text-sm font-medium">{fund.amount}</span>
<span className="text-muted-foreground/70 w-8 text-right text-xs">
{fund.share}%
</span>
</div>
{index < period.funds.length - 1 ? (
<Separator className="w-auto" />
) : null}
</li>
))}
</ul>
)
}
function InflowPeriodPanel({ period }: { period: InflowPeriod }) {
return (
<div className="grid gap-6 @sm:grid-cols-[8.25rem_minmax(0,1fr)] @sm:items-center">
<InflowDonut period={period} />
<InflowLegend period={period} />
</div>
)
}
function InflowChart() {
return (
<TooltipProvider delay={150}>
<style>{CHART_REVEAL_STYLE}</style>
<Frame className="@container h-full w-full">
<FramePanel className="ps-3.5! pe-5! pt-5! pb-3.5!">
<Tabs defaultValue="week" className="gap-6">
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-0.5 ps-1.5">
<h2 className="text-sm font-medium">Decision Flow</h2>
<InfoTooltip />
</div>
<TabsList className="w-full @sm:w-auto">
{inflowPeriods.map((period) => (
<TabsTrigger key={period.value} value={period.value}>
{period.label}
</TabsTrigger>
))}
</TabsList>
</div>
{/* Content */}
{inflowPeriods.map((period) => (
<TabsContent
key={period.value}
value={period.value}
className="mt-0"
>
<InflowPeriodPanel period={period} />
</TabsContent>
))}
</Tabs>
</FramePanel>
</Frame>
</TooltipProvider>
)
}
export function Chart() {
return (
<div className="@container grid h-full w-full min-w-0 auto-rows-fr gap-3">
<AllocationChart />
<InflowChart />
</div>
)
}
@@ -0,0 +1,64 @@
import { Frame, FramePanel } from "@/components/reui/frame"
import { cn } from "@evobgp/ui/lib/utils"
import { Item, ItemMedia } from "@evobgp/ui/components/item"
import { FULFILLMENT_CARDS, type FulfillmentCard } from "./data"
function CardItem({ card }: { card: FulfillmentCard }) {
return (
<FramePanel>
{/* Heading */}
<div className="flex items-center gap-2.5">
<Item
className={cn(
"p-0",
"border-background flex size-10 items-center justify-center border-2 [background-image:radial-gradient(48.05%_48.05%_at_50%_5.95%,rgba(255,255,255,0.4)_0%,rgba(255,255,255,0)_100%)] shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-5 [&_svg]:text-white",
card.iconBg
)}
>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-sm leading-tight">
{card.typeLabel}
</p>
<h3 className="text-sm leading-tight font-medium">{card.title}</h3>
</div>
</div>
<div className="mt-5 space-y-1.5">
<p className="text-muted-foreground text-sm leading-tight">
{card.metricLabel}
</p>
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-xl font-medium tracking-tight">
{card.balance}
</span>
<span
className={cn(
"text-sm font-medium",
card.change.positive ? "text-teal-600" : "text-rose-600"
)}
>
{card.change.percent} ({card.change.amount})
</span>
</div>
</div>
</FramePanel>
)
}
export function Chart() {
return (
<Frame className="@container w-full">
{/* Grid */}
<div className="grid gap-1 @2xl:grid-cols-2 @5xl:grid-cols-4">
{FULFILLMENT_CARDS.map((card) => (
<CardItem key={card.title} card={card} />
))}
</div>
</Frame>
)
}
@@ -0,0 +1,155 @@
import { Badge } from "@/components/reui/badge"
import {
Frame,
FrameFooter,
FramePanel,
} from "@/components/reui/frame"
import { cn } from "@evobgp/ui/lib/utils"
import { Button } from "@evobgp/ui/components/button"
import { Progress } from "@evobgp/ui/components/progress"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@evobgp/ui/components/select"
import { Separator } from "@evobgp/ui/components/separator"
import {
PERFORMANCE_RANGE_OPTIONS,
SHIFT_ACTIVITY,
SHIFT_PERFORMANCE,
SHIFT_PIPELINE_PROGRESS,
} from "./data"
import { TrendingUp, TrendingDown, CircleCheckIcon } from "lucide-react"
export function InvestorCard() {
return (
<Frame className="h-full w-full">
{/* Content */}
<FramePanel>
<div className="mb-6 flex items-start justify-between gap-3">
<div className="flex flex-col gap-px">
<h3 className="text-base font-semibold">Shift Performance</h3>
</div>
<div className="flex items-center gap-2">
<Select defaultValue="today" items={PERFORMANCE_RANGE_OPTIONS}>
<SelectTrigger className="h-8! w-28">
<SelectValue />
</SelectTrigger>
<SelectContent
align="start"
alignItemWithTrigger={false}
className="w-28"
>
{PERFORMANCE_RANGE_OPTIONS.map((range) => (
<SelectItem key={range.value} value={range.value}>
{range.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-5">
<div className="grid grid-cols-3 gap-2">
{SHIFT_PERFORMANCE.map((item) => (
<div
className="flex flex-col items-start justify-start"
key={item.label}
>
<div className="text-foreground text-xl font-bold">
{item.value}
</div>
<div className="text-muted-foreground mb-1 text-xs font-medium">
{item.label}
</div>
<span
className={cn(
"flex items-center gap-0.5 text-xs font-semibold [&_svg]:h-3 [&_svg]:w-3",
item.trend === "positive"
? "text-emerald-500"
: "text-destructive"
)}
>
{item.trend === "positive" ? (
<TrendingUp aria-hidden="true" />
) : (
<TrendingDown aria-hidden="true" />
)}
{item.delta}
</span>
</div>
))}
</div>
<Separator />
<div>
<div className="mb-2.5 flex items-center justify-between">
<span className="text-foreground text-sm font-medium">
Pipeline Progress
</span>
<span className="text-foreground text-xs font-semibold">
{SHIFT_PIPELINE_PROGRESS}%
</span>
</div>
<Progress
value={SHIFT_PIPELINE_PROGRESS}
className="h-1! **:data-[slot=progress-track]:h-1"
/>
</div>
<Separator />
<div>
<div className="text-foreground mb-2.5 text-sm font-medium">
Recent Activity
</div>
<ul className="space-y-2">
{SHIFT_ACTIVITY.map((activity) => (
<li
key={activity.id}
className="flex items-center justify-between gap-2.5 text-sm"
>
<span className="flex min-w-0 items-center gap-2">
<CircleCheckIcon className={cn(
"h-3.5 w-3.5 shrink-0",
activity.tone === "success" && "text-emerald-500",
activity.tone === "info" && "text-sky-500",
activity.tone === "warning" && "text-amber-500"
)} aria-hidden="true" />
<span className="text-foreground truncate text-xs">
{activity.title}
</span>
</span>
<Badge
variant={
activity.tone === "success"
? "success-light"
: activity.tone === "info"
? "info-light"
: "warning-light"
}
className="shrink-0"
>
{activity.status}
</Badge>
</li>
))}
</ul>
</div>
</div>
</FramePanel>
{/* Footer */}
<FrameFooter className="flex-row items-center gap-2.5 p-2!">
<Button variant="outline" className="flex-1">
Schedule
</Button>
<Button className="flex-1">Full Report</Button>
</FrameFooter>
</Frame>
)
}
@@ -0,0 +1,33 @@
import { Chart as CapacityChart } from "./capacity-chart"
import { Chart as ChartCards } from "./chart-cards"
import { InvestorCard as CommanderCard } from "./commander-card"
import { ExceptionGrid } from "./exception-grid"
import { Navbar } from "./navbar"
export function Dashboard() {
return (
<div className="text-foreground @container mx-auto flex w-full max-w-7xl flex-col gap-2">
<Navbar />
<section aria-label="Fulfillment metrics">
<ChartCards />
</section>
<section
aria-label="Fulfillment operations"
className="grid min-w-0 items-stretch gap-3 @5xl:grid-cols-2"
>
<div className="flex min-w-0">
<CommanderCard />
</div>
<div className="flex min-w-0">
<CapacityChart />
</div>
</section>
<section aria-label="Fulfillment exception queue">
<ExceptionGrid />
</section>
</div>
)
}
@@ -0,0 +1,676 @@
import { type ReactNode } from "react"
import { type BadgeProps } from "@/components/reui/badge"
import { type ChartConfig } from "@evobgp/ui/components/chart"
import { PackageIcon, TruckIcon, TriangleAlertIcon, BotIcon } from "lucide-react"
export type FulfillmentStatus = "On Time" | "At Risk" | "Delayed" | "Blocked"
export type AutomationLevel = "Autopilot" | "Copilot" | "Manual"
export interface TeamMember {
name: string
initials: string
avatar: string
role: string
}
export interface FulfillmentException {
id: string
reference: string
customer: string
email: string
avatar: string
initials: string
lane: string
facility: string
stage: string
promise: string
slaMinutes: number
automation: AutomationLevel
owner: string
units: number
value: number
risk: string
status: FulfillmentStatus
}
export const STATUS_ORDER: FulfillmentStatus[] = [
"On Time",
"At Risk",
"Delayed",
"Blocked",
]
export const STATUS_BADGE_VARIANT: Record<
FulfillmentStatus,
BadgeProps["variant"]
> = {
"On Time": "success-outline",
"At Risk": "warning-outline",
Delayed: "info-outline",
Blocked: "destructive-outline",
}
export const AUTOMATION_BADGE_VARIANT: Record<
AutomationLevel,
BadgeProps["variant"]
> = {
Autopilot: "success-light",
Copilot: "info-light",
Manual: "warning-light",
}
export const NAV_MEMBERS: TeamMember[] = [
{
name: "Maya Singh",
initials: "MS",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
role: "Fulfillment lead",
},
{
name: "Leo Martins",
initials: "LM",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
role: "Automation owner",
},
{
name: "Nora Albright",
initials: "NA",
avatar:
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=96&h=96&dpr=2&q=80",
role: "Capacity planner",
},
]
export const TEAM_MEMBERS = NAV_MEMBERS.map((member) => ({
src: member.avatar,
initials: member.initials,
name: member.name,
}))
export const TEAM_EXTRA_COUNT = 11
export interface FulfillmentCardChange {
positive: boolean
percent: string
amount: string
}
export interface FulfillmentCard {
typeLabel: string
title: string
metricLabel: string
balance: string
change: FulfillmentCardChange
icon: ReactNode
iconBg: string
}
export const FULFILLMENT_CARDS: FulfillmentCard[] = [
{
typeLabel: "Outbound",
title: "Orders Ready",
metricLabel: "Ready Volume",
balance: "18,420",
change: {
positive: true,
percent: "+11.8%",
amount: "1,946",
},
iconBg: "bg-neutral-950",
icon: (
<PackageIcon aria-hidden="true" />
),
},
{
typeLabel: "Promise",
title: "Same-Day SLA",
metricLabel: "Service Level",
balance: "94.8%",
change: {
positive: true,
percent: "+1.2 pts",
amount: "shift",
},
iconBg: "bg-indigo-600",
icon: (
<TruckIcon aria-hidden="true" />
),
},
{
typeLabel: "Inventory",
title: "Stock Risk",
metricLabel: "Blocked SKUs",
balance: "31",
change: {
positive: true,
percent: "13 fewer",
amount: "since 06:00",
},
iconBg: "bg-amber-400",
icon: (
<TriangleAlertIcon aria-hidden="true" />
),
},
{
typeLabel: "Policy",
title: "AI Autopilot",
metricLabel: "Auto Resolved",
balance: "71.6%",
change: {
positive: true,
percent: "+8.4 pts",
amount: "policy",
},
iconBg: "bg-cyan-600",
icon: (
<BotIcon aria-hidden="true" />
),
},
]
export type AllocationPeriod = {
value: "week" | "month" | "year"
label: string
allocation: string
delta: string
comparison: string
exposure: string
filledSegments: number
}
export type AllocationMember = {
name: string
initials: string
avatar?: string
}
export const SEGMENT_COUNT = 56
export const allocationMemberCount = 6
export const allocationPeriods: AllocationPeriod[] = [
{
value: "week",
label: "Week",
allocation: "86%",
delta: "+5.8%",
comparison: "vs labor plan",
exposure: "3,840 orders",
filledSegments: 48,
},
{
value: "month",
label: "Month",
allocation: "79%",
delta: "+2.4%",
comparison: "vs prior month",
exposure: "18 priority lanes",
filledSegments: 44,
},
{
value: "year",
label: "Year",
allocation: "74%",
delta: "+9.2%",
comparison: "automation lift",
exposure: "6 facilities",
filledSegments: 41,
},
]
export const allocationMembers: AllocationMember[] = TEAM_MEMBERS.map(
(member) => ({
name: member.name,
initials: member.initials,
avatar: member.src,
})
)
export type PerformanceTrend = "positive" | "negative"
export type ActivityTone = "success" | "info" | "warning"
export interface PerformanceMetric {
label: string
value: string
trend: PerformanceTrend
delta: string
}
export interface ShiftActivity {
id: string
title: string
time: string
status: string
tone: ActivityTone
}
export const PERFORMANCE_RANGE_OPTIONS = [
{ label: "Today", value: "today" },
{ label: "Week", value: "week" },
{ label: "Month", value: "month" },
]
export const SHIFT_PERFORMANCE: PerformanceMetric[] = [
{
label: "Orders Cleared",
value: "18.4k",
trend: "positive",
delta: "+11.8%",
},
{
label: "SLA Recovery",
value: "94.8%",
trend: "positive",
delta: "+1.2 pts",
},
{
label: "Risk Exposure",
value: "$128k",
trend: "negative",
delta: "-9.4%",
},
]
export const SHIFT_PIPELINE_PROGRESS = 76
export const SHIFT_ACTIVITY: ShiftActivity[] = [
{
id: "wave-release",
title: "Released priority wave to dock B",
time: "4 min ago",
status: "Cleared",
tone: "success",
},
{
id: "carrier-reprice",
title: "Carrier mix repriced for zone 6",
time: "12 min ago",
status: "Review",
tone: "info",
},
{
id: "inventory-hold",
title: "Inventory hold isolated to 3 SKUs",
time: "23 min ago",
status: "Watch",
tone: "warning",
},
]
export type InflowFundKey = "autopilot" | "copilot" | "manual" | "reserve"
export interface InflowFund {
key: Exclude<InflowFundKey, "reserve">
name: string
amount: string
share: number
color: string
fill: string
}
export interface InflowPeriod {
value: "week" | "month" | "year"
label: string
total: string
headline: string
description: string
delta: string
funds: InflowFund[]
}
const inflowAutopilotColor = "oklch(0.62 0.19 149)"
const inflowCopilotColor = "oklch(0.58 0.18 257)"
const inflowManualColor = "oklch(0.72 0.16 78)"
export const inflowChartConfig = {
flow: {
label: "Flow",
},
autopilot: {
label: "Autopilot",
color: inflowAutopilotColor,
},
copilot: {
label: "Copilot",
color: inflowCopilotColor,
},
manual: {
label: "Manual",
color: inflowManualColor,
},
reserve: {
label: "Reserve",
color: "oklch(0.7 0.04 260)",
},
} satisfies ChartConfig
export const inflowPeriods: InflowPeriod[] = [
{
value: "week",
label: "Week",
total: "18.4k",
headline: "Exception Flow",
description: "Orders entering decision lanes",
delta: "+6.2%",
funds: [
{
key: "autopilot",
name: "Autopilot",
amount: "9.1k",
share: 49.5,
color: inflowAutopilotColor,
fill: "var(--color-autopilot)",
},
{
key: "copilot",
name: "Copilot",
amount: "5.2k",
share: 28.3,
color: inflowCopilotColor,
fill: "var(--color-copilot)",
},
{
key: "manual",
name: "Manual",
amount: "2.8k",
share: 15.2,
color: inflowManualColor,
fill: "var(--color-manual)",
},
],
},
{
value: "month",
label: "Month",
total: "76.8k",
headline: "Resolved Flow",
description: "Completed decisions this month",
delta: "+14.8%",
funds: [
{
key: "autopilot",
name: "Autopilot",
amount: "41.6k",
share: 54.2,
color: inflowAutopilotColor,
fill: "var(--color-autopilot)",
},
{
key: "copilot",
name: "Copilot",
amount: "20.3k",
share: 26.4,
color: inflowCopilotColor,
fill: "var(--color-copilot)",
},
{
key: "manual",
name: "Manual",
amount: "9.8k",
share: 12.8,
color: inflowManualColor,
fill: "var(--color-manual)",
},
],
},
{
value: "year",
label: "Year",
total: "812k",
headline: "Network Flow",
description: "Decisions across six facilities",
delta: "+21.5%",
funds: [
{
key: "autopilot",
name: "Autopilot",
amount: "428k",
share: 52.7,
color: inflowAutopilotColor,
fill: "var(--color-autopilot)",
},
{
key: "copilot",
name: "Copilot",
amount: "224k",
share: 27.6,
color: inflowCopilotColor,
fill: "var(--color-copilot)",
},
{
key: "manual",
name: "Manual",
amount: "103k",
share: 12.7,
color: inflowManualColor,
fill: "var(--color-manual)",
},
],
},
]
export const FULFILLMENT_ROWS: FulfillmentException[] = [
{
id: "row-1001",
reference: "NSC-84721",
customer: "Avery Outdoor",
email: "ops@averyoutdoor.example",
avatar:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=96&h=96&dpr=2&q=80",
initials: "AO",
lane: "Chicago to Austin",
facility: "ORD-2",
stage: "Carrier tender",
promise: "Today 18:00",
slaMinutes: 42,
automation: "Copilot",
owner: "Maya Singh",
units: 480,
value: 38240,
risk: "Carrier capacity is tight after midday cutoff",
status: "At Risk",
},
{
id: "row-1002",
reference: "NSC-84734",
customer: "Field & Frame",
email: "priority@fieldframe.example",
avatar:
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=96&h=96&dpr=2&q=80",
initials: "FF",
lane: "Dallas to Phoenix",
facility: "DFW-1",
stage: "Pick wave",
promise: "Today 16:30",
slaMinutes: 88,
automation: "Autopilot",
owner: "Leo Martins",
units: 310,
value: 21480,
risk: "Wave optimized by carton density",
status: "On Time",
},
{
id: "row-1003",
reference: "NSC-84755",
customer: "MetroFit Labs",
email: "ops@metrofit.example",
avatar:
"https://images.unsplash.com/photo-1519345182560-3f2917c472ef?w=96&h=96&dpr=2&q=80",
initials: "ML",
lane: "Newark to Boston",
facility: "EWR-3",
stage: "Inventory hold",
promise: "Today 15:15",
slaMinutes: -24,
automation: "Manual",
owner: "Nora Albright",
units: 126,
value: 18760,
risk: "Lot trace requires human release",
status: "Blocked",
},
{
id: "row-1004",
reference: "NSC-84763",
customer: "Northline Studio",
email: "returns@northline.example",
avatar:
"https://images.unsplash.com/photo-1517841905240-472988babdf9?w=96&h=96&dpr=2&q=80",
initials: "NS",
lane: "Los Angeles to Seattle",
facility: "LAX-4",
stage: "Packing",
promise: "Today 19:45",
slaMinutes: 114,
automation: "Autopilot",
owner: "Leo Martins",
units: 840,
value: 52210,
risk: "Packing line is running above plan",
status: "On Time",
},
{
id: "row-1005",
reference: "NSC-84801",
customer: "Urban Pantry",
email: "supply@urbanpantry.example",
avatar:
"https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?w=96&h=96&dpr=2&q=80",
initials: "UP",
lane: "Atlanta to Miami",
facility: "ATL-2",
stage: "Cold chain",
promise: "Today 17:00",
slaMinutes: 9,
automation: "Copilot",
owner: "Maya Singh",
units: 212,
value: 30440,
risk: "Reefer handoff needs confirmation",
status: "Delayed",
},
{
id: "row-1006",
reference: "NSC-84819",
customer: "Glow Market",
email: "vip@glowmarket.example",
avatar:
"https://images.unsplash.com/photo-1489424731084-a5d8b219a5bb?w=96&h=96&dpr=2&q=80",
initials: "GM",
lane: "Las Vegas to Denver",
facility: "LAS-1",
stage: "Labeling",
promise: "Tomorrow 09:20",
slaMinutes: 312,
automation: "Autopilot",
owner: "Nora Albright",
units: 94,
value: 10920,
risk: "No current risk",
status: "On Time",
},
{
id: "row-1007",
reference: "NSC-84827",
customer: "Ridge Supply",
email: "buyers@ridgesupply.example",
avatar:
"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=96&h=96&dpr=2&q=80",
initials: "RS",
lane: "Portland to San Jose",
facility: "PDX-1",
stage: "Split shipment",
promise: "Today 20:00",
slaMinutes: 36,
automation: "Copilot",
owner: "Maya Singh",
units: 176,
value: 14680,
risk: "Two SKUs short at primary node",
status: "At Risk",
},
{
id: "row-1008",
reference: "NSC-84842",
customer: "Casa Verde",
email: "storeops@casaverde.example",
avatar:
"https://images.unsplash.com/photo-1544725176-7c40e5a71c5e?w=96&h=96&dpr=2&q=80",
initials: "CV",
lane: "Nashville to Charlotte",
facility: "BNA-2",
stage: "Dock queue",
promise: "Today 14:30",
slaMinutes: -51,
automation: "Manual",
owner: "Nora Albright",
units: 265,
value: 22750,
risk: "Outbound door is constrained",
status: "Delayed",
},
{
id: "row-1009",
reference: "NSC-84864",
customer: "Beacon Cycle",
email: "logistics@beaconcycle.example",
avatar:
"https://images.unsplash.com/photo-1552058544-f2b08422138a?w=96&h=96&dpr=2&q=80",
initials: "BC",
lane: "Columbus to Pittsburgh",
facility: "CMH-1",
stage: "Fraud review",
promise: "Tomorrow 11:45",
slaMinutes: 510,
automation: "Manual",
owner: "Maya Singh",
units: 58,
value: 8920,
risk: "Payment review blocks release",
status: "Blocked",
},
{
id: "row-1010",
reference: "NSC-84888",
customer: "Aster Goods",
email: "ops@astergoods.example",
avatar:
"https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?w=96&h=96&dpr=2&q=80",
initials: "AG",
lane: "Reno to Salt Lake City",
facility: "RNO-1",
stage: "Manifest",
promise: "Today 22:15",
slaMinutes: 177,
automation: "Autopilot",
owner: "Leo Martins",
units: 390,
value: 19340,
risk: "Manifest is ready for carrier scan",
status: "On Time",
},
]
export function fulfillmentSearchBlob(row: FulfillmentException): string {
return [
row.reference,
row.customer,
row.email,
row.lane,
row.facility,
row.stage,
row.promise,
row.automation,
row.owner,
row.risk,
row.status,
String(row.units),
String(row.value),
]
.filter(Boolean)
.join(" ")
.toLowerCase()
}
@@ -0,0 +1,520 @@
import { memo } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import {
DataGridTableRowSelect,
DataGridTableRowSelectAll,
} from "@/components/reui/data-grid/data-grid-table"
import { type ColumnDef, type Row } from "@tanstack/react-table"
import { toast } from "sonner"
import { cn } from "@evobgp/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@evobgp/ui/components/avatar"
import { Button } from "@evobgp/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@evobgp/ui/components/dropdown-menu"
import { Item, ItemMedia } from "@evobgp/ui/components/item"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@evobgp/ui/components/tooltip"
import {
AUTOMATION_BADGE_VARIANT,
STATUS_BADGE_VARIANT,
type AutomationLevel,
type FulfillmentException,
type FulfillmentStatus,
} from "./data"
import { PackageIcon, InfoIcon, MoreHorizontalIcon, EyeIcon, BellIcon, CopyIcon, TriangleAlertIcon } from "lucide-react"
const currencyCompact = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
})
const numberCompact = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
})
const availabilityColor: Record<FulfillmentStatus, string> = {
"On Time": "bg-success",
"At Risk": "bg-warning",
Delayed: "bg-info",
Blocked: "bg-destructive",
}
const stageProgress: Record<string, number> = {
"Carrier tender": 72,
"Pick wave": 64,
"Inventory hold": 28,
Packing: 82,
"Cold chain": 48,
Labeling: 76,
"Split shipment": 39,
"Dock queue": 31,
"Fraud review": 24,
Manifest: 90,
}
function DotSeparator() {
return (
<span
aria-hidden="true"
className="bg-muted-foreground/40 size-1 shrink-0 rounded-full"
/>
)
}
export const StatusBadge = memo(function StatusBadge({
status,
}: {
status: FulfillmentStatus
}) {
return (
<Badge variant={STATUS_BADGE_VARIANT[status]} className="gap-1.5">
<span
aria-hidden="true"
className={cn("size-1.5 rounded-full", availabilityColor[status])}
/>
{status}
</Badge>
)
})
function AutomationBadge({ level }: { level: AutomationLevel }) {
return <Badge variant={AUTOMATION_BADGE_VARIANT[level]}>{level}</Badge>
}
const ReferenceCell = memo(function ReferenceCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const order = row.original
return (
<div className="flex min-w-0 flex-col gap-1">
<a
href="#"
className="text-primary truncate text-sm font-medium underline-offset-2 transition-colors hover:underline"
aria-label={`View order ${order.reference}`}
>
{order.reference}
</a>
<div className="text-muted-foreground flex min-w-0 items-center gap-1.5 text-xs">
<span className="shrink-0">{order.facility}</span>
<DotSeparator />
<span className="truncate">{order.owner}</span>
</div>
</div>
)
})
const CustomerCell = memo(function CustomerCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const order = row.original
return (
<div className="flex min-w-0 items-center gap-2">
<div className="relative shrink-0">
<Avatar className="size-8">
<AvatarImage src={order.avatar} alt={order.customer} />
<AvatarFallback>{order.initials}</AvatarFallback>
</Avatar>
<span
className={cn(
"ring-background absolute right-0 bottom-0.5 size-2 rounded-full ring-2",
availabilityColor[order.status]
)}
aria-hidden="true"
/>
</div>
<div className="min-w-0">
<a
href="#"
className="text-foreground hover:text-primary line-clamp-1 font-medium underline-offset-2 transition-colors hover:underline"
aria-label={`View customer ${order.customer}`}
>
{order.customer}
</a>
<div
className="text-muted-foreground line-clamp-1 text-xs"
title={order.email}
>
{order.email}
</div>
</div>
</div>
)
})
const StageCell = memo(function StageCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const order = row.original
const progress = stageProgress[order.stage] ?? 50
return (
<div className="flex min-w-0 flex-col gap-1.5">
<div className="flex min-w-0 items-center gap-1.5">
<Item render={<span />} className="w-auto shrink-0 border-0 p-0">
<ItemMedia variant="icon" className="text-muted-foreground size-auto">
<PackageIcon className="size-4" aria-hidden="true" />
</ItemMedia>
</Item>
<span className="text-foreground min-w-0 truncate font-medium">
{order.stage}
</span>
</div>
<div className="flex min-w-0 items-center gap-2">
<span className="bg-muted block h-1.5 min-w-16 flex-1 overflow-hidden rounded-full">
<span
className={cn(
"block h-full rounded-full",
availabilityColor[order.status]
)}
style={{ width: `${progress}%` }}
/>
</span>
<span className="text-muted-foreground shrink-0 text-xs tabular-nums">
{progress}%
</span>
</div>
</div>
)
})
const LaneCell = memo(function LaneCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const order = row.original
return (
<div className="flex max-w-full min-w-0 flex-col gap-0.5">
<span
className="text-foreground block max-w-full min-w-0 truncate font-medium"
title={order.lane}
>
{order.lane}
</span>
<span
className="text-muted-foreground block max-w-full min-w-0 truncate text-xs"
title={order.facility}
>
{order.facility}
</span>
</div>
)
})
const ValueCell = memo(function ValueCell({
row,
}: {
row: Row<FulfillmentException>
}) {
const valueHint =
row.original.value >= 30000 ? "Priority lane" : "Standard lane"
return (
<div className="flex min-w-0 items-center gap-1.5">
<span className="text-foreground font-medium tabular-nums">
{currencyCompact.format(row.original.value)}
</span>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex size-5 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-offset-2"
aria-label={`Value hint for ${row.original.reference}: ${valueHint}`}
/>
}
>
<InfoIcon className="size-3.5" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-48 p-2.5 text-xs leading-5">
<div className="flex flex-col">
<span>{valueHint}</span>
<span className="text-background/80">
{numberCompact.format(row.original.units)} units
</span>
</div>
</TooltipContent>
</Tooltip>
</div>
)
})
function StateCell({ row }: { row: Row<FulfillmentException> }) {
const sla = row.original.slaMinutes
const slaHint =
sla < 0
? `${Math.abs(sla)} min overdue`
: sla <= 45
? `${sla} min buffer`
: `Due ${row.original.promise}`
return (
<div className="flex min-w-0 flex-col items-start gap-1">
<StatusBadge status={row.original.status} />
<span className="text-muted-foreground max-w-full truncate text-xs">
{slaHint}
</span>
</div>
)
}
function RiskCell({ row }: { row: Row<FulfillmentException> }) {
return (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex items-center gap-1.5 rounded-full focus-visible:ring-2 focus-visible:ring-offset-2"
aria-label={`Risk note for ${row.original.reference}: ${row.original.risk}`}
/>
}
>
<InfoIcon className="size-3.5" aria-hidden="true" />
<span className="max-w-32 truncate text-xs">{row.original.risk}</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs p-3 text-xs leading-5">
{row.original.risk}
</TooltipContent>
</Tooltip>
)
}
function ActionsCell({ row }: { row: Row<FulfillmentException> }) {
const copyReference = async () => {
await navigator.clipboard?.writeText(row.original.reference)
toast.success("Reference copied", {
description: row.original.reference,
})
}
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
size="icon"
variant="ghost"
className="size-7"
aria-label={`Actions for ${row.original.reference}`}
/>
}
>
<MoreHorizontalIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-44">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() =>
toast.info("Opening order", {
description: row.original.reference,
})
}
>
<EyeIcon className="size-4" aria-hidden="true" />
View order
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.info("Owner notified", {
description: row.original.owner,
})
}
>
<BellIcon className="size-4" aria-hidden="true" />
Notify owner
</DropdownMenuItem>
<DropdownMenuItem onClick={copyReference}>
<CopyIcon className="size-4" aria-hidden="true" />
Copy reference
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() =>
toast.warning("Escalation staged", {
description: "Connect this action to your incident workflow.",
})
}
>
<TriangleAlertIcon className="size-4" aria-hidden="true" />
Escalate
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
export const columns: ColumnDef<FulfillmentException>[] = [
{
accessorKey: "id",
id: "id",
header: () => <DataGridTableRowSelectAll />,
cell: ({ row }) => <DataGridTableRowSelect row={row} />,
enableSorting: false,
size: 35,
enableResizing: false,
enableHiding: false,
meta: {
headerClassName: "ps-4!",
cellClassName: "ps-4!",
},
},
{
accessorKey: "reference",
id: "reference",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <ReferenceCell row={row} />,
size: 138,
enableSorting: true,
enableHiding: false,
enableResizing: true,
meta: {
headerTitle: "Order",
},
},
{
accessorKey: "customer",
id: "customer",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <CustomerCell row={row} />,
size: 210,
enableSorting: true,
enableHiding: false,
enableResizing: true,
minSize: 190,
meta: {
headerTitle: "Customer",
autoSize: true,
},
},
{
accessorKey: "lane",
id: "lane",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <LaneCell row={row} />,
size: 165,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Lane",
},
},
{
accessorKey: "stage",
id: "stage",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <StageCell row={row} />,
size: 160,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Stage",
},
},
{
accessorKey: "automation",
id: "automation",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <AutomationBadge level={row.original.automation} />,
size: 112,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Automation",
},
},
{
accessorKey: "value",
id: "value",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <ValueCell row={row} />,
size: 120,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Value",
},
},
{
accessorKey: "risk",
id: "risk",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <RiskCell row={row} />,
size: 170,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "Risk",
},
},
{
accessorKey: "status",
id: "status",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <StateCell row={row} />,
size: 142,
enableSorting: true,
enableHiding: true,
enableResizing: true,
meta: {
headerTitle: "State",
},
},
{
id: "actions",
header: "",
cell: ({ row }) => <ActionsCell row={row} />,
size: 46,
enableSorting: false,
enableHiding: false,
enableResizing: false,
},
]
@@ -0,0 +1,369 @@
import { useMemo, useState } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGrid as ReuiDataGrid } from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/reui/frame"
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
type PaginationState,
type RowSelectionState,
type SortingState,
type VisibilityState,
} from "@tanstack/react-table"
import { toast } from "sonner"
import { Button } from "@evobgp/ui/components/button"
import { Checkbox } from "@evobgp/ui/components/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@evobgp/ui/components/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@evobgp/ui/components/input-group"
import { Label } from "@evobgp/ui/components/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@evobgp/ui/components/popover"
import { Separator } from "@evobgp/ui/components/separator"
import { TooltipProvider } from "@evobgp/ui/components/tooltip"
import {
FULFILLMENT_ROWS,
fulfillmentSearchBlob,
STATUS_ORDER,
type FulfillmentStatus,
} from "./data"
import { columns, StatusBadge } from "./exception-columns"
import { SearchIcon, XIcon, FilterIcon, MoreHorizontalIcon, FileDownIcon, RefreshCwIcon, SettingsIcon, PlusIcon } from "lucide-react"
interface ToolbarProps {
searchQuery: string
onSearchChange: (value: string) => void
selectedStatuses: FulfillmentStatus[]
onStatusChange: (checked: boolean, status: FulfillmentStatus) => void
onClearFilters: () => void
hasActiveFilters: boolean
statusCounts: Record<string, number>
}
function Toolbar({
searchQuery,
onSearchChange,
selectedStatuses,
onStatusChange,
onClearFilters,
hasActiveFilters,
statusCounts,
}: ToolbarProps) {
return (
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<InputGroup className="w-full min-w-52 sm:w-60">
<InputGroupAddon align="inline-start">
<SearchIcon aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
placeholder="Search orders..."
aria-label="Search orders"
value={searchQuery}
onChange={(event) => onSearchChange(event.target.value)}
/>
{searchQuery.length > 0 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label="Clear search"
size="icon-xs"
onClick={() => onSearchChange("")}
>
<XIcon aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
<Popover>
<PopoverTrigger
render={
<Button variant="outline" aria-label="Filter by order status">
<FilterIcon aria-hidden="true" />
Status
{selectedStatuses.length > 0 && (
<Badge variant="info-outline">
{selectedStatuses.length}
</Badge>
)}
</Button>
}
/>
<PopoverContent
align="start"
className="flex w-48 flex-col gap-2.5 p-3"
>
<span className="text-muted-foreground text-xs font-medium">
Filter by status
</span>
{STATUS_ORDER.map((status) => (
<div key={status} className="flex items-center gap-2.5">
<Checkbox
id={`status-${status.toLowerCase().replace(/\s+/g, "-")}`}
checked={selectedStatuses.includes(status)}
onCheckedChange={(checked) =>
onStatusChange(checked === true, status)
}
/>
<Label
htmlFor={`status-${status.toLowerCase().replace(/\s+/g, "-")}`}
className="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-2 font-normal"
>
<StatusBadge status={status} />
<span className="text-muted-foreground shrink-0 text-xs tabular-nums">
{statusCounts[status] ?? 0}
</span>
</Label>
</div>
))}
</PopoverContent>
</Popover>
{hasActiveFilters && (
<Button
type="button"
variant="ghost"
size="sm"
className="text-muted-foreground"
onClick={onClearFilters}
>
Clear filters
</Button>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" aria-label="Exception queue actions">
<MoreHorizontalIcon aria-hidden="true" />
Actions
</Button>
}
/>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() =>
toast.success("Export ready", {
description: "Exception queue export prepared.",
})
}
>
<FileDownIcon aria-hidden="true" />
Export CSV
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.message("Queue refreshed", {
description: "Live data would refresh through your API.",
})
}
>
<RefreshCwIcon aria-hidden="true" />
Refresh
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.info("View settings", {
description: "Column and density controls are available.",
})
}
>
<SettingsIcon aria-hidden="true" />
View settings
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
export function ExceptionGrid() {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
})
const [sorting, setSorting] = useState<SortingState>([
{ id: "value", desc: true },
])
const [searchQuery, setSearchQuery] = useState("")
const [selectedStatuses, setSelectedStatuses] = useState<FulfillmentStatus[]>(
[]
)
const [columnOrder, setColumnOrder] = useState<string[]>(
columns.map((column) => column.id as string)
)
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
risk: false,
})
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const statusCounts = useMemo(
() =>
FULFILLMENT_ROWS.reduce(
(acc, row) => {
acc[row.status] = (acc[row.status] || 0) + 1
return acc
},
{} as Record<string, number>
),
[]
)
const filteredData = useMemo(() => {
return FULFILLMENT_ROWS.filter((row) => {
const matchesStatus =
!selectedStatuses.length || selectedStatuses.includes(row.status)
const matchesSearch =
!searchQuery ||
fulfillmentSearchBlob(row).includes(searchQuery.toLowerCase())
return matchesStatus && matchesSearch
})
}, [searchQuery, selectedStatuses])
const hasActiveFilters =
searchQuery.trim().length > 0 || selectedStatuses.length > 0
const resetToFirstPage = () => {
setPagination((current) =>
current.pageIndex === 0 ? current : { ...current, pageIndex: 0 }
)
}
const handleSearchChange = (value: string) => {
setSearchQuery(value)
resetToFirstPage()
}
const handleStatusChange = (checked: boolean, status: FulfillmentStatus) => {
setSelectedStatuses((current) =>
checked ? [...current, status] : current.filter((item) => item !== status)
)
resetToFirstPage()
}
const handleClearFilters = () => {
setSelectedStatuses([])
setSearchQuery("")
resetToFirstPage()
}
const table = useReactTable({
columns,
data: filteredData,
pageCount: Math.ceil(filteredData.length / pagination.pageSize),
getRowId: (row) => row.id,
state: { pagination, sorting, columnOrder, columnVisibility, rowSelection },
columnResizeMode: "onChange",
enableRowSelection: true,
autoResetPageIndex: false,
onColumnOrderChange: setColumnOrder,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<TooltipProvider delay={200}>
<ReuiDataGrid
table={table}
recordCount={filteredData.length}
emptyMessage={
filteredData.length === 0
? "No fulfillment exceptions match your filters."
: undefined
}
tableLayout={{
columnsPinnable: true,
columnsResizable: true,
columnsMovable: true,
columnsVisibility: true,
headerSticky: true,
dense: true,
}}
tableClassNames={{
bodyRow: "[&>td]:h-16",
}}
>
<Frame variant="default" spacing="sm" className="w-full">
<FrameHeader className="flex-row items-center justify-between gap-3">
<div className="flex flex-col gap-0.5">
<FrameTitle className="text-balance">Exception Queue</FrameTitle>
<FrameDescription className="text-xs text-pretty">
{filteredData.length} of {FULFILLMENT_ROWS.length} fulfillment
records
</FrameDescription>
</div>
<Button
type="button"
onClick={() =>
toast.info("Create exception", {
description:
"Connect this button to your incident intake flow.",
})
}
>
<PlusIcon aria-hidden="true" />
Add exception
</Button>
</FrameHeader>
<FramePanel className="bg-card p-0! shadow-none!">
<div className="px-4 py-3">
<Toolbar
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
selectedStatuses={selectedStatuses}
onStatusChange={handleStatusChange}
onClearFilters={handleClearFilters}
hasActiveFilters={hasActiveFilters}
statusCounts={statusCounts}
/>
</div>
<Separator />
<DataGridScrollArea>
<DataGridTable />
</DataGridScrollArea>
</FramePanel>
<FrameFooter>
<DataGridPagination />
</FrameFooter>
</Frame>
</ReuiDataGrid>
</TooltipProvider>
)
}
@@ -0,0 +1,246 @@
import { useState } from "react"
import { format } from "date-fns"
import { type DateRange } from "react-day-picker"
import { toast } from "sonner"
import { Button } from "@evobgp/ui/components/button"
import { Calendar } from "@evobgp/ui/components/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@evobgp/ui/components/popover"
import { CalendarIcon, DownloadIcon } from "lucide-react"
type PeriodKey = "last30" | "prev30"
type ReportDateRange = {
from: Date
to: Date
}
type DateRangePreset = {
id: string
label: string
period: PeriodKey
range: ReportDateRange
}
const reportRange = (
fromMonth: number,
fromDay: number,
toMonth: number,
toDay: number,
year = 2026
): ReportDateRange => ({
from: new Date(year, fromMonth, fromDay),
to: new Date(year, toMonth, toDay),
})
const preset = (
id: string,
label: string,
period: PeriodKey,
range: ReportDateRange
): DateRangePreset => ({ id, label, period, range })
const LAST_30_RANGE = reportRange(4, 12, 5, 10)
const PREVIOUS_30_RANGE = reportRange(3, 12, 4, 11)
const REPORT_RANGE_PRESETS: DateRangePreset[] = [
preset("today", "Today", "last30", reportRange(5, 10, 5, 10)),
preset("yesterday", "Yesterday", "last30", reportRange(5, 9, 5, 9)),
preset("last7", "Last 7 days", "last30", reportRange(5, 4, 5, 10)),
preset("last30", "Last 30 days", "last30", LAST_30_RANGE),
preset("monthToDate", "Month to date", "last30", reportRange(5, 1, 5, 10)),
preset("lastMonth", "Last month", "last30", reportRange(4, 1, 4, 31)),
preset("yearToDate", "Year to date", "last30", reportRange(0, 1, 5, 10)),
preset("lastYear", "Last year", "prev30", reportRange(0, 1, 11, 31, 2025)),
]
const MAX_REPORT_DATE = LAST_30_RANGE.to
function isSameRange(first: ReportDateRange, second: DateRange) {
const secondFrom = second.from
const secondTo = second.to ?? second.from
return (
Boolean(secondFrom && secondTo) &&
first.from.getTime() === secondFrom?.getTime() &&
first.to.getTime() === secondTo?.getTime()
)
}
function normalizeRange(
range: DateRange | undefined,
fallback: ReportDateRange
): ReportDateRange {
if (!range?.from) return fallback
const from = range.from
const to = range.to ?? range.from
return from.getTime() <= to.getTime() ? { from, to } : { from: to, to: from }
}
function formatReportRange(range: ReportDateRange) {
return `${format(range.from, "MMM d, yyyy")} - ${format(range.to, "MMM d, yyyy")}`
}
function getPeriodForRange(range: ReportDateRange) {
const matchingPreset = getMatchingPreset(range)
if (matchingPreset) return matchingPreset.period
return range.to.getTime() <= PREVIOUS_30_RANGE.to.getTime()
? "prev30"
: "last30"
}
function getMatchingPreset(range: DateRange | undefined) {
if (!range?.from || !range.to) return undefined
const normalizedRange = normalizeRange(range, LAST_30_RANGE)
return REPORT_RANGE_PRESETS.find((preset) =>
isSameRange(preset.range, normalizedRange)
)
}
function ReportDateRangePicker({
period,
onPeriodChange,
}: {
period: PeriodKey
onPeriodChange: (value: PeriodKey) => void
}) {
const initialRange = period === "prev30" ? PREVIOUS_30_RANGE : LAST_30_RANGE
const [open, setOpen] = useState(false)
const [committedRange, setCommittedRange] =
useState<ReportDateRange>(initialRange)
const [draftRange, setDraftRange] = useState<DateRange | undefined>(
initialRange
)
const selectedPresetId = getMatchingPreset(draftRange ?? committedRange)?.id
function handleOpenChange(nextOpen: boolean) {
if (nextOpen) {
setDraftRange(committedRange)
}
setOpen(nextOpen)
}
function handleApply() {
const nextRange = normalizeRange(draftRange, committedRange)
setCommittedRange(nextRange)
onPeriodChange(getPeriodForRange(nextRange))
setOpen(false)
}
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger
render={
<Button
type="button"
size="sm"
variant="outline"
className="group/pick-date w-[250px] max-w-full justify-between leading-none font-normal tabular-nums"
>
<span className="truncate">
{formatReportRange(committedRange)}
</span>
<CalendarIcon className="text-muted-foreground/80 group-hover/pick-date:text-foreground shrink-0 transition-colors" aria-hidden="true" />
</Button>
}
/>
<PopoverContent align="end" className="w-auto p-0">
<div className="flex flex-col">
<div className="flex flex-col sm:grid sm:grid-cols-[10rem_1fr]">
<div className="border-border flex flex-wrap gap-1 border-b p-2 sm:flex-col sm:border-r sm:border-b-0">
{REPORT_RANGE_PRESETS.map((preset) => {
const selected = selectedPresetId === preset.id
return (
<Button
key={preset.id}
type="button"
size="sm"
variant={selected ? "secondary" : "ghost"}
className={
selected
? "justify-start"
: "text-muted-foreground justify-start"
}
onClick={() => setDraftRange(preset.range)}
>
{preset.label}
</Button>
)
})}
</div>
<Calendar
mode="range"
selected={draftRange}
onSelect={setDraftRange}
numberOfMonths={2}
defaultMonth={draftRange?.from ?? committedRange.from}
disabled={{
after: MAX_REPORT_DATE,
}}
/>
</div>
<div className="border-border flex items-center justify-between gap-2 border-t px-3 py-1.5">
<Button
type="button"
size="sm"
variant="outline"
onClick={() => setDraftRange(LAST_30_RANGE)}
>
Reset
</Button>
<div className="flex items-center gap-2">
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => {
setDraftRange(committedRange)
setOpen(false)
}}
>
Cancel
</Button>
<Button type="button" size="sm" onClick={handleApply}>
Apply
</Button>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)
}
// Header action controls reused from the solution-agents-8 report toolbar.
export function NavbarActions() {
const [periodKey, setPeriodKey] = useState<PeriodKey>("last30")
function handleExport() {
toast.success("Export queued", {
description: "Fulfillment command report is being prepared.",
})
}
return (
<div className="flex shrink-0 items-center gap-2">
<ReportDateRangePicker period={periodKey} onPeriodChange={setPeriodKey} />
<Button size="sm" type="button" onClick={handleExport}>
<DownloadIcon aria-hidden="true" />
<span className="hidden sm:block">Export</span>
</Button>
</div>
)
}
@@ -0,0 +1,34 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@evobgp/ui/components/breadcrumb"
// Navbar breadcrumb
export function NavbarBreadcrumb() {
return (
<Breadcrumb className="min-w-0">
<BreadcrumbList className="flex-nowrap">
<BreadcrumbItem className="hidden md:inline-flex">
<BreadcrumbLink render={<a href="#" />}>Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:flex" />
<BreadcrumbItem className="hidden md:inline-flex">
<BreadcrumbLink render={<a href="#" />}>Operations</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:flex" />
<BreadcrumbItem className="min-w-0">
<BreadcrumbPage className="truncate">Fulfillment</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -0,0 +1,80 @@
import { useState } from "react"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@evobgp/ui/components/avatar"
import { Button } from "@evobgp/ui/components/button"
import { Input } from "@evobgp/ui/components/input"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@evobgp/ui/components/popover"
import { TEAM_EXTRA_COUNT, TEAM_MEMBERS } from "./data"
import { UserPlusIcon } from "lucide-react"
// Header presence controls with team avatars and invite action.
export function NavbarPresence() {
const [email, setEmail] = useState("")
const [open, setOpen] = useState(false)
const handleInvite = () => {
if (!email.trim()) return
setEmail("")
setOpen(false)
}
return (
<div className="flex shrink-0 items-center gap-1.5">
<AvatarGroup>
{TEAM_MEMBERS.map((member, index) => (
<Avatar key={index} size="sm">
<AvatarImage src={member.src} alt={member.name} />
<AvatarFallback className="text-[9px]! font-medium">
{member.initials}
</AvatarFallback>
</Avatar>
))}
<AvatarGroupCount className="text-[10px]! font-medium">
+{TEAM_EXTRA_COUNT}
</AvatarGroupCount>
</AvatarGroup>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label="Invite team member"
/>
}
>
<UserPlusIcon aria-hidden="true" />
</PopoverTrigger>
<PopoverContent sideOffset={7} align="end" className="w-72">
<div className="flex flex-col gap-3">
<h4 className="text-foreground text-sm">Invite team member</h4>
<Input
type="email"
placeholder="email@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleInvite()}
/>
<Button onClick={handleInvite} disabled={!email.trim()}>
Send invite
</Button>
</div>
</PopoverContent>
</Popover>
</div>
)
}
@@ -0,0 +1,17 @@
import { NavbarActions } from "./navbar-actions"
import { NavbarBreadcrumb } from "./navbar-breadcrumb"
// Navbar with breadcrumb and report range actions.
export function Navbar() {
return (
<header
className="flex min-h-9 w-full shrink-0 items-center justify-between gap-2 pb-1"
aria-label="Fulfillment command header"
>
<NavbarBreadcrumb />
<NavbarActions />
</header>
)
}
@@ -0,0 +1,9 @@
import { Dashboard } from "./components/dashboard"
export function Page() {
return (
<main className="bg-background min-h-svh w-full p-3 sm:p-4 lg:p-6">
<Dashboard />
</main>
)
}
@@ -0,0 +1,141 @@
import { Area, AreaChart, ResponsiveContainer, Tooltip } from "recharts"
import { Card, CardContent } from "@evobgp/ui/components/card"
import { activeUsersData, customersData, revenueData } from "./data"
// Business metric cards
const businessCards = [
{
title: "Revenue",
period: "reui.io, 28 days",
value: "$6.2K",
timestamp: "",
data: revenueData,
color: "var(--color-emerald-500)",
gradientId: "revenueGradient",
formatValue: (value: number) => `$${(value / 1000).toFixed(1)}K`,
},
{
title: "Signups",
period: "Last 28 days",
value: "4,238",
timestamp: "3h ago",
data: customersData,
color: "var(--color-blue-500)",
gradientId: "customersGradient",
formatValue: (value: number) => `${(value / 1000).toFixed(1)}K`,
},
{
title: "Active Licenses",
period: "ReUI Cloud, 28 days",
value: "4,238",
timestamp: "1h ago",
data: activeUsersData,
color: "var(--color-violet-500)",
gradientId: "usersGradient",
formatValue: (value: number) => `${(value / 1000).toFixed(1)}K`,
},
]
export function Chart() {
return (
<div className="@container w-full max-w-6xl">
<div className="grid grid-cols-1 gap-4 @3xl:grid-cols-3">
{businessCards.map((card) => (
<Card key={card.title}>
<CardContent className="space-y-5">
{/* Header */}
<div className="text-sm font-semibold">{card.title}</div>
{/* Chart */}
<div className="flex items-end justify-between gap-2.5">
{/* Value */}
<div className="flex flex-col gap-px pb-2">
<div className="text-muted-foreground text-xs whitespace-nowrap">
{card.period}
</div>
<div className="text-foreground text-xl font-semibold tracking-tight">
{card.value}
</div>
</div>
<div className="relative h-16 w-full max-w-40">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={card.data}
margin={{ top: 5, right: 5, left: 5, bottom: 5 }}
>
<defs>
<linearGradient
id={card.gradientId}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={card.color}
stopOpacity={0.3}
/>
<stop
offset="100%"
stopColor={card.color}
stopOpacity={0.05}
/>
</linearGradient>
</defs>
<Tooltip
cursor={{
stroke: card.color,
strokeWidth: 1,
strokeDasharray: "2 2",
}}
content={({ active, payload }) => {
if (active && payload && payload.length) {
const value = payload[0].value as number
return (
<Card className="bg-popover text-popover-foreground pointer-events-none p-0 shadow-md">
<CardContent className="flex min-w-24 flex-col gap-1 px-2 py-1.5">
<span className="text-muted-foreground text-[10px] leading-none font-medium">
{card.title}
</span>
<span className="text-popover-foreground text-xs leading-none font-semibold tabular-nums">
{card.formatValue(value)}
</span>
</CardContent>
</Card>
)
}
return null
}}
/>
<Area
type="monotone"
dataKey="value"
stroke={card.color}
fill={`url(#${card.gradientId})`}
strokeWidth={2}
dot={false}
activeDot={{
r: 4,
fill: card.color,
stroke: "white",
strokeWidth: 2,
}}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
}
@@ -0,0 +1,338 @@
import { type ComponentProps } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import { type ColumnDef } from "@tanstack/react-table"
import { cn } from "@evobgp/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@evobgp/ui/components/avatar"
import { Button } from "@evobgp/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@evobgp/ui/components/dropdown-menu"
import { type ModuleRecord, type ModuleStatus } from "./data"
import { CalendarDaysIcon, FileTextIcon, StarIcon, MoreHorizontalIcon, CopyIcon, ArchiveIcon } from "lucide-react"
export type ModuleRowAction = "open" | "favorite" | "duplicate" | "archive"
const moduleStatusVariant: Record<
ModuleStatus,
ComponentProps<typeof Badge>["variant"]
> = {
Planned: "info-outline",
Backlog: "outline",
"In Progress": "warning-outline",
}
const moduleStatusDotClass: Record<ModuleStatus, string> = {
Planned: "bg-sky-500 dark:bg-sky-400",
Backlog: "bg-muted-foreground/50",
"In Progress": "bg-amber-500 dark:bg-amber-400",
}
function DotSeparator() {
return (
<span
className="bg-muted-foreground/45 size-1 shrink-0 rounded-full"
aria-hidden="true"
/>
)
}
function getProgressToneClass(value: number) {
if (value >= 75) return "text-emerald-500 dark:text-emerald-400"
if (value >= 40) return "text-amber-500 dark:text-amber-400"
if (value > 0) return "text-sky-500 dark:text-sky-400"
return "text-muted-foreground/35"
}
function getWindowDurationLabel(module: ModuleRecord) {
const start = new Date(module.dateStart).getTime()
const end = new Date(module.dateEnd).getTime()
const dayMs = 24 * 60 * 60 * 1000
const days = Math.max(1, Math.round((end - start) / dayMs))
return `${days}-day window`
}
function getCompactDateRange(module: ModuleRecord) {
return module.dateRange.replace(/, 2026/g, "")
}
function ModuleProgress({ module }: { module: ModuleRecord }) {
const value = module.progress
const radius = 18
const circumference = 2 * Math.PI * radius
const dashOffset = circumference - (value / 100) * circumference
const progressClassName = getProgressToneClass(value)
return (
<div className="flex min-w-0 items-center gap-2.5">
<div className="relative size-10 shrink-0">
<svg
viewBox="0 0 44 44"
className="absolute inset-0 size-10 -rotate-90"
aria-hidden="true"
>
<circle
cx="22"
cy="22"
r={radius}
fill="none"
className="stroke-muted-foreground/20"
strokeWidth="3.25"
/>
<circle
cx="22"
cy="22"
r={radius}
fill="none"
className={cn("stroke-current", progressClassName)}
strokeWidth="3.25"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
/>
</svg>
<span className="text-muted-foreground absolute inset-0 flex items-center justify-center text-[9px] leading-none font-medium tabular-nums">
{value}%
</span>
</div>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-foreground text-sm font-medium tabular-nums">
{value}% ready
</span>
<span className="text-muted-foreground truncate text-xs tabular-nums">
{module.tasksCompleted}/{module.tasksTotal} tasks
</span>
</div>
</div>
)
}
function ModuleNameCell({ module }: { module: ModuleRecord }) {
return (
<div className="flex min-w-0 flex-col gap-1">
<span className="text-foreground truncate text-sm leading-5 font-medium">
{module.name}
</span>
<div className="text-muted-foreground flex min-w-0 flex-wrap items-center gap-1.5 text-xs">
<span className="shrink-0">{module.kind}</span>
<DotSeparator />
<span className="inline-flex min-w-0 items-center gap-1.5">
<Avatar className="size-4 shrink-0">
{module.owner.avatar ? (
<AvatarImage src={module.owner.avatar} alt={module.owner.name} />
) : null}
<AvatarFallback className="text-[8px]">
{module.owner.initials}
</AvatarFallback>
</Avatar>
<span className="truncate">{module.owner.name}</span>
</span>
<DotSeparator />
<span className="truncate font-mono tracking-wide">
{module.domain}
</span>
</div>
</div>
)
}
function ModuleDateCell({ module }: { module: ModuleRecord }) {
return (
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-foreground truncate text-sm font-medium tabular-nums">
{getCompactDateRange(module)}
</span>
<span className="text-muted-foreground inline-flex min-w-0 items-center gap-1.5 truncate text-xs">
<CalendarDaysIcon className="size-3.5 shrink-0" aria-hidden="true" />
{getWindowDurationLabel(module)}
</span>
</div>
)
}
function ModuleStatusCell({ module }: { module: ModuleRecord }) {
return (
<Badge variant={moduleStatusVariant[module.status]}>
<span
className={cn(
"size-1.5 shrink-0 rounded-full!",
moduleStatusDotClass[module.status]
)}
aria-hidden="true"
/>
{module.status}
</Badge>
)
}
function ModuleActions({
module,
onAction,
}: {
module: ModuleRecord
onAction: (action: ModuleRowAction, module: ModuleRecord) => void
}) {
return (
<div className="flex items-center justify-end gap-1">
<Button
type="button"
size="icon-sm"
variant="ghost"
aria-label={`Open ${module.name}`}
onClick={(event) => {
event.stopPropagation()
onAction("open", module)
}}
>
<FileTextIcon aria-hidden="true" />
</Button>
<Button
type="button"
size="icon-sm"
variant="ghost"
aria-label={`${module.favorite ? "Unfavorite" : "Favorite"} ${
module.name
}`}
className={cn(module.favorite && "text-amber-500")}
onClick={(event) => {
event.stopPropagation()
onAction("favorite", module)
}}
>
<StarIcon aria-hidden="true" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
size="icon-sm"
variant="ghost"
aria-label={`More actions for ${module.name}`}
onClick={(event) => event.stopPropagation()}
/>
}
>
<MoreHorizontalIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => onAction("duplicate", module)}>
<CopyIcon aria-hidden="true" />
Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => onAction("archive", module)}
>
<ArchiveIcon aria-hidden="true" />
Archive
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
export function createModuleGridColumns({
onAction,
}: {
onAction: (action: ModuleRowAction, module: ModuleRecord) => void
}): ColumnDef<ModuleRecord>[] {
return [
{
accessorKey: "progress",
id: "progress",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <ModuleProgress module={row.original} />,
size: 210,
enableSorting: true,
enableHiding: false,
enableResizing: false,
meta: {
headerTitle: "Progress",
},
},
{
accessorKey: "name",
id: "name",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <ModuleNameCell module={row.original} />,
minSize: 300,
enableSorting: true,
enableHiding: false,
enableResizing: false,
meta: {
autoSize: true,
headerTitle: "Module",
},
},
{
accessorKey: "dateStart",
id: "dateStart",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <ModuleDateCell module={row.original} />,
sortingFn: (rowA, rowB) =>
new Date(rowA.original.dateStart).getTime() -
new Date(rowB.original.dateStart).getTime(),
size: 180,
enableSorting: true,
enableHiding: false,
enableResizing: false,
meta: {
headerTitle: "Window",
},
},
{
accessorKey: "status",
id: "status",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => <ModuleStatusCell module={row.original} />,
size: 126,
enableSorting: true,
enableHiding: false,
enableResizing: false,
meta: {
headerTitle: "Status",
},
},
{
id: "actions",
header: ({ column }) => (
<DataGridColumnHeader column={column} visibility={true} />
),
cell: ({ row }) => (
<ModuleActions module={row.original} onAction={onAction} />
),
size: 104,
enableSorting: false,
enableHiding: false,
enableResizing: false,
meta: {
headerTitle: "Actions",
},
},
]
}
@@ -0,0 +1,33 @@
"use client"
import { Chart } from "./chart"
import { ModulesDataGridView } from "./data-grid-view"
import { Navbar } from "./navbar"
/**
* ReUI operations dashboard: navbar -> metric charts -> module grid.
* The sections are copied from reviewed donor blocks.
* Customize: swap the records and chart series in data.tsx first.
*/
export function Dashboard() {
return (
<div className="bg-background text-foreground flex min-h-svh w-full flex-col">
<Navbar />
<main
className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-6 p-4 pt-6 sm:p-8"
aria-labelledby="page-heading"
>
<h1 id="page-heading" className="sr-only">
ReUI Operations Dashboard
</h1>
{/* Metric Charts */}
<Chart />
{/* Module Grid */}
<ModulesDataGridView />
</main>
</div>
)
}
@@ -0,0 +1,380 @@
import { useCallback, useMemo, useState } from "react"
import { Badge } from "@/components/reui/badge"
import { DataGrid } from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import {
DataGridTable,
DataGridTableHeader,
} from "@/components/reui/data-grid/data-grid-table"
import {
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
type PaginationState,
type SortingState,
} from "@tanstack/react-table"
import { toast } from "sonner"
import { Button } from "@evobgp/ui/components/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@evobgp/ui/components/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@evobgp/ui/components/input-group"
import { createModuleGridColumns, type ModuleRowAction } from "./columns"
import {
MODULE_RECORDS,
MODULE_STATUS_OPTIONS,
type ModuleRecord,
type ModuleStatus,
} from "./data"
import { CircleCheckIcon, FlagIcon, ChevronRightIcon, PackageIcon, SearchIcon, XIcon, ArrowUpDownIcon, ChevronDownIcon, FilterIcon } from "lucide-react"
type ModuleSort = "name" | "dateStart" | "progress" | "status"
const sortLabels: Record<ModuleSort, string> = {
name: "Name",
dateStart: "Window",
progress: "Progress",
status: "Status",
}
const EMPTY_MODULE_MESSAGE = "No ReUI modules match the selected filters."
function buildSorting(sortBy: ModuleSort): SortingState {
return [{ id: sortBy, desc: false }]
}
function getModuleSearchBlob(module: ModuleRecord) {
return [
module.name,
module.id,
module.kind,
module.domain,
module.owner.name,
module.owner.role,
module.health,
module.status,
module.dateRange,
]
.join(" ")
.toLowerCase()
}
export function ModulesDataGridView() {
const [modules, setModules] = useState<ModuleRecord[]>(MODULE_RECORDS)
const [searchQuery, setSearchQuery] = useState("")
const [selectedStatuses, setSelectedStatuses] = useState<ModuleStatus[]>([])
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const [sortBy, setSortBy] = useState<ModuleSort>("name")
const [sorting, setSorting] = useState<SortingState>(() =>
buildSorting("name")
)
const filteredModules = useMemo(() => {
const normalizedSearchQuery = searchQuery.trim().toLowerCase()
return modules.filter((module) => {
const matchesSearch =
normalizedSearchQuery.length === 0 ||
getModuleSearchBlob(module).includes(normalizedSearchQuery)
const matchesStatus =
selectedStatuses.length === 0 ||
selectedStatuses.includes(module.status)
return matchesSearch && matchesStatus
})
}, [modules, searchQuery, selectedStatuses])
const activeFilterCount = selectedStatuses.length
const resetPagination = useCallback(() => {
setPagination((current) => ({
...current,
pageIndex: 0,
}))
}, [])
const handleSearchChange = useCallback(
(value: string) => {
setSearchQuery(value)
resetPagination()
},
[resetPagination]
)
const handleStatusToggle = useCallback(
(status: ModuleStatus, checked: boolean) => {
setSelectedStatuses((current) => {
if (checked) {
return current.includes(status) ? current : [...current, status]
}
return current.filter((item) => item !== status)
})
resetPagination()
},
[resetPagination]
)
const handleSortChange = useCallback(
(value: string) => {
const nextSort = value as ModuleSort
setSortBy(nextSort)
setSorting(buildSorting(nextSort))
resetPagination()
},
[resetPagination]
)
const handleModuleAction = useCallback(
(action: ModuleRowAction, module: ModuleRecord) => {
if (action === "favorite") {
setModules((current) =>
current.map((item) =>
item.id === module.id
? {
...item,
favorite: !item.favorite,
}
: item
)
)
toast.success(module.favorite ? "Removed favorite" : "Module starred", {
description: module.name,
icon: (
<CircleCheckIcon className="size-4" aria-hidden="true" />
),
})
return
}
if (action === "open") {
toast.info("Open ReUI module", {
description: `${module.name} (${module.kind})`,
})
return
}
toast.message(
action === "duplicate" ? "Duplicate module" : "Archive module",
{
description: `Connect this action to your ${module.name} flow.`,
}
)
},
[]
)
const handleAddModule = () => {
toast.success("Add ReUI module", {
description: "Open your module creation dialog.",
icon: (
<CircleCheckIcon className="size-4" aria-hidden="true" />
),
})
}
const columns = useMemo(
() => createModuleGridColumns({ onAction: handleModuleAction }),
[handleModuleAction]
)
// eslint-disable-next-line react-hooks/incompatible-library
const table = useReactTable({
data: filteredModules,
columns,
pageCount: Math.ceil(filteredModules.length / pagination.pageSize),
state: {
pagination,
sorting,
},
onPaginationChange: setPagination,
onSortingChange: setSorting,
getRowId: (row) => row.id,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<DataGrid
table={table}
recordCount={filteredModules.length}
emptyMessage={EMPTY_MODULE_MESSAGE}
tableLayout={{
dense: true,
rowBorder: true,
headerSticky: false,
columnsVisibility: false,
columnsResizable: false,
columnsMovable: false,
width: "fixed",
}}
tableClassNames={{
bodyRow: "group/module-row [&>td]:h-16",
}}
>
<div className="flex w-full max-w-6xl flex-col">
<div className="flex flex-col gap-3 border-b px-0 py-3 lg:min-h-14 lg:flex-row lg:items-center lg:gap-4 lg:py-0">
<div className="flex min-w-0 items-center gap-2">
<FlagIcon className="size-3.5 shrink-0 fill-amber-400 text-amber-400" aria-hidden="true" />
<span className="text-muted-foreground truncate text-sm">ReUI</span>
<ChevronRightIcon className="text-muted-foreground size-3.5 shrink-0" aria-hidden="true" />
<PackageIcon className="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
<h2 className="text-foreground truncate text-sm font-medium">
Modules
</h2>
</div>
<div className="flex min-w-0 flex-wrap items-center gap-2 lg:ml-auto lg:flex-nowrap">
<InputGroup className="w-full min-w-40 sm:w-48">
<InputGroupAddon align="inline-start">
<SearchIcon className="text-muted-foreground size-4" aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
value={searchQuery}
onChange={(event) => handleSearchChange(event.target.value)}
placeholder="Search..."
aria-label="Search modules"
/>
{searchQuery.length > 0 ? (
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label="Clear search"
onClick={() => handleSearchChange("")}
>
<XIcon className="size-4" aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
) : null}
</InputGroup>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
render={
<Button type="button" variant="outline">
<ArrowUpDownIcon data-icon="inline-start" aria-hidden="true" />
{sortLabels[sortBy]}
<ChevronDownIcon data-icon="inline-end" aria-hidden="true" />
</Button>
}
/>
<DropdownMenuContent align="end" className="min-w-44">
<DropdownMenuGroup>
{(["name", "dateStart", "progress", "status"] as const).map(
(value) => (
<DropdownMenuItem
key={value}
onClick={() => handleSortChange(value)}
>
{sortLabels[value]}
</DropdownMenuItem>
)
)}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
render={
<Button type="button" variant="outline">
<FilterIcon data-icon="inline-start" aria-hidden="true" />
Filters
{activeFilterCount > 0 ? (
<Badge variant="outline" radius="full">
{activeFilterCount}
</Badge>
) : null}
</Button>
}
/>
<DropdownMenuContent align="end" className="min-w-48">
<DropdownMenuGroup>
<DropdownMenuLabel>Status</DropdownMenuLabel>
{MODULE_STATUS_OPTIONS.map((status) => (
<DropdownMenuCheckboxItem
key={status}
checked={selectedStatuses.includes(status)}
closeOnClick={false}
onCheckedChange={(checked) =>
handleStatusToggle(status, checked === true)
}
>
{status}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
{activeFilterCount > 0 ? (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
closeOnClick={false}
onClick={() => {
setSelectedStatuses([])
resetPagination()
}}
>
Reset filters
</DropdownMenuItem>
</>
) : null}
</DropdownMenuContent>
</DropdownMenu>
<Button type="button" onClick={handleAddModule}>
Add Module
</Button>
</div>
</div>
{filteredModules.length > 0 ? (
<DataGridScrollArea>
<DataGridTable />
</DataGridScrollArea>
) : (
<>
<DataGridScrollArea>
<DataGridTableHeader />
</DataGridScrollArea>
<div className="text-muted-foreground flex min-h-48 w-full items-center justify-center px-4 text-center text-sm">
{EMPTY_MODULE_MESSAGE}
</div>
</>
)}
<div className="border-t px-0 py-3">
{filteredModules.length > 0 ? (
<DataGridPagination
sizes={[10, 15, 20]}
info="{from} - {to} of {count} modules"
className="py-0"
/>
) : (
<p className="text-muted-foreground text-center text-sm">
0 modules
</p>
)}
</div>
</div>
</DataGrid>
)
}
@@ -0,0 +1,490 @@
export type ModuleStatus = "Planned" | "Backlog" | "In Progress"
export type ModuleKind = "System" | "Feature" | "Area"
export type ModuleHealth = "On Track" | "Watch" | "Blocked"
export interface ModuleOwner {
name: string
initials: string
role: string
avatar?: string
}
export interface ModuleRecord {
id: string
name: string
kind: ModuleKind
owner: ModuleOwner
domain: string
progress: number
tasksCompleted: number
tasksTotal: number
contributors: number
blockers: number
health: ModuleHealth
dateStart: string
dateEnd: string
dateRange: string
status: ModuleStatus
favorite: boolean
}
export const MODULE_STATUS_OPTIONS: ModuleStatus[] = [
"Planned",
"Backlog",
"In Progress",
]
const moduleOwners = {
maya: {
name: "Nora Vale",
initials: "NV",
role: "ReUI release lead",
avatar:
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=96&h=96&dpr=2&q=80",
},
jonah: {
name: "Jonah Lee",
initials: "JL",
role: "ReUI product ops",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
},
nina: {
name: "Nina Santos",
initials: "NS",
role: "Docs owner",
avatar:
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=96&h=96&dpr=2&q=80",
},
elijah: {
name: "Elijah Morgan",
initials: "EM",
role: "License lead",
avatar:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=96&h=96&dpr=2&q=80",
},
priya: {
name: "Priya Shah",
initials: "PS",
role: "Lifecycle PM",
avatar:
"https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?w=96&h=96&dpr=2&q=80",
},
omar: {
name: "Omar Haddad",
initials: "OH",
role: "Trust owner",
avatar:
"https://images.unsplash.com/photo-1507591064344-4c6ce005b128?w=96&h=96&dpr=2&q=80",
},
sofia: {
name: "Sofia Romero",
initials: "SR",
role: "Content lead",
avatar:
"https://images.unsplash.com/photo-1517841905240-472988babdf9?w=96&h=96&dpr=2&q=80",
},
kenji: {
name: "Kenji Tan",
initials: "KT",
role: "Platform lead",
avatar:
"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=96&h=96&dpr=2&q=80",
},
lena: {
name: "Lena Wade",
initials: "LW",
role: "Developer tools",
},
} satisfies Record<string, ModuleOwner>
export const MODULE_RECORDS: ModuleRecord[] = [
{
id: "core-workflow",
name: "Registry Sync",
kind: "System",
owner: moduleOwners.maya,
domain: "registry.reui.io",
progress: 25,
tasksCompleted: 8,
tasksTotal: 32,
contributors: 6,
blockers: 0,
health: "On Track",
dateStart: "2026-04-17",
dateEnd: "2026-05-01",
dateRange: "Apr 17 - May 01, 2026",
status: "Planned",
favorite: false,
},
{
id: "onboarding-flow",
name: "Pro Onboarding",
kind: "Feature",
owner: moduleOwners.jonah,
domain: "pro.reui.io",
progress: 0,
tasksCompleted: 0,
tasksTotal: 18,
contributors: 4,
blockers: 0,
health: "Watch",
dateStart: "2026-04-19",
dateEnd: "2026-05-03",
dateRange: "Apr 19 - May 03, 2026",
status: "Backlog",
favorite: false,
},
{
id: "workspace-setup",
name: "Docs Portal",
kind: "Area",
owner: moduleOwners.nina,
domain: "docs.reui.io",
progress: 0,
tasksCompleted: 2,
tasksTotal: 14,
contributors: 3,
blockers: 1,
health: "Blocked",
dateStart: "2026-04-21",
dateEnd: "2026-05-05",
dateRange: "Apr 21 - May 05, 2026",
status: "In Progress",
favorite: false,
},
{
id: "permission-matrix",
name: "Access Matrix",
kind: "System",
owner: moduleOwners.maya,
domain: "admin.reui.io",
progress: 42,
tasksCompleted: 11,
tasksTotal: 26,
contributors: 5,
blockers: 0,
health: "Watch",
dateStart: "2026-04-22",
dateEnd: "2026-05-06",
dateRange: "Apr 22 - May 06, 2026",
status: "In Progress",
favorite: true,
},
{
id: "billing-rules",
name: "License Billing",
kind: "Feature",
owner: moduleOwners.elijah,
domain: "billing.reui.io",
progress: 64,
tasksCompleted: 21,
tasksTotal: 33,
contributors: 7,
blockers: 0,
health: "On Track",
dateStart: "2026-04-18",
dateEnd: "2026-05-02",
dateRange: "Apr 18 - May 02, 2026",
status: "In Progress",
favorite: false,
},
{
id: "notification-center",
name: "Release Notes",
kind: "Area",
owner: moduleOwners.priya,
domain: "changelog.reui.io",
progress: 18,
tasksCompleted: 5,
tasksTotal: 28,
contributors: 4,
blockers: 2,
health: "Blocked",
dateStart: "2026-04-23",
dateEnd: "2026-05-09",
dateRange: "Apr 23 - May 09, 2026",
status: "Backlog",
favorite: false,
},
{
id: "audit-trail",
name: "Trust Audit",
kind: "System",
owner: moduleOwners.omar,
domain: "trust.reui.io",
progress: 76,
tasksCompleted: 19,
tasksTotal: 25,
contributors: 5,
blockers: 0,
health: "On Track",
dateStart: "2026-04-15",
dateEnd: "2026-04-30",
dateRange: "Apr 15 - Apr 30, 2026",
status: "In Progress",
favorite: true,
},
{
id: "template-library",
name: "Block Library",
kind: "Feature",
owner: moduleOwners.sofia,
domain: "blocks.reui.io",
progress: 33,
tasksCompleted: 10,
tasksTotal: 30,
contributors: 6,
blockers: 0,
health: "Watch",
dateStart: "2026-04-24",
dateEnd: "2026-05-10",
dateRange: "Apr 24 - May 10, 2026",
status: "Planned",
favorite: false,
},
{
id: "integration-hub",
name: "Integration Hub",
kind: "Area",
owner: moduleOwners.kenji,
domain: "integrations.reui.io",
progress: 58,
tasksCompleted: 14,
tasksTotal: 24,
contributors: 8,
blockers: 1,
health: "Watch",
dateStart: "2026-04-20",
dateEnd: "2026-05-04",
dateRange: "Apr 20 - May 04, 2026",
status: "In Progress",
favorite: false,
},
{
id: "api-console",
name: "API Console",
kind: "Feature",
owner: moduleOwners.lena,
domain: "api.reui.io",
progress: 91,
tasksCompleted: 29,
tasksTotal: 32,
contributors: 4,
blockers: 0,
health: "On Track",
dateStart: "2026-04-12",
dateEnd: "2026-04-26",
dateRange: "Apr 12 - Apr 26, 2026",
status: "In Progress",
favorite: true,
},
{
id: "role-automation",
name: "Role Automation",
kind: "System",
owner: moduleOwners.maya,
domain: "admin.reui.io",
progress: 12,
tasksCompleted: 3,
tasksTotal: 25,
contributors: 3,
blockers: 1,
health: "Blocked",
dateStart: "2026-04-25",
dateEnd: "2026-05-12",
dateRange: "Apr 25 - May 12, 2026",
status: "Backlog",
favorite: false,
},
{
id: "workspace-invites",
name: "Team Invites",
kind: "Feature",
owner: moduleOwners.jonah,
domain: "teams.reui.io",
progress: 47,
tasksCompleted: 15,
tasksTotal: 32,
contributors: 5,
blockers: 0,
health: "Watch",
dateStart: "2026-04-19",
dateEnd: "2026-05-06",
dateRange: "Apr 19 - May 06, 2026",
status: "Planned",
favorite: false,
},
{
id: "release-checklist",
name: "Release Checklist",
kind: "Area",
owner: moduleOwners.nina,
domain: "release.reui.io",
progress: 84,
tasksCompleted: 26,
tasksTotal: 31,
contributors: 7,
blockers: 0,
health: "On Track",
dateStart: "2026-04-16",
dateEnd: "2026-05-01",
dateRange: "Apr 16 - May 01, 2026",
status: "In Progress",
favorite: false,
},
{
id: "reporting-digest",
name: "Usage Digest",
kind: "Feature",
owner: moduleOwners.priya,
domain: "reports.reui.io",
progress: 5,
tasksCompleted: 2,
tasksTotal: 38,
contributors: 3,
blockers: 0,
health: "Watch",
dateStart: "2026-04-28",
dateEnd: "2026-05-16",
dateRange: "Apr 28 - May 16, 2026",
status: "Backlog",
favorite: false,
},
{
id: "security-review",
name: "Security Review",
kind: "System",
owner: moduleOwners.omar,
domain: "trust.reui.io",
progress: 69,
tasksCompleted: 18,
tasksTotal: 26,
contributors: 6,
blockers: 2,
health: "Blocked",
dateStart: "2026-04-18",
dateEnd: "2026-05-07",
dateRange: "Apr 18 - May 07, 2026",
status: "In Progress",
favorite: false,
},
{
id: "help-center",
name: "Help Center",
kind: "Area",
owner: moduleOwners.sofia,
domain: "help.reui.io",
progress: 39,
tasksCompleted: 9,
tasksTotal: 23,
contributors: 4,
blockers: 0,
health: "On Track",
dateStart: "2026-04-23",
dateEnd: "2026-05-11",
dateRange: "Apr 23 - May 11, 2026",
status: "Planned",
favorite: false,
},
{
id: "data-retention",
name: "Data Retention",
kind: "System",
owner: moduleOwners.kenji,
domain: "privacy.reui.io",
progress: 22,
tasksCompleted: 7,
tasksTotal: 32,
contributors: 5,
blockers: 1,
health: "Watch",
dateStart: "2026-04-27",
dateEnd: "2026-05-14",
dateRange: "Apr 27 - May 14, 2026",
status: "Backlog",
favorite: false,
},
]
export const revenueData = [
{ value: 1000 },
{ value: 4500 },
{ value: 2000 },
{ value: 5200 },
{ value: 1500 },
{ value: 6100 },
{ value: 3000 },
{ value: 6800 },
{ value: 2000 },
{ value: 1000 },
{ value: 4000 },
{ value: 2000 },
{ value: 3000 },
{ value: 2000 },
{ value: 6238 },
]
export const customersData = [
{ value: 2000 },
{ value: 4500 },
{ value: 2000 },
{ value: 5200 },
{ value: 1500 },
{ value: 5100 },
{ value: 2500 },
{ value: 6800 },
{ value: 1800 },
{ value: 1000 },
{ value: 3000 },
{ value: 2000 },
{ value: 2700 },
{ value: 2000 },
{ value: 4238 },
]
export const activeUsersData = [
{ value: 2000 },
{ value: 3500 },
{ value: 2000 },
{ value: 5200 },
{ value: 1200 },
{ value: 4100 },
{ value: 3500 },
{ value: 5800 },
{ value: 2000 },
{ value: 800 },
{ value: 3000 },
{ value: 1000 },
{ value: 4000 },
{ value: 2000 },
{ value: 4238 },
]
export type TeamMember = {
src: string
name: string
initials: string
}
export const TEAM_MEMBERS: TeamMember[] = [
{
src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
name: "Mira Stone",
initials: "MS",
},
{
src: "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
name: "Alex Johnson",
initials: "AJ",
},
{
src: "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
name: "Sarah Chen",
initials: "SC",
},
]
export const TEAM_EXTRA_COUNT = 8
@@ -0,0 +1,67 @@
import { toast } from "sonner"
import { Button } from "@evobgp/ui/components/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@evobgp/ui/components/dropdown-menu"
import { PlusIcon, MoreHorizontalIcon, CopyIcon, Share2Icon, DownloadIcon, SettingsIcon } from "lucide-react"
// Navbar actions with a primary module action and overflow menu.
export function NavbarActions() {
const handleAddModule = () => {
toast.success("Add Module", {
description: "Open your module creation dialog.",
})
}
return (
<div className="flex shrink-0 items-center gap-2">
<Button type="button" onClick={handleAddModule}>
<PlusIcon data-icon="inline-start" aria-hidden="true" />
<span className="sr-only md:not-sr-only">Add Module</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="ghost" size="icon" aria-label="More options" />
}
>
<MoreHorizontalIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={7} align="end">
<DropdownMenuGroup>
<DropdownMenuItem>
<CopyIcon aria-hidden="true" />
Copy link
</DropdownMenuItem>
<DropdownMenuItem>
<Share2Icon aria-hidden="true" />
Share
</DropdownMenuItem>
<DropdownMenuItem>
<DownloadIcon aria-hidden="true" />
Export
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<SettingsIcon aria-hidden="true" />
Settings
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
@@ -0,0 +1,35 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@evobgp/ui/components/breadcrumb"
import { HouseIcon, LayoutDashboardIcon } from "lucide-react"
// Navbar breadcrumb
export function NavbarBreadcrumb() {
return (
<Breadcrumb>
{/* List */}
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#" className="flex items-center md:gap-1.5">
<HouseIcon className="text-muted-foreground size-3.5" aria-hidden="true" />
<span className="hidden md:block">Home</span>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="#" className="flex items-center gap-1.5">
<LayoutDashboardIcon className="text-muted-foreground size-3.5" aria-hidden="true" />
<span className="hidden md:block">ReUI</span>
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -0,0 +1,77 @@
import { useState } from "react"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@evobgp/ui/components/avatar"
import { Button } from "@evobgp/ui/components/button"
import { Input } from "@evobgp/ui/components/input"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@evobgp/ui/components/popover"
import { Separator } from "@evobgp/ui/components/separator"
import { TEAM_EXTRA_COUNT, TEAM_MEMBERS } from "./data"
import { UserPlusIcon } from "lucide-react"
// Navbar presence with team avatars and invite
export function NavbarPresence() {
const [email, setEmail] = useState("")
const [open, setOpen] = useState(false)
const handleInvite = () => {
if (!email.trim()) return
setEmail("")
setOpen(false)
}
return (
<div className="flex shrink-0 items-center gap-2">
{/* List */}
<AvatarGroup>
{TEAM_MEMBERS.map((member, index) => (
<Avatar key={index} size="sm">
<AvatarImage src={member.src} alt={member.name} />
<AvatarFallback className="text-[8px]">
{member.initials}
</AvatarFallback>
</Avatar>
))}
<AvatarGroupCount>+{TEAM_EXTRA_COUNT}</AvatarGroupCount>
</AvatarGroup>
<Separator orientation="vertical" className="my-auto h-4" />
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
render={<Button variant="outline" aria-label="Invite team member" />}
>
<UserPlusIcon aria-hidden="true" />
<span className="hidden md:block">Invite</span>
</PopoverTrigger>
<PopoverContent sideOffset={7} align="end" className="w-72">
<div className="flex flex-col gap-3">
<h4 className="text-foreground text-sm">Invite team member</h4>
<Input
type="email"
placeholder="name@reui.io"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleInvite()}
/>
<Button onClick={handleInvite} disabled={!email.trim()}>
Send invite
</Button>
</div>
</PopoverContent>
</Popover>
</div>
)
}
@@ -0,0 +1,20 @@
import { NavbarActions } from "./navbar-actions"
import { NavbarBreadcrumb } from "./navbar-breadcrumb"
import { NavbarPresence } from "./navbar-presence"
// Navbar with breadcrumb, team presence, and actions
export function Navbar() {
return (
<header className="border-border bg-background sticky top-0 z-20 flex h-12 w-full shrink-0 items-center justify-between gap-2 border-b px-4">
{/* Left - breadcrumb */}
<NavbarBreadcrumb />
{/* Right - team presence + actions */}
<div className="flex shrink-0 items-center gap-2">
<NavbarPresence />
<NavbarActions />
</div>
</header>
)
}
@@ -0,0 +1,5 @@
import { Dashboard } from "./components/dashboard"
export function Page() {
return <Dashboard />
}
@@ -0,0 +1,90 @@
import { useEffect, useRef } from "react"
import { cn } from "@evobgp/ui/lib/utils"
// Deterministic per-dot value so the field reads as noise, not a flat grid.
function grain(i: number, j: number) {
const n = Math.sin(i * 127.1 + j * 311.7) * 43758.5453
return n - Math.floor(n)
}
/**
* Static dot field adapted from card-5's reviewed background effect.
* The dots resolve from the current text color, so the surface stays
* neutral and theme-aware without hard-coded colors.
*/
export function CardDotField({ className }: { className?: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
const GAP = 3
const DOT = 1.5
const BASE = 0.03
const PEAK = 0.2
const draw = () => {
const rect = canvas.getBoundingClientRect()
if (!rect.width || !rect.height) return
const dpr = Math.min(window.devicePixelRatio || 1, 2)
canvas.width = Math.round(rect.width * dpr)
canvas.height = Math.round(rect.height * dpr)
ctx.fillStyle = getComputedStyle(canvas).color || "rgb(115,115,115)"
ctx.fillRect(0, 0, 1, 1)
const px = ctx.getImageData(0, 0, 1, 1).data
const color = `rgb(${px[0]}, ${px[1]}, ${px[2]})`
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.clearRect(0, 0, rect.width, rect.height)
ctx.fillStyle = color
const cols = Math.ceil(rect.width / GAP) + 1
const rows = Math.ceil(rect.height / GAP) + 1
for (let i = 0; i < cols; i++) {
const x = i * GAP
for (let j = 0; j < rows; j++) {
const q = grain(i, j)
const amp = 0.7 + 0.6 * grain(j * 2 + 1, i * 2 + 1)
let a = (BASE + (PEAK - BASE) * q * q) * amp
if (a > 1) a = 1
ctx.globalAlpha = a
ctx.fillRect(x, j * GAP, DOT, DOT)
}
}
ctx.globalAlpha = 1
}
draw()
const resizeObserver = new ResizeObserver(() => draw())
resizeObserver.observe(canvas)
const themeObserver = new MutationObserver(() => draw())
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style"],
})
return () => {
resizeObserver.disconnect()
themeObserver.disconnect()
}
}, [])
return (
<canvas
ref={canvasRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full",
className
)}
/>
)
}
@@ -0,0 +1,51 @@
import { Card, CardContent } from "@evobgp/ui/components/card"
type TooltipItem = {
value?: unknown
name?: unknown
color?: string
}
export function ChartTooltip({
active,
payload,
label,
}: {
active?: boolean
payload?: readonly TooltipItem[]
label?: string | number
}) {
if (!active || !payload?.length) return null
return (
<Card className="bg-popover text-popover-foreground pointer-events-none p-0 shadow-md">
<CardContent className="min-w-28 space-y-1.5 px-2 py-1.5">
<div className="text-muted-foreground text-[10px] leading-none">
{label}
</div>
<div className="space-y-1">
{payload.map((item, index) => (
<div
key={`${String(item.name)}-${index}`}
className="flex items-center justify-between gap-3 text-xs leading-none"
>
<span className="flex items-center gap-1.5">
<span
className="size-1.5 rounded-full"
style={{ backgroundColor: item.color }}
aria-hidden="true"
/>
<span className="text-muted-foreground">
{String(item.name)}
</span>
</span>
<span className="font-semibold tabular-nums">
{Number(item.value ?? 0).toLocaleString()}
</span>
</div>
))}
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,200 @@
import { type ComponentProps } from "react"
import { Badge } from "@/components/reui/badge"
export type MetricTone = "danger" | "success" | "warning" | "info"
export type MetricCard = {
id: string
title: string
label: string
value: string
delta: string
deltaVariant: ComponentProps<typeof Badge>["variant"]
detail: string
tone: MetricTone
sparkline: number[]
}
export const metricCards: MetricCard[] = [
{
id: "signal-risk",
title: "Signal Risk",
label: "Threat Index",
value: "Elevated",
delta: "+12.5%",
deltaVariant: "destructive-light",
detail: "5 cases",
tone: "danger",
sparkline: [18, 21, 15, 33, 29, 35, 28, 31, 19],
},
{
id: "mesh-uptime",
title: "Mesh Uptime",
label: "Service Health",
value: "99.98%",
delta: "+0.2%",
deltaVariant: "success-light",
detail: "58 zones",
tone: "success",
sparkline: [42, 40, 42, 38, 39, 41, 40, 43, 46],
},
{
id: "edge-traffic",
title: "Edge Traffic",
label: "Scrubbed Load",
value: "4.8 GB/s",
delta: "-3.1%",
deltaVariant: "warning-light",
detail: "clean flow",
tone: "warning",
sparkline: [21, 27, 28, 34, 32, 35, 33, 31, 34],
},
{
id: "sensor-reach",
title: "Sensor Reach",
label: "Global Nodes",
value: "18,420",
delta: "+6.8%",
deltaVariant: "success-light",
detail: "93 regions",
tone: "success",
sparkline: [25, 23, 29, 28, 26, 31, 27, 30, 29],
},
]
export const threatVectors = [
{ name: "Bot", blocked: 34, watched: 62 },
{ name: "Phish", blocked: 30, watched: 74 },
{ name: "DDoS", blocked: 52, watched: 33 },
{ name: "Inject", blocked: 22, watched: 48 },
{ name: "Auth", blocked: 43, watched: 68 },
{ name: "Probe", blocked: 18, watched: 58 },
{ name: "Exfil", blocked: 56, watched: 35 },
{ name: "Beacon", blocked: 38, watched: 64 },
{ name: "Day0", blocked: 14, watched: 28 },
]
export const networkFlow = [
{ month: "January", api: 1820, webhook: 1640 },
{ month: "February", api: 2340, webhook: 2160 },
{ month: "March", api: 1960, webhook: 1880 },
{ month: "April", api: 2780, webhook: 2540 },
{ month: "May", api: 2100, webhook: 1920 },
{ month: "June", api: 3120, webhook: 2880 },
{ month: "July", api: 2540, webhook: 2320 },
{ month: "August", api: 3480, webhook: 3160 },
{ month: "September", api: 2860, webhook: 2580 },
{ month: "October", api: 2420, webhook: 2140 },
{ month: "November", api: 3240, webhook: 2960 },
{ month: "December", api: 2680, webhook: 2440 },
]
export const networkFlowSummary = {
change: "+12.8%",
year: "2026",
}
const loadSamples = [
72, 68, 22, 18, 61, 71, 20, 26, 31, 70, 46, 88, 39, 25, 12, 33, 18, 28, 42,
36, 61, 68, 74, 70, 91, 32, 82, 66, 52, 76, 48, 35, 70, 62, 57, 49, 37, 58,
71, 7, 11, 69, 34, 28, 40, 61, 17, 55, 64, 19, 63, 67,
] as const
export type LoadState = "nominal" | "warm" | "critical"
export type LoadPoint = {
node: string
load: number
state: LoadState
}
export const loadDistribution: LoadPoint[] = loadSamples.map((load, index) => ({
node: `N${String(index).padStart(2, "0")}`,
load,
state: load >= 86 ? "critical" : load >= 76 ? "warm" : "nominal",
}))
const loadStateTotals = loadDistribution.reduce<Record<LoadState, number>>(
(totals, point) => ({
...totals,
[point.state]: totals[point.state] + 1,
}),
{
nominal: 0,
warm: 0,
critical: 0,
}
)
export const clusterLoadSummary = {
details: [
{
label: "Nodes",
value: String(loadDistribution.length),
variant: "secondary",
},
{
label: "Warm",
value: String(loadStateTotals.warm),
variant: "warning-light",
},
{
label: "Critical",
value: String(loadStateTotals.critical),
variant: "destructive-light",
},
],
} satisfies {
details: Array<{
label: string
value: string
variant: ComponentProps<typeof Badge>["variant"]
}>
}
export const activeThreats = [
{
id: "00",
label: "API Flood",
source: "Edge WAF",
state: "Mitigating",
value: 4521,
progress: 92,
tone: "danger",
},
{
id: "01",
label: "Mail Spoof",
source: "Mail Relay",
state: "Reviewing",
value: 3102,
progress: 64,
tone: "warning",
},
{
id: "02",
label: "Cloud Probe",
source: "Cloud API",
state: "Queued",
value: 1250,
progress: 26,
tone: "info",
},
{
id: "03",
label: "Mesh Beacon",
source: "Int Node",
state: "Watching",
value: 420,
progress: 9,
tone: "success",
},
] satisfies Array<{
id: string
label: string
source: string
state: string
value: number
progress: number
tone: MetricTone
}>
@@ -0,0 +1,182 @@
import { Badge } from "@/components/reui/badge"
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts"
import { cn } from "@evobgp/ui/lib/utils"
import { Card, CardContent } from "@evobgp/ui/components/card"
import { ChartTooltip } from "./chart-tooltip"
import { activeThreats, clusterLoadSummary, loadDistribution } from "./data"
import { PanelCorners, PanelHeading } from "./panel-heading"
import { loadStateColor, toneStyles } from "./tone-styles"
const chartGridProps = {
vertical: false,
stroke: "var(--border)",
strokeDasharray: "3 3",
strokeOpacity: 0.75,
}
export function LoadPanel() {
return (
<Card className="relative h-full overflow-hidden p-0">
<PanelCorners />
<CardContent className="flex h-full flex-col p-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-0.5">
<h2 className="text-sm leading-4 font-semibold">Cluster Load</h2>
<div className="text-muted-foreground flex flex-wrap items-center gap-3 text-xs leading-4">
<span className="flex items-center gap-1.5">
<span
className="size-2 rounded-full bg-zinc-300"
aria-hidden="true"
/>
Normal
</span>
<span className="flex items-center gap-1.5">
<span
className="size-2 rounded-full bg-amber-500"
aria-hidden="true"
/>
Warm
</span>
<span className="flex items-center gap-1.5">
<span
className="size-2 rounded-full bg-red-500"
aria-hidden="true"
/>
Critical
</span>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
{clusterLoadSummary.details.map((detail) => (
<Badge
key={detail.label}
variant={detail.variant}
radius="full"
className="h-6 gap-1.5 px-2 text-xs"
>
<span className="font-semibold tabular-nums">
{detail.value}
</span>
<span>{detail.label}</span>
</Badge>
))}
</div>
</div>
<div className="mt-auto h-60 pt-5">
<ResponsiveContainer width="100%" height="100%">
<BarChart
accessibilityLayer
data={loadDistribution}
margin={{ top: 10, right: 0, bottom: 0, left: 0 }}
barCategoryGap={2}
>
<CartesianGrid {...chartGridProps} />
<XAxis
dataKey="node"
axisLine={false}
interval={25}
tickLine={false}
tickMargin={10}
tick={{ fontSize: 10 }}
/>
<YAxis hide domain={[0, 100]} />
<Tooltip
cursor={{ fill: "var(--muted)" }}
content={({ active, payload, label }) => (
<ChartTooltip
active={active}
payload={payload}
label={label}
/>
)}
/>
<Bar
dataKey="load"
name="Load"
radius={[5, 5, 5, 5]}
isAnimationActive={false}
>
{loadDistribution.map((entry) => (
<Cell
key={entry.node}
fill={loadStateColor[entry.state]}
opacity={entry.state === "nominal" ? 0.62 : 1}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)
}
export function ActiveThreatsPanel() {
return (
<Card className="relative overflow-hidden p-0">
<PanelCorners />
<CardContent className="space-y-5 p-4">
<PanelHeading title="Active Lanes" description="Threat Queue" />
<div className="space-y-4">
{activeThreats.map((threat) => {
const tone = toneStyles[threat.tone]
return (
<div key={threat.id} className="space-y-2.5">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-1">
<div className="flex min-w-0 items-center gap-2">
<span
className={cn("size-2 shrink-0 rounded-full", tone.dot)}
aria-hidden="true"
/>
<span className="truncate text-sm font-medium">
[{threat.id}] {threat.label}
</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center gap-1.5 text-xs">
<span>{threat.source}</span>
<span
aria-hidden="true"
className="bg-muted-foreground/40 size-1 shrink-0 rounded-full"
/>
<span>{threat.state}</span>
</div>
</div>
<div className="text-right text-sm font-semibold tabular-nums">
{threat.value.toLocaleString()}
</div>
</div>
<div className="flex items-center gap-2">
<div className="bg-muted h-1.5 min-w-0 flex-1 overflow-hidden rounded-full">
<div
className={cn("h-full rounded-full", tone.bar)}
style={{ width: `${threat.progress}%` }}
/>
</div>
<span className="text-muted-foreground w-8 text-right text-[10px] leading-none tabular-nums">
{threat.progress}%
</span>
</div>
</div>
)
})}
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,83 @@
import { Badge } from "@/components/reui/badge"
import { Card, CardContent } from "@evobgp/ui/components/card"
import { CardDotField } from "./card-dot-field"
import { type MetricCard } from "./data"
import { PanelCorners } from "./panel-heading"
import { toneStyles } from "./tone-styles"
function Sparkline({
values,
color,
}: {
values: readonly number[]
color: string
}) {
const min = Math.min(...values)
const max = Math.max(...values)
const spread = Math.max(1, max - min)
const points = values
.map((value, index) => {
const x = (index / (values.length - 1)) * 72
const y = 28 - ((value - min) / spread) * 22
return `${x.toFixed(1)},${y.toFixed(1)}`
})
.join(" ")
return (
<svg
viewBox="0 0 72 32"
className="h-9 w-24 shrink-0 opacity-95"
role="img"
aria-label="Metric trend"
>
<polyline
points={points}
fill="none"
stroke={color}
strokeWidth="2"
strokeLinecap="square"
strokeLinejoin="miter"
/>
</svg>
)
}
export function MetricTile({ metric }: { metric: MetricCard }) {
const tone = toneStyles[metric.tone]
return (
<Card className="relative overflow-hidden p-0">
<CardDotField className="text-muted-foreground [mask-image:radial-gradient(72%_64%_at_50%_44%,black,transparent)] opacity-70" />
<PanelCorners />
<CardContent className="relative z-10 flex min-h-[7.25rem] flex-col justify-between gap-5 p-4">
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-foreground truncate text-sm leading-4 font-semibold">
{metric.title}
</span>
<span className="text-muted-foreground truncate text-xs leading-4">
{metric.label}
</span>
</div>
<div className="flex items-end justify-between gap-4">
<div className="min-w-0 space-y-2.5">
<div className="text-foreground text-2xl leading-none font-semibold tracking-tight">
{metric.value}
</div>
<div className="flex items-center gap-2">
<Badge variant={metric.deltaVariant} radius="full">
{metric.delta}
</Badge>
<span className="text-muted-foreground text-xs">
{metric.detail}
</span>
</div>
</div>
<Sparkline values={metric.sparkline} color={tone.stroke} />
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,248 @@
import { useState } from "react"
import { format } from "date-fns"
import { type DateRange } from "react-day-picker"
import { toast } from "sonner"
import { Button } from "@evobgp/ui/components/button"
import { Calendar } from "@evobgp/ui/components/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@evobgp/ui/components/popover"
import { CalendarIcon, ShieldCheckIcon, DownloadIcon } from "lucide-react"
type PeriodKey = "last30" | "prev30"
type ReportDateRange = {
from: Date
to: Date
}
type DateRangePreset = {
id: string
label: string
period: PeriodKey
range: ReportDateRange
}
const reportRange = (
fromMonth: number,
fromDay: number,
toMonth: number,
toDay: number,
year = 2026
): ReportDateRange => ({
from: new Date(year, fromMonth, fromDay),
to: new Date(year, toMonth, toDay),
})
const preset = (
id: string,
label: string,
period: PeriodKey,
range: ReportDateRange
): DateRangePreset => ({ id, label, period, range })
const LAST_30_RANGE = reportRange(4, 12, 5, 10)
const PREVIOUS_30_RANGE = reportRange(3, 12, 4, 11)
const REPORT_RANGE_PRESETS: DateRangePreset[] = [
preset("today", "Today", "last30", reportRange(5, 10, 5, 10)),
preset("yesterday", "Yesterday", "last30", reportRange(5, 9, 5, 9)),
preset("last7", "Last 7 days", "last30", reportRange(5, 4, 5, 10)),
preset("last30", "Last 30 days", "last30", LAST_30_RANGE),
preset("monthToDate", "Month to date", "last30", reportRange(5, 1, 5, 10)),
preset("lastMonth", "Last month", "last30", reportRange(4, 1, 4, 31)),
preset("yearToDate", "Year to date", "last30", reportRange(0, 1, 5, 10)),
preset("lastYear", "Last year", "prev30", reportRange(0, 1, 11, 31, 2025)),
]
const MAX_REPORT_DATE = LAST_30_RANGE.to
function isSameRange(first: ReportDateRange, second: DateRange) {
const secondFrom = second.from
const secondTo = second.to ?? second.from
return (
Boolean(secondFrom && secondTo) &&
first.from.getTime() === secondFrom?.getTime() &&
first.to.getTime() === secondTo?.getTime()
)
}
function normalizeRange(
range: DateRange | undefined,
fallback: ReportDateRange
): ReportDateRange {
if (!range?.from) return fallback
const from = range.from
const to = range.to ?? range.from
return from.getTime() <= to.getTime() ? { from, to } : { from: to, to: from }
}
function formatReportRange(range: ReportDateRange) {
return `${format(range.from, "MMM d, yyyy")} - ${format(range.to, "MMM d, yyyy")}`
}
function getPeriodForRange(range: ReportDateRange) {
const matchingPreset = getMatchingPreset(range)
if (matchingPreset) return matchingPreset.period
return range.to.getTime() <= PREVIOUS_30_RANGE.to.getTime()
? "prev30"
: "last30"
}
function getMatchingPreset(range: DateRange | undefined) {
if (!range?.from || !range.to) return undefined
const normalizedRange = normalizeRange(range, LAST_30_RANGE)
return REPORT_RANGE_PRESETS.find((preset) =>
isSameRange(preset.range, normalizedRange)
)
}
function ReportDateRangePicker({
period,
onPeriodChange,
}: {
period: PeriodKey
onPeriodChange: (value: PeriodKey) => void
}) {
const initialRange = period === "prev30" ? PREVIOUS_30_RANGE : LAST_30_RANGE
const [open, setOpen] = useState(false)
const [committedRange, setCommittedRange] =
useState<ReportDateRange>(initialRange)
const [draftRange, setDraftRange] = useState<DateRange | undefined>(
initialRange
)
const selectedPresetId = getMatchingPreset(draftRange ?? committedRange)?.id
function handleOpenChange(nextOpen: boolean) {
if (nextOpen) {
setDraftRange(committedRange)
}
setOpen(nextOpen)
}
function handleApply() {
const nextRange = normalizeRange(draftRange, committedRange)
setCommittedRange(nextRange)
onPeriodChange(getPeriodForRange(nextRange))
setOpen(false)
}
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger
render={
<Button
type="button"
size="sm"
variant="outline"
className="group/pick-date w-[250px] max-w-full justify-between leading-none font-normal tabular-nums"
>
<span className="truncate">
{formatReportRange(committedRange)}
</span>
<CalendarIcon className="text-muted-foreground/80 group-hover/pick-date:text-foreground shrink-0 transition-colors" aria-hidden="true" />
</Button>
}
/>
<PopoverContent align="end" className="w-auto p-0">
<div className="flex flex-col">
<div className="flex flex-col sm:grid sm:grid-cols-[10rem_1fr]">
<div className="border-border flex flex-wrap gap-1 border-b p-2 sm:flex-col sm:border-r sm:border-b-0">
{REPORT_RANGE_PRESETS.map((preset) => {
const selected = selectedPresetId === preset.id
return (
<Button
key={preset.id}
type="button"
size="sm"
variant={selected ? "secondary" : "ghost"}
className={
selected
? "justify-start"
: "text-muted-foreground justify-start"
}
onClick={() => setDraftRange(preset.range)}
>
{preset.label}
</Button>
)
})}
</div>
<Calendar
mode="range"
selected={draftRange}
onSelect={setDraftRange}
numberOfMonths={2}
defaultMonth={draftRange?.from ?? committedRange.from}
disabled={{
after: MAX_REPORT_DATE,
}}
/>
</div>
<div className="border-border flex items-center justify-between gap-2 border-t px-3 py-1.5">
<Button
type="button"
size="sm"
variant="outline"
onClick={() => setDraftRange(LAST_30_RANGE)}
>
Reset
</Button>
<div className="flex items-center gap-2">
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => {
setDraftRange(committedRange)
setOpen(false)
}}
>
Cancel
</Button>
<Button type="button" size="sm" onClick={handleApply}>
Apply
</Button>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)
}
export function NavbarActions() {
const [periodKey, setPeriodKey] = useState<PeriodKey>("last30")
function handleExport() {
toast.success("Export queued", {
description: "Security telemetry report is being prepared.",
icon: (
<ShieldCheckIcon className="size-4" aria-hidden="true" />
),
})
}
return (
<div className="flex shrink-0 items-center gap-2">
<ReportDateRangePicker period={periodKey} onPeriodChange={setPeriodKey} />
<Button size="sm" type="button" onClick={handleExport}>
<DownloadIcon aria-hidden="true" />
<span className="hidden sm:block">Export</span>
</Button>
</div>
)
}
@@ -0,0 +1,32 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@evobgp/ui/components/breadcrumb"
export function NavbarBreadcrumb() {
return (
<Breadcrumb className="min-w-0">
<BreadcrumbList className="flex-nowrap">
<BreadcrumbItem className="hidden md:inline-flex">
<BreadcrumbLink render={<a href="#" />}>Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:flex" />
<BreadcrumbItem className="hidden md:inline-flex">
<BreadcrumbLink render={<a href="#" />}>Security</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:flex" />
<BreadcrumbItem className="min-w-0">
<BreadcrumbPage className="truncate">Telemetry</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
@@ -0,0 +1,15 @@
import { NavbarActions } from "./navbar-actions"
import { NavbarBreadcrumb } from "./navbar-breadcrumb"
export function Navbar() {
return (
<header
className="flex min-h-9 w-full shrink-0 items-center justify-between gap-2 pb-1"
aria-label="Security telemetry header"
>
<NavbarBreadcrumb />
<NavbarActions />
</header>
)
}
@@ -0,0 +1,305 @@
import { Badge } from "@/components/reui/badge"
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
XAxis,
YAxis,
} from "recharts"
import { Card, CardContent } from "@evobgp/ui/components/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@evobgp/ui/components/chart"
import { networkFlow, networkFlowSummary, threatVectors } from "./data"
import { PanelCorners, PanelHeading } from "./panel-heading"
const threatChartConfig = {
blocked: {
label: "Blocked",
color: "var(--color-blue-600)",
},
watched: {
label: "Watched",
color: "var(--color-sky-300)",
},
} satisfies ChartConfig
const flowChartConfig = {
api: {
label: "API Calls",
color: "var(--color-yellow-500)",
},
webhook: {
label: "Webhooks",
color: "var(--color-emerald-500)",
},
} satisfies ChartConfig
const chartGridProps = {
vertical: false,
stroke: "var(--border)",
strokeDasharray: "3 3",
strokeOpacity: 0.75,
}
function getChartColor(config: ChartConfig, name: string | number) {
return config[String(name)]?.color
}
function getChartLabel(config: ChartConfig, name: string | number) {
return config[String(name)]?.label ?? String(name)
}
function formatTooltipItem(
config: ChartConfig,
value: unknown,
name: string | number
) {
return (
<div className="flex w-full items-center justify-between gap-3">
<div className="flex items-center gap-1.5">
<span
className="size-2 shrink-0 rounded-full"
style={{ backgroundColor: getChartColor(config, name) }}
aria-hidden="true"
/>
<span className="text-muted-foreground">
{getChartLabel(config, name)}
</span>
</div>
<span className="text-foreground font-semibold tabular-nums">
{Number(value ?? 0).toLocaleString()}
</span>
</div>
)
}
function CrosshatchPattern({
config,
idPrefix,
}: {
config: ChartConfig
idPrefix: string
}) {
const entries = Object.entries(config).filter(([, value]) => value.color)
return (
<>
{entries.map(([key, { color }]) => (
<pattern
key={key}
id={`${idPrefix}-${key}`}
x="0"
y="0"
width="8"
height="8"
patternUnits="userSpaceOnUse"
>
<path d="M0,8 L8,0" stroke={color} strokeWidth="0.8" opacity="0.4" />
<path d="M0,0 L8,8" stroke={color} strokeWidth="0.8" opacity="0.2" />
</pattern>
))}
</>
)
}
export function ThreatVectorsPanel() {
return (
<Card className="relative overflow-hidden p-0">
<PanelCorners />
<CardContent className="flex flex-col gap-5 p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<PanelHeading title="Threat Vectors" description="Blocked Signals" />
<div className="text-muted-foreground flex shrink-0 flex-wrap items-center gap-4 text-xs sm:justify-end">
<span className="flex items-center gap-1.5">
<span
className="size-2 rounded-full bg-blue-600"
aria-hidden="true"
/>
Blocked
</span>
<span className="flex items-center gap-1.5">
<span
className="size-2 rounded-full bg-sky-300"
aria-hidden="true"
/>
Watched
</span>
</div>
</div>
<ChartContainer
config={threatChartConfig}
className="h-56 w-full min-w-0"
>
<BarChart
accessibilityLayer
data={threatVectors}
margin={{ top: 8, right: 4, bottom: 0, left: 4 }}
barGap={3}
barCategoryGap={8}
>
<defs>
<pattern
id="dashboard5-threat-blocked"
patternUnits="userSpaceOnUse"
width="8"
height="8"
>
<rect
width="8"
height="8"
fill="var(--color-blocked)"
opacity="0.1"
/>
<path
d="M0,8 L8,0 M4,12 L12,4 M-4,4 L4,-4"
stroke="var(--color-blocked)"
strokeWidth="1.5"
opacity="0.55"
/>
<path
d="M2,10 L10,2 M6,14 L14,6 M-2,6 L6,-2"
stroke="var(--color-blocked)"
strokeWidth="1"
opacity="0.25"
/>
</pattern>
</defs>
<CartesianGrid {...chartGridProps} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tickMargin={10}
tick={{ fontSize: 10 }}
/>
<YAxis hide domain={[0, 78]} />
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
indicator="dot"
className="min-w-36 gap-2"
labelFormatter={(value) => (
<div className="border-border/50 mb-0.5 border-b pb-2">
<span className="text-xs font-medium">{value}</span>
</div>
)}
formatter={(value, name) =>
formatTooltipItem(threatChartConfig, value, name)
}
/>
}
/>
<Bar
dataKey="blocked"
fill="url(#dashboard5-threat-blocked)"
stroke="var(--color-blocked)"
strokeWidth={1}
radius={[5, 5, 5, 5]}
isAnimationActive={false}
/>
<Bar
dataKey="watched"
fill="var(--color-watched)"
fillOpacity={0.86}
stroke="var(--color-watched)"
strokeWidth={1}
radius={[5, 5, 5, 5]}
isAnimationActive={false}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
)
}
export function NetworkFlowPanel() {
return (
<Card className="relative overflow-hidden p-0">
<PanelCorners />
<CardContent className="flex flex-col gap-5 p-4">
<div className="flex items-start justify-between gap-3">
<PanelHeading title="Network Flow" description="API and webhooks" />
<Badge variant="success-light" radius="full">
{networkFlowSummary.change}
</Badge>
</div>
<ChartContainer
config={flowChartConfig}
className="h-56 w-full min-w-0"
>
<AreaChart
accessibilityLayer
data={networkFlow}
margin={{ top: 20, right: 0, bottom: 0, left: 0 }}
>
<CartesianGrid {...chartGridProps} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => String(value).slice(0, 3)}
/>
<YAxis hide />
<ChartTooltip
content={
<ChartTooltipContent
indicator="dot"
className="min-w-40 gap-2.5"
labelFormatter={(value) => (
<div className="border-border/50 mb-0.5 border-b pb-2">
<span className="text-xs font-medium">
{value} {networkFlowSummary.year}
</span>
</div>
)}
formatter={(value, name) =>
formatTooltipItem(flowChartConfig, value, name)
}
/>
}
/>
<defs>
<CrosshatchPattern
config={flowChartConfig}
idPrefix="dashboard5-flow-crosshatch"
/>
</defs>
<Area
dataKey="webhook"
type="natural"
fill="url(#dashboard5-flow-crosshatch-webhook)"
fillOpacity={0.5}
stroke="var(--color-webhook)"
stackId="a"
strokeWidth={1.25}
isAnimationActive={false}
/>
<Area
dataKey="api"
type="natural"
fill="url(#dashboard5-flow-crosshatch-api)"
fillOpacity={0.5}
stroke="var(--color-api)"
stackId="a"
strokeWidth={1.25}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}
@@ -0,0 +1,29 @@
export function PanelCorners() {
return (
<>
<span
aria-hidden="true"
className="border-foreground/65 absolute top-0 left-0 size-2 border-t border-l"
/>
<span
aria-hidden="true"
className="border-foreground/65 absolute right-0 bottom-0 size-2 border-r border-b"
/>
</>
)
}
export function PanelHeading({
title,
description,
}: {
title: string
description: string
}) {
return (
<div className="space-y-0.5">
<h2 className="text-sm leading-4 font-semibold">{title}</h2>
<p className="text-muted-foreground text-xs leading-4">{description}</p>
</div>
)
}
@@ -0,0 +1,47 @@
import { metricCards } from "./data"
import { ActiveThreatsPanel, LoadPanel } from "./load-panels"
import { MetricTile } from "./metric-tile"
import { Navbar } from "./navbar"
import { NetworkFlowPanel, ThreatVectorsPanel } from "./network-panels"
/**
* Dense edge security dashboard inspired by a telemetry wall.
* The main entry only owns section order; records and panel details stay
* in focused local files so the block remains easy to adapt.
*/
export function SecurityDashboard() {
return (
<div className="text-foreground @container mx-auto flex w-full max-w-7xl flex-col gap-4">
<h1 id="page-heading" className="sr-only">
Edge Security Telemetry
</h1>
<Navbar />
<section
aria-label="Security Summary"
className="grid grid-cols-1 gap-4 @3xl:grid-cols-2 @6xl:grid-cols-4"
>
{metricCards.map((metric) => (
<MetricTile key={metric.id} metric={metric} />
))}
</section>
<section
aria-label="Security Flow"
className="grid grid-cols-1 gap-4 @5xl:grid-cols-2"
>
<ThreatVectorsPanel />
<NetworkFlowPanel />
</section>
<section
aria-label="Security Load"
className="grid grid-cols-1 gap-4 @5xl:grid-cols-[minmax(0,2fr)_minmax(300px,1fr)]"
>
<LoadPanel />
<ActiveThreatsPanel />
</section>
</div>
)
}
@@ -0,0 +1,37 @@
import { type LoadState, type MetricTone } from "./data"
export const toneStyles: Record<
MetricTone,
{
dot: string
stroke: string
bar: string
}
> = {
danger: {
dot: "bg-red-500",
stroke: "var(--color-red-500)",
bar: "bg-red-500",
},
success: {
dot: "bg-emerald-500",
stroke: "var(--color-emerald-500)",
bar: "bg-emerald-500",
},
warning: {
dot: "bg-amber-500",
stroke: "var(--color-amber-500)",
bar: "bg-amber-500",
},
info: {
dot: "bg-blue-500",
stroke: "var(--color-blue-500)",
bar: "bg-blue-500",
},
}
export const loadStateColor: Record<LoadState, string> = {
nominal: "var(--color-zinc-300)",
warm: "var(--color-amber-500)",
critical: "var(--color-red-500)",
}
@@ -0,0 +1,12 @@
import { SecurityDashboard } from "./components/security-dashboard"
export function Page() {
return (
<main
className="bg-background min-h-svh w-full p-3 sm:p-4 lg:p-6"
aria-labelledby="page-heading"
>
<SecurityDashboard />
</main>
)
}
@@ -0,0 +1,45 @@
import { ReactNode } from "react"
import { Badge } from "@/components/reui/badge"
import { Headphones, CircleCheckIcon, SmileIcon } from "lucide-react"
// ── Types ──
export interface CardData {
icon: ReactNode
iconBg: string
value: string | number
label: string
info: ReactNode
}
// ── Data ──
export const cards: CardData[] = [
{
icon: (
<Headphones aria-hidden="true" />
),
iconBg: "text-blue-600 dark:text-blue-400",
value: 320,
label: "Support Tickets",
info: <Badge variant="info-light">12 Open, 308 Closed</Badge>,
},
{
icon: (
<CircleCheckIcon aria-hidden="true" />
),
iconBg: "text-emerald-600 dark:text-emerald-400",
value: "98%",
label: "Resolved",
info: <Badge variant="success-light">+2.1% this month</Badge>,
},
{
icon: (
<SmileIcon aria-hidden="true" />
),
iconBg: "text-amber-600 dark:text-amber-400",
value: "4.8",
label: "Satisfaction Rate",
info: <Badge variant="warning-light">Avg. (out of 5)</Badge>,
},
]
@@ -0,0 +1,45 @@
"use client"
import { Frame, FramePanel } from "@/components/reui/frame"
import { cn } from "@evobgp/ui/lib/utils"
import { Item, ItemMedia } from "@evobgp/ui/components/item"
import { cards } from "./data"
export function Stats() {
return (
<div className="@container w-full grow">
{/* Grid */}
<div className="mx-auto grid max-w-5xl grow grid-cols-1 gap-5 @3xl:grid-cols-3">
{cards.map((card, i) => (
<Frame key={i}>
<FramePanel className="flex flex-col items-start gap-6">
<Item
className={cn(
"border-background bg-muted flex size-10.5 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4",
card.iconBg
)}
>
<ItemMedia variant="icon" className="size-auto">
{card.icon}
</ItemMedia>
</Item>
<div className="space-y-0.5">
<div className="text-foreground text-2xl leading-none font-bold">
{card.value}
</div>
<div className="text-muted-foreground text-sm font-medium">
{card.label}
</div>
</div>
{card.info}
</FramePanel>
</Frame>
))}
</div>
</div>
)
}
@@ -0,0 +1,9 @@
import { Stats } from "./components/stats"
export function Page() {
return (
<div className="flex w-full items-center justify-center p-10 md:p-20">
<Stats />
</div>
)
}
@@ -0,0 +1,131 @@
export type ActivityDotBadge = {
label: string
dotClass: string
}
export type ActivityLightBadge = {
label: string
variant:
| "primary-light"
| "success-light"
| "warning-light"
| "info-light"
| "destructive-light"
}
export type FinanceActivity = {
id: number
user: string
avatar: string
action: string
target: string
detail: string
badges: [ActivityDotBadge] | [ActivityDotBadge, ActivityLightBadge]
attachment?: {
name: string
size: string
}
participants?: {
src: string
fallback: string
}[]
participantCount?: number
actions?: {
label: string
variant?: "default" | "outline"
}[]
date: string
dateTime: string
}
export const financeActivities: FinanceActivity[] = [
{
id: 1,
user: "Nadia Flores",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80",
action: "approved payout batch",
target: "ACH-4182",
detail: "$142,800 routed to 38 merchant accounts",
badges: [
{ label: "Payout", dotClass: "bg-emerald-500" },
{ label: "Same Day", variant: "success-light" },
],
date: "5 minutes ago",
dateTime: "2026-05-06T09:45:00+05:00",
},
{
id: 2,
user: "Theo Ramsey",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&dpr=2&q=80",
action: "flagged review on",
target: "Transfer TX-9041",
detail: "Velocity threshold exceeded for new payee",
badges: [
{ label: "Risk", dotClass: "bg-amber-500" },
{ label: "High", variant: "warning-light" },
],
actions: [{ label: "Review" }, { label: "Clear", variant: "outline" }],
date: "18 minutes ago",
dateTime: "2026-05-06T09:32:00+05:00",
},
{
id: 3,
user: "Iris Chen",
avatar:
"https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=96&h=96&dpr=2&q=80",
action: "reconciled ledger entry",
target: "LDG-7749",
detail: "Subscription invoice matched to bank settlement",
badges: [{ label: "Ledger", dotClass: "bg-sky-500" }],
attachment: {
name: "settlement-match.csv",
size: "48kb",
},
date: "42 minutes ago",
dateTime: "2026-05-06T09:08:00+05:00",
},
{
id: 4,
user: "Marcus Bell",
avatar:
"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=96&h=96&dpr=2&q=80",
action: "raised limit for",
target: "Northstar Workspace",
detail: "Monthly card volume increased to $850K",
badges: [{ label: "Limit Raised", dotClass: "bg-violet-500" }],
participants: [
{
src: "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
fallback: "SC",
},
{
src: "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
fallback: "MR",
},
],
participantCount: 2,
date: "1 hour ago",
dateTime: "2026-05-06T08:50:00+05:00",
},
{
id: 5,
user: "Amara Okafor",
avatar:
"https://images.unsplash.com/photo-1544723795-3fb6469f5b39?w=96&h=96&dpr=2&q=80",
action: "sent invoice reminder",
target: "INV-2098",
detail: "Net 7 renewal balance due tomorrow",
badges: [
{ label: "Invoice", dotClass: "bg-orange-500" },
{ label: "Due Tomorrow", variant: "destructive-light" },
],
attachment: {
name: "invoice-2098.pdf",
size: "76kb",
},
date: "2 hours ago",
dateTime: "2026-05-06T07:50:00+05:00",
},
]
@@ -0,0 +1,218 @@
import { Badge } from "@/components/reui/badge"
import {
Timeline,
TimelineContent,
TimelineDate,
TimelineHeader,
TimelineIndicator,
TimelineItem,
TimelineSeparator,
} from "@/components/reui/timeline"
import { cn } from "@evobgp/ui/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@evobgp/ui/components/avatar"
import { Button } from "@evobgp/ui/components/button"
import { ButtonGroup } from "@evobgp/ui/components/button-group"
import {
financeActivities,
type ActivityDotBadge,
type FinanceActivity,
} from "./data"
import { PaperclipIcon, DownloadIcon } from "lucide-react"
function getInitials(name: string) {
return name
.split(" ")
.map((part) => part[0])
.join("")
}
function ActivityTarget({ target }: { target: string }) {
const idMatch = target.match(/^(.*?)([A-Z]+-\d+)$/)
if (!idMatch) {
return <span className="font-medium">{target}</span>
}
const [, prefix, id] = idMatch
return (
<>
{prefix}
<a
href="#"
className="text-foreground hover:text-primary font-medium underline underline-offset-2"
>
{id}
</a>
</>
)
}
function ActivityAttachment({
attachment,
}: {
attachment: NonNullable<FinanceActivity["attachment"]>
}) {
return (
<ButtonGroup>
<Button type="button" variant="outline" size="xs">
<PaperclipIcon aria-hidden="true" />
<span className="truncate">{attachment.name}</span>
<span className="opacity-60">({attachment.size})</span>
</Button>
<Button
type="button"
variant="outline"
size="icon-xs"
aria-label={`Download ${attachment.name}`}
>
<DownloadIcon aria-hidden="true" />
</Button>
</ButtonGroup>
)
}
function ParticipantGroup({
participants,
count,
}: {
participants: NonNullable<FinanceActivity["participants"]>
count?: number
}) {
return (
<AvatarGroup className="-space-x-1">
{participants.map((participant) => (
<Avatar key={participant.src} className="size-5">
<AvatarImage src={participant.src} alt={participant.fallback} />
<AvatarFallback className="text-[9px]">
{participant.fallback}
</AvatarFallback>
</Avatar>
))}
{count && count > 0 ? (
<AvatarGroupCount className="size-5 text-[9px] leading-none">
+{count}
</AvatarGroupCount>
) : null}
</AvatarGroup>
)
}
function DotBadge({ badge }: { badge: ActivityDotBadge }) {
return (
<Badge variant="outline" className="gap-1.5">
<span
className={cn("size-1.5 shrink-0 rounded-full", badge.dotClass)}
aria-hidden="true"
/>
{badge.label}
</Badge>
)
}
function ActivityStatusRow({ activity }: { activity: FinanceActivity }) {
const [primaryBadge, secondaryBadge] = activity.badges
return (
<div className="mt-2 flex flex-wrap items-center gap-1.5">
<TimelineDate dateTime={activity.dateTime} className="mt-0 mb-0">
{activity.date}
</TimelineDate>
<DotBadge badge={primaryBadge} />
{secondaryBadge ? (
<Badge variant={secondaryBadge.variant}>{secondaryBadge.label}</Badge>
) : null}
</div>
)
}
export function FinanceActivityTimeline() {
return (
<section
className="w-full max-w-md"
aria-labelledby="finance-activity-title"
>
<div className="mb-5 space-y-0.5">
<h1 id="finance-activity-title" className="text-base font-semibold">
Finance Activity
</h1>
<p className="text-muted-foreground text-sm leading-5">
Payouts, risk, invoices, and ledger updates.
</p>
</div>
<Timeline defaultValue={financeActivities.length}>
{financeActivities.map((activity) => (
<TimelineItem
key={activity.id}
step={activity.id}
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-4"
>
<TimelineHeader>
<TimelineSeparator className="bg-border! group-data-[orientation=vertical]/timeline:top-2 group-data-[orientation=vertical]/timeline:-left-8 group-data-[orientation=vertical]/timeline:h-[calc(100%-2.25rem)] group-data-[orientation=vertical]/timeline:translate-y-6" />
<TimelineIndicator className="size-7 overflow-hidden rounded-full border-none group-data-[orientation=vertical]/timeline:-left-8">
<Avatar className="size-7">
<AvatarImage src={activity.avatar} alt={activity.user} />
<AvatarFallback className="text-[10px] font-medium">
{getInitials(activity.user)}
</AvatarFallback>
</Avatar>
</TimelineIndicator>
</TimelineHeader>
<TimelineContent className="min-w-0 pb-1">
<div className="min-w-0">
<p className="text-sm leading-5">
<span className="text-foreground hover:text-primary font-medium">
{activity.user}
</span>{" "}
<span className="text-muted-foreground">
{activity.action}
</span>{" "}
<ActivityTarget target={activity.target} />
</p>
</div>
<p className="text-muted-foreground mt-1 text-xs leading-4">
{activity.detail}
</p>
{(activity.attachment ||
activity.actions?.length ||
activity.participants?.length) && (
<div className="mt-2 flex flex-wrap items-center gap-1.5">
{activity.attachment ? (
<ActivityAttachment attachment={activity.attachment} />
) : null}
{activity.participants?.length ? (
<ParticipantGroup
participants={activity.participants}
count={activity.participantCount}
/>
) : null}
{activity.actions?.map((action) => (
<Button
key={action.label}
type="button"
size="xs"
variant={
action.variant === "outline" ? "outline" : "default"
}
>
{action.label}
</Button>
))}
</div>
)}
<ActivityStatusRow activity={activity} />
</TimelineContent>
</TimelineItem>
))}
</Timeline>
</section>
)
}

Some files were not shown because too many files have changed in this diff Show More