Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef7bb79d4e |
No files matched your search
@@ -1,83 +0,0 @@
|
||||
---
|
||||
name: reui
|
||||
description: "ReUI design and component rules for React apps on shadcn/ui and Tailwind. Use for any UI work in a project with @reui in components.json or a components/reui folder, and in any shadcn project that asks for data grids, kanban boards, dashboards or full pages: building or restyling pages, forms and tables; colors, theme tokens and dark mode; spacing, typography and layout; animation and hover motion; installing ReUI components, examples, Pro blocks or Motion Icons. Works with the ReUI MCP."
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
metadata:
|
||||
user-invocable: "false"
|
||||
---
|
||||
|
||||
> **ReUI skill version `68f0e6fa91`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer in the mode it was installed with to update this skill: `get_agent_skill` -> `install.recommended` from the project root, with `REUI_GLOBAL=1` before `node` when this file is `~/.claude/skills/reui/SKILL.md` or `~/.agents/skills/reui/SKILL.md` (a global install). Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI
|
||||
|
||||
ReUI is an existing design system on shadcn/ui. Keep its look. Generic frontend advice (vary fonts or themes, gradients, page-load or staggered reveals, "2-3 intentional motions", Framer Motion) does not apply here.
|
||||
It ships free components, free `c-*` examples, Pro blocks and Ultimate Motion Icons through the shadcn CLI; the ReUI MCP finds them and serves their APIs.
|
||||
|
||||
## Order of authority
|
||||
|
||||
1. The installed style (`components.json` `style`, baked into `components/ui` and `components/reui`): it owns radius, height, padding, shadow and overlay motion.
|
||||
2. The installed component's variants and props (`size`, `variant`, `spacing`).
|
||||
3. These rules and the topic files.
|
||||
4. Other design skills, then your taste: where they disagree with 1 to 3, ReUI wins. Never redefine `--ease-out` or any Tailwind built-in.
|
||||
|
||||
## Before you write UI code, write this plan in your reply
|
||||
|
||||
1. Surface: operational, marketing or flow.
|
||||
2. Archetype: 1 from [compose.md](./rules/compose.md).
|
||||
3. Container family: Frame-led, Card-led, or Cards for the KPIs and a Frame for the table.
|
||||
4. Density: dense (`size="sm"` on controls that take it) or default.
|
||||
5. Regions: top to bottom, each with the item that fills it.
|
||||
6. States: empty, loading and error for each data region.
|
||||
7. Motion: none, or the 1 entrance.
|
||||
|
||||
Then build exactly that plan; if you deviate, say where.
|
||||
|
||||
## The loop
|
||||
|
||||
1. **Find**: `compose_page` for a full page, else `search` with `type` and `component` hints.
|
||||
2. **Install**: `npx shadcn@latest add @reui/<name> --yes`, with `<name>` exactly as a tool returned it (shadcn/ui parts install by bare name: `npx shadcn@latest add button --yes`).
|
||||
3. **Read the API on your base**: `components.json` style `base-nova` is Base UI, `radix-nova` is Radix. Call `get_component([...componentsUsed])` once, then install 1 `get_examples` example and read its files.
|
||||
4. **Adapt**: change data, copy and props; keep the structure and classes.
|
||||
5. **Check**, after every UI edit, follow-ups included: `validate_usage` on any name you did not read, the self-checks C1 to C4 in [quality.md](./rules/quality.md), [anti-patterns.md](./rules/anti-patterns.md), `get_audit_checklist`, typecheck, lint. Hand back in the 5 lines quality.md gives.
|
||||
|
||||
Show every item's `previewUrl` whenever you list, recommend or install it.
|
||||
|
||||
## Rules that always apply
|
||||
|
||||
- **Owners**: tables `data-grid`, filtering `filters`, boards `kanban`, modals `Dialog` (destructive confirms `AlertDialog`), toasts `sonner`; never hand-roll.
|
||||
- **Color**: semantic tokens for meaning; state colors through Badge or Alert variants (`@reui/badge` and `@reui/alert`, not shadcn's); raw palette only for categorical identity, always with its `dark:` pair.
|
||||
- **Contrast**: never `text-success` or `text-warning` as body text in light mode, and never `text-<state>-foreground` on a solid `bg-<state>` fill.
|
||||
- **Shape**: no `rounded-*` except `rounded-full`, and no `shadow-*`, `h-*` or `px-*` on Button, Input, SelectTrigger, Card or Frame; pick `size` and `variant`. A panel is a Card or Frame, never a div with `rounded-lg border`.
|
||||
- **Spacing**: `gap-*` on the parent, not `space-y-*`; `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections; title to description `gap-0.5`.
|
||||
- **Type**: `text-sm` body, `text-xs` meta (controls keep their style's size), `font-medium` or `font-semibold`, headings `tracking-tight`, numbers `tabular-nums`; nothing below `text-xs` except illustrative mini-UI.
|
||||
- **Motion**: installed components own their motion. Your own code uses `transition-colors` for hover and press, `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]` to reveal, `duration-300` with the same curve at most (media hover zoom: 500), and `SPRING` (`{ type: "spring", visualDuration: 0.25, bounce: 0 }`) for drag and reorder. At most 1 entrance animation per view (a stagger counts as 1). Animate only `transform` and `opacity`, CSS before motion/react, never set state per frame; `<MotionConfig reducedMotion="user">` once at the root plus `motion-reduce:` on CSS, where reduced motion means a calm fade, not nothing.
|
||||
- **Surfaces**: prefer 1 container family per screen. With a KPI row and a table, use FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame or a Frame inside a Card. Icons come from 1 library, the `iconLibrary` in `components.json` (on Ultimate, 1 Motion Icon on the primary action is the exception).
|
||||
- **States**: each data region has empty (1 action), loading (a `Skeleton` in the real layout) and error (Retry) states; light and dark work.
|
||||
- **Never invent**: never write a prop, variant or `@reui/` name you did not read in an `api`, an installed file or a tool result.
|
||||
|
||||
## Topics
|
||||
|
||||
Always read `compose` before building a screen, and `quality` plus `anti-patterns` at the Check step. Besides those, read only the topic files the task touches, at most 4 (for example `color`, `motion`, `layout` and `components`).
|
||||
|
||||
Installed skill: open the file. MCP only: call `get_agent_skill` with the topic, or fetch `https://mcp.reui.io/skill/reui/<file>`.
|
||||
|
||||
| Topic | File | Read when |
|
||||
| --------------- | -------------------------------------------------- | --------------------------------------------- |
|
||||
| `compose` | [rules/compose.md](./rules/compose.md) | before building any screen or section |
|
||||
| `workflow` | [rules/workflow.md](./rules/workflow.md) | `compose_page`, search hints, installing |
|
||||
| `adapting` | [rules/adapting.md](./rules/adapting.md) | changing an installed block or example |
|
||||
| `components` | [rules/components.md](./rules/components.md) | `data-grid`, `filters`, `kanban` and the rest |
|
||||
| `color` | [rules/color.md](./rules/color.md) | any color, status, chart or dark mode choice |
|
||||
| `theming` | [rules/theming.md](./rules/theming.md) | the styles, radius, density and sizes |
|
||||
| `motion` | [rules/motion.md](./rules/motion.md) | any transition, animation or motion/react |
|
||||
| `layout` | [rules/layout.md](./rules/layout.md) | house defaults, hierarchy, surfaces, states |
|
||||
| `icons` | [rules/icons.md](./rules/icons.md) | icon imports, Motion Icons |
|
||||
| `quality` | [rules/quality.md](./rules/quality.md) | the done gate: self-checks, a11y, hand-back |
|
||||
| `anti-patterns` | [rules/anti-patterns.md](./rules/anti-patterns.md) | the Check step: drift pairs, stale habits |
|
||||
| `registry` | [rules/registry.md](./rules/registry.md) | free vs Pro vs Ultimate, base vs radix |
|
||||
| `cli` | [rules/cli.md](./rules/cli.md) | `components.json`, license key, install flags |
|
||||
| `tools` | [tools.md](./tools.md) | MCP result shapes, errors, quota, commands |
|
||||
|
||||
## Plans
|
||||
|
||||
Free covers components, `c-*` examples, this skill and the MCP (a ReUI account and a daily allowance). Pro adds blocks. Ultimate adds Motion Icons. A plan miss is a normal answer with `locked: true`; keep building with what the plan covers.
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "ReUI"
|
||||
short_description: "ReUI design and component rules for React apps on shadcn/ui and Tailwind."
|
||||
default_prompt: "Use ReUI to build this UI with installed components and the ReUI motion and color rules."
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
- Never read `window`, `localStorage`, `location`, `Date.now()` or `Math.random()` in render or in a `useState` initializer; read them in an effect.
|
||||
- **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, and the layout must still hold with long names, empty values and crowded data.
|
||||
@@ -1,117 +0,0 @@
|
||||
# Anti-patterns
|
||||
|
||||
Read this at the Check step, after the self-checks in [quality.md](./quality.md). Each entry: an `Incorrect:` line, then its `Correct:` fix. Detector: the self-check that prints it (C1 to C4), or `visual` when reading finds it. Source: the pilot run that wrote it, or `default` (a training-data habit).
|
||||
|
||||
## Pairs
|
||||
|
||||
### 1. Stagger on every row (Detector: C2; source: run-08)
|
||||
|
||||
Incorrect: `transition={{ delay: i * 0.05 }}` on all 50 rows of a table
|
||||
Correct: 1 stagger of at most 6 items, `staggerChildren: 0.05`, never on table rows
|
||||
|
||||
### 2. Transition everything on a card (Detector: C1; source: runs 10, 15)
|
||||
|
||||
Incorrect: `className="rounded-xl border transition-all duration-300 hover:shadow-lg"`
|
||||
Correct: `transition-colors` for a hover color; a lift is `transform` over a static shadow
|
||||
|
||||
### 3. Raw palette for a state (Detector: C3; source: runs 08, 15)
|
||||
|
||||
Incorrect: `<span className="text-green-600">Active</span>`
|
||||
Correct: `<Badge variant="success-light">Active</Badge>`
|
||||
|
||||
### 4. Animated table rows (Detector: C1; source: runs 01, 08, 17)
|
||||
|
||||
Incorrect: `<motion.tr whileHover={{ backgroundColor: "var(--muted)" }}>`
|
||||
Correct: `data-grid` rows, which already hover; never animate rows with motion/react
|
||||
|
||||
### 5. Hover scale on the default spring (Detector: C1; source: runs 01, 10)
|
||||
|
||||
Incorrect: `<motion.div whileHover={{ scale: 1.03 }}>` with no `transition`
|
||||
Correct: CSS first: `transition-colors` for hover, `transition-transform active:scale-[0.98] motion-reduce:transition-none` for press
|
||||
|
||||
### 6. Entrance from scale 0 (Detector: C1; source: runs 10, 15)
|
||||
|
||||
Incorrect: `<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }}>`
|
||||
Correct: `initial={{ opacity: 0, scale: 0.95 }}`: start at scale 0.95 or more and move 8px or less
|
||||
|
||||
### 7. Hand-built modal or toast (Detector: C4; source: run-15)
|
||||
|
||||
Incorrect: `<div className="fixed inset-0 z-50 bg-black/50">` as a modal
|
||||
Correct: `Dialog`, `AlertDialog` for a destructive confirm, `sonner` for toasts; a palette: `<CommandDialog><Command><CommandInput /><CommandList>`, as `CommandDialog` holds no `Command`
|
||||
|
||||
### 8. Native select (Detector: C4; source: run-15)
|
||||
|
||||
Incorrect: `<select>` with `<option>` children
|
||||
Correct: the shadcn `Select`, or `NativeSelect` when the native control is wanted
|
||||
|
||||
### 9. Hand-rolled table (Detector: C4; source: run-17)
|
||||
|
||||
Incorrect: `<table>` with mapped `<tr>` rows
|
||||
Correct: `data-grid`: a `useTable({ features: dataGridFeatures })` instance passed as `table={table}`, columns typed `ColumnDef<DataGridFeatures, Order>[]`
|
||||
|
||||
### 10. Button inside a clickable wrapper (Detector: visual; source: run-02)
|
||||
|
||||
Incorrect: `<motion.button><Button>Save</Button></motion.button>`
|
||||
Correct: the `Button` alone: 1 interactive element, and it already has a press state
|
||||
|
||||
### 11. Radix asChild on Base UI (Detector: C4; source: run-17, judge)
|
||||
|
||||
Incorrect: `<DialogTrigger asChild><Button>Open</Button></DialogTrigger>`
|
||||
Correct: `<DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>` on base-nova; a link: `<Button nativeButton={false} render={<Link href="/x" />}>`
|
||||
|
||||
### 12. Component defined inside render (Detector: visual; source: Haiku runs, judge)
|
||||
|
||||
Incorrect: `const Row = () => <div />` inside the page component
|
||||
Correct: declare `Row` at module scope and pass it props, so React keeps its state
|
||||
|
||||
### 13. A panel inside a panel (Detector: visual; source: run-05)
|
||||
|
||||
Incorrect: `<FramePanel><Card>...</Card></FramePanel>`, or a Card inside a Card
|
||||
Correct: FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame
|
||||
|
||||
### 14. Small badges (Detector: visual; source: runs 03, 04, 06, 07, 10, 13, 16, 18)
|
||||
|
||||
Incorrect: `<Badge variant="success-light" size="sm">Paid</Badge>`
|
||||
Correct: `<Badge variant="success-light">Paid</Badge>` with `size` left off
|
||||
|
||||
### 15. A colored circle behind every KPI icon (Detector: visual; source: default)
|
||||
|
||||
Incorrect: `<div className="rounded-full bg-blue-100 p-2">` around each KPI icon
|
||||
Correct: the number leads; at most 1 `text-muted-foreground` icon
|
||||
|
||||
### 16. Gradient text (Detector: visual; source: default)
|
||||
|
||||
Incorrect: `bg-linear-to-r from-violet-500 to-pink-500 bg-clip-text text-transparent` on a heading
|
||||
Correct: `text-foreground` and `tracking-tight`; color carries meaning, not decoration
|
||||
|
||||
### 17. A dot or check for the current item (Detector: visual; source: default)
|
||||
|
||||
Incorrect: a dot or a `Check` icon beside the active nav item
|
||||
Correct: `bg-muted` (`dark:bg-muted/50`) plus `aria-current`
|
||||
|
||||
### 18. framer-motion (Detector: C4; source: default)
|
||||
|
||||
Incorrect: `import { motion } from "framer-motion"`
|
||||
Correct: `import { motion } from "motion/react"`
|
||||
|
||||
### 19. Clock or randomness in render (Detector: C4; source: default, near run-05)
|
||||
|
||||
Incorrect: `const [id] = useState(() => Math.random())`
|
||||
Correct: `useId()` for ids; read the clock in an effect or an event handler
|
||||
|
||||
### 20. Placeholder data (Detector: visual; source: default)
|
||||
|
||||
Incorrect: "John Doe", "Acme Inc" and round figures like `$1,000`
|
||||
Correct: specific, plausible data: "Mara Lind", `$1,284.50`, 37 open orders
|
||||
|
||||
## Your training data is out of date
|
||||
|
||||
| Old habit | In this stack |
|
||||
| ----------------------------- | --------------------------------------------------------------- |
|
||||
| `framer-motion` | `motion/react` |
|
||||
| `<DataGrid data columns>` | `useTable({ features: dataGridFeatures })` plus `table={table}` |
|
||||
| A fixed div modal | `Dialog` or `AlertDialog` |
|
||||
| A hand-built toast | `sonner` |
|
||||
| `w-5 h-5` | `size-5` |
|
||||
| never `space-y-4` in new code | `flex flex-col gap-4` |
|
||||
| `asChild` | `render` on base-nova |
|
||||
@@ -1,77 +0,0 @@
|
||||
# CLI: registry setup, license, non-interactive install
|
||||
|
||||
## Registry setup (one-time, per project)
|
||||
|
||||
Free items (the ReUI 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 and Motion Icons) require a ReUI license at install:
|
||||
|
||||
1. Add the key to `.env.local`:
|
||||
|
||||
```bash
|
||||
REUI_LICENSE_KEY=your-license-key
|
||||
```
|
||||
|
||||
2. Switch `components.json` to the authenticated object form:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${REUI_LICENSE_KEY}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`. MCP client configs expand environment variables too, but each client has its OWN syntax, so wire the ReUI MCP server with the form that client understands:
|
||||
|
||||
- Claude Code (`.mcp.json`, `~/.claude.json`): `"Authorization": "Bearer ${REUI_LICENSE_KEY}"`
|
||||
- Cursor (`.cursor/mcp.json`) and VS Code: `"Authorization": "Bearer ${env:REUI_LICENSE_KEY}"`
|
||||
- OpenCode (`opencode.json`): `"Authorization": "Bearer {env:REUI_LICENSE_KEY}"`
|
||||
- Codex (`~/.codex/config.toml`): `bearer_token_env_var = "REUI_LICENSE_KEY"` - Codex reads the variable itself, so there is no header to write
|
||||
|
||||
Only a client with no interpolation at all needs the raw `reui_pat_...` token, and then only in a file that is never committed. Copying the `components.json` form into a client that does not expand it is the common failure: the literal `${REUI_LICENSE_KEY}` placeholder is sent to the server as the credential and every call comes back 401.
|
||||
|
||||
The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry?ref=skill
|
||||
|
||||
## 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.
|
||||
|
||||
`<name>` is the item name exactly as a ReUI tool returned it. The `@reui/` prefix is for ReUI items only:
|
||||
|
||||
- The shadcn/ui components ReUI builds on (`button`, `dialog`, `select`, `card`, ...) are not ReUI items. Install them from shadcn by bare name: `npx shadcn@latest add button --yes`.
|
||||
- `badge` and `alert` exist in both registries: `@reui/badge` and `@reui/alert` are ReUI's components, bare `badge` and `alert` are shadcn's.
|
||||
- In an item's `registryDependencies`, a `@reui/<name>` entry is a ReUI item and a bare name is a shadcn/ui item. `add` installs both, so you never add them by hand.
|
||||
- Templates are not registry items. Download a template from its page on reui.io (https://reui.io/templates?ref=skill); no `shadcn add` command installs one.
|
||||
|
||||
## 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 ReUI 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/...`) need Ultimate.
|
||||
- Templates are downloads from their page on reui.io, not installs: free ones need only a ReUI account, and each template's page says what the others need.
|
||||
|
||||
If an install 401/403s, the license key is missing, invalid, or the plan does not cover that resource (blocks: Pro or higher; icons: Ultimate). Point the user to https://reui.io/account?ref=skill (their key) or https://reui.io/pricing?ref=skill (upgrade).
|
||||
|
||||
If an install 404s, the name is not a ReUI item. Read the CLI's error message, which names the right install command when there is one, or look the name up with `get_install_command` or `search`.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Color
|
||||
|
||||
Semantic tokens carry meaning; raw palette carries identity. Check every color in light and dark.
|
||||
|
||||
## Tokens
|
||||
|
||||
Surfaces, text and borders use your theme tokens (`background`, `foreground`, `card`, `muted`, `muted-foreground`, `primary`, `accent`, `destructive`, `border`, `ring`), so a theme change re-tints them. ReUI adds state tokens: `--success`, `--info` and `--warning` with their `-foreground`, `--destructive-foreground` (beside shadcn's `--destructive`) and `--invert` with `--invert-foreground` for inverted surfaces.
|
||||
|
||||
They are not part of shadcn's base theme. Installing `@reui/badge`, `@reui/alert` or `@reui/code-block` adds them to your global CSS; until one of those is installed, classes such as `bg-success` generate nothing. Check for `--success:` in your global CSS. If it is missing, add these values to `:root` and `.dark`, plus a `--color-<name>: var(--<name>)` line for each under `@theme inline`:
|
||||
|
||||
<!-- gen:tokens:start -->
|
||||
|
||||
| Token | `:root` | `.dark` |
|
||||
| -------------------------- | -------------------------- | -------------------------- |
|
||||
| `--destructive-foreground` | `var(--color-red-800)` | `var(--color-red-600)` |
|
||||
| `--success` | `var(--color-emerald-500)` | `var(--color-emerald-500)` |
|
||||
| `--success-foreground` | `var(--color-emerald-900)` | `var(--color-emerald-600)` |
|
||||
| `--info` | `var(--color-violet-500)` | `var(--color-violet-500)` |
|
||||
| `--info-foreground` | `var(--color-violet-900)` | `var(--color-violet-600)` |
|
||||
| `--warning` | `var(--color-yellow-500)` | `var(--color-yellow-500)` |
|
||||
| `--warning-foreground` | `var(--color-yellow-900)` | `var(--color-yellow-600)` |
|
||||
| `--invert` | `var(--color-zinc-900)` | `var(--color-zinc-700)` |
|
||||
| `--invert-foreground` | `var(--color-zinc-50)` | `var(--color-zinc-50)` |
|
||||
|
||||
<!-- gen:tokens:end -->
|
||||
|
||||
## Meaning vs identity
|
||||
|
||||
- Meaning (a state, emphasis, a surface, text) takes semantic tokens, through the Badge or Alert variant that already carries them.
|
||||
- Identity (a chart series, a tag color, a team, an avatar) may use raw palette: ReUI's own lint keeps it on purpose. Always hand-pair it for dark, as in `text-violet-600 dark:text-violet-400`.
|
||||
- **Incorrect:** `<span className="text-green-600">Active</span>` (raw palette for a state). **Correct:** `<Badge variant="success-light">Active</Badge>`.
|
||||
|
||||
## State recipes
|
||||
|
||||
- State badge: `<Badge variant="success-light">` from `@/components/reui/badge` (`@reui/badge`; shadcn's `components/ui/badge` has no `-light` variants or `size`), also `info-light`, `warning-light`, `destructive-light`, and `primary-light` for brand or pending. Neutral badge: `variant="outline"`. A solid `<Badge variant="success">` (white text) only when the user asks for a solid fill.
|
||||
- Inline status text: `text-success-foreground dark:text-success`, the pair the ReUI Badge uses.
|
||||
- Tinted surface: `border-success/15 bg-success/10 text-success-foreground dark:border-success/25 dark:bg-success/15 dark:text-success` (the Badge's `success-light` recipe).
|
||||
- A message block: `<Alert variant="success">` from `@/components/reui/alert` (`@reui/alert`), also `info`, `warning` and `destructive`.
|
||||
- Never `text-success` or `text-warning` as body text in light mode, and never `text-<state>-foreground` on a solid `bg-<state>` fill.
|
||||
|
||||
## Contrast
|
||||
|
||||
<!-- gen:contrast:start -->
|
||||
|
||||
Light is `text-<tone>-foreground` on `background` and on the `bg-<tone>/10` tint; dark is `dark:text-<tone>` on `background` and on the `dark:bg-<tone>/15` tint. Ratios against the neutral theme, rounded down; body text needs 4.5:1.
|
||||
|
||||
| Tone | Light | Light on tint | Dark | Dark on tint |
|
||||
| ------------- | ----- | ------------- | ---- | ------------ |
|
||||
| `success` | 9.6 | 8.8 | 8.0 | 6.6 |
|
||||
| `warning` | 8.6 | 8.1 | 10.3 | 8.1 |
|
||||
| `info` | 11.0 | 9.7 | 4.4 | 3.9 |
|
||||
| `destructive` | 8.3 | 7.0 | 6.8 | 5.7 |
|
||||
|
||||
Raw state color as light-mode text on `background`: `text-success` 2.4, `text-warning` 1.9, `text-info` 4.4.
|
||||
|
||||
<!-- gen:contrast:end -->
|
||||
|
||||
Info text in dark mode is under 4.5:1: where it must be read, write the words in `text-foreground` and color only the icon.
|
||||
|
||||
## Dark mode
|
||||
|
||||
- shadcn uses the class strategy: `@custom-variant dark (&:is(.dark *));` in your global CSS, and `.dark` on `<html>` turns it on.
|
||||
- Tokens switch by themselves; raw palette needs its `dark:` pair.
|
||||
- Check both themes before you hand back.
|
||||
|
||||
## Charts
|
||||
|
||||
Check your theme's `--chart-1` to `--chart-5` in both modes. The current shadcn themes keep the same chart colors in light and dark; some themes (reui.io's own among them) change hue. For a series whose identity must hold in both modes, use a palette var such as `var(--color-violet-500)`, as ReUI blocks do.
|
||||
|
||||
<!-- gen:charts:start -->
|
||||
|
||||
Same chart colors in light and dark: neutral, stone, zinc, mauve, olive, mist, taupe, amber, blue, cyan, emerald, fuchsia, green, indigo, lime, orange, pink, purple, red, rose, sky, teal, violet, yellow. Changes hue between light and dark: reui.io's own theme.
|
||||
|
||||
<!-- gen:charts:end -->
|
||||
@@ -1,521 +0,0 @@
|
||||
# ReUI components
|
||||
|
||||
The ReUI components: `alert`, `autocomplete`, `badge`, `cascader`, `code-block`, `color-picker`, `data-grid`, `date-selector`, `emoji-picker`, `event-calendar`, `file-type`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `signature-pad`, `sortable`, `stepper`, `time-picker`, `timeline`, `tree`. Examples and blocks are composed from these. `file-upload` is a ReUI component too, shipped as the `use-file-upload` hook rather than a primitive, so it is not in that list: install it as `@reui/use-file-upload`, which writes `hooks/use-file-upload.ts` (import `useFileUpload` from `@/hooks/use-file-upload`), call `get_component("file-upload")` for its API and `get_examples("file-upload")` for its free examples. There is no `@reui/file-upload`: the UI is a `c-file-upload-<n>` example, and one built on the hook installs it.
|
||||
|
||||
**Rule one: never guess a component's API.** Call **`get_component(name)`** for its inline `api` and share its `docsUrl` with the user, then **`get_examples(name)`** and copy a worked example. The contracts below are first-try orientation (required props, shape, the one gotcha); the `api` is the reference. A large `api` (`cascader`, `filters`, `data-grid`) is trimmed on heading boundaries: `sectionsOmitted` names what was dropped and the response's `next` hint pulls it back. `validate_usage` always checks the full API. No single block fits? Compose from components, 1 `get_examples` example each ([compose.md](./compose.md)).
|
||||
|
||||
## data-grid (the flagship - read its API every time)
|
||||
|
||||
`data-grid` wraps TanStack Table v9. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
|
||||
|
||||
- Build a TanStack table with `useTable({ features: dataGridFeatures, ... })`. `dataGridFeatures` already bundles sorting, filtering, pagination, selection, expanding, pinning, resizing and faceting: no row models to wire.
|
||||
- Pass that instance to `<DataGrid table={table} recordCount={total}>`.
|
||||
- Compose the body with `DataGridTable` inside `DataGrid`, and enable features through `tableLayout` (e.g. `{ headerSticky: true, columnsResizable: true }`), not ad-hoc classes.
|
||||
- Server-side data: `manualPagination: true` and `rowCount: total` in `useTable`, `recordCount={total}`, and fetch with the table's `pagination` state (`DataGridPagination` changes `pageSize`).
|
||||
- Columns are TanStack v9, `ColumnDef<DataGridFeatures, Order>[]`, never v8's `ColumnDef<Order>[]`; a sortable column's `header` renders `DataGridColumnHeader` (a string header cannot sort).
|
||||
- Imports, each from `@/components/reui/data-grid/<file>`: `DataGrid`, `DataGridFeatures` and `dataGridFeatures` from `data-grid`, `DataGridColumnHeader` from `data-grid-column-header`, `DataGridTable` from `data-grid-table`, `DataGridPagination` from `data-grid-pagination`, `DataGridScrollArea` from `data-grid-scroll-area`; `useTable` and `ColumnDef` from `@tanstack/react-table`.
|
||||
- States: `isLoading` renders skeleton rows from each column's `meta.skeleton` (a `ReactNode`: `<Skeleton className="h-4 w-20" />`); `emptyMessage` (a `ReactNode`) fills a grid with no rows: after a failed fetch, pass it the error with a Retry.
|
||||
- Numeric column: `meta: { headerClassName: "text-end [&>div]:justify-end", cellClassName: "text-end tabular-nums" }`.
|
||||
- Rows edited in place: `autoResetPageIndex: false` in `useTable`, or each write jumps back to page 1.
|
||||
- In a Frame: `<FramePanel className="p-0">` around `DataGridScrollArea` and `DataGridTable`, plus `tableClassNames={{ base: "min-w-[720px]" }}` so on a phone the table scrolls inside the panel instead of squeezing.
|
||||
|
||||
```tsx
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, Order>[]>(() => [
|
||||
{ accessorKey: "total", header: ({ column }) => <DataGridColumnHeader title="Total" column={column} /> },
|
||||
], [])
|
||||
const table = useTable({ features: dataGridFeatures, data, columns })
|
||||
|
||||
<DataGrid table={table} recordCount={data.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
Never a raw `<table>` or hand-rolled pagination: the API covers sticky header, virtualization and row selection.
|
||||
|
||||
## event-calendar
|
||||
|
||||
**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EventCalendar defaultEvents={events} defaultView="month" className="h-[560px]">
|
||||
<EventCalendarNav />
|
||||
<EventCalendarContent />
|
||||
</EventCalendar>
|
||||
```
|
||||
|
||||
**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column.
|
||||
|
||||
## gantt
|
||||
|
||||
**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Gantt defaultEvents={bars} resources={tasks} defaultScale="month" className="h-[480px]">
|
||||
<GanttNav />
|
||||
<GanttView />
|
||||
</Gantt>
|
||||
```
|
||||
|
||||
**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height.
|
||||
|
||||
## kanban
|
||||
|
||||
**Required:** `value` (`Record<string, T[]>`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
|
||||
<KanbanBoard>
|
||||
{Object.entries(cols).map(([id, items]) => (
|
||||
<KanbanColumn key={id} value={id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3>{id}</h3>
|
||||
<KanbanColumnHandle aria-label="Move column"><GripVertical /></KanbanColumnHandle>
|
||||
</div>
|
||||
<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. `KanbanColumnHandle` is hidden until its column is hovered or it has keyboard focus, so keep the column title outside it.
|
||||
|
||||
## 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:** `fields` (`FilterField[]`). The value is ONE `FilterQuery` tree - `query` + `onQueryChange`, or uncontrolled `defaultQuery`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const fields: FilterField[] = [
|
||||
{ id: "title", label: "Title", type: "text" },
|
||||
{
|
||||
id: "status",
|
||||
label: "Status",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "archived", label: "Archived" },
|
||||
],
|
||||
},
|
||||
]
|
||||
const [query, setQuery] = useState<FilterQuery>(() => createFilterQuery())
|
||||
|
||||
<Filters fields={fields} query={query} onQueryChange={setQuery} />
|
||||
```
|
||||
|
||||
**Gotcha:** the state is a TREE, not a list of chips. `FilterQuery` is a group of rules joined by `and`/`or` and a group may hold another group, so `(A and B) or C` is expressible; a rule is `{ id, type: "rule", path: ["status"], operator, value }` and `path` is the whole nested attribute path, root first. The pre-rewrite API is GONE: there is no `filters`/`onChange` prop, no `FilterFieldConfig` (fields are `FilterField`, nested through their own `fields`, keyed `id` not `key`), and no `createFilter()`: ids come from `createFilterIdFactory(seed)` seeded off `useId` (never minted in render), and `createFilterRule({ id })` takes one, `createFilterQuery()` none. Read the query back with `flattenFilterConditions` (`{ path, field, operator, values, negated }` per rule, incomplete rules skipped) and walk the tree yourself when the parentheses carry meaning - the primitive compiles nothing, no SQL, no query string.
|
||||
|
||||
`variant` picks the chrome over that one query: `"basic"`, the default, is the flat chip row for a toolbar over a table; `"advanced"` is the condition builder, hung off a trigger or rendered in place with `advancedMode="inline"`. Both read and write the same tree, so a saved view built in one opens in the other. Other props worth knowing before you hand-roll them: `size` is two rungs, `"sm" | "default"`, resolved per style (there is no `lg`); `reorderable` turns on drag and Alt+Arrow row moves in the builder; `onBeforeQueryChange` is the ONE veto point for every write (return `false` to refuse, it cannot rewrite); `editors` registers custom value editors a field selects by `editor` name; `labels` / `operatorLabels` own every rendered string, and `renderChip` / `renderValue` / `renderEmpty` replace rendered parts. On a field, `loadOptions` supplies async options with paging and `resolveValues` renders a chip restored from a saved view whose option was never loaded.
|
||||
|
||||
## cascader
|
||||
|
||||
**Required:** `items` (a tree of `{ value, label, children? }`), plus the panel parts inside `CascaderContent`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Cascader items={items} value={value} onValueChange={setValue}>
|
||||
<CascaderTrigger render={<Button variant="outline" />}>
|
||||
<CascaderValue placeholder="Select an attribute" />
|
||||
</CascaderTrigger>
|
||||
<CascaderContent className="w-80">
|
||||
<CascaderPanel>
|
||||
<CascaderNav>
|
||||
<CascaderBreadcrumb />
|
||||
<CascaderInput />
|
||||
</CascaderNav>
|
||||
<CascaderEmpty />
|
||||
<CascaderList maxHeight={288}>
|
||||
<CascaderItems />
|
||||
</CascaderList>
|
||||
<CascaderStatus />
|
||||
</CascaderPanel>
|
||||
</CascaderContent>
|
||||
</Cascader>
|
||||
```
|
||||
|
||||
**Gotcha:** pressing a branch NAVIGATES, it does not select - only leaves are selectable until you pass `selectable="any"` or a predicate, and once a branch is selectable its chevron becomes the only way to open it. `CascaderInput` must stay inside `CascaderContent` (Base UI refills the query from the selection when the input sits outside the popup). Always include `CascaderStatus`: it is the live region announcing level changes, which the visual breadcrumb does not provide to screen readers. Accepts a flat adjacency list via `getParent` as well as nested `children`. `searchScope="deep"` searches the current level and everything under it, `searchScope="global"` the whole tree from any level, and both annotate results with their path; `multiple` gives checkbox rows; `inline` + a bare `CascaderPanel` embeds it with no popover.
|
||||
|
||||
The shape above is `mode="drill"`, the default. `mode="tree"` keeps the same parts (drop `CascaderBreadcrumb`, pass `showBack={false}`, drive expansion with `expanded`/`onExpandedChange`); `mode="columns"` REPLACES `CascaderList` + `CascaderItems` with a single `CascaderColumns`, and has no breadcrumb. Other props worth knowing before you hand-roll them: `cascade` (multi-select only, parent/child selection with indeterminate branches - pair it with `selectable="any"`, since a leaf-only tree can never cascade), `virtualize` plus `CascaderVirtualItems` for long levels, and `getChildren` for async levels (paging, retry, `prefetch`). `CascaderFooter` pins commands below the list and `CascaderSubmenu` opens one as a side flyout. To head a run of rows use `CascaderGroup` wrapping a `CascaderLabel` - a bare label inside a listbox names nothing and is dropped from the accessibility tree - and `CascaderSeparator` for the rule between runs.
|
||||
|
||||
## 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/reui/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). `smooth` defaults to true: pass `smooth={!reduceMotion}` from motion/react's `useReducedMotion()`. Mark the active link `aria-current="location"` (track it with `onUpdate`); `"page"` is for route links.
|
||||
|
||||
## frame
|
||||
|
||||
**Required:** `Frame` > `FramePanel`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Frame>
|
||||
<FrameHeader>
|
||||
<FrameTitle>Title</FrameTitle>
|
||||
<FrameDescription>Description</FrameDescription>
|
||||
</FrameHeader>
|
||||
<FramePanel>Content</FramePanel>
|
||||
<FrameFooter>Footer</FrameFooter>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Gotcha:** the shell for data and tool-like surfaces. `spacing` (`xs` | `sm` | `default` | `lg`) sets panel padding and the gap between panels, so pick it instead of padding classes (the 1 exception: `<FramePanel className="p-0">` around a `data-grid`); `variant` is `default` | `inverse` | `ghost`; `stacked` joins panels with shared borders; `dense` removes the frame's own inset and gap (panels sit flush), and panel padding stays; `--frame-radius` is the one radius knob (panels derive `--frame-panel-radius`).
|
||||
|
||||
## icon-stack
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconStack aria-hidden="true">
|
||||
<InboxIcon className="size-4" />
|
||||
</IconStack>
|
||||
```
|
||||
|
||||
**Gotcha:** isometric layered artwork for empty states and illustrations; style the inner icon via its own `className`. Mark purely decorative stacks `aria-hidden="true"` and keep the real label in surrounding copy.
|
||||
|
||||
## icon-tile
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconTile variant="elevated" size="lg">
|
||||
<PackageIcon />
|
||||
</IconTile>
|
||||
```
|
||||
|
||||
**Gotcha:** the square container an icon sits in, so every list row, feature card and empty state shares one affordance. `variant`: `outline` (default) | `elevated` (muted fill, raised ring) | `soft` (tinted nested, tone from currentColor) | `solid` (filled tone, contrasting glyph) | `frame` (double container). `soft` and `solid` retint from one text color class (they default to `text-primary`). `size`: `xs | sm | default | lg | xl` (24/32/40/48/64px tile, glyph scales 12/14/16/20/24px). `radius`: `default | full`. Do not set a `size-*` class on the child icon unless you mean to override the tile's glyph size; recolor with `className` on the tile, not the icon.
|
||||
|
||||
## code-block
|
||||
|
||||
**Required:** `code` + `language`, or pre-highlighted `lines`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<CodeBlock code={code} language="tsx" />
|
||||
|
||||
<CodeBlock code={code} language="tsx" showLineNumbers maxLines={20}>
|
||||
<CodeBlockHeader>
|
||||
<CodeBlockTitle>use-totals.ts</CodeBlockTitle>
|
||||
<CodeBlockLanguage />
|
||||
<CodeBlockCopyButton className="ml-auto" />
|
||||
</CodeBlockHeader>
|
||||
<CodeBlockExpandButton />
|
||||
</CodeBlock>
|
||||
```
|
||||
|
||||
**Gotcha:** the one-liner is already a complete block; every child is optional chrome, at any depth. Shiki is the only npm dependency and loads lazily on first highlight; `lines` (from `highlightCode` in `code-block-highlight`, which is server-safe and has no `"use client"`) or `highlight={false}` loads nothing at all. `maxLines` caps the height AND marks the block collapsible, which is what makes `CodeBlockExpandButton` appear. Use `variant="ghost"` when the block sits inside a surface that already has a border.
|
||||
|
||||
## 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.
|
||||
|
||||
## badge
|
||||
|
||||
**Required:** none (text child).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="success-light">Success</Badge>
|
||||
<Badge variant="outline">Draft</Badge>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible. Rich `variant` set (solid, `-outline`, `-light` per color), `size` `xs..xl` (keep the default), `radius` `default | full`. Like `alert`, the color variants rely on ReUI extended tokens. Prefer `Badge` variants over raw color classes: `<tone>-light` for a state, `outline` for a neutral label; an omitted `variant` is the solid primary fill.
|
||||
|
||||
## color-picker
|
||||
|
||||
**Required:** none (`<ColorPicker />` alone is complete); wire `value` + `onValueChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<ColorPicker value={color} onValueChange={setColor} alpha>
|
||||
<ColorPickerTrigger />
|
||||
<ColorPickerContent>
|
||||
<ColorPickerPanel>
|
||||
<ColorPickerArea />
|
||||
<ColorPickerSlider channel="hue" />
|
||||
<ColorPickerSlider channel="alpha" />
|
||||
<ColorPickerFields />
|
||||
</ColorPickerPanel>
|
||||
</ColorPickerContent>
|
||||
</ColorPicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a CSS color string in `format` (`"hex"` default, `"rgb"`, `"hsl"`, `"hsb"` or `"oklch"`), `null` when empty, never an object. `onValueChange` fires on every drag move, so save in `onValueCommit`. Without `alpha` the value is always opaque. A bare `ColorPickerPanel` inside the root is an inline picker; `ColorPickerInput` replaces the trigger with a typable field.
|
||||
|
||||
## emoji-picker
|
||||
|
||||
**Required:** none; wire `onEmojiSelect` to insert a pick, or `value` + `onValueChange` to use it as a field.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EmojiPicker onEmojiSelect={({ value }) => insert(value)}>
|
||||
<EmojiPickerTrigger />
|
||||
<EmojiPickerContent>
|
||||
<EmojiPickerHeader>
|
||||
<EmojiPickerSearch />
|
||||
</EmojiPickerHeader>
|
||||
<EmojiPickerCategories />
|
||||
<EmojiPickerList />
|
||||
<EmojiPickerFooter>
|
||||
<EmojiPickerPreview />
|
||||
<EmojiPickerSkinTone />
|
||||
</EmojiPickerFooter>
|
||||
</EmojiPickerContent>
|
||||
</EmojiPicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the emoji data is fetched at runtime from a pinned jsDelivr path (`EMOJI_PICKER_DATA_URL`): under a strict Content Security Policy allow it in `connect-src`, self-host with `dataUrl`, or pass `data` (from `normalizeEmojibaseData`). Store the pick's `value` (the native emoji in its skin tone, or `:shortcode:` for a custom one). `multiple` makes the value a list, capped by `max`.
|
||||
|
||||
## file-type
|
||||
|
||||
**Required:** none; pass `type` (an extension, a family name or any string).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<FileType type="pdf" />
|
||||
<FileType type={getFileType(file.name)} size="sm" aria-hidden="true" />
|
||||
```
|
||||
|
||||
**Gotcha:** one `span` with `role="img"`, no children, no `"use client"` (fine in a Server Component). Resolve a file name, path, URL or MIME type with `getFileType`, never by splitting on the dot. `variant`: `outline` (default) | `solid` | `light` | `mono`; `size`: `mini` | `xs` | `sm` | `default` | `lg` | `xl` (18 to 56px); recolor with a `text-*` class. Pass `aria-hidden="true"` when the file name sits beside it.
|
||||
|
||||
## signature-pad
|
||||
|
||||
**Required:** `SignaturePad` > `SignaturePadArea`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<SignaturePad value={strokes} onValueChange={setStrokes}>
|
||||
<SignaturePadArea>
|
||||
<SignaturePadGuide />
|
||||
<SignaturePadPlaceholder />
|
||||
<SignaturePadControls>
|
||||
<SignaturePadUndo />
|
||||
<SignaturePadClear />
|
||||
</SignaturePadControls>
|
||||
</SignaturePadArea>
|
||||
</SignaturePad>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is an array of strokes (`{ points: [x, y, size][], color? }`), not an image: store it as JSON, and export with `useSignaturePad()` or `apiRef` (`toSVG`, `toDataURL`, `toBlob`) or `SignaturePadSave` (`format`, `onSave`). `onValueChange` fires per finished stroke, never per pointer move. Size the area with a class (`h-40` by default); an overlay in it that is not a control needs `pointer-events-none`. Drawing needs a pointer, so offer a typed name where a signature is required.
|
||||
|
||||
## time-picker
|
||||
|
||||
**Required:** none (`<TimePicker />` alone is complete); wire `value` + `onValueChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<TimePicker value={time} onValueChange={setTime}>
|
||||
<TimePickerTrigger />
|
||||
<TimePickerContent>
|
||||
<TimePickerColumns />
|
||||
<TimePickerFooter>
|
||||
<TimePickerNow />
|
||||
<TimePickerClear />
|
||||
<TimePickerConfirm />
|
||||
</TimePickerFooter>
|
||||
</TimePickerContent>
|
||||
</TimePicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a 24-hour `"HH:mm"` string (`"HH:mm:ss"` with `granularity="second"`) or `null`, never a `Date`, whatever `hourCycle={12}` shows. `minuteStep`, `min`, `max` and `isTimeDisabled` narrow the options; keep `isTimeDisabled` stable (module scope or memoized). `requireConfirm` holds picks as a draft until OK. `TimePickerInput` replaces `TimePickerTrigger` for a typable field, never beside it.
|
||||
|
||||
## base vs radix - write for the project's base
|
||||
|
||||
Every component ships a `base` (Base UI) and a `radix` (Radix UI) build under 1 name, and the CLI installs the one the project uses. Read `components.json` `style`: the part before the first `-` is the base (`base-nova` is **Base UI**, `radix-nova` is **Radix UI**). Write against that base; the deltas mirror shadcn's:
|
||||
|
||||
- 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"`.
|
||||
|
||||
Installed files and `c-*` examples are already in your base: reuse their wiring. When an `api` or example shows the other base's shape, translate it (or `validate_usage` to confirm).
|
||||
@@ -1,62 +0,0 @@
|
||||
# Compose a screen
|
||||
|
||||
Read this before you build any screen or section, then write the router's 7-line plan in your reply (Surface, Archetype, Container family, Density, Regions, States, Motion) and build exactly that plan. Style it with the topics the screen touches: [layout.md](./layout.md) (house defaults), [color.md](./color.md), [motion.md](./motion.md).
|
||||
|
||||
If the brief names a layout or a motion (a left nav, 2 entrances), follow the brief and name the default you overrode. Several entrances run as 1 stagger: at most 6 items, 50ms apart, never on table rows or lists of 20 or more.
|
||||
|
||||
## Surface
|
||||
|
||||
- **Operational**: a screen people work in every day (dashboard, records, settings, admin tools). Frame-led, dense (`size="sm"` on controls that take it), no hero, no page-load motion.
|
||||
- **Marketing**: a page a visitor reads once (landing, pricing, features). Card-led sections on the spine below, default density.
|
||||
- **Flow**: a task people finish and leave (sign in, onboarding, checkout). 1 centered Card or a Dialog, 1 primary action per step.
|
||||
|
||||
## Archetypes: pick 1 per screen
|
||||
|
||||
- **Vertical rail settings** (3 to 6 sections): a section rail on the left, 1 content column, 1 section at a time; the rail turns horizontal below `md`.
|
||||
- **Horizontal tabs** (5 to 8 views of 1 object): a full-width `Tabs` rail under the header that scrolls sideways when it overflows, 1 content column.
|
||||
- **Frame stack**: stacked Frames, each 1 focused group of settings or records; a flat page split by `Separator` rows when it should feel lighter.
|
||||
- **Table-backed management**: header, toolbar (search, `filters`), then a `data-grid` with row actions and pagination, all in 1 Frame.
|
||||
- **Dashboard grid**: a KPI row, then 1 or 2 chart or list panels, then the table.
|
||||
- **Board**: header and filters, then a `kanban` board; `KanbanBoard` stacks its columns below `sm`, and a wider board scrolls inside its own region.
|
||||
- **Record detail**: header (title, status `Badge`, actions), a main column of sections, a side column of facts that stacks under it below `lg`.
|
||||
- **Marketing spine**: optional `Badge` eyebrow, headline (the only loud element), 1-clause subhead, action row (1 primary `Button`, a second `variant="outline"`, a third a text link), specific proof beside it ("27 reviews"), media. Omit parts; never reorder them.
|
||||
- **Auth card**: 1 centered `Card` at `max-w-sm`: title, fields, 1 full-width primary `Button`, a text link to the other flow.
|
||||
- **Wizard dialog**: a `Dialog` holding a `stepper`, 1 step visible, Back (`variant="outline"`) and Next (primary) in its footer.
|
||||
|
||||
An app shell around any of them is the shadcn `Sidebar`, which turns into a `Sheet` below `md` by itself.
|
||||
|
||||
## Fill each region in this order
|
||||
|
||||
1. The ReUI component that owns the job: the router's Owners (`data-grid`, `filters`, `kanban`), `useFileUpload` from `@/hooks/use-file-upload` for file drop, or the one whose contract in [components.md](./components.md) matches.
|
||||
2. A shadcn/ui component (`Tabs`, `Dialog`, `AlertDialog`, `Sheet`, `Empty`, `Skeleton`, `sonner`).
|
||||
3. A free `c-*` example from `get_examples` that already composes it: install it and adapt.
|
||||
4. Your own composition of those parts. Never a hand-rolled table, modal or tabs.
|
||||
|
||||
Import ReUI parts file by file: `data-grid`, `filters`, `cascader`, `event-calendar` and `gantt` are folders with no index file. `data-grid`: the import line in [components.md](./components.md). `filters`, each from `@/components/reui/filters/<file>`: `Filters` from `filters`, `createFilterQuery` and `flattenFilterConditions` (1 argument, the query) from `filters-query`, the types `FilterField` and `FilterQuery` from `filters-types`.
|
||||
|
||||
## Wiring that works
|
||||
|
||||
- `data-grid`: columns are `ColumnDef<DataGridFeatures, Order>[]`, a sortable header renders `DataGridColumnHeader`, and server paging needs `manualPagination: true` and `rowCount`.
|
||||
- `filters`: apply each condition by its `operator`. Negate swaps a rule to its inverse (`is` and `is_not`, `contains` and `not_contains`, `is_any_of` and `is_none_of`, `empty` and `not_empty`) and sets `negated` only when there is none: honour both.
|
||||
- A Base UI `Button` that navigates: `<Button nativeButton={false} render={<Link href="/x" />}>`.
|
||||
- shadcn's `CommandDialog` has no `Command` inside, unlike older shadcn: wrap `CommandInput` and `CommandList` in one. `CommandItem` search matches its `value`: omit it (text is used) or use the label; ids in `onSelect`.
|
||||
|
||||
## Headers
|
||||
|
||||
A page or panel header puts the title (1 to 3 words) and a 1-clause description on the left and the actions on the right: 1 primary `Button`, the rest `variant="outline"`, row actions `variant="ghost"`.
|
||||
|
||||
## Responsive
|
||||
|
||||
- No horizontal page scroll at 390px: grids drop to 1 column, side columns stack, `min-w-0` on every shrinking parent.
|
||||
- A KPI row is `grid-cols-1 sm:grid-cols-2 xl:grid-cols-4`.
|
||||
- Every header and toolbar action stays reachable at every width: a control hidden below a breakpoint moves into a menu shown only there.
|
||||
- A table scrolls sideways inside its Frame, never the page (the `data-grid` contract in [components.md](./components.md)).
|
||||
|
||||
## States and motion
|
||||
|
||||
- Each data region owns its states: empty (shadcn `Empty` with a title, 1 line and the primary action), loading (`Skeleton` in the real layout; `data-grid` takes `isLoading`) and error (inline in the region, with Retry). Never 1 spinner for the whole page.
|
||||
- Motion: none, or the 1 entrance per view; installed parts keep their own motion, and hover and press follow [motion.md](./motion.md). An Alert, field error, notice or data that appears after an action or a load is a reveal, not an entrance: fade it in with the reveal row.
|
||||
|
||||
## Remove first
|
||||
|
||||
Overloaded? Remove, in order: decorative summaries, duplicate context, extra badges, long descriptions, secondary actions, mismatched icon treatments. Add a region only when the brief needs it.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Icons (ReUI delta over shadcn)
|
||||
|
||||
Use the project's configured `iconLibrary`, put `data-icon="inline-start"` on an icon before the label inside `Button` (`data-icon="inline-end"` on one after it), put no sizing classes on icons inside components, and pass icons as components, not string keys. ReUI adds the following.
|
||||
|
||||
## Portable icons (library-agnostic)
|
||||
|
||||
ReUI components, examples, and blocks are authored to be icon-library-agnostic. The shadcn CLI installs each item's icons in the library named by `iconLibrary` in `components.json` - you swap nothing. When `iconLibrary` is not set, the CLI falls back to `lucide`, so the icons arrive as `lucide-react` imports. If that is not the project's library, set `iconLibrary` before installing, or change the **import source and component name** to your library, keeping the same icon-name semantics (each key below is an `iconLibrary` value):
|
||||
|
||||
- `lucide` -> `lucide-react`
|
||||
- `tabler` -> `@tabler/icons-react`
|
||||
- `phosphor` -> `@phosphor-icons/react`
|
||||
- `remixicon` -> `@remixicon/react`
|
||||
- `hugeicons` -> icons from `@hugeicons/core-free-icons`, rendered through `HugeiconsIcon` from `@hugeicons/react`
|
||||
|
||||
Don't assume `lucide-react`: read `iconLibrary` from `components.json`, and when it is unset, check which icon package the project already uses.
|
||||
|
||||
## 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). Every icon installs from the default path; some also have a real hover animation (motion/react) on the animated path:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/icons/default/<style>/<name> --yes # static, every icon
|
||||
npx shadcn@latest add @reui/icons/animated/<style>/<name> --yes # hover-animated, only when the icon has installAnimated
|
||||
```
|
||||
|
||||
The animated path answers for every icon, but for an icon without a real animation it installs the same static drawing, so only use it when the icon carries `installAnimated`.
|
||||
|
||||
Motion Icons need an Ultimate license for **both discovery and install** (`REUI_LICENSE_KEY`, see [cli.md](./cli.md)). Without it `search_icons` comes back `locked` with no results and `search` returns no icons at all, so check the plan before you promise the user icons: on a free or Pro account, use the project's own `iconLibrary` instead. On Ultimate, a Motion Icon on the primary action is the 1 hover flourish (see [motion.md](./motion.md)).
|
||||
|
||||
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 real hover animation; a concept with none comes back empty and `animatedNote` says so.
|
||||
- One icon: `search` with `type: "icon"`.
|
||||
- Icon results and `get_icon` carry `animated: true` and `installAnimated` only when the icon really animates - use those install strings, do not construct paths by hand.
|
||||
- Every icon result carries a `previewUrl` (the icon's own page) and a `thumbnail` (its 4 styles side by side) - **show or share them** so the user can SEE the icon before installing, and look at the thumbnail yourself before trusting a name.
|
||||
- Several icons can fit one concept. Before installing, read `get_icon`'s `whenNotToUse` (each entry names the icon to use instead), and use its `label` as the `aria-label` of an icon-only button.
|
||||
- Icon names were revised in the icons v2 update. An old name (`save-2`, `abstract-18`) still answers in `get_icon`, `get_install_command`, `validate_usage` and `get_thumbnail` with the icon it became and `renamedFrom`, and still installs that same drawing; write the current name in new code. That includes names renamed because they misdescribed the drawing (`watch` installs a sand timer, `chevrons-down` points up), so look at the `thumbnail` before trusting an old name. `search_icons` answers an old name the same way, unless it is also an ordinary word icons are named for today (`watch`): then it ranks today's icons for the word, and `oldNameNotes` names the icon the old name installs. `search` ranks by current names only, so look an old name up with `get_icon`, never `search`. Only `tag` (it drew a money bag) no longer answers: the reply names the icons to use instead.
|
||||
|
||||
The `icon-stack` component composes multiple icons into a stacked display.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Layout: house defaults, hierarchy, spacing, surfaces and states
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
## House defaults
|
||||
|
||||
<!-- gen:house-defaults:start -->
|
||||
|
||||
- **Spacing ladder**: `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections, `gap-0.5` title to description. Deviate when: skip a level if you like, never invert them.
|
||||
- **Panel title**: no size class on `FrameTitle` or `CardTitle`. Deviate when: never; `FrameTitle` ships `text-sm font-semibold`, `CardTitle` follows the installed style.
|
||||
- **Meta text**: `text-xs`. Deviate when: never smaller, except inside illustrative mini-UI artwork.
|
||||
- **Frame spacing**: `spacing="default"`, written out. Deviate when: `sm` in a sheet, dialog, drawer or popover, or for a dense data grid in a panel; `xs` or `lg` only with a stated reason.
|
||||
- **Leading row icon**: `size-4`. Deviate when: never in a glyph column; `Spinner` ships `size-4`, so a loading swap keeps the width.
|
||||
- **Icon inside a control**: no size class; the control sizes it per style and size. Deviate when: never pin `size-*` on an svg inside `Button`, `InputGroupButton`, `InputGroupAddon`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioItem`, `DropdownMenuSubTrigger`, `SelectItem`, `CommandItem`, `Toggle`, `ToggleGroupItem`, `IconTile` or `MarkerIcon`.
|
||||
- **Badge size**: `default` (leave `size` off). Deviate when: `lg` or `xl` only when the user asks; never `sm` or `xs`.
|
||||
- **Neutral badge**: `variant="outline"`. Deviate when: never `secondary`, `default` or `primary-light` for an id, tag, version, count or Draft/Beta label; inline after the title, never `ml-auto`.
|
||||
- **State badge**: `<tone>-light`: `success-light`, `info-light`, `warning-light`, `destructive-light`; `primary-light` for brand or pending. Deviate when: never the solid tone, `secondary` or `outline` for a state; 1 state badge per row.
|
||||
- **Solid badge**: never unless asked; always name a variant (an omitted one is the solid primary fill). Deviate when: only when the user asks for a solid fill (`default`, `secondary`, `success`, `info`, `warning`, `destructive`, `focus`, `invert`).
|
||||
- **Slot padding**: the primitive's own inset; set density with `Frame spacing=` or `Card size="sm"`. Deviate when: never a padding class on `CardHeader`, `CardContent`, `CardFooter`, `FrameHeader`, `FramePanel`, `FrameFooter`, `SheetHeader`, `SheetContent`, `SheetFooter`, `DialogHeader`, `DialogContent`, `DialogFooter`, `PopoverContent`, `DropdownMenuContent`, `TabsContent` or `AccordionContent`.
|
||||
- **Inline separator**: `<span aria-hidden className="bg-muted-foreground/40 size-1 shrink-0 rounded-full" />`. Deviate when: never a typed `·` between rendered segments; a literal dot only inside 1 data string.
|
||||
- **List rows**: dense, auto height; a row with no second line stays 1 tight line. Deviate when: never uniform height.
|
||||
- **Current list item**: ghost hover fill (`bg-muted`, `dark:bg-muted/50`) plus `aria-current`. Deviate when: never a dot or a check mark.
|
||||
|
||||
<!-- gen:house-defaults:end -->
|
||||
|
||||
## Hierarchy
|
||||
|
||||
- 1 focal point per card or panel: the dominant metric or task, then its label, then detail. Borders, separators and surfaces make 2 to 3 bands, never equal weight everywhere.
|
||||
- Overloaded? Remove, in order: decorative summaries, duplicate context, extra badges, long descriptions, secondary actions, mismatched icon treatments.
|
||||
|
||||
## Spacing and copy
|
||||
|
||||
- Siblings take the parent's `gap-*`, never their own margins; a heading sits nearer its own content than the section above it. Never pad an operational surface like a marketing page.
|
||||
- Titles 1 to 3 words, a description 1 clause (about 8 words), a context line 2 segments at most, labels and buttons 1 to 2 words, a toast title 4 words at most. Copy that wraps at desktop width is over budget: cut it, never shrink the type.
|
||||
|
||||
## Surfaces
|
||||
|
||||
- `Frame` for data and operational surfaces (tables, lists, tool panels); `Card` for bounded sections and marketing.
|
||||
- Prefer 1 container family per screen. With a KPI row and a table, use FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame or a Frame inside a Card.
|
||||
- A `data-grid` inside a Frame is the one place for `<FramePanel className="p-0">`; elsewhere no padding on slots. This is the 1 exception to the Slot padding row above.
|
||||
- 1 archetype per screen (operational ones and the marketing spine): [compose.md](./compose.md) lists each with its regions.
|
||||
|
||||
## Type and buttons
|
||||
|
||||
- A KPI value is `text-2xl font-semibold tracking-tight tabular-nums`.
|
||||
- `size="sm"` on dense surfaces, `variant="outline"` for secondary actions, `ghost` for row actions; 1 primary per region, 1 variant per toolbar.
|
||||
- A toolbar separator is `<Separator orientation="vertical" className="my-auto h-4" />`; at a 390px viewport a toolbar stays on 1 row.
|
||||
|
||||
## States and responsive
|
||||
|
||||
- Empty: a short message plus the primary action. Loading: a `Skeleton` in the real layout, never a centered spinner. Error: inline with a retry.
|
||||
- No horizontal page scroll at 390px: reflow to 1 column instead of shrinking; `min-w-0` on the shrinking parent and `truncate` on single-line labels.
|
||||
@@ -1,80 +0,0 @@
|
||||
# Motion
|
||||
|
||||
## Installed motion: never retime it
|
||||
|
||||
<!-- gen:installed-motion:start -->
|
||||
|
||||
| Installed part | Duration |
|
||||
| ------------------ | ----------------- |
|
||||
| Overlays | 100ms |
|
||||
| Tooltip | 150ms (default) |
|
||||
| Sheet, sidebar | 200ms |
|
||||
| Navigation menu | 300ms, exit 150ms |
|
||||
| rhea control focus | 200ms |
|
||||
|
||||
<!-- gen:installed-motion:end -->
|
||||
|
||||
Never add or change `duration-*`, `ease-*`, `animate-*` or `transition-*` on installed parts (`components/ui`, `components/reui`); Button already presses with `active:not-aria-[haspopup]:translate-y-px`.
|
||||
|
||||
## House motion: your own code
|
||||
|
||||
<!-- gen:house-motion:start -->
|
||||
|
||||
| Use | Tailwind |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| Hover, press, color | `transition-colors` (150ms, no duration or ease class) |
|
||||
| Reveal or expand | `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]` |
|
||||
| Large surface (ceiling) | `duration-300` with the same curve |
|
||||
| An overlay you build | `duration-100` with the same curve |
|
||||
| Media hover zoom | `duration-500` with the same curve, `scale-105` at most |
|
||||
| Width tied to the sidebar | `duration-200 ease-linear` |
|
||||
|
||||
<!-- gen:house-motion:end -->
|
||||
|
||||
- Press on a custom surface (not Button): `transition-transform active:scale-[0.98] motion-reduce:transition-none`.
|
||||
- Hover color plus press on 1 element: `transition-[color,background-color,border-color,scale]` (`transition-colors` and `transition-transform` override each other; `scale-*` sets `scale`).
|
||||
- motion/react takes the same values in seconds: `{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }`; hover `{ duration: 0.15 }`.
|
||||
- Drag, reorder and layout use `const SPRING = { type: "spring", visualDuration: 0.25, bounce: 0 }`. Define `SPRING` only when something drags, reorders or toggles layout.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Animate `transform` and `opacity`; color only for hover and press. Never `transition-all`; width, height, top or left only in the sidebar row.
|
||||
- Never `animate-bounce` or a spring with bounce above 0. `ease-linear` only for progress, spinners, loops and the sidebar; never Tailwind's `ease-in`, `ease-out` or `ease-in-out`.
|
||||
- `motion/react` only: never framer-motion, GSAP or react-spring.
|
||||
- At most 1 entrance animation per view (a stagger counts as 1), and every animation has a reduced-motion path. Feedback that appears after an action or a load (an Alert, a field error, a saved notice, data replacing its `Skeleton`) is a reveal, not an entrance: fade it in with the reveal row.
|
||||
- Never add named `duration-*` utilities or redefine `--ease-out`.
|
||||
- Loading: a `Skeleton` (it already pulses) in the real layout; a spinner only inside a busy button.
|
||||
- Motion Icons (Ultimate only): 1 on the primary action is the 1 hover flourish; install it with its `installAnimated` command; never retime it.
|
||||
|
||||
## Performance
|
||||
|
||||
Each line names its lint rule, or judgement (no check).
|
||||
|
||||
- Never animate blur, drop-shadow, backdrop-filter, box-shadow, border-radius, background-position, background-size or a custom property that feeds transform; clip-path and stroke-dashoffset only as a one-shot reveal of 300ms or less, or a loop on an element of 240x240px or less. Name the properties: never a bare `transition` (it also animates box-shadow and filter). (`reui/no-paint-animation`; 240px: judgement (no check))
|
||||
- A hover lift is `transform` over a static shadow, or a shadow layer whose `opacity` fades; never a transitioned `hover:shadow-*`. (`reui/no-paint-animation`)
|
||||
- CSS first: hover, press, reveals and ambient loops are Tailwind transitions, `tw-animate-css` or `@keyframes`. motion/react only for exit on unmount, drag, springs that follow input, `layout` and scroll-linked values. (judgement (no check))
|
||||
- In motion/react, `x`, `y`, `scale`, `rotate`, `pathLength` and every SVG target run on the main thread: never `repeat: Infinity` on them (loop with CSS `@keyframes`), and keep 20 or fewer JS-driven animations running per view. An opacity loop leaves the main thread only on an HTML element without `repeatDelay`, `repeatType: "mirror"` or `onUpdate`. (`reui/no-main-thread-loop`)
|
||||
- SVG geometry (a rect's `width`, a circle's `r`) does not reflow the page but runs in motion's JS loop: one-shot only. (`reui/no-main-thread-loop`)
|
||||
- Never set React state per frame (requestAnimationFrame, `useAnimationFrame`, `onUpdate`, scroll or pointermove listeners): bind styles to `useMotionValue`, `useTransform` or a CSS variable on the element. (`reui/no-frame-setstate`)
|
||||
- A JS-driven loop (motion `repeat`, requestAnimationFrame, canvas) pauses offscreen with `useInView` or IntersectionObserver; `content-visibility: auto` never stops script, and a setInterval loop also pauses on `visibilitychange`. (`reui/loop-offscreen`)
|
||||
- No permanent `will-change`: no `will-change-*` class or `willChange` style, except during a drag, removed when it ends. (`reui/will-change`)
|
||||
- Stagger 50ms or less per step (`staggerChildren: 0.05`), only the first 6 items, the whole entrance done in 500ms, first mount only, never on table rows or lists of 20 or more; scroll reveals use `whileInView` with `viewport={{ once: true }}`. (`reui/stagger-budget`; item count and tail: judgement (no check))
|
||||
- A height reveal (`grid-template-rows`) starts only on a click or key press, runs 200ms or less, 1 at a time; never `initial={{ height: 0 }}`. (`reui/no-layout-transition`)
|
||||
- The motion `layout` prop on 1 list at a time (about 20 elements), never in data-grid or virtualized rows; drag and drop goes through `@reui/sortable` or `@reui/kanban`. (judgement (no check))
|
||||
- AnimatePresence `mode="wait"` only with an exit of 150ms or less; `initial={false}` around content present at first render. (`reui/stagger-budget`)
|
||||
- Never hide the hero or the largest heading behind a motion/react `initial` at opacity 0 (hidden until JavaScript runs). (judgement (no check))
|
||||
- At most 3 ambient loops per view: CSS `@keyframes` on transform or opacity, a period of 2s or more, exempt from the duration table; once 3 run in a view they pause offscreen (a `content-visibility: auto` wrapper with `contain-intrinsic-size: auto <h>` where nothing paints outside it, or IntersectionObserver toggling `animation-play-state`). At most 50 animating elements per view (Skeletons, loaders and CSS opacity loops not counted). A static blur above 12px, at most 1 per view, only on a surface that neither moves nor sits over motion. (judgement (no check))
|
||||
|
||||
## Reduced motion
|
||||
|
||||
- Reduced motion is a substitute, not an off switch: replace movement (translate, scale, rotate, slide, zoom, layout, parallax, auto-scroll) with an opacity fade of 150ms or less or an instant change. Keep color and opacity feedback, focus, progress bars, loaders, the Skeleton pulse, chevrons and the switch thumb. (`reui/reduced-motion`)
|
||||
- The reduced state is the resting state: content never waits at opacity 0, hidden or offset for an animation that will not play; a count-up shows its final value. (`reui/reduced-motion`)
|
||||
- In every mode an entrance moves 8px or less and starts at scale 0.95 or more; nothing moves a surface larger than half the viewport. (`reui/reduced-motion`; half the viewport: judgement (no check))
|
||||
- `<MotionConfig reducedMotion="user">` once at the app root (its default ignores the OS) AND a `motion-reduce:` path on every CSS animation (MotionConfig never reaches CSS). An example or block wraps its own motion subtree. (`reui/reduced-motion`)
|
||||
- MotionConfig makes only transform keys and width, height, top and left instant; opacity, filter, clipPath, pathLength, color and a `transform` string keep animating, so a loop or those keys branch on `useReducedMotion()` and rest on the final value. (`reui/reduced-motion`)
|
||||
- `useReducedMotion()` is null on the server: never let it pick markup (JSX, className, `initial`) or reach `animate` under `initial={false}`; branch only animate, transition, `while*` or exit values. (`reui/reduced-motion-ssr`)
|
||||
- No SMIL (`<animate>`, `<animateMotion>`, `<animateTransform>`); canvas and requestAnimationFrame loops draw 1 resting frame under `prefers-reduced-motion: reduce`. (`reui/no-smil`, `reui/reduced-motion`)
|
||||
- Smooth scroll only under `motion-safe:scroll-smooth`. A focus ring appears at once: never `transition-all`, `transition-shadow` or a bare `transition` on it. (`reui/reduced-motion`, `reui/focus-transition`)
|
||||
- Loaders and typing dots keep their opacity pulse; every decorative loop gets `motion-reduce:animate-none`, never an inline `style={{ animation }}` (it beats the class). (`reui/reduced-motion`)
|
||||
- Anything that moves on its own for more than 5 seconds (autoplay, marquee, ticker) gets a visible Pause control, pauses on hover and focus, and never autoplays under reduced motion. (`reui/pausable-autoplay`)
|
||||
- Installed overlays keep their 100ms fade, zoom and 8px slide under reduced motion: disclosed, not edited; an app that needs WCAG 2.3.3 adds its own override. (judgement (no check))
|
||||
@@ -1,57 +0,0 @@
|
||||
# Quality gates (security, accessibility, scroll, self-checks)
|
||||
|
||||
This is the **done gate**: before you call any ReUI work finished, call `get_audit_checklist` and pass every item below (plus the bar and the states in [layout.md](./layout.md)). Then run the self-checks, typecheck and lint, compare with [anti-patterns.md](./anti-patterns.md), and hand back in the 5 lines below.
|
||||
|
||||
## 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. Hover on a display-only card: make the card a link to its detail, or give it no hover.
|
||||
- `aria-current="page"` on the current route link, `"location"` on the current in-page section link.
|
||||
- Destructive confirms use `AlertDialog`, not `Dialog`, and cannot be dismissed while pending.
|
||||
- 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 their keyboard behavior; never re-implement it.
|
||||
- Announce async UI: loading and error messages use `role="status"` or `aria-live`.
|
||||
|
||||
## 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.
|
||||
|
||||
## Self-checks
|
||||
|
||||
After every UI edit, run these with ripgrep on `<files>`: each file you wrote or changed, by path, never `components/ui` or `components/reui`. A clean check prints nothing; a hit prints `file:line:code`. The last filter drops comment lines.
|
||||
|
||||
```sh
|
||||
# C1 banned motion: transition-all, bare or paint transitions, layout keys, bounce, overshoot, scale 0, hover and tap gestures, will-change
|
||||
rg -nH 'transition-all|transition(-property)?: *[^;]*\b(all|width|height|top|left|margin|padding|box-shadow|filter)\b|[\x27\x60" ]transition( +(duration|ease|delay|hover|focus|active|group|motion)|[\x27\x60"])|transition-shadow|transition-\[[^\]]*\b(width|height|top|left|right|bottom|inset|margin|padding|filter|radius|background-position|background-size|all)\b|animate-bounce|will-change|willChange|bounce: *(0?\.0*[1-9]|[1-9])|[\x27"](backIn|backOut|backInOut|anticipate)[\x27"]|(cubic-bezier\(|ease: *\[)([^,]*,|([^,]*,){3})[ _]*(-|1\.[0-9]*[1-9]|[2-9])|scale: *0 *[,}]|\bscale-0\b|while(Hover|Tap|Focus)=|(initial|animate|exit)=\{\{[^}]*\b(width|height|maxHeight|top|left|margin|padding|boxShadow|filter|backdropFilter|borderRadius)[A-Za-z]*:' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C2 off-scale motion: a duration off 0, 100, 150, 200 and 300 (duration-500 only on a media hover zoom), ease-in or ease-out, a stagger step above 0.05
|
||||
rg -nH 'duration-([1-9]|[1-9][0-9]|1(0[1-9]|[1-46-9][0-9]|5[1-9])|[23](0[1-9]|[1-9][0-9])|[4-9][0-9]{2}|[1-9][0-9]{3,})\b|duration-\[|\b([1-9]|[1-9][0-9]|1(0[1-9]|[1-46-9][0-9]|5[1-9])|[23](0[1-9]|[1-9][0-9])|[4-9][0-9]{2}|[1-9][0-9]{3,})ms\b|(^|[^0-9.])(0?\.(0[0-9]*|1[0-46-9][0-9]*|15[0-9]+|[23][0-9]+|[4-9][0-9]*)|1\.[0-9]+)s\b|duration: *([1-9]\b|0?\.(0[0-9]*|1[0-46-9][0-9]*|15[0-9]+|[23][0-9]+|[4-9][0-9]*)\b)|\bease-(in|out|in-out)\b|(staggerChildren: *|stagger\( *)(0?\.0[6-9]|0?\.[1-9]|[1-9])|delay: *[^,}]*\b(i|index|idx)\b *\* *(0?\.0[6-9]|0?\.[1-9]|[1-9])|delay: *(0?\.0[6-9]|0?\.[1-9]|[1-9])[0-9.]* *\* *\b(i|index|idx)\b' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C2 read by hand: a duration or delay that is not a number, every index delay (allowed: 0.05 or less on the first 6 items, as Math.min(i, 5) * 0.05, never on table rows)
|
||||
rg -nH '([dD]uration|[dD]elay): *[^0-9 .]|delay: *[0-9.]+ *\*' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C3 state color: raw palette on a line that names a state
|
||||
rg -nH '\b(text|bg|border|ring|fill|stroke)-(red|rose|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet)-[0-9]{2,3}\b' <files> | rg -i ':[0-9]+:.*\b(success|paid|active|completed?|done|approved|online|healthy|live|error|failed|critical|overdue|declined|rejected|cancell?ed|blocked|expired|warning|pending|degraded|due|info|processing|running|scheduled|queued)\b' | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C4 structure: a raw table, select or fixed overlay, framer-motion, Math.random() or Date.now()
|
||||
rg -nH '<table\b|<select\b|fixed inset-0|from ["\x27]framer-motion|Math\.random\(|Date\.now\(' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C4 on a base-* style only (skip on radix-*): Radix asChild
|
||||
rg -nH '\basChild\b' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
```
|
||||
|
||||
Fix every hit, run again, stop after the second run and list what is left with file and line. A C4 hit stays only when a comment in the file says why. These also stay, named on hand-back line 2: C1 on the motion.md sidebar row or on will-change set in a drag handler and removed when it ends; C2 on a media hover zoom; `Math.min(i, 5) * 0.05`.
|
||||
|
||||
No `rg` (a missing one also prints nothing): run each first pattern with your own search tool, skipping comment lines. No shell: read each changed file for what each `#` line names.
|
||||
|
||||
## Hand-back
|
||||
|
||||
End your reply with these 5 lines:
|
||||
|
||||
1. Installed: each item with its `previewUrl`.
|
||||
2. Motion: each animated element and its [motion.md](./motion.md) table row, or "none".
|
||||
3. States: covered, or which are missing.
|
||||
4. Checks: C1 to C4, screenshots or "not visually verified", the audit checklist, typecheck, lint; each one run, or "not run" and why.
|
||||
5. Deviations: any rule broken or check hit left, with file:line and why.
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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 ReUI components 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 (a full dashboard or settings page). Pro or Ultimate license at install. Its surfaces, text and borders use your theme tokens; its category accents (chart series, tags, avatars) are fixed palette colors paired for light and dark.
|
||||
- **icon** - Motion Icons in 4 styles (outline, solid, duotone, filled), static (`@reui/icons/default/<style>/<name>`), plus a hover-animated variant (`@reui/icons/animated/<style>/<name>`) only for icons that carry `installAnimated`. Ultimate license at install. See [icons.md](./icons.md).
|
||||
|
||||
## The @reui registry
|
||||
|
||||
ReUI items install through the shadcn CLI as `npx shadcn@latest add @reui/<name> --yes`, with `<name>` exactly as a ReUI tool returned it (`search`, a getter, `get_install_command`). 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.
|
||||
|
||||
## ReUI items vs shadcn/ui components
|
||||
|
||||
- **ReUI items** (the components, `c-*` examples, blocks, Motion Icons) install as `@reui/<name>`.
|
||||
- **shadcn/ui components** that ReUI builds on (`button`, `dialog`, `select`, `card`, ...) are not ReUI items. They install from shadcn by bare name: `npx shadcn@latest add button --yes`.
|
||||
- **`badge` and `alert` exist in both registries.** `@reui/badge` and `@reui/alert` are ReUI's components (the extra variants in [components.md](./components.md)); bare `badge` and `alert` are shadcn's.
|
||||
- **In an item's `registryDependencies`**, a `@reui/<name>` entry is a ReUI item and a bare name is a shadcn/ui item. `add` installs both for you.
|
||||
- **Templates are not registry items.** Download one from its page on reui.io (https://reui.io/templates?ref=skill); no `shadcn add` command installs a template.
|
||||
|
||||
## 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 take surfaces, text and borders from your theme tokens, so a theme change re-tints them; category accents stay fixed by design.
|
||||
|
||||
## Free vs premium
|
||||
|
||||
- **Free, no license key:** the ReUI components, all `c-*` examples, and this skill. The MCP is free too but still needs a free ReUI account (the agent signs in on first use); a license key is only for premium installs.
|
||||
- **Premium, license required at install:** blocks (Pro or Ultimate) and Motion Icons (Ultimate). Set `REUI_LICENSE_KEY` (see [cli.md](./cli.md)).
|
||||
- **Templates** are downloads, not installs: free ones need only a ReUI account, and each template's page on reui.io says what the others need.
|
||||
|
||||
## 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.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Theming: styles, radius, density and sizes
|
||||
|
||||
## Read your style
|
||||
|
||||
`components.json` `style` is `<base>-<style>`: the part after the first `-` names the style (`base-nova` is nova on Base UI). The style is baked into `components/ui` and `components/reui` at install, so the installed files hold your real values: read your installed `button.tsx` (usually `components/ui/button.tsx`) for heights, radius and padding before you build anything that sits beside a control.
|
||||
|
||||
## The style ladder
|
||||
|
||||
<!-- gen:style-ladder:start -->
|
||||
|
||||
| Style | Button `sm` / default / `lg` | Button radius | Card radius, shadow | Card spacing | Button text |
|
||||
| ----- | ---------------------------- | -------------- | ---------------------------------------------------- | ------------ | -------------------- |
|
||||
| vega | `h-8` / `h-9` / `h-10` | `rounded-md` | `rounded-xl`, `shadow-xs` | 6, sm 4 | `text-sm` |
|
||||
| nova | `h-7` / `h-8` / `h-9` | `rounded-lg` | `rounded-xl` | 4, sm 3 | `text-sm` |
|
||||
| maia | `h-8` / `h-9` / `h-10` | `rounded-4xl` | `rounded-2xl` | 6, sm 4 | `text-sm` |
|
||||
| lyra | `h-7` / `h-8` / `h-9` | `rounded-none` | `rounded-none` | 4, sm 3 | `text-xs` |
|
||||
| mira | `h-6` / `h-7` / `h-8` | `rounded-md` | `rounded-lg` | 4, sm 3 | `text-xs/relaxed` |
|
||||
| luma | `h-8` / `h-9` / `h-10` | `rounded-4xl` | `rounded-4xl`, `shadow-md` | 6, sm 4 | `text-sm` |
|
||||
| sera | `h-9` / `h-10` / `h-11` | `rounded-none` | square, `shadow-sm` | 8, sm 5 | `text-xs`, uppercase |
|
||||
| rhea | `h-7` / `h-8` / `h-9` | `rounded-2xl` | `rounded-[min(var(--radius-4xl),24px)]`, `shadow-sm` | 5, sm 4 | `text-sm` |
|
||||
|
||||
<!-- gen:style-ladder:end -->
|
||||
|
||||
Card spacing is the `--card-spacing` step (`--spacing(n)`) a Card pads with; `Card size="sm"` uses the sm step. The default Button text weight is `font-medium` (sera: `font-semibold`).
|
||||
|
||||
## What the style owns
|
||||
|
||||
- Never set what the style owns on its parts: radius (except `rounded-full`), height, horizontal padding, shadow, ring or font size. Pick `size` and `variant` instead.
|
||||
- A panel is a `Card` or a `Frame`, never a div with `rounded-*`, `border` and `bg-card`; set density with `Card size="sm"` or Frame `spacing`.
|
||||
- A bespoke element beside a control (a chip, a custom trigger) takes its height and radius from that control's row above.
|
||||
- A focusable control you build copies the focus classes of your installed `input.tsx`.
|
||||
- A variant before a class: `className` is for layout (placement, width, gap). `size-*` when width equals height, `cn()` for conditional classes (never a template literal), a scale step before an arbitrary value (`w-60`, not `w-[240px]`), no inline `style` for color or spacing, and no `z-index` on dialogs, sheets or popovers.
|
||||
- Change the look through the theme CSS variables in `:root` and `.dark` (`--radius`, `--primary` and the rest) or with a style switch (a new `style` in `components.json`, which means reinstalling the components; ask the user first). Never edit installed files to restyle them.
|
||||
|
||||
## Components with no per-style values
|
||||
|
||||
`event-calendar`, `gantt`, `icon-stack`, `kanban`, `rating`, `scrollspy`, `sortable` and `timeline` install the same file in every style. Their look comes from your theme tokens and the parts you put inside them (a `Card` in a kanban column follows the style), so never copy another style's radius onto them.
|
||||
@@ -1,66 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
- A full page or section (dashboard, billing, auth, pricing, settings): `compose_page` first (step 1).
|
||||
- A data table with sorting, filtering, pagination or virtualization: the `data-grid` component, never a hand-rolled `<table>`.
|
||||
- A drag-and-drop board: the `kanban` component.
|
||||
- Column filters, a date range, a tree, a stepper and the like: the matching ReUI component.
|
||||
- A single generic control shadcn already has (Button, Dialog, Select): plain shadcn.
|
||||
|
||||
## 1. Find (ReUI MCP `search` / `compose_page`)
|
||||
|
||||
**Full multi-section page ask?** Call `compose_page(intent)` FIRST (`sections`, 1 to 10, caps how many sections, default 8), before searching block-by-block. It returns ordered sections, each with the best block for the intent (top pick + alternates); on a free account it answers `mode: "free"` and fills the same sections with free `c-*` examples instead, so the plan is always buildable. 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"` (a ReUI component), `"example"` (a c-\* use-case), `"block"` (a full page/section), `"icon"`.
|
||||
- `component`: the ReUI component the request implies (`"data-grid"`, `"kanban"`, ...).
|
||||
- `category`, `free`.
|
||||
|
||||
Example: "build a users management page with filters" -> `search({ query: "users management page with filters", type: "block", component: "data-grid" })` (on a free account use `type: "example"`).
|
||||
|
||||
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.
|
||||
|
||||
**Pick with the guidance, not the title.** Look-alikes are common (dialog, sheet and drawer; dozens of data-grid examples and app shells). Before installing, read the item's guidance: `get_component` returns `whenToUse` and `whenNotToUse`, `get_example` and `get_block` return `bestFor` (what sets the item apart from its siblings), `agentNotes` and `whenNotToUse`, and a block adds `categoryGuidance` (its category's key note), and `get_examples` rows carry `bestFor`. Each `whenNotToUse` entry names the item to use instead and its `type` (`component` -> `get_component`, `example` -> `get_example`, `block` -> `get_block`, `block-category` "<group>/<category>" -> `search({ query, type: "block", category: "<category>" })`); when the user's situation matches its `when`, switch. Apply every `agentNotes` line before you ship: it names the real identifiers to replace or wire (sample data, stubbed actions, required props). An item with no guidance yet has none of these keys; fall back to its description and thumbnail.
|
||||
|
||||
Results are scoped to the account's plan, so you are never shown an item the user could not install: blocks need Pro, Motion Icons need Ultimate. A `type` hint the plan cannot use (or a query whose only matches are premium) comes back as a normal HTTP 200 answer with `locked: true` and `requiredPlan` rather than results - that is a paywall, not a missing item, so switch to free components and `c-*` examples and mention https://reui.io/pricing?ref=skill.
|
||||
|
||||
**Show the preview.** List every item with its `previewUrl` (a live preview page). Where a result carries `thumbnail` (blocks, `c-*` examples and icons), show the image as markdown linked to `previewUrl`, so the user sees it before you install.
|
||||
|
||||
## 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 deeper API, call **`get_component(names)`** with ALL of `componentsUsed` in ONE call (it accepts an array) and read each inline `api` - no web fetch. A very large API is trimmed on heading boundaries to fit your context and lists what it dropped in `sectionsOmitted`; when the part you need is in that list, re-read it as the response's `next` hint says rather than guessing. `validate_usage` always checks the FULL API, so it still catches a prop you invented from a trimmed capsule. **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. Write against your project's base (see [components.md](./components.md)). After `add`:
|
||||
|
||||
1. **Read the added files**; keep the composition intact. For a block, verify the components are wired correctly (for `data-grid`: a `useTable({ features: dataGridFeatures, ... })` instance passed as `table`, `recordCount` set - see [components.md](./components.md)).
|
||||
2. **Replace demo data with the user's real data** via typed structures (see [adapting.md](./adapting.md)).
|
||||
3. **Fix icon imports** to the project's icon library (see [icons.md](./icons.md)).
|
||||
4. **Align styling** to the active theme: semantic tokens for anything that carries meaning, raw palette only for categorical identity and always with its `dark:` pair (see [color.md](./color.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, motion that leaves installed components alone, and mobile-first responsiveness (see [layout.md](./layout.md) and [motion.md](./motion.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.
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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; Pro unlocks premium blocks, Ultimate adds Motion Icons, and either removes the daily request limit by default. 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 (`sections` is a number, 1 to 10: the most sections to return, default 8); it returns ordered sections, each with the best premium block for the intent (top pick + alternates). On a free account (or in free-only mode) it answers with `mode: "free"` and composes the same sections from free `c-*` examples instead, so the plan is always buildable. 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. Only some icons have a real hover animation: `animated: true` returns only those, and a concept with none comes back empty with an `animatedNote`. Each icon carries a `thumbnail` of its 4 styles; look at it before trusting a name. An old icon name still resolves (`renamedFrom` names it), unless it is also an ordinary word icons are named for today (`watch`): then it ranks today's icons for the word and `oldNameNotes` names the icon the old name installs. Motion Icons are Ultimate for **discovery as well as install**: without an Ultimate credential this returns `locked: true` and zero concepts, so check the plan before promising the user icons.
|
||||
- **`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.
|
||||
- **`get_thumbnail`** - when the choice is VISUAL and the ranking has not already made it: 2 or more candidates within about 15 score points. When the top hit leads by a wide margin, or `missedTerms` already rules the others out, skip it; an image costs roughly 10 times a result row. It returns the hosted preview image as image content so you can look yourself; pass up to 4 names in one call to compare `compose_page` alternates side by side before installing any of them. To let the USER see something, share the item's `thumbnail` and `previewUrl` instead - this tool is for your own eyes, and a client that cannot render images gets the URLs in the text block.
|
||||
- **`get_agent_skill`** - when the agent has no local ReUI skill (a cloud or tools-only client), or to check whether the installed one is stale. With no `topic` it returns the core workflow, the topic index and the installer; `topic` (for example `motion`) returns that one file in full. Compare its `version` with the one stamped atop your local `SKILL.md`.
|
||||
|
||||
## All 19 tools
|
||||
|
||||
`search`, `get_block`, `get_example`, `get_icon`, `get_thumbnail`, `list_block_groups`, `list_block_categories`, `list_example_categories`, `list_icon_categories`, `list_components`, `get_component`, `get_examples`, `search_icons`, `compose_page`, `validate_usage`, `get_install_command`, `get_project_context`, `get_agent_skill`, `get_audit_checklist`. The MCP serves the full parameter schemas; do not guess parameters beyond them.
|
||||
|
||||
## Commands (MCP prompts)
|
||||
|
||||
In Claude Code the ReUI MCP adds these prompts; other agents show MCP prompts under their own names, or not at all. Plain language runs the same loop.
|
||||
|
||||
- `/mcp__reui__build <what>`: compose a page, section or feature (plan, install, read the API, adapt, audit).
|
||||
- `/mcp__reui__add <item>`: find, install and wire in 1 component, example, block or icon.
|
||||
- `/mcp__reui__fix [target]`: fix ReUI usage (undocumented props, a base or radix mismatch, missing states, accessibility, scroll).
|
||||
- `/mcp__reui__improve [target]`: refine existing ReUI UI (hierarchy, density, states, responsive, motion).
|
||||
|
||||
## 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`, `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.
|
||||
- An inline `api` is not always the whole API: a very large one is trimmed on heading boundaries to fit your context, and the response names every dropped heading in `sectionsOmitted` plus a `next` hint for re-reading one. Never treat a trimmed capsule as complete; `validate_usage` still checks the FULL API.
|
||||
- Icon results and `get_icon` include `animated: true` and `installAnimated` only when the icon has a real hover animation (animated: `@reui/icons/animated/<style>/<name>`; static: `@reui/icons/default/<style>/<name>`). Use the strings given; never build an animated path by hand, it installs a static copy for every other icon.
|
||||
- Blocks, `c-*` examples and icons carry `thumbnail`, an absolute URL to a hosted preview image (light theme; blocks 1080x720, examples 900x600, icons 640x200 with the 4 styles side by side). Show the top 2 or 3 as markdown images linked to `previewUrl` so the user picks by eye; use `get_thumbnail` when YOU need to see them. Components have no thumbnail.
|
||||
- Each result carries `missedTerms`, the words from YOUR query it does not mention (omitted when it matches them all). A high score with 3 missed words is a near miss wearing a good score: read this before `get_thumbnail`, it is far cheaper.
|
||||
- `weakMatch: true` means the top result misses more of the query's words than it matches; `unmatchedTerms` lists the words none of the top results mention and `weakMatchNote` says what to do. Stop and re-search with the registry's own words, or compose from components, instead of installing the top hit.
|
||||
- On a free account `premiumPicks` (a sibling of `results`, never inside it) lists up to 3 premium blocks that fit the query more closely than the free answer, as previews with no install command, plus `unlock` with the plan and a pricing link. Mention the upgrade at most once per conversation, then keep building with what the plan covers. `weakMatch` and `premiumPicks` can appear together: the free answer is weak AND a premium block fits; `weakMatchNote` says so.
|
||||
- Guidance is getter-only, never on search rows: `get_component` adds `whenToUse`, `whenNotToUse` and `agentNotes`; `get_example` and `get_block` add `bestFor`, `agentNotes` and `whenNotToUse`, and `get_block` also `categoryGuidance`; `get_examples` rows add `bestFor`. Answers carry the key lines; the item page's `.md` twin has every line. Each `whenNotToUse` entry is `{ when, instead, type }`: fetch `instead` with the getter its `type` names.
|
||||
- Results are scoped to the caller's plan, so a free account never sees a block or icon it could not install. When a search matches only hidden premium items, the answer says so (`locked`, `requiredPlan`, `premiumMatches`) instead of pretending the registry is empty.
|
||||
|
||||
## Error playbook
|
||||
|
||||
- **401** - the MCP requires a signed-in ReUI account, on every request (there is no anonymous access). 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?ref=skill) as `Authorization: Bearer`.
|
||||
- **locked result** - a valid account whose plan does not cover the item. This is NOT an error and NOT a 403: it comes back as a normal HTTP 200 result carrying `locked: true` and `requiredPlan` (`"pro"` for premium blocks, `"ultimate"` for Motion Icons). Keep working with the free components and `c-*` examples, and point the user at https://reui.io/pricing?ref=skill if they want the locked item.
|
||||
- **daily allowance reached** - a free account has a per-account, per-UTC-day allowance on tool calls (ReUI sets the number and can change it, so read it from the message and never assume one). It comes back as a normal tool RESULT with `isError: true` at HTTP 200: not an HTTP error status, no back-off header to honor, and no per-minute limit to wait out. Retrying the same call just spends the next unit, so surface the message (it carries the reset and the upgrade link) to the user instead of looping. The count resets at UTC midnight, and using a Pro or Ultimate license as the MCP credential removes the limit.
|
||||
- **not found** (`found: false`) - use the returned `suggestions`, or `search`. 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?ref=skill
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
name: reui
|
||||
description: "ReUI design and component rules for React apps on shadcn/ui and Tailwind. Use for any UI work in a project with @reui in components.json or a components/reui folder, and in any shadcn project that asks for data grids, kanban boards, dashboards or full pages: building or restyling pages, forms and tables; colors, theme tokens and dark mode; spacing, typography and layout; animation and hover motion; installing ReUI components, examples, Pro blocks or Motion Icons. Works with the ReUI MCP."
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
metadata:
|
||||
user-invocable: "false"
|
||||
---
|
||||
|
||||
> **ReUI skill version `68f0e6fa91`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer in the mode it was installed with to update this skill: `get_agent_skill` -> `install.recommended` from the project root, with `REUI_GLOBAL=1` before `node` when this file is `~/.claude/skills/reui/SKILL.md` or `~/.agents/skills/reui/SKILL.md` (a global install). Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI
|
||||
|
||||
ReUI is an existing design system on shadcn/ui. Keep its look. Generic frontend advice (vary fonts or themes, gradients, page-load or staggered reveals, "2-3 intentional motions", Framer Motion) does not apply here.
|
||||
It ships free components, free `c-*` examples, Pro blocks and Ultimate Motion Icons through the shadcn CLI; the ReUI MCP finds them and serves their APIs.
|
||||
|
||||
## Order of authority
|
||||
|
||||
1. The installed style (`components.json` `style`, baked into `components/ui` and `components/reui`): it owns radius, height, padding, shadow and overlay motion.
|
||||
2. The installed component's variants and props (`size`, `variant`, `spacing`).
|
||||
3. These rules and the topic files.
|
||||
4. Other design skills, then your taste: where they disagree with 1 to 3, ReUI wins. Never redefine `--ease-out` or any Tailwind built-in.
|
||||
|
||||
## Before you write UI code, write this plan in your reply
|
||||
|
||||
1. Surface: operational, marketing or flow.
|
||||
2. Archetype: 1 from [compose.md](./rules/compose.md).
|
||||
3. Container family: Frame-led, Card-led, or Cards for the KPIs and a Frame for the table.
|
||||
4. Density: dense (`size="sm"` on controls that take it) or default.
|
||||
5. Regions: top to bottom, each with the item that fills it.
|
||||
6. States: empty, loading and error for each data region.
|
||||
7. Motion: none, or the 1 entrance.
|
||||
|
||||
Then build exactly that plan; if you deviate, say where.
|
||||
|
||||
## The loop
|
||||
|
||||
1. **Find**: `compose_page` for a full page, else `search` with `type` and `component` hints.
|
||||
2. **Install**: `npx shadcn@latest add @reui/<name> --yes`, with `<name>` exactly as a tool returned it (shadcn/ui parts install by bare name: `npx shadcn@latest add button --yes`).
|
||||
3. **Read the API on your base**: `components.json` style `base-nova` is Base UI, `radix-nova` is Radix. Call `get_component([...componentsUsed])` once, then install 1 `get_examples` example and read its files.
|
||||
4. **Adapt**: change data, copy and props; keep the structure and classes.
|
||||
5. **Check**, after every UI edit, follow-ups included: `validate_usage` on any name you did not read, the self-checks C1 to C4 in [quality.md](./rules/quality.md), [anti-patterns.md](./rules/anti-patterns.md), `get_audit_checklist`, typecheck, lint. Hand back in the 5 lines quality.md gives.
|
||||
|
||||
Show every item's `previewUrl` whenever you list, recommend or install it.
|
||||
|
||||
## Rules that always apply
|
||||
|
||||
- **Owners**: tables `data-grid`, filtering `filters`, boards `kanban`, modals `Dialog` (destructive confirms `AlertDialog`), toasts `sonner`; never hand-roll.
|
||||
- **Color**: semantic tokens for meaning; state colors through Badge or Alert variants (`@reui/badge` and `@reui/alert`, not shadcn's); raw palette only for categorical identity, always with its `dark:` pair.
|
||||
- **Contrast**: never `text-success` or `text-warning` as body text in light mode, and never `text-<state>-foreground` on a solid `bg-<state>` fill.
|
||||
- **Shape**: no `rounded-*` except `rounded-full`, and no `shadow-*`, `h-*` or `px-*` on Button, Input, SelectTrigger, Card or Frame; pick `size` and `variant`. A panel is a Card or Frame, never a div with `rounded-lg border`.
|
||||
- **Spacing**: `gap-*` on the parent, not `space-y-*`; `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections; title to description `gap-0.5`.
|
||||
- **Type**: `text-sm` body, `text-xs` meta (controls keep their style's size), `font-medium` or `font-semibold`, headings `tracking-tight`, numbers `tabular-nums`; nothing below `text-xs` except illustrative mini-UI.
|
||||
- **Motion**: installed components own their motion. Your own code uses `transition-colors` for hover and press, `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]` to reveal, `duration-300` with the same curve at most (media hover zoom: 500), and `SPRING` (`{ type: "spring", visualDuration: 0.25, bounce: 0 }`) for drag and reorder. At most 1 entrance animation per view (a stagger counts as 1). Animate only `transform` and `opacity`, CSS before motion/react, never set state per frame; `<MotionConfig reducedMotion="user">` once at the root plus `motion-reduce:` on CSS, where reduced motion means a calm fade, not nothing.
|
||||
- **Surfaces**: prefer 1 container family per screen. With a KPI row and a table, use FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame or a Frame inside a Card. Icons come from 1 library, the `iconLibrary` in `components.json` (on Ultimate, 1 Motion Icon on the primary action is the exception).
|
||||
- **States**: each data region has empty (1 action), loading (a `Skeleton` in the real layout) and error (Retry) states; light and dark work.
|
||||
- **Never invent**: never write a prop, variant or `@reui/` name you did not read in an `api`, an installed file or a tool result.
|
||||
|
||||
## Topics
|
||||
|
||||
Always read `compose` before building a screen, and `quality` plus `anti-patterns` at the Check step. Besides those, read only the topic files the task touches, at most 4 (for example `color`, `motion`, `layout` and `components`).
|
||||
|
||||
Installed skill: open the file. MCP only: call `get_agent_skill` with the topic, or fetch `https://mcp.reui.io/skill/reui/<file>`.
|
||||
|
||||
| Topic | File | Read when |
|
||||
| --------------- | -------------------------------------------------- | --------------------------------------------- |
|
||||
| `compose` | [rules/compose.md](./rules/compose.md) | before building any screen or section |
|
||||
| `workflow` | [rules/workflow.md](./rules/workflow.md) | `compose_page`, search hints, installing |
|
||||
| `adapting` | [rules/adapting.md](./rules/adapting.md) | changing an installed block or example |
|
||||
| `components` | [rules/components.md](./rules/components.md) | `data-grid`, `filters`, `kanban` and the rest |
|
||||
| `color` | [rules/color.md](./rules/color.md) | any color, status, chart or dark mode choice |
|
||||
| `theming` | [rules/theming.md](./rules/theming.md) | the styles, radius, density and sizes |
|
||||
| `motion` | [rules/motion.md](./rules/motion.md) | any transition, animation or motion/react |
|
||||
| `layout` | [rules/layout.md](./rules/layout.md) | house defaults, hierarchy, surfaces, states |
|
||||
| `icons` | [rules/icons.md](./rules/icons.md) | icon imports, Motion Icons |
|
||||
| `quality` | [rules/quality.md](./rules/quality.md) | the done gate: self-checks, a11y, hand-back |
|
||||
| `anti-patterns` | [rules/anti-patterns.md](./rules/anti-patterns.md) | the Check step: drift pairs, stale habits |
|
||||
| `registry` | [rules/registry.md](./rules/registry.md) | free vs Pro vs Ultimate, base vs radix |
|
||||
| `cli` | [rules/cli.md](./rules/cli.md) | `components.json`, license key, install flags |
|
||||
| `tools` | [tools.md](./tools.md) | MCP result shapes, errors, quota, commands |
|
||||
|
||||
## Plans
|
||||
|
||||
Free covers components, `c-*` examples, this skill and the MCP (a ReUI account and a daily allowance). Pro adds blocks. Ultimate adds Motion Icons. A plan miss is a normal answer with `locked: true`; keep building with what the plan covers.
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "ReUI"
|
||||
short_description: "ReUI design and component rules for React apps on shadcn/ui and Tailwind."
|
||||
default_prompt: "Use ReUI to build this UI with installed components and the ReUI motion and color rules."
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
- Never read `window`, `localStorage`, `location`, `Date.now()` or `Math.random()` in render or in a `useState` initializer; read them in an effect.
|
||||
- **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, and the layout must still hold with long names, empty values and crowded data.
|
||||
@@ -1,117 +0,0 @@
|
||||
# Anti-patterns
|
||||
|
||||
Read this at the Check step, after the self-checks in [quality.md](./quality.md). Each entry: an `Incorrect:` line, then its `Correct:` fix. Detector: the self-check that prints it (C1 to C4), or `visual` when reading finds it. Source: the pilot run that wrote it, or `default` (a training-data habit).
|
||||
|
||||
## Pairs
|
||||
|
||||
### 1. Stagger on every row (Detector: C2; source: run-08)
|
||||
|
||||
Incorrect: `transition={{ delay: i * 0.05 }}` on all 50 rows of a table
|
||||
Correct: 1 stagger of at most 6 items, `staggerChildren: 0.05`, never on table rows
|
||||
|
||||
### 2. Transition everything on a card (Detector: C1; source: runs 10, 15)
|
||||
|
||||
Incorrect: `className="rounded-xl border transition-all duration-300 hover:shadow-lg"`
|
||||
Correct: `transition-colors` for a hover color; a lift is `transform` over a static shadow
|
||||
|
||||
### 3. Raw palette for a state (Detector: C3; source: runs 08, 15)
|
||||
|
||||
Incorrect: `<span className="text-green-600">Active</span>`
|
||||
Correct: `<Badge variant="success-light">Active</Badge>`
|
||||
|
||||
### 4. Animated table rows (Detector: C1; source: runs 01, 08, 17)
|
||||
|
||||
Incorrect: `<motion.tr whileHover={{ backgroundColor: "var(--muted)" }}>`
|
||||
Correct: `data-grid` rows, which already hover; never animate rows with motion/react
|
||||
|
||||
### 5. Hover scale on the default spring (Detector: C1; source: runs 01, 10)
|
||||
|
||||
Incorrect: `<motion.div whileHover={{ scale: 1.03 }}>` with no `transition`
|
||||
Correct: CSS first: `transition-colors` for hover, `transition-transform active:scale-[0.98] motion-reduce:transition-none` for press
|
||||
|
||||
### 6. Entrance from scale 0 (Detector: C1; source: runs 10, 15)
|
||||
|
||||
Incorrect: `<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }}>`
|
||||
Correct: `initial={{ opacity: 0, scale: 0.95 }}`: start at scale 0.95 or more and move 8px or less
|
||||
|
||||
### 7. Hand-built modal or toast (Detector: C4; source: run-15)
|
||||
|
||||
Incorrect: `<div className="fixed inset-0 z-50 bg-black/50">` as a modal
|
||||
Correct: `Dialog`, `AlertDialog` for a destructive confirm, `sonner` for toasts; a palette: `<CommandDialog><Command><CommandInput /><CommandList>`, as `CommandDialog` holds no `Command`
|
||||
|
||||
### 8. Native select (Detector: C4; source: run-15)
|
||||
|
||||
Incorrect: `<select>` with `<option>` children
|
||||
Correct: the shadcn `Select`, or `NativeSelect` when the native control is wanted
|
||||
|
||||
### 9. Hand-rolled table (Detector: C4; source: run-17)
|
||||
|
||||
Incorrect: `<table>` with mapped `<tr>` rows
|
||||
Correct: `data-grid`: a `useTable({ features: dataGridFeatures })` instance passed as `table={table}`, columns typed `ColumnDef<DataGridFeatures, Order>[]`
|
||||
|
||||
### 10. Button inside a clickable wrapper (Detector: visual; source: run-02)
|
||||
|
||||
Incorrect: `<motion.button><Button>Save</Button></motion.button>`
|
||||
Correct: the `Button` alone: 1 interactive element, and it already has a press state
|
||||
|
||||
### 11. Radix asChild on Base UI (Detector: C4; source: run-17, judge)
|
||||
|
||||
Incorrect: `<DialogTrigger asChild><Button>Open</Button></DialogTrigger>`
|
||||
Correct: `<DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>` on base-nova; a link: `<Button nativeButton={false} render={<Link href="/x" />}>`
|
||||
|
||||
### 12. Component defined inside render (Detector: visual; source: Haiku runs, judge)
|
||||
|
||||
Incorrect: `const Row = () => <div />` inside the page component
|
||||
Correct: declare `Row` at module scope and pass it props, so React keeps its state
|
||||
|
||||
### 13. A panel inside a panel (Detector: visual; source: run-05)
|
||||
|
||||
Incorrect: `<FramePanel><Card>...</Card></FramePanel>`, or a Card inside a Card
|
||||
Correct: FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame
|
||||
|
||||
### 14. Small badges (Detector: visual; source: runs 03, 04, 06, 07, 10, 13, 16, 18)
|
||||
|
||||
Incorrect: `<Badge variant="success-light" size="sm">Paid</Badge>`
|
||||
Correct: `<Badge variant="success-light">Paid</Badge>` with `size` left off
|
||||
|
||||
### 15. A colored circle behind every KPI icon (Detector: visual; source: default)
|
||||
|
||||
Incorrect: `<div className="rounded-full bg-blue-100 p-2">` around each KPI icon
|
||||
Correct: the number leads; at most 1 `text-muted-foreground` icon
|
||||
|
||||
### 16. Gradient text (Detector: visual; source: default)
|
||||
|
||||
Incorrect: `bg-linear-to-r from-violet-500 to-pink-500 bg-clip-text text-transparent` on a heading
|
||||
Correct: `text-foreground` and `tracking-tight`; color carries meaning, not decoration
|
||||
|
||||
### 17. A dot or check for the current item (Detector: visual; source: default)
|
||||
|
||||
Incorrect: a dot or a `Check` icon beside the active nav item
|
||||
Correct: `bg-muted` (`dark:bg-muted/50`) plus `aria-current`
|
||||
|
||||
### 18. framer-motion (Detector: C4; source: default)
|
||||
|
||||
Incorrect: `import { motion } from "framer-motion"`
|
||||
Correct: `import { motion } from "motion/react"`
|
||||
|
||||
### 19. Clock or randomness in render (Detector: C4; source: default, near run-05)
|
||||
|
||||
Incorrect: `const [id] = useState(() => Math.random())`
|
||||
Correct: `useId()` for ids; read the clock in an effect or an event handler
|
||||
|
||||
### 20. Placeholder data (Detector: visual; source: default)
|
||||
|
||||
Incorrect: "John Doe", "Acme Inc" and round figures like `$1,000`
|
||||
Correct: specific, plausible data: "Mara Lind", `$1,284.50`, 37 open orders
|
||||
|
||||
## Your training data is out of date
|
||||
|
||||
| Old habit | In this stack |
|
||||
| ----------------------------- | --------------------------------------------------------------- |
|
||||
| `framer-motion` | `motion/react` |
|
||||
| `<DataGrid data columns>` | `useTable({ features: dataGridFeatures })` plus `table={table}` |
|
||||
| A fixed div modal | `Dialog` or `AlertDialog` |
|
||||
| A hand-built toast | `sonner` |
|
||||
| `w-5 h-5` | `size-5` |
|
||||
| never `space-y-4` in new code | `flex flex-col gap-4` |
|
||||
| `asChild` | `render` on base-nova |
|
||||
@@ -1,77 +0,0 @@
|
||||
# CLI: registry setup, license, non-interactive install
|
||||
|
||||
## Registry setup (one-time, per project)
|
||||
|
||||
Free items (the ReUI 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 and Motion Icons) require a ReUI license at install:
|
||||
|
||||
1. Add the key to `.env.local`:
|
||||
|
||||
```bash
|
||||
REUI_LICENSE_KEY=your-license-key
|
||||
```
|
||||
|
||||
2. Switch `components.json` to the authenticated object form:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${REUI_LICENSE_KEY}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`. MCP client configs expand environment variables too, but each client has its OWN syntax, so wire the ReUI MCP server with the form that client understands:
|
||||
|
||||
- Claude Code (`.mcp.json`, `~/.claude.json`): `"Authorization": "Bearer ${REUI_LICENSE_KEY}"`
|
||||
- Cursor (`.cursor/mcp.json`) and VS Code: `"Authorization": "Bearer ${env:REUI_LICENSE_KEY}"`
|
||||
- OpenCode (`opencode.json`): `"Authorization": "Bearer {env:REUI_LICENSE_KEY}"`
|
||||
- Codex (`~/.codex/config.toml`): `bearer_token_env_var = "REUI_LICENSE_KEY"` - Codex reads the variable itself, so there is no header to write
|
||||
|
||||
Only a client with no interpolation at all needs the raw `reui_pat_...` token, and then only in a file that is never committed. Copying the `components.json` form into a client that does not expand it is the common failure: the literal `${REUI_LICENSE_KEY}` placeholder is sent to the server as the credential and every call comes back 401.
|
||||
|
||||
The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry?ref=skill
|
||||
|
||||
## 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.
|
||||
|
||||
`<name>` is the item name exactly as a ReUI tool returned it. The `@reui/` prefix is for ReUI items only:
|
||||
|
||||
- The shadcn/ui components ReUI builds on (`button`, `dialog`, `select`, `card`, ...) are not ReUI items. Install them from shadcn by bare name: `npx shadcn@latest add button --yes`.
|
||||
- `badge` and `alert` exist in both registries: `@reui/badge` and `@reui/alert` are ReUI's components, bare `badge` and `alert` are shadcn's.
|
||||
- In an item's `registryDependencies`, a `@reui/<name>` entry is a ReUI item and a bare name is a shadcn/ui item. `add` installs both, so you never add them by hand.
|
||||
- Templates are not registry items. Download a template from its page on reui.io (https://reui.io/templates?ref=skill); no `shadcn add` command installs one.
|
||||
|
||||
## 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 ReUI 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/...`) need Ultimate.
|
||||
- Templates are downloads from their page on reui.io, not installs: free ones need only a ReUI account, and each template's page says what the others need.
|
||||
|
||||
If an install 401/403s, the license key is missing, invalid, or the plan does not cover that resource (blocks: Pro or higher; icons: Ultimate). Point the user to https://reui.io/account?ref=skill (their key) or https://reui.io/pricing?ref=skill (upgrade).
|
||||
|
||||
If an install 404s, the name is not a ReUI item. Read the CLI's error message, which names the right install command when there is one, or look the name up with `get_install_command` or `search`.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Color
|
||||
|
||||
Semantic tokens carry meaning; raw palette carries identity. Check every color in light and dark.
|
||||
|
||||
## Tokens
|
||||
|
||||
Surfaces, text and borders use your theme tokens (`background`, `foreground`, `card`, `muted`, `muted-foreground`, `primary`, `accent`, `destructive`, `border`, `ring`), so a theme change re-tints them. ReUI adds state tokens: `--success`, `--info` and `--warning` with their `-foreground`, `--destructive-foreground` (beside shadcn's `--destructive`) and `--invert` with `--invert-foreground` for inverted surfaces.
|
||||
|
||||
They are not part of shadcn's base theme. Installing `@reui/badge`, `@reui/alert` or `@reui/code-block` adds them to your global CSS; until one of those is installed, classes such as `bg-success` generate nothing. Check for `--success:` in your global CSS. If it is missing, add these values to `:root` and `.dark`, plus a `--color-<name>: var(--<name>)` line for each under `@theme inline`:
|
||||
|
||||
<!-- gen:tokens:start -->
|
||||
|
||||
| Token | `:root` | `.dark` |
|
||||
| -------------------------- | -------------------------- | -------------------------- |
|
||||
| `--destructive-foreground` | `var(--color-red-800)` | `var(--color-red-600)` |
|
||||
| `--success` | `var(--color-emerald-500)` | `var(--color-emerald-500)` |
|
||||
| `--success-foreground` | `var(--color-emerald-900)` | `var(--color-emerald-600)` |
|
||||
| `--info` | `var(--color-violet-500)` | `var(--color-violet-500)` |
|
||||
| `--info-foreground` | `var(--color-violet-900)` | `var(--color-violet-600)` |
|
||||
| `--warning` | `var(--color-yellow-500)` | `var(--color-yellow-500)` |
|
||||
| `--warning-foreground` | `var(--color-yellow-900)` | `var(--color-yellow-600)` |
|
||||
| `--invert` | `var(--color-zinc-900)` | `var(--color-zinc-700)` |
|
||||
| `--invert-foreground` | `var(--color-zinc-50)` | `var(--color-zinc-50)` |
|
||||
|
||||
<!-- gen:tokens:end -->
|
||||
|
||||
## Meaning vs identity
|
||||
|
||||
- Meaning (a state, emphasis, a surface, text) takes semantic tokens, through the Badge or Alert variant that already carries them.
|
||||
- Identity (a chart series, a tag color, a team, an avatar) may use raw palette: ReUI's own lint keeps it on purpose. Always hand-pair it for dark, as in `text-violet-600 dark:text-violet-400`.
|
||||
- **Incorrect:** `<span className="text-green-600">Active</span>` (raw palette for a state). **Correct:** `<Badge variant="success-light">Active</Badge>`.
|
||||
|
||||
## State recipes
|
||||
|
||||
- State badge: `<Badge variant="success-light">` from `@/components/reui/badge` (`@reui/badge`; shadcn's `components/ui/badge` has no `-light` variants or `size`), also `info-light`, `warning-light`, `destructive-light`, and `primary-light` for brand or pending. Neutral badge: `variant="outline"`. A solid `<Badge variant="success">` (white text) only when the user asks for a solid fill.
|
||||
- Inline status text: `text-success-foreground dark:text-success`, the pair the ReUI Badge uses.
|
||||
- Tinted surface: `border-success/15 bg-success/10 text-success-foreground dark:border-success/25 dark:bg-success/15 dark:text-success` (the Badge's `success-light` recipe).
|
||||
- A message block: `<Alert variant="success">` from `@/components/reui/alert` (`@reui/alert`), also `info`, `warning` and `destructive`.
|
||||
- Never `text-success` or `text-warning` as body text in light mode, and never `text-<state>-foreground` on a solid `bg-<state>` fill.
|
||||
|
||||
## Contrast
|
||||
|
||||
<!-- gen:contrast:start -->
|
||||
|
||||
Light is `text-<tone>-foreground` on `background` and on the `bg-<tone>/10` tint; dark is `dark:text-<tone>` on `background` and on the `dark:bg-<tone>/15` tint. Ratios against the neutral theme, rounded down; body text needs 4.5:1.
|
||||
|
||||
| Tone | Light | Light on tint | Dark | Dark on tint |
|
||||
| ------------- | ----- | ------------- | ---- | ------------ |
|
||||
| `success` | 9.6 | 8.8 | 8.0 | 6.6 |
|
||||
| `warning` | 8.6 | 8.1 | 10.3 | 8.1 |
|
||||
| `info` | 11.0 | 9.7 | 4.4 | 3.9 |
|
||||
| `destructive` | 8.3 | 7.0 | 6.8 | 5.7 |
|
||||
|
||||
Raw state color as light-mode text on `background`: `text-success` 2.4, `text-warning` 1.9, `text-info` 4.4.
|
||||
|
||||
<!-- gen:contrast:end -->
|
||||
|
||||
Info text in dark mode is under 4.5:1: where it must be read, write the words in `text-foreground` and color only the icon.
|
||||
|
||||
## Dark mode
|
||||
|
||||
- shadcn uses the class strategy: `@custom-variant dark (&:is(.dark *));` in your global CSS, and `.dark` on `<html>` turns it on.
|
||||
- Tokens switch by themselves; raw palette needs its `dark:` pair.
|
||||
- Check both themes before you hand back.
|
||||
|
||||
## Charts
|
||||
|
||||
Check your theme's `--chart-1` to `--chart-5` in both modes. The current shadcn themes keep the same chart colors in light and dark; some themes (reui.io's own among them) change hue. For a series whose identity must hold in both modes, use a palette var such as `var(--color-violet-500)`, as ReUI blocks do.
|
||||
|
||||
<!-- gen:charts:start -->
|
||||
|
||||
Same chart colors in light and dark: neutral, stone, zinc, mauve, olive, mist, taupe, amber, blue, cyan, emerald, fuchsia, green, indigo, lime, orange, pink, purple, red, rose, sky, teal, violet, yellow. Changes hue between light and dark: reui.io's own theme.
|
||||
|
||||
<!-- gen:charts:end -->
|
||||
@@ -1,521 +0,0 @@
|
||||
# ReUI components
|
||||
|
||||
The ReUI components: `alert`, `autocomplete`, `badge`, `cascader`, `code-block`, `color-picker`, `data-grid`, `date-selector`, `emoji-picker`, `event-calendar`, `file-type`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `signature-pad`, `sortable`, `stepper`, `time-picker`, `timeline`, `tree`. Examples and blocks are composed from these. `file-upload` is a ReUI component too, shipped as the `use-file-upload` hook rather than a primitive, so it is not in that list: install it as `@reui/use-file-upload`, which writes `hooks/use-file-upload.ts` (import `useFileUpload` from `@/hooks/use-file-upload`), call `get_component("file-upload")` for its API and `get_examples("file-upload")` for its free examples. There is no `@reui/file-upload`: the UI is a `c-file-upload-<n>` example, and one built on the hook installs it.
|
||||
|
||||
**Rule one: never guess a component's API.** Call **`get_component(name)`** for its inline `api` and share its `docsUrl` with the user, then **`get_examples(name)`** and copy a worked example. The contracts below are first-try orientation (required props, shape, the one gotcha); the `api` is the reference. A large `api` (`cascader`, `filters`, `data-grid`) is trimmed on heading boundaries: `sectionsOmitted` names what was dropped and the response's `next` hint pulls it back. `validate_usage` always checks the full API. No single block fits? Compose from components, 1 `get_examples` example each ([compose.md](./compose.md)).
|
||||
|
||||
## data-grid (the flagship - read its API every time)
|
||||
|
||||
`data-grid` wraps TanStack Table v9. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
|
||||
|
||||
- Build a TanStack table with `useTable({ features: dataGridFeatures, ... })`. `dataGridFeatures` already bundles sorting, filtering, pagination, selection, expanding, pinning, resizing and faceting: no row models to wire.
|
||||
- Pass that instance to `<DataGrid table={table} recordCount={total}>`.
|
||||
- Compose the body with `DataGridTable` inside `DataGrid`, and enable features through `tableLayout` (e.g. `{ headerSticky: true, columnsResizable: true }`), not ad-hoc classes.
|
||||
- Server-side data: `manualPagination: true` and `rowCount: total` in `useTable`, `recordCount={total}`, and fetch with the table's `pagination` state (`DataGridPagination` changes `pageSize`).
|
||||
- Columns are TanStack v9, `ColumnDef<DataGridFeatures, Order>[]`, never v8's `ColumnDef<Order>[]`; a sortable column's `header` renders `DataGridColumnHeader` (a string header cannot sort).
|
||||
- Imports, each from `@/components/reui/data-grid/<file>`: `DataGrid`, `DataGridFeatures` and `dataGridFeatures` from `data-grid`, `DataGridColumnHeader` from `data-grid-column-header`, `DataGridTable` from `data-grid-table`, `DataGridPagination` from `data-grid-pagination`, `DataGridScrollArea` from `data-grid-scroll-area`; `useTable` and `ColumnDef` from `@tanstack/react-table`.
|
||||
- States: `isLoading` renders skeleton rows from each column's `meta.skeleton` (a `ReactNode`: `<Skeleton className="h-4 w-20" />`); `emptyMessage` (a `ReactNode`) fills a grid with no rows: after a failed fetch, pass it the error with a Retry.
|
||||
- Numeric column: `meta: { headerClassName: "text-end [&>div]:justify-end", cellClassName: "text-end tabular-nums" }`.
|
||||
- Rows edited in place: `autoResetPageIndex: false` in `useTable`, or each write jumps back to page 1.
|
||||
- In a Frame: `<FramePanel className="p-0">` around `DataGridScrollArea` and `DataGridTable`, plus `tableClassNames={{ base: "min-w-[720px]" }}` so on a phone the table scrolls inside the panel instead of squeezing.
|
||||
|
||||
```tsx
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, Order>[]>(() => [
|
||||
{ accessorKey: "total", header: ({ column }) => <DataGridColumnHeader title="Total" column={column} /> },
|
||||
], [])
|
||||
const table = useTable({ features: dataGridFeatures, data, columns })
|
||||
|
||||
<DataGrid table={table} recordCount={data.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
Never a raw `<table>` or hand-rolled pagination: the API covers sticky header, virtualization and row selection.
|
||||
|
||||
## event-calendar
|
||||
|
||||
**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EventCalendar defaultEvents={events} defaultView="month" className="h-[560px]">
|
||||
<EventCalendarNav />
|
||||
<EventCalendarContent />
|
||||
</EventCalendar>
|
||||
```
|
||||
|
||||
**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column.
|
||||
|
||||
## gantt
|
||||
|
||||
**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Gantt defaultEvents={bars} resources={tasks} defaultScale="month" className="h-[480px]">
|
||||
<GanttNav />
|
||||
<GanttView />
|
||||
</Gantt>
|
||||
```
|
||||
|
||||
**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height.
|
||||
|
||||
## kanban
|
||||
|
||||
**Required:** `value` (`Record<string, T[]>`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
|
||||
<KanbanBoard>
|
||||
{Object.entries(cols).map(([id, items]) => (
|
||||
<KanbanColumn key={id} value={id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3>{id}</h3>
|
||||
<KanbanColumnHandle aria-label="Move column"><GripVertical /></KanbanColumnHandle>
|
||||
</div>
|
||||
<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. `KanbanColumnHandle` is hidden until its column is hovered or it has keyboard focus, so keep the column title outside it.
|
||||
|
||||
## 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:** `fields` (`FilterField[]`). The value is ONE `FilterQuery` tree - `query` + `onQueryChange`, or uncontrolled `defaultQuery`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const fields: FilterField[] = [
|
||||
{ id: "title", label: "Title", type: "text" },
|
||||
{
|
||||
id: "status",
|
||||
label: "Status",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "archived", label: "Archived" },
|
||||
],
|
||||
},
|
||||
]
|
||||
const [query, setQuery] = useState<FilterQuery>(() => createFilterQuery())
|
||||
|
||||
<Filters fields={fields} query={query} onQueryChange={setQuery} />
|
||||
```
|
||||
|
||||
**Gotcha:** the state is a TREE, not a list of chips. `FilterQuery` is a group of rules joined by `and`/`or` and a group may hold another group, so `(A and B) or C` is expressible; a rule is `{ id, type: "rule", path: ["status"], operator, value }` and `path` is the whole nested attribute path, root first. The pre-rewrite API is GONE: there is no `filters`/`onChange` prop, no `FilterFieldConfig` (fields are `FilterField`, nested through their own `fields`, keyed `id` not `key`), and no `createFilter()`: ids come from `createFilterIdFactory(seed)` seeded off `useId` (never minted in render), and `createFilterRule({ id })` takes one, `createFilterQuery()` none. Read the query back with `flattenFilterConditions` (`{ path, field, operator, values, negated }` per rule, incomplete rules skipped) and walk the tree yourself when the parentheses carry meaning - the primitive compiles nothing, no SQL, no query string.
|
||||
|
||||
`variant` picks the chrome over that one query: `"basic"`, the default, is the flat chip row for a toolbar over a table; `"advanced"` is the condition builder, hung off a trigger or rendered in place with `advancedMode="inline"`. Both read and write the same tree, so a saved view built in one opens in the other. Other props worth knowing before you hand-roll them: `size` is two rungs, `"sm" | "default"`, resolved per style (there is no `lg`); `reorderable` turns on drag and Alt+Arrow row moves in the builder; `onBeforeQueryChange` is the ONE veto point for every write (return `false` to refuse, it cannot rewrite); `editors` registers custom value editors a field selects by `editor` name; `labels` / `operatorLabels` own every rendered string, and `renderChip` / `renderValue` / `renderEmpty` replace rendered parts. On a field, `loadOptions` supplies async options with paging and `resolveValues` renders a chip restored from a saved view whose option was never loaded.
|
||||
|
||||
## cascader
|
||||
|
||||
**Required:** `items` (a tree of `{ value, label, children? }`), plus the panel parts inside `CascaderContent`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Cascader items={items} value={value} onValueChange={setValue}>
|
||||
<CascaderTrigger render={<Button variant="outline" />}>
|
||||
<CascaderValue placeholder="Select an attribute" />
|
||||
</CascaderTrigger>
|
||||
<CascaderContent className="w-80">
|
||||
<CascaderPanel>
|
||||
<CascaderNav>
|
||||
<CascaderBreadcrumb />
|
||||
<CascaderInput />
|
||||
</CascaderNav>
|
||||
<CascaderEmpty />
|
||||
<CascaderList maxHeight={288}>
|
||||
<CascaderItems />
|
||||
</CascaderList>
|
||||
<CascaderStatus />
|
||||
</CascaderPanel>
|
||||
</CascaderContent>
|
||||
</Cascader>
|
||||
```
|
||||
|
||||
**Gotcha:** pressing a branch NAVIGATES, it does not select - only leaves are selectable until you pass `selectable="any"` or a predicate, and once a branch is selectable its chevron becomes the only way to open it. `CascaderInput` must stay inside `CascaderContent` (Base UI refills the query from the selection when the input sits outside the popup). Always include `CascaderStatus`: it is the live region announcing level changes, which the visual breadcrumb does not provide to screen readers. Accepts a flat adjacency list via `getParent` as well as nested `children`. `searchScope="deep"` searches the current level and everything under it, `searchScope="global"` the whole tree from any level, and both annotate results with their path; `multiple` gives checkbox rows; `inline` + a bare `CascaderPanel` embeds it with no popover.
|
||||
|
||||
The shape above is `mode="drill"`, the default. `mode="tree"` keeps the same parts (drop `CascaderBreadcrumb`, pass `showBack={false}`, drive expansion with `expanded`/`onExpandedChange`); `mode="columns"` REPLACES `CascaderList` + `CascaderItems` with a single `CascaderColumns`, and has no breadcrumb. Other props worth knowing before you hand-roll them: `cascade` (multi-select only, parent/child selection with indeterminate branches - pair it with `selectable="any"`, since a leaf-only tree can never cascade), `virtualize` plus `CascaderVirtualItems` for long levels, and `getChildren` for async levels (paging, retry, `prefetch`). `CascaderFooter` pins commands below the list and `CascaderSubmenu` opens one as a side flyout. To head a run of rows use `CascaderGroup` wrapping a `CascaderLabel` - a bare label inside a listbox names nothing and is dropped from the accessibility tree - and `CascaderSeparator` for the rule between runs.
|
||||
|
||||
## 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/reui/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). `smooth` defaults to true: pass `smooth={!reduceMotion}` from motion/react's `useReducedMotion()`. Mark the active link `aria-current="location"` (track it with `onUpdate`); `"page"` is for route links.
|
||||
|
||||
## frame
|
||||
|
||||
**Required:** `Frame` > `FramePanel`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Frame>
|
||||
<FrameHeader>
|
||||
<FrameTitle>Title</FrameTitle>
|
||||
<FrameDescription>Description</FrameDescription>
|
||||
</FrameHeader>
|
||||
<FramePanel>Content</FramePanel>
|
||||
<FrameFooter>Footer</FrameFooter>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Gotcha:** the shell for data and tool-like surfaces. `spacing` (`xs` | `sm` | `default` | `lg`) sets panel padding and the gap between panels, so pick it instead of padding classes (the 1 exception: `<FramePanel className="p-0">` around a `data-grid`); `variant` is `default` | `inverse` | `ghost`; `stacked` joins panels with shared borders; `dense` removes the frame's own inset and gap (panels sit flush), and panel padding stays; `--frame-radius` is the one radius knob (panels derive `--frame-panel-radius`).
|
||||
|
||||
## icon-stack
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconStack aria-hidden="true">
|
||||
<InboxIcon className="size-4" />
|
||||
</IconStack>
|
||||
```
|
||||
|
||||
**Gotcha:** isometric layered artwork for empty states and illustrations; style the inner icon via its own `className`. Mark purely decorative stacks `aria-hidden="true"` and keep the real label in surrounding copy.
|
||||
|
||||
## icon-tile
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconTile variant="elevated" size="lg">
|
||||
<PackageIcon />
|
||||
</IconTile>
|
||||
```
|
||||
|
||||
**Gotcha:** the square container an icon sits in, so every list row, feature card and empty state shares one affordance. `variant`: `outline` (default) | `elevated` (muted fill, raised ring) | `soft` (tinted nested, tone from currentColor) | `solid` (filled tone, contrasting glyph) | `frame` (double container). `soft` and `solid` retint from one text color class (they default to `text-primary`). `size`: `xs | sm | default | lg | xl` (24/32/40/48/64px tile, glyph scales 12/14/16/20/24px). `radius`: `default | full`. Do not set a `size-*` class on the child icon unless you mean to override the tile's glyph size; recolor with `className` on the tile, not the icon.
|
||||
|
||||
## code-block
|
||||
|
||||
**Required:** `code` + `language`, or pre-highlighted `lines`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<CodeBlock code={code} language="tsx" />
|
||||
|
||||
<CodeBlock code={code} language="tsx" showLineNumbers maxLines={20}>
|
||||
<CodeBlockHeader>
|
||||
<CodeBlockTitle>use-totals.ts</CodeBlockTitle>
|
||||
<CodeBlockLanguage />
|
||||
<CodeBlockCopyButton className="ml-auto" />
|
||||
</CodeBlockHeader>
|
||||
<CodeBlockExpandButton />
|
||||
</CodeBlock>
|
||||
```
|
||||
|
||||
**Gotcha:** the one-liner is already a complete block; every child is optional chrome, at any depth. Shiki is the only npm dependency and loads lazily on first highlight; `lines` (from `highlightCode` in `code-block-highlight`, which is server-safe and has no `"use client"`) or `highlight={false}` loads nothing at all. `maxLines` caps the height AND marks the block collapsible, which is what makes `CodeBlockExpandButton` appear. Use `variant="ghost"` when the block sits inside a surface that already has a border.
|
||||
|
||||
## 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.
|
||||
|
||||
## badge
|
||||
|
||||
**Required:** none (text child).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="success-light">Success</Badge>
|
||||
<Badge variant="outline">Draft</Badge>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible. Rich `variant` set (solid, `-outline`, `-light` per color), `size` `xs..xl` (keep the default), `radius` `default | full`. Like `alert`, the color variants rely on ReUI extended tokens. Prefer `Badge` variants over raw color classes: `<tone>-light` for a state, `outline` for a neutral label; an omitted `variant` is the solid primary fill.
|
||||
|
||||
## color-picker
|
||||
|
||||
**Required:** none (`<ColorPicker />` alone is complete); wire `value` + `onValueChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<ColorPicker value={color} onValueChange={setColor} alpha>
|
||||
<ColorPickerTrigger />
|
||||
<ColorPickerContent>
|
||||
<ColorPickerPanel>
|
||||
<ColorPickerArea />
|
||||
<ColorPickerSlider channel="hue" />
|
||||
<ColorPickerSlider channel="alpha" />
|
||||
<ColorPickerFields />
|
||||
</ColorPickerPanel>
|
||||
</ColorPickerContent>
|
||||
</ColorPicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a CSS color string in `format` (`"hex"` default, `"rgb"`, `"hsl"`, `"hsb"` or `"oklch"`), `null` when empty, never an object. `onValueChange` fires on every drag move, so save in `onValueCommit`. Without `alpha` the value is always opaque. A bare `ColorPickerPanel` inside the root is an inline picker; `ColorPickerInput` replaces the trigger with a typable field.
|
||||
|
||||
## emoji-picker
|
||||
|
||||
**Required:** none; wire `onEmojiSelect` to insert a pick, or `value` + `onValueChange` to use it as a field.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EmojiPicker onEmojiSelect={({ value }) => insert(value)}>
|
||||
<EmojiPickerTrigger />
|
||||
<EmojiPickerContent>
|
||||
<EmojiPickerHeader>
|
||||
<EmojiPickerSearch />
|
||||
</EmojiPickerHeader>
|
||||
<EmojiPickerCategories />
|
||||
<EmojiPickerList />
|
||||
<EmojiPickerFooter>
|
||||
<EmojiPickerPreview />
|
||||
<EmojiPickerSkinTone />
|
||||
</EmojiPickerFooter>
|
||||
</EmojiPickerContent>
|
||||
</EmojiPicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the emoji data is fetched at runtime from a pinned jsDelivr path (`EMOJI_PICKER_DATA_URL`): under a strict Content Security Policy allow it in `connect-src`, self-host with `dataUrl`, or pass `data` (from `normalizeEmojibaseData`). Store the pick's `value` (the native emoji in its skin tone, or `:shortcode:` for a custom one). `multiple` makes the value a list, capped by `max`.
|
||||
|
||||
## file-type
|
||||
|
||||
**Required:** none; pass `type` (an extension, a family name or any string).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<FileType type="pdf" />
|
||||
<FileType type={getFileType(file.name)} size="sm" aria-hidden="true" />
|
||||
```
|
||||
|
||||
**Gotcha:** one `span` with `role="img"`, no children, no `"use client"` (fine in a Server Component). Resolve a file name, path, URL or MIME type with `getFileType`, never by splitting on the dot. `variant`: `outline` (default) | `solid` | `light` | `mono`; `size`: `mini` | `xs` | `sm` | `default` | `lg` | `xl` (18 to 56px); recolor with a `text-*` class. Pass `aria-hidden="true"` when the file name sits beside it.
|
||||
|
||||
## signature-pad
|
||||
|
||||
**Required:** `SignaturePad` > `SignaturePadArea`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<SignaturePad value={strokes} onValueChange={setStrokes}>
|
||||
<SignaturePadArea>
|
||||
<SignaturePadGuide />
|
||||
<SignaturePadPlaceholder />
|
||||
<SignaturePadControls>
|
||||
<SignaturePadUndo />
|
||||
<SignaturePadClear />
|
||||
</SignaturePadControls>
|
||||
</SignaturePadArea>
|
||||
</SignaturePad>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is an array of strokes (`{ points: [x, y, size][], color? }`), not an image: store it as JSON, and export with `useSignaturePad()` or `apiRef` (`toSVG`, `toDataURL`, `toBlob`) or `SignaturePadSave` (`format`, `onSave`). `onValueChange` fires per finished stroke, never per pointer move. Size the area with a class (`h-40` by default); an overlay in it that is not a control needs `pointer-events-none`. Drawing needs a pointer, so offer a typed name where a signature is required.
|
||||
|
||||
## time-picker
|
||||
|
||||
**Required:** none (`<TimePicker />` alone is complete); wire `value` + `onValueChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<TimePicker value={time} onValueChange={setTime}>
|
||||
<TimePickerTrigger />
|
||||
<TimePickerContent>
|
||||
<TimePickerColumns />
|
||||
<TimePickerFooter>
|
||||
<TimePickerNow />
|
||||
<TimePickerClear />
|
||||
<TimePickerConfirm />
|
||||
</TimePickerFooter>
|
||||
</TimePickerContent>
|
||||
</TimePicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a 24-hour `"HH:mm"` string (`"HH:mm:ss"` with `granularity="second"`) or `null`, never a `Date`, whatever `hourCycle={12}` shows. `minuteStep`, `min`, `max` and `isTimeDisabled` narrow the options; keep `isTimeDisabled` stable (module scope or memoized). `requireConfirm` holds picks as a draft until OK. `TimePickerInput` replaces `TimePickerTrigger` for a typable field, never beside it.
|
||||
|
||||
## base vs radix - write for the project's base
|
||||
|
||||
Every component ships a `base` (Base UI) and a `radix` (Radix UI) build under 1 name, and the CLI installs the one the project uses. Read `components.json` `style`: the part before the first `-` is the base (`base-nova` is **Base UI**, `radix-nova` is **Radix UI**). Write against that base; the deltas mirror shadcn's:
|
||||
|
||||
- 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"`.
|
||||
|
||||
Installed files and `c-*` examples are already in your base: reuse their wiring. When an `api` or example shows the other base's shape, translate it (or `validate_usage` to confirm).
|
||||
@@ -1,62 +0,0 @@
|
||||
# Compose a screen
|
||||
|
||||
Read this before you build any screen or section, then write the router's 7-line plan in your reply (Surface, Archetype, Container family, Density, Regions, States, Motion) and build exactly that plan. Style it with the topics the screen touches: [layout.md](./layout.md) (house defaults), [color.md](./color.md), [motion.md](./motion.md).
|
||||
|
||||
If the brief names a layout or a motion (a left nav, 2 entrances), follow the brief and name the default you overrode. Several entrances run as 1 stagger: at most 6 items, 50ms apart, never on table rows or lists of 20 or more.
|
||||
|
||||
## Surface
|
||||
|
||||
- **Operational**: a screen people work in every day (dashboard, records, settings, admin tools). Frame-led, dense (`size="sm"` on controls that take it), no hero, no page-load motion.
|
||||
- **Marketing**: a page a visitor reads once (landing, pricing, features). Card-led sections on the spine below, default density.
|
||||
- **Flow**: a task people finish and leave (sign in, onboarding, checkout). 1 centered Card or a Dialog, 1 primary action per step.
|
||||
|
||||
## Archetypes: pick 1 per screen
|
||||
|
||||
- **Vertical rail settings** (3 to 6 sections): a section rail on the left, 1 content column, 1 section at a time; the rail turns horizontal below `md`.
|
||||
- **Horizontal tabs** (5 to 8 views of 1 object): a full-width `Tabs` rail under the header that scrolls sideways when it overflows, 1 content column.
|
||||
- **Frame stack**: stacked Frames, each 1 focused group of settings or records; a flat page split by `Separator` rows when it should feel lighter.
|
||||
- **Table-backed management**: header, toolbar (search, `filters`), then a `data-grid` with row actions and pagination, all in 1 Frame.
|
||||
- **Dashboard grid**: a KPI row, then 1 or 2 chart or list panels, then the table.
|
||||
- **Board**: header and filters, then a `kanban` board; `KanbanBoard` stacks its columns below `sm`, and a wider board scrolls inside its own region.
|
||||
- **Record detail**: header (title, status `Badge`, actions), a main column of sections, a side column of facts that stacks under it below `lg`.
|
||||
- **Marketing spine**: optional `Badge` eyebrow, headline (the only loud element), 1-clause subhead, action row (1 primary `Button`, a second `variant="outline"`, a third a text link), specific proof beside it ("27 reviews"), media. Omit parts; never reorder them.
|
||||
- **Auth card**: 1 centered `Card` at `max-w-sm`: title, fields, 1 full-width primary `Button`, a text link to the other flow.
|
||||
- **Wizard dialog**: a `Dialog` holding a `stepper`, 1 step visible, Back (`variant="outline"`) and Next (primary) in its footer.
|
||||
|
||||
An app shell around any of them is the shadcn `Sidebar`, which turns into a `Sheet` below `md` by itself.
|
||||
|
||||
## Fill each region in this order
|
||||
|
||||
1. The ReUI component that owns the job: the router's Owners (`data-grid`, `filters`, `kanban`), `useFileUpload` from `@/hooks/use-file-upload` for file drop, or the one whose contract in [components.md](./components.md) matches.
|
||||
2. A shadcn/ui component (`Tabs`, `Dialog`, `AlertDialog`, `Sheet`, `Empty`, `Skeleton`, `sonner`).
|
||||
3. A free `c-*` example from `get_examples` that already composes it: install it and adapt.
|
||||
4. Your own composition of those parts. Never a hand-rolled table, modal or tabs.
|
||||
|
||||
Import ReUI parts file by file: `data-grid`, `filters`, `cascader`, `event-calendar` and `gantt` are folders with no index file. `data-grid`: the import line in [components.md](./components.md). `filters`, each from `@/components/reui/filters/<file>`: `Filters` from `filters`, `createFilterQuery` and `flattenFilterConditions` (1 argument, the query) from `filters-query`, the types `FilterField` and `FilterQuery` from `filters-types`.
|
||||
|
||||
## Wiring that works
|
||||
|
||||
- `data-grid`: columns are `ColumnDef<DataGridFeatures, Order>[]`, a sortable header renders `DataGridColumnHeader`, and server paging needs `manualPagination: true` and `rowCount`.
|
||||
- `filters`: apply each condition by its `operator`. Negate swaps a rule to its inverse (`is` and `is_not`, `contains` and `not_contains`, `is_any_of` and `is_none_of`, `empty` and `not_empty`) and sets `negated` only when there is none: honour both.
|
||||
- A Base UI `Button` that navigates: `<Button nativeButton={false} render={<Link href="/x" />}>`.
|
||||
- shadcn's `CommandDialog` has no `Command` inside, unlike older shadcn: wrap `CommandInput` and `CommandList` in one. `CommandItem` search matches its `value`: omit it (text is used) or use the label; ids in `onSelect`.
|
||||
|
||||
## Headers
|
||||
|
||||
A page or panel header puts the title (1 to 3 words) and a 1-clause description on the left and the actions on the right: 1 primary `Button`, the rest `variant="outline"`, row actions `variant="ghost"`.
|
||||
|
||||
## Responsive
|
||||
|
||||
- No horizontal page scroll at 390px: grids drop to 1 column, side columns stack, `min-w-0` on every shrinking parent.
|
||||
- A KPI row is `grid-cols-1 sm:grid-cols-2 xl:grid-cols-4`.
|
||||
- Every header and toolbar action stays reachable at every width: a control hidden below a breakpoint moves into a menu shown only there.
|
||||
- A table scrolls sideways inside its Frame, never the page (the `data-grid` contract in [components.md](./components.md)).
|
||||
|
||||
## States and motion
|
||||
|
||||
- Each data region owns its states: empty (shadcn `Empty` with a title, 1 line and the primary action), loading (`Skeleton` in the real layout; `data-grid` takes `isLoading`) and error (inline in the region, with Retry). Never 1 spinner for the whole page.
|
||||
- Motion: none, or the 1 entrance per view; installed parts keep their own motion, and hover and press follow [motion.md](./motion.md). An Alert, field error, notice or data that appears after an action or a load is a reveal, not an entrance: fade it in with the reveal row.
|
||||
|
||||
## Remove first
|
||||
|
||||
Overloaded? Remove, in order: decorative summaries, duplicate context, extra badges, long descriptions, secondary actions, mismatched icon treatments. Add a region only when the brief needs it.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Icons (ReUI delta over shadcn)
|
||||
|
||||
Use the project's configured `iconLibrary`, put `data-icon="inline-start"` on an icon before the label inside `Button` (`data-icon="inline-end"` on one after it), put no sizing classes on icons inside components, and pass icons as components, not string keys. ReUI adds the following.
|
||||
|
||||
## Portable icons (library-agnostic)
|
||||
|
||||
ReUI components, examples, and blocks are authored to be icon-library-agnostic. The shadcn CLI installs each item's icons in the library named by `iconLibrary` in `components.json` - you swap nothing. When `iconLibrary` is not set, the CLI falls back to `lucide`, so the icons arrive as `lucide-react` imports. If that is not the project's library, set `iconLibrary` before installing, or change the **import source and component name** to your library, keeping the same icon-name semantics (each key below is an `iconLibrary` value):
|
||||
|
||||
- `lucide` -> `lucide-react`
|
||||
- `tabler` -> `@tabler/icons-react`
|
||||
- `phosphor` -> `@phosphor-icons/react`
|
||||
- `remixicon` -> `@remixicon/react`
|
||||
- `hugeicons` -> icons from `@hugeicons/core-free-icons`, rendered through `HugeiconsIcon` from `@hugeicons/react`
|
||||
|
||||
Don't assume `lucide-react`: read `iconLibrary` from `components.json`, and when it is unset, check which icon package the project already uses.
|
||||
|
||||
## 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). Every icon installs from the default path; some also have a real hover animation (motion/react) on the animated path:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/icons/default/<style>/<name> --yes # static, every icon
|
||||
npx shadcn@latest add @reui/icons/animated/<style>/<name> --yes # hover-animated, only when the icon has installAnimated
|
||||
```
|
||||
|
||||
The animated path answers for every icon, but for an icon without a real animation it installs the same static drawing, so only use it when the icon carries `installAnimated`.
|
||||
|
||||
Motion Icons need an Ultimate license for **both discovery and install** (`REUI_LICENSE_KEY`, see [cli.md](./cli.md)). Without it `search_icons` comes back `locked` with no results and `search` returns no icons at all, so check the plan before you promise the user icons: on a free or Pro account, use the project's own `iconLibrary` instead. On Ultimate, a Motion Icon on the primary action is the 1 hover flourish (see [motion.md](./motion.md)).
|
||||
|
||||
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 real hover animation; a concept with none comes back empty and `animatedNote` says so.
|
||||
- One icon: `search` with `type: "icon"`.
|
||||
- Icon results and `get_icon` carry `animated: true` and `installAnimated` only when the icon really animates - use those install strings, do not construct paths by hand.
|
||||
- Every icon result carries a `previewUrl` (the icon's own page) and a `thumbnail` (its 4 styles side by side) - **show or share them** so the user can SEE the icon before installing, and look at the thumbnail yourself before trusting a name.
|
||||
- Several icons can fit one concept. Before installing, read `get_icon`'s `whenNotToUse` (each entry names the icon to use instead), and use its `label` as the `aria-label` of an icon-only button.
|
||||
- Icon names were revised in the icons v2 update. An old name (`save-2`, `abstract-18`) still answers in `get_icon`, `get_install_command`, `validate_usage` and `get_thumbnail` with the icon it became and `renamedFrom`, and still installs that same drawing; write the current name in new code. That includes names renamed because they misdescribed the drawing (`watch` installs a sand timer, `chevrons-down` points up), so look at the `thumbnail` before trusting an old name. `search_icons` answers an old name the same way, unless it is also an ordinary word icons are named for today (`watch`): then it ranks today's icons for the word, and `oldNameNotes` names the icon the old name installs. `search` ranks by current names only, so look an old name up with `get_icon`, never `search`. Only `tag` (it drew a money bag) no longer answers: the reply names the icons to use instead.
|
||||
|
||||
The `icon-stack` component composes multiple icons into a stacked display.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Layout: house defaults, hierarchy, spacing, surfaces and states
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
## House defaults
|
||||
|
||||
<!-- gen:house-defaults:start -->
|
||||
|
||||
- **Spacing ladder**: `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections, `gap-0.5` title to description. Deviate when: skip a level if you like, never invert them.
|
||||
- **Panel title**: no size class on `FrameTitle` or `CardTitle`. Deviate when: never; `FrameTitle` ships `text-sm font-semibold`, `CardTitle` follows the installed style.
|
||||
- **Meta text**: `text-xs`. Deviate when: never smaller, except inside illustrative mini-UI artwork.
|
||||
- **Frame spacing**: `spacing="default"`, written out. Deviate when: `sm` in a sheet, dialog, drawer or popover, or for a dense data grid in a panel; `xs` or `lg` only with a stated reason.
|
||||
- **Leading row icon**: `size-4`. Deviate when: never in a glyph column; `Spinner` ships `size-4`, so a loading swap keeps the width.
|
||||
- **Icon inside a control**: no size class; the control sizes it per style and size. Deviate when: never pin `size-*` on an svg inside `Button`, `InputGroupButton`, `InputGroupAddon`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioItem`, `DropdownMenuSubTrigger`, `SelectItem`, `CommandItem`, `Toggle`, `ToggleGroupItem`, `IconTile` or `MarkerIcon`.
|
||||
- **Badge size**: `default` (leave `size` off). Deviate when: `lg` or `xl` only when the user asks; never `sm` or `xs`.
|
||||
- **Neutral badge**: `variant="outline"`. Deviate when: never `secondary`, `default` or `primary-light` for an id, tag, version, count or Draft/Beta label; inline after the title, never `ml-auto`.
|
||||
- **State badge**: `<tone>-light`: `success-light`, `info-light`, `warning-light`, `destructive-light`; `primary-light` for brand or pending. Deviate when: never the solid tone, `secondary` or `outline` for a state; 1 state badge per row.
|
||||
- **Solid badge**: never unless asked; always name a variant (an omitted one is the solid primary fill). Deviate when: only when the user asks for a solid fill (`default`, `secondary`, `success`, `info`, `warning`, `destructive`, `focus`, `invert`).
|
||||
- **Slot padding**: the primitive's own inset; set density with `Frame spacing=` or `Card size="sm"`. Deviate when: never a padding class on `CardHeader`, `CardContent`, `CardFooter`, `FrameHeader`, `FramePanel`, `FrameFooter`, `SheetHeader`, `SheetContent`, `SheetFooter`, `DialogHeader`, `DialogContent`, `DialogFooter`, `PopoverContent`, `DropdownMenuContent`, `TabsContent` or `AccordionContent`.
|
||||
- **Inline separator**: `<span aria-hidden className="bg-muted-foreground/40 size-1 shrink-0 rounded-full" />`. Deviate when: never a typed `·` between rendered segments; a literal dot only inside 1 data string.
|
||||
- **List rows**: dense, auto height; a row with no second line stays 1 tight line. Deviate when: never uniform height.
|
||||
- **Current list item**: ghost hover fill (`bg-muted`, `dark:bg-muted/50`) plus `aria-current`. Deviate when: never a dot or a check mark.
|
||||
|
||||
<!-- gen:house-defaults:end -->
|
||||
|
||||
## Hierarchy
|
||||
|
||||
- 1 focal point per card or panel: the dominant metric or task, then its label, then detail. Borders, separators and surfaces make 2 to 3 bands, never equal weight everywhere.
|
||||
- Overloaded? Remove, in order: decorative summaries, duplicate context, extra badges, long descriptions, secondary actions, mismatched icon treatments.
|
||||
|
||||
## Spacing and copy
|
||||
|
||||
- Siblings take the parent's `gap-*`, never their own margins; a heading sits nearer its own content than the section above it. Never pad an operational surface like a marketing page.
|
||||
- Titles 1 to 3 words, a description 1 clause (about 8 words), a context line 2 segments at most, labels and buttons 1 to 2 words, a toast title 4 words at most. Copy that wraps at desktop width is over budget: cut it, never shrink the type.
|
||||
|
||||
## Surfaces
|
||||
|
||||
- `Frame` for data and operational surfaces (tables, lists, tool panels); `Card` for bounded sections and marketing.
|
||||
- Prefer 1 container family per screen. With a KPI row and a table, use FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame or a Frame inside a Card.
|
||||
- A `data-grid` inside a Frame is the one place for `<FramePanel className="p-0">`; elsewhere no padding on slots. This is the 1 exception to the Slot padding row above.
|
||||
- 1 archetype per screen (operational ones and the marketing spine): [compose.md](./compose.md) lists each with its regions.
|
||||
|
||||
## Type and buttons
|
||||
|
||||
- A KPI value is `text-2xl font-semibold tracking-tight tabular-nums`.
|
||||
- `size="sm"` on dense surfaces, `variant="outline"` for secondary actions, `ghost` for row actions; 1 primary per region, 1 variant per toolbar.
|
||||
- A toolbar separator is `<Separator orientation="vertical" className="my-auto h-4" />`; at a 390px viewport a toolbar stays on 1 row.
|
||||
|
||||
## States and responsive
|
||||
|
||||
- Empty: a short message plus the primary action. Loading: a `Skeleton` in the real layout, never a centered spinner. Error: inline with a retry.
|
||||
- No horizontal page scroll at 390px: reflow to 1 column instead of shrinking; `min-w-0` on the shrinking parent and `truncate` on single-line labels.
|
||||
@@ -1,80 +0,0 @@
|
||||
# Motion
|
||||
|
||||
## Installed motion: never retime it
|
||||
|
||||
<!-- gen:installed-motion:start -->
|
||||
|
||||
| Installed part | Duration |
|
||||
| ------------------ | ----------------- |
|
||||
| Overlays | 100ms |
|
||||
| Tooltip | 150ms (default) |
|
||||
| Sheet, sidebar | 200ms |
|
||||
| Navigation menu | 300ms, exit 150ms |
|
||||
| rhea control focus | 200ms |
|
||||
|
||||
<!-- gen:installed-motion:end -->
|
||||
|
||||
Never add or change `duration-*`, `ease-*`, `animate-*` or `transition-*` on installed parts (`components/ui`, `components/reui`); Button already presses with `active:not-aria-[haspopup]:translate-y-px`.
|
||||
|
||||
## House motion: your own code
|
||||
|
||||
<!-- gen:house-motion:start -->
|
||||
|
||||
| Use | Tailwind |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| Hover, press, color | `transition-colors` (150ms, no duration or ease class) |
|
||||
| Reveal or expand | `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]` |
|
||||
| Large surface (ceiling) | `duration-300` with the same curve |
|
||||
| An overlay you build | `duration-100` with the same curve |
|
||||
| Media hover zoom | `duration-500` with the same curve, `scale-105` at most |
|
||||
| Width tied to the sidebar | `duration-200 ease-linear` |
|
||||
|
||||
<!-- gen:house-motion:end -->
|
||||
|
||||
- Press on a custom surface (not Button): `transition-transform active:scale-[0.98] motion-reduce:transition-none`.
|
||||
- Hover color plus press on 1 element: `transition-[color,background-color,border-color,scale]` (`transition-colors` and `transition-transform` override each other; `scale-*` sets `scale`).
|
||||
- motion/react takes the same values in seconds: `{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }`; hover `{ duration: 0.15 }`.
|
||||
- Drag, reorder and layout use `const SPRING = { type: "spring", visualDuration: 0.25, bounce: 0 }`. Define `SPRING` only when something drags, reorders or toggles layout.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Animate `transform` and `opacity`; color only for hover and press. Never `transition-all`; width, height, top or left only in the sidebar row.
|
||||
- Never `animate-bounce` or a spring with bounce above 0. `ease-linear` only for progress, spinners, loops and the sidebar; never Tailwind's `ease-in`, `ease-out` or `ease-in-out`.
|
||||
- `motion/react` only: never framer-motion, GSAP or react-spring.
|
||||
- At most 1 entrance animation per view (a stagger counts as 1), and every animation has a reduced-motion path. Feedback that appears after an action or a load (an Alert, a field error, a saved notice, data replacing its `Skeleton`) is a reveal, not an entrance: fade it in with the reveal row.
|
||||
- Never add named `duration-*` utilities or redefine `--ease-out`.
|
||||
- Loading: a `Skeleton` (it already pulses) in the real layout; a spinner only inside a busy button.
|
||||
- Motion Icons (Ultimate only): 1 on the primary action is the 1 hover flourish; install it with its `installAnimated` command; never retime it.
|
||||
|
||||
## Performance
|
||||
|
||||
Each line names its lint rule, or judgement (no check).
|
||||
|
||||
- Never animate blur, drop-shadow, backdrop-filter, box-shadow, border-radius, background-position, background-size or a custom property that feeds transform; clip-path and stroke-dashoffset only as a one-shot reveal of 300ms or less, or a loop on an element of 240x240px or less. Name the properties: never a bare `transition` (it also animates box-shadow and filter). (`reui/no-paint-animation`; 240px: judgement (no check))
|
||||
- A hover lift is `transform` over a static shadow, or a shadow layer whose `opacity` fades; never a transitioned `hover:shadow-*`. (`reui/no-paint-animation`)
|
||||
- CSS first: hover, press, reveals and ambient loops are Tailwind transitions, `tw-animate-css` or `@keyframes`. motion/react only for exit on unmount, drag, springs that follow input, `layout` and scroll-linked values. (judgement (no check))
|
||||
- In motion/react, `x`, `y`, `scale`, `rotate`, `pathLength` and every SVG target run on the main thread: never `repeat: Infinity` on them (loop with CSS `@keyframes`), and keep 20 or fewer JS-driven animations running per view. An opacity loop leaves the main thread only on an HTML element without `repeatDelay`, `repeatType: "mirror"` or `onUpdate`. (`reui/no-main-thread-loop`)
|
||||
- SVG geometry (a rect's `width`, a circle's `r`) does not reflow the page but runs in motion's JS loop: one-shot only. (`reui/no-main-thread-loop`)
|
||||
- Never set React state per frame (requestAnimationFrame, `useAnimationFrame`, `onUpdate`, scroll or pointermove listeners): bind styles to `useMotionValue`, `useTransform` or a CSS variable on the element. (`reui/no-frame-setstate`)
|
||||
- A JS-driven loop (motion `repeat`, requestAnimationFrame, canvas) pauses offscreen with `useInView` or IntersectionObserver; `content-visibility: auto` never stops script, and a setInterval loop also pauses on `visibilitychange`. (`reui/loop-offscreen`)
|
||||
- No permanent `will-change`: no `will-change-*` class or `willChange` style, except during a drag, removed when it ends. (`reui/will-change`)
|
||||
- Stagger 50ms or less per step (`staggerChildren: 0.05`), only the first 6 items, the whole entrance done in 500ms, first mount only, never on table rows or lists of 20 or more; scroll reveals use `whileInView` with `viewport={{ once: true }}`. (`reui/stagger-budget`; item count and tail: judgement (no check))
|
||||
- A height reveal (`grid-template-rows`) starts only on a click or key press, runs 200ms or less, 1 at a time; never `initial={{ height: 0 }}`. (`reui/no-layout-transition`)
|
||||
- The motion `layout` prop on 1 list at a time (about 20 elements), never in data-grid or virtualized rows; drag and drop goes through `@reui/sortable` or `@reui/kanban`. (judgement (no check))
|
||||
- AnimatePresence `mode="wait"` only with an exit of 150ms or less; `initial={false}` around content present at first render. (`reui/stagger-budget`)
|
||||
- Never hide the hero or the largest heading behind a motion/react `initial` at opacity 0 (hidden until JavaScript runs). (judgement (no check))
|
||||
- At most 3 ambient loops per view: CSS `@keyframes` on transform or opacity, a period of 2s or more, exempt from the duration table; once 3 run in a view they pause offscreen (a `content-visibility: auto` wrapper with `contain-intrinsic-size: auto <h>` where nothing paints outside it, or IntersectionObserver toggling `animation-play-state`). At most 50 animating elements per view (Skeletons, loaders and CSS opacity loops not counted). A static blur above 12px, at most 1 per view, only on a surface that neither moves nor sits over motion. (judgement (no check))
|
||||
|
||||
## Reduced motion
|
||||
|
||||
- Reduced motion is a substitute, not an off switch: replace movement (translate, scale, rotate, slide, zoom, layout, parallax, auto-scroll) with an opacity fade of 150ms or less or an instant change. Keep color and opacity feedback, focus, progress bars, loaders, the Skeleton pulse, chevrons and the switch thumb. (`reui/reduced-motion`)
|
||||
- The reduced state is the resting state: content never waits at opacity 0, hidden or offset for an animation that will not play; a count-up shows its final value. (`reui/reduced-motion`)
|
||||
- In every mode an entrance moves 8px or less and starts at scale 0.95 or more; nothing moves a surface larger than half the viewport. (`reui/reduced-motion`; half the viewport: judgement (no check))
|
||||
- `<MotionConfig reducedMotion="user">` once at the app root (its default ignores the OS) AND a `motion-reduce:` path on every CSS animation (MotionConfig never reaches CSS). An example or block wraps its own motion subtree. (`reui/reduced-motion`)
|
||||
- MotionConfig makes only transform keys and width, height, top and left instant; opacity, filter, clipPath, pathLength, color and a `transform` string keep animating, so a loop or those keys branch on `useReducedMotion()` and rest on the final value. (`reui/reduced-motion`)
|
||||
- `useReducedMotion()` is null on the server: never let it pick markup (JSX, className, `initial`) or reach `animate` under `initial={false}`; branch only animate, transition, `while*` or exit values. (`reui/reduced-motion-ssr`)
|
||||
- No SMIL (`<animate>`, `<animateMotion>`, `<animateTransform>`); canvas and requestAnimationFrame loops draw 1 resting frame under `prefers-reduced-motion: reduce`. (`reui/no-smil`, `reui/reduced-motion`)
|
||||
- Smooth scroll only under `motion-safe:scroll-smooth`. A focus ring appears at once: never `transition-all`, `transition-shadow` or a bare `transition` on it. (`reui/reduced-motion`, `reui/focus-transition`)
|
||||
- Loaders and typing dots keep their opacity pulse; every decorative loop gets `motion-reduce:animate-none`, never an inline `style={{ animation }}` (it beats the class). (`reui/reduced-motion`)
|
||||
- Anything that moves on its own for more than 5 seconds (autoplay, marquee, ticker) gets a visible Pause control, pauses on hover and focus, and never autoplays under reduced motion. (`reui/pausable-autoplay`)
|
||||
- Installed overlays keep their 100ms fade, zoom and 8px slide under reduced motion: disclosed, not edited; an app that needs WCAG 2.3.3 adds its own override. (judgement (no check))
|
||||
@@ -1,57 +0,0 @@
|
||||
# Quality gates (security, accessibility, scroll, self-checks)
|
||||
|
||||
This is the **done gate**: before you call any ReUI work finished, call `get_audit_checklist` and pass every item below (plus the bar and the states in [layout.md](./layout.md)). Then run the self-checks, typecheck and lint, compare with [anti-patterns.md](./anti-patterns.md), and hand back in the 5 lines below.
|
||||
|
||||
## 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. Hover on a display-only card: make the card a link to its detail, or give it no hover.
|
||||
- `aria-current="page"` on the current route link, `"location"` on the current in-page section link.
|
||||
- Destructive confirms use `AlertDialog`, not `Dialog`, and cannot be dismissed while pending.
|
||||
- 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 their keyboard behavior; never re-implement it.
|
||||
- Announce async UI: loading and error messages use `role="status"` or `aria-live`.
|
||||
|
||||
## 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.
|
||||
|
||||
## Self-checks
|
||||
|
||||
After every UI edit, run these with ripgrep on `<files>`: each file you wrote or changed, by path, never `components/ui` or `components/reui`. A clean check prints nothing; a hit prints `file:line:code`. The last filter drops comment lines.
|
||||
|
||||
```sh
|
||||
# C1 banned motion: transition-all, bare or paint transitions, layout keys, bounce, overshoot, scale 0, hover and tap gestures, will-change
|
||||
rg -nH 'transition-all|transition(-property)?: *[^;]*\b(all|width|height|top|left|margin|padding|box-shadow|filter)\b|[\x27\x60" ]transition( +(duration|ease|delay|hover|focus|active|group|motion)|[\x27\x60"])|transition-shadow|transition-\[[^\]]*\b(width|height|top|left|right|bottom|inset|margin|padding|filter|radius|background-position|background-size|all)\b|animate-bounce|will-change|willChange|bounce: *(0?\.0*[1-9]|[1-9])|[\x27"](backIn|backOut|backInOut|anticipate)[\x27"]|(cubic-bezier\(|ease: *\[)([^,]*,|([^,]*,){3})[ _]*(-|1\.[0-9]*[1-9]|[2-9])|scale: *0 *[,}]|\bscale-0\b|while(Hover|Tap|Focus)=|(initial|animate|exit)=\{\{[^}]*\b(width|height|maxHeight|top|left|margin|padding|boxShadow|filter|backdropFilter|borderRadius)[A-Za-z]*:' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C2 off-scale motion: a duration off 0, 100, 150, 200 and 300 (duration-500 only on a media hover zoom), ease-in or ease-out, a stagger step above 0.05
|
||||
rg -nH 'duration-([1-9]|[1-9][0-9]|1(0[1-9]|[1-46-9][0-9]|5[1-9])|[23](0[1-9]|[1-9][0-9])|[4-9][0-9]{2}|[1-9][0-9]{3,})\b|duration-\[|\b([1-9]|[1-9][0-9]|1(0[1-9]|[1-46-9][0-9]|5[1-9])|[23](0[1-9]|[1-9][0-9])|[4-9][0-9]{2}|[1-9][0-9]{3,})ms\b|(^|[^0-9.])(0?\.(0[0-9]*|1[0-46-9][0-9]*|15[0-9]+|[23][0-9]+|[4-9][0-9]*)|1\.[0-9]+)s\b|duration: *([1-9]\b|0?\.(0[0-9]*|1[0-46-9][0-9]*|15[0-9]+|[23][0-9]+|[4-9][0-9]*)\b)|\bease-(in|out|in-out)\b|(staggerChildren: *|stagger\( *)(0?\.0[6-9]|0?\.[1-9]|[1-9])|delay: *[^,}]*\b(i|index|idx)\b *\* *(0?\.0[6-9]|0?\.[1-9]|[1-9])|delay: *(0?\.0[6-9]|0?\.[1-9]|[1-9])[0-9.]* *\* *\b(i|index|idx)\b' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C2 read by hand: a duration or delay that is not a number, every index delay (allowed: 0.05 or less on the first 6 items, as Math.min(i, 5) * 0.05, never on table rows)
|
||||
rg -nH '([dD]uration|[dD]elay): *[^0-9 .]|delay: *[0-9.]+ *\*' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C3 state color: raw palette on a line that names a state
|
||||
rg -nH '\b(text|bg|border|ring|fill|stroke)-(red|rose|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet)-[0-9]{2,3}\b' <files> | rg -i ':[0-9]+:.*\b(success|paid|active|completed?|done|approved|online|healthy|live|error|failed|critical|overdue|declined|rejected|cancell?ed|blocked|expired|warning|pending|degraded|due|info|processing|running|scheduled|queued)\b' | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C4 structure: a raw table, select or fixed overlay, framer-motion, Math.random() or Date.now()
|
||||
rg -nH '<table\b|<select\b|fixed inset-0|from ["\x27]framer-motion|Math\.random\(|Date\.now\(' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C4 on a base-* style only (skip on radix-*): Radix asChild
|
||||
rg -nH '\basChild\b' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
```
|
||||
|
||||
Fix every hit, run again, stop after the second run and list what is left with file and line. A C4 hit stays only when a comment in the file says why. These also stay, named on hand-back line 2: C1 on the motion.md sidebar row or on will-change set in a drag handler and removed when it ends; C2 on a media hover zoom; `Math.min(i, 5) * 0.05`.
|
||||
|
||||
No `rg` (a missing one also prints nothing): run each first pattern with your own search tool, skipping comment lines. No shell: read each changed file for what each `#` line names.
|
||||
|
||||
## Hand-back
|
||||
|
||||
End your reply with these 5 lines:
|
||||
|
||||
1. Installed: each item with its `previewUrl`.
|
||||
2. Motion: each animated element and its [motion.md](./motion.md) table row, or "none".
|
||||
3. States: covered, or which are missing.
|
||||
4. Checks: C1 to C4, screenshots or "not visually verified", the audit checklist, typecheck, lint; each one run, or "not run" and why.
|
||||
5. Deviations: any rule broken or check hit left, with file:line and why.
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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 ReUI components 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 (a full dashboard or settings page). Pro or Ultimate license at install. Its surfaces, text and borders use your theme tokens; its category accents (chart series, tags, avatars) are fixed palette colors paired for light and dark.
|
||||
- **icon** - Motion Icons in 4 styles (outline, solid, duotone, filled), static (`@reui/icons/default/<style>/<name>`), plus a hover-animated variant (`@reui/icons/animated/<style>/<name>`) only for icons that carry `installAnimated`. Ultimate license at install. See [icons.md](./icons.md).
|
||||
|
||||
## The @reui registry
|
||||
|
||||
ReUI items install through the shadcn CLI as `npx shadcn@latest add @reui/<name> --yes`, with `<name>` exactly as a ReUI tool returned it (`search`, a getter, `get_install_command`). 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.
|
||||
|
||||
## ReUI items vs shadcn/ui components
|
||||
|
||||
- **ReUI items** (the components, `c-*` examples, blocks, Motion Icons) install as `@reui/<name>`.
|
||||
- **shadcn/ui components** that ReUI builds on (`button`, `dialog`, `select`, `card`, ...) are not ReUI items. They install from shadcn by bare name: `npx shadcn@latest add button --yes`.
|
||||
- **`badge` and `alert` exist in both registries.** `@reui/badge` and `@reui/alert` are ReUI's components (the extra variants in [components.md](./components.md)); bare `badge` and `alert` are shadcn's.
|
||||
- **In an item's `registryDependencies`**, a `@reui/<name>` entry is a ReUI item and a bare name is a shadcn/ui item. `add` installs both for you.
|
||||
- **Templates are not registry items.** Download one from its page on reui.io (https://reui.io/templates?ref=skill); no `shadcn add` command installs a template.
|
||||
|
||||
## 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 take surfaces, text and borders from your theme tokens, so a theme change re-tints them; category accents stay fixed by design.
|
||||
|
||||
## Free vs premium
|
||||
|
||||
- **Free, no license key:** the ReUI components, all `c-*` examples, and this skill. The MCP is free too but still needs a free ReUI account (the agent signs in on first use); a license key is only for premium installs.
|
||||
- **Premium, license required at install:** blocks (Pro or Ultimate) and Motion Icons (Ultimate). Set `REUI_LICENSE_KEY` (see [cli.md](./cli.md)).
|
||||
- **Templates** are downloads, not installs: free ones need only a ReUI account, and each template's page on reui.io says what the others need.
|
||||
|
||||
## 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.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Theming: styles, radius, density and sizes
|
||||
|
||||
## Read your style
|
||||
|
||||
`components.json` `style` is `<base>-<style>`: the part after the first `-` names the style (`base-nova` is nova on Base UI). The style is baked into `components/ui` and `components/reui` at install, so the installed files hold your real values: read your installed `button.tsx` (usually `components/ui/button.tsx`) for heights, radius and padding before you build anything that sits beside a control.
|
||||
|
||||
## The style ladder
|
||||
|
||||
<!-- gen:style-ladder:start -->
|
||||
|
||||
| Style | Button `sm` / default / `lg` | Button radius | Card radius, shadow | Card spacing | Button text |
|
||||
| ----- | ---------------------------- | -------------- | ---------------------------------------------------- | ------------ | -------------------- |
|
||||
| vega | `h-8` / `h-9` / `h-10` | `rounded-md` | `rounded-xl`, `shadow-xs` | 6, sm 4 | `text-sm` |
|
||||
| nova | `h-7` / `h-8` / `h-9` | `rounded-lg` | `rounded-xl` | 4, sm 3 | `text-sm` |
|
||||
| maia | `h-8` / `h-9` / `h-10` | `rounded-4xl` | `rounded-2xl` | 6, sm 4 | `text-sm` |
|
||||
| lyra | `h-7` / `h-8` / `h-9` | `rounded-none` | `rounded-none` | 4, sm 3 | `text-xs` |
|
||||
| mira | `h-6` / `h-7` / `h-8` | `rounded-md` | `rounded-lg` | 4, sm 3 | `text-xs/relaxed` |
|
||||
| luma | `h-8` / `h-9` / `h-10` | `rounded-4xl` | `rounded-4xl`, `shadow-md` | 6, sm 4 | `text-sm` |
|
||||
| sera | `h-9` / `h-10` / `h-11` | `rounded-none` | square, `shadow-sm` | 8, sm 5 | `text-xs`, uppercase |
|
||||
| rhea | `h-7` / `h-8` / `h-9` | `rounded-2xl` | `rounded-[min(var(--radius-4xl),24px)]`, `shadow-sm` | 5, sm 4 | `text-sm` |
|
||||
|
||||
<!-- gen:style-ladder:end -->
|
||||
|
||||
Card spacing is the `--card-spacing` step (`--spacing(n)`) a Card pads with; `Card size="sm"` uses the sm step. The default Button text weight is `font-medium` (sera: `font-semibold`).
|
||||
|
||||
## What the style owns
|
||||
|
||||
- Never set what the style owns on its parts: radius (except `rounded-full`), height, horizontal padding, shadow, ring or font size. Pick `size` and `variant` instead.
|
||||
- A panel is a `Card` or a `Frame`, never a div with `rounded-*`, `border` and `bg-card`; set density with `Card size="sm"` or Frame `spacing`.
|
||||
- A bespoke element beside a control (a chip, a custom trigger) takes its height and radius from that control's row above.
|
||||
- A focusable control you build copies the focus classes of your installed `input.tsx`.
|
||||
- A variant before a class: `className` is for layout (placement, width, gap). `size-*` when width equals height, `cn()` for conditional classes (never a template literal), a scale step before an arbitrary value (`w-60`, not `w-[240px]`), no inline `style` for color or spacing, and no `z-index` on dialogs, sheets or popovers.
|
||||
- Change the look through the theme CSS variables in `:root` and `.dark` (`--radius`, `--primary` and the rest) or with a style switch (a new `style` in `components.json`, which means reinstalling the components; ask the user first). Never edit installed files to restyle them.
|
||||
|
||||
## Components with no per-style values
|
||||
|
||||
`event-calendar`, `gantt`, `icon-stack`, `kanban`, `rating`, `scrollspy`, `sortable` and `timeline` install the same file in every style. Their look comes from your theme tokens and the parts you put inside them (a `Card` in a kanban column follows the style), so never copy another style's radius onto them.
|
||||
@@ -1,66 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
- A full page or section (dashboard, billing, auth, pricing, settings): `compose_page` first (step 1).
|
||||
- A data table with sorting, filtering, pagination or virtualization: the `data-grid` component, never a hand-rolled `<table>`.
|
||||
- A drag-and-drop board: the `kanban` component.
|
||||
- Column filters, a date range, a tree, a stepper and the like: the matching ReUI component.
|
||||
- A single generic control shadcn already has (Button, Dialog, Select): plain shadcn.
|
||||
|
||||
## 1. Find (ReUI MCP `search` / `compose_page`)
|
||||
|
||||
**Full multi-section page ask?** Call `compose_page(intent)` FIRST (`sections`, 1 to 10, caps how many sections, default 8), before searching block-by-block. It returns ordered sections, each with the best block for the intent (top pick + alternates); on a free account it answers `mode: "free"` and fills the same sections with free `c-*` examples instead, so the plan is always buildable. 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"` (a ReUI component), `"example"` (a c-\* use-case), `"block"` (a full page/section), `"icon"`.
|
||||
- `component`: the ReUI component the request implies (`"data-grid"`, `"kanban"`, ...).
|
||||
- `category`, `free`.
|
||||
|
||||
Example: "build a users management page with filters" -> `search({ query: "users management page with filters", type: "block", component: "data-grid" })` (on a free account use `type: "example"`).
|
||||
|
||||
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.
|
||||
|
||||
**Pick with the guidance, not the title.** Look-alikes are common (dialog, sheet and drawer; dozens of data-grid examples and app shells). Before installing, read the item's guidance: `get_component` returns `whenToUse` and `whenNotToUse`, `get_example` and `get_block` return `bestFor` (what sets the item apart from its siblings), `agentNotes` and `whenNotToUse`, and a block adds `categoryGuidance` (its category's key note), and `get_examples` rows carry `bestFor`. Each `whenNotToUse` entry names the item to use instead and its `type` (`component` -> `get_component`, `example` -> `get_example`, `block` -> `get_block`, `block-category` "<group>/<category>" -> `search({ query, type: "block", category: "<category>" })`); when the user's situation matches its `when`, switch. Apply every `agentNotes` line before you ship: it names the real identifiers to replace or wire (sample data, stubbed actions, required props). An item with no guidance yet has none of these keys; fall back to its description and thumbnail.
|
||||
|
||||
Results are scoped to the account's plan, so you are never shown an item the user could not install: blocks need Pro, Motion Icons need Ultimate. A `type` hint the plan cannot use (or a query whose only matches are premium) comes back as a normal HTTP 200 answer with `locked: true` and `requiredPlan` rather than results - that is a paywall, not a missing item, so switch to free components and `c-*` examples and mention https://reui.io/pricing?ref=skill.
|
||||
|
||||
**Show the preview.** List every item with its `previewUrl` (a live preview page). Where a result carries `thumbnail` (blocks, `c-*` examples and icons), show the image as markdown linked to `previewUrl`, so the user sees it before you install.
|
||||
|
||||
## 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 deeper API, call **`get_component(names)`** with ALL of `componentsUsed` in ONE call (it accepts an array) and read each inline `api` - no web fetch. A very large API is trimmed on heading boundaries to fit your context and lists what it dropped in `sectionsOmitted`; when the part you need is in that list, re-read it as the response's `next` hint says rather than guessing. `validate_usage` always checks the FULL API, so it still catches a prop you invented from a trimmed capsule. **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. Write against your project's base (see [components.md](./components.md)). After `add`:
|
||||
|
||||
1. **Read the added files**; keep the composition intact. For a block, verify the components are wired correctly (for `data-grid`: a `useTable({ features: dataGridFeatures, ... })` instance passed as `table`, `recordCount` set - see [components.md](./components.md)).
|
||||
2. **Replace demo data with the user's real data** via typed structures (see [adapting.md](./adapting.md)).
|
||||
3. **Fix icon imports** to the project's icon library (see [icons.md](./icons.md)).
|
||||
4. **Align styling** to the active theme: semantic tokens for anything that carries meaning, raw palette only for categorical identity and always with its `dark:` pair (see [color.md](./color.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, motion that leaves installed components alone, and mobile-first responsiveness (see [layout.md](./layout.md) and [motion.md](./motion.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.
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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; Pro unlocks premium blocks, Ultimate adds Motion Icons, and either removes the daily request limit by default. 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 (`sections` is a number, 1 to 10: the most sections to return, default 8); it returns ordered sections, each with the best premium block for the intent (top pick + alternates). On a free account (or in free-only mode) it answers with `mode: "free"` and composes the same sections from free `c-*` examples instead, so the plan is always buildable. 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. Only some icons have a real hover animation: `animated: true` returns only those, and a concept with none comes back empty with an `animatedNote`. Each icon carries a `thumbnail` of its 4 styles; look at it before trusting a name. An old icon name still resolves (`renamedFrom` names it), unless it is also an ordinary word icons are named for today (`watch`): then it ranks today's icons for the word and `oldNameNotes` names the icon the old name installs. Motion Icons are Ultimate for **discovery as well as install**: without an Ultimate credential this returns `locked: true` and zero concepts, so check the plan before promising the user icons.
|
||||
- **`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.
|
||||
- **`get_thumbnail`** - when the choice is VISUAL and the ranking has not already made it: 2 or more candidates within about 15 score points. When the top hit leads by a wide margin, or `missedTerms` already rules the others out, skip it; an image costs roughly 10 times a result row. It returns the hosted preview image as image content so you can look yourself; pass up to 4 names in one call to compare `compose_page` alternates side by side before installing any of them. To let the USER see something, share the item's `thumbnail` and `previewUrl` instead - this tool is for your own eyes, and a client that cannot render images gets the URLs in the text block.
|
||||
- **`get_agent_skill`** - when the agent has no local ReUI skill (a cloud or tools-only client), or to check whether the installed one is stale. With no `topic` it returns the core workflow, the topic index and the installer; `topic` (for example `motion`) returns that one file in full. Compare its `version` with the one stamped atop your local `SKILL.md`.
|
||||
|
||||
## All 19 tools
|
||||
|
||||
`search`, `get_block`, `get_example`, `get_icon`, `get_thumbnail`, `list_block_groups`, `list_block_categories`, `list_example_categories`, `list_icon_categories`, `list_components`, `get_component`, `get_examples`, `search_icons`, `compose_page`, `validate_usage`, `get_install_command`, `get_project_context`, `get_agent_skill`, `get_audit_checklist`. The MCP serves the full parameter schemas; do not guess parameters beyond them.
|
||||
|
||||
## Commands (MCP prompts)
|
||||
|
||||
In Claude Code the ReUI MCP adds these prompts; other agents show MCP prompts under their own names, or not at all. Plain language runs the same loop.
|
||||
|
||||
- `/mcp__reui__build <what>`: compose a page, section or feature (plan, install, read the API, adapt, audit).
|
||||
- `/mcp__reui__add <item>`: find, install and wire in 1 component, example, block or icon.
|
||||
- `/mcp__reui__fix [target]`: fix ReUI usage (undocumented props, a base or radix mismatch, missing states, accessibility, scroll).
|
||||
- `/mcp__reui__improve [target]`: refine existing ReUI UI (hierarchy, density, states, responsive, motion).
|
||||
|
||||
## 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`, `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.
|
||||
- An inline `api` is not always the whole API: a very large one is trimmed on heading boundaries to fit your context, and the response names every dropped heading in `sectionsOmitted` plus a `next` hint for re-reading one. Never treat a trimmed capsule as complete; `validate_usage` still checks the FULL API.
|
||||
- Icon results and `get_icon` include `animated: true` and `installAnimated` only when the icon has a real hover animation (animated: `@reui/icons/animated/<style>/<name>`; static: `@reui/icons/default/<style>/<name>`). Use the strings given; never build an animated path by hand, it installs a static copy for every other icon.
|
||||
- Blocks, `c-*` examples and icons carry `thumbnail`, an absolute URL to a hosted preview image (light theme; blocks 1080x720, examples 900x600, icons 640x200 with the 4 styles side by side). Show the top 2 or 3 as markdown images linked to `previewUrl` so the user picks by eye; use `get_thumbnail` when YOU need to see them. Components have no thumbnail.
|
||||
- Each result carries `missedTerms`, the words from YOUR query it does not mention (omitted when it matches them all). A high score with 3 missed words is a near miss wearing a good score: read this before `get_thumbnail`, it is far cheaper.
|
||||
- `weakMatch: true` means the top result misses more of the query's words than it matches; `unmatchedTerms` lists the words none of the top results mention and `weakMatchNote` says what to do. Stop and re-search with the registry's own words, or compose from components, instead of installing the top hit.
|
||||
- On a free account `premiumPicks` (a sibling of `results`, never inside it) lists up to 3 premium blocks that fit the query more closely than the free answer, as previews with no install command, plus `unlock` with the plan and a pricing link. Mention the upgrade at most once per conversation, then keep building with what the plan covers. `weakMatch` and `premiumPicks` can appear together: the free answer is weak AND a premium block fits; `weakMatchNote` says so.
|
||||
- Guidance is getter-only, never on search rows: `get_component` adds `whenToUse`, `whenNotToUse` and `agentNotes`; `get_example` and `get_block` add `bestFor`, `agentNotes` and `whenNotToUse`, and `get_block` also `categoryGuidance`; `get_examples` rows add `bestFor`. Answers carry the key lines; the item page's `.md` twin has every line. Each `whenNotToUse` entry is `{ when, instead, type }`: fetch `instead` with the getter its `type` names.
|
||||
- Results are scoped to the caller's plan, so a free account never sees a block or icon it could not install. When a search matches only hidden premium items, the answer says so (`locked`, `requiredPlan`, `premiumMatches`) instead of pretending the registry is empty.
|
||||
|
||||
## Error playbook
|
||||
|
||||
- **401** - the MCP requires a signed-in ReUI account, on every request (there is no anonymous access). 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?ref=skill) as `Authorization: Bearer`.
|
||||
- **locked result** - a valid account whose plan does not cover the item. This is NOT an error and NOT a 403: it comes back as a normal HTTP 200 result carrying `locked: true` and `requiredPlan` (`"pro"` for premium blocks, `"ultimate"` for Motion Icons). Keep working with the free components and `c-*` examples, and point the user at https://reui.io/pricing?ref=skill if they want the locked item.
|
||||
- **daily allowance reached** - a free account has a per-account, per-UTC-day allowance on tool calls (ReUI sets the number and can change it, so read it from the message and never assume one). It comes back as a normal tool RESULT with `isError: true` at HTTP 200: not an HTTP error status, no back-off header to honor, and no per-minute limit to wait out. Retrying the same call just spends the next unit, so surface the message (it carries the reset and the upgrade link) to the user instead of looping. The count resets at UTC midnight, and using a Pro or Ultimate license as the MCP credential removes the limit.
|
||||
- **not found** (`found: false`) - use the returned `suggestions`, or `search`. 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?ref=skill
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"reui": {
|
||||
"url": "https://mcp.reui.io/api/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
---
|
||||
description: Frontend — shadcn primitives + ReUI PRO blocks; CLI-first; kit patterns
|
||||
globs: apps/web/**/*,packages/ui/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Frontend — shadcn/ui + ReUI PRO
|
||||
|
||||
**Источник истины:** MCP **`user-reui`** (pages/blocks/KPI/settings) + MCP `plugin-shadcn-shadcn` (primitives) + docs. Не выдумывать UI.
|
||||
|
||||
Monorepo — [`frontend-monorepo.mdc`](frontend-monorepo.mdc). ReUI — [`reui-mcp.mdc`](reui-mcp.mdc). Patterns — [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc).
|
||||
|
||||
| Документ | URL |
|
||||
|----------|-----|
|
||||
| **shadcn Components** | https://ui.shadcn.com/docs/components |
|
||||
| **ReUI Blocks** | https://reui.io/blocks |
|
||||
| **ReUI llms.txt** | https://reui.io/llms.txt |
|
||||
| **ReUI Settings** | https://reui.io/blocks/application/settings |
|
||||
| **Installation** | https://ui.shadcn.com/docs/installation |
|
||||
| **Monorepo** | https://ui.shadcn.com/docs/monorepo |
|
||||
| **Forms (RHF)** | https://ui.shadcn.com/docs/forms/react-hook-form |
|
||||
|
||||
## Шаг 0 — перед любым UI-кодом
|
||||
|
||||
0. Найти существующие `reui-kit/*` / shared
|
||||
1. **Pages / KPI / settings / enterprise** → MCP `user-reui` (`search` → `get_block` / `compose_page`) + cite `previewUrl`
|
||||
2. **Primitives** → MCP `plugin-shadcn-shadcn` + `pnpm dlx shadcn@latest docs <component>`
|
||||
3. CLI add из `apps/web` → adapt
|
||||
|
||||
**Новая страница** → сначала ReUI PRO `compose_page` / blocks ([reui.io/blocks](https://reui.io/blocks)), не ui.shadcn.com/blocks как primary.
|
||||
|
||||
## Shared / kit (обязательно)
|
||||
|
||||
| Component | Файл |
|
||||
|-----------|------|
|
||||
| `PageShell` | `page-shell.tsx` |
|
||||
| `ResourcePage` | `reui-kit/resource-page.tsx` |
|
||||
| `OpsDashboard` / `KpiStatGrid` / `QuickActionGrid` | `reui-kit/ops-dashboard.tsx`, `kpi-stat-grid.tsx`, `quick-action-grid.tsx` |
|
||||
| `SettingsShell` | `reui-kit/kanban-board.tsx` |
|
||||
| `DetailPanel` | `reui-kit/detail-panel.tsx` |
|
||||
| `SettingsShell` | `reui-kit/settings-shell.tsx` |
|
||||
| `EmptyState` | `empty-state.tsx` |
|
||||
| `QueryState` | `query-state.tsx` |
|
||||
| `ConfirmDialog` | `confirm-dialog.tsx` |
|
||||
| `StatusBadge` | `status-badge.tsx` |
|
||||
| `FormSheet` | `form-sheet.tsx` |
|
||||
| `FormField` | `form-field.tsx` |
|
||||
| `LoadingButton` | `loading-button.tsx` |
|
||||
| `SettingRow` | `setting-row.tsx` |
|
||||
|
||||
**Не использовать как эталон:** устаревшие `PageHeader` / `SectionCards` / `DataGridCard` / `TableCard` (если удалены или мёртвы).
|
||||
|
||||
**Overlay:** Sheet — forms; AlertDialog — destructive confirm.
|
||||
|
||||
## Шаг 1 — CLI
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
pnpm dlx shadcn@latest add button field input ...
|
||||
pnpm dlx shadcn@latest add @reui/frame @reui/data-grid @reui/filters
|
||||
pnpm dlx shadcn@latest add @reui/stats-12 @reui/card-35 @reui/settings-16 @reui/auth-13
|
||||
pnpm dlx shadcn@latest apply b2fA --only theme -y
|
||||
```
|
||||
|
||||
## Приоритет композиции
|
||||
|
||||
1. `@authportal/ui/components/*` (primitives)
|
||||
2. ReUI PRO block → adapt (`blocks/` reference + kit)
|
||||
3. `reui-kit/*` + shared
|
||||
4. Domain-обёртка
|
||||
|
||||
## Запрещено
|
||||
|
||||
| ❌ | ✅ |
|
||||
|----|-----|
|
||||
| `<table>`, raw select | DataGrid / `Select` |
|
||||
| `bg-emerald-*` | semantic / ReUI `variant` |
|
||||
| Card как ops-shell | ReUI `Frame` |
|
||||
| Hand-roll KPI | `KpiStatGrid` / [stats-12](https://reui.io/preview/base/stats-12) |
|
||||
| `space-y-*` | `flex` + `gap-*` |
|
||||
| `@/components/ui/*` | `@authportal/ui/components/*` |
|
||||
|
||||
## Эталоны проекта
|
||||
|
||||
| Зона | Файл | Preview |
|
||||
|------|------|---------|
|
||||
| Shell | `layout/app-shell.tsx` | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
||||
| Login | `routes/login.tsx` | [auth-13](https://reui.io/preview/base/auth-13) |
|
||||
| Dashboard | `routes/_auth/index.tsx` | [stats-12](https://reui.io/preview/base/stats-12) / dashboard-1 |
|
||||
| Lists | `ResourcePage` | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||||
| Settings | `settings/integrations.tsx` | [settings-16](https://reui.io/preview/base/settings-16) |
|
||||
|
||||
## Чеклист
|
||||
|
||||
- [ ] `user-reui` + preview/docs для зоны
|
||||
- [ ] Frame surface
|
||||
- [ ] CLI add при новых items
|
||||
- [ ] `pnpm --filter web build`
|
||||
|
||||
## Язык
|
||||
|
||||
Ответы — русский. Commits — [`commit-messages-ru.mdc`](commit-messages-ru.mdc).
|
||||
@@ -1,194 +0,0 @@
|
||||
---
|
||||
description: Единые UI-паттерны web — shared components, docs workflow, матрица стандартизации
|
||||
globs: apps/web/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Frontend UI Patterns
|
||||
|
||||
См. также: [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`reui-mcp.mdc`](reui-mcp.mdc), [Components](https://ui.shadcn.com/docs/components), [ReUI llms.txt](https://reui.io/llms.txt).
|
||||
|
||||
## Docs workflow (обязательно)
|
||||
|
||||
0. Codegraph / поиск существующих shared/domain / `reui-kit`
|
||||
1. Skill ReUI + shadcn — component selection
|
||||
2. **Primary:** MCP `user-reui` — search → `get_block` / `compose_page` (`surface: "frame"`) — **всегда cite `previewUrl` + `docsUrl`**
|
||||
3. Primitives: MCP `plugin-shadcn-shadcn` + `pnpm dlx shadcn@latest docs <component>`
|
||||
4. CLI: `cd apps/web && pnpm dlx shadcn@latest add @reui/...` → сверить API
|
||||
5. Код по examples + docs (только после MCP ↔ docs)
|
||||
6. Context7 — **только** TanStack Router/Query, Recharts
|
||||
7. MCP `validate_usage` / `get_audit_checklist` — перед merge
|
||||
|
||||
## Surface
|
||||
|
||||
Проект использует **ReUI Frame** (`surface: frame`), не shadcn Card как оболочку list/ops-экранов. Эталон списка: [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2).
|
||||
|
||||
## Обязательные референсы по зонам
|
||||
|
||||
| Зона | Preview |
|
||||
|------|---------|
|
||||
| KPI | [stats-12](https://reui.io/preview/base/stats-12) — EvoBGP hybrid SoT |
|
||||
| Quick Actions | [stats-12](https://reui.io/preview/base/stats-12) · [card-12](https://reui.io/preview/base/card-12) → `QuickActionGrid` |
|
||||
| List | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||||
| Settings | [settings-16](https://reui.io/preview/base/settings-16), [Application Settings](https://reui.io/blocks/application/settings) |
|
||||
| Settings rows / Health-check | [settings-2](https://reui.io/preview/base/settings-2), [settings-3](https://reui.io/preview/base/settings-3) |
|
||||
| Auth | [auth-13](https://reui.io/preview/base/auth-13) |
|
||||
| Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) |
|
||||
| Forms | [form-7](https://reui.io/preview/base/form-7) |
|
||||
| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
||||
|
||||
**SettingRow:** `FieldSeparator` opt-in (`separated`); не между toggle-row и nested fields (Health-check). Settings-секции — отдельные Frame + `gap`, без hairline под PageHeader.
|
||||
|
||||
## Иерархия компонентов
|
||||
|
||||
```
|
||||
@authportal/ui/components/* ← shadcn CLI (packages/ui)
|
||||
@/components/reui/* ← ReUI CLI @reui/* (apps/web)
|
||||
apps/web/src/components/ ← shared + domain + layout
|
||||
page-shell.tsx
|
||||
reui-kit/
|
||||
resource-page.tsx ← list: Frame + line Tabs + Filters + DataGrid
|
||||
kanban-board.tsx ← kanban + SettingsShellSkeleton
|
||||
detail-panel.tsx ← detail: Frame header/metrics
|
||||
settings-shell.tsx
|
||||
ops-dashboard.tsx ← KPI stats-12 + charts
|
||||
catalog-board-toggle.tsx
|
||||
empty-state.tsx
|
||||
query-state.tsx
|
||||
confirm-dialog.tsx
|
||||
status-badge.tsx
|
||||
form-sheet.tsx
|
||||
form-field.tsx
|
||||
loading-button.tsx
|
||||
layout/ ← app-shell, site-header, app-sidebar
|
||||
domain-* ← бизнес-компоненты
|
||||
```
|
||||
|
||||
## Матрица стандартизации
|
||||
|
||||
| Элемент | Shared | Primitive |
|
||||
|---------|--------|-----------|
|
||||
| Page wrapper | `PageShell` | — |
|
||||
| List page | `ResourcePage` | ReUI `Frame` + `data-grid` + `filters` + shadcn `Tabs` `variant="line"` |
|
||||
| Catalog / Board | `CatalogBoardToggle` + `ResourcePage` / `SettingsShell` | `?view=board` на `/groups`, `/services` |
|
||||
| Kanban | `SettingsShell` / `SettingsShellSkeleton` | ReUI `kanban` + `Frame` |
|
||||
| Detail | `DetailPanel` | ReUI `Frame` |
|
||||
| Settings | `SettingsShell` | — |
|
||||
| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [stats-12](https://reui.io/preview/base/stats-12) hybrid |
|
||||
| Quick Actions | `QuickActionGrid` | Frame tiles + Badge «Перейти» |
|
||||
| Empty | `EmptyState` | `Empty` |
|
||||
| Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` |
|
||||
| Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) |
|
||||
| Create/Edit | `FormSheet` + `*-edit-sheet.tsx` | `Sheet`, `Field` |
|
||||
| Form field | `FormField` | `Field`, `Input`, `Select` |
|
||||
| Submit button | `LoadingButton` | `Button`, `Spinner` |
|
||||
| Delete confirm | `ConfirmDialog` | `AlertDialog` |
|
||||
| Nav | `AppSidebar` (`isActive` обязателен) | `Sidebar` |
|
||||
| Breadcrumbs | `SiteHeader` | `Breadcrumb` |
|
||||
| Dates | `lib/format.ts` | — |
|
||||
|
||||
## Header actions
|
||||
|
||||
Независимые CTA в header → `flex shrink-0 flex-wrap items-center justify-end gap-2`.
|
||||
|
||||
**Не** оборачивать в `ButtonGroup` (он склеивает кнопки). `ButtonGroup` — только для связанных контролок (filter chips и т.п.).
|
||||
|
||||
Max 1 primary (`default`) на экран; остальные `outline` / `ghost`.
|
||||
|
||||
Toggle «Доска» / «К каталогу» — всегда `outline` в `primaryAction` / `SettingsShell.toolbarActions` (не отдельный Frame-shell).
|
||||
|
||||
## Line tabs (project standard)
|
||||
|
||||
Эталон: [c-tabs-2](https://reui.io/preview/base/components/c-tabs-2) + counted [filtering-2](https://reui.io/preview/base/data-grid-filtering-2).
|
||||
|
||||
```tsx
|
||||
<TabsList variant="line" className="gap-5">
|
||||
<TabsTrigger value="…" className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3">
|
||||
<span>Label</span>
|
||||
<span className="bg-muted text-muted-foreground … tabular-nums rounded-md">{count}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
```
|
||||
|
||||
- Примитив: [`packages/ui/.../tabs.tsx`](../../packages/ui/src/components/tabs.tsx) — line = `h-auto`, без `flex-1`, без `dark:data-active:bg-input/30`, underline `after:bottom-0`
|
||||
- Active = яркий текст + **foreground underline**, без фона
|
||||
- Count pill всегда `bg-muted`
|
||||
- Active state — Base UI `data-active`, не Radix `data-[state=active]`
|
||||
- Не трогать internals `reui/date-selector`
|
||||
|
||||
## Catalog / Board (`/groups`, `/services`)
|
||||
|
||||
| Режим | UI | Search |
|
||||
|-------|-----|--------|
|
||||
| Catalog (default) | `ResourcePage` + primary create | omit / `view=catalog` |
|
||||
| Board | `SettingsShell` DnD | `?view=board` |
|
||||
|
||||
- Groups tabs: Все / С доменами / Пустые
|
||||
- Services tabs: Все / Включены / Выключены / Без группы
|
||||
- DnD только на board; kanban hooks/cards не удалять
|
||||
|
||||
## Dashboard KPI
|
||||
|
||||
Эталон: [stats-12](https://reui.io/preview/base/stats-12) — icon tile + value + label + optional badge/footer; клик через `to` (`Link`) / `onSelect`. Compact strip: [card-35](https://reui.io/preview/base/card-35).
|
||||
|
||||
## Sidebar
|
||||
|
||||
- Каждый `SidebarMenuButton` получает `isActive` от pathname (`useRouterState`)
|
||||
- `--sidebar-accent` в `AppShell` — заметный mix primary (~14%), не 5%
|
||||
|
||||
## Overlay selection
|
||||
|
||||
| Сценарий | Компонент |
|
||||
|----------|-----------|
|
||||
| Create/edit форма | `Sheet` |
|
||||
| Destructive confirm | `AlertDialog` via `ConfirmDialog` |
|
||||
| Modal preview | `Dialog` |
|
||||
|
||||
## Block registry
|
||||
|
||||
| Зона | Block / эталон |
|
||||
|------|----------------|
|
||||
| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
||||
| List + tabs + filters | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||||
| Line tabs | [c-tabs-2](https://reui.io/preview/base/components/c-tabs-2) |
|
||||
| KPI | [stats-12](https://reui.io/preview/base/stats-12) — на dashboard, certificates, domains, groups, services |
|
||||
| Login | [auth-13](https://reui.io/preview/base/auth-13) |
|
||||
| Settings | [settings-16](https://reui.io/preview/base/settings-16) / [settings-6](https://reui.io/preview/base/settings-6) |
|
||||
| Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) |
|
||||
|
||||
## Spacing
|
||||
|
||||
```
|
||||
AppShell main: gap-4 md:gap-6, px-4 md:px-6, py-4 md:py-5
|
||||
(--sidebar-width: 240px; header h-12; AppSwitcher + AppsMenu — Shared App Shell chrome)
|
||||
PageShell: gap-4 md:gap-6
|
||||
Title/desc: gap-px
|
||||
Card/Frame grid: gap-4 (dashboard denser: gap-2 md:gap-3)
|
||||
FieldGroup: gap-4
|
||||
Item list: gap-2
|
||||
Toolbar / header actions: gap-2
|
||||
Tabs list (line): gap-5
|
||||
```
|
||||
|
||||
Shared chrome (vps-tracker / CFDM / EvoBGP): см. [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md) — секция **Shared App Shell chrome**. Preview: [app-shell-12](https://reui.io/preview/base/app-shell-12).
|
||||
|
||||
**Запрещено:** `space-y-*`, raw colors (`bg-emerald-*`), custom empty divs, page-level Spinner / plain «Загрузка…» без Skeleton.
|
||||
|
||||
## UX/UI (состояния данных)
|
||||
|
||||
Каждый блок: **default, hover, focus, disabled, empty, loading, error**.
|
||||
|
||||
- **Loading** — `Skeleton` / `ResourcePage` skeleton / `SettingsShellSkeleton` / `OpsDashboard` skeleton, не Spinner на странице
|
||||
- **Empty** — `EmptyState` с CTA
|
||||
- **Zero-results** — message внутри DataGrid (+ «Сбросить»)
|
||||
- **Error** — `QueryState` / `Alert` + `onRetry`
|
||||
- **Overflow** — `truncate`, `max-w-*`, `Tooltip`; `tabular-nums` для чисел
|
||||
- **Density** — operational (`dense` Frame/DataGrid); max 1 primary CTA
|
||||
- **A11y** — `aria-invalid`, `aria-label`/`sr-only` на icon-only, `aria-current="page"` на active nav/settings
|
||||
|
||||
## Button hierarchy (max 1 primary per screen)
|
||||
|
||||
1. `default` — главный CTA
|
||||
2. `outline` — вторичные действия
|
||||
3. `ghost` / `link` — навигация, cancel
|
||||
4. `destructive` — только с `ConfirmDialog`
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
description: Только hybrid KPI — KpiStatGrid / row tile DNA (stats-12 + IconTile). Запрет SectionCards и hand-roll.
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# KPI hybrid — только kit (stats-12 + IconTile)
|
||||
|
||||
Preview: [stats-12](https://reui.io/preview/base/stats-12). Primitive: [icon-tile](https://reui.io/docs/components/base/icon-tile). SoT DNA = EvoBGP. Markup: `apps/web/src/components/reui-kit/kpi-stat-grid.tsx`.
|
||||
|
||||
Связанные: [`reui-mcp.mdc`](reui-mcp.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc).
|
||||
|
||||
## MUST
|
||||
|
||||
| Зона | Компонент / DNA |
|
||||
|------|-----------------|
|
||||
| KPI-полосы / dashboard metrics | только `reui-kit/KpiStatGrid` (через `OpsDashboard` / `DetailPanel.Metrics` при наличии) |
|
||||
| Markup | horizontal compact hybrid: icon left `IconTile` `variant="elevated"` `className="size-10.5"` + label/Badge + value ± `variant` |
|
||||
| Row icon tiles (data-grid) | та же DNA — semantic `text-*` на IconTile elevated |
|
||||
| Quick Actions | только `reui-kit/QuickActionGrid` (sibling hybrid DNA, IconTile elevated) |
|
||||
|
||||
Импорты UI: `@authportal/ui/components/*`. IconTile: `@/components/reui/icon-tile`.
|
||||
|
||||
## NEVER
|
||||
|
||||
- SectionCards / vertical-only KPI / hand-roll Frame/Card KPI
|
||||
- `Item` `size-10.5` `bg-muted` hybrid вместо IconTile
|
||||
- Другой size / radius / solid brand fill вместо `elevated`
|
||||
- `card-35` как замена stats-12 hybrid KPI
|
||||
- Копипаст ReUI block в route — adapt через `reui-kit/`
|
||||
- Голый lucide `size-4` в name-cell без IconTile
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
description: ReUI PRO (@reui) — MCP user-reui, Frame surface, kit, license, матрица выбора
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# ReUI MCP — обязательно (PRO + free)
|
||||
|
||||
Проект: **Base UI** (`style: base-nova`), surface lock **`frame`**.
|
||||
|
||||
Связанные: [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`frontend-monorepo.mdc`](frontend-monorepo.mdc), [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc).
|
||||
|
||||
| Документ | URL |
|
||||
|----------|-----|
|
||||
| **Introduction** | https://reui.io/docs |
|
||||
| **llms.txt** | https://reui.io/llms.txt |
|
||||
| **Get Started** | https://reui.io/docs/get-started |
|
||||
| **Styling** | https://reui.io/docs/styling |
|
||||
| **Registry** | https://reui.io/docs/registry |
|
||||
| **MCP** | https://reui.io/docs/mcp |
|
||||
| **Agent Skills** | https://reui.io/docs/agent-skills |
|
||||
| **Cursor MCP** | https://reui.io/docs/cursor |
|
||||
| **Blocks** | https://reui.io/blocks |
|
||||
| **Settings blocks** | https://reui.io/blocks/application/settings |
|
||||
| **License** | https://reui.io/docs/license-setup |
|
||||
| **Base UI components** | https://reui.io/docs/components/base/<name> |
|
||||
|
||||
## Primary MCP
|
||||
|
||||
1. **`user-reui`** — `search` / `compose_page` / `get_block` / `get_component` / `get_install_command` / `validate_usage` / `get_audit_checklist`
|
||||
2. **`plugin-shadcn-shadcn`** — primitives `@shadcn`; для `@reui` — вторично
|
||||
|
||||
**Обязательно** цитировать `previewUrl` + `docsUrl` для каждой UI-зоны.
|
||||
|
||||
**Registry (актуально):** 20 free components — `alert`, `autocomplete`, `badge`, `data-grid`, `date-selector`, `event-calendar`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `sortable`, `stepper`, `timeline`, `tree` ([docs](https://reui.io/docs), [MCP](https://reui.io/docs/mcp)). Skill: `.claude/skills/reui` (v `668fb463eb`); обновление: `curl.exe -fsSL https://mcp.reui.io/install | node -` из корня проекта.
|
||||
|
||||
**Важно:** skill описывает текущий registry (в т.ч. data-grid на TanStack Table v9). Установленный в проекте `@reui/data-grid` может оставаться на v8 до явного CLI upgrade — не ломать kit без миграции.
|
||||
|
||||
## Когда ReUI vs shadcn
|
||||
|
||||
| Задача | Registry | Импорт |
|
||||
|--------|----------|--------|
|
||||
| Button, Sheet, Field, Sidebar, Tabs | `@shadcn` | `@authportal/ui/components/*` |
|
||||
| PRO pages/sections (settings, stats, auth, dashboard) | `@reui` blocks | adapt → `apps/web/src/components/` / `reui-kit/` |
|
||||
| Data Grid | `@reui` | `@/components/reui/data-grid/*` → `ResourcePage` |
|
||||
| Filters | `@reui` | `@/components/reui/filters` |
|
||||
| Frame surface | `@reui` | `@/components/reui/frame` |
|
||||
| KPI | block [stats-12](https://reui.io/preview/base/stats-12) | `reui-kit/KpiStatGrid` — см. [`kpi-hybrid.mdc`](kpi-hybrid.mdc) |
|
||||
| Quick Actions | Frame tiles sibling KPI | `reui-kit/QuickActionGrid` |
|
||||
| Semantic badge / alert | `@reui` | `@/components/reui/badge`, `@/components/reui/alert` |
|
||||
| Number / date / autocomplete / color / kanban / timeline / stepper / tree | `@reui` | `@/components/reui/*` |
|
||||
| Event Calendar / Gantt / Icon Tile (registry, 20 free) | `@reui` | CLI `@reui/<name>` → `@/components/reui/*` при установке |
|
||||
|
||||
**Сложные списки** — `ResourcePage` (Frame + data-grid + filters), не raw `<table>`, не DataGridCard.
|
||||
**Quick Actions** — только `QuickActionGrid` (не Card / Button grid).
|
||||
|
||||
## MCP workflow
|
||||
|
||||
0. Codegraph / поиск существующих `reui-kit/*`, `@/components/reui/*`
|
||||
1. `user-reui` `search` (`surface: "frame"`, `category` при известном)
|
||||
2. Страница целиком → `compose_page`; секция → `get_block`
|
||||
3. `get_component` для API primitives из `componentsUsed`
|
||||
4. CLI из `apps/web`: `pnpm dlx shadcn@latest add @reui/<name> --yes`
|
||||
5. Post-add: shadcn imports → `@authportal/ui/components/*`
|
||||
6. Adapt by reuse → kit / route
|
||||
7. `validate_usage` + `get_audit_checklist`
|
||||
|
||||
## Размещение
|
||||
|
||||
| Слой | Путь | Импорт |
|
||||
|------|------|--------|
|
||||
| shadcn | `packages/ui/src/components/` | `@authportal/ui/components/*` |
|
||||
| ReUI CLI | `apps/web/src/components/reui/` | `@/components/reui/*` |
|
||||
| PRO blocks (reference) | `apps/web/src/components/blocks/` | adapt into kit, не копипаст в routes |
|
||||
| Kit | `apps/web/src/components/reui-kit/` | `@/components/reui-kit/*` |
|
||||
|
||||
## Установленные ReUI (apps/web)
|
||||
|
||||
**Components:** `frame`, `data-grid/*`, `filters`, `badge`, `alert`, `timeline`, `icon-tile`, `icon-stack`
|
||||
|
||||
**Kit:** `KpiStatGrid`, `QuickActionGrid`, `AdminUsersGrid` (lists = PageShell + Frame + DataGrid; `ResourcePage` не list-kit)
|
||||
|
||||
**Blocks (reference):** `stats-12`, `card-35`, `auth-13`, `app-shell-12`, `settings-16`, `settings-8`, `empty-state-12`, `form-7`, `data-grid-filtering-2`, `dashboard-1`, …
|
||||
|
||||
## License
|
||||
|
||||
```env
|
||||
# .env.local (gitignored)
|
||||
REUI_LICENSE_KEY=
|
||||
```
|
||||
|
||||
`apps/web/components.json` → `@reui` с `Authorization: Bearer ${REUI_LICENSE_KEY}`.
|
||||
|
||||
## Эталоны preview
|
||||
|
||||
| Зона | Preview |
|
||||
|------|---------|
|
||||
| KPI / Quick Actions | https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-12 |
|
||||
| List | https://reui.io/preview/base/data-grid-filtering-2 |
|
||||
| Settings | https://reui.io/preview/base/settings-16 |
|
||||
| Auth | https://reui.io/preview/base/auth-13 |
|
||||
| Shell | https://reui.io/preview/base/app-shell-12 |
|
||||
| Empty | https://reui.io/preview/base/empty-state-12 |
|
||||
|
||||
## Запрещено
|
||||
|
||||
- Копипаст с reui.io без CLI
|
||||
- ReUI в `packages/ui` / импорт как `@authportal/ui`
|
||||
- Radix-варианты docs — только Base UI
|
||||
- Raw `bg-emerald-*` вместо ReUI `variant`
|
||||
- Hand-roll data-grid/filters/KPI/Quick Actions/settings rows при наличии `@reui` / kit
|
||||
- Смешивать Card и Frame на ops-экране
|
||||
- Ставить shadcn/ui blocks выше ReUI PRO
|
||||
|
||||
## Чеклист
|
||||
|
||||
- [ ] `user-reui` search/get_block + previewUrl
|
||||
- [ ] `surface: frame`
|
||||
- [ ] CLI add из `apps/web` при новом item
|
||||
- [ ] Kit / `@/components/reui` / `@authportal/ui` — правильный слой
|
||||
- [ ] `pnpm --filter web build`
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
description: ReUI - read the reui skill before any UI work
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
## ReUI design rules (every UI edit, follow-ups included)
|
||||
|
||||
This project uses an existing design system: ReUI on shadcn/ui. Keep its look. Generic frontend advice (vary fonts or themes, gradients, page-load or staggered reveals, "2-3 intentional motions", Framer Motion) does not apply here, and other design skills lose where they disagree.
|
||||
|
||||
1. Use installed components first (`components/ui`, `components/reui`): tables `data-grid`, filtering `filters`, boards `kanban`, modals `Dialog` or `AlertDialog`, toasts `sonner`. Never set height, radius, padding, shadow or motion on them; use `size`, `variant`, `spacing`.
|
||||
2. Color: semantic tokens. State: `<Badge variant="success-light">` from `@/components/reui/badge` at default size, or Alert variants. Raw palette only for categorical identity, with its `dark:` pair.
|
||||
3. Spacing: `gap-*` on the parent: `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections. Type: `text-sm` body, `text-xs` meta, nothing smaller; never resize FrameTitle or CardTitle.
|
||||
4. Motion: installed parts already animate; never retime them. Yours: hover `transition-colors`; reveal `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]`; transform and opacity only; never `transition-all`, bounce, or a stagger on table rows or lists of 20+; at most 1 entrance per view; `MotionConfig reducedMotion="user"` plus `motion-reduce:`.
|
||||
5. Every data region: empty state with an action, `Skeleton` loading, inline error with Retry.
|
||||
6. Before UI code, write the plan from `.agents/skills/reui/SKILL.md` in your reply. Before you finish, run its self-check on the files you changed and fix every hit (2 rounds at most). Full rules: `.agents/skills/reui/SKILL.md`. If that folder is missing, call the `reui` MCP tool `get_agent_skill`.
|
||||
|
||||
Managed by the ReUI installer. This file is replaced on the next run.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
description: shadcn MCP — primitives secondary; pages/blocks → user-reui first
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# shadcn MCP — primitives (secondary)
|
||||
|
||||
Перед UI-задачей: **сначала** MCP `user-reui` для pages / KPI / lists / settings / Frame ([`reui-mcp.mdc`](reui-mcp.mdc)).
|
||||
MCP `plugin-shadcn-shadcn` — для **primitives** `@shadcn` и registry sync, не вместо PRO blocks.
|
||||
|
||||
CLI и docs — **после** MCP, по команде из `get_add_command_for_items`.
|
||||
|
||||
## Порядок (primitives)
|
||||
|
||||
0. Поиск существующих shared / `reui-kit`
|
||||
1. **`get_project_registries`**
|
||||
2. **`search_items_in_registries`** — primitive / example
|
||||
3. **`get_item_examples_from_registries`**
|
||||
4. **`get_add_command_for_items`** → CLI из `apps/web`
|
||||
5. `pnpm dlx shadcn@latest docs <component>` — сверить API
|
||||
6. Адаптировать под TanStack → `apps/web/src/`
|
||||
7. Context7 — только TanStack / Recharts / не-shadcn
|
||||
8. **`get_audit_checklist`** — перед merge
|
||||
|
||||
## Когда
|
||||
|
||||
| Задача | MCP |
|
||||
|--------|-----|
|
||||
| Новая страница / KPI / list / settings | **`user-reui`** ([`reui-mcp.mdc`](reui-mcp.mdc)) |
|
||||
| Нет примитива в `@authportal/ui` | `plugin-shadcn-shadcn` → add |
|
||||
| Сомнение в props примитива | examples + docs CLI |
|
||||
|
||||
## Запрещено
|
||||
|
||||
- Писать UI по памяти
|
||||
- Ставить ui.shadcn.com/blocks выше ReUI PRO
|
||||
- Самописные примитивы при наличии registry item
|
||||
|
||||
## Сервер
|
||||
|
||||
- **MCP:** `plugin-shadcn-shadcn` (+ `user-reui` primary)
|
||||
- Schema: `mcps/plugin-shadcn-shadcn/tools/`
|
||||
|
||||
Связанные: [`shadcn-ui-production.mdc`](shadcn-ui-production.mdc), [`reui-mcp.mdc`](reui-mcp.mdc), [`frontend-shadcn.mdc`](frontend-shadcn.mdc).
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
description: shadcn/ui + ReUI PRO — глобальные UI-принципы; ReUI PRO выше shadcn
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# shadcn/ui + ReUI PRO — правила проекта
|
||||
|
||||
UI строится по [shadcn/ui](https://ui.shadcn.com/docs/installation) + **ReUI PRO** (`@reui`): [Components](https://ui.shadcn.com/docs/components), [Blocks](https://ui.shadcn.com/blocks), [ReUI Get Started](https://reui.io/docs/get-started), [llms.txt](https://reui.io/llms.txt).
|
||||
|
||||
**Иерархия:** ReUI PRO (`user-reui`) **выше** базового shadcn. Pages / KPI / lists / settings / shell → ReUI; shadcn — primitives.
|
||||
|
||||
**Первый шаг UI-задачи:**
|
||||
1. MCP **`user-reui`** для pages / KPI / data-grid / settings / Frame / Quick Actions (`surface: frame`)
|
||||
2. MCP **`plugin-shadcn-shadcn`** для primitives `@shadcn`
|
||||
|
||||
См. [`shadcn-mcp.mdc`](shadcn-mcp.mdc), [`reui-mcp.mdc`](reui-mcp.mdc). Contract: [`docs/ui-design-contract.md`](../../docs/ui-design-contract.md).
|
||||
|
||||
## Frontend
|
||||
|
||||
Детали: [`frontend-shadcn.mdc`](frontend-shadcn.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc), [`frontend-monorepo.mdc`](frontend-monorepo.mdc).
|
||||
|
||||
Кратко: MCP → CLI из `apps/web` → Block → kit → `pnpm --filter web build`. Кастомный CSS и самописные примитивы **запрещены**.
|
||||
|
||||
## Стек
|
||||
|
||||
- Monorepo: `apps/web` + `packages/ui` (`@authportal/ui`), pnpm workspaces
|
||||
- Vite + TanStack Router/Query + shadcn **base-nova** + ReUI **@reui**
|
||||
- Конфиг: [`apps/web/components.json`](apps/web/components.json), [`packages/ui/components.json`](packages/ui/components.json)
|
||||
- Тема: `pnpm dlx shadcn@latest apply b2fA --only theme -y`; ReUI tokens — [Styling](https://reui.io/docs/styling)
|
||||
- License: `REUI_LICENSE_KEY` в `apps/web/.env.local`
|
||||
|
||||
## Backend → UI
|
||||
|
||||
При правках API с экранами: [`backend-api-ui.mdc`](backend-api-ui.mdc). Backend: `apps/api` (Fastify + Drizzle).
|
||||
|
||||
## Язык
|
||||
|
||||
Русский. Commits: [`commit-messages-ru.mdc`](commit-messages-ru.mdc).
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
name: reui
|
||||
description: "ReUI design and component rules for React apps on shadcn/ui and Tailwind. Use for any UI work in a project with @reui in components.json or a components/reui folder, and in any shadcn project that asks for data grids, kanban boards, dashboards or full pages: building or restyling pages, forms and tables; colors, theme tokens and dark mode; spacing, typography and layout; animation and hover motion; installing ReUI components, examples, Pro blocks or Motion Icons. Works with the ReUI MCP."
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
metadata:
|
||||
user-invocable: "false"
|
||||
---
|
||||
|
||||
> **ReUI skill version `68f0e6fa91`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer in the mode it was installed with to update this skill: `get_agent_skill` -> `install.recommended` from the project root, with `REUI_GLOBAL=1` before `node` when this file is `~/.claude/skills/reui/SKILL.md` or `~/.agents/skills/reui/SKILL.md` (a global install). Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI
|
||||
|
||||
ReUI is an existing design system on shadcn/ui. Keep its look. Generic frontend advice (vary fonts or themes, gradients, page-load or staggered reveals, "2-3 intentional motions", Framer Motion) does not apply here.
|
||||
It ships free components, free `c-*` examples, Pro blocks and Ultimate Motion Icons through the shadcn CLI; the ReUI MCP finds them and serves their APIs.
|
||||
|
||||
## Order of authority
|
||||
|
||||
1. The installed style (`components.json` `style`, baked into `components/ui` and `components/reui`): it owns radius, height, padding, shadow and overlay motion.
|
||||
2. The installed component's variants and props (`size`, `variant`, `spacing`).
|
||||
3. These rules and the topic files.
|
||||
4. Other design skills, then your taste: where they disagree with 1 to 3, ReUI wins. Never redefine `--ease-out` or any Tailwind built-in.
|
||||
|
||||
## Before you write UI code, write this plan in your reply
|
||||
|
||||
1. Surface: operational, marketing or flow.
|
||||
2. Archetype: 1 from [compose.md](./rules/compose.md).
|
||||
3. Container family: Frame-led, Card-led, or Cards for the KPIs and a Frame for the table.
|
||||
4. Density: dense (`size="sm"` on controls that take it) or default.
|
||||
5. Regions: top to bottom, each with the item that fills it.
|
||||
6. States: empty, loading and error for each data region.
|
||||
7. Motion: none, or the 1 entrance.
|
||||
|
||||
Then build exactly that plan; if you deviate, say where.
|
||||
|
||||
## The loop
|
||||
|
||||
1. **Find**: `compose_page` for a full page, else `search` with `type` and `component` hints.
|
||||
2. **Install**: `npx shadcn@latest add @reui/<name> --yes`, with `<name>` exactly as a tool returned it (shadcn/ui parts install by bare name: `npx shadcn@latest add button --yes`).
|
||||
3. **Read the API on your base**: `components.json` style `base-nova` is Base UI, `radix-nova` is Radix. Call `get_component([...componentsUsed])` once, then install 1 `get_examples` example and read its files.
|
||||
4. **Adapt**: change data, copy and props; keep the structure and classes.
|
||||
5. **Check**, after every UI edit, follow-ups included: `validate_usage` on any name you did not read, the self-checks C1 to C4 in [quality.md](./rules/quality.md), [anti-patterns.md](./rules/anti-patterns.md), `get_audit_checklist`, typecheck, lint. Hand back in the 5 lines quality.md gives.
|
||||
|
||||
Show every item's `previewUrl` whenever you list, recommend or install it.
|
||||
|
||||
## Rules that always apply
|
||||
|
||||
- **Owners**: tables `data-grid`, filtering `filters`, boards `kanban`, modals `Dialog` (destructive confirms `AlertDialog`), toasts `sonner`; never hand-roll.
|
||||
- **Color**: semantic tokens for meaning; state colors through Badge or Alert variants (`@reui/badge` and `@reui/alert`, not shadcn's); raw palette only for categorical identity, always with its `dark:` pair.
|
||||
- **Contrast**: never `text-success` or `text-warning` as body text in light mode, and never `text-<state>-foreground` on a solid `bg-<state>` fill.
|
||||
- **Shape**: no `rounded-*` except `rounded-full`, and no `shadow-*`, `h-*` or `px-*` on Button, Input, SelectTrigger, Card or Frame; pick `size` and `variant`. A panel is a Card or Frame, never a div with `rounded-lg border`.
|
||||
- **Spacing**: `gap-*` on the parent, not `space-y-*`; `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections; title to description `gap-0.5`.
|
||||
- **Type**: `text-sm` body, `text-xs` meta (controls keep their style's size), `font-medium` or `font-semibold`, headings `tracking-tight`, numbers `tabular-nums`; nothing below `text-xs` except illustrative mini-UI.
|
||||
- **Motion**: installed components own their motion. Your own code uses `transition-colors` for hover and press, `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]` to reveal, `duration-300` with the same curve at most (media hover zoom: 500), and `SPRING` (`{ type: "spring", visualDuration: 0.25, bounce: 0 }`) for drag and reorder. At most 1 entrance animation per view (a stagger counts as 1). Animate only `transform` and `opacity`, CSS before motion/react, never set state per frame; `<MotionConfig reducedMotion="user">` once at the root plus `motion-reduce:` on CSS, where reduced motion means a calm fade, not nothing.
|
||||
- **Surfaces**: prefer 1 container family per screen. With a KPI row and a table, use FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame or a Frame inside a Card. Icons come from 1 library, the `iconLibrary` in `components.json` (on Ultimate, 1 Motion Icon on the primary action is the exception).
|
||||
- **States**: each data region has empty (1 action), loading (a `Skeleton` in the real layout) and error (Retry) states; light and dark work.
|
||||
- **Never invent**: never write a prop, variant or `@reui/` name you did not read in an `api`, an installed file or a tool result.
|
||||
|
||||
## Topics
|
||||
|
||||
Always read `compose` before building a screen, and `quality` plus `anti-patterns` at the Check step. Besides those, read only the topic files the task touches, at most 4 (for example `color`, `motion`, `layout` and `components`).
|
||||
|
||||
Installed skill: open the file. MCP only: call `get_agent_skill` with the topic, or fetch `https://mcp.reui.io/skill/reui/<file>`.
|
||||
|
||||
| Topic | File | Read when |
|
||||
| --------------- | -------------------------------------------------- | --------------------------------------------- |
|
||||
| `compose` | [rules/compose.md](./rules/compose.md) | before building any screen or section |
|
||||
| `workflow` | [rules/workflow.md](./rules/workflow.md) | `compose_page`, search hints, installing |
|
||||
| `adapting` | [rules/adapting.md](./rules/adapting.md) | changing an installed block or example |
|
||||
| `components` | [rules/components.md](./rules/components.md) | `data-grid`, `filters`, `kanban` and the rest |
|
||||
| `color` | [rules/color.md](./rules/color.md) | any color, status, chart or dark mode choice |
|
||||
| `theming` | [rules/theming.md](./rules/theming.md) | the styles, radius, density and sizes |
|
||||
| `motion` | [rules/motion.md](./rules/motion.md) | any transition, animation or motion/react |
|
||||
| `layout` | [rules/layout.md](./rules/layout.md) | house defaults, hierarchy, surfaces, states |
|
||||
| `icons` | [rules/icons.md](./rules/icons.md) | icon imports, Motion Icons |
|
||||
| `quality` | [rules/quality.md](./rules/quality.md) | the done gate: self-checks, a11y, hand-back |
|
||||
| `anti-patterns` | [rules/anti-patterns.md](./rules/anti-patterns.md) | the Check step: drift pairs, stale habits |
|
||||
| `registry` | [rules/registry.md](./rules/registry.md) | free vs Pro vs Ultimate, base vs radix |
|
||||
| `cli` | [rules/cli.md](./rules/cli.md) | `components.json`, license key, install flags |
|
||||
| `tools` | [tools.md](./tools.md) | MCP result shapes, errors, quota, commands |
|
||||
|
||||
## Plans
|
||||
|
||||
Free covers components, `c-*` examples, this skill and the MCP (a ReUI account and a daily allowance). Pro adds blocks. Ultimate adds Motion Icons. A plan miss is a normal answer with `locked: true`; keep building with what the plan covers.
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "ReUI"
|
||||
short_description: "ReUI design and component rules for React apps on shadcn/ui and Tailwind."
|
||||
default_prompt: "Use ReUI to build this UI with installed components and the ReUI motion and color rules."
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
- Never read `window`, `localStorage`, `location`, `Date.now()` or `Math.random()` in render or in a `useState` initializer; read them in an effect.
|
||||
- **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, and the layout must still hold with long names, empty values and crowded data.
|
||||
@@ -1,117 +0,0 @@
|
||||
# Anti-patterns
|
||||
|
||||
Read this at the Check step, after the self-checks in [quality.md](./quality.md). Each entry: an `Incorrect:` line, then its `Correct:` fix. Detector: the self-check that prints it (C1 to C4), or `visual` when reading finds it. Source: the pilot run that wrote it, or `default` (a training-data habit).
|
||||
|
||||
## Pairs
|
||||
|
||||
### 1. Stagger on every row (Detector: C2; source: run-08)
|
||||
|
||||
Incorrect: `transition={{ delay: i * 0.05 }}` on all 50 rows of a table
|
||||
Correct: 1 stagger of at most 6 items, `staggerChildren: 0.05`, never on table rows
|
||||
|
||||
### 2. Transition everything on a card (Detector: C1; source: runs 10, 15)
|
||||
|
||||
Incorrect: `className="rounded-xl border transition-all duration-300 hover:shadow-lg"`
|
||||
Correct: `transition-colors` for a hover color; a lift is `transform` over a static shadow
|
||||
|
||||
### 3. Raw palette for a state (Detector: C3; source: runs 08, 15)
|
||||
|
||||
Incorrect: `<span className="text-green-600">Active</span>`
|
||||
Correct: `<Badge variant="success-light">Active</Badge>`
|
||||
|
||||
### 4. Animated table rows (Detector: C1; source: runs 01, 08, 17)
|
||||
|
||||
Incorrect: `<motion.tr whileHover={{ backgroundColor: "var(--muted)" }}>`
|
||||
Correct: `data-grid` rows, which already hover; never animate rows with motion/react
|
||||
|
||||
### 5. Hover scale on the default spring (Detector: C1; source: runs 01, 10)
|
||||
|
||||
Incorrect: `<motion.div whileHover={{ scale: 1.03 }}>` with no `transition`
|
||||
Correct: CSS first: `transition-colors` for hover, `transition-transform active:scale-[0.98] motion-reduce:transition-none` for press
|
||||
|
||||
### 6. Entrance from scale 0 (Detector: C1; source: runs 10, 15)
|
||||
|
||||
Incorrect: `<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }}>`
|
||||
Correct: `initial={{ opacity: 0, scale: 0.95 }}`: start at scale 0.95 or more and move 8px or less
|
||||
|
||||
### 7. Hand-built modal or toast (Detector: C4; source: run-15)
|
||||
|
||||
Incorrect: `<div className="fixed inset-0 z-50 bg-black/50">` as a modal
|
||||
Correct: `Dialog`, `AlertDialog` for a destructive confirm, `sonner` for toasts; a palette: `<CommandDialog><Command><CommandInput /><CommandList>`, as `CommandDialog` holds no `Command`
|
||||
|
||||
### 8. Native select (Detector: C4; source: run-15)
|
||||
|
||||
Incorrect: `<select>` with `<option>` children
|
||||
Correct: the shadcn `Select`, or `NativeSelect` when the native control is wanted
|
||||
|
||||
### 9. Hand-rolled table (Detector: C4; source: run-17)
|
||||
|
||||
Incorrect: `<table>` with mapped `<tr>` rows
|
||||
Correct: `data-grid`: a `useTable({ features: dataGridFeatures })` instance passed as `table={table}`, columns typed `ColumnDef<DataGridFeatures, Order>[]`
|
||||
|
||||
### 10. Button inside a clickable wrapper (Detector: visual; source: run-02)
|
||||
|
||||
Incorrect: `<motion.button><Button>Save</Button></motion.button>`
|
||||
Correct: the `Button` alone: 1 interactive element, and it already has a press state
|
||||
|
||||
### 11. Radix asChild on Base UI (Detector: C4; source: run-17, judge)
|
||||
|
||||
Incorrect: `<DialogTrigger asChild><Button>Open</Button></DialogTrigger>`
|
||||
Correct: `<DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>` on base-nova; a link: `<Button nativeButton={false} render={<Link href="/x" />}>`
|
||||
|
||||
### 12. Component defined inside render (Detector: visual; source: Haiku runs, judge)
|
||||
|
||||
Incorrect: `const Row = () => <div />` inside the page component
|
||||
Correct: declare `Row` at module scope and pass it props, so React keeps its state
|
||||
|
||||
### 13. A panel inside a panel (Detector: visual; source: run-05)
|
||||
|
||||
Incorrect: `<FramePanel><Card>...</Card></FramePanel>`, or a Card inside a Card
|
||||
Correct: FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame
|
||||
|
||||
### 14. Small badges (Detector: visual; source: runs 03, 04, 06, 07, 10, 13, 16, 18)
|
||||
|
||||
Incorrect: `<Badge variant="success-light" size="sm">Paid</Badge>`
|
||||
Correct: `<Badge variant="success-light">Paid</Badge>` with `size` left off
|
||||
|
||||
### 15. A colored circle behind every KPI icon (Detector: visual; source: default)
|
||||
|
||||
Incorrect: `<div className="rounded-full bg-blue-100 p-2">` around each KPI icon
|
||||
Correct: the number leads; at most 1 `text-muted-foreground` icon
|
||||
|
||||
### 16. Gradient text (Detector: visual; source: default)
|
||||
|
||||
Incorrect: `bg-linear-to-r from-violet-500 to-pink-500 bg-clip-text text-transparent` on a heading
|
||||
Correct: `text-foreground` and `tracking-tight`; color carries meaning, not decoration
|
||||
|
||||
### 17. A dot or check for the current item (Detector: visual; source: default)
|
||||
|
||||
Incorrect: a dot or a `Check` icon beside the active nav item
|
||||
Correct: `bg-muted` (`dark:bg-muted/50`) plus `aria-current`
|
||||
|
||||
### 18. framer-motion (Detector: C4; source: default)
|
||||
|
||||
Incorrect: `import { motion } from "framer-motion"`
|
||||
Correct: `import { motion } from "motion/react"`
|
||||
|
||||
### 19. Clock or randomness in render (Detector: C4; source: default, near run-05)
|
||||
|
||||
Incorrect: `const [id] = useState(() => Math.random())`
|
||||
Correct: `useId()` for ids; read the clock in an effect or an event handler
|
||||
|
||||
### 20. Placeholder data (Detector: visual; source: default)
|
||||
|
||||
Incorrect: "John Doe", "Acme Inc" and round figures like `$1,000`
|
||||
Correct: specific, plausible data: "Mara Lind", `$1,284.50`, 37 open orders
|
||||
|
||||
## Your training data is out of date
|
||||
|
||||
| Old habit | In this stack |
|
||||
| ----------------------------- | --------------------------------------------------------------- |
|
||||
| `framer-motion` | `motion/react` |
|
||||
| `<DataGrid data columns>` | `useTable({ features: dataGridFeatures })` plus `table={table}` |
|
||||
| A fixed div modal | `Dialog` or `AlertDialog` |
|
||||
| A hand-built toast | `sonner` |
|
||||
| `w-5 h-5` | `size-5` |
|
||||
| never `space-y-4` in new code | `flex flex-col gap-4` |
|
||||
| `asChild` | `render` on base-nova |
|
||||
@@ -1,77 +0,0 @@
|
||||
# CLI: registry setup, license, non-interactive install
|
||||
|
||||
## Registry setup (one-time, per project)
|
||||
|
||||
Free items (the ReUI 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 and Motion Icons) require a ReUI license at install:
|
||||
|
||||
1. Add the key to `.env.local`:
|
||||
|
||||
```bash
|
||||
REUI_LICENSE_KEY=your-license-key
|
||||
```
|
||||
|
||||
2. Switch `components.json` to the authenticated object form:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${REUI_LICENSE_KEY}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`. MCP client configs expand environment variables too, but each client has its OWN syntax, so wire the ReUI MCP server with the form that client understands:
|
||||
|
||||
- Claude Code (`.mcp.json`, `~/.claude.json`): `"Authorization": "Bearer ${REUI_LICENSE_KEY}"`
|
||||
- Cursor (`.cursor/mcp.json`) and VS Code: `"Authorization": "Bearer ${env:REUI_LICENSE_KEY}"`
|
||||
- OpenCode (`opencode.json`): `"Authorization": "Bearer {env:REUI_LICENSE_KEY}"`
|
||||
- Codex (`~/.codex/config.toml`): `bearer_token_env_var = "REUI_LICENSE_KEY"` - Codex reads the variable itself, so there is no header to write
|
||||
|
||||
Only a client with no interpolation at all needs the raw `reui_pat_...` token, and then only in a file that is never committed. Copying the `components.json` form into a client that does not expand it is the common failure: the literal `${REUI_LICENSE_KEY}` placeholder is sent to the server as the credential and every call comes back 401.
|
||||
|
||||
The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry?ref=skill
|
||||
|
||||
## 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.
|
||||
|
||||
`<name>` is the item name exactly as a ReUI tool returned it. The `@reui/` prefix is for ReUI items only:
|
||||
|
||||
- The shadcn/ui components ReUI builds on (`button`, `dialog`, `select`, `card`, ...) are not ReUI items. Install them from shadcn by bare name: `npx shadcn@latest add button --yes`.
|
||||
- `badge` and `alert` exist in both registries: `@reui/badge` and `@reui/alert` are ReUI's components, bare `badge` and `alert` are shadcn's.
|
||||
- In an item's `registryDependencies`, a `@reui/<name>` entry is a ReUI item and a bare name is a shadcn/ui item. `add` installs both, so you never add them by hand.
|
||||
- Templates are not registry items. Download a template from its page on reui.io (https://reui.io/templates?ref=skill); no `shadcn add` command installs one.
|
||||
|
||||
## 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 ReUI 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/...`) need Ultimate.
|
||||
- Templates are downloads from their page on reui.io, not installs: free ones need only a ReUI account, and each template's page says what the others need.
|
||||
|
||||
If an install 401/403s, the license key is missing, invalid, or the plan does not cover that resource (blocks: Pro or higher; icons: Ultimate). Point the user to https://reui.io/account?ref=skill (their key) or https://reui.io/pricing?ref=skill (upgrade).
|
||||
|
||||
If an install 404s, the name is not a ReUI item. Read the CLI's error message, which names the right install command when there is one, or look the name up with `get_install_command` or `search`.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Color
|
||||
|
||||
Semantic tokens carry meaning; raw palette carries identity. Check every color in light and dark.
|
||||
|
||||
## Tokens
|
||||
|
||||
Surfaces, text and borders use your theme tokens (`background`, `foreground`, `card`, `muted`, `muted-foreground`, `primary`, `accent`, `destructive`, `border`, `ring`), so a theme change re-tints them. ReUI adds state tokens: `--success`, `--info` and `--warning` with their `-foreground`, `--destructive-foreground` (beside shadcn's `--destructive`) and `--invert` with `--invert-foreground` for inverted surfaces.
|
||||
|
||||
They are not part of shadcn's base theme. Installing `@reui/badge`, `@reui/alert` or `@reui/code-block` adds them to your global CSS; until one of those is installed, classes such as `bg-success` generate nothing. Check for `--success:` in your global CSS. If it is missing, add these values to `:root` and `.dark`, plus a `--color-<name>: var(--<name>)` line for each under `@theme inline`:
|
||||
|
||||
<!-- gen:tokens:start -->
|
||||
|
||||
| Token | `:root` | `.dark` |
|
||||
| -------------------------- | -------------------------- | -------------------------- |
|
||||
| `--destructive-foreground` | `var(--color-red-800)` | `var(--color-red-600)` |
|
||||
| `--success` | `var(--color-emerald-500)` | `var(--color-emerald-500)` |
|
||||
| `--success-foreground` | `var(--color-emerald-900)` | `var(--color-emerald-600)` |
|
||||
| `--info` | `var(--color-violet-500)` | `var(--color-violet-500)` |
|
||||
| `--info-foreground` | `var(--color-violet-900)` | `var(--color-violet-600)` |
|
||||
| `--warning` | `var(--color-yellow-500)` | `var(--color-yellow-500)` |
|
||||
| `--warning-foreground` | `var(--color-yellow-900)` | `var(--color-yellow-600)` |
|
||||
| `--invert` | `var(--color-zinc-900)` | `var(--color-zinc-700)` |
|
||||
| `--invert-foreground` | `var(--color-zinc-50)` | `var(--color-zinc-50)` |
|
||||
|
||||
<!-- gen:tokens:end -->
|
||||
|
||||
## Meaning vs identity
|
||||
|
||||
- Meaning (a state, emphasis, a surface, text) takes semantic tokens, through the Badge or Alert variant that already carries them.
|
||||
- Identity (a chart series, a tag color, a team, an avatar) may use raw palette: ReUI's own lint keeps it on purpose. Always hand-pair it for dark, as in `text-violet-600 dark:text-violet-400`.
|
||||
- **Incorrect:** `<span className="text-green-600">Active</span>` (raw palette for a state). **Correct:** `<Badge variant="success-light">Active</Badge>`.
|
||||
|
||||
## State recipes
|
||||
|
||||
- State badge: `<Badge variant="success-light">` from `@/components/reui/badge` (`@reui/badge`; shadcn's `components/ui/badge` has no `-light` variants or `size`), also `info-light`, `warning-light`, `destructive-light`, and `primary-light` for brand or pending. Neutral badge: `variant="outline"`. A solid `<Badge variant="success">` (white text) only when the user asks for a solid fill.
|
||||
- Inline status text: `text-success-foreground dark:text-success`, the pair the ReUI Badge uses.
|
||||
- Tinted surface: `border-success/15 bg-success/10 text-success-foreground dark:border-success/25 dark:bg-success/15 dark:text-success` (the Badge's `success-light` recipe).
|
||||
- A message block: `<Alert variant="success">` from `@/components/reui/alert` (`@reui/alert`), also `info`, `warning` and `destructive`.
|
||||
- Never `text-success` or `text-warning` as body text in light mode, and never `text-<state>-foreground` on a solid `bg-<state>` fill.
|
||||
|
||||
## Contrast
|
||||
|
||||
<!-- gen:contrast:start -->
|
||||
|
||||
Light is `text-<tone>-foreground` on `background` and on the `bg-<tone>/10` tint; dark is `dark:text-<tone>` on `background` and on the `dark:bg-<tone>/15` tint. Ratios against the neutral theme, rounded down; body text needs 4.5:1.
|
||||
|
||||
| Tone | Light | Light on tint | Dark | Dark on tint |
|
||||
| ------------- | ----- | ------------- | ---- | ------------ |
|
||||
| `success` | 9.6 | 8.8 | 8.0 | 6.6 |
|
||||
| `warning` | 8.6 | 8.1 | 10.3 | 8.1 |
|
||||
| `info` | 11.0 | 9.7 | 4.4 | 3.9 |
|
||||
| `destructive` | 8.3 | 7.0 | 6.8 | 5.7 |
|
||||
|
||||
Raw state color as light-mode text on `background`: `text-success` 2.4, `text-warning` 1.9, `text-info` 4.4.
|
||||
|
||||
<!-- gen:contrast:end -->
|
||||
|
||||
Info text in dark mode is under 4.5:1: where it must be read, write the words in `text-foreground` and color only the icon.
|
||||
|
||||
## Dark mode
|
||||
|
||||
- shadcn uses the class strategy: `@custom-variant dark (&:is(.dark *));` in your global CSS, and `.dark` on `<html>` turns it on.
|
||||
- Tokens switch by themselves; raw palette needs its `dark:` pair.
|
||||
- Check both themes before you hand back.
|
||||
|
||||
## Charts
|
||||
|
||||
Check your theme's `--chart-1` to `--chart-5` in both modes. The current shadcn themes keep the same chart colors in light and dark; some themes (reui.io's own among them) change hue. For a series whose identity must hold in both modes, use a palette var such as `var(--color-violet-500)`, as ReUI blocks do.
|
||||
|
||||
<!-- gen:charts:start -->
|
||||
|
||||
Same chart colors in light and dark: neutral, stone, zinc, mauve, olive, mist, taupe, amber, blue, cyan, emerald, fuchsia, green, indigo, lime, orange, pink, purple, red, rose, sky, teal, violet, yellow. Changes hue between light and dark: reui.io's own theme.
|
||||
|
||||
<!-- gen:charts:end -->
|
||||
@@ -1,521 +0,0 @@
|
||||
# ReUI components
|
||||
|
||||
The ReUI components: `alert`, `autocomplete`, `badge`, `cascader`, `code-block`, `color-picker`, `data-grid`, `date-selector`, `emoji-picker`, `event-calendar`, `file-type`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `signature-pad`, `sortable`, `stepper`, `time-picker`, `timeline`, `tree`. Examples and blocks are composed from these. `file-upload` is a ReUI component too, shipped as the `use-file-upload` hook rather than a primitive, so it is not in that list: install it as `@reui/use-file-upload`, which writes `hooks/use-file-upload.ts` (import `useFileUpload` from `@/hooks/use-file-upload`), call `get_component("file-upload")` for its API and `get_examples("file-upload")` for its free examples. There is no `@reui/file-upload`: the UI is a `c-file-upload-<n>` example, and one built on the hook installs it.
|
||||
|
||||
**Rule one: never guess a component's API.** Call **`get_component(name)`** for its inline `api` and share its `docsUrl` with the user, then **`get_examples(name)`** and copy a worked example. The contracts below are first-try orientation (required props, shape, the one gotcha); the `api` is the reference. A large `api` (`cascader`, `filters`, `data-grid`) is trimmed on heading boundaries: `sectionsOmitted` names what was dropped and the response's `next` hint pulls it back. `validate_usage` always checks the full API. No single block fits? Compose from components, 1 `get_examples` example each ([compose.md](./compose.md)).
|
||||
|
||||
## data-grid (the flagship - read its API every time)
|
||||
|
||||
`data-grid` wraps TanStack Table v9. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
|
||||
|
||||
- Build a TanStack table with `useTable({ features: dataGridFeatures, ... })`. `dataGridFeatures` already bundles sorting, filtering, pagination, selection, expanding, pinning, resizing and faceting: no row models to wire.
|
||||
- Pass that instance to `<DataGrid table={table} recordCount={total}>`.
|
||||
- Compose the body with `DataGridTable` inside `DataGrid`, and enable features through `tableLayout` (e.g. `{ headerSticky: true, columnsResizable: true }`), not ad-hoc classes.
|
||||
- Server-side data: `manualPagination: true` and `rowCount: total` in `useTable`, `recordCount={total}`, and fetch with the table's `pagination` state (`DataGridPagination` changes `pageSize`).
|
||||
- Columns are TanStack v9, `ColumnDef<DataGridFeatures, Order>[]`, never v8's `ColumnDef<Order>[]`; a sortable column's `header` renders `DataGridColumnHeader` (a string header cannot sort).
|
||||
- Imports, each from `@/components/reui/data-grid/<file>`: `DataGrid`, `DataGridFeatures` and `dataGridFeatures` from `data-grid`, `DataGridColumnHeader` from `data-grid-column-header`, `DataGridTable` from `data-grid-table`, `DataGridPagination` from `data-grid-pagination`, `DataGridScrollArea` from `data-grid-scroll-area`; `useTable` and `ColumnDef` from `@tanstack/react-table`.
|
||||
- States: `isLoading` renders skeleton rows from each column's `meta.skeleton` (a `ReactNode`: `<Skeleton className="h-4 w-20" />`); `emptyMessage` (a `ReactNode`) fills a grid with no rows: after a failed fetch, pass it the error with a Retry.
|
||||
- Numeric column: `meta: { headerClassName: "text-end [&>div]:justify-end", cellClassName: "text-end tabular-nums" }`.
|
||||
- Rows edited in place: `autoResetPageIndex: false` in `useTable`, or each write jumps back to page 1.
|
||||
- In a Frame: `<FramePanel className="p-0">` around `DataGridScrollArea` and `DataGridTable`, plus `tableClassNames={{ base: "min-w-[720px]" }}` so on a phone the table scrolls inside the panel instead of squeezing.
|
||||
|
||||
```tsx
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, Order>[]>(() => [
|
||||
{ accessorKey: "total", header: ({ column }) => <DataGridColumnHeader title="Total" column={column} /> },
|
||||
], [])
|
||||
const table = useTable({ features: dataGridFeatures, data, columns })
|
||||
|
||||
<DataGrid table={table} recordCount={data.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
Never a raw `<table>` or hand-rolled pagination: the API covers sticky header, virtualization and row selection.
|
||||
|
||||
## event-calendar
|
||||
|
||||
**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EventCalendar defaultEvents={events} defaultView="month" className="h-[560px]">
|
||||
<EventCalendarNav />
|
||||
<EventCalendarContent />
|
||||
</EventCalendar>
|
||||
```
|
||||
|
||||
**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column.
|
||||
|
||||
## gantt
|
||||
|
||||
**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Gantt defaultEvents={bars} resources={tasks} defaultScale="month" className="h-[480px]">
|
||||
<GanttNav />
|
||||
<GanttView />
|
||||
</Gantt>
|
||||
```
|
||||
|
||||
**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height.
|
||||
|
||||
## kanban
|
||||
|
||||
**Required:** `value` (`Record<string, T[]>`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
|
||||
<KanbanBoard>
|
||||
{Object.entries(cols).map(([id, items]) => (
|
||||
<KanbanColumn key={id} value={id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3>{id}</h3>
|
||||
<KanbanColumnHandle aria-label="Move column"><GripVertical /></KanbanColumnHandle>
|
||||
</div>
|
||||
<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. `KanbanColumnHandle` is hidden until its column is hovered or it has keyboard focus, so keep the column title outside it.
|
||||
|
||||
## 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:** `fields` (`FilterField[]`). The value is ONE `FilterQuery` tree - `query` + `onQueryChange`, or uncontrolled `defaultQuery`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const fields: FilterField[] = [
|
||||
{ id: "title", label: "Title", type: "text" },
|
||||
{
|
||||
id: "status",
|
||||
label: "Status",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "archived", label: "Archived" },
|
||||
],
|
||||
},
|
||||
]
|
||||
const [query, setQuery] = useState<FilterQuery>(() => createFilterQuery())
|
||||
|
||||
<Filters fields={fields} query={query} onQueryChange={setQuery} />
|
||||
```
|
||||
|
||||
**Gotcha:** the state is a TREE, not a list of chips. `FilterQuery` is a group of rules joined by `and`/`or` and a group may hold another group, so `(A and B) or C` is expressible; a rule is `{ id, type: "rule", path: ["status"], operator, value }` and `path` is the whole nested attribute path, root first. The pre-rewrite API is GONE: there is no `filters`/`onChange` prop, no `FilterFieldConfig` (fields are `FilterField`, nested through their own `fields`, keyed `id` not `key`), and no `createFilter()`: ids come from `createFilterIdFactory(seed)` seeded off `useId` (never minted in render), and `createFilterRule({ id })` takes one, `createFilterQuery()` none. Read the query back with `flattenFilterConditions` (`{ path, field, operator, values, negated }` per rule, incomplete rules skipped) and walk the tree yourself when the parentheses carry meaning - the primitive compiles nothing, no SQL, no query string.
|
||||
|
||||
`variant` picks the chrome over that one query: `"basic"`, the default, is the flat chip row for a toolbar over a table; `"advanced"` is the condition builder, hung off a trigger or rendered in place with `advancedMode="inline"`. Both read and write the same tree, so a saved view built in one opens in the other. Other props worth knowing before you hand-roll them: `size` is two rungs, `"sm" | "default"`, resolved per style (there is no `lg`); `reorderable` turns on drag and Alt+Arrow row moves in the builder; `onBeforeQueryChange` is the ONE veto point for every write (return `false` to refuse, it cannot rewrite); `editors` registers custom value editors a field selects by `editor` name; `labels` / `operatorLabels` own every rendered string, and `renderChip` / `renderValue` / `renderEmpty` replace rendered parts. On a field, `loadOptions` supplies async options with paging and `resolveValues` renders a chip restored from a saved view whose option was never loaded.
|
||||
|
||||
## cascader
|
||||
|
||||
**Required:** `items` (a tree of `{ value, label, children? }`), plus the panel parts inside `CascaderContent`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Cascader items={items} value={value} onValueChange={setValue}>
|
||||
<CascaderTrigger render={<Button variant="outline" />}>
|
||||
<CascaderValue placeholder="Select an attribute" />
|
||||
</CascaderTrigger>
|
||||
<CascaderContent className="w-80">
|
||||
<CascaderPanel>
|
||||
<CascaderNav>
|
||||
<CascaderBreadcrumb />
|
||||
<CascaderInput />
|
||||
</CascaderNav>
|
||||
<CascaderEmpty />
|
||||
<CascaderList maxHeight={288}>
|
||||
<CascaderItems />
|
||||
</CascaderList>
|
||||
<CascaderStatus />
|
||||
</CascaderPanel>
|
||||
</CascaderContent>
|
||||
</Cascader>
|
||||
```
|
||||
|
||||
**Gotcha:** pressing a branch NAVIGATES, it does not select - only leaves are selectable until you pass `selectable="any"` or a predicate, and once a branch is selectable its chevron becomes the only way to open it. `CascaderInput` must stay inside `CascaderContent` (Base UI refills the query from the selection when the input sits outside the popup). Always include `CascaderStatus`: it is the live region announcing level changes, which the visual breadcrumb does not provide to screen readers. Accepts a flat adjacency list via `getParent` as well as nested `children`. `searchScope="deep"` searches the current level and everything under it, `searchScope="global"` the whole tree from any level, and both annotate results with their path; `multiple` gives checkbox rows; `inline` + a bare `CascaderPanel` embeds it with no popover.
|
||||
|
||||
The shape above is `mode="drill"`, the default. `mode="tree"` keeps the same parts (drop `CascaderBreadcrumb`, pass `showBack={false}`, drive expansion with `expanded`/`onExpandedChange`); `mode="columns"` REPLACES `CascaderList` + `CascaderItems` with a single `CascaderColumns`, and has no breadcrumb. Other props worth knowing before you hand-roll them: `cascade` (multi-select only, parent/child selection with indeterminate branches - pair it with `selectable="any"`, since a leaf-only tree can never cascade), `virtualize` plus `CascaderVirtualItems` for long levels, and `getChildren` for async levels (paging, retry, `prefetch`). `CascaderFooter` pins commands below the list and `CascaderSubmenu` opens one as a side flyout. To head a run of rows use `CascaderGroup` wrapping a `CascaderLabel` - a bare label inside a listbox names nothing and is dropped from the accessibility tree - and `CascaderSeparator` for the rule between runs.
|
||||
|
||||
## 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/reui/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). `smooth` defaults to true: pass `smooth={!reduceMotion}` from motion/react's `useReducedMotion()`. Mark the active link `aria-current="location"` (track it with `onUpdate`); `"page"` is for route links.
|
||||
|
||||
## frame
|
||||
|
||||
**Required:** `Frame` > `FramePanel`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Frame>
|
||||
<FrameHeader>
|
||||
<FrameTitle>Title</FrameTitle>
|
||||
<FrameDescription>Description</FrameDescription>
|
||||
</FrameHeader>
|
||||
<FramePanel>Content</FramePanel>
|
||||
<FrameFooter>Footer</FrameFooter>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Gotcha:** the shell for data and tool-like surfaces. `spacing` (`xs` | `sm` | `default` | `lg`) sets panel padding and the gap between panels, so pick it instead of padding classes (the 1 exception: `<FramePanel className="p-0">` around a `data-grid`); `variant` is `default` | `inverse` | `ghost`; `stacked` joins panels with shared borders; `dense` removes the frame's own inset and gap (panels sit flush), and panel padding stays; `--frame-radius` is the one radius knob (panels derive `--frame-panel-radius`).
|
||||
|
||||
## icon-stack
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconStack aria-hidden="true">
|
||||
<InboxIcon className="size-4" />
|
||||
</IconStack>
|
||||
```
|
||||
|
||||
**Gotcha:** isometric layered artwork for empty states and illustrations; style the inner icon via its own `className`. Mark purely decorative stacks `aria-hidden="true"` and keep the real label in surrounding copy.
|
||||
|
||||
## icon-tile
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconTile variant="elevated" size="lg">
|
||||
<PackageIcon />
|
||||
</IconTile>
|
||||
```
|
||||
|
||||
**Gotcha:** the square container an icon sits in, so every list row, feature card and empty state shares one affordance. `variant`: `outline` (default) | `elevated` (muted fill, raised ring) | `soft` (tinted nested, tone from currentColor) | `solid` (filled tone, contrasting glyph) | `frame` (double container). `soft` and `solid` retint from one text color class (they default to `text-primary`). `size`: `xs | sm | default | lg | xl` (24/32/40/48/64px tile, glyph scales 12/14/16/20/24px). `radius`: `default | full`. Do not set a `size-*` class on the child icon unless you mean to override the tile's glyph size; recolor with `className` on the tile, not the icon.
|
||||
|
||||
## code-block
|
||||
|
||||
**Required:** `code` + `language`, or pre-highlighted `lines`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<CodeBlock code={code} language="tsx" />
|
||||
|
||||
<CodeBlock code={code} language="tsx" showLineNumbers maxLines={20}>
|
||||
<CodeBlockHeader>
|
||||
<CodeBlockTitle>use-totals.ts</CodeBlockTitle>
|
||||
<CodeBlockLanguage />
|
||||
<CodeBlockCopyButton className="ml-auto" />
|
||||
</CodeBlockHeader>
|
||||
<CodeBlockExpandButton />
|
||||
</CodeBlock>
|
||||
```
|
||||
|
||||
**Gotcha:** the one-liner is already a complete block; every child is optional chrome, at any depth. Shiki is the only npm dependency and loads lazily on first highlight; `lines` (from `highlightCode` in `code-block-highlight`, which is server-safe and has no `"use client"`) or `highlight={false}` loads nothing at all. `maxLines` caps the height AND marks the block collapsible, which is what makes `CodeBlockExpandButton` appear. Use `variant="ghost"` when the block sits inside a surface that already has a border.
|
||||
|
||||
## 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.
|
||||
|
||||
## badge
|
||||
|
||||
**Required:** none (text child).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="success-light">Success</Badge>
|
||||
<Badge variant="outline">Draft</Badge>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible. Rich `variant` set (solid, `-outline`, `-light` per color), `size` `xs..xl` (keep the default), `radius` `default | full`. Like `alert`, the color variants rely on ReUI extended tokens. Prefer `Badge` variants over raw color classes: `<tone>-light` for a state, `outline` for a neutral label; an omitted `variant` is the solid primary fill.
|
||||
|
||||
## color-picker
|
||||
|
||||
**Required:** none (`<ColorPicker />` alone is complete); wire `value` + `onValueChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<ColorPicker value={color} onValueChange={setColor} alpha>
|
||||
<ColorPickerTrigger />
|
||||
<ColorPickerContent>
|
||||
<ColorPickerPanel>
|
||||
<ColorPickerArea />
|
||||
<ColorPickerSlider channel="hue" />
|
||||
<ColorPickerSlider channel="alpha" />
|
||||
<ColorPickerFields />
|
||||
</ColorPickerPanel>
|
||||
</ColorPickerContent>
|
||||
</ColorPicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a CSS color string in `format` (`"hex"` default, `"rgb"`, `"hsl"`, `"hsb"` or `"oklch"`), `null` when empty, never an object. `onValueChange` fires on every drag move, so save in `onValueCommit`. Without `alpha` the value is always opaque. A bare `ColorPickerPanel` inside the root is an inline picker; `ColorPickerInput` replaces the trigger with a typable field.
|
||||
|
||||
## emoji-picker
|
||||
|
||||
**Required:** none; wire `onEmojiSelect` to insert a pick, or `value` + `onValueChange` to use it as a field.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EmojiPicker onEmojiSelect={({ value }) => insert(value)}>
|
||||
<EmojiPickerTrigger />
|
||||
<EmojiPickerContent>
|
||||
<EmojiPickerHeader>
|
||||
<EmojiPickerSearch />
|
||||
</EmojiPickerHeader>
|
||||
<EmojiPickerCategories />
|
||||
<EmojiPickerList />
|
||||
<EmojiPickerFooter>
|
||||
<EmojiPickerPreview />
|
||||
<EmojiPickerSkinTone />
|
||||
</EmojiPickerFooter>
|
||||
</EmojiPickerContent>
|
||||
</EmojiPicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the emoji data is fetched at runtime from a pinned jsDelivr path (`EMOJI_PICKER_DATA_URL`): under a strict Content Security Policy allow it in `connect-src`, self-host with `dataUrl`, or pass `data` (from `normalizeEmojibaseData`). Store the pick's `value` (the native emoji in its skin tone, or `:shortcode:` for a custom one). `multiple` makes the value a list, capped by `max`.
|
||||
|
||||
## file-type
|
||||
|
||||
**Required:** none; pass `type` (an extension, a family name or any string).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<FileType type="pdf" />
|
||||
<FileType type={getFileType(file.name)} size="sm" aria-hidden="true" />
|
||||
```
|
||||
|
||||
**Gotcha:** one `span` with `role="img"`, no children, no `"use client"` (fine in a Server Component). Resolve a file name, path, URL or MIME type with `getFileType`, never by splitting on the dot. `variant`: `outline` (default) | `solid` | `light` | `mono`; `size`: `mini` | `xs` | `sm` | `default` | `lg` | `xl` (18 to 56px); recolor with a `text-*` class. Pass `aria-hidden="true"` when the file name sits beside it.
|
||||
|
||||
## signature-pad
|
||||
|
||||
**Required:** `SignaturePad` > `SignaturePadArea`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<SignaturePad value={strokes} onValueChange={setStrokes}>
|
||||
<SignaturePadArea>
|
||||
<SignaturePadGuide />
|
||||
<SignaturePadPlaceholder />
|
||||
<SignaturePadControls>
|
||||
<SignaturePadUndo />
|
||||
<SignaturePadClear />
|
||||
</SignaturePadControls>
|
||||
</SignaturePadArea>
|
||||
</SignaturePad>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is an array of strokes (`{ points: [x, y, size][], color? }`), not an image: store it as JSON, and export with `useSignaturePad()` or `apiRef` (`toSVG`, `toDataURL`, `toBlob`) or `SignaturePadSave` (`format`, `onSave`). `onValueChange` fires per finished stroke, never per pointer move. Size the area with a class (`h-40` by default); an overlay in it that is not a control needs `pointer-events-none`. Drawing needs a pointer, so offer a typed name where a signature is required.
|
||||
|
||||
## time-picker
|
||||
|
||||
**Required:** none (`<TimePicker />` alone is complete); wire `value` + `onValueChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<TimePicker value={time} onValueChange={setTime}>
|
||||
<TimePickerTrigger />
|
||||
<TimePickerContent>
|
||||
<TimePickerColumns />
|
||||
<TimePickerFooter>
|
||||
<TimePickerNow />
|
||||
<TimePickerClear />
|
||||
<TimePickerConfirm />
|
||||
</TimePickerFooter>
|
||||
</TimePickerContent>
|
||||
</TimePicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a 24-hour `"HH:mm"` string (`"HH:mm:ss"` with `granularity="second"`) or `null`, never a `Date`, whatever `hourCycle={12}` shows. `minuteStep`, `min`, `max` and `isTimeDisabled` narrow the options; keep `isTimeDisabled` stable (module scope or memoized). `requireConfirm` holds picks as a draft until OK. `TimePickerInput` replaces `TimePickerTrigger` for a typable field, never beside it.
|
||||
|
||||
## base vs radix - write for the project's base
|
||||
|
||||
Every component ships a `base` (Base UI) and a `radix` (Radix UI) build under 1 name, and the CLI installs the one the project uses. Read `components.json` `style`: the part before the first `-` is the base (`base-nova` is **Base UI**, `radix-nova` is **Radix UI**). Write against that base; the deltas mirror shadcn's:
|
||||
|
||||
- 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"`.
|
||||
|
||||
Installed files and `c-*` examples are already in your base: reuse their wiring. When an `api` or example shows the other base's shape, translate it (or `validate_usage` to confirm).
|
||||
@@ -1,62 +0,0 @@
|
||||
# Compose a screen
|
||||
|
||||
Read this before you build any screen or section, then write the router's 7-line plan in your reply (Surface, Archetype, Container family, Density, Regions, States, Motion) and build exactly that plan. Style it with the topics the screen touches: [layout.md](./layout.md) (house defaults), [color.md](./color.md), [motion.md](./motion.md).
|
||||
|
||||
If the brief names a layout or a motion (a left nav, 2 entrances), follow the brief and name the default you overrode. Several entrances run as 1 stagger: at most 6 items, 50ms apart, never on table rows or lists of 20 or more.
|
||||
|
||||
## Surface
|
||||
|
||||
- **Operational**: a screen people work in every day (dashboard, records, settings, admin tools). Frame-led, dense (`size="sm"` on controls that take it), no hero, no page-load motion.
|
||||
- **Marketing**: a page a visitor reads once (landing, pricing, features). Card-led sections on the spine below, default density.
|
||||
- **Flow**: a task people finish and leave (sign in, onboarding, checkout). 1 centered Card or a Dialog, 1 primary action per step.
|
||||
|
||||
## Archetypes: pick 1 per screen
|
||||
|
||||
- **Vertical rail settings** (3 to 6 sections): a section rail on the left, 1 content column, 1 section at a time; the rail turns horizontal below `md`.
|
||||
- **Horizontal tabs** (5 to 8 views of 1 object): a full-width `Tabs` rail under the header that scrolls sideways when it overflows, 1 content column.
|
||||
- **Frame stack**: stacked Frames, each 1 focused group of settings or records; a flat page split by `Separator` rows when it should feel lighter.
|
||||
- **Table-backed management**: header, toolbar (search, `filters`), then a `data-grid` with row actions and pagination, all in 1 Frame.
|
||||
- **Dashboard grid**: a KPI row, then 1 or 2 chart or list panels, then the table.
|
||||
- **Board**: header and filters, then a `kanban` board; `KanbanBoard` stacks its columns below `sm`, and a wider board scrolls inside its own region.
|
||||
- **Record detail**: header (title, status `Badge`, actions), a main column of sections, a side column of facts that stacks under it below `lg`.
|
||||
- **Marketing spine**: optional `Badge` eyebrow, headline (the only loud element), 1-clause subhead, action row (1 primary `Button`, a second `variant="outline"`, a third a text link), specific proof beside it ("27 reviews"), media. Omit parts; never reorder them.
|
||||
- **Auth card**: 1 centered `Card` at `max-w-sm`: title, fields, 1 full-width primary `Button`, a text link to the other flow.
|
||||
- **Wizard dialog**: a `Dialog` holding a `stepper`, 1 step visible, Back (`variant="outline"`) and Next (primary) in its footer.
|
||||
|
||||
An app shell around any of them is the shadcn `Sidebar`, which turns into a `Sheet` below `md` by itself.
|
||||
|
||||
## Fill each region in this order
|
||||
|
||||
1. The ReUI component that owns the job: the router's Owners (`data-grid`, `filters`, `kanban`), `useFileUpload` from `@/hooks/use-file-upload` for file drop, or the one whose contract in [components.md](./components.md) matches.
|
||||
2. A shadcn/ui component (`Tabs`, `Dialog`, `AlertDialog`, `Sheet`, `Empty`, `Skeleton`, `sonner`).
|
||||
3. A free `c-*` example from `get_examples` that already composes it: install it and adapt.
|
||||
4. Your own composition of those parts. Never a hand-rolled table, modal or tabs.
|
||||
|
||||
Import ReUI parts file by file: `data-grid`, `filters`, `cascader`, `event-calendar` and `gantt` are folders with no index file. `data-grid`: the import line in [components.md](./components.md). `filters`, each from `@/components/reui/filters/<file>`: `Filters` from `filters`, `createFilterQuery` and `flattenFilterConditions` (1 argument, the query) from `filters-query`, the types `FilterField` and `FilterQuery` from `filters-types`.
|
||||
|
||||
## Wiring that works
|
||||
|
||||
- `data-grid`: columns are `ColumnDef<DataGridFeatures, Order>[]`, a sortable header renders `DataGridColumnHeader`, and server paging needs `manualPagination: true` and `rowCount`.
|
||||
- `filters`: apply each condition by its `operator`. Negate swaps a rule to its inverse (`is` and `is_not`, `contains` and `not_contains`, `is_any_of` and `is_none_of`, `empty` and `not_empty`) and sets `negated` only when there is none: honour both.
|
||||
- A Base UI `Button` that navigates: `<Button nativeButton={false} render={<Link href="/x" />}>`.
|
||||
- shadcn's `CommandDialog` has no `Command` inside, unlike older shadcn: wrap `CommandInput` and `CommandList` in one. `CommandItem` search matches its `value`: omit it (text is used) or use the label; ids in `onSelect`.
|
||||
|
||||
## Headers
|
||||
|
||||
A page or panel header puts the title (1 to 3 words) and a 1-clause description on the left and the actions on the right: 1 primary `Button`, the rest `variant="outline"`, row actions `variant="ghost"`.
|
||||
|
||||
## Responsive
|
||||
|
||||
- No horizontal page scroll at 390px: grids drop to 1 column, side columns stack, `min-w-0` on every shrinking parent.
|
||||
- A KPI row is `grid-cols-1 sm:grid-cols-2 xl:grid-cols-4`.
|
||||
- Every header and toolbar action stays reachable at every width: a control hidden below a breakpoint moves into a menu shown only there.
|
||||
- A table scrolls sideways inside its Frame, never the page (the `data-grid` contract in [components.md](./components.md)).
|
||||
|
||||
## States and motion
|
||||
|
||||
- Each data region owns its states: empty (shadcn `Empty` with a title, 1 line and the primary action), loading (`Skeleton` in the real layout; `data-grid` takes `isLoading`) and error (inline in the region, with Retry). Never 1 spinner for the whole page.
|
||||
- Motion: none, or the 1 entrance per view; installed parts keep their own motion, and hover and press follow [motion.md](./motion.md). An Alert, field error, notice or data that appears after an action or a load is a reveal, not an entrance: fade it in with the reveal row.
|
||||
|
||||
## Remove first
|
||||
|
||||
Overloaded? Remove, in order: decorative summaries, duplicate context, extra badges, long descriptions, secondary actions, mismatched icon treatments. Add a region only when the brief needs it.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Icons (ReUI delta over shadcn)
|
||||
|
||||
Use the project's configured `iconLibrary`, put `data-icon="inline-start"` on an icon before the label inside `Button` (`data-icon="inline-end"` on one after it), put no sizing classes on icons inside components, and pass icons as components, not string keys. ReUI adds the following.
|
||||
|
||||
## Portable icons (library-agnostic)
|
||||
|
||||
ReUI components, examples, and blocks are authored to be icon-library-agnostic. The shadcn CLI installs each item's icons in the library named by `iconLibrary` in `components.json` - you swap nothing. When `iconLibrary` is not set, the CLI falls back to `lucide`, so the icons arrive as `lucide-react` imports. If that is not the project's library, set `iconLibrary` before installing, or change the **import source and component name** to your library, keeping the same icon-name semantics (each key below is an `iconLibrary` value):
|
||||
|
||||
- `lucide` -> `lucide-react`
|
||||
- `tabler` -> `@tabler/icons-react`
|
||||
- `phosphor` -> `@phosphor-icons/react`
|
||||
- `remixicon` -> `@remixicon/react`
|
||||
- `hugeicons` -> icons from `@hugeicons/core-free-icons`, rendered through `HugeiconsIcon` from `@hugeicons/react`
|
||||
|
||||
Don't assume `lucide-react`: read `iconLibrary` from `components.json`, and when it is unset, check which icon package the project already uses.
|
||||
|
||||
## 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). Every icon installs from the default path; some also have a real hover animation (motion/react) on the animated path:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/icons/default/<style>/<name> --yes # static, every icon
|
||||
npx shadcn@latest add @reui/icons/animated/<style>/<name> --yes # hover-animated, only when the icon has installAnimated
|
||||
```
|
||||
|
||||
The animated path answers for every icon, but for an icon without a real animation it installs the same static drawing, so only use it when the icon carries `installAnimated`.
|
||||
|
||||
Motion Icons need an Ultimate license for **both discovery and install** (`REUI_LICENSE_KEY`, see [cli.md](./cli.md)). Without it `search_icons` comes back `locked` with no results and `search` returns no icons at all, so check the plan before you promise the user icons: on a free or Pro account, use the project's own `iconLibrary` instead. On Ultimate, a Motion Icon on the primary action is the 1 hover flourish (see [motion.md](./motion.md)).
|
||||
|
||||
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 real hover animation; a concept with none comes back empty and `animatedNote` says so.
|
||||
- One icon: `search` with `type: "icon"`.
|
||||
- Icon results and `get_icon` carry `animated: true` and `installAnimated` only when the icon really animates - use those install strings, do not construct paths by hand.
|
||||
- Every icon result carries a `previewUrl` (the icon's own page) and a `thumbnail` (its 4 styles side by side) - **show or share them** so the user can SEE the icon before installing, and look at the thumbnail yourself before trusting a name.
|
||||
- Several icons can fit one concept. Before installing, read `get_icon`'s `whenNotToUse` (each entry names the icon to use instead), and use its `label` as the `aria-label` of an icon-only button.
|
||||
- Icon names were revised in the icons v2 update. An old name (`save-2`, `abstract-18`) still answers in `get_icon`, `get_install_command`, `validate_usage` and `get_thumbnail` with the icon it became and `renamedFrom`, and still installs that same drawing; write the current name in new code. That includes names renamed because they misdescribed the drawing (`watch` installs a sand timer, `chevrons-down` points up), so look at the `thumbnail` before trusting an old name. `search_icons` answers an old name the same way, unless it is also an ordinary word icons are named for today (`watch`): then it ranks today's icons for the word, and `oldNameNotes` names the icon the old name installs. `search` ranks by current names only, so look an old name up with `get_icon`, never `search`. Only `tag` (it drew a money bag) no longer answers: the reply names the icons to use instead.
|
||||
|
||||
The `icon-stack` component composes multiple icons into a stacked display.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Layout: house defaults, hierarchy, spacing, surfaces and states
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
## House defaults
|
||||
|
||||
<!-- gen:house-defaults:start -->
|
||||
|
||||
- **Spacing ladder**: `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections, `gap-0.5` title to description. Deviate when: skip a level if you like, never invert them.
|
||||
- **Panel title**: no size class on `FrameTitle` or `CardTitle`. Deviate when: never; `FrameTitle` ships `text-sm font-semibold`, `CardTitle` follows the installed style.
|
||||
- **Meta text**: `text-xs`. Deviate when: never smaller, except inside illustrative mini-UI artwork.
|
||||
- **Frame spacing**: `spacing="default"`, written out. Deviate when: `sm` in a sheet, dialog, drawer or popover, or for a dense data grid in a panel; `xs` or `lg` only with a stated reason.
|
||||
- **Leading row icon**: `size-4`. Deviate when: never in a glyph column; `Spinner` ships `size-4`, so a loading swap keeps the width.
|
||||
- **Icon inside a control**: no size class; the control sizes it per style and size. Deviate when: never pin `size-*` on an svg inside `Button`, `InputGroupButton`, `InputGroupAddon`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioItem`, `DropdownMenuSubTrigger`, `SelectItem`, `CommandItem`, `Toggle`, `ToggleGroupItem`, `IconTile` or `MarkerIcon`.
|
||||
- **Badge size**: `default` (leave `size` off). Deviate when: `lg` or `xl` only when the user asks; never `sm` or `xs`.
|
||||
- **Neutral badge**: `variant="outline"`. Deviate when: never `secondary`, `default` or `primary-light` for an id, tag, version, count or Draft/Beta label; inline after the title, never `ml-auto`.
|
||||
- **State badge**: `<tone>-light`: `success-light`, `info-light`, `warning-light`, `destructive-light`; `primary-light` for brand or pending. Deviate when: never the solid tone, `secondary` or `outline` for a state; 1 state badge per row.
|
||||
- **Solid badge**: never unless asked; always name a variant (an omitted one is the solid primary fill). Deviate when: only when the user asks for a solid fill (`default`, `secondary`, `success`, `info`, `warning`, `destructive`, `focus`, `invert`).
|
||||
- **Slot padding**: the primitive's own inset; set density with `Frame spacing=` or `Card size="sm"`. Deviate when: never a padding class on `CardHeader`, `CardContent`, `CardFooter`, `FrameHeader`, `FramePanel`, `FrameFooter`, `SheetHeader`, `SheetContent`, `SheetFooter`, `DialogHeader`, `DialogContent`, `DialogFooter`, `PopoverContent`, `DropdownMenuContent`, `TabsContent` or `AccordionContent`.
|
||||
- **Inline separator**: `<span aria-hidden className="bg-muted-foreground/40 size-1 shrink-0 rounded-full" />`. Deviate when: never a typed `·` between rendered segments; a literal dot only inside 1 data string.
|
||||
- **List rows**: dense, auto height; a row with no second line stays 1 tight line. Deviate when: never uniform height.
|
||||
- **Current list item**: ghost hover fill (`bg-muted`, `dark:bg-muted/50`) plus `aria-current`. Deviate when: never a dot or a check mark.
|
||||
|
||||
<!-- gen:house-defaults:end -->
|
||||
|
||||
## Hierarchy
|
||||
|
||||
- 1 focal point per card or panel: the dominant metric or task, then its label, then detail. Borders, separators and surfaces make 2 to 3 bands, never equal weight everywhere.
|
||||
- Overloaded? Remove, in order: decorative summaries, duplicate context, extra badges, long descriptions, secondary actions, mismatched icon treatments.
|
||||
|
||||
## Spacing and copy
|
||||
|
||||
- Siblings take the parent's `gap-*`, never their own margins; a heading sits nearer its own content than the section above it. Never pad an operational surface like a marketing page.
|
||||
- Titles 1 to 3 words, a description 1 clause (about 8 words), a context line 2 segments at most, labels and buttons 1 to 2 words, a toast title 4 words at most. Copy that wraps at desktop width is over budget: cut it, never shrink the type.
|
||||
|
||||
## Surfaces
|
||||
|
||||
- `Frame` for data and operational surfaces (tables, lists, tool panels); `Card` for bounded sections and marketing.
|
||||
- Prefer 1 container family per screen. With a KPI row and a table, use FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame or a Frame inside a Card.
|
||||
- A `data-grid` inside a Frame is the one place for `<FramePanel className="p-0">`; elsewhere no padding on slots. This is the 1 exception to the Slot padding row above.
|
||||
- 1 archetype per screen (operational ones and the marketing spine): [compose.md](./compose.md) lists each with its regions.
|
||||
|
||||
## Type and buttons
|
||||
|
||||
- A KPI value is `text-2xl font-semibold tracking-tight tabular-nums`.
|
||||
- `size="sm"` on dense surfaces, `variant="outline"` for secondary actions, `ghost` for row actions; 1 primary per region, 1 variant per toolbar.
|
||||
- A toolbar separator is `<Separator orientation="vertical" className="my-auto h-4" />`; at a 390px viewport a toolbar stays on 1 row.
|
||||
|
||||
## States and responsive
|
||||
|
||||
- Empty: a short message plus the primary action. Loading: a `Skeleton` in the real layout, never a centered spinner. Error: inline with a retry.
|
||||
- No horizontal page scroll at 390px: reflow to 1 column instead of shrinking; `min-w-0` on the shrinking parent and `truncate` on single-line labels.
|
||||
@@ -1,80 +0,0 @@
|
||||
# Motion
|
||||
|
||||
## Installed motion: never retime it
|
||||
|
||||
<!-- gen:installed-motion:start -->
|
||||
|
||||
| Installed part | Duration |
|
||||
| ------------------ | ----------------- |
|
||||
| Overlays | 100ms |
|
||||
| Tooltip | 150ms (default) |
|
||||
| Sheet, sidebar | 200ms |
|
||||
| Navigation menu | 300ms, exit 150ms |
|
||||
| rhea control focus | 200ms |
|
||||
|
||||
<!-- gen:installed-motion:end -->
|
||||
|
||||
Never add or change `duration-*`, `ease-*`, `animate-*` or `transition-*` on installed parts (`components/ui`, `components/reui`); Button already presses with `active:not-aria-[haspopup]:translate-y-px`.
|
||||
|
||||
## House motion: your own code
|
||||
|
||||
<!-- gen:house-motion:start -->
|
||||
|
||||
| Use | Tailwind |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| Hover, press, color | `transition-colors` (150ms, no duration or ease class) |
|
||||
| Reveal or expand | `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]` |
|
||||
| Large surface (ceiling) | `duration-300` with the same curve |
|
||||
| An overlay you build | `duration-100` with the same curve |
|
||||
| Media hover zoom | `duration-500` with the same curve, `scale-105` at most |
|
||||
| Width tied to the sidebar | `duration-200 ease-linear` |
|
||||
|
||||
<!-- gen:house-motion:end -->
|
||||
|
||||
- Press on a custom surface (not Button): `transition-transform active:scale-[0.98] motion-reduce:transition-none`.
|
||||
- Hover color plus press on 1 element: `transition-[color,background-color,border-color,scale]` (`transition-colors` and `transition-transform` override each other; `scale-*` sets `scale`).
|
||||
- motion/react takes the same values in seconds: `{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }`; hover `{ duration: 0.15 }`.
|
||||
- Drag, reorder and layout use `const SPRING = { type: "spring", visualDuration: 0.25, bounce: 0 }`. Define `SPRING` only when something drags, reorders or toggles layout.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Animate `transform` and `opacity`; color only for hover and press. Never `transition-all`; width, height, top or left only in the sidebar row.
|
||||
- Never `animate-bounce` or a spring with bounce above 0. `ease-linear` only for progress, spinners, loops and the sidebar; never Tailwind's `ease-in`, `ease-out` or `ease-in-out`.
|
||||
- `motion/react` only: never framer-motion, GSAP or react-spring.
|
||||
- At most 1 entrance animation per view (a stagger counts as 1), and every animation has a reduced-motion path. Feedback that appears after an action or a load (an Alert, a field error, a saved notice, data replacing its `Skeleton`) is a reveal, not an entrance: fade it in with the reveal row.
|
||||
- Never add named `duration-*` utilities or redefine `--ease-out`.
|
||||
- Loading: a `Skeleton` (it already pulses) in the real layout; a spinner only inside a busy button.
|
||||
- Motion Icons (Ultimate only): 1 on the primary action is the 1 hover flourish; install it with its `installAnimated` command; never retime it.
|
||||
|
||||
## Performance
|
||||
|
||||
Each line names its lint rule, or judgement (no check).
|
||||
|
||||
- Never animate blur, drop-shadow, backdrop-filter, box-shadow, border-radius, background-position, background-size or a custom property that feeds transform; clip-path and stroke-dashoffset only as a one-shot reveal of 300ms or less, or a loop on an element of 240x240px or less. Name the properties: never a bare `transition` (it also animates box-shadow and filter). (`reui/no-paint-animation`; 240px: judgement (no check))
|
||||
- A hover lift is `transform` over a static shadow, or a shadow layer whose `opacity` fades; never a transitioned `hover:shadow-*`. (`reui/no-paint-animation`)
|
||||
- CSS first: hover, press, reveals and ambient loops are Tailwind transitions, `tw-animate-css` or `@keyframes`. motion/react only for exit on unmount, drag, springs that follow input, `layout` and scroll-linked values. (judgement (no check))
|
||||
- In motion/react, `x`, `y`, `scale`, `rotate`, `pathLength` and every SVG target run on the main thread: never `repeat: Infinity` on them (loop with CSS `@keyframes`), and keep 20 or fewer JS-driven animations running per view. An opacity loop leaves the main thread only on an HTML element without `repeatDelay`, `repeatType: "mirror"` or `onUpdate`. (`reui/no-main-thread-loop`)
|
||||
- SVG geometry (a rect's `width`, a circle's `r`) does not reflow the page but runs in motion's JS loop: one-shot only. (`reui/no-main-thread-loop`)
|
||||
- Never set React state per frame (requestAnimationFrame, `useAnimationFrame`, `onUpdate`, scroll or pointermove listeners): bind styles to `useMotionValue`, `useTransform` or a CSS variable on the element. (`reui/no-frame-setstate`)
|
||||
- A JS-driven loop (motion `repeat`, requestAnimationFrame, canvas) pauses offscreen with `useInView` or IntersectionObserver; `content-visibility: auto` never stops script, and a setInterval loop also pauses on `visibilitychange`. (`reui/loop-offscreen`)
|
||||
- No permanent `will-change`: no `will-change-*` class or `willChange` style, except during a drag, removed when it ends. (`reui/will-change`)
|
||||
- Stagger 50ms or less per step (`staggerChildren: 0.05`), only the first 6 items, the whole entrance done in 500ms, first mount only, never on table rows or lists of 20 or more; scroll reveals use `whileInView` with `viewport={{ once: true }}`. (`reui/stagger-budget`; item count and tail: judgement (no check))
|
||||
- A height reveal (`grid-template-rows`) starts only on a click or key press, runs 200ms or less, 1 at a time; never `initial={{ height: 0 }}`. (`reui/no-layout-transition`)
|
||||
- The motion `layout` prop on 1 list at a time (about 20 elements), never in data-grid or virtualized rows; drag and drop goes through `@reui/sortable` or `@reui/kanban`. (judgement (no check))
|
||||
- AnimatePresence `mode="wait"` only with an exit of 150ms or less; `initial={false}` around content present at first render. (`reui/stagger-budget`)
|
||||
- Never hide the hero or the largest heading behind a motion/react `initial` at opacity 0 (hidden until JavaScript runs). (judgement (no check))
|
||||
- At most 3 ambient loops per view: CSS `@keyframes` on transform or opacity, a period of 2s or more, exempt from the duration table; once 3 run in a view they pause offscreen (a `content-visibility: auto` wrapper with `contain-intrinsic-size: auto <h>` where nothing paints outside it, or IntersectionObserver toggling `animation-play-state`). At most 50 animating elements per view (Skeletons, loaders and CSS opacity loops not counted). A static blur above 12px, at most 1 per view, only on a surface that neither moves nor sits over motion. (judgement (no check))
|
||||
|
||||
## Reduced motion
|
||||
|
||||
- Reduced motion is a substitute, not an off switch: replace movement (translate, scale, rotate, slide, zoom, layout, parallax, auto-scroll) with an opacity fade of 150ms or less or an instant change. Keep color and opacity feedback, focus, progress bars, loaders, the Skeleton pulse, chevrons and the switch thumb. (`reui/reduced-motion`)
|
||||
- The reduced state is the resting state: content never waits at opacity 0, hidden or offset for an animation that will not play; a count-up shows its final value. (`reui/reduced-motion`)
|
||||
- In every mode an entrance moves 8px or less and starts at scale 0.95 or more; nothing moves a surface larger than half the viewport. (`reui/reduced-motion`; half the viewport: judgement (no check))
|
||||
- `<MotionConfig reducedMotion="user">` once at the app root (its default ignores the OS) AND a `motion-reduce:` path on every CSS animation (MotionConfig never reaches CSS). An example or block wraps its own motion subtree. (`reui/reduced-motion`)
|
||||
- MotionConfig makes only transform keys and width, height, top and left instant; opacity, filter, clipPath, pathLength, color and a `transform` string keep animating, so a loop or those keys branch on `useReducedMotion()` and rest on the final value. (`reui/reduced-motion`)
|
||||
- `useReducedMotion()` is null on the server: never let it pick markup (JSX, className, `initial`) or reach `animate` under `initial={false}`; branch only animate, transition, `while*` or exit values. (`reui/reduced-motion-ssr`)
|
||||
- No SMIL (`<animate>`, `<animateMotion>`, `<animateTransform>`); canvas and requestAnimationFrame loops draw 1 resting frame under `prefers-reduced-motion: reduce`. (`reui/no-smil`, `reui/reduced-motion`)
|
||||
- Smooth scroll only under `motion-safe:scroll-smooth`. A focus ring appears at once: never `transition-all`, `transition-shadow` or a bare `transition` on it. (`reui/reduced-motion`, `reui/focus-transition`)
|
||||
- Loaders and typing dots keep their opacity pulse; every decorative loop gets `motion-reduce:animate-none`, never an inline `style={{ animation }}` (it beats the class). (`reui/reduced-motion`)
|
||||
- Anything that moves on its own for more than 5 seconds (autoplay, marquee, ticker) gets a visible Pause control, pauses on hover and focus, and never autoplays under reduced motion. (`reui/pausable-autoplay`)
|
||||
- Installed overlays keep their 100ms fade, zoom and 8px slide under reduced motion: disclosed, not edited; an app that needs WCAG 2.3.3 adds its own override. (judgement (no check))
|
||||
@@ -1,57 +0,0 @@
|
||||
# Quality gates (security, accessibility, scroll, self-checks)
|
||||
|
||||
This is the **done gate**: before you call any ReUI work finished, call `get_audit_checklist` and pass every item below (plus the bar and the states in [layout.md](./layout.md)). Then run the self-checks, typecheck and lint, compare with [anti-patterns.md](./anti-patterns.md), and hand back in the 5 lines below.
|
||||
|
||||
## 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. Hover on a display-only card: make the card a link to its detail, or give it no hover.
|
||||
- `aria-current="page"` on the current route link, `"location"` on the current in-page section link.
|
||||
- Destructive confirms use `AlertDialog`, not `Dialog`, and cannot be dismissed while pending.
|
||||
- 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 their keyboard behavior; never re-implement it.
|
||||
- Announce async UI: loading and error messages use `role="status"` or `aria-live`.
|
||||
|
||||
## 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.
|
||||
|
||||
## Self-checks
|
||||
|
||||
After every UI edit, run these with ripgrep on `<files>`: each file you wrote or changed, by path, never `components/ui` or `components/reui`. A clean check prints nothing; a hit prints `file:line:code`. The last filter drops comment lines.
|
||||
|
||||
```sh
|
||||
# C1 banned motion: transition-all, bare or paint transitions, layout keys, bounce, overshoot, scale 0, hover and tap gestures, will-change
|
||||
rg -nH 'transition-all|transition(-property)?: *[^;]*\b(all|width|height|top|left|margin|padding|box-shadow|filter)\b|[\x27\x60" ]transition( +(duration|ease|delay|hover|focus|active|group|motion)|[\x27\x60"])|transition-shadow|transition-\[[^\]]*\b(width|height|top|left|right|bottom|inset|margin|padding|filter|radius|background-position|background-size|all)\b|animate-bounce|will-change|willChange|bounce: *(0?\.0*[1-9]|[1-9])|[\x27"](backIn|backOut|backInOut|anticipate)[\x27"]|(cubic-bezier\(|ease: *\[)([^,]*,|([^,]*,){3})[ _]*(-|1\.[0-9]*[1-9]|[2-9])|scale: *0 *[,}]|\bscale-0\b|while(Hover|Tap|Focus)=|(initial|animate|exit)=\{\{[^}]*\b(width|height|maxHeight|top|left|margin|padding|boxShadow|filter|backdropFilter|borderRadius)[A-Za-z]*:' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C2 off-scale motion: a duration off 0, 100, 150, 200 and 300 (duration-500 only on a media hover zoom), ease-in or ease-out, a stagger step above 0.05
|
||||
rg -nH 'duration-([1-9]|[1-9][0-9]|1(0[1-9]|[1-46-9][0-9]|5[1-9])|[23](0[1-9]|[1-9][0-9])|[4-9][0-9]{2}|[1-9][0-9]{3,})\b|duration-\[|\b([1-9]|[1-9][0-9]|1(0[1-9]|[1-46-9][0-9]|5[1-9])|[23](0[1-9]|[1-9][0-9])|[4-9][0-9]{2}|[1-9][0-9]{3,})ms\b|(^|[^0-9.])(0?\.(0[0-9]*|1[0-46-9][0-9]*|15[0-9]+|[23][0-9]+|[4-9][0-9]*)|1\.[0-9]+)s\b|duration: *([1-9]\b|0?\.(0[0-9]*|1[0-46-9][0-9]*|15[0-9]+|[23][0-9]+|[4-9][0-9]*)\b)|\bease-(in|out|in-out)\b|(staggerChildren: *|stagger\( *)(0?\.0[6-9]|0?\.[1-9]|[1-9])|delay: *[^,}]*\b(i|index|idx)\b *\* *(0?\.0[6-9]|0?\.[1-9]|[1-9])|delay: *(0?\.0[6-9]|0?\.[1-9]|[1-9])[0-9.]* *\* *\b(i|index|idx)\b' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C2 read by hand: a duration or delay that is not a number, every index delay (allowed: 0.05 or less on the first 6 items, as Math.min(i, 5) * 0.05, never on table rows)
|
||||
rg -nH '([dD]uration|[dD]elay): *[^0-9 .]|delay: *[0-9.]+ *\*' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C3 state color: raw palette on a line that names a state
|
||||
rg -nH '\b(text|bg|border|ring|fill|stroke)-(red|rose|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet)-[0-9]{2,3}\b' <files> | rg -i ':[0-9]+:.*\b(success|paid|active|completed?|done|approved|online|healthy|live|error|failed|critical|overdue|declined|rejected|cancell?ed|blocked|expired|warning|pending|degraded|due|info|processing|running|scheduled|queued)\b' | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C4 structure: a raw table, select or fixed overlay, framer-motion, Math.random() or Date.now()
|
||||
rg -nH '<table\b|<select\b|fixed inset-0|from ["\x27]framer-motion|Math\.random\(|Date\.now\(' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C4 on a base-* style only (skip on radix-*): Radix asChild
|
||||
rg -nH '\basChild\b' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
```
|
||||
|
||||
Fix every hit, run again, stop after the second run and list what is left with file and line. A C4 hit stays only when a comment in the file says why. These also stay, named on hand-back line 2: C1 on the motion.md sidebar row or on will-change set in a drag handler and removed when it ends; C2 on a media hover zoom; `Math.min(i, 5) * 0.05`.
|
||||
|
||||
No `rg` (a missing one also prints nothing): run each first pattern with your own search tool, skipping comment lines. No shell: read each changed file for what each `#` line names.
|
||||
|
||||
## Hand-back
|
||||
|
||||
End your reply with these 5 lines:
|
||||
|
||||
1. Installed: each item with its `previewUrl`.
|
||||
2. Motion: each animated element and its [motion.md](./motion.md) table row, or "none".
|
||||
3. States: covered, or which are missing.
|
||||
4. Checks: C1 to C4, screenshots or "not visually verified", the audit checklist, typecheck, lint; each one run, or "not run" and why.
|
||||
5. Deviations: any rule broken or check hit left, with file:line and why.
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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 ReUI components 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 (a full dashboard or settings page). Pro or Ultimate license at install. Its surfaces, text and borders use your theme tokens; its category accents (chart series, tags, avatars) are fixed palette colors paired for light and dark.
|
||||
- **icon** - Motion Icons in 4 styles (outline, solid, duotone, filled), static (`@reui/icons/default/<style>/<name>`), plus a hover-animated variant (`@reui/icons/animated/<style>/<name>`) only for icons that carry `installAnimated`. Ultimate license at install. See [icons.md](./icons.md).
|
||||
|
||||
## The @reui registry
|
||||
|
||||
ReUI items install through the shadcn CLI as `npx shadcn@latest add @reui/<name> --yes`, with `<name>` exactly as a ReUI tool returned it (`search`, a getter, `get_install_command`). 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.
|
||||
|
||||
## ReUI items vs shadcn/ui components
|
||||
|
||||
- **ReUI items** (the components, `c-*` examples, blocks, Motion Icons) install as `@reui/<name>`.
|
||||
- **shadcn/ui components** that ReUI builds on (`button`, `dialog`, `select`, `card`, ...) are not ReUI items. They install from shadcn by bare name: `npx shadcn@latest add button --yes`.
|
||||
- **`badge` and `alert` exist in both registries.** `@reui/badge` and `@reui/alert` are ReUI's components (the extra variants in [components.md](./components.md)); bare `badge` and `alert` are shadcn's.
|
||||
- **In an item's `registryDependencies`**, a `@reui/<name>` entry is a ReUI item and a bare name is a shadcn/ui item. `add` installs both for you.
|
||||
- **Templates are not registry items.** Download one from its page on reui.io (https://reui.io/templates?ref=skill); no `shadcn add` command installs a template.
|
||||
|
||||
## 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 take surfaces, text and borders from your theme tokens, so a theme change re-tints them; category accents stay fixed by design.
|
||||
|
||||
## Free vs premium
|
||||
|
||||
- **Free, no license key:** the ReUI components, all `c-*` examples, and this skill. The MCP is free too but still needs a free ReUI account (the agent signs in on first use); a license key is only for premium installs.
|
||||
- **Premium, license required at install:** blocks (Pro or Ultimate) and Motion Icons (Ultimate). Set `REUI_LICENSE_KEY` (see [cli.md](./cli.md)).
|
||||
- **Templates** are downloads, not installs: free ones need only a ReUI account, and each template's page on reui.io says what the others need.
|
||||
|
||||
## 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.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Theming: styles, radius, density and sizes
|
||||
|
||||
## Read your style
|
||||
|
||||
`components.json` `style` is `<base>-<style>`: the part after the first `-` names the style (`base-nova` is nova on Base UI). The style is baked into `components/ui` and `components/reui` at install, so the installed files hold your real values: read your installed `button.tsx` (usually `components/ui/button.tsx`) for heights, radius and padding before you build anything that sits beside a control.
|
||||
|
||||
## The style ladder
|
||||
|
||||
<!-- gen:style-ladder:start -->
|
||||
|
||||
| Style | Button `sm` / default / `lg` | Button radius | Card radius, shadow | Card spacing | Button text |
|
||||
| ----- | ---------------------------- | -------------- | ---------------------------------------------------- | ------------ | -------------------- |
|
||||
| vega | `h-8` / `h-9` / `h-10` | `rounded-md` | `rounded-xl`, `shadow-xs` | 6, sm 4 | `text-sm` |
|
||||
| nova | `h-7` / `h-8` / `h-9` | `rounded-lg` | `rounded-xl` | 4, sm 3 | `text-sm` |
|
||||
| maia | `h-8` / `h-9` / `h-10` | `rounded-4xl` | `rounded-2xl` | 6, sm 4 | `text-sm` |
|
||||
| lyra | `h-7` / `h-8` / `h-9` | `rounded-none` | `rounded-none` | 4, sm 3 | `text-xs` |
|
||||
| mira | `h-6` / `h-7` / `h-8` | `rounded-md` | `rounded-lg` | 4, sm 3 | `text-xs/relaxed` |
|
||||
| luma | `h-8` / `h-9` / `h-10` | `rounded-4xl` | `rounded-4xl`, `shadow-md` | 6, sm 4 | `text-sm` |
|
||||
| sera | `h-9` / `h-10` / `h-11` | `rounded-none` | square, `shadow-sm` | 8, sm 5 | `text-xs`, uppercase |
|
||||
| rhea | `h-7` / `h-8` / `h-9` | `rounded-2xl` | `rounded-[min(var(--radius-4xl),24px)]`, `shadow-sm` | 5, sm 4 | `text-sm` |
|
||||
|
||||
<!-- gen:style-ladder:end -->
|
||||
|
||||
Card spacing is the `--card-spacing` step (`--spacing(n)`) a Card pads with; `Card size="sm"` uses the sm step. The default Button text weight is `font-medium` (sera: `font-semibold`).
|
||||
|
||||
## What the style owns
|
||||
|
||||
- Never set what the style owns on its parts: radius (except `rounded-full`), height, horizontal padding, shadow, ring or font size. Pick `size` and `variant` instead.
|
||||
- A panel is a `Card` or a `Frame`, never a div with `rounded-*`, `border` and `bg-card`; set density with `Card size="sm"` or Frame `spacing`.
|
||||
- A bespoke element beside a control (a chip, a custom trigger) takes its height and radius from that control's row above.
|
||||
- A focusable control you build copies the focus classes of your installed `input.tsx`.
|
||||
- A variant before a class: `className` is for layout (placement, width, gap). `size-*` when width equals height, `cn()` for conditional classes (never a template literal), a scale step before an arbitrary value (`w-60`, not `w-[240px]`), no inline `style` for color or spacing, and no `z-index` on dialogs, sheets or popovers.
|
||||
- Change the look through the theme CSS variables in `:root` and `.dark` (`--radius`, `--primary` and the rest) or with a style switch (a new `style` in `components.json`, which means reinstalling the components; ask the user first). Never edit installed files to restyle them.
|
||||
|
||||
## Components with no per-style values
|
||||
|
||||
`event-calendar`, `gantt`, `icon-stack`, `kanban`, `rating`, `scrollspy`, `sortable` and `timeline` install the same file in every style. Their look comes from your theme tokens and the parts you put inside them (a `Card` in a kanban column follows the style), so never copy another style's radius onto them.
|
||||
@@ -1,66 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
- A full page or section (dashboard, billing, auth, pricing, settings): `compose_page` first (step 1).
|
||||
- A data table with sorting, filtering, pagination or virtualization: the `data-grid` component, never a hand-rolled `<table>`.
|
||||
- A drag-and-drop board: the `kanban` component.
|
||||
- Column filters, a date range, a tree, a stepper and the like: the matching ReUI component.
|
||||
- A single generic control shadcn already has (Button, Dialog, Select): plain shadcn.
|
||||
|
||||
## 1. Find (ReUI MCP `search` / `compose_page`)
|
||||
|
||||
**Full multi-section page ask?** Call `compose_page(intent)` FIRST (`sections`, 1 to 10, caps how many sections, default 8), before searching block-by-block. It returns ordered sections, each with the best block for the intent (top pick + alternates); on a free account it answers `mode: "free"` and fills the same sections with free `c-*` examples instead, so the plan is always buildable. 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"` (a ReUI component), `"example"` (a c-\* use-case), `"block"` (a full page/section), `"icon"`.
|
||||
- `component`: the ReUI component the request implies (`"data-grid"`, `"kanban"`, ...).
|
||||
- `category`, `free`.
|
||||
|
||||
Example: "build a users management page with filters" -> `search({ query: "users management page with filters", type: "block", component: "data-grid" })` (on a free account use `type: "example"`).
|
||||
|
||||
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.
|
||||
|
||||
**Pick with the guidance, not the title.** Look-alikes are common (dialog, sheet and drawer; dozens of data-grid examples and app shells). Before installing, read the item's guidance: `get_component` returns `whenToUse` and `whenNotToUse`, `get_example` and `get_block` return `bestFor` (what sets the item apart from its siblings), `agentNotes` and `whenNotToUse`, and a block adds `categoryGuidance` (its category's key note), and `get_examples` rows carry `bestFor`. Each `whenNotToUse` entry names the item to use instead and its `type` (`component` -> `get_component`, `example` -> `get_example`, `block` -> `get_block`, `block-category` "<group>/<category>" -> `search({ query, type: "block", category: "<category>" })`); when the user's situation matches its `when`, switch. Apply every `agentNotes` line before you ship: it names the real identifiers to replace or wire (sample data, stubbed actions, required props). An item with no guidance yet has none of these keys; fall back to its description and thumbnail.
|
||||
|
||||
Results are scoped to the account's plan, so you are never shown an item the user could not install: blocks need Pro, Motion Icons need Ultimate. A `type` hint the plan cannot use (or a query whose only matches are premium) comes back as a normal HTTP 200 answer with `locked: true` and `requiredPlan` rather than results - that is a paywall, not a missing item, so switch to free components and `c-*` examples and mention https://reui.io/pricing?ref=skill.
|
||||
|
||||
**Show the preview.** List every item with its `previewUrl` (a live preview page). Where a result carries `thumbnail` (blocks, `c-*` examples and icons), show the image as markdown linked to `previewUrl`, so the user sees it before you install.
|
||||
|
||||
## 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 deeper API, call **`get_component(names)`** with ALL of `componentsUsed` in ONE call (it accepts an array) and read each inline `api` - no web fetch. A very large API is trimmed on heading boundaries to fit your context and lists what it dropped in `sectionsOmitted`; when the part you need is in that list, re-read it as the response's `next` hint says rather than guessing. `validate_usage` always checks the FULL API, so it still catches a prop you invented from a trimmed capsule. **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. Write against your project's base (see [components.md](./components.md)). After `add`:
|
||||
|
||||
1. **Read the added files**; keep the composition intact. For a block, verify the components are wired correctly (for `data-grid`: a `useTable({ features: dataGridFeatures, ... })` instance passed as `table`, `recordCount` set - see [components.md](./components.md)).
|
||||
2. **Replace demo data with the user's real data** via typed structures (see [adapting.md](./adapting.md)).
|
||||
3. **Fix icon imports** to the project's icon library (see [icons.md](./icons.md)).
|
||||
4. **Align styling** to the active theme: semantic tokens for anything that carries meaning, raw palette only for categorical identity and always with its `dark:` pair (see [color.md](./color.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, motion that leaves installed components alone, and mobile-first responsiveness (see [layout.md](./layout.md) and [motion.md](./motion.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.
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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; Pro unlocks premium blocks, Ultimate adds Motion Icons, and either removes the daily request limit by default. 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 (`sections` is a number, 1 to 10: the most sections to return, default 8); it returns ordered sections, each with the best premium block for the intent (top pick + alternates). On a free account (or in free-only mode) it answers with `mode: "free"` and composes the same sections from free `c-*` examples instead, so the plan is always buildable. 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. Only some icons have a real hover animation: `animated: true` returns only those, and a concept with none comes back empty with an `animatedNote`. Each icon carries a `thumbnail` of its 4 styles; look at it before trusting a name. An old icon name still resolves (`renamedFrom` names it), unless it is also an ordinary word icons are named for today (`watch`): then it ranks today's icons for the word and `oldNameNotes` names the icon the old name installs. Motion Icons are Ultimate for **discovery as well as install**: without an Ultimate credential this returns `locked: true` and zero concepts, so check the plan before promising the user icons.
|
||||
- **`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.
|
||||
- **`get_thumbnail`** - when the choice is VISUAL and the ranking has not already made it: 2 or more candidates within about 15 score points. When the top hit leads by a wide margin, or `missedTerms` already rules the others out, skip it; an image costs roughly 10 times a result row. It returns the hosted preview image as image content so you can look yourself; pass up to 4 names in one call to compare `compose_page` alternates side by side before installing any of them. To let the USER see something, share the item's `thumbnail` and `previewUrl` instead - this tool is for your own eyes, and a client that cannot render images gets the URLs in the text block.
|
||||
- **`get_agent_skill`** - when the agent has no local ReUI skill (a cloud or tools-only client), or to check whether the installed one is stale. With no `topic` it returns the core workflow, the topic index and the installer; `topic` (for example `motion`) returns that one file in full. Compare its `version` with the one stamped atop your local `SKILL.md`.
|
||||
|
||||
## All 19 tools
|
||||
|
||||
`search`, `get_block`, `get_example`, `get_icon`, `get_thumbnail`, `list_block_groups`, `list_block_categories`, `list_example_categories`, `list_icon_categories`, `list_components`, `get_component`, `get_examples`, `search_icons`, `compose_page`, `validate_usage`, `get_install_command`, `get_project_context`, `get_agent_skill`, `get_audit_checklist`. The MCP serves the full parameter schemas; do not guess parameters beyond them.
|
||||
|
||||
## Commands (MCP prompts)
|
||||
|
||||
In Claude Code the ReUI MCP adds these prompts; other agents show MCP prompts under their own names, or not at all. Plain language runs the same loop.
|
||||
|
||||
- `/mcp__reui__build <what>`: compose a page, section or feature (plan, install, read the API, adapt, audit).
|
||||
- `/mcp__reui__add <item>`: find, install and wire in 1 component, example, block or icon.
|
||||
- `/mcp__reui__fix [target]`: fix ReUI usage (undocumented props, a base or radix mismatch, missing states, accessibility, scroll).
|
||||
- `/mcp__reui__improve [target]`: refine existing ReUI UI (hierarchy, density, states, responsive, motion).
|
||||
|
||||
## 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`, `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.
|
||||
- An inline `api` is not always the whole API: a very large one is trimmed on heading boundaries to fit your context, and the response names every dropped heading in `sectionsOmitted` plus a `next` hint for re-reading one. Never treat a trimmed capsule as complete; `validate_usage` still checks the FULL API.
|
||||
- Icon results and `get_icon` include `animated: true` and `installAnimated` only when the icon has a real hover animation (animated: `@reui/icons/animated/<style>/<name>`; static: `@reui/icons/default/<style>/<name>`). Use the strings given; never build an animated path by hand, it installs a static copy for every other icon.
|
||||
- Blocks, `c-*` examples and icons carry `thumbnail`, an absolute URL to a hosted preview image (light theme; blocks 1080x720, examples 900x600, icons 640x200 with the 4 styles side by side). Show the top 2 or 3 as markdown images linked to `previewUrl` so the user picks by eye; use `get_thumbnail` when YOU need to see them. Components have no thumbnail.
|
||||
- Each result carries `missedTerms`, the words from YOUR query it does not mention (omitted when it matches them all). A high score with 3 missed words is a near miss wearing a good score: read this before `get_thumbnail`, it is far cheaper.
|
||||
- `weakMatch: true` means the top result misses more of the query's words than it matches; `unmatchedTerms` lists the words none of the top results mention and `weakMatchNote` says what to do. Stop and re-search with the registry's own words, or compose from components, instead of installing the top hit.
|
||||
- On a free account `premiumPicks` (a sibling of `results`, never inside it) lists up to 3 premium blocks that fit the query more closely than the free answer, as previews with no install command, plus `unlock` with the plan and a pricing link. Mention the upgrade at most once per conversation, then keep building with what the plan covers. `weakMatch` and `premiumPicks` can appear together: the free answer is weak AND a premium block fits; `weakMatchNote` says so.
|
||||
- Guidance is getter-only, never on search rows: `get_component` adds `whenToUse`, `whenNotToUse` and `agentNotes`; `get_example` and `get_block` add `bestFor`, `agentNotes` and `whenNotToUse`, and `get_block` also `categoryGuidance`; `get_examples` rows add `bestFor`. Answers carry the key lines; the item page's `.md` twin has every line. Each `whenNotToUse` entry is `{ when, instead, type }`: fetch `instead` with the getter its `type` names.
|
||||
- Results are scoped to the caller's plan, so a free account never sees a block or icon it could not install. When a search matches only hidden premium items, the answer says so (`locked`, `requiredPlan`, `premiumMatches`) instead of pretending the registry is empty.
|
||||
|
||||
## Error playbook
|
||||
|
||||
- **401** - the MCP requires a signed-in ReUI account, on every request (there is no anonymous access). 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?ref=skill) as `Authorization: Bearer`.
|
||||
- **locked result** - a valid account whose plan does not cover the item. This is NOT an error and NOT a 403: it comes back as a normal HTTP 200 result carrying `locked: true` and `requiredPlan` (`"pro"` for premium blocks, `"ultimate"` for Motion Icons). Keep working with the free components and `c-*` examples, and point the user at https://reui.io/pricing?ref=skill if they want the locked item.
|
||||
- **daily allowance reached** - a free account has a per-account, per-UTC-day allowance on tool calls (ReUI sets the number and can change it, so read it from the message and never assume one). It comes back as a normal tool RESULT with `isError: true` at HTTP 200: not an HTTP error status, no back-off header to honor, and no per-minute limit to wait out. Retrying the same call just spends the next unit, so surface the message (it carries the reset and the upgrade link) to the user instead of looping. The count resets at UTC midnight, and using a Pro or Ultimate license as the MCP credential removes the limit.
|
||||
- **not found** (`found: false`) - use the returned `suggestions`, or `search`. 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?ref=skill
|
||||
@@ -1,33 +0,0 @@
|
||||
# Database
|
||||
DATABASE_URL=sqlite:data/app.db
|
||||
|
||||
# Auth JWT (access tokens for apps)
|
||||
JWT_SECRET=dev-secret-change-me
|
||||
JWT_TTL_HOURS=1
|
||||
REFRESH_TTL_DAYS=14
|
||||
ISSUER=https://auth.shnt.top
|
||||
# OIDC IdP (defaults to ISSUER). Optional PEM PKCS8 RSA key — otherwise auto-generated in DB.
|
||||
# OIDC_ISSUER=https://auth.shnt.top
|
||||
# OIDC_RSA_PRIVATE_KEY=
|
||||
|
||||
# Bootstrap admin (created on first start if DB empty)
|
||||
ADMIN_EMAIL=[email protected]
|
||||
ADMIN_PASSWORD=admin
|
||||
ADMIN_NAME=Admin
|
||||
|
||||
# Allowed return_to hosts (comma-separated), e.g. .shnt.top or full origins
|
||||
RETURN_TO_ALLOWLIST=.shnt.top,localhost,private,http://localhost:5173,http://localhost:5174,http://localhost:5176
|
||||
|
||||
# ReUI PRO (apps/web/components.json → @reui Authorization)
|
||||
# Ключ: https://reui.io/docs/license-setup — класть в apps/web/.env.local (gitignored)
|
||||
REUI_LICENSE_KEY=
|
||||
|
||||
# Local app URLs for SSO Open (apps/web/.env.local)
|
||||
# VITE_VPS_APP_URL=http://localhost:5173
|
||||
# VITE_CDN_APP_URL=http://localhost:5176
|
||||
# VITE_RETURN_TO_ALLOWLIST=.shnt.top,localhost,http://localhost:5173,http://localhost:5176
|
||||
|
||||
# Server
|
||||
SERVER_PORT=8080
|
||||
STATIC_DIR=
|
||||
LOG_LEVEL=info
|
||||
@@ -0,0 +1,108 @@
|
||||
name: MCP isolated portal acceptance preview
|
||||
'on':
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
expected_sha:
|
||||
description: Exact reviewed full commit SHA on the selected ref; includes all accepted source changes
|
||||
required: true
|
||||
type: string
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
jobs:
|
||||
portal-preview:
|
||||
if: ${{ gitea.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
COMPONENT: portal
|
||||
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||
DISPATCH_SHA: ${{ gitea.sha }}
|
||||
DISPATCH_EVENT: ${{ gitea.event_name }}
|
||||
REPOSITORY: ${{ gitea.repository }}
|
||||
RUN_ID: ${{ gitea.run_id }}
|
||||
RUN_ATTEMPT: ${{ gitea.run_attempt }}
|
||||
steps:
|
||||
- name: Checkout exact dispatch commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ gitea.sha }}
|
||||
persist-credentials: false
|
||||
- name: Use existing Linux Node22 build toolchain
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Verify source and isolated preview destination
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test "$DISPATCH_EVENT" = workflow_dispatch
|
||||
[[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
test "$EXPECTED_SHA" = "$DISPATCH_SHA"
|
||||
test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"
|
||||
git diff --exit-code
|
||||
git diff --cached --exit-code
|
||||
[[ "$RUN_ID" =~ ^[0-9]+$ && "$RUN_ATTEMPT" =~ ^[0-9]+$ ]]
|
||||
owner=$(printf '%s' "$REPOSITORY" | cut -d/ -f1 | tr '[:upper:]' '[:lower:]')
|
||||
name=$(printf '%s' "$REPOSITORY" | cut -d/ -f2 | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$owner" =~ ^[a-z0-9][a-z0-9._-]*$ && "$name" =~ ^[a-z0-9][a-z0-9._-]*$ ]]
|
||||
stem=$(printf '%s' "$name" | sed -E 's/-[0-9]+$//')
|
||||
echo "IMAGE_NAME=git.shx.one/$owner/$stem-$COMPONENT-mcp-acceptance" >> "$GITHUB_ENV"
|
||||
echo "PREVIEW_TAG=mcp-acceptance-$EXPECTED_SHA-$RUN_ID-$RUN_ATTEMPT" >> "$GITHUB_ENV"
|
||||
echo "PREVIEW_CACHE=git.shx.one/$owner/$stem-$COMPONENT-mcp-acceptance-buildcache:node-buildcache" >> "$GITHUB_ENV"
|
||||
mkdir -p .ci/acceptance-evidence
|
||||
node <<'NODE'
|
||||
const fs=require('node:fs'),cp=require('node:child_process'),crypto=require('node:crypto');
|
||||
const files=cp.execFileSync('git',['ls-files','-z'],{encoding:'utf8'}).split('\0').filter(Boolean);
|
||||
const inputs=files.map(path=>{const info=fs.lstatSync(path);if(!info.isFile()||info.isSymbolicLink())throw Error('Regular tracked source inputs required');const data=fs.readFileSync(path);return {path,sha256:crypto.createHash('sha256').update(data).digest('hex'),bytes:data.length}});
|
||||
fs.writeFileSync('.ci/acceptance-evidence/source.json',JSON.stringify({format:'mcp-preview-source-v1',repository:process.env.REPOSITORY,sourceSha:process.env.EXPECTED_SHA,sourceTree:cp.execFileSync('git',['rev-parse','HEAD^{tree}'],{encoding:'utf8'}).trim(),platform:'linux/amd64',component:process.env.COMPONENT,runId:process.env.RUN_ID,runAttempt:process.env.RUN_ATTEMPT,authPortalOrigin:process.env.AUTH_PORTAL_ORIGIN??null,inputs},null,2)+'\n');
|
||||
NODE
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to existing Gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.shx.one
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.ACTIONS_PAT }}
|
||||
- name: Build and push isolated acceptance image
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: deploy/docker/authportal/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.IMAGE_NAME }}:${{ env.PREVIEW_TAG }}
|
||||
build-args: |-
|
||||
VERSION=${{ env.PREVIEW_TAG }}
|
||||
GIT_SHA=${{ gitea.sha }}
|
||||
cache-from: type=registry,ref=${{ env.PREVIEW_CACHE }}
|
||||
cache-to: type=registry,ref=${{ env.PREVIEW_CACHE }},mode=max
|
||||
provenance: mode=min
|
||||
sbom: true
|
||||
labels: |-
|
||||
org.opencontainers.image.revision=${{ gitea.sha }}
|
||||
org.opencontainers.image.version=${{ env.PREVIEW_TAG }}
|
||||
io.mmapp.acceptance-only=true
|
||||
- name: Record exact digest and build metadata
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
|
||||
BUILD_METADATA: ${{ steps.build.outputs.metadata }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node <<'NODE'
|
||||
const fs=require('node:fs'),crypto=require('node:crypto');
|
||||
const digest=process.env.IMAGE_DIGEST;if(!/^sha256:[a-f0-9]{64}$/.test(digest))throw Error('Exact registry digest required');
|
||||
const metadata=JSON.parse(process.env.BUILD_METADATA);const source=fs.readFileSync('.ci/acceptance-evidence/source.json');
|
||||
fs.writeFileSync('.ci/acceptance-evidence/image.json',JSON.stringify({format:'mcp-preview-image-v1',sourceSha:process.env.EXPECTED_SHA,sourceProofSha256:crypto.createHash('sha256').update(source).digest('hex'),image:process.env.IMAGE_NAME,tag:process.env.PREVIEW_TAG,digest,pinnedReference:process.env.IMAGE_NAME+'@'+digest,platform:'linux/amd64',component:process.env.COMPONENT,authPortalOrigin:process.env.AUTH_PORTAL_ORIGIN??null,buildMetadata:metadata,productionDeployed:false,wholeInstallation2GbAccepted:false},null,2)+'\n');
|
||||
console.log('Acceptance image digest: '+process.env.IMAGE_NAME+'@'+digest);
|
||||
NODE
|
||||
- name: Retain source and image binding artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mcp-acceptance-${{ env.COMPONENT }}-${{ gitea.sha }}-${{ gitea.run_id }}-${{ gitea.run_attempt }}
|
||||
path: .ci/acceptance-evidence/*.json
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
name: reui
|
||||
description: "ReUI design and component rules for React apps on shadcn/ui and Tailwind. Use for any UI work in a project with @reui in components.json or a components/reui folder, and in any shadcn project that asks for data grids, kanban boards, dashboards or full pages: building or restyling pages, forms and tables; colors, theme tokens and dark mode; spacing, typography and layout; animation and hover motion; installing ReUI components, examples, Pro blocks or Motion Icons. Works with the ReUI MCP."
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
metadata:
|
||||
user-invocable: "false"
|
||||
---
|
||||
|
||||
> **ReUI skill version `68f0e6fa91`.** If the ReUI MCP's `get_agent_skill` reports a newer `version`, re-run the ReUI installer in the mode it was installed with to update this skill: `get_agent_skill` -> `install.recommended` from the project root, with `REUI_GLOBAL=1` before `node` when this file is `~/.claude/skills/reui/SKILL.md` or `~/.agents/skills/reui/SKILL.md` (a global install). Cloud/tools-only agents have no local file and always read the latest - they can ignore this.
|
||||
|
||||
# ReUI
|
||||
|
||||
ReUI is an existing design system on shadcn/ui. Keep its look. Generic frontend advice (vary fonts or themes, gradients, page-load or staggered reveals, "2-3 intentional motions", Framer Motion) does not apply here.
|
||||
It ships free components, free `c-*` examples, Pro blocks and Ultimate Motion Icons through the shadcn CLI; the ReUI MCP finds them and serves their APIs.
|
||||
|
||||
## Order of authority
|
||||
|
||||
1. The installed style (`components.json` `style`, baked into `components/ui` and `components/reui`): it owns radius, height, padding, shadow and overlay motion.
|
||||
2. The installed component's variants and props (`size`, `variant`, `spacing`).
|
||||
3. These rules and the topic files.
|
||||
4. Other design skills, then your taste: where they disagree with 1 to 3, ReUI wins. Never redefine `--ease-out` or any Tailwind built-in.
|
||||
|
||||
## Before you write UI code, write this plan in your reply
|
||||
|
||||
1. Surface: operational, marketing or flow.
|
||||
2. Archetype: 1 from [compose.md](./rules/compose.md).
|
||||
3. Container family: Frame-led, Card-led, or Cards for the KPIs and a Frame for the table.
|
||||
4. Density: dense (`size="sm"` on controls that take it) or default.
|
||||
5. Regions: top to bottom, each with the item that fills it.
|
||||
6. States: empty, loading and error for each data region.
|
||||
7. Motion: none, or the 1 entrance.
|
||||
|
||||
Then build exactly that plan; if you deviate, say where.
|
||||
|
||||
## The loop
|
||||
|
||||
1. **Find**: `compose_page` for a full page, else `search` with `type` and `component` hints.
|
||||
2. **Install**: `npx shadcn@latest add @reui/<name> --yes`, with `<name>` exactly as a tool returned it (shadcn/ui parts install by bare name: `npx shadcn@latest add button --yes`).
|
||||
3. **Read the API on your base**: `components.json` style `base-nova` is Base UI, `radix-nova` is Radix. Call `get_component([...componentsUsed])` once, then install 1 `get_examples` example and read its files.
|
||||
4. **Adapt**: change data, copy and props; keep the structure and classes.
|
||||
5. **Check**, after every UI edit, follow-ups included: `validate_usage` on any name you did not read, the self-checks C1 to C4 in [quality.md](./rules/quality.md), [anti-patterns.md](./rules/anti-patterns.md), `get_audit_checklist`, typecheck, lint. Hand back in the 5 lines quality.md gives.
|
||||
|
||||
Show every item's `previewUrl` whenever you list, recommend or install it.
|
||||
|
||||
## Rules that always apply
|
||||
|
||||
- **Owners**: tables `data-grid`, filtering `filters`, boards `kanban`, modals `Dialog` (destructive confirms `AlertDialog`), toasts `sonner`; never hand-roll.
|
||||
- **Color**: semantic tokens for meaning; state colors through Badge or Alert variants (`@reui/badge` and `@reui/alert`, not shadcn's); raw palette only for categorical identity, always with its `dark:` pair.
|
||||
- **Contrast**: never `text-success` or `text-warning` as body text in light mode, and never `text-<state>-foreground` on a solid `bg-<state>` fill.
|
||||
- **Shape**: no `rounded-*` except `rounded-full`, and no `shadow-*`, `h-*` or `px-*` on Button, Input, SelectTrigger, Card or Frame; pick `size` and `variant`. A panel is a Card or Frame, never a div with `rounded-lg border`.
|
||||
- **Spacing**: `gap-*` on the parent, not `space-y-*`; `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections; title to description `gap-0.5`.
|
||||
- **Type**: `text-sm` body, `text-xs` meta (controls keep their style's size), `font-medium` or `font-semibold`, headings `tracking-tight`, numbers `tabular-nums`; nothing below `text-xs` except illustrative mini-UI.
|
||||
- **Motion**: installed components own their motion. Your own code uses `transition-colors` for hover and press, `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]` to reveal, `duration-300` with the same curve at most (media hover zoom: 500), and `SPRING` (`{ type: "spring", visualDuration: 0.25, bounce: 0 }`) for drag and reorder. At most 1 entrance animation per view (a stagger counts as 1). Animate only `transform` and `opacity`, CSS before motion/react, never set state per frame; `<MotionConfig reducedMotion="user">` once at the root plus `motion-reduce:` on CSS, where reduced motion means a calm fade, not nothing.
|
||||
- **Surfaces**: prefer 1 container family per screen. With a KPI row and a table, use FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame or a Frame inside a Card. Icons come from 1 library, the `iconLibrary` in `components.json` (on Ultimate, 1 Motion Icon on the primary action is the exception).
|
||||
- **States**: each data region has empty (1 action), loading (a `Skeleton` in the real layout) and error (Retry) states; light and dark work.
|
||||
- **Never invent**: never write a prop, variant or `@reui/` name you did not read in an `api`, an installed file or a tool result.
|
||||
|
||||
## Topics
|
||||
|
||||
Always read `compose` before building a screen, and `quality` plus `anti-patterns` at the Check step. Besides those, read only the topic files the task touches, at most 4 (for example `color`, `motion`, `layout` and `components`).
|
||||
|
||||
Installed skill: open the file. MCP only: call `get_agent_skill` with the topic, or fetch `https://mcp.reui.io/skill/reui/<file>`.
|
||||
|
||||
| Topic | File | Read when |
|
||||
| --------------- | -------------------------------------------------- | --------------------------------------------- |
|
||||
| `compose` | [rules/compose.md](./rules/compose.md) | before building any screen or section |
|
||||
| `workflow` | [rules/workflow.md](./rules/workflow.md) | `compose_page`, search hints, installing |
|
||||
| `adapting` | [rules/adapting.md](./rules/adapting.md) | changing an installed block or example |
|
||||
| `components` | [rules/components.md](./rules/components.md) | `data-grid`, `filters`, `kanban` and the rest |
|
||||
| `color` | [rules/color.md](./rules/color.md) | any color, status, chart or dark mode choice |
|
||||
| `theming` | [rules/theming.md](./rules/theming.md) | the styles, radius, density and sizes |
|
||||
| `motion` | [rules/motion.md](./rules/motion.md) | any transition, animation or motion/react |
|
||||
| `layout` | [rules/layout.md](./rules/layout.md) | house defaults, hierarchy, surfaces, states |
|
||||
| `icons` | [rules/icons.md](./rules/icons.md) | icon imports, Motion Icons |
|
||||
| `quality` | [rules/quality.md](./rules/quality.md) | the done gate: self-checks, a11y, hand-back |
|
||||
| `anti-patterns` | [rules/anti-patterns.md](./rules/anti-patterns.md) | the Check step: drift pairs, stale habits |
|
||||
| `registry` | [rules/registry.md](./rules/registry.md) | free vs Pro vs Ultimate, base vs radix |
|
||||
| `cli` | [rules/cli.md](./rules/cli.md) | `components.json`, license key, install flags |
|
||||
| `tools` | [tools.md](./tools.md) | MCP result shapes, errors, quota, commands |
|
||||
|
||||
## Plans
|
||||
|
||||
Free covers components, `c-*` examples, this skill and the MCP (a ReUI account and a daily allowance). Pro adds blocks. Ultimate adds Motion Icons. A plan miss is a normal answer with `locked: true`; keep building with what the plan covers.
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "ReUI"
|
||||
short_description: "ReUI design and component rules for React apps on shadcn/ui and Tailwind."
|
||||
default_prompt: "Use ReUI to build this UI with installed components and the ReUI motion and color rules."
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
- Never read `window`, `localStorage`, `location`, `Date.now()` or `Math.random()` in render or in a `useState` initializer; read them in an effect.
|
||||
- **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, and the layout must still hold with long names, empty values and crowded data.
|
||||
@@ -1,117 +0,0 @@
|
||||
# Anti-patterns
|
||||
|
||||
Read this at the Check step, after the self-checks in [quality.md](./quality.md). Each entry: an `Incorrect:` line, then its `Correct:` fix. Detector: the self-check that prints it (C1 to C4), or `visual` when reading finds it. Source: the pilot run that wrote it, or `default` (a training-data habit).
|
||||
|
||||
## Pairs
|
||||
|
||||
### 1. Stagger on every row (Detector: C2; source: run-08)
|
||||
|
||||
Incorrect: `transition={{ delay: i * 0.05 }}` on all 50 rows of a table
|
||||
Correct: 1 stagger of at most 6 items, `staggerChildren: 0.05`, never on table rows
|
||||
|
||||
### 2. Transition everything on a card (Detector: C1; source: runs 10, 15)
|
||||
|
||||
Incorrect: `className="rounded-xl border transition-all duration-300 hover:shadow-lg"`
|
||||
Correct: `transition-colors` for a hover color; a lift is `transform` over a static shadow
|
||||
|
||||
### 3. Raw palette for a state (Detector: C3; source: runs 08, 15)
|
||||
|
||||
Incorrect: `<span className="text-green-600">Active</span>`
|
||||
Correct: `<Badge variant="success-light">Active</Badge>`
|
||||
|
||||
### 4. Animated table rows (Detector: C1; source: runs 01, 08, 17)
|
||||
|
||||
Incorrect: `<motion.tr whileHover={{ backgroundColor: "var(--muted)" }}>`
|
||||
Correct: `data-grid` rows, which already hover; never animate rows with motion/react
|
||||
|
||||
### 5. Hover scale on the default spring (Detector: C1; source: runs 01, 10)
|
||||
|
||||
Incorrect: `<motion.div whileHover={{ scale: 1.03 }}>` with no `transition`
|
||||
Correct: CSS first: `transition-colors` for hover, `transition-transform active:scale-[0.98] motion-reduce:transition-none` for press
|
||||
|
||||
### 6. Entrance from scale 0 (Detector: C1; source: runs 10, 15)
|
||||
|
||||
Incorrect: `<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }}>`
|
||||
Correct: `initial={{ opacity: 0, scale: 0.95 }}`: start at scale 0.95 or more and move 8px or less
|
||||
|
||||
### 7. Hand-built modal or toast (Detector: C4; source: run-15)
|
||||
|
||||
Incorrect: `<div className="fixed inset-0 z-50 bg-black/50">` as a modal
|
||||
Correct: `Dialog`, `AlertDialog` for a destructive confirm, `sonner` for toasts; a palette: `<CommandDialog><Command><CommandInput /><CommandList>`, as `CommandDialog` holds no `Command`
|
||||
|
||||
### 8. Native select (Detector: C4; source: run-15)
|
||||
|
||||
Incorrect: `<select>` with `<option>` children
|
||||
Correct: the shadcn `Select`, or `NativeSelect` when the native control is wanted
|
||||
|
||||
### 9. Hand-rolled table (Detector: C4; source: run-17)
|
||||
|
||||
Incorrect: `<table>` with mapped `<tr>` rows
|
||||
Correct: `data-grid`: a `useTable({ features: dataGridFeatures })` instance passed as `table={table}`, columns typed `ColumnDef<DataGridFeatures, Order>[]`
|
||||
|
||||
### 10. Button inside a clickable wrapper (Detector: visual; source: run-02)
|
||||
|
||||
Incorrect: `<motion.button><Button>Save</Button></motion.button>`
|
||||
Correct: the `Button` alone: 1 interactive element, and it already has a press state
|
||||
|
||||
### 11. Radix asChild on Base UI (Detector: C4; source: run-17, judge)
|
||||
|
||||
Incorrect: `<DialogTrigger asChild><Button>Open</Button></DialogTrigger>`
|
||||
Correct: `<DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>` on base-nova; a link: `<Button nativeButton={false} render={<Link href="/x" />}>`
|
||||
|
||||
### 12. Component defined inside render (Detector: visual; source: Haiku runs, judge)
|
||||
|
||||
Incorrect: `const Row = () => <div />` inside the page component
|
||||
Correct: declare `Row` at module scope and pass it props, so React keeps its state
|
||||
|
||||
### 13. A panel inside a panel (Detector: visual; source: run-05)
|
||||
|
||||
Incorrect: `<FramePanel><Card>...</Card></FramePanel>`, or a Card inside a Card
|
||||
Correct: FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame
|
||||
|
||||
### 14. Small badges (Detector: visual; source: runs 03, 04, 06, 07, 10, 13, 16, 18)
|
||||
|
||||
Incorrect: `<Badge variant="success-light" size="sm">Paid</Badge>`
|
||||
Correct: `<Badge variant="success-light">Paid</Badge>` with `size` left off
|
||||
|
||||
### 15. A colored circle behind every KPI icon (Detector: visual; source: default)
|
||||
|
||||
Incorrect: `<div className="rounded-full bg-blue-100 p-2">` around each KPI icon
|
||||
Correct: the number leads; at most 1 `text-muted-foreground` icon
|
||||
|
||||
### 16. Gradient text (Detector: visual; source: default)
|
||||
|
||||
Incorrect: `bg-linear-to-r from-violet-500 to-pink-500 bg-clip-text text-transparent` on a heading
|
||||
Correct: `text-foreground` and `tracking-tight`; color carries meaning, not decoration
|
||||
|
||||
### 17. A dot or check for the current item (Detector: visual; source: default)
|
||||
|
||||
Incorrect: a dot or a `Check` icon beside the active nav item
|
||||
Correct: `bg-muted` (`dark:bg-muted/50`) plus `aria-current`
|
||||
|
||||
### 18. framer-motion (Detector: C4; source: default)
|
||||
|
||||
Incorrect: `import { motion } from "framer-motion"`
|
||||
Correct: `import { motion } from "motion/react"`
|
||||
|
||||
### 19. Clock or randomness in render (Detector: C4; source: default, near run-05)
|
||||
|
||||
Incorrect: `const [id] = useState(() => Math.random())`
|
||||
Correct: `useId()` for ids; read the clock in an effect or an event handler
|
||||
|
||||
### 20. Placeholder data (Detector: visual; source: default)
|
||||
|
||||
Incorrect: "John Doe", "Acme Inc" and round figures like `$1,000`
|
||||
Correct: specific, plausible data: "Mara Lind", `$1,284.50`, 37 open orders
|
||||
|
||||
## Your training data is out of date
|
||||
|
||||
| Old habit | In this stack |
|
||||
| ----------------------------- | --------------------------------------------------------------- |
|
||||
| `framer-motion` | `motion/react` |
|
||||
| `<DataGrid data columns>` | `useTable({ features: dataGridFeatures })` plus `table={table}` |
|
||||
| A fixed div modal | `Dialog` or `AlertDialog` |
|
||||
| A hand-built toast | `sonner` |
|
||||
| `w-5 h-5` | `size-5` |
|
||||
| never `space-y-4` in new code | `flex flex-col gap-4` |
|
||||
| `asChild` | `render` on base-nova |
|
||||
@@ -1,77 +0,0 @@
|
||||
# CLI: registry setup, license, non-interactive install
|
||||
|
||||
## Registry setup (one-time, per project)
|
||||
|
||||
Free items (the ReUI 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 and Motion Icons) require a ReUI license at install:
|
||||
|
||||
1. Add the key to `.env.local`:
|
||||
|
||||
```bash
|
||||
REUI_LICENSE_KEY=your-license-key
|
||||
```
|
||||
|
||||
2. Switch `components.json` to the authenticated object form:
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@reui": {
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${REUI_LICENSE_KEY}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shadcn CLI expands `${REUI_LICENSE_KEY}` from `.env.local` inside `components.json`. MCP client configs expand environment variables too, but each client has its OWN syntax, so wire the ReUI MCP server with the form that client understands:
|
||||
|
||||
- Claude Code (`.mcp.json`, `~/.claude.json`): `"Authorization": "Bearer ${REUI_LICENSE_KEY}"`
|
||||
- Cursor (`.cursor/mcp.json`) and VS Code: `"Authorization": "Bearer ${env:REUI_LICENSE_KEY}"`
|
||||
- OpenCode (`opencode.json`): `"Authorization": "Bearer {env:REUI_LICENSE_KEY}"`
|
||||
- Codex (`~/.codex/config.toml`): `bearer_token_env_var = "REUI_LICENSE_KEY"` - Codex reads the variable itself, so there is no header to write
|
||||
|
||||
Only a client with no interpolation at all needs the raw `reui_pat_...` token, and then only in a file that is never committed. Copying the `components.json` form into a client that does not expand it is the common failure: the literal `${REUI_LICENSE_KEY}` placeholder is sent to the server as the credential and every call comes back 401.
|
||||
|
||||
The MCP `get_project_context` tool returns the right config. Full guide: https://reui.io/docs/registry?ref=skill
|
||||
|
||||
## 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.
|
||||
|
||||
`<name>` is the item name exactly as a ReUI tool returned it. The `@reui/` prefix is for ReUI items only:
|
||||
|
||||
- The shadcn/ui components ReUI builds on (`button`, `dialog`, `select`, `card`, ...) are not ReUI items. Install them from shadcn by bare name: `npx shadcn@latest add button --yes`.
|
||||
- `badge` and `alert` exist in both registries: `@reui/badge` and `@reui/alert` are ReUI's components, bare `badge` and `alert` are shadcn's.
|
||||
- In an item's `registryDependencies`, a `@reui/<name>` entry is a ReUI item and a bare name is a shadcn/ui item. `add` installs both, so you never add them by hand.
|
||||
- Templates are not registry items. Download a template from its page on reui.io (https://reui.io/templates?ref=skill); no `shadcn add` command installs one.
|
||||
|
||||
## 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 ReUI 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/...`) need Ultimate.
|
||||
- Templates are downloads from their page on reui.io, not installs: free ones need only a ReUI account, and each template's page says what the others need.
|
||||
|
||||
If an install 401/403s, the license key is missing, invalid, or the plan does not cover that resource (blocks: Pro or higher; icons: Ultimate). Point the user to https://reui.io/account?ref=skill (their key) or https://reui.io/pricing?ref=skill (upgrade).
|
||||
|
||||
If an install 404s, the name is not a ReUI item. Read the CLI's error message, which names the right install command when there is one, or look the name up with `get_install_command` or `search`.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Color
|
||||
|
||||
Semantic tokens carry meaning; raw palette carries identity. Check every color in light and dark.
|
||||
|
||||
## Tokens
|
||||
|
||||
Surfaces, text and borders use your theme tokens (`background`, `foreground`, `card`, `muted`, `muted-foreground`, `primary`, `accent`, `destructive`, `border`, `ring`), so a theme change re-tints them. ReUI adds state tokens: `--success`, `--info` and `--warning` with their `-foreground`, `--destructive-foreground` (beside shadcn's `--destructive`) and `--invert` with `--invert-foreground` for inverted surfaces.
|
||||
|
||||
They are not part of shadcn's base theme. Installing `@reui/badge`, `@reui/alert` or `@reui/code-block` adds them to your global CSS; until one of those is installed, classes such as `bg-success` generate nothing. Check for `--success:` in your global CSS. If it is missing, add these values to `:root` and `.dark`, plus a `--color-<name>: var(--<name>)` line for each under `@theme inline`:
|
||||
|
||||
<!-- gen:tokens:start -->
|
||||
|
||||
| Token | `:root` | `.dark` |
|
||||
| -------------------------- | -------------------------- | -------------------------- |
|
||||
| `--destructive-foreground` | `var(--color-red-800)` | `var(--color-red-600)` |
|
||||
| `--success` | `var(--color-emerald-500)` | `var(--color-emerald-500)` |
|
||||
| `--success-foreground` | `var(--color-emerald-900)` | `var(--color-emerald-600)` |
|
||||
| `--info` | `var(--color-violet-500)` | `var(--color-violet-500)` |
|
||||
| `--info-foreground` | `var(--color-violet-900)` | `var(--color-violet-600)` |
|
||||
| `--warning` | `var(--color-yellow-500)` | `var(--color-yellow-500)` |
|
||||
| `--warning-foreground` | `var(--color-yellow-900)` | `var(--color-yellow-600)` |
|
||||
| `--invert` | `var(--color-zinc-900)` | `var(--color-zinc-700)` |
|
||||
| `--invert-foreground` | `var(--color-zinc-50)` | `var(--color-zinc-50)` |
|
||||
|
||||
<!-- gen:tokens:end -->
|
||||
|
||||
## Meaning vs identity
|
||||
|
||||
- Meaning (a state, emphasis, a surface, text) takes semantic tokens, through the Badge or Alert variant that already carries them.
|
||||
- Identity (a chart series, a tag color, a team, an avatar) may use raw palette: ReUI's own lint keeps it on purpose. Always hand-pair it for dark, as in `text-violet-600 dark:text-violet-400`.
|
||||
- **Incorrect:** `<span className="text-green-600">Active</span>` (raw palette for a state). **Correct:** `<Badge variant="success-light">Active</Badge>`.
|
||||
|
||||
## State recipes
|
||||
|
||||
- State badge: `<Badge variant="success-light">` from `@/components/reui/badge` (`@reui/badge`; shadcn's `components/ui/badge` has no `-light` variants or `size`), also `info-light`, `warning-light`, `destructive-light`, and `primary-light` for brand or pending. Neutral badge: `variant="outline"`. A solid `<Badge variant="success">` (white text) only when the user asks for a solid fill.
|
||||
- Inline status text: `text-success-foreground dark:text-success`, the pair the ReUI Badge uses.
|
||||
- Tinted surface: `border-success/15 bg-success/10 text-success-foreground dark:border-success/25 dark:bg-success/15 dark:text-success` (the Badge's `success-light` recipe).
|
||||
- A message block: `<Alert variant="success">` from `@/components/reui/alert` (`@reui/alert`), also `info`, `warning` and `destructive`.
|
||||
- Never `text-success` or `text-warning` as body text in light mode, and never `text-<state>-foreground` on a solid `bg-<state>` fill.
|
||||
|
||||
## Contrast
|
||||
|
||||
<!-- gen:contrast:start -->
|
||||
|
||||
Light is `text-<tone>-foreground` on `background` and on the `bg-<tone>/10` tint; dark is `dark:text-<tone>` on `background` and on the `dark:bg-<tone>/15` tint. Ratios against the neutral theme, rounded down; body text needs 4.5:1.
|
||||
|
||||
| Tone | Light | Light on tint | Dark | Dark on tint |
|
||||
| ------------- | ----- | ------------- | ---- | ------------ |
|
||||
| `success` | 9.6 | 8.8 | 8.0 | 6.6 |
|
||||
| `warning` | 8.6 | 8.1 | 10.3 | 8.1 |
|
||||
| `info` | 11.0 | 9.7 | 4.4 | 3.9 |
|
||||
| `destructive` | 8.3 | 7.0 | 6.8 | 5.7 |
|
||||
|
||||
Raw state color as light-mode text on `background`: `text-success` 2.4, `text-warning` 1.9, `text-info` 4.4.
|
||||
|
||||
<!-- gen:contrast:end -->
|
||||
|
||||
Info text in dark mode is under 4.5:1: where it must be read, write the words in `text-foreground` and color only the icon.
|
||||
|
||||
## Dark mode
|
||||
|
||||
- shadcn uses the class strategy: `@custom-variant dark (&:is(.dark *));` in your global CSS, and `.dark` on `<html>` turns it on.
|
||||
- Tokens switch by themselves; raw palette needs its `dark:` pair.
|
||||
- Check both themes before you hand back.
|
||||
|
||||
## Charts
|
||||
|
||||
Check your theme's `--chart-1` to `--chart-5` in both modes. The current shadcn themes keep the same chart colors in light and dark; some themes (reui.io's own among them) change hue. For a series whose identity must hold in both modes, use a palette var such as `var(--color-violet-500)`, as ReUI blocks do.
|
||||
|
||||
<!-- gen:charts:start -->
|
||||
|
||||
Same chart colors in light and dark: neutral, stone, zinc, mauve, olive, mist, taupe, amber, blue, cyan, emerald, fuchsia, green, indigo, lime, orange, pink, purple, red, rose, sky, teal, violet, yellow. Changes hue between light and dark: reui.io's own theme.
|
||||
|
||||
<!-- gen:charts:end -->
|
||||
@@ -1,521 +0,0 @@
|
||||
# ReUI components
|
||||
|
||||
The ReUI components: `alert`, `autocomplete`, `badge`, `cascader`, `code-block`, `color-picker`, `data-grid`, `date-selector`, `emoji-picker`, `event-calendar`, `file-type`, `filters`, `frame`, `gantt`, `icon-stack`, `icon-tile`, `kanban`, `number-field`, `phone-input`, `rating`, `scrollspy`, `signature-pad`, `sortable`, `stepper`, `time-picker`, `timeline`, `tree`. Examples and blocks are composed from these. `file-upload` is a ReUI component too, shipped as the `use-file-upload` hook rather than a primitive, so it is not in that list: install it as `@reui/use-file-upload`, which writes `hooks/use-file-upload.ts` (import `useFileUpload` from `@/hooks/use-file-upload`), call `get_component("file-upload")` for its API and `get_examples("file-upload")` for its free examples. There is no `@reui/file-upload`: the UI is a `c-file-upload-<n>` example, and one built on the hook installs it.
|
||||
|
||||
**Rule one: never guess a component's API.** Call **`get_component(name)`** for its inline `api` and share its `docsUrl` with the user, then **`get_examples(name)`** and copy a worked example. The contracts below are first-try orientation (required props, shape, the one gotcha); the `api` is the reference. A large `api` (`cascader`, `filters`, `data-grid`) is trimmed on heading boundaries: `sectionsOmitted` names what was dropped and the response's `next` hint pulls it back. `validate_usage` always checks the full API. No single block fits? Compose from components, 1 `get_examples` example each ([compose.md](./compose.md)).
|
||||
|
||||
## data-grid (the flagship - read its API every time)
|
||||
|
||||
`data-grid` wraps TanStack Table v9. It is NOT a styled `<table>` and does NOT take `data`/`columns` props directly. The contract:
|
||||
|
||||
- Build a TanStack table with `useTable({ features: dataGridFeatures, ... })`. `dataGridFeatures` already bundles sorting, filtering, pagination, selection, expanding, pinning, resizing and faceting: no row models to wire.
|
||||
- Pass that instance to `<DataGrid table={table} recordCount={total}>`.
|
||||
- Compose the body with `DataGridTable` inside `DataGrid`, and enable features through `tableLayout` (e.g. `{ headerSticky: true, columnsResizable: true }`), not ad-hoc classes.
|
||||
- Server-side data: `manualPagination: true` and `rowCount: total` in `useTable`, `recordCount={total}`, and fetch with the table's `pagination` state (`DataGridPagination` changes `pageSize`).
|
||||
- Columns are TanStack v9, `ColumnDef<DataGridFeatures, Order>[]`, never v8's `ColumnDef<Order>[]`; a sortable column's `header` renders `DataGridColumnHeader` (a string header cannot sort).
|
||||
- Imports, each from `@/components/reui/data-grid/<file>`: `DataGrid`, `DataGridFeatures` and `dataGridFeatures` from `data-grid`, `DataGridColumnHeader` from `data-grid-column-header`, `DataGridTable` from `data-grid-table`, `DataGridPagination` from `data-grid-pagination`, `DataGridScrollArea` from `data-grid-scroll-area`; `useTable` and `ColumnDef` from `@tanstack/react-table`.
|
||||
- States: `isLoading` renders skeleton rows from each column's `meta.skeleton` (a `ReactNode`: `<Skeleton className="h-4 w-20" />`); `emptyMessage` (a `ReactNode`) fills a grid with no rows: after a failed fetch, pass it the error with a Retry.
|
||||
- Numeric column: `meta: { headerClassName: "text-end [&>div]:justify-end", cellClassName: "text-end tabular-nums" }`.
|
||||
- Rows edited in place: `autoResetPageIndex: false` in `useTable`, or each write jumps back to page 1.
|
||||
- In a Frame: `<FramePanel className="p-0">` around `DataGridScrollArea` and `DataGridTable`, plus `tableClassNames={{ base: "min-w-[720px]" }}` so on a phone the table scrolls inside the panel instead of squeezing.
|
||||
|
||||
```tsx
|
||||
const columns = useMemo<ColumnDef<DataGridFeatures, Order>[]>(() => [
|
||||
{ accessorKey: "total", header: ({ column }) => <DataGridColumnHeader title="Total" column={column} /> },
|
||||
], [])
|
||||
const table = useTable({ features: dataGridFeatures, data, columns })
|
||||
|
||||
<DataGrid table={table} recordCount={data.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
```
|
||||
|
||||
Never a raw `<table>` or hand-rolled pagination: the API covers sticky header, virtualization and row selection.
|
||||
|
||||
## event-calendar
|
||||
|
||||
**Required:** events via `events`/`onEventsChange` (controlled) or `defaultEvents` (uncontrolled), plus a height on the root.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EventCalendar defaultEvents={events} defaultView="month" className="h-[560px]">
|
||||
<EventCalendarNav />
|
||||
<EventCalendarContent />
|
||||
</EventCalendar>
|
||||
```
|
||||
|
||||
**Gotcha:** headless-first: `EventCalendarContent` renders the active view (month/week/day/days/agenda; a resource view activates when `resources` is passed) - there is no per-view JSX to compose. Events are `{ id, title, start, end (exclusive), allDay?, color?, recurrence?, resourceId? }`. Mutations flow through `onEventUpdate`/`canDropEvent` (return `false` to reject); the root needs an explicit height because it is a min-h-0 flex column.
|
||||
|
||||
## gantt
|
||||
|
||||
**Required:** `resources` (the left tree) plus bars via `events`/`defaultEvents` attached by `resourceId`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Gantt defaultEvents={bars} resources={tasks} defaultScale="month" className="h-[480px]">
|
||||
<GanttNav />
|
||||
<GanttView />
|
||||
</Gantt>
|
||||
```
|
||||
|
||||
**Gotcha:** bars move along the time axis only (never across rows) and are all-day spans with exclusive `end`; `progress` is 0-100. Scales are `day | week | month | quarter | year`. Zoom control, infinite scroll, summary rollups, and row checkboxes are ON by default - turn off what you do not need. Same `onEventUpdate`/`canDropEvent` commit pipeline as `event-calendar`; the root needs an explicit height.
|
||||
|
||||
## kanban
|
||||
|
||||
**Required:** `value` (`Record<string, T[]>`), `onValueChange`, `getItemValue`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
|
||||
<KanbanBoard>
|
||||
{Object.entries(cols).map(([id, items]) => (
|
||||
<KanbanColumn key={id} value={id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3>{id}</h3>
|
||||
<KanbanColumnHandle aria-label="Move column"><GripVertical /></KanbanColumnHandle>
|
||||
</div>
|
||||
<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. `KanbanColumnHandle` is hidden until its column is hovered or it has keyboard focus, so keep the column title outside it.
|
||||
|
||||
## 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:** `fields` (`FilterField[]`). The value is ONE `FilterQuery` tree - `query` + `onQueryChange`, or uncontrolled `defaultQuery`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
const fields: FilterField[] = [
|
||||
{ id: "title", label: "Title", type: "text" },
|
||||
{
|
||||
id: "status",
|
||||
label: "Status",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "archived", label: "Archived" },
|
||||
],
|
||||
},
|
||||
]
|
||||
const [query, setQuery] = useState<FilterQuery>(() => createFilterQuery())
|
||||
|
||||
<Filters fields={fields} query={query} onQueryChange={setQuery} />
|
||||
```
|
||||
|
||||
**Gotcha:** the state is a TREE, not a list of chips. `FilterQuery` is a group of rules joined by `and`/`or` and a group may hold another group, so `(A and B) or C` is expressible; a rule is `{ id, type: "rule", path: ["status"], operator, value }` and `path` is the whole nested attribute path, root first. The pre-rewrite API is GONE: there is no `filters`/`onChange` prop, no `FilterFieldConfig` (fields are `FilterField`, nested through their own `fields`, keyed `id` not `key`), and no `createFilter()`: ids come from `createFilterIdFactory(seed)` seeded off `useId` (never minted in render), and `createFilterRule({ id })` takes one, `createFilterQuery()` none. Read the query back with `flattenFilterConditions` (`{ path, field, operator, values, negated }` per rule, incomplete rules skipped) and walk the tree yourself when the parentheses carry meaning - the primitive compiles nothing, no SQL, no query string.
|
||||
|
||||
`variant` picks the chrome over that one query: `"basic"`, the default, is the flat chip row for a toolbar over a table; `"advanced"` is the condition builder, hung off a trigger or rendered in place with `advancedMode="inline"`. Both read and write the same tree, so a saved view built in one opens in the other. Other props worth knowing before you hand-roll them: `size` is two rungs, `"sm" | "default"`, resolved per style (there is no `lg`); `reorderable` turns on drag and Alt+Arrow row moves in the builder; `onBeforeQueryChange` is the ONE veto point for every write (return `false` to refuse, it cannot rewrite); `editors` registers custom value editors a field selects by `editor` name; `labels` / `operatorLabels` own every rendered string, and `renderChip` / `renderValue` / `renderEmpty` replace rendered parts. On a field, `loadOptions` supplies async options with paging and `resolveValues` renders a chip restored from a saved view whose option was never loaded.
|
||||
|
||||
## cascader
|
||||
|
||||
**Required:** `items` (a tree of `{ value, label, children? }`), plus the panel parts inside `CascaderContent`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Cascader items={items} value={value} onValueChange={setValue}>
|
||||
<CascaderTrigger render={<Button variant="outline" />}>
|
||||
<CascaderValue placeholder="Select an attribute" />
|
||||
</CascaderTrigger>
|
||||
<CascaderContent className="w-80">
|
||||
<CascaderPanel>
|
||||
<CascaderNav>
|
||||
<CascaderBreadcrumb />
|
||||
<CascaderInput />
|
||||
</CascaderNav>
|
||||
<CascaderEmpty />
|
||||
<CascaderList maxHeight={288}>
|
||||
<CascaderItems />
|
||||
</CascaderList>
|
||||
<CascaderStatus />
|
||||
</CascaderPanel>
|
||||
</CascaderContent>
|
||||
</Cascader>
|
||||
```
|
||||
|
||||
**Gotcha:** pressing a branch NAVIGATES, it does not select - only leaves are selectable until you pass `selectable="any"` or a predicate, and once a branch is selectable its chevron becomes the only way to open it. `CascaderInput` must stay inside `CascaderContent` (Base UI refills the query from the selection when the input sits outside the popup). Always include `CascaderStatus`: it is the live region announcing level changes, which the visual breadcrumb does not provide to screen readers. Accepts a flat adjacency list via `getParent` as well as nested `children`. `searchScope="deep"` searches the current level and everything under it, `searchScope="global"` the whole tree from any level, and both annotate results with their path; `multiple` gives checkbox rows; `inline` + a bare `CascaderPanel` embeds it with no popover.
|
||||
|
||||
The shape above is `mode="drill"`, the default. `mode="tree"` keeps the same parts (drop `CascaderBreadcrumb`, pass `showBack={false}`, drive expansion with `expanded`/`onExpandedChange`); `mode="columns"` REPLACES `CascaderList` + `CascaderItems` with a single `CascaderColumns`, and has no breadcrumb. Other props worth knowing before you hand-roll them: `cascade` (multi-select only, parent/child selection with indeterminate branches - pair it with `selectable="any"`, since a leaf-only tree can never cascade), `virtualize` plus `CascaderVirtualItems` for long levels, and `getChildren` for async levels (paging, retry, `prefetch`). `CascaderFooter` pins commands below the list and `CascaderSubmenu` opens one as a side flyout. To head a run of rows use `CascaderGroup` wrapping a `CascaderLabel` - a bare label inside a listbox names nothing and is dropped from the accessibility tree - and `CascaderSeparator` for the rule between runs.
|
||||
|
||||
## 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/reui/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). `smooth` defaults to true: pass `smooth={!reduceMotion}` from motion/react's `useReducedMotion()`. Mark the active link `aria-current="location"` (track it with `onUpdate`); `"page"` is for route links.
|
||||
|
||||
## frame
|
||||
|
||||
**Required:** `Frame` > `FramePanel`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Frame>
|
||||
<FrameHeader>
|
||||
<FrameTitle>Title</FrameTitle>
|
||||
<FrameDescription>Description</FrameDescription>
|
||||
</FrameHeader>
|
||||
<FramePanel>Content</FramePanel>
|
||||
<FrameFooter>Footer</FrameFooter>
|
||||
</Frame>
|
||||
```
|
||||
|
||||
**Gotcha:** the shell for data and tool-like surfaces. `spacing` (`xs` | `sm` | `default` | `lg`) sets panel padding and the gap between panels, so pick it instead of padding classes (the 1 exception: `<FramePanel className="p-0">` around a `data-grid`); `variant` is `default` | `inverse` | `ghost`; `stacked` joins panels with shared borders; `dense` removes the frame's own inset and gap (panels sit flush), and panel padding stays; `--frame-radius` is the one radius knob (panels derive `--frame-panel-radius`).
|
||||
|
||||
## icon-stack
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconStack aria-hidden="true">
|
||||
<InboxIcon className="size-4" />
|
||||
</IconStack>
|
||||
```
|
||||
|
||||
**Gotcha:** isometric layered artwork for empty states and illustrations; style the inner icon via its own `className`. Mark purely decorative stacks `aria-hidden="true"` and keep the real label in surrounding copy.
|
||||
|
||||
## icon-tile
|
||||
|
||||
**Required:** one child icon
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<IconTile variant="elevated" size="lg">
|
||||
<PackageIcon />
|
||||
</IconTile>
|
||||
```
|
||||
|
||||
**Gotcha:** the square container an icon sits in, so every list row, feature card and empty state shares one affordance. `variant`: `outline` (default) | `elevated` (muted fill, raised ring) | `soft` (tinted nested, tone from currentColor) | `solid` (filled tone, contrasting glyph) | `frame` (double container). `soft` and `solid` retint from one text color class (they default to `text-primary`). `size`: `xs | sm | default | lg | xl` (24/32/40/48/64px tile, glyph scales 12/14/16/20/24px). `radius`: `default | full`. Do not set a `size-*` class on the child icon unless you mean to override the tile's glyph size; recolor with `className` on the tile, not the icon.
|
||||
|
||||
## code-block
|
||||
|
||||
**Required:** `code` + `language`, or pre-highlighted `lines`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<CodeBlock code={code} language="tsx" />
|
||||
|
||||
<CodeBlock code={code} language="tsx" showLineNumbers maxLines={20}>
|
||||
<CodeBlockHeader>
|
||||
<CodeBlockTitle>use-totals.ts</CodeBlockTitle>
|
||||
<CodeBlockLanguage />
|
||||
<CodeBlockCopyButton className="ml-auto" />
|
||||
</CodeBlockHeader>
|
||||
<CodeBlockExpandButton />
|
||||
</CodeBlock>
|
||||
```
|
||||
|
||||
**Gotcha:** the one-liner is already a complete block; every child is optional chrome, at any depth. Shiki is the only npm dependency and loads lazily on first highlight; `lines` (from `highlightCode` in `code-block-highlight`, which is server-safe and has no `"use client"`) or `highlight={false}` loads nothing at all. `maxLines` caps the height AND marks the block collapsible, which is what makes `CodeBlockExpandButton` appear. Use `variant="ghost"` when the block sits inside a surface that already has a border.
|
||||
|
||||
## 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.
|
||||
|
||||
## badge
|
||||
|
||||
**Required:** none (text child).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="success-light">Success</Badge>
|
||||
<Badge variant="outline">Draft</Badge>
|
||||
```
|
||||
|
||||
**Gotcha:** shadcn-compatible. Rich `variant` set (solid, `-outline`, `-light` per color), `size` `xs..xl` (keep the default), `radius` `default | full`. Like `alert`, the color variants rely on ReUI extended tokens. Prefer `Badge` variants over raw color classes: `<tone>-light` for a state, `outline` for a neutral label; an omitted `variant` is the solid primary fill.
|
||||
|
||||
## color-picker
|
||||
|
||||
**Required:** none (`<ColorPicker />` alone is complete); wire `value` + `onValueChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<ColorPicker value={color} onValueChange={setColor} alpha>
|
||||
<ColorPickerTrigger />
|
||||
<ColorPickerContent>
|
||||
<ColorPickerPanel>
|
||||
<ColorPickerArea />
|
||||
<ColorPickerSlider channel="hue" />
|
||||
<ColorPickerSlider channel="alpha" />
|
||||
<ColorPickerFields />
|
||||
</ColorPickerPanel>
|
||||
</ColorPickerContent>
|
||||
</ColorPicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a CSS color string in `format` (`"hex"` default, `"rgb"`, `"hsl"`, `"hsb"` or `"oklch"`), `null` when empty, never an object. `onValueChange` fires on every drag move, so save in `onValueCommit`. Without `alpha` the value is always opaque. A bare `ColorPickerPanel` inside the root is an inline picker; `ColorPickerInput` replaces the trigger with a typable field.
|
||||
|
||||
## emoji-picker
|
||||
|
||||
**Required:** none; wire `onEmojiSelect` to insert a pick, or `value` + `onValueChange` to use it as a field.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<EmojiPicker onEmojiSelect={({ value }) => insert(value)}>
|
||||
<EmojiPickerTrigger />
|
||||
<EmojiPickerContent>
|
||||
<EmojiPickerHeader>
|
||||
<EmojiPickerSearch />
|
||||
</EmojiPickerHeader>
|
||||
<EmojiPickerCategories />
|
||||
<EmojiPickerList />
|
||||
<EmojiPickerFooter>
|
||||
<EmojiPickerPreview />
|
||||
<EmojiPickerSkinTone />
|
||||
</EmojiPickerFooter>
|
||||
</EmojiPickerContent>
|
||||
</EmojiPicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the emoji data is fetched at runtime from a pinned jsDelivr path (`EMOJI_PICKER_DATA_URL`): under a strict Content Security Policy allow it in `connect-src`, self-host with `dataUrl`, or pass `data` (from `normalizeEmojibaseData`). Store the pick's `value` (the native emoji in its skin tone, or `:shortcode:` for a custom one). `multiple` makes the value a list, capped by `max`.
|
||||
|
||||
## file-type
|
||||
|
||||
**Required:** none; pass `type` (an extension, a family name or any string).
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<FileType type="pdf" />
|
||||
<FileType type={getFileType(file.name)} size="sm" aria-hidden="true" />
|
||||
```
|
||||
|
||||
**Gotcha:** one `span` with `role="img"`, no children, no `"use client"` (fine in a Server Component). Resolve a file name, path, URL or MIME type with `getFileType`, never by splitting on the dot. `variant`: `outline` (default) | `solid` | `light` | `mono`; `size`: `mini` | `xs` | `sm` | `default` | `lg` | `xl` (18 to 56px); recolor with a `text-*` class. Pass `aria-hidden="true"` when the file name sits beside it.
|
||||
|
||||
## signature-pad
|
||||
|
||||
**Required:** `SignaturePad` > `SignaturePadArea`
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<SignaturePad value={strokes} onValueChange={setStrokes}>
|
||||
<SignaturePadArea>
|
||||
<SignaturePadGuide />
|
||||
<SignaturePadPlaceholder />
|
||||
<SignaturePadControls>
|
||||
<SignaturePadUndo />
|
||||
<SignaturePadClear />
|
||||
</SignaturePadControls>
|
||||
</SignaturePadArea>
|
||||
</SignaturePad>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is an array of strokes (`{ points: [x, y, size][], color? }`), not an image: store it as JSON, and export with `useSignaturePad()` or `apiRef` (`toSVG`, `toDataURL`, `toBlob`) or `SignaturePadSave` (`format`, `onSave`). `onValueChange` fires per finished stroke, never per pointer move. Size the area with a class (`h-40` by default); an overlay in it that is not a control needs `pointer-events-none`. Drawing needs a pointer, so offer a typed name where a signature is required.
|
||||
|
||||
## time-picker
|
||||
|
||||
**Required:** none (`<TimePicker />` alone is complete); wire `value` + `onValueChange`.
|
||||
**Shape:**
|
||||
|
||||
```tsx
|
||||
<TimePicker value={time} onValueChange={setTime}>
|
||||
<TimePickerTrigger />
|
||||
<TimePickerContent>
|
||||
<TimePickerColumns />
|
||||
<TimePickerFooter>
|
||||
<TimePickerNow />
|
||||
<TimePickerClear />
|
||||
<TimePickerConfirm />
|
||||
</TimePickerFooter>
|
||||
</TimePickerContent>
|
||||
</TimePicker>
|
||||
```
|
||||
|
||||
**Gotcha:** the value is a 24-hour `"HH:mm"` string (`"HH:mm:ss"` with `granularity="second"`) or `null`, never a `Date`, whatever `hourCycle={12}` shows. `minuteStep`, `min`, `max` and `isTimeDisabled` narrow the options; keep `isTimeDisabled` stable (module scope or memoized). `requireConfirm` holds picks as a draft until OK. `TimePickerInput` replaces `TimePickerTrigger` for a typable field, never beside it.
|
||||
|
||||
## base vs radix - write for the project's base
|
||||
|
||||
Every component ships a `base` (Base UI) and a `radix` (Radix UI) build under 1 name, and the CLI installs the one the project uses. Read `components.json` `style`: the part before the first `-` is the base (`base-nova` is **Base UI**, `radix-nova` is **Radix UI**). Write against that base; the deltas mirror shadcn's:
|
||||
|
||||
- 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"`.
|
||||
|
||||
Installed files and `c-*` examples are already in your base: reuse their wiring. When an `api` or example shows the other base's shape, translate it (or `validate_usage` to confirm).
|
||||
@@ -1,62 +0,0 @@
|
||||
# Compose a screen
|
||||
|
||||
Read this before you build any screen or section, then write the router's 7-line plan in your reply (Surface, Archetype, Container family, Density, Regions, States, Motion) and build exactly that plan. Style it with the topics the screen touches: [layout.md](./layout.md) (house defaults), [color.md](./color.md), [motion.md](./motion.md).
|
||||
|
||||
If the brief names a layout or a motion (a left nav, 2 entrances), follow the brief and name the default you overrode. Several entrances run as 1 stagger: at most 6 items, 50ms apart, never on table rows or lists of 20 or more.
|
||||
|
||||
## Surface
|
||||
|
||||
- **Operational**: a screen people work in every day (dashboard, records, settings, admin tools). Frame-led, dense (`size="sm"` on controls that take it), no hero, no page-load motion.
|
||||
- **Marketing**: a page a visitor reads once (landing, pricing, features). Card-led sections on the spine below, default density.
|
||||
- **Flow**: a task people finish and leave (sign in, onboarding, checkout). 1 centered Card or a Dialog, 1 primary action per step.
|
||||
|
||||
## Archetypes: pick 1 per screen
|
||||
|
||||
- **Vertical rail settings** (3 to 6 sections): a section rail on the left, 1 content column, 1 section at a time; the rail turns horizontal below `md`.
|
||||
- **Horizontal tabs** (5 to 8 views of 1 object): a full-width `Tabs` rail under the header that scrolls sideways when it overflows, 1 content column.
|
||||
- **Frame stack**: stacked Frames, each 1 focused group of settings or records; a flat page split by `Separator` rows when it should feel lighter.
|
||||
- **Table-backed management**: header, toolbar (search, `filters`), then a `data-grid` with row actions and pagination, all in 1 Frame.
|
||||
- **Dashboard grid**: a KPI row, then 1 or 2 chart or list panels, then the table.
|
||||
- **Board**: header and filters, then a `kanban` board; `KanbanBoard` stacks its columns below `sm`, and a wider board scrolls inside its own region.
|
||||
- **Record detail**: header (title, status `Badge`, actions), a main column of sections, a side column of facts that stacks under it below `lg`.
|
||||
- **Marketing spine**: optional `Badge` eyebrow, headline (the only loud element), 1-clause subhead, action row (1 primary `Button`, a second `variant="outline"`, a third a text link), specific proof beside it ("27 reviews"), media. Omit parts; never reorder them.
|
||||
- **Auth card**: 1 centered `Card` at `max-w-sm`: title, fields, 1 full-width primary `Button`, a text link to the other flow.
|
||||
- **Wizard dialog**: a `Dialog` holding a `stepper`, 1 step visible, Back (`variant="outline"`) and Next (primary) in its footer.
|
||||
|
||||
An app shell around any of them is the shadcn `Sidebar`, which turns into a `Sheet` below `md` by itself.
|
||||
|
||||
## Fill each region in this order
|
||||
|
||||
1. The ReUI component that owns the job: the router's Owners (`data-grid`, `filters`, `kanban`), `useFileUpload` from `@/hooks/use-file-upload` for file drop, or the one whose contract in [components.md](./components.md) matches.
|
||||
2. A shadcn/ui component (`Tabs`, `Dialog`, `AlertDialog`, `Sheet`, `Empty`, `Skeleton`, `sonner`).
|
||||
3. A free `c-*` example from `get_examples` that already composes it: install it and adapt.
|
||||
4. Your own composition of those parts. Never a hand-rolled table, modal or tabs.
|
||||
|
||||
Import ReUI parts file by file: `data-grid`, `filters`, `cascader`, `event-calendar` and `gantt` are folders with no index file. `data-grid`: the import line in [components.md](./components.md). `filters`, each from `@/components/reui/filters/<file>`: `Filters` from `filters`, `createFilterQuery` and `flattenFilterConditions` (1 argument, the query) from `filters-query`, the types `FilterField` and `FilterQuery` from `filters-types`.
|
||||
|
||||
## Wiring that works
|
||||
|
||||
- `data-grid`: columns are `ColumnDef<DataGridFeatures, Order>[]`, a sortable header renders `DataGridColumnHeader`, and server paging needs `manualPagination: true` and `rowCount`.
|
||||
- `filters`: apply each condition by its `operator`. Negate swaps a rule to its inverse (`is` and `is_not`, `contains` and `not_contains`, `is_any_of` and `is_none_of`, `empty` and `not_empty`) and sets `negated` only when there is none: honour both.
|
||||
- A Base UI `Button` that navigates: `<Button nativeButton={false} render={<Link href="/x" />}>`.
|
||||
- shadcn's `CommandDialog` has no `Command` inside, unlike older shadcn: wrap `CommandInput` and `CommandList` in one. `CommandItem` search matches its `value`: omit it (text is used) or use the label; ids in `onSelect`.
|
||||
|
||||
## Headers
|
||||
|
||||
A page or panel header puts the title (1 to 3 words) and a 1-clause description on the left and the actions on the right: 1 primary `Button`, the rest `variant="outline"`, row actions `variant="ghost"`.
|
||||
|
||||
## Responsive
|
||||
|
||||
- No horizontal page scroll at 390px: grids drop to 1 column, side columns stack, `min-w-0` on every shrinking parent.
|
||||
- A KPI row is `grid-cols-1 sm:grid-cols-2 xl:grid-cols-4`.
|
||||
- Every header and toolbar action stays reachable at every width: a control hidden below a breakpoint moves into a menu shown only there.
|
||||
- A table scrolls sideways inside its Frame, never the page (the `data-grid` contract in [components.md](./components.md)).
|
||||
|
||||
## States and motion
|
||||
|
||||
- Each data region owns its states: empty (shadcn `Empty` with a title, 1 line and the primary action), loading (`Skeleton` in the real layout; `data-grid` takes `isLoading`) and error (inline in the region, with Retry). Never 1 spinner for the whole page.
|
||||
- Motion: none, or the 1 entrance per view; installed parts keep their own motion, and hover and press follow [motion.md](./motion.md). An Alert, field error, notice or data that appears after an action or a load is a reveal, not an entrance: fade it in with the reveal row.
|
||||
|
||||
## Remove first
|
||||
|
||||
Overloaded? Remove, in order: decorative summaries, duplicate context, extra badges, long descriptions, secondary actions, mismatched icon treatments. Add a region only when the brief needs it.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Icons (ReUI delta over shadcn)
|
||||
|
||||
Use the project's configured `iconLibrary`, put `data-icon="inline-start"` on an icon before the label inside `Button` (`data-icon="inline-end"` on one after it), put no sizing classes on icons inside components, and pass icons as components, not string keys. ReUI adds the following.
|
||||
|
||||
## Portable icons (library-agnostic)
|
||||
|
||||
ReUI components, examples, and blocks are authored to be icon-library-agnostic. The shadcn CLI installs each item's icons in the library named by `iconLibrary` in `components.json` - you swap nothing. When `iconLibrary` is not set, the CLI falls back to `lucide`, so the icons arrive as `lucide-react` imports. If that is not the project's library, set `iconLibrary` before installing, or change the **import source and component name** to your library, keeping the same icon-name semantics (each key below is an `iconLibrary` value):
|
||||
|
||||
- `lucide` -> `lucide-react`
|
||||
- `tabler` -> `@tabler/icons-react`
|
||||
- `phosphor` -> `@phosphor-icons/react`
|
||||
- `remixicon` -> `@remixicon/react`
|
||||
- `hugeicons` -> icons from `@hugeicons/core-free-icons`, rendered through `HugeiconsIcon` from `@hugeicons/react`
|
||||
|
||||
Don't assume `lucide-react`: read `iconLibrary` from `components.json`, and when it is unset, check which icon package the project already uses.
|
||||
|
||||
## 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). Every icon installs from the default path; some also have a real hover animation (motion/react) on the animated path:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @reui/icons/default/<style>/<name> --yes # static, every icon
|
||||
npx shadcn@latest add @reui/icons/animated/<style>/<name> --yes # hover-animated, only when the icon has installAnimated
|
||||
```
|
||||
|
||||
The animated path answers for every icon, but for an icon without a real animation it installs the same static drawing, so only use it when the icon carries `installAnimated`.
|
||||
|
||||
Motion Icons need an Ultimate license for **both discovery and install** (`REUI_LICENSE_KEY`, see [cli.md](./cli.md)). Without it `search_icons` comes back `locked` with no results and `search` returns no icons at all, so check the plan before you promise the user icons: on a free or Pro account, use the project's own `iconLibrary` instead. On Ultimate, a Motion Icon on the primary action is the 1 hover flourish (see [motion.md](./motion.md)).
|
||||
|
||||
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 real hover animation; a concept with none comes back empty and `animatedNote` says so.
|
||||
- One icon: `search` with `type: "icon"`.
|
||||
- Icon results and `get_icon` carry `animated: true` and `installAnimated` only when the icon really animates - use those install strings, do not construct paths by hand.
|
||||
- Every icon result carries a `previewUrl` (the icon's own page) and a `thumbnail` (its 4 styles side by side) - **show or share them** so the user can SEE the icon before installing, and look at the thumbnail yourself before trusting a name.
|
||||
- Several icons can fit one concept. Before installing, read `get_icon`'s `whenNotToUse` (each entry names the icon to use instead), and use its `label` as the `aria-label` of an icon-only button.
|
||||
- Icon names were revised in the icons v2 update. An old name (`save-2`, `abstract-18`) still answers in `get_icon`, `get_install_command`, `validate_usage` and `get_thumbnail` with the icon it became and `renamedFrom`, and still installs that same drawing; write the current name in new code. That includes names renamed because they misdescribed the drawing (`watch` installs a sand timer, `chevrons-down` points up), so look at the `thumbnail` before trusting an old name. `search_icons` answers an old name the same way, unless it is also an ordinary word icons are named for today (`watch`): then it ranks today's icons for the word, and `oldNameNotes` names the icon the old name installs. `search` ranks by current names only, so look an old name up with `get_icon`, never `search`. Only `tag` (it drew a money bag) no longer answers: the reply names the icons to use instead.
|
||||
|
||||
The `icon-stack` component composes multiple icons into a stacked display.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Layout: house defaults, hierarchy, spacing, surfaces and states
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
## House defaults
|
||||
|
||||
<!-- gen:house-defaults:start -->
|
||||
|
||||
- **Spacing ladder**: `gap-1` to `gap-2` in a row, `gap-3` to `gap-4` between groups, `gap-6` to `gap-8` between sections, `gap-0.5` title to description. Deviate when: skip a level if you like, never invert them.
|
||||
- **Panel title**: no size class on `FrameTitle` or `CardTitle`. Deviate when: never; `FrameTitle` ships `text-sm font-semibold`, `CardTitle` follows the installed style.
|
||||
- **Meta text**: `text-xs`. Deviate when: never smaller, except inside illustrative mini-UI artwork.
|
||||
- **Frame spacing**: `spacing="default"`, written out. Deviate when: `sm` in a sheet, dialog, drawer or popover, or for a dense data grid in a panel; `xs` or `lg` only with a stated reason.
|
||||
- **Leading row icon**: `size-4`. Deviate when: never in a glyph column; `Spinner` ships `size-4`, so a loading swap keeps the width.
|
||||
- **Icon inside a control**: no size class; the control sizes it per style and size. Deviate when: never pin `size-*` on an svg inside `Button`, `InputGroupButton`, `InputGroupAddon`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioItem`, `DropdownMenuSubTrigger`, `SelectItem`, `CommandItem`, `Toggle`, `ToggleGroupItem`, `IconTile` or `MarkerIcon`.
|
||||
- **Badge size**: `default` (leave `size` off). Deviate when: `lg` or `xl` only when the user asks; never `sm` or `xs`.
|
||||
- **Neutral badge**: `variant="outline"`. Deviate when: never `secondary`, `default` or `primary-light` for an id, tag, version, count or Draft/Beta label; inline after the title, never `ml-auto`.
|
||||
- **State badge**: `<tone>-light`: `success-light`, `info-light`, `warning-light`, `destructive-light`; `primary-light` for brand or pending. Deviate when: never the solid tone, `secondary` or `outline` for a state; 1 state badge per row.
|
||||
- **Solid badge**: never unless asked; always name a variant (an omitted one is the solid primary fill). Deviate when: only when the user asks for a solid fill (`default`, `secondary`, `success`, `info`, `warning`, `destructive`, `focus`, `invert`).
|
||||
- **Slot padding**: the primitive's own inset; set density with `Frame spacing=` or `Card size="sm"`. Deviate when: never a padding class on `CardHeader`, `CardContent`, `CardFooter`, `FrameHeader`, `FramePanel`, `FrameFooter`, `SheetHeader`, `SheetContent`, `SheetFooter`, `DialogHeader`, `DialogContent`, `DialogFooter`, `PopoverContent`, `DropdownMenuContent`, `TabsContent` or `AccordionContent`.
|
||||
- **Inline separator**: `<span aria-hidden className="bg-muted-foreground/40 size-1 shrink-0 rounded-full" />`. Deviate when: never a typed `·` between rendered segments; a literal dot only inside 1 data string.
|
||||
- **List rows**: dense, auto height; a row with no second line stays 1 tight line. Deviate when: never uniform height.
|
||||
- **Current list item**: ghost hover fill (`bg-muted`, `dark:bg-muted/50`) plus `aria-current`. Deviate when: never a dot or a check mark.
|
||||
|
||||
<!-- gen:house-defaults:end -->
|
||||
|
||||
## Hierarchy
|
||||
|
||||
- 1 focal point per card or panel: the dominant metric or task, then its label, then detail. Borders, separators and surfaces make 2 to 3 bands, never equal weight everywhere.
|
||||
- Overloaded? Remove, in order: decorative summaries, duplicate context, extra badges, long descriptions, secondary actions, mismatched icon treatments.
|
||||
|
||||
## Spacing and copy
|
||||
|
||||
- Siblings take the parent's `gap-*`, never their own margins; a heading sits nearer its own content than the section above it. Never pad an operational surface like a marketing page.
|
||||
- Titles 1 to 3 words, a description 1 clause (about 8 words), a context line 2 segments at most, labels and buttons 1 to 2 words, a toast title 4 words at most. Copy that wraps at desktop width is over budget: cut it, never shrink the type.
|
||||
|
||||
## Surfaces
|
||||
|
||||
- `Frame` for data and operational surfaces (tables, lists, tool panels); `Card` for bounded sections and marketing.
|
||||
- Prefer 1 container family per screen. With a KPI row and a table, use FramePanels for both, or Cards for the KPIs and a Frame for the table; never a Card inside a Frame or a Frame inside a Card.
|
||||
- A `data-grid` inside a Frame is the one place for `<FramePanel className="p-0">`; elsewhere no padding on slots. This is the 1 exception to the Slot padding row above.
|
||||
- 1 archetype per screen (operational ones and the marketing spine): [compose.md](./compose.md) lists each with its regions.
|
||||
|
||||
## Type and buttons
|
||||
|
||||
- A KPI value is `text-2xl font-semibold tracking-tight tabular-nums`.
|
||||
- `size="sm"` on dense surfaces, `variant="outline"` for secondary actions, `ghost` for row actions; 1 primary per region, 1 variant per toolbar.
|
||||
- A toolbar separator is `<Separator orientation="vertical" className="my-auto h-4" />`; at a 390px viewport a toolbar stays on 1 row.
|
||||
|
||||
## States and responsive
|
||||
|
||||
- Empty: a short message plus the primary action. Loading: a `Skeleton` in the real layout, never a centered spinner. Error: inline with a retry.
|
||||
- No horizontal page scroll at 390px: reflow to 1 column instead of shrinking; `min-w-0` on the shrinking parent and `truncate` on single-line labels.
|
||||
@@ -1,80 +0,0 @@
|
||||
# Motion
|
||||
|
||||
## Installed motion: never retime it
|
||||
|
||||
<!-- gen:installed-motion:start -->
|
||||
|
||||
| Installed part | Duration |
|
||||
| ------------------ | ----------------- |
|
||||
| Overlays | 100ms |
|
||||
| Tooltip | 150ms (default) |
|
||||
| Sheet, sidebar | 200ms |
|
||||
| Navigation menu | 300ms, exit 150ms |
|
||||
| rhea control focus | 200ms |
|
||||
|
||||
<!-- gen:installed-motion:end -->
|
||||
|
||||
Never add or change `duration-*`, `ease-*`, `animate-*` or `transition-*` on installed parts (`components/ui`, `components/reui`); Button already presses with `active:not-aria-[haspopup]:translate-y-px`.
|
||||
|
||||
## House motion: your own code
|
||||
|
||||
<!-- gen:house-motion:start -->
|
||||
|
||||
| Use | Tailwind |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| Hover, press, color | `transition-colors` (150ms, no duration or ease class) |
|
||||
| Reveal or expand | `duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]` |
|
||||
| Large surface (ceiling) | `duration-300` with the same curve |
|
||||
| An overlay you build | `duration-100` with the same curve |
|
||||
| Media hover zoom | `duration-500` with the same curve, `scale-105` at most |
|
||||
| Width tied to the sidebar | `duration-200 ease-linear` |
|
||||
|
||||
<!-- gen:house-motion:end -->
|
||||
|
||||
- Press on a custom surface (not Button): `transition-transform active:scale-[0.98] motion-reduce:transition-none`.
|
||||
- Hover color plus press on 1 element: `transition-[color,background-color,border-color,scale]` (`transition-colors` and `transition-transform` override each other; `scale-*` sets `scale`).
|
||||
- motion/react takes the same values in seconds: `{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }`; hover `{ duration: 0.15 }`.
|
||||
- Drag, reorder and layout use `const SPRING = { type: "spring", visualDuration: 0.25, bounce: 0 }`. Define `SPRING` only when something drags, reorders or toggles layout.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Animate `transform` and `opacity`; color only for hover and press. Never `transition-all`; width, height, top or left only in the sidebar row.
|
||||
- Never `animate-bounce` or a spring with bounce above 0. `ease-linear` only for progress, spinners, loops and the sidebar; never Tailwind's `ease-in`, `ease-out` or `ease-in-out`.
|
||||
- `motion/react` only: never framer-motion, GSAP or react-spring.
|
||||
- At most 1 entrance animation per view (a stagger counts as 1), and every animation has a reduced-motion path. Feedback that appears after an action or a load (an Alert, a field error, a saved notice, data replacing its `Skeleton`) is a reveal, not an entrance: fade it in with the reveal row.
|
||||
- Never add named `duration-*` utilities or redefine `--ease-out`.
|
||||
- Loading: a `Skeleton` (it already pulses) in the real layout; a spinner only inside a busy button.
|
||||
- Motion Icons (Ultimate only): 1 on the primary action is the 1 hover flourish; install it with its `installAnimated` command; never retime it.
|
||||
|
||||
## Performance
|
||||
|
||||
Each line names its lint rule, or judgement (no check).
|
||||
|
||||
- Never animate blur, drop-shadow, backdrop-filter, box-shadow, border-radius, background-position, background-size or a custom property that feeds transform; clip-path and stroke-dashoffset only as a one-shot reveal of 300ms or less, or a loop on an element of 240x240px or less. Name the properties: never a bare `transition` (it also animates box-shadow and filter). (`reui/no-paint-animation`; 240px: judgement (no check))
|
||||
- A hover lift is `transform` over a static shadow, or a shadow layer whose `opacity` fades; never a transitioned `hover:shadow-*`. (`reui/no-paint-animation`)
|
||||
- CSS first: hover, press, reveals and ambient loops are Tailwind transitions, `tw-animate-css` or `@keyframes`. motion/react only for exit on unmount, drag, springs that follow input, `layout` and scroll-linked values. (judgement (no check))
|
||||
- In motion/react, `x`, `y`, `scale`, `rotate`, `pathLength` and every SVG target run on the main thread: never `repeat: Infinity` on them (loop with CSS `@keyframes`), and keep 20 or fewer JS-driven animations running per view. An opacity loop leaves the main thread only on an HTML element without `repeatDelay`, `repeatType: "mirror"` or `onUpdate`. (`reui/no-main-thread-loop`)
|
||||
- SVG geometry (a rect's `width`, a circle's `r`) does not reflow the page but runs in motion's JS loop: one-shot only. (`reui/no-main-thread-loop`)
|
||||
- Never set React state per frame (requestAnimationFrame, `useAnimationFrame`, `onUpdate`, scroll or pointermove listeners): bind styles to `useMotionValue`, `useTransform` or a CSS variable on the element. (`reui/no-frame-setstate`)
|
||||
- A JS-driven loop (motion `repeat`, requestAnimationFrame, canvas) pauses offscreen with `useInView` or IntersectionObserver; `content-visibility: auto` never stops script, and a setInterval loop also pauses on `visibilitychange`. (`reui/loop-offscreen`)
|
||||
- No permanent `will-change`: no `will-change-*` class or `willChange` style, except during a drag, removed when it ends. (`reui/will-change`)
|
||||
- Stagger 50ms or less per step (`staggerChildren: 0.05`), only the first 6 items, the whole entrance done in 500ms, first mount only, never on table rows or lists of 20 or more; scroll reveals use `whileInView` with `viewport={{ once: true }}`. (`reui/stagger-budget`; item count and tail: judgement (no check))
|
||||
- A height reveal (`grid-template-rows`) starts only on a click or key press, runs 200ms or less, 1 at a time; never `initial={{ height: 0 }}`. (`reui/no-layout-transition`)
|
||||
- The motion `layout` prop on 1 list at a time (about 20 elements), never in data-grid or virtualized rows; drag and drop goes through `@reui/sortable` or `@reui/kanban`. (judgement (no check))
|
||||
- AnimatePresence `mode="wait"` only with an exit of 150ms or less; `initial={false}` around content present at first render. (`reui/stagger-budget`)
|
||||
- Never hide the hero or the largest heading behind a motion/react `initial` at opacity 0 (hidden until JavaScript runs). (judgement (no check))
|
||||
- At most 3 ambient loops per view: CSS `@keyframes` on transform or opacity, a period of 2s or more, exempt from the duration table; once 3 run in a view they pause offscreen (a `content-visibility: auto` wrapper with `contain-intrinsic-size: auto <h>` where nothing paints outside it, or IntersectionObserver toggling `animation-play-state`). At most 50 animating elements per view (Skeletons, loaders and CSS opacity loops not counted). A static blur above 12px, at most 1 per view, only on a surface that neither moves nor sits over motion. (judgement (no check))
|
||||
|
||||
## Reduced motion
|
||||
|
||||
- Reduced motion is a substitute, not an off switch: replace movement (translate, scale, rotate, slide, zoom, layout, parallax, auto-scroll) with an opacity fade of 150ms or less or an instant change. Keep color and opacity feedback, focus, progress bars, loaders, the Skeleton pulse, chevrons and the switch thumb. (`reui/reduced-motion`)
|
||||
- The reduced state is the resting state: content never waits at opacity 0, hidden or offset for an animation that will not play; a count-up shows its final value. (`reui/reduced-motion`)
|
||||
- In every mode an entrance moves 8px or less and starts at scale 0.95 or more; nothing moves a surface larger than half the viewport. (`reui/reduced-motion`; half the viewport: judgement (no check))
|
||||
- `<MotionConfig reducedMotion="user">` once at the app root (its default ignores the OS) AND a `motion-reduce:` path on every CSS animation (MotionConfig never reaches CSS). An example or block wraps its own motion subtree. (`reui/reduced-motion`)
|
||||
- MotionConfig makes only transform keys and width, height, top and left instant; opacity, filter, clipPath, pathLength, color and a `transform` string keep animating, so a loop or those keys branch on `useReducedMotion()` and rest on the final value. (`reui/reduced-motion`)
|
||||
- `useReducedMotion()` is null on the server: never let it pick markup (JSX, className, `initial`) or reach `animate` under `initial={false}`; branch only animate, transition, `while*` or exit values. (`reui/reduced-motion-ssr`)
|
||||
- No SMIL (`<animate>`, `<animateMotion>`, `<animateTransform>`); canvas and requestAnimationFrame loops draw 1 resting frame under `prefers-reduced-motion: reduce`. (`reui/no-smil`, `reui/reduced-motion`)
|
||||
- Smooth scroll only under `motion-safe:scroll-smooth`. A focus ring appears at once: never `transition-all`, `transition-shadow` or a bare `transition` on it. (`reui/reduced-motion`, `reui/focus-transition`)
|
||||
- Loaders and typing dots keep their opacity pulse; every decorative loop gets `motion-reduce:animate-none`, never an inline `style={{ animation }}` (it beats the class). (`reui/reduced-motion`)
|
||||
- Anything that moves on its own for more than 5 seconds (autoplay, marquee, ticker) gets a visible Pause control, pauses on hover and focus, and never autoplays under reduced motion. (`reui/pausable-autoplay`)
|
||||
- Installed overlays keep their 100ms fade, zoom and 8px slide under reduced motion: disclosed, not edited; an app that needs WCAG 2.3.3 adds its own override. (judgement (no check))
|
||||
@@ -1,57 +0,0 @@
|
||||
# Quality gates (security, accessibility, scroll, self-checks)
|
||||
|
||||
This is the **done gate**: before you call any ReUI work finished, call `get_audit_checklist` and pass every item below (plus the bar and the states in [layout.md](./layout.md)). Then run the self-checks, typecheck and lint, compare with [anti-patterns.md](./anti-patterns.md), and hand back in the 5 lines below.
|
||||
|
||||
## 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. Hover on a display-only card: make the card a link to its detail, or give it no hover.
|
||||
- `aria-current="page"` on the current route link, `"location"` on the current in-page section link.
|
||||
- Destructive confirms use `AlertDialog`, not `Dialog`, and cannot be dismissed while pending.
|
||||
- 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 their keyboard behavior; never re-implement it.
|
||||
- Announce async UI: loading and error messages use `role="status"` or `aria-live`.
|
||||
|
||||
## 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.
|
||||
|
||||
## Self-checks
|
||||
|
||||
After every UI edit, run these with ripgrep on `<files>`: each file you wrote or changed, by path, never `components/ui` or `components/reui`. A clean check prints nothing; a hit prints `file:line:code`. The last filter drops comment lines.
|
||||
|
||||
```sh
|
||||
# C1 banned motion: transition-all, bare or paint transitions, layout keys, bounce, overshoot, scale 0, hover and tap gestures, will-change
|
||||
rg -nH 'transition-all|transition(-property)?: *[^;]*\b(all|width|height|top|left|margin|padding|box-shadow|filter)\b|[\x27\x60" ]transition( +(duration|ease|delay|hover|focus|active|group|motion)|[\x27\x60"])|transition-shadow|transition-\[[^\]]*\b(width|height|top|left|right|bottom|inset|margin|padding|filter|radius|background-position|background-size|all)\b|animate-bounce|will-change|willChange|bounce: *(0?\.0*[1-9]|[1-9])|[\x27"](backIn|backOut|backInOut|anticipate)[\x27"]|(cubic-bezier\(|ease: *\[)([^,]*,|([^,]*,){3})[ _]*(-|1\.[0-9]*[1-9]|[2-9])|scale: *0 *[,}]|\bscale-0\b|while(Hover|Tap|Focus)=|(initial|animate|exit)=\{\{[^}]*\b(width|height|maxHeight|top|left|margin|padding|boxShadow|filter|backdropFilter|borderRadius)[A-Za-z]*:' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C2 off-scale motion: a duration off 0, 100, 150, 200 and 300 (duration-500 only on a media hover zoom), ease-in or ease-out, a stagger step above 0.05
|
||||
rg -nH 'duration-([1-9]|[1-9][0-9]|1(0[1-9]|[1-46-9][0-9]|5[1-9])|[23](0[1-9]|[1-9][0-9])|[4-9][0-9]{2}|[1-9][0-9]{3,})\b|duration-\[|\b([1-9]|[1-9][0-9]|1(0[1-9]|[1-46-9][0-9]|5[1-9])|[23](0[1-9]|[1-9][0-9])|[4-9][0-9]{2}|[1-9][0-9]{3,})ms\b|(^|[^0-9.])(0?\.(0[0-9]*|1[0-46-9][0-9]*|15[0-9]+|[23][0-9]+|[4-9][0-9]*)|1\.[0-9]+)s\b|duration: *([1-9]\b|0?\.(0[0-9]*|1[0-46-9][0-9]*|15[0-9]+|[23][0-9]+|[4-9][0-9]*)\b)|\bease-(in|out|in-out)\b|(staggerChildren: *|stagger\( *)(0?\.0[6-9]|0?\.[1-9]|[1-9])|delay: *[^,}]*\b(i|index|idx)\b *\* *(0?\.0[6-9]|0?\.[1-9]|[1-9])|delay: *(0?\.0[6-9]|0?\.[1-9]|[1-9])[0-9.]* *\* *\b(i|index|idx)\b' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C2 read by hand: a duration or delay that is not a number, every index delay (allowed: 0.05 or less on the first 6 items, as Math.min(i, 5) * 0.05, never on table rows)
|
||||
rg -nH '([dD]uration|[dD]elay): *[^0-9 .]|delay: *[0-9.]+ *\*' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C3 state color: raw palette on a line that names a state
|
||||
rg -nH '\b(text|bg|border|ring|fill|stroke)-(red|rose|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet)-[0-9]{2,3}\b' <files> | rg -i ':[0-9]+:.*\b(success|paid|active|completed?|done|approved|online|healthy|live|error|failed|critical|overdue|declined|rejected|cancell?ed|blocked|expired|warning|pending|degraded|due|info|processing|running|scheduled|queued)\b' | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C4 structure: a raw table, select or fixed overlay, framer-motion, Math.random() or Date.now()
|
||||
rg -nH '<table\b|<select\b|fixed inset-0|from ["\x27]framer-motion|Math\.random\(|Date\.now\(' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
# C4 on a base-* style only (skip on radix-*): Radix asChild
|
||||
rg -nH '\basChild\b' <files> | rg -v ':[0-9]+:\s*(//|/\*|\*(\s|/|$)|\{/\*)'
|
||||
```
|
||||
|
||||
Fix every hit, run again, stop after the second run and list what is left with file and line. A C4 hit stays only when a comment in the file says why. These also stay, named on hand-back line 2: C1 on the motion.md sidebar row or on will-change set in a drag handler and removed when it ends; C2 on a media hover zoom; `Math.min(i, 5) * 0.05`.
|
||||
|
||||
No `rg` (a missing one also prints nothing): run each first pattern with your own search tool, skipping comment lines. No shell: read each changed file for what each `#` line names.
|
||||
|
||||
## Hand-back
|
||||
|
||||
End your reply with these 5 lines:
|
||||
|
||||
1. Installed: each item with its `previewUrl`.
|
||||
2. Motion: each animated element and its [motion.md](./motion.md) table row, or "none".
|
||||
3. States: covered, or which are missing.
|
||||
4. Checks: C1 to C4, screenshots or "not visually verified", the audit checklist, typecheck, lint; each one run, or "not run" and why.
|
||||
5. Deviations: any rule broken or check hit left, with file:line and why.
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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 ReUI components 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 (a full dashboard or settings page). Pro or Ultimate license at install. Its surfaces, text and borders use your theme tokens; its category accents (chart series, tags, avatars) are fixed palette colors paired for light and dark.
|
||||
- **icon** - Motion Icons in 4 styles (outline, solid, duotone, filled), static (`@reui/icons/default/<style>/<name>`), plus a hover-animated variant (`@reui/icons/animated/<style>/<name>`) only for icons that carry `installAnimated`. Ultimate license at install. See [icons.md](./icons.md).
|
||||
|
||||
## The @reui registry
|
||||
|
||||
ReUI items install through the shadcn CLI as `npx shadcn@latest add @reui/<name> --yes`, with `<name>` exactly as a ReUI tool returned it (`search`, a getter, `get_install_command`). 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.
|
||||
|
||||
## ReUI items vs shadcn/ui components
|
||||
|
||||
- **ReUI items** (the components, `c-*` examples, blocks, Motion Icons) install as `@reui/<name>`.
|
||||
- **shadcn/ui components** that ReUI builds on (`button`, `dialog`, `select`, `card`, ...) are not ReUI items. They install from shadcn by bare name: `npx shadcn@latest add button --yes`.
|
||||
- **`badge` and `alert` exist in both registries.** `@reui/badge` and `@reui/alert` are ReUI's components (the extra variants in [components.md](./components.md)); bare `badge` and `alert` are shadcn's.
|
||||
- **In an item's `registryDependencies`**, a `@reui/<name>` entry is a ReUI item and a bare name is a shadcn/ui item. `add` installs both for you.
|
||||
- **Templates are not registry items.** Download one from its page on reui.io (https://reui.io/templates?ref=skill); no `shadcn add` command installs a template.
|
||||
|
||||
## 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 take surfaces, text and borders from your theme tokens, so a theme change re-tints them; category accents stay fixed by design.
|
||||
|
||||
## Free vs premium
|
||||
|
||||
- **Free, no license key:** the ReUI components, all `c-*` examples, and this skill. The MCP is free too but still needs a free ReUI account (the agent signs in on first use); a license key is only for premium installs.
|
||||
- **Premium, license required at install:** blocks (Pro or Ultimate) and Motion Icons (Ultimate). Set `REUI_LICENSE_KEY` (see [cli.md](./cli.md)).
|
||||
- **Templates** are downloads, not installs: free ones need only a ReUI account, and each template's page on reui.io says what the others need.
|
||||
|
||||
## 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.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Theming: styles, radius, density and sizes
|
||||
|
||||
## Read your style
|
||||
|
||||
`components.json` `style` is `<base>-<style>`: the part after the first `-` names the style (`base-nova` is nova on Base UI). The style is baked into `components/ui` and `components/reui` at install, so the installed files hold your real values: read your installed `button.tsx` (usually `components/ui/button.tsx`) for heights, radius and padding before you build anything that sits beside a control.
|
||||
|
||||
## The style ladder
|
||||
|
||||
<!-- gen:style-ladder:start -->
|
||||
|
||||
| Style | Button `sm` / default / `lg` | Button radius | Card radius, shadow | Card spacing | Button text |
|
||||
| ----- | ---------------------------- | -------------- | ---------------------------------------------------- | ------------ | -------------------- |
|
||||
| vega | `h-8` / `h-9` / `h-10` | `rounded-md` | `rounded-xl`, `shadow-xs` | 6, sm 4 | `text-sm` |
|
||||
| nova | `h-7` / `h-8` / `h-9` | `rounded-lg` | `rounded-xl` | 4, sm 3 | `text-sm` |
|
||||
| maia | `h-8` / `h-9` / `h-10` | `rounded-4xl` | `rounded-2xl` | 6, sm 4 | `text-sm` |
|
||||
| lyra | `h-7` / `h-8` / `h-9` | `rounded-none` | `rounded-none` | 4, sm 3 | `text-xs` |
|
||||
| mira | `h-6` / `h-7` / `h-8` | `rounded-md` | `rounded-lg` | 4, sm 3 | `text-xs/relaxed` |
|
||||
| luma | `h-8` / `h-9` / `h-10` | `rounded-4xl` | `rounded-4xl`, `shadow-md` | 6, sm 4 | `text-sm` |
|
||||
| sera | `h-9` / `h-10` / `h-11` | `rounded-none` | square, `shadow-sm` | 8, sm 5 | `text-xs`, uppercase |
|
||||
| rhea | `h-7` / `h-8` / `h-9` | `rounded-2xl` | `rounded-[min(var(--radius-4xl),24px)]`, `shadow-sm` | 5, sm 4 | `text-sm` |
|
||||
|
||||
<!-- gen:style-ladder:end -->
|
||||
|
||||
Card spacing is the `--card-spacing` step (`--spacing(n)`) a Card pads with; `Card size="sm"` uses the sm step. The default Button text weight is `font-medium` (sera: `font-semibold`).
|
||||
|
||||
## What the style owns
|
||||
|
||||
- Never set what the style owns on its parts: radius (except `rounded-full`), height, horizontal padding, shadow, ring or font size. Pick `size` and `variant` instead.
|
||||
- A panel is a `Card` or a `Frame`, never a div with `rounded-*`, `border` and `bg-card`; set density with `Card size="sm"` or Frame `spacing`.
|
||||
- A bespoke element beside a control (a chip, a custom trigger) takes its height and radius from that control's row above.
|
||||
- A focusable control you build copies the focus classes of your installed `input.tsx`.
|
||||
- A variant before a class: `className` is for layout (placement, width, gap). `size-*` when width equals height, `cn()` for conditional classes (never a template literal), a scale step before an arbitrary value (`w-60`, not `w-[240px]`), no inline `style` for color or spacing, and no `z-index` on dialogs, sheets or popovers.
|
||||
- Change the look through the theme CSS variables in `:root` and `.dark` (`--radius`, `--primary` and the rest) or with a style switch (a new `style` in `components.json`, which means reinstalling the components; ask the user first). Never edit installed files to restyle them.
|
||||
|
||||
## Components with no per-style values
|
||||
|
||||
`event-calendar`, `gantt`, `icon-stack`, `kanban`, `rating`, `scrollspy`, `sortable` and `timeline` install the same file in every style. Their look comes from your theme tokens and the parts you put inside them (a `Card` in a kanban column follows the style), so never copy another style's radius onto them.
|
||||
@@ -1,66 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## When to reach for ReUI vs plain shadcn
|
||||
|
||||
- A full page or section (dashboard, billing, auth, pricing, settings): `compose_page` first (step 1).
|
||||
- A data table with sorting, filtering, pagination or virtualization: the `data-grid` component, never a hand-rolled `<table>`.
|
||||
- A drag-and-drop board: the `kanban` component.
|
||||
- Column filters, a date range, a tree, a stepper and the like: the matching ReUI component.
|
||||
- A single generic control shadcn already has (Button, Dialog, Select): plain shadcn.
|
||||
|
||||
## 1. Find (ReUI MCP `search` / `compose_page`)
|
||||
|
||||
**Full multi-section page ask?** Call `compose_page(intent)` FIRST (`sections`, 1 to 10, caps how many sections, default 8), before searching block-by-block. It returns ordered sections, each with the best block for the intent (top pick + alternates); on a free account it answers `mode: "free"` and fills the same sections with free `c-*` examples instead, so the plan is always buildable. 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"` (a ReUI component), `"example"` (a c-\* use-case), `"block"` (a full page/section), `"icon"`.
|
||||
- `component`: the ReUI component the request implies (`"data-grid"`, `"kanban"`, ...).
|
||||
- `category`, `free`.
|
||||
|
||||
Example: "build a users management page with filters" -> `search({ query: "users management page with filters", type: "block", component: "data-grid" })` (on a free account use `type: "example"`).
|
||||
|
||||
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.
|
||||
|
||||
**Pick with the guidance, not the title.** Look-alikes are common (dialog, sheet and drawer; dozens of data-grid examples and app shells). Before installing, read the item's guidance: `get_component` returns `whenToUse` and `whenNotToUse`, `get_example` and `get_block` return `bestFor` (what sets the item apart from its siblings), `agentNotes` and `whenNotToUse`, and a block adds `categoryGuidance` (its category's key note), and `get_examples` rows carry `bestFor`. Each `whenNotToUse` entry names the item to use instead and its `type` (`component` -> `get_component`, `example` -> `get_example`, `block` -> `get_block`, `block-category` "<group>/<category>" -> `search({ query, type: "block", category: "<category>" })`); when the user's situation matches its `when`, switch. Apply every `agentNotes` line before you ship: it names the real identifiers to replace or wire (sample data, stubbed actions, required props). An item with no guidance yet has none of these keys; fall back to its description and thumbnail.
|
||||
|
||||
Results are scoped to the account's plan, so you are never shown an item the user could not install: blocks need Pro, Motion Icons need Ultimate. A `type` hint the plan cannot use (or a query whose only matches are premium) comes back as a normal HTTP 200 answer with `locked: true` and `requiredPlan` rather than results - that is a paywall, not a missing item, so switch to free components and `c-*` examples and mention https://reui.io/pricing?ref=skill.
|
||||
|
||||
**Show the preview.** List every item with its `previewUrl` (a live preview page). Where a result carries `thumbnail` (blocks, `c-*` examples and icons), show the image as markdown linked to `previewUrl`, so the user sees it before you install.
|
||||
|
||||
## 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 deeper API, call **`get_component(names)`** with ALL of `componentsUsed` in ONE call (it accepts an array) and read each inline `api` - no web fetch. A very large API is trimmed on heading boundaries to fit your context and lists what it dropped in `sectionsOmitted`; when the part you need is in that list, re-read it as the response's `next` hint says rather than guessing. `validate_usage` always checks the FULL API, so it still catches a prop you invented from a trimmed capsule. **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. Write against your project's base (see [components.md](./components.md)). After `add`:
|
||||
|
||||
1. **Read the added files**; keep the composition intact. For a block, verify the components are wired correctly (for `data-grid`: a `useTable({ features: dataGridFeatures, ... })` instance passed as `table`, `recordCount` set - see [components.md](./components.md)).
|
||||
2. **Replace demo data with the user's real data** via typed structures (see [adapting.md](./adapting.md)).
|
||||
3. **Fix icon imports** to the project's icon library (see [icons.md](./icons.md)).
|
||||
4. **Align styling** to the active theme: semantic tokens for anything that carries meaning, raw palette only for categorical identity and always with its `dark:` pair (see [color.md](./color.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, motion that leaves installed components alone, and mobile-first responsiveness (see [layout.md](./layout.md) and [motion.md](./motion.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.
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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; Pro unlocks premium blocks, Ultimate adds Motion Icons, and either removes the daily request limit by default. 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 (`sections` is a number, 1 to 10: the most sections to return, default 8); it returns ordered sections, each with the best premium block for the intent (top pick + alternates). On a free account (or in free-only mode) it answers with `mode: "free"` and composes the same sections from free `c-*` examples instead, so the plan is always buildable. 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. Only some icons have a real hover animation: `animated: true` returns only those, and a concept with none comes back empty with an `animatedNote`. Each icon carries a `thumbnail` of its 4 styles; look at it before trusting a name. An old icon name still resolves (`renamedFrom` names it), unless it is also an ordinary word icons are named for today (`watch`): then it ranks today's icons for the word and `oldNameNotes` names the icon the old name installs. Motion Icons are Ultimate for **discovery as well as install**: without an Ultimate credential this returns `locked: true` and zero concepts, so check the plan before promising the user icons.
|
||||
- **`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.
|
||||
- **`get_thumbnail`** - when the choice is VISUAL and the ranking has not already made it: 2 or more candidates within about 15 score points. When the top hit leads by a wide margin, or `missedTerms` already rules the others out, skip it; an image costs roughly 10 times a result row. It returns the hosted preview image as image content so you can look yourself; pass up to 4 names in one call to compare `compose_page` alternates side by side before installing any of them. To let the USER see something, share the item's `thumbnail` and `previewUrl` instead - this tool is for your own eyes, and a client that cannot render images gets the URLs in the text block.
|
||||
- **`get_agent_skill`** - when the agent has no local ReUI skill (a cloud or tools-only client), or to check whether the installed one is stale. With no `topic` it returns the core workflow, the topic index and the installer; `topic` (for example `motion`) returns that one file in full. Compare its `version` with the one stamped atop your local `SKILL.md`.
|
||||
|
||||
## All 19 tools
|
||||
|
||||
`search`, `get_block`, `get_example`, `get_icon`, `get_thumbnail`, `list_block_groups`, `list_block_categories`, `list_example_categories`, `list_icon_categories`, `list_components`, `get_component`, `get_examples`, `search_icons`, `compose_page`, `validate_usage`, `get_install_command`, `get_project_context`, `get_agent_skill`, `get_audit_checklist`. The MCP serves the full parameter schemas; do not guess parameters beyond them.
|
||||
|
||||
## Commands (MCP prompts)
|
||||
|
||||
In Claude Code the ReUI MCP adds these prompts; other agents show MCP prompts under their own names, or not at all. Plain language runs the same loop.
|
||||
|
||||
- `/mcp__reui__build <what>`: compose a page, section or feature (plan, install, read the API, adapt, audit).
|
||||
- `/mcp__reui__add <item>`: find, install and wire in 1 component, example, block or icon.
|
||||
- `/mcp__reui__fix [target]`: fix ReUI usage (undocumented props, a base or radix mismatch, missing states, accessibility, scroll).
|
||||
- `/mcp__reui__improve [target]`: refine existing ReUI UI (hierarchy, density, states, responsive, motion).
|
||||
|
||||
## 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`, `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.
|
||||
- An inline `api` is not always the whole API: a very large one is trimmed on heading boundaries to fit your context, and the response names every dropped heading in `sectionsOmitted` plus a `next` hint for re-reading one. Never treat a trimmed capsule as complete; `validate_usage` still checks the FULL API.
|
||||
- Icon results and `get_icon` include `animated: true` and `installAnimated` only when the icon has a real hover animation (animated: `@reui/icons/animated/<style>/<name>`; static: `@reui/icons/default/<style>/<name>`). Use the strings given; never build an animated path by hand, it installs a static copy for every other icon.
|
||||
- Blocks, `c-*` examples and icons carry `thumbnail`, an absolute URL to a hosted preview image (light theme; blocks 1080x720, examples 900x600, icons 640x200 with the 4 styles side by side). Show the top 2 or 3 as markdown images linked to `previewUrl` so the user picks by eye; use `get_thumbnail` when YOU need to see them. Components have no thumbnail.
|
||||
- Each result carries `missedTerms`, the words from YOUR query it does not mention (omitted when it matches them all). A high score with 3 missed words is a near miss wearing a good score: read this before `get_thumbnail`, it is far cheaper.
|
||||
- `weakMatch: true` means the top result misses more of the query's words than it matches; `unmatchedTerms` lists the words none of the top results mention and `weakMatchNote` says what to do. Stop and re-search with the registry's own words, or compose from components, instead of installing the top hit.
|
||||
- On a free account `premiumPicks` (a sibling of `results`, never inside it) lists up to 3 premium blocks that fit the query more closely than the free answer, as previews with no install command, plus `unlock` with the plan and a pricing link. Mention the upgrade at most once per conversation, then keep building with what the plan covers. `weakMatch` and `premiumPicks` can appear together: the free answer is weak AND a premium block fits; `weakMatchNote` says so.
|
||||
- Guidance is getter-only, never on search rows: `get_component` adds `whenToUse`, `whenNotToUse` and `agentNotes`; `get_example` and `get_block` add `bestFor`, `agentNotes` and `whenNotToUse`, and `get_block` also `categoryGuidance`; `get_examples` rows add `bestFor`. Answers carry the key lines; the item page's `.md` twin has every line. Each `whenNotToUse` entry is `{ when, instead, type }`: fetch `instead` with the getter its `type` names.
|
||||
- Results are scoped to the caller's plan, so a free account never sees a block or icon it could not install. When a search matches only hidden premium items, the answer says so (`locked`, `requiredPlan`, `premiumMatches`) instead of pretending the registry is empty.
|
||||
|
||||
## Error playbook
|
||||
|
||||
- **401** - the MCP requires a signed-in ReUI account, on every request (there is no anonymous access). 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?ref=skill) as `Authorization: Bearer`.
|
||||
- **locked result** - a valid account whose plan does not cover the item. This is NOT an error and NOT a 403: it comes back as a normal HTTP 200 result carrying `locked: true` and `requiredPlan` (`"pro"` for premium blocks, `"ultimate"` for Motion Icons). Keep working with the free components and `c-*` examples, and point the user at https://reui.io/pricing?ref=skill if they want the locked item.
|
||||
- **daily allowance reached** - a free account has a per-account, per-UTC-day allowance on tool calls (ReUI sets the number and can change it, so read it from the message and never assume one). It comes back as a normal tool RESULT with `isError: true` at HTTP 200: not an HTTP error status, no back-off header to honor, and no per-minute limit to wait out. Retrying the same call just spends the next unit, so surface the message (it carries the reset and the upgrade link) to the user instead of looping. The count resets at UTC midnight, and using a Pro or Ultimate license as the MCP credential removes the limit.
|
||||
- **not found** (`found: false`) - use the returned `suggestions`, or `search`. 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?ref=skill
|
||||
@@ -42,6 +42,7 @@ import { oidcRoutes } from './routes/oidc.js'
|
||||
import { webauthnRoutes } from './routes/webauthn.js'
|
||||
import { startAuditRetentionJob } from './services/audit-retention.js'
|
||||
import { startHousekeepingJob } from './services/housekeeping.js'
|
||||
import { configurePortalRevocationSender } from './services/portal-revocations.js'
|
||||
import { startBackupJob } from './services/backup.js'
|
||||
import {
|
||||
ensureOidcSigningKey,
|
||||
@@ -141,6 +142,8 @@ export async function buildApp(opts: {
|
||||
app.decorate('config', config)
|
||||
app.decorate('db', db)
|
||||
app.decorate('sqlite', sqlite)
|
||||
// Configure before bootstrap/access writes; failed opt-in must close this owned handle.
|
||||
try { configurePortalRevocationSender(app) } catch (error) { closeDb(sqlite); throw error }
|
||||
app.addHook('onClose', async () => {
|
||||
closeDb(sqlite)
|
||||
})
|
||||
|
||||
@@ -55,6 +55,9 @@ export const configSchema = z.object({
|
||||
shutdownTimeoutMs: z.coerce.number().int().positive().default(8000),
|
||||
isProd: z.boolean(),
|
||||
auditIngestSecret: z.string().min(8).optional(),
|
||||
portalRevocationsEnabled: z.boolean().default(false),
|
||||
portalRevocationsTarget: z.string().optional(),
|
||||
portalRevocationsSecret: z.string().optional(),
|
||||
webauthnRpID: z.string().min(1),
|
||||
webauthnRpName: z.string().min(1).default('Auth Portal'),
|
||||
webauthnOrigins: z.array(z.string().url()).min(1),
|
||||
@@ -134,6 +137,9 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||
boolFromEnv(env.NODE_ENV === 'production' ? 'true' : undefined, false) ||
|
||||
isProd,
|
||||
auditIngestSecret,
|
||||
portalRevocationsEnabled: boolFromEnv(env.MCP_REVOCATIONS_ENABLED, false),
|
||||
portalRevocationsTarget: env.MCP_REVOCATIONS_TARGET || undefined,
|
||||
portalRevocationsSecret: env.MCP_REVOCATIONS_SECRET || undefined,
|
||||
webauthnRpID: rpID,
|
||||
webauthnRpName: env.WEBAUTHN_RP_NAME || 'Auth Portal',
|
||||
webauthnOrigins: origins,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { createHmac } from 'node:crypto'
|
||||
import https from 'node:https'
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { completePortalRevocation, enablePortalRevocations, nextPortalRevocation } from '@authportal/db'
|
||||
|
||||
export function portalRevocationTarget(raw: string): URL {
|
||||
if (raw.length > 512) throw new Error('PORTAL_REVOCATION_TARGET_INVALID')
|
||||
let url: URL
|
||||
try { url = new URL(raw) } catch { throw new Error('PORTAL_REVOCATION_TARGET_INVALID') }
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash
|
||||
|| url.pathname !== '/_internal/mcp/portal/revocations' || url.href !== raw) throw new Error('PORTAL_REVOCATION_TARGET_INVALID')
|
||||
return url
|
||||
}
|
||||
/** One fixed operator-selected HTTPS destination. Core https never follows redirects. */
|
||||
export function sendPortalRevocation(target: URL, secret: string, body: string, signal: AbortSignal): Promise<boolean> {
|
||||
if (Buffer.byteLength(body) > 4096) return Promise.resolve(false)
|
||||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||||
const signature = 'sha256=' + createHmac('sha256', secret).update(timestamp + '.').update(body).digest('hex')
|
||||
return new Promise(resolve => {
|
||||
let settled = false
|
||||
let outcome = false
|
||||
const finish = (accepted: boolean) => {
|
||||
if (!settled) { settled = true; outcome = accepted; clearTimeout(deadline); request.destroy() }
|
||||
}
|
||||
const request = https.request(target, { method: 'POST', agent: false, rejectUnauthorized: true, signal, headers: {
|
||||
'content-type': 'application/json', 'content-length': Buffer.byteLength(body),
|
||||
'x-mm-timestamp': timestamp, 'x-mm-signature': signature,
|
||||
} }, response => {
|
||||
const chunks: Buffer[] = []; let size = 0
|
||||
response.on('data', (chunk: Buffer) => {
|
||||
size += chunk.length
|
||||
if (size > 4096) { response.destroy(); request.destroy(); finish(false) }
|
||||
else chunks.push(chunk)
|
||||
})
|
||||
response.once('error', () => finish(false))
|
||||
response.once('aborted', () => finish(false))
|
||||
response.once('end', () => {
|
||||
let accepted = false
|
||||
try {
|
||||
const value: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
||||
accepted = response.statusCode === 200 && !!value && typeof value === 'object' && !Array.isArray(value)
|
||||
&& Object.keys(value).sort().join(',') === 'duplicate,ok' && (value as {ok?: unknown}).ok === true
|
||||
&& typeof (value as {duplicate?: unknown}).duplicate === 'boolean'
|
||||
} catch { /* No raw transport/response/error material is logged. */ }
|
||||
finish(accepted)
|
||||
})
|
||||
})
|
||||
const deadline = setTimeout(() => { request.destroy(); finish(false) }, 3000)
|
||||
request.once('error', () => finish(false))
|
||||
request.once('close', () => { clearTimeout(deadline); resolve(outcome) })
|
||||
request.end(body)
|
||||
})
|
||||
}
|
||||
|
||||
export function configurePortalRevocationSender(app: FastifyInstance): void {
|
||||
if (!app.config.portalRevocationsEnabled) return
|
||||
const target = portalRevocationTarget(app.config.portalRevocationsTarget ?? '')
|
||||
const secret = app.config.portalRevocationsSecret
|
||||
if (!secret || secret.length < 32 || Buffer.byteLength(secret) > 1024) throw new Error('PORTAL_REVOCATION_SECRET_REQUIRED')
|
||||
enablePortalRevocations(app.db, target.href)
|
||||
let stopped = false, running: Promise<void> | undefined
|
||||
const abort = new AbortController()
|
||||
const run = async () => {
|
||||
// Bounded work per tick, single in-flight request; rows survive all failures/restarts.
|
||||
for (let i = 0; i < 4 && !stopped; i++) {
|
||||
const event = nextPortalRevocation(app.db)
|
||||
if (!event) return
|
||||
const delivered = await sendPortalRevocation(target, secret, event.body, abort.signal)
|
||||
completePortalRevocation(app.db, event, delivered)
|
||||
if (!delivered) return
|
||||
}
|
||||
}
|
||||
const tick = () => {
|
||||
if (stopped || running) return
|
||||
running = run().catch(() => { app.log.error({event: 'portal.revocation.delivery-unavailable'}, 'Revocation delivery unavailable') })
|
||||
.finally(() => { running = undefined })
|
||||
}
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
app.addHook('onReady', async () => { interval = setInterval(tick, 1000); interval.unref(); tick() })
|
||||
// preClose executes before onClose closes SQLite. Abort then drain exact in-flight work.
|
||||
app.addHook('preClose', async () => { stopped = true; clearInterval(interval); abort.abort(); await running })
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
APP_IDS,
|
||||
APPS,
|
||||
PERMISSION_CATALOG,
|
||||
PERMISSION_ACTION_LABELS,
|
||||
isIndependentPermissionSection,
|
||||
withIndependentPermission,
|
||||
permissionKey,
|
||||
type AdminUser,
|
||||
type AppId,
|
||||
@@ -413,6 +416,24 @@ export function UserAccessSheet({
|
||||
<ItemDescription className="text-xs">
|
||||
{section.description}
|
||||
</ItemDescription>
|
||||
{isIndependentPermissionSection(app.appId, section.id) ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{section.actions.map((action) => {
|
||||
const key = permissionKey(app.appId, section.id, action)
|
||||
return (
|
||||
<label key={action} className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm">{PERMISSION_ACTION_LABELS[action]}</span>
|
||||
<Switch
|
||||
checked={permissionSet.has(key)}
|
||||
disabled={!apps.includes(app.appId)}
|
||||
aria-label={`${section.title}: ${PERMISSION_ACTION_LABELS[action]}`}
|
||||
onCheckedChange={(enabled) => setPermissions((prev) => withIndependentPermission(prev, app.appId, section.id, action, enabled))}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<ButtonGroup className="mt-0.5 flex-wrap">
|
||||
{levels.map((lvl) => (
|
||||
<Button
|
||||
@@ -436,6 +457,7 @@ export function UserAccessSheet({
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</ItemContent>
|
||||
</Item>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Field, FieldDescription, FieldLabel } from '@authportal/ui/components/field'
|
||||
import { APPS, PERMISSION_CATALOG, type AdminUser, type AppId } from '@authportal/shared'
|
||||
import { APPS, PERMISSION_CATALOG, PERMISSION_ACTION_LABELS, isIndependentPermissionSection, withIndependentPermission, permissionKey, type AdminUser, type AppId } from '@authportal/shared'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -82,10 +82,25 @@ export function AccessEditor({
|
||||
<div className="flex flex-col gap-4">
|
||||
{catalog.sections.map((section) => {
|
||||
const level = levelOf(value.permissions, appId, section.id, section.actions)
|
||||
const options: Level[] = ['none', ...section.actions]
|
||||
const options: Level[] = ['none', ...section.actions.filter((action): action is Exclude<Level, 'none'> => action === 'read' || action === 'write' || action === 'admin')]
|
||||
return (
|
||||
<Field key={section.id}>
|
||||
<FieldLabel>{section.title}</FieldLabel>
|
||||
{isIndependentPermissionSection(appId, section.id) ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{section.actions.map((action) => (
|
||||
<label key={action} className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm">{PERMISSION_ACTION_LABELS[action]}</span>
|
||||
<Switch
|
||||
checked={value.permissions.includes(permissionKey(appId, section.id, action))}
|
||||
disabled={!value.apps.includes(appId)}
|
||||
aria-label={`${section.title}: ${PERMISSION_ACTION_LABELS[action]}`}
|
||||
onCheckedChange={(enabled) => onChange({ ...value, permissions: withIndependentPermission(value.permissions, appId, section.id, action, enabled) })}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={level}
|
||||
onValueChange={(v) =>
|
||||
@@ -106,7 +121,8 @@ export function AccessEditor({
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>{level === 'none' ? 'Раздел скрыт в приложении' : 'Иерархия: админ ⊃ запись ⊃ чтение'}</FieldDescription>
|
||||
)}
|
||||
<FieldDescription>{isIndependentPermissionSection(appId, section.id) ? 'Администрирование включает чтение; одобрение и пауза отдельны. Включение приложения не выдаёт права MCP.' : level === 'none' ? 'Раздел скрыт в приложении' : 'Иерархия: админ ⊃ запись ⊃ чтение'}</FieldDescription>
|
||||
</Field>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
PERMISSION_CATALOG,
|
||||
isIndependentPermissionSection,
|
||||
hasPermission,
|
||||
permissionKey,
|
||||
type AppId,
|
||||
@@ -54,7 +55,7 @@ export function keysForLevel(appId: AppId, sectionId: string, actions: readonly
|
||||
export function grantCount(perms: readonly string[], appId: AppId): { granted: number; total: number } {
|
||||
const catalog = PERMISSION_CATALOG.find((c) => c.appId === appId)
|
||||
const sections = catalog?.sections ?? []
|
||||
const granted = sections.filter((s) => levelOf(perms, appId, s.id, s.actions) !== 'none').length
|
||||
const granted = sections.filter((s) => isIndependentPermissionSection(appId, s.id) ? s.actions.some(action => perms.includes(permissionKey(appId, s.id, action))) : levelOf(perms, appId, s.id, s.actions) !== 'none').length
|
||||
return { granted, total: sections.length }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# MCP revocation delivery (default off)
|
||||
|
||||
This capability is not enabled by adding the code. Deploy and verify the MM v2
|
||||
receiver first, using the existing separate `MCP_PORTAL_WEBHOOK_SECRET`. Then
|
||||
explicitly configure portal:
|
||||
|
||||
- `MCP_REVOCATIONS_ENABLED=true`
|
||||
- `MCP_REVOCATIONS_TARGET=https://<fixed-mm-host>/_internal/mcp/portal/revocations`
|
||||
- `MCP_REVOCATIONS_SECRET`: the same separate random secret, at least32 characters.
|
||||
|
||||
Never reuse JWT or audit-ingest secrets. Do not put credentials/query/fragment in
|
||||
the target. Only canonical HTTPS is accepted, with certificate verification,
|
||||
no redirects and a3-second whole request deadline; request/response limit4KiB.
|
||||
The target fingerprint is pinned in SQLite; changing destination refuses startup
|
||||
until an explicit operator-reviewed migration. Rotation of the shared secret
|
||||
does not change queued event bodies or IDs.
|
||||
|
||||
Portal SQLite migration2 adds an outbox without a user FK. When enabled, SQLite
|
||||
synchronous=FULL is required before mutations. Actual access/admin/disable/delete,
|
||||
logout, single-session and revoke-all functions enqueue in their same synchronous
|
||||
transaction. Enqueue failure/cap4096 rolls back the mutation. Queue-full prevents
|
||||
further relevant security mutations until delivery recovers; rows are never
|
||||
silently expired/discarded. Default-off neither captures new events nor sends;
|
||||
it does not promise revocation while disabled. Existing pending rows remain.
|
||||
|
||||
Delivery is at least once, FIFO, one request at a time, at most4 events per tick;
|
||||
retry delay grows to60seconds. A valid200 `{ok:true,duplicate:boolean}` ack removes
|
||||
the exact row/body. Crash after MM commit but before ack causes safe redelivery
|
||||
with the same eventId/body. Sender shutdown aborts and awaits request close before
|
||||
SQLite closes. Logs contain only a fixed unavailable event, never secret/body/cause.
|
||||
|
||||
V2 signs `timestamp + "." + exactQueuedBody` with HMAC-SHA256. `x-mm-timestamp`
|
||||
is fresh delivery time, while body `eventOccurredAt` is the immutable original
|
||||
mutation time. Re-signing/retrying never advances the owner cutoff. MM applies
|
||||
monotonic owner cutoffs and connection/credential/grant/plan/profile effects only
|
||||
to claims issued at or before that event. Legacy three-field events remain
|
||||
accepted with their historical signed-timestamp cutoff; this sender always usesv2.
|
||||
|
||||
Legacy JWT iat has second resolution. A JWT minted in the event's same second can
|
||||
remain rejected; obtain a fresh login after the natural second boundary. No future
|
||||
iat, artificial millisecond JWT precision or claims-version authority is added.
|
||||
Logout/single-session revoke is owner-wide for MCP because current portal access
|
||||
JWTs have no session identifier. Password-only/name/email edits retain their
|
||||
existing semantics; this capability does not invent a new password/session policy.
|
||||
|
||||
Before enabling: use portal's locked dependencies for targeted DB tests and API/DB
|
||||
typecheck, then verify actual HTTPS and MM PostgreSQL behavior (old JWT rejected,
|
||||
fresh JWT after boundary preserved by late/out-of-order events, replay/conflict,
|
||||
outage/restart/drain). Source-only and mock-network tests are not that acceptance.
|
||||
SQLite backup restore is an explicit operator security operation; an outbox cannot
|
||||
recover mutations deliberately removed by restoring an older database snapshot.
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"reui": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.reui.io/api/mcp",
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"Authorization": "Bearer {env:REUI_LICENSE_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
||||
import * as schema from './schema/index.js'
|
||||
import { migratePortalRevocations } from './portal-revocations.js'
|
||||
|
||||
export type Sqlite = Database.Database
|
||||
export type AppDb = ReturnType<typeof drizzle<typeof schema>>
|
||||
@@ -38,14 +39,14 @@ export function createMemoryDb(): { db: AppDb; sqlite: Sqlite } {
|
||||
/** Версия схемы в PRAGMA user_version; при совпадении migrateSchema ничего не делает. */
|
||||
// ВАЖНО: любое изменение baseMigrate()/migrateV1() требует увеличить SCHEMA_VERSION
|
||||
// (+ новая migrateVN), иначе существующие БД со скипом по user_version его не применят.
|
||||
export const SCHEMA_VERSION = 1
|
||||
export const SCHEMA_VERSION = 2
|
||||
|
||||
export function migrateSchema(sqlite: Sqlite): void {
|
||||
const current = sqlite.pragma('user_version', { simple: true }) as number
|
||||
if (current >= SCHEMA_VERSION) return
|
||||
sqlite.transaction(() => {
|
||||
baseMigrate(sqlite)
|
||||
migrateV1(sqlite)
|
||||
if (current < 1) { baseMigrate(sqlite); migrateV1(sqlite) }
|
||||
if (current < 2) migratePortalRevocations(sqlite)
|
||||
sqlite.pragma(`user_version = ${SCHEMA_VERSION}`)
|
||||
})()
|
||||
}
|
||||
@@ -280,3 +281,5 @@ export * from './settings.js'
|
||||
export * from './audit-log.js'
|
||||
export * from './oidc.js'
|
||||
export * from './webauthn.js'
|
||||
|
||||
export * from './portal-revocations.js'
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import type { AppDb, Sqlite } from './index.js'
|
||||
|
||||
const enabled = new WeakSet<AppDb>()
|
||||
export type PortalRevocationType = 'permissions.changed' | 'user.disabled' | 'sessions.revoked'
|
||||
export const portalRevocationLimit = 4096
|
||||
|
||||
export function migratePortalRevocations(sqlite: Sqlite): void {
|
||||
sqlite.exec(`
|
||||
CREATE TABLE portal_revocation_target (id INTEGER PRIMARY KEY CHECK(id=1), target_sha256 TEXT NOT NULL);
|
||||
CREATE TABLE portal_revocation_outbox (
|
||||
sequence INTEGER PRIMARY KEY AUTOINCREMENT, event_id TEXT NOT NULL UNIQUE,
|
||||
body TEXT NOT NULL CHECK(length(body)<=4096), occurred_at INTEGER NOT NULL,
|
||||
attempts INTEGER NOT NULL DEFAULT 0, next_attempt_at INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
||||
/** Explicit startup opt-in. FULL precedes all enabled mutations, never an async hook. */
|
||||
export function enablePortalRevocations(db: AppDb, target: string): void {
|
||||
db.$client.pragma('synchronous = FULL')
|
||||
if (db.$client.pragma('synchronous', { simple: true }) !== 2) throw new Error('PORTAL_REVOCATION_DURABILITY_UNAVAILABLE')
|
||||
const fingerprint = createHash('sha256').update(target).digest('hex')
|
||||
db.$client.transaction(() => {
|
||||
const existing = db.$client.prepare('SELECT target_sha256 FROM portal_revocation_target WHERE id=1').get() as { target_sha256: string } | undefined
|
||||
if (existing && existing.target_sha256 !== fingerprint) throw new Error('PORTAL_REVOCATION_TARGET_CHANGED')
|
||||
if (!existing) db.$client.prepare('INSERT INTO portal_revocation_target(id,target_sha256) VALUES(1,?)').run(fingerprint)
|
||||
})()
|
||||
enabled.add(db)
|
||||
}
|
||||
|
||||
/** Called INSIDE the same synchronous transaction as the genuine user mutation. */
|
||||
export function enqueuePortalRevocation(db: AppDb, userId: string, type: PortalRevocationType): void {
|
||||
if (!enabled.has(db)) return
|
||||
if (!db.$client.inTransaction) throw new Error('PORTAL_REVOCATION_TRANSACTION_REQUIRED')
|
||||
const count = db.$client.prepare('SELECT count(*) AS count FROM portal_revocation_outbox').get() as { count: number }
|
||||
if (count.count >= portalRevocationLimit) throw new Error('PORTAL_REVOCATION_OUTBOX_FULL')
|
||||
const eventOccurredAt = Date.now(), eventId = randomUUID()
|
||||
const body = JSON.stringify({ version: 2, eventId, userId, type, eventOccurredAt })
|
||||
if (!Number.isSafeInteger(eventOccurredAt) || eventOccurredAt <= 0 || Buffer.byteLength(body) > 4096) throw new Error('PORTAL_REVOCATION_EVENT_INVALID')
|
||||
// No user FK: a genuine user deletion must not cascade-delete its own revocation.
|
||||
db.$client.prepare('INSERT INTO portal_revocation_outbox(event_id,body,occurred_at) VALUES(?,?,?)').run(eventId, body, eventOccurredAt)
|
||||
}
|
||||
|
||||
export type PortalRevocationDelivery = { sequence: number; body: string; attempts: number }
|
||||
export function nextPortalRevocation(db: AppDb): PortalRevocationDelivery | undefined {
|
||||
// Stable FIFO; a failed oldest event cannot be starved by subsequent mutations.
|
||||
const row = db.$client.prepare('SELECT sequence,body,attempts,next_attempt_at FROM portal_revocation_outbox ORDER BY sequence LIMIT 1').get() as (PortalRevocationDelivery & { next_attempt_at: number }) | undefined
|
||||
return row && row.next_attempt_at <= Date.now() ? row : undefined
|
||||
}
|
||||
export function completePortalRevocation(db: AppDb, event: PortalRevocationDelivery, delivered: boolean): void {
|
||||
if (delivered) db.$client.prepare('DELETE FROM portal_revocation_outbox WHERE sequence=? AND body=?').run(event.sequence, event.body)
|
||||
else db.$client.prepare('UPDATE portal_revocation_outbox SET attempts=min(attempts+1,30),next_attempt_at=? WHERE sequence=? AND body=?')
|
||||
.run(Date.now() + Math.min(60000, 1000 * 2 ** Math.min(event.attempts, 6)), event.sequence, event.body)
|
||||
}
|
||||
+70
-49
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto'
|
||||
import type { AppDb } from './index.js'
|
||||
import { bumpUserDataGeneration } from './generation.js'
|
||||
import { withTransaction } from './tx.js'
|
||||
import { enqueuePortalRevocation } from './portal-revocations.js'
|
||||
import {
|
||||
refreshSessions,
|
||||
userApps,
|
||||
@@ -158,28 +159,35 @@ export function updateUser(
|
||||
disabled?: boolean
|
||||
},
|
||||
): UserRow | undefined {
|
||||
const existing = getUserById(db, id)
|
||||
if (!existing) return undefined
|
||||
const now = new Date().toISOString()
|
||||
db.update(users)
|
||||
.set({
|
||||
email: patch.email?.toLowerCase() ?? existing.email,
|
||||
name: patch.name ?? existing.name,
|
||||
passwordHash: patch.passwordHash ?? existing.passwordHash,
|
||||
isAdmin: patch.isAdmin ?? existing.isAdmin,
|
||||
disabled: patch.disabled ?? existing.disabled,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
.run()
|
||||
bumpUserDataGeneration()
|
||||
return getUserById(db, id)
|
||||
return withTransaction(db, () => {
|
||||
const existing = getUserById(db, id)
|
||||
if (!existing) return undefined
|
||||
const now = new Date().toISOString()
|
||||
db.update(users)
|
||||
.set({
|
||||
email: patch.email?.toLowerCase() ?? existing.email,
|
||||
name: patch.name ?? existing.name,
|
||||
passwordHash: patch.passwordHash ?? existing.passwordHash,
|
||||
isAdmin: patch.isAdmin ?? existing.isAdmin,
|
||||
disabled: patch.disabled ?? existing.disabled,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(users.id, id))
|
||||
.run()
|
||||
bumpUserDataGeneration()
|
||||
if (patch.disabled === true && !existing.disabled) enqueuePortalRevocation(db, id, 'user.disabled')
|
||||
else if (patch.isAdmin !== undefined && patch.isAdmin !== existing.isAdmin) enqueuePortalRevocation(db, id, 'permissions.changed')
|
||||
return getUserById(db, id)
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUser(db: AppDb, id: string): boolean {
|
||||
const result = db.delete(users).where(eq(users.id, id)).run()
|
||||
bumpUserDataGeneration()
|
||||
return result.changes > 0
|
||||
return withTransaction(db, () => {
|
||||
const result = db.delete(users).where(eq(users.id, id)).run()
|
||||
if (result.changes > 0) enqueuePortalRevocation(db, id, 'user.disabled')
|
||||
bumpUserDataGeneration()
|
||||
return result.changes > 0
|
||||
})
|
||||
}
|
||||
|
||||
export function setUserAccess(
|
||||
@@ -191,6 +199,8 @@ export function setUserAccess(
|
||||
const uniqApps = [...new Set(apps)]
|
||||
const uniqPerms = [...new Set(permissions)]
|
||||
withTransaction(db, () => {
|
||||
const changed = JSON.stringify(getUserApps(db, userId).sort()) !== JSON.stringify([...uniqApps].sort())
|
||||
|| JSON.stringify(getUserPermissions(db, userId).sort()) !== JSON.stringify([...uniqPerms].sort())
|
||||
db.delete(userApps).where(eq(userApps.userId, userId)).run()
|
||||
db.delete(userPermissions).where(eq(userPermissions.userId, userId)).run()
|
||||
if (uniqApps.length > 0) {
|
||||
@@ -207,6 +217,7 @@ export function setUserAccess(
|
||||
.set({ updatedAt: new Date().toISOString() })
|
||||
.where(eq(users.id, userId))
|
||||
.run()
|
||||
if (changed) enqueuePortalRevocation(db, userId, 'permissions.changed')
|
||||
})
|
||||
bumpUserDataGeneration()
|
||||
}
|
||||
@@ -279,11 +290,15 @@ export function createRefreshSession(
|
||||
}
|
||||
|
||||
export function revokeRefreshSession(db: AppDb, rawToken: string): void {
|
||||
const now = new Date().toISOString()
|
||||
db.update(refreshSessions)
|
||||
.set({ revokedAt: now })
|
||||
.where(eq(refreshSessions.tokenHash, hashToken(rawToken)))
|
||||
.run()
|
||||
withTransaction(db, () => {
|
||||
const session = db.select().from(refreshSessions).where(eq(refreshSessions.tokenHash, hashToken(rawToken))).get()
|
||||
const now = new Date().toISOString()
|
||||
db.update(refreshSessions)
|
||||
.set({ revokedAt: now })
|
||||
.where(eq(refreshSessions.tokenHash, hashToken(rawToken)))
|
||||
.run()
|
||||
if (session && !session.revokedAt) enqueuePortalRevocation(db, session.userId, 'sessions.revoked')
|
||||
})
|
||||
}
|
||||
|
||||
/** Active refresh session → user (for OIDC authorize cookie). */
|
||||
@@ -343,32 +358,38 @@ export function listActiveSessions(
|
||||
}
|
||||
|
||||
export function revokeSessionById(db: AppDb, sessionId: string): boolean {
|
||||
const row = db
|
||||
.select()
|
||||
.from(refreshSessions)
|
||||
.where(eq(refreshSessions.id, sessionId))
|
||||
.get()
|
||||
if (!row || row.revokedAt) return false
|
||||
db.update(refreshSessions)
|
||||
.set({ revokedAt: new Date().toISOString() })
|
||||
.where(eq(refreshSessions.id, sessionId))
|
||||
.run()
|
||||
return true
|
||||
return withTransaction(db, () => {
|
||||
const row = db
|
||||
.select()
|
||||
.from(refreshSessions)
|
||||
.where(eq(refreshSessions.id, sessionId))
|
||||
.get()
|
||||
if (!row || row.revokedAt) return false
|
||||
db.update(refreshSessions)
|
||||
.set({ revokedAt: new Date().toISOString() })
|
||||
.where(eq(refreshSessions.id, sessionId))
|
||||
.run()
|
||||
enqueuePortalRevocation(db, row.userId, 'sessions.revoked')
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function revokeAllSessionsForUser(db: AppDb, userId: string): number {
|
||||
const now = new Date().toISOString()
|
||||
// Активная сессия = не отозвана и не истекла; один UPDATE вместо цикла.
|
||||
const result = db
|
||||
.update(refreshSessions)
|
||||
.set({ revokedAt: now })
|
||||
.where(
|
||||
and(
|
||||
eq(refreshSessions.userId, userId),
|
||||
isNull(refreshSessions.revokedAt),
|
||||
gt(refreshSessions.expiresAt, now),
|
||||
),
|
||||
)
|
||||
.run()
|
||||
return result.changes
|
||||
return withTransaction(db, () => {
|
||||
const now = new Date().toISOString()
|
||||
// Активная сессия = не отозвана и не истекла; один UPDATE вместо цикла.
|
||||
const result = db
|
||||
.update(refreshSessions)
|
||||
.set({ revokedAt: now })
|
||||
.where(
|
||||
and(
|
||||
eq(refreshSessions.userId, userId),
|
||||
isNull(refreshSessions.revokedAt),
|
||||
gt(refreshSessions.expiresAt, now),
|
||||
),
|
||||
)
|
||||
.run()
|
||||
if (getUserById(db, userId)) enqueuePortalRevocation(db, userId, 'sessions.revoked')
|
||||
return result.changes
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createMemoryDb, migrateSchema, closeDb, createUser, getUserById,
|
||||
getUserPermissions, setUserAccess, updateUser, deleteUser,
|
||||
createRefreshSession, revokeRefreshSession, revokeSessionById, revokeAllSessionsForUser,
|
||||
enablePortalRevocations,
|
||||
} from '../src/index.js'
|
||||
|
||||
const target = 'https://mm.example.invalid/_internal/mcp/portal/revocations'
|
||||
describe('integrated portal revocation outbox', () => {
|
||||
it('opts in explicitly and retains deletion event independently of user FK', () => {
|
||||
const { db, sqlite } = createMemoryDb(); migrateSchema(sqlite)
|
||||
try {
|
||||
const user = createUser(db, {email:'[email protected]',name:'owned',passwordHash:'synthetic',isAdmin:false})
|
||||
setUserAccess(db,user.id,['mm'],['old'])
|
||||
expect(sqlite.prepare('SELECT count(*) AS n FROM portal_revocation_outbox').get()).toEqual({n:0})
|
||||
enablePortalRevocations(db,target)
|
||||
setUserAccess(db,user.id,['mm'],['new']); deleteUser(db,user.id)
|
||||
const rows = sqlite.prepare('SELECT body FROM portal_revocation_outbox ORDER BY sequence').all() as {body:string}[]
|
||||
expect(rows.map(row=>JSON.parse(row.body).type)).toEqual(['permissions.changed','user.disabled'])
|
||||
for (const row of rows) expect(JSON.parse(row.body)).toMatchObject({version:2,userId:user.id})
|
||||
expect(getUserById(db,user.id)).toBeUndefined()
|
||||
} finally { closeDb(sqlite) }
|
||||
})
|
||||
it('enqueue failure prevents success of every genuine access/disable/delete/session mutation', () => {
|
||||
const { db, sqlite } = createMemoryDb(); migrateSchema(sqlite)
|
||||
try {
|
||||
const user = createUser(db,{email:'[email protected]',name:'owned',passwordHash:'synthetic',isAdmin:false})
|
||||
setUserAccess(db,user.id,['mm'],['old'])
|
||||
const session = createRefreshSession(db,user.id,'owned-refresh',new Date(Date.now()+60000))
|
||||
enablePortalRevocations(db,target)
|
||||
sqlite.exec("CREATE TRIGGER deny_revocation BEFORE INSERT ON portal_revocation_outbox BEGIN SELECT RAISE(ABORT,'OWNED_ENQUEUE_FAILURE'); END")
|
||||
for (const mutation of [
|
||||
()=>setUserAccess(db,user.id,[],[]), ()=>updateUser(db,user.id,{isAdmin:true}),
|
||||
()=>updateUser(db,user.id,{disabled:true}), ()=>deleteUser(db,user.id),
|
||||
()=>revokeSessionById(db,session), ()=>revokeRefreshSession(db,'owned-refresh'),
|
||||
()=>revokeAllSessionsForUser(db,user.id),
|
||||
]) {
|
||||
expect(mutation).toThrow('OWNED_ENQUEUE_FAILURE')
|
||||
expect(getUserById(db,user.id)).toMatchObject({isAdmin:false,disabled:false})
|
||||
expect(getUserPermissions(db,user.id)).toEqual(['old'])
|
||||
expect(sqlite.prepare('SELECT revoked_at FROM refresh_sessions WHERE id=?').get(session)).toEqual({revoked_at:null})
|
||||
expect(sqlite.prepare('SELECT count(*) AS n FROM portal_revocation_outbox').get()).toEqual({n:0})
|
||||
}
|
||||
} finally { closeDb(sqlite) }
|
||||
})
|
||||
})
|
||||
@@ -8,10 +8,26 @@ export const APP_AUTH_MODES = ['jwt', 'oidc'] as const
|
||||
export type AppAuthMode = (typeof APP_AUTH_MODES)[number]
|
||||
export const appAuthModeSchema = z.enum(APP_AUTH_MODES)
|
||||
|
||||
export const PERMISSION_ACTIONS = ['read', 'write', 'admin'] as const
|
||||
export const PERMISSION_ACTIONS = ['read', 'write', 'admin', 'approve', 'pause'] as const
|
||||
export type PermissionAction = (typeof PERMISSION_ACTIONS)[number]
|
||||
export const permissionActionSchema = z.enum(PERMISSION_ACTIONS)
|
||||
|
||||
export const PERMISSION_ACTION_LABELS: Readonly<Record<PermissionAction, string>> = Object.freeze({
|
||||
read: 'Чтение', write: 'Редактирование', admin: 'Администрирование',
|
||||
approve: 'Одобрение планов', pause: 'Пауза автоматизации',
|
||||
})
|
||||
export function isIndependentPermissionSection(appId: AppId, sectionId: string): boolean {
|
||||
return appId === 'mm' && sectionId === 'mcp'
|
||||
}
|
||||
/** Toggle ONE explicitly supported independent key; never cascade or normalize unrelated grants. */
|
||||
export function withIndependentPermission(granted: readonly string[], appId: AppId, sectionId: string, action: PermissionAction, enabled: boolean): string[] {
|
||||
const section = PERMISSION_CATALOG.find(app => app.appId === appId)?.sections.find(section => section.id === sectionId)
|
||||
if (!isIndependentPermissionSection(appId, sectionId) || !section?.actions.includes(action)) throw new Error('Unsupported independent permission')
|
||||
const key = permissionKey(appId, sectionId, action)
|
||||
return enabled ? granted.includes(key) ? [...granted] : [...granted, key] : granted.filter(value => value !== key)
|
||||
}
|
||||
|
||||
|
||||
export type AppMeta = {
|
||||
id: AppId
|
||||
title: string
|
||||
@@ -195,6 +211,8 @@ export const PERMISSION_CATALOG: AppPermissionCatalog[] = [
|
||||
section('backups', 'Бэкапы', 'Конфиги и расписание'),
|
||||
section('certificates', 'Сертификаты', 'ACME / TLS'),
|
||||
section('network', 'Сеть', 'GRE, OSPF, пути'),
|
||||
section('users', 'Пользователи MM', 'Учётные записи и привязки интерфейсов в MikrotikManager', ['read', 'write']),
|
||||
section('mcp', 'MCP', 'Одобрение и пауза назначаются отдельно; администрирование включает чтение', ['read', 'approve', 'pause', 'admin']),
|
||||
section('settings', 'Настройки', 'Настройки приложения', ['admin']),
|
||||
],
|
||||
},
|
||||
@@ -366,7 +384,7 @@ export function normalizePermissionKeys(
|
||||
|
||||
export const permissionKeySchema = z
|
||||
.string()
|
||||
.regex(/^[a-z]+:[a-z0-9_]+:(read|write|admin)$/)
|
||||
.regex(/^(?:[a-z]+:[a-z0-9_]+:(?:read|write|admin)|mm:mcp:(?:approve|pause))$/)
|
||||
|
||||
export const meResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
|
||||
Reference in new issue
Block a user