Списки на ResourcePage, KPI на EvoBGP hybrid KpiStatGrid; удалены DataGridCard/SectionCards/ModeToggle; обновлены rules, skill и design contract. Co-authored-by: Cursor <cursoragent@cursor.com>
14 KiB
ReUI components
The 19 ReUI building blocks: alert, autocomplete, badge, data-grid, date-selector, event-calendar, filters, frame, gantt, icon-stack, kanban, number-field, phone-input, rating, scrollspy, sortable, stepper, timeline, tree. Examples and blocks are composed from these.
Rule one: never guess a component's API. Read it first. Call get_component(name) for its inline api (props + usage, no web fetch), and share the result's docsUrl (the component's API documentation page) with the user whenever you work with that component's API, so they have the full reference (the /llms.txt index is a further fallback). Then call get_examples(name) to install a worked example and copy real composition. The contracts below are first-try orientation (required props, composition shape, the one gotcha); the inline api is the full reference. No single block fits? Compose: search the components you need, read each get_component, install a get_examples example per component, and adapt.
data-grid (the flagship - read its API every time)
data-grid wraps TanStack Table v8. It is NOT a styled <table> and does NOT take data/columns props directly. The contract:
- Build a TanStack table instance with
useReactTable(...)(columns, data, the feature models you need: sorting, pagination, row selection). - Pass that instance to
<DataGrid table={table} recordCount={total}>. - Compose the body with
DataGridTableinsideDataGrid, and enable features throughtableLayout(e.g.{ headerSticky: true, columnsResizable: true }), not ad-hoc classes. - Server-side data uses the documented fetch shape (
recordCountis the total for pagination).
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
// add sorting/pagination/selection models per the API
})
<DataGrid table={table} recordCount={data.length}>
<DataGridTable />
</DataGrid>
Common mistakes:
- Incorrect:
<DataGrid data={rows} columns={cols} />- these props do not exist. Correct: build auseReactTableinstance and passtable={table}+recordCount. - Incorrect: a raw
<table>/ hand-rolled pagination. Correct: usedata-grid; read its API for sticky header, pagination, virtualization, row selection. - Incorrect: styling rows/cells with arbitrary classes. Correct: drive layout via
tableLayoutand the documentedColumnMeta(e.g.cellClassName,headerTitle).
event-calendar
Required: events via events/onEventsChange (controlled) or defaultEvents (uncontrolled), plus a height on the root.
Shape:
<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:
<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:
<Kanban value={cols} onValueChange={setCols} getItemValue={(i) => i.id}>
<KanbanBoard>
{Object.entries(cols).map(([id, items]) => (
<KanbanColumn key={id} value={id}>
<KanbanColumnHandle><h3>{id}</h3></KanbanColumnHandle>
<KanbanColumnContent value={id}>
{items.map((i) => (
<KanbanItem key={i.id} value={i.id}>
<KanbanItemHandle>{i.title}</KanbanItemHandle>
</KanbanItem>
))}
</KanbanColumnContent>
</KanbanColumn>
))}
</KanbanBoard>
<KanbanOverlay><div className="bg-muted size-full rounded-md" /></KanbanOverlay>
</Kanban>
Gotcha: state is Record<columnId, T[]>. Each KanbanColumnContent value must match its parent KanbanColumn value. Omit KanbanOverlay and the drag preview silently breaks.
sortable
Required: value (T[]), onValueChange, getItemValue
Shape:
<Sortable value={items} onValueChange={setItems} getItemValue={(i) => i.id}>
{items.map((i) => (
<SortableItem key={i.id} value={i.id}>
<SortableItemHandle><GripVertical /></SortableItemHandle>
{i.label}
</SortableItem>
))}
</Sortable>
Gotcha: a flat 1D reorder list (not columns - that is kanban). getItemValue must return a stable, unique string. Pass layout="grid" or layout="nested" for non-list layouts.
filters
Required: filters (Filter[]), fields (FilterFieldConfig[]), onChange
Shape:
const [filters, setFilters] = useState<Filter[]>([
createFilter("priority", "is_any_of", ["low"]),
])
const fields: FilterFieldConfig[] = [
{ key: "priority", label: "Priority", type: "multiselect",
options: [{ value: "low", label: "Low" }, { value: "high", label: "High" }] },
]
<Filters filters={filters} fields={fields} onChange={setFilters} />
Gotcha: always build initial filters with createFilter(field, operator, values) - it generates the required id. Never hand-construct a Filter object. Pairs naturally with data-grid.
date-selector
Required: none, but wire onChange to capture the value.
Shape:
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:
<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:
<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:
<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:
<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:
<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:
<NumberField defaultValue={0}>
<NumberFieldScrubArea label="Quantity" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
Gotcha: import from @/components/ui/number-field. The accessible label goes on NumberFieldScrubArea, not NumberField. External API: https://base-ui.com/react/components/number-field
rating
Required: rating (number)
Shape:
<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:
<Scrollspy targetRef={containerRef}>
<a href="#s1" data-scrollspy-anchor="s1">Section 1</a>
<a href="#s2" data-scrollspy-anchor="s2">Section 2</a>
</Scrollspy>
<div ref={containerRef}>
<div id="s1">...</div>
<div id="s2">...</div>
</div>
Gotcha: each link's data-scrollspy-anchor must match a section id. targetRef is the scrollable container (defaults to the window).
frame
Required: Frame > FramePanel
Shape:
<Frame>
<FramePanel>
<FrameHeader>
<FrameTitle>Title</FrameTitle>
<FrameDescription>Description</FrameDescription>
</FrameHeader>
<div className="p-5">Content</div>
<FrameFooter>Footer</FrameFooter>
</FramePanel>
</Frame>
Gotcha: a structured card shell for tool-like surfaces. stacked connects multiple panels with shared borders; dense removes panel padding; radius via the --frame-radius CSS variable.
icon-stack
Required: one child icon Shape:
<IconStack aria-hidden="true">
<InboxIcon className="size-4" />
</IconStack>
Gotcha: isometric layered artwork for empty states and illustrations; style the inner icon via its own className. Mark purely decorative stacks aria-hidden="true" and keep the real label in surrounding copy.
alert
Required: Alert > AlertTitle
Shape:
<Alert variant="success">
<ShieldCheckIcon />
<AlertTitle>Security update</AlertTitle>
<AlertDescription>Enable two-factor authentication.</AlertDescription>
<AlertAction><Button size="xs">Update</Button></AlertAction>
</Alert>
Gotcha: shadcn-compatible API. variant: default | destructive | info | success | warning | invert. The non-default variants use ReUI extended color tokens (--success/--info/--warning/--invert), which the install adds. Defer generic alert rules to the shadcn skill.
badge
Required: none (text child). Shape:
<Badge variant="success-light" size="sm">Success</Badge>
<Badge variant="outline" radius="full">Pill</Badge>
Gotcha: shadcn-compatible. Rich variant set (solid, -outline, -light per color), size xs..xl, radius default | full. Like alert, the color variants rely on ReUI extended tokens. Prefer Badge variants over raw color classes for statuses.
base vs radix - write for the project's base
ReUI ships every component in two builds: base (Base UI) and radix (Radix UI). The install command and name are identical, and the CLI installs the build matching the project. But you must write/adapt code against the right base, because their APIs differ.
Detect the base first. Read components.json -> style and take the segment before the first -:
"style": "base-nova"-> Base UI"style": "radix-nova"-> Radix UI
Then use that base's API. The deltas mirror shadcn's base-vs-radix split:
- Slot/composition: Base UI
render={<… />}vs RadixasChild. Select: Base UI takesitems; Radix uses<SelectItem>children.ToggleGroup: Base UImultipleboolean vs Radixtype="single" | "multiple".
The safest path is to read the installed files and c-* examples - they're already in your base, so reuse their wiring instead of guessing. When get_component's inline api or an example shows the other base's shape, translate it to your base (or validate_usage to confirm). Defer the generic base/radix mechanics to the shadcn skill.