
@arkyn/templates doesn't export any components — it's the data layer meant to be plugged into Select, PhoneInput, and CurrencyInput from @arkyn/components. This guide shows how the pieces are meant to fit together, and clears up a couple of naming details that are easy to trip over.brazilianStates already matches Select's options shape ({ label, value }) exactly, so no mapping step is needed:tsx
import { Select } from "@arkyn/components/select";import { brazilianStates } from "@arkyn/templates";<Select name="state" label="State" options={brazilianStates} />
countries needs a small mapping step, since its objects carry more fields than Select expects:tsx
import { Select } from "@arkyn/components/select";import { countries } from "@arkyn/templates";const countryOptions = countries.map((country) => ({label: `${country.name} (${country.code})`,value: country.iso,}));<Select name="country" label="Country" options={countryOptions} isSearchable />
PhoneInput consumes @arkyn/templates' countries internally for its
built-in country selector (defaultCountryIso) — you don't need to wire
that yourself. Reuse the same countries list for any other field on
the same form (like a "shipping country" Select) instead of hand-rolling
a second list, so both fields stay in sync if Arkyn ever updates its data.CurrencyInput's prop is called locale, but it actually expects a currency code ("USD", "BRL", "EUR", …) — the same strings used as keys in countryCurrencies — not a language-region locale string like "pt-BR". countryCurrencies' own countryLanguage field is the actual BCP-47 locale, meant for Intl.NumberFormat, and is a different value entirely.tsx
import { CurrencyInput } from "@arkyn/components/currencyInput";import { countryCurrencies, maximumFractionDigits } from "@arkyn/templates";// countryCurrencies.BRL = { countryLanguage: "pt-BR", countryCurrency: "BRL" }function PriceField({ currencyCode }: { currencyCode: keyof typeof countryCurrencies }) {const { countryLanguage, countryCurrency } = countryCurrencies[currencyCode];return (<>{/* CurrencyInput's "locale" prop takes the currency code, not countryLanguage */}<CurrencyInput name="price" locale={countryCurrency as any} label="Price" />{/* Use countryLanguage for anything formatted with Intl.NumberFormat elsewhere on the page */}<span>{new Intl.NumberFormat(countryLanguage, {style: "currency",currency: countryCurrency,maximumFractionDigits,}).format(1234.5)}</span></>);}
CurrencyInput's locale dynamically from a country/currency picker, derive it from Object.keys(countryCurrencies) (or from the selected country's currency) rather than hardcoding a separate list of supported currencies — that way both stay backed by the same source of truth.PhoneInput's hidden submitted value is the selected country's dialing code concatenated directly with the entered digits — e.g. +5511999999999 — which is the same E.164-style shape that @arkyn/shared's findCountryMask/formatToPhone and @arkyn/server's validatePhone expect as input. This isn't a coincidence: all three read from the same @arkyn/templates countries list.tsx
// Client: PhoneInput submits "+5511999999999" as-is<PhoneInput name="phone" label="Phone" defaultCountryIso="BR" />
typescript
// Server: validate and format the exact same value, no reformatting neededimport { validatePhone } from "@arkyn/server/validatePhone";import { formatToPhone } from "@arkyn/shared/formatToPhone";const phone = formData.get("phone") as string; // "+5511999999999"if (!validatePhone(phone)) {throw new BadRequest("Invalid phone number");}const displayPhone = formatToPhone(phone); // "(11) 99999-9999"
PhoneInput), use
findCountryMask
instead of writing your own country/mask matching — it already resolves
the correct entry from the same countries template.countries, brazilianStates, and countryCurrencies are plain data — there's no fetch, no async loading, and no peer dependency required to use them (unlike findCountryMask/formatToPhone, which need libphonenumber-js).mask field in countries is an array (currently only Brazil, for the with/without-ninth-digit variants) rather than a single string — code that reads country.mask directly should handle both shapes if it bypasses PhoneInput/findCountryMask.validatePhone check turns into a fieldErrors entry the client can display.SearchPlaces' returned stateShortName lines up with brazilianStates' value field, useful if you need to reconcile a free-text address search with a fixed state Select on the same form.