Unitpost

Brand & interface system

Patterns

Pages are compositions of the kit

A list page, a detail page, a settings section, an empty state and an error state each have one shape. Copy the shape; fill in the resource.

Page width

Every dashboard page renders inside PageContainer. It owns the wrapper, the padding, and one of three widths; a page never sets its own max-w. default (4xl) is everything; reading (3xl) is one message or feed; full is a padded tool page. Email and SMS siblings always share a size.

default · max-w-4xllists, details, settings, dashboards
<PageContainer className="space-y-6">…</PageContainer>
reading · max-w-3xlone email, message, batch, or the activity feed
<PageContainer width="reading">…</PageContainer>
full · no max-wpadded tool page; full-bleed editors use data-fullbleed instead
<PageContainer width="full">…</PageContainer>

List page

Title and one-line subtitle on the left, the single primary action on the right. Below: filters (line tabs) left, search right. The table sits in an outline card and spans the full width; status is a Badge, numbers are right-aligned and tabular, row actions live at the end of the row. With no rows, the table is replaced by an EmptyState that repeats the primary action.

Domains

Send from your own address.

DomainStatusSent (30d)Added
acme.comVerified12,7572 mo ago
mail.acme.comPending02 h ago
news.acme.comVerified3,1023 wk ago
List pagemirrors /dashboard/email/domains

Detail page

The object's name as the title (mono when it is a machine value), one meta line under it, secondary actions on the right. A meta grid of overline label + value comes first, then one filled DetailSection per topic. Anything destructive is last, in a destructive-tone card.

acme.com

Verified · added 2 months ago

Status
Verified
Region
EU (Frankfurt)
Sent (30d)
12,757
Bounce rate
0.4%

DNS records

Three records keep sending authenticated.
TypeHostStatus
TXTacme.comSPF ok
CNAMEup._domainkey.acme.comDKIM ok
TXT_dmarc.acme.comDMARC ok
Danger zone
Deleting stops sending from this domain immediately. This can't be undone.
Detail pagemirrors /dashboard/email/domains/[id]

Settings section

One filled Card per topic: title + description in the header, fields with labels above, Save right-aligned in the footer. Fields are h-8 pills; a two-column grid on wide screens collapses to one.

Workspace
Your internal workspace name and default sender.
Settings sectionmirrors /dashboard/settings

Empty and error states

Icon tile, a title that names what's missing, one sentence of why it matters, at most two actions (the first repeats the page's primary action). Errors say what happened and offer Retry.

No domains yet

Verify a domain to send from your own address.

EmptyState

Couldn't load contacts

The request timed out. Your data is safe.

ErrorState

No contacts yet

Add contacts to manage, segment, and reach your audience.

EmptyState with a secondary action

Messaging (SMS) patterns

Phone numbers, reach, registration state, and skip reasons each render one way. The words come from @unitpost/contracts (reach table, skip vocabulary); these components own only how they look. DESIGN.md §3 lists the props and when to use each.

🇺🇸+14155550123🇬🇧+442071234567Not set🇺🇸+1•••••0123
PhoneNumber
<PhoneNumber number="+14155550123" />  <PhoneNumber number={null} />  <PhoneNumber number="+1•••••0123" country="US" masked />
🇺🇸United States🇬🇧United Kingdom🇩🇪Germany🇫🇷France+3Reach not set
  • 🇬🇧United Kingdom
  • 🇩🇪Germany
ReachList
<ReachList kind="TOLL_FREE" country="US" />  <ReachList kind="ALPHANUMERIC" countries={[…]} max={3} />  <ReachList kind="ALPHANUMERIC" country={null} />
Registration status
Needs changes

The regulator asked for a correction. Fix it and resubmit; approval usually follows within days.

Submitted
12 Sep 2026
Regulator review · What the regulator asked for
  • The legal name does not match the registration certificate.
RegistrationStatusBlock
<RegistrationStatusBlock status="REQUIRES_UPDATES" labels={…} variants={…} help="…" feedback={[…]} source="regulator" onResubmit={…} extraFields={[{ label: "Submitted", value: "12 Sep 2026" }]} />
In review

Carriers usually answer within two business days.

Unitpost check
  • The contact email must be on the brand's website domain.
Provider issue
  • Rate plan unavailable in this region.
StatusCaption · CarrierFeedback
<StatusCaption badge={<Badge variant="info">In review</Badge>} caption="…" />  <CarrierFeedback items={…} source="unitpost" />
outside this number's reachcountry not supported yetwithout a phone numberopted outwithout marketing consentdeferred for quiet hourssender not active
outside this number's reachThis number sends to United States recipients only (recipient is in United Kingdom).

Request a sender for United Kingdom to reach these recipients.Open senders

without marketing consentMarketing SMS needs the recipient's express consent on record; none is.

Record consent from the contact page or via POST /contacts/{id}/sms-consent.

deferred for quiet hours

Inside the recipient's quiet hours. Resumes at 09:00 local.

Nothing to do; it sends when the local window opens.

sender not active

The sender is pending registration, not active.

Fix or replace the sender, then resume the campaign.Open senders

SkipReasonLine
<SkipReasonLine reason="out_of_reach" ctx={{ country: "GB", reach: ["US"] }} variant="badge | line | alert" />

1 of 4 recipients reachable · 3 skipped · 1 excluded

RecipientConsentRouted via
🇺🇸+12125550100Ursula NgMarketing🇺🇸+18885550100Toll-free
🇺🇸+12065550100Eli ParkTransactionalwithout marketing consentTransactional only. Ask for marketing consent
Not setUnnamed contactNonewithout a phone numberAdd a phone number
🇨🇦+14165550100Chris RoyMarketingoutside this number's reachRequest a Canada sender
🇺🇸+13105550100Dana ItoUnscoped🇺🇸+18885550100Toll-free

5 recipients in this segment

SmsRecipientReview
<SmsRecipientReview state={preflight} target={{ campaignId }} excludedIds={ids} onExcludedChange={setIds} />