# @handamade/psi-react 15 React 19 components consuming @handamade/psi-tokens. Zero runtime deps, CSS Modules, ref-as-prop. ## Machine-readable artifacts - dist/manifest.json: full component/prop inventory with types and defaults - dist/patterns.json: 3 composition patterns (D47) — gap-annotated recipes with clarifying parameters; presets are generated JSX, never hand-written - docs/{Component}.md: per-component docs (props, theming, variant guidance) ## Rules for generated code - import { Button } from "@handamade/psi-react"; import "@handamade/psi-react/styles"; - Also import from @handamade/psi-tokens: base.css, a theme css, components.css, utilities.css. - size is a px number (24|32|40|48), never "sm"/"md"/"lg". - variant vocabulary: accent | accent-subtle | neutral | neutral-subtle | ghost | danger | danger-subtle | outline. - One accent per visual group. danger only for destructive actions. - Before composing multi-component UI, consult dist/patterns.json (or psi-mcp search) for a matching pattern and honor its clarifying parameters; presets are copy-paste JSX generated from the compose tree. ## Button: href + outline (D33/D34) - Button renders as an when given `href` (plus optional `target`/`rel`); disabled anchors get aria-disabled, no href attribute, and pointer-events: none instead of the disabled attribute. - `outline` is a bordered-ghost variant: visible structure, no fill until hover — good for marketing CTAs and download buttons. - --psi-button-font overrides button typography across all sizes in one declaration; brands can set it via componentOverrides (ember → mono). ## Card - : stacked places media above the body, featured places media beside it (~1.6fr/1fr, stacks under the md breakpoint). hoverLift lifts the card translateY(-6px) over --psi-duration-350. Border uses --psi-border-faint. ## Panel - : the elevated surface panel (D51) — bg-secondary, faint hairline border, radius-12, space-24 padding (16 for compact). Binds the shared --psi-surface-* recipe Dialog's panel also uses; brands retune both by overriding --psi-surface-*. Not a Card: no media slot, no hover lift. ## Toolbar - : horizontal wrapping row for filter/search controls (Input, Select, Tag, Button). Wraps on overflow (D52); with aria-label it announces as role=group. Deliberately not ARIA role=toolbar (no roving tabindex). Unblocks the filter-toolbar pattern — consult dist/patterns.json for the preset. ## NavBar - {links}: brand slot leading, nav links centered/inline, actions slot trailing (theme switch, CTA). Renders inside .psi-container. Background defaults to --psi-scrim-heavy so content can scroll underneath; border uses --psi-border-faint. ## AspectRatio - {children}: a frame div with CSS aspect-ratio set from the numeric ratio prop — wrap images/media to reserve layout space before load. Combine with the .psi-media-tint utility for the brand media-tint treatment. ## Icons (22) - Full icon set: IconPlus, IconMinus, IconCheck, IconClose, IconChevronDown, IconChevronRight, IconSearch, IconSettings, IconUser, IconEye, IconEyeOff, IconEdit, IconCopy, IconTrash, IconLoader, IconExternalLink, plus D36 additions — arrows IconArrowDown and IconArrowUpRight, and social glyphs IconLinkedIn, IconGitHub, IconX, IconInstagram. ## Compositions (recipes for what has no component yet) - Form field: wrap the label, control, and message with `{control}` — label association (htmlFor/id), the description/error message line (swaps to error, aria-live="polite"), and aria-describedby/aria-invalid wiring onto a nested Input/Select are automatic (D49). `group` mode renders fieldset/legend for self-labeled controls (Checkbox/Switch). Standalone Input/Select used outside Field still need `aria-invalid` and `aria-describedby` paired manually. Spacing: `psi-gap-24` between fields, `psi-gap-12` for toggle groups, `psi-gap-8` for button rows and label+Tag rows. - Stacks/rows: `.psi-gap-*` utilities set only the gap — they do not create a flex/grid container. Pair them with `display: flex` (column for stacks, row for button groups). - Native pass-through: components spread unlisted props onto their underlying element (Input → ``, Select → `