Conventions and guidelines for AI agents working in this codebase.
Monorepo (pnpm workspaces) for Relay's cross-chain swap/bridge UI kit.
| Package | Purpose |
|---|---|
packages/sdk |
Core SDK — API client, execution logic (Viem-based) |
packages/ui |
React UI components (Tailwind v4, Radix primitives) |
packages/hooks |
React hooks for SDK integration (TanStack Query) |
packages/*-wallet-adapter |
Multi-chain wallet adapters (EVM, SVM, BVM, Tron) |
demo/ |
Next.js 15 playground |
- Tailwind
relay:prefix — ALL Tailwind classes must use therelay:prefix to prevent host app collisions. Example:relay:bg-white relay:p-4 - Import extensions — Always use
.jsextensions in imports:import { x } from './file.js' - Use
cn()for class merging — Never concatenate class strings manually. Import fromutils/cn.js - CSS variables prefixed
--relay-— All custom properties use this prefix - No Tailwind translate on animated elements — Animated dialogs use CSS classes (
relay-centered-modal) with hardcoded transforms. Adding Tailwind translate classes breaks animations - Access SDK via hooks — Use
useRelayClient()from context, never pass as prop - Prettier: no semicolons, single quotes, no trailing commas
- Primitives (
components/primitives/) — Base UI: Button (CVA variants), Dialog (Radix), Flex, Box, Text, Input, Tabs, AccessibleList - Common (
components/common/) — Business logic: TokenSelector, ChainFilter, TransactionModal, SlippageConfig - Widgets (
components/widgets/) — Drop-in components: SwapWidget, OnrampWidget - Renderer pattern — Widgets split into
index.tsx(state) andRenderer.tsx(render logic)
- Tailwind v4 with
@layerscoping (relay-base, relay-components, relay-utilities) - Theme via CSS variables injected by
RelayKitProviderinto.relay-kit-resetwrapper - Dark mode:
.darkclass or[data-theme="dark"]attribute - Radix color scales (slate, gray, violet, amber, etc.)
- Keyframes prefixed
relay-to prevent collisions
Relay Kit exposes an optional onHapticEvent callback on RelayKitProvider. No haptics library is bundled — integrators provide their own implementation.
import { useHapticEvent } from '../../providers/RelayKitProvider.js'
const haptic = useHapticEvent() // safe — wraps in try-catch
haptic('light') // fire haptic| Type | When | Examples |
|---|---|---|
light |
Minor taps, selections | Token select, chain star, max button, toggle, "Done" button |
medium |
Primary CTA button press | Swap button, Connect Wallet, onramp CTA |
selection |
Picker/tab switches | Chain filter tab, slippage Auto/Custom toggle |
success |
Async operation completed | Swap success, onramp complete |
error |
Async operation failed | Swap error, approval failure |
warning |
Caution moments | Unverified token modal |
heavy |
Emphatic gestures | Long-press actions |
- Supplements, never replaces — Always pair with visual feedback. UI must work without haptics.
- Match intensity to significance — Light interactions =
light/selection. CTA =medium. Results =success/error. - Don't overuse — Not every tap should vibrate. Only meaningful moments.
- Sync with visual change — Fire at the exact instant the UI updates.
- iOS caveat —
haptic()calls insideuseEffector async callbacks won't work on iOS Safari (no user activation context). For async results like swap success, the haptic fires for Android/desktop; iOS users get haptic on the subsequent "Done" tap instead.
- Haptic on every tap (fatigue)
errortype for non-errors- Haptic without accompanying visual feedback
- Haptic on page load or passive scroll
heavyfor minor interactions
LinkedWallettype:{ address, vmType, connector, walletLogoUrl? }- VM types:
'evm' | 'svm' | 'bvm' | 'tvm' - Chain icons:
https://assets.relay.link/icons/square/{chainId}/light.png(ordark.png) - Token logos:
https://assets.relay.link/icons/currencies/{id}.png - Address validation per VM type via
isValidAddress(address, vmType)
RelayKitProvider→ProviderOptionsContext(theme, haptics, app config)RelayClientProvider→useRelayClient()(SDK client instance)- Data fetching: TanStack Query v5 via hooks package (
useQuote,useTokenList,useRelayChains) - localStorage key:
relay-ui-kit(starred chains, accepted unverified tokens)
Every changeset becomes a public changelog entry at docs.relay.link/changelog. Write it for someone using the package, not for someone reading the diff.
Required shape — lead with the outcome, then the effect, then any action:
---
'@relayprotocol/relay-sdk': minor
---
Add TON support: new `@relayprotocol/relay-ton-wallet-adapter` package exporting
`adaptTonWallet`, plus `tonvm` support across the SDK and UI kit.- State what changed for the reader, not the mechanics. "Sync SDK types" and "Refactor
the token selector" describe the diff; "Suggested tokens now use the API's
logoURI" describes the change. - No commit-style prefixes —
feat:,fix:,chore:belong in the commit, not here. - Name the surface exactly — endpoint, hook, prop, error code, parameter. Preserve exact identifiers and versions.
- Breaking changes state the migration. What was removed or renamed, what to use instead, in the same entry. A breaking change without migration guidance is incomplete.
- One changeset per customer outcome. A change spanning SDK, UI kit, and hooks is one changeset naming all three packages — not three changesets.
- Nothing customer-visible? Start the body with
[internal]. That keeps it out of the public changelog and skips the prose checks. Use it for refactors, test-only changes, and dependency bumps — not as a way past the linter. - Don't guess at impact. If you cannot state the user-visible effect, say what changed
and let review fill in the rest, or mark it
[internal].
| Don't | Do |
|---|---|
Sync api types |
[internal] Sync generated API types |
fix: dead address |
Fix the Bitcoin dead-address preview error on same-chain quotes |
Refactor EOA detection |
Detect EOAs before quoting so smart-account routes are not offered to EOAs |
Add mappings |
Add TRANSACTION_SUBMISSION_FAILED and TRANSACTION_NOT_INCLUDED to the failure reasons surfaced on the transaction page |
pnpm lint:changesets enforces the mechanical minimums — prefixes, weak openers, and a
length floor. It cannot tell whether the writing is good, only whether it is obviously not
prose, so passing it is the floor rather than the goal.
pnpm install # install deps
pnpm build # build all packages
pnpm dev # run demo app
pnpm test # run tests (SDK)- Output:
_esm/,_cjs/,_types/,dist/(CSS) - CSS built via Tailwind CLI then copied to ESM/CJS output dirs
sideEffects: ["./dist/styles.css"]ensures CSS isn't tree-shaken