@payfit/unity-components
Version:
271 lines (213 loc) • 9.18 kB
Markdown
---
name: unity-find-component
description: >
Load when selecting a Unity component is unclear or before creating a custom
UI primitive that may already exist in @payfit/unity-components. Use it to
find the existing component, then fall back to React Aria plus uy: classes
only when Unity has no fit.
metadata:
type: core
library: '@payfit/unity-components'
library_version: '2.x'
---
Routing skill for selecting a Unity component before writing UI code. Walk
the catalog, then the decision tree, then the commonly-confused pairs.
## Component catalog
Inspect the canonical [component catalog](../../src/agent-references/component-catalog.json)
before searching component source. It contains exact component ids, titles,
descriptions, keywords, sections, tags, related entries, and Storybook preview
metadata. There is no separate canonical Markdown catalog.
For a targeted lookup, search the full JSON and compare all plausible matches. Use whatever reliable local search tool is available (for example `rg`, a JSON-aware CLI, or a short Node script); do not assume `jq` is installed:
```sh
rg -i -C 2 'dialog|confirmation|modal' \
libs/shared/unity/components/src/agent-references/component-catalog.json
```
## Standard operating procedure
1. Build an inventory of distinct interface needs from all task inputs,
including the primary workflow, surrounding controls, navigation, status
information, and secondary actions. Do not silently omit peripheral UI.
2. Expand every identified need into synonyms, interaction patterns, and
adjacent component terms.
3. Search the complete catalog for every inventory item; do not stop after
finding components for the primary workflow or at the first plausible hit.
4. Rank candidates by intent fit, description, keywords, tags, section, and related entries.
5. Return the best match first. Mention an alternative only when the evidence is genuinely ambiguous.
6. Search `libs/shared/unity/components/src` to confirm implementation details or locate nearby source.
7. Inspect Storybook stories only for props, API details, or usage examples after a likely component has been identified. When the Storybook MCP is available, use its curated component manifest or matching Storybook entry as a complementary source for rendered examples; the canonical JSON catalog remains the source of truth for discovery.
8. Walk the decision tree below for every identified need before creating a custom primitive.
9. Before implementation, perform a coverage check: every inventoried
interface need must map to a Unity component, an explicit React Aria
fallback, or a deliberate omission requested by the user.
## Decision Tree
For every UI need, walk these three levels in order. Stop at the first that
fits.
### Level 1 — Use Unity directly
If a named export covers the use case, import it. No abstractions over the
top.
```tsx
import {
Button,
Dialog,
DialogActions,
DialogContent,
Pill,
} from '@payfit/unity-components'
export function ConfirmDelete({
isOpen,
onClose,
}: {
isOpen: boolean
onClose: () => void
}) {
return (
<Dialog isOpen={isOpen} onOpenChange={onClose}>
<DialogContent>
<Pill color="danger">Destructive</Pill>
</DialogContent>
<DialogActions>
<Button variant="secondary" onPress={onClose}>
Cancel
</Button>
<Button color="danger" onPress={onClose}>
Delete
</Button>
</DialogActions>
</Dialog>
)
}
```
### Level 2 — Fall back to React Aria + uy:\* classes
Build your own primitive only when Unity has no equivalent. Compose React
Aria primitives, style with the `uy:` prefix, and merge classes with
`uyMerge` / `uyTv`.
```tsx
import { uyTv } from '@payfit/unity-themes'
import { ToggleButton } from 'react-aria-components'
const toggleStyles = uyTv({
base: 'uy:inline-flex uy:items-center uy:gap-100 uy:rounded-100 uy:px-200 uy:py-100',
variants: {
isSelected: {
true: 'uy:bg-surface-action uy:text-content-on-action',
false: 'uy:bg-surface-secondary uy:text-content-neutral',
},
},
})
export function CustomPivot({ label }: { label: string }) {
return (
<ToggleButton className={({ isSelected }) => toggleStyles({ isSelected })}>
{label}
</ToggleButton>
)
}
```
### Level 3 — Midnight (last resort, deprecated)
Only when (a) Unity has no equivalent, (b) React Aria + `uy:*` cannot
realistically rebuild it, and (c) the feature ships against a deadline. Open
a follow-up to migrate. Per `AGENTS.md`, Midnight is deprecated; never
import it into a new module.
## Commonly Confused Pairs
- `Badge` vs `Pill`: `Badge` is a numeric/dot indicator anchored to another
element (notification count). `Pill` is a standalone label/status chip
with text content.
- `Card` vs `SelectableCard...` vs `NavigationCard`: `Card` is the generic
container. `SelectableCardCheckboxGroup` / `SelectableCardRadioGroup`
wrap cards as form inputs. `NavigationCard` wraps a card as a router-aware
link.
- `Button` vs `IconButton` vs `RawLinkButton`: `Button` for actions with
text. `IconButton` / `CircularIconButton` for icon-only actions
(requires `aria-label`). `RawLinkButton` renders as an anchor but styled
like a button — use when the action navigates.
- `Search` vs `TextField`: use `Search` for query, lookup, and filtering
controls with search affordances and clearing behavior. Use `TextField` for
general textual data entry.
- `Menu` vs `Popover`: `Menu` is a list of actionable items keyed by
keyboard (Enter/Arrow). `Popover` is a free-form floating panel and
requires a `title`.
- `Dialog` vs `PromoDialog`: `Dialog` for confirmation / edit flows.
`PromoDialog` for marketing / onboarding announcements; requires
`PromoDialogHero`.
- `Table` vs `DataTable`: `Table` is the layout primitive (header, body,
rows, cells) with no behavior. `DataTable` wires Tanstack Table for
sorting, filtering, pagination, virtualization, bulk actions.
- `ErrorState` vs `Alert`: `ErrorState` is a full-area empty-replacement
for "this section failed to load." `Alert` is an inline banner that
coexists with surrounding content.
## Icon selection
Choose the component here (`Icon`, `IconButton`, a prefix/suffix icon, and so
on), then use `unity-icons` to select an exact sprite name, type shared icon
props, or migrate a Midnight icon. Do not duplicate or guess the icon catalog
from the Components package.
## Forms Notice
Tanstack Form (`useTanstackUnityForm`) is the only supported form system.
The React Hook Form path — `useUnityForm` plus the legacy `TextField` /
`SelectField` / `NumberField` etc. RHF wrappers exported from the same
index — is deprecated. Do not author new code with `useUnityForm`. See
`unity-tanstack-form`.
## Common Mistakes
### HIGH Hand-roll component that already exists
Wrong:
```tsx
const Tag = ({ children }) => (
<span className="uy:rounded-full uy:px-200 uy:py-100 uy:bg-surface-primary">
{children}
</span>
)
```
Correct:
```tsx
import { Pill } from '@payfit/unity-components'
;<Pill>{children}</Pill>
```
The hand-rolled span re-derives Pill's tokens by guesswork and drifts from
the design-system source-of-truth on every theme update.
Source: libs/shared/unity/components/src/components/pill/Pill.tsx
### HIGH Use React Aria primitive directly when Unity wraps it
Wrong:
```tsx
import { Button as AriaButton } from 'react-aria-components'
;<AriaButton>Click</AriaButton>
```
Correct:
```tsx
import { Button } from '@payfit/unity-components'
;<Button variant="primary">Click</Button>
```
The bare React Aria Button has no Unity theming, intl, or styling
defaults; you ship an unstyled element with no `uy:*` classes.
Source: libs/shared/unity/components/src/components/button/Button.tsx
### HIGH Reach for Midnight when Unity has an equivalent
Wrong:
```tsx
import { Button, Modal } from '@payfit/midnight'
```
Correct:
```tsx
import { Button, Dialog } from '@payfit/unity-components'
```
Midnight is deprecated; new screens that import it cannot match the Unity
theme tokens and will require a migration pass later anyway.
Source: AGENTS.md "Do NOT use (deprecated)"
### MEDIUM Combine Input + FormField when \*Field exists
Wrong:
```tsx
<FormField label="Name" error={errors.name}>
<Input {...register('name')} />
</FormField>
```
Correct:
```tsx
<form.AppField name="name">
{field => <field.TextField label="Name" />}
</form.AppField>
```
Manual `FormField` + `Input` skips the label-for/aria-describedby wiring,
the `field.state.meta` error plumbing, and the required-state inference
that the `*Field` components handle.
Source: libs/shared/unity/components/src/components/form-field/FormField.tsx; index.ts:205-223
## See also
- For projects not yet configured for Unity, use the repository's
`@payfit/nx-tools:setup-unity` generator
- `unity-migrate-from-midnight` — when Level 3 fallback hits a Midnight
screen, follow this skill to replace it
- `unity-icons` — select and type the exact icon after choosing the component
- `unity-tanstack-form` — the only supported form authoring path