@payfit/unity-components
Version:
296 lines (228 loc) • 9.43 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. Extract the user's intent and expand it into synonyms, interaction patterns, and adjacent component terms.
2. Search the complete catalog; do not stop at the first slice or plausible hit.
3. Rank candidates by intent fit, description, keywords, tags, section, and related entries.
4. Return the best match first. Mention an alternative only when the evidence is genuinely ambiguous.
5. Search `libs/shared/unity/components/src` to confirm implementation details or locate nearby source.
6. 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.
7. Walk the decision tree below before creating a custom primitive.
## 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.
- `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 Source Convention
`Icon` takes a typed `src` prop of type `UnityIcon` — a literal union of
PascalCase names with a `Filled` or `Outlined` suffix (~310 values, from
`@payfit/unity-icons`). Strings outside that union are a type error. Do
not cast to `UnityIcon`; the cast bypasses the sprite-id guard and the
icon silently renders empty.
```tsx
import type { UnityIcon } from '@payfit/unity-icons'
import { Icon } from '@payfit/unity-components'
const icon: UnityIcon = 'MagnifyingGlassOutlined'
;<Icon src={icon} size={20} />
```
## 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
### HIGH Pass an untyped string to Icon src and guess the name
Wrong:
```tsx
<Icon src="search" size={20} />
<Icon src="trash-filled" />
const name: string = 'trash'
<Icon src={name as UnityIcon} />
```
Correct:
```tsx
import { Icon } from '@payfit/unity-components'
import type { UnityIcon } from '@payfit/unity-icons'
<Icon src="MagnifyingGlassOutlined" size={20} />
<Icon src="TrashFilled" />
type Props = { icon: UnityIcon }
```
The `UnityIcon` literal union encodes the exact sprite ids; lowercase or
kebab-case strings have no matching `<symbol id>` in the injected sprite,
so `<use href="#search">` resolves to nothing and the SVG renders empty.
Source: libs/shared/unity/icons/src/components/icon/parts/IconSprite.tsx; generated/index.ts (UnityIcon type)
## 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-tanstack-form` — the only supported form authoring path