UNPKG

@payfit/unity-components

Version:

370 lines (286 loc) 12.3 kB
--- name: unity-find-component description: > Load before choosing or creating Unity UI. Use it to map a UI need to an existing @payfit/unity-components export, then fall back to React Aria plus uy: classes only when Unity has no fit. type: core library: '@payfit/unity-components' library_version: '2.x' sources: - 'PayFit/hr-apps:libs/shared/unity/components/src/index.ts' - 'PayFit/hr-apps:libs/shared/unity/icons/src/components/icon/parts/IconSprite.tsx' - 'PayFit/hr-apps:libs/shared/unity/icons/src/generated/index.ts' - 'PayFit/hr-apps:libs/shared/unity/components/OVERVIEW.md' - 'PayFit/hr-apps:AGENTS.md' --- Routing skill for selecting a Unity component before writing UI code. Walk the catalog, then the decision tree, then the commonly-confused pairs. ## Quick Reference All exports come from a single entry: `@payfit/unity-components`. There are no sub-paths for runtime components; the only sub-paths are `@payfit/unity-components/integrations/tanstack-router` (router-aware navigation) and `@payfit/unity-components/i18n/<locale>.json` (message bundles). Names below are grouped by purpose. ### Layout `Flex`, `FlexItem`, `Grid`, `GridItem`, `Card`, `CardTitle`, `CardContent`, `SelectableCard...`, `NavigationCard`, `NavigationCardGroup`, `Page`, `PageHeader`, `PageHeading`, `AppLayout`, `AppMenu`, `FunnelLayout`, `FunnelPage`, `FunnelBody`, `FunnelSidebar`, `FunnelTopBar`, `FunnelPageHeader`, `FunnelPageContent`, `FunnelPageFooter`, `FunnelPageActions`, `FunnelBackButton`. ### Navigation Router-agnostic (base entry): `RawLink`, `RawNavItem`, `RawBreadcrumbLink`, `RawPaginationLink`, `RawPaginationPrevious`, `RawPaginationNext`, `RawTab`, `Nav`, `NavGroup`, `Breadcrumbs`, `Breadcrumb`, `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationEllipsis`, `Tabs`, `TabList`, `TabPanel`, `SkipLinks`, `TaskMenu`, `RawTask`, `RawSubTask`, `TaskGroup`, `ListView`, `RawListViewItem`, `ListViewSection`, `ListViewItemLabel`, `ListViewItemText`. Router-aware (from `@payfit/unity-components/integrations/tanstack-router`): `Link`, `NavItem`, `BreadcrumbLink`, `PaginationLink`, `Tab`. ### Form fields (Tanstack Form — current) Bound via `form.AppField` then `field.<Name>`: `TextField`, `SelectField`, `NumberField`, `CheckboxField`, `CheckboxGroupField`, `DatePickerField`, `DateRangePickerField`, `MultiSelectField`, `RadioButtonGroupField`, `SelectableButtonGroupField`, `SelectableCardCheckboxGroupField`, `SelectableCardRadioGroupField`, `ToggleSwitchField`, `ToggleSwitchGroupField`, `PasswordField`. ### Form primitives (no form state) `Input`, `NumberInput`, `Select`, `SelectButton`, `SelectOption`, `SelectOptionGroup`, `SelectOptionHelper`, `MultiSelect`, `MultiSelectOption`, `MultiSelectOptGroup`, `Checkbox`, `CheckboxStandalone`, `CheckboxGroup`, `RadioButtonGroup`, `RadioButton`, `RadioButtonHelper`, `TextArea`, `ToggleSwitch`, `ToggleSwitchGroup`, `SegmentedButtonGroup`, `ToggleButton`, `SelectableButtonGroup`, `SelectableButton`, `Search`, `PhoneNumberInput`, `DatePicker`, `DateCalendar`, `DateRangePicker`, `DateRangeCalendar`, `Autocomplete`, `AutocompleteItem`, `AutocompleteItemGroup`, `Fieldset`, `FieldGroup`, `Label`, `FormField`, `FormControl`, `FormLabel`, `FormHelperText`, `FormFeedbackText`. ### Buttons and actions `Button`, `IconButton`, `CircularIconButton`, `RawLinkButton`, `Actionable`, `ActionBar`, `ActionBarRoot`, `ActionBarButton`, `ActionBarIconButton`, `FloatingActionBar`, `Anchor`. ### Overlays Modal: `Dialog`, `DialogContent`, `DialogTitle`, `DialogActions`, `DialogButton`, `PromoDialog`, `PromoDialogHero`, `PromoDialogTitle`, `PromoDialogSubtitle`, `PromoDialogContent`, `PromoDialogActions`, `SidePanel`, `SidePanelHeader`, `SidePanelContent`, `SidePanelFooter`, `BottomSheet`, `BottomSheetHeader`, `BottomSheetContent`, `BottomSheetFooter`. Non-modal: `Tooltip`, `DefinitionTooltip`, `Popover`, `Menu`, `MenuTrigger`, `MenuContent`, `MenuHeader`, `RawMenuItem`, `MenuSeparator`. ### Content and data `Text`, `Icon`, `Pill`, `Badge`, `Alert`, `AlertTitle`, `AlertContent`, `AlertActions`, `Avatar`, `AvatarFallback`, `AvatarIcon`, `AvatarImage`, `AvatarPair`, `DataTable`, `DataTableRoot`, `DataTableBulkActions`, `Table`, `TableBody`, `TableHeader`, `TableColumnHeader`, `TableRow`, `TableCell`, `TableEmptyState`, `TablePagination`, `Filter`, `FilterToolbar`, `Carousel`, `CarouselHeader`, `CarouselContent`, `CarouselSlide`, `CarouselNav`, `Collapsible`, `CollapsibleTitle`, `CollapsibleContent`, `Timeline`, `TimelineStep`, `TimelineStepHeader`, `TimelineStepDescription`. ### Status and loading `Spinner`, `ProgressBar`, `FullPageLoader`, `EmptyState`, `EmptyStateIcon`, `EmptyStateContent`, `EmptyStateActions`, `EmptyStateGetStarted`, `EmptyStateWaitingForData`, `EmptyStateGoodJob`, `EmptyStateUpgradeRequired`, `EmptyStateNoSearchResults`, `EmptyStateUseDesktop`, `ErrorState`, `ToastManager`, `toast`. ### Semantic and brand `PayFitBrand`, `PayFitPreprod`. ## 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 and scheduled for removal after the rebrand. 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)"; maintainer interview ### 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); maintainer interview ## See also - `unity-setup-feature-plugin` — required before any Unity import will work - `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