Support

The screen a person arrives on when they need help.

It covers a support section end to end: whether anyone is there, the ways to reach them, what the problem is about, what happened to the request the reader already made — and the thread itself, once one is open.

It does not render the transcript. The turns are Message inside MessageScroller, and the composer is AIInput; a second set of bubbles in here would drift away from those the first time either changed. What Support adds is everything around them: who is answering, which request it belongs to, the moment it changes hands, the suggested replies, the wait time above the composer, and the question at the end.

Every part is optional. An app with no help centre leaves out Support.Articles; an app that routes everything to one queue leaves out Support.Topics.

availability is the one thing the root decides, and the parts below read it from context — a screen that is half "we're here" and half "we're not" is worse than either.

Installation

Support ships with the library — no separate install.

import { Support, MessageCircleIcon, SparklesIcon, MailIcon, CardIcon, BugIcon, HeadsetIcon, PaperclipIcon, Avatar, Message, MessageScroller, AIInput, Response, Rating } from 'panelui-native';

Or copy the source into your project, to own and edit it:

npx panelui-cli@latest add support

Usage

<Support availability="online">
  <Support.Header title="Support" description="We answer most things the same day" />
  <Support.Status replyTime="Typically replies in under an hour" />

  <Support.Channels>
    <Support.Channel
      icon={<MessageCircleIcon />}
      label="Message us"
      description="Start a conversation with the team"
      detail="~1h"
      onPress={openThread}
    />
  </Support.Channels>
</Support>

Composition

{/* the screen they arrive on */}
<Support availability="online">
  <Support.Header />         {/* the screen's name, and who answers */}
  <Support.Status />         {/* whether anyone is there, and how long a reply takes */}
  <Support.Channels>         {/* the ways to reach somebody */}
    <Support.Channel />
  </Support.Channels>
  <Support.Topics>           {/* what the problem is about */}
    <Support.Topic />
  </Support.Topics>
  <Support.Conversations>    {/* requests already made */}
    <Support.Conversation />
  </Support.Conversations>
  <Support.Articles>         {/* the questions with a written answer */}
    <Support.Article />
  </Support.Articles>
</Support>

{/* and the thread, once one is open */}
<Support.Agent />            {/* who is answering */}
<Support.Ticket />           {/* which request this is */}
<Support.Handoff />          {/* the moment it changes hands */}
  …Message / Response turns…
<Support.Replies>            {/* the suggested answers */}
  <Support.Reply />
</Support.Replies>
<Support.Resolution />       {/* did this help? */}
<Support.Note />             {/* the wait, above the composer */}
  • Support.Header — The screen's name and one line under it, with a trailing slot for the faces of the people who answer.
  • Support.Status — Whether anyone is there, and how long an answer takes. The dot takes its colour from availability; replyTime is your own wording, because the honest version is a median somebody measured and the rounding is a judgement about tone.
  • Support.Channels — The titled run of ways to get in touch.
  • Support.Channel — One of them. A row rather than a tile, because each channel is a sentence — what it is, and what it costs you in waiting — and a grid of icons with one word under each is a quiz about which one is right.
  • Support.Topics — Single-select group for what the problem is about. A request goes to one queue, so it selects one. Controlled with value, or left to track its own.
  • Support.Topic — One thing the problem could be about, with room for a line of what belongs under it.
  • Support.Conversations — The titled run of requests the reader already made.
  • Support.Conversation — One of them: the subject, the last thing said, where it has got to, and when it last moved.
  • Support.Articles — The titled run of help-centre pages.
  • Support.Article — One page, with the collection it came from under it.
  • Support.Agent — Who the reader is talking to, above the transcript: a face, a name, what they are, and whether they are there. typing replaces the status line rather than sitting beside it — the two answer the same question and the live one wins.
  • Support.Ticket — Which request the thread belongs to, pinned above it, with its status. Worth having on screen: a reader who came back a day later does not otherwise know which of their requests they are looking at.
  • Support.Handoff — The moment the thread changes hands. Between two turns rather than inside either, because it is not something anybody said — an assistant's answer followed by a person's, with nothing between them, leaves the reader working out who they are talking to from the writing style.
  • Support.Note — One quiet line above the composer — what the wait is, or when they are back. It belongs there rather than at the top of the screen because that is where somebody is deciding whether it is worth typing, and an expectation set four screens ago is not one they still have in mind.
  • Support.Replies — The suggested answers under a turn.
  • Support.Reply — One of them. It is a part rather than a row of buttons because of what goes in it: the way out to a person is the reply that has to be there from the first turn, and a component that makes it easy to put there is one that gets it put there.
  • Support.Resolution — Whether it actually helped, asked at the end. The control that takes the answer is yours — pass a Rating, or two buttons.

Examples

The ways in

Each channel is a row with an action for a label, a line of what it is for, and what the wait is. disabled is for a channel that is real but shut — out of hours, or not on this plan — which reads better than a channel that vanishes.

<Support.Channels title="Get in touch">
  <Support.Channel
    icon={<SparklesIcon />}
    label="Ask the assistant"
    description="Answers most questions straight away"
    onPress={openAssistant}
  />
  <Support.Channel
    icon={<MessageCircleIcon />}
    label="Message the team"
    description="We pick these up through the day"
    detail="~1h"
    onPress={openThread}
  />
  <Support.Channel
    icon={<MailIcon />}
    label="Email us"
    description="support@example.com"
    detail="1 day"
    onPress={openMail}
  />
</Support.Channels>

Whether anyone is there

availability is set once on the root and read by everything below it. Support.Status states it in your words through replyTime, or falls back to saying the availability itself when there is no measured figure to give.

<Support availability="away">
  <Support.Status replyTime="Back tomorrow morning — leave a message and we'll reply first thing" />
</Support>

// With nothing to say about timing, the availability speaks for itself.
<Support availability="offline">
  <Support.Status />
</Support>

Routing it to the right queue

Support.Topics is a single-select list, and it is optional — plenty of support screens are better without one. Add it where the answer decides which team a request lands with.

const [topic, setTopic] = useState<string | null>(null);

<Support.Topics title="What is it about?" value={topic} onValueChange={setTopic}>
  <Support.Topic
    value="billing"
    icon={<CardIcon />}
    label="Billing"
    description="Invoices, refunds and plans"
  />
  <Support.Topic
    value="bug"
    icon={<BugIcon />}
    label="Something is broken"
    description="Send us what you were doing at the time"
  />
</Support.Topics>

What happened to the last one

status is one of four words the reader can act on: open, pending (waiting on them), solved and closed.

unread and updated are separate on purpose. A reply nobody has read and a thread that merely moved are different facts, and one dot meaning both means neither — so the unread dot takes the accent colour and the updated one stays grey.

<Support.Conversations title="Your requests">
  <Support.Conversation
    title="Refund for order #4821"
    preview="We've issued the refund — it should land in 3–5 days."
    status="solved"
    reference="#4821"
    timestamp="2h"
    unread
    onPress={() => open('4821')}
  />
  <Support.Conversation
    title="Can't add a second device"
    preview="Could you tell us which OS version it is running?"
    status="pending"
    timestamp="Yesterday"
    onPress={() => open('4810')}
  />
</Support.Conversations>

The conversation itself

Support supplies everything around the transcript; the turns are Message and the composer is AIInput.

The order is the order a reader needs it in: who is answering, which request this is, then the thread, then — last, just above the composer — how long a reply takes, because that is the moment they are deciding whether to type.

<View className="flex-1">
  <View className="gap-3 border-b border-border px-4 pb-3">
    <Support availability="online">
      <Support.Agent
        name="Khalid Abdi"
        role="Support"
        avatar={<Avatar size="sm" fallback="KA" />}
      />
      <Support.Ticket subject="Refund for order #4821" reference="#4821" status="open" />
    </Support>
  </View>

  <MessageScroller autoScroll className="flex-1">
    <MessageScroller.Viewport>
      <MessageScroller.Content className="gap-3 px-4 py-4">
        <Support.Handoff icon={<HeadsetIcon />}>
          You were passed to Khalid, who can see everything you told the assistant.
        </Support.Handoff>

        {turns.map((turn) => (
          <Message key={turn.id} align={turn.mine ? 'end' : 'start'}>

          </Message>
        ))}

        <Support.Resolution>
          <Rating defaultValue={0} max={5} onValueCommit={rate} />
        </Support.Resolution>
      </MessageScroller.Content>
    </MessageScroller.Viewport>
  </MessageScroller>

  <View className="gap-2 px-4">
    <Support.Note>Khalid typically replies in under an hour</Support.Note>
    <AIInput value={draft} onValueChange={setDraft} onSubmit={send}>
      <AIInput.Field placeholder="Write a reply" />
      <AIInput.Toolbar>
        <AIInput.Action label="Attach" icon={<PaperclipIcon />} onPress={attach} />
        <AIInput.Spacer />
        <AIInput.Submit />
      </AIInput.Toolbar>
    </AIInput>
  </View>
</View>

Handing off to a person

The escalation is a suggested reply, and it is on screen from the first turn rather than offered once the assistant has run out of answers. A reader who has decided they want a person has decided; making them prove it first is how a support screen earns its reputation.

Support.Handoff then marks the change in the transcript, so the next turn is not just a different tone of voice.

<Support.Replies>
  <Support.Reply label="Talk to a person" onPress={escalate} />
  <Support.Reply label="That's all, thanks" onPress={close} />
</Support.Replies>

// …and once it has happened, in the transcript:
<Support.Handoff icon={<HeadsetIcon />}>
  You were passed to Khalid, who can see everything you told the assistant.
</Support.Handoff>

The whole way through

The four screens in order: the help screen, the topic, the assistant's answer, and the person who can act on it. Keep them as separate routes rather than one screen that changes shape — they scroll differently, and the thread wants the keyboard handling AIInput already has.

function Help() {
  const router = useRouter();

  return (
    <Support availability="online">
      <Support.Header title="Support">
        <Avatar.Group size="sm" max={3}>
          <Avatar fallback="KA" />
          <Avatar fallback="KB" />
          <Avatar fallback="JR" />
        </Avatar.Group>
      </Support.Header>

      <Support.Status replyTime="Typically replies in under an hour" />

      <Support.Channels>
        <Support.Channel
          icon={<MessageCircleIcon />}
          label="Message us"
          onPress={() => router.push('/support/new')}
        />
      </Support.Channels>
    </Support>
  );
}

// /support/new renders the transcript.
<MessageScroller autoScroll>
  <MessageScroller.Viewport>
    <MessageScroller.Content>
      {turns.map((turn) => (
        <Message key={turn.id} align={turn.mine ? 'end' : 'start'}>

        </Message>
      ))}
    </MessageScroller.Content>
  </MessageScroller.Viewport>
</MessageScroller>
<AIInput value={draft} onValueChange={setDraft} onSubmit={send} />

Variants

availability

  • online (default)
  • away
  • offline
<Support availability="online">…</Support>
<Support availability="away">…</Support>
<Support availability="offline">…</Support>

API Reference

Support

PropTypeDefaultDescription
classNamestring
availabilitySupportAvailabilityonlineWhether anyone is at the other end. Read by every part below rather than passed to each — a screen that is half "we're here" and half "we're not" is worse than either.

Support.Header

PropTypeDefaultDescription
classNamestring
titlestringThe screen's own name.
descriptionstringOne line under it — who answers, or what this section covers.

Support.Status

PropTypeDefaultDescription
classNamestring
availabilitySupportAvailabilityonlineOverrides the availability the root published.
replyTimestringHow long a reply takes, in your own words — "Typically replies in under an hour", "Back on Monday". Left unset, the availability alone is stated. A string rather than a number of minutes, because the honest version of this is a median somebody measured and the rounding is a judgement about tone: "a few minutes" and "4 minutes" promise different things.

Support.Group

PropTypeDefaultDescription
classNamestring
titlestringSmall heading above the rows.

Support.Channel

PropTypeDefaultDescription
classNamestring
iconReactNodeThe glyph on the left. Tinted to match the row.
labelstringWhat the channel is called — the thing pressed, so write it as an action.
descriptionstringOne line under it: what it is for, or the address behind it.
detailstringTrailing note — a wait time, an opening hour.
disabledboolean
onPress() => void

Support.Topics

PropTypeDefaultDescription
classNamestring
titlestringSmall heading above the list.
valuestring | nullThe selected topic. Leave unset to let the group track it.
defaultValuestring | nullThe topic selected before anybody pressed anything.
onValueChange(value: string) => void

Support.Topic

PropTypeDefaultDescription
classNamestring
valuestringWhat this topic is worth to the group's value.
iconReactNode
labelstring
descriptionstringOne line of what belongs under it, for the ones whose name is not enough.
disabledboolean
onPress() => void

Support.Conversation

PropTypeDefaultDescription
classNamestring
titlestringWhat it was about.
previewstringThe last thing said in it.
statusSupportConversationStatus'open'Where it has got to.
timestampstringWhen it last moved, in your own words — "2h", "Yesterday".
unreadbooleanfalseThere is a reply here the reader has not seen. Separate from updated, because they are different facts: a thread can move without anything being addressed to you, and a dot that means both means neither.
updatedbooleanfalseThe thread moved since it was last opened, with nothing new to read.
referencestringReference for the request, shown beside the status.
onPress() => void

Support.Article

PropTypeDefaultDescription
classNamestring
iconReactNode
titlestringThe article's own title.
collectionstringWhich part of the help centre it came from.
onPress() => void

Support.Agent

PropTypeDefaultDescription
classNamestring
avatarReactNodeSlot for the avatar.
namestringWho is answering.
rolestringWhat they are — "Support", "Billing team", "Assistant".
availabilitySupportAvailabilityonlineOverrides the availability the root published.
typingbooleanThey are writing a reply right now.

Support.Ticket

PropTypeDefaultDescription
classNamestring
referencestringReference for the request, as the reader sees it.
subjectstringWhat it was about.
statusSupportConversationStatus'open'

Support.Handoff

PropTypeDefaultDescription
classNamestring
iconReactNode

Support.Note

PropTypeDefaultDescription
classNamestring

Support.Replies

PropTypeDefaultDescription
classNamestring

Support.Reply

PropTypeDefaultDescription
classNamestring
valuestringWhat pressing it sends. Defaults to the label.
labelstring
disabledboolean
onPress(value: string) => void

Support.Resolution

PropTypeDefaultDescription
classNamestring
titlestringThe question asked.

Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.

Notes

Availability

availability is published by the root and read from context by Support.Status. A part can override it for itself, which is for the case where one channel keeps different hours from the rest — not for saying two different things about the same team.

Why the conversation is not in here

A support thread is a chat transcript. Message, MessageScroller, Response, Sources, Reasoning and AIInput already cover one, including the scroll behaviour and the keyboard handling, and a second set of bubbles living in this component would drift away from those the first time either changed. So Support ends where the conversation begins.

Statuses

Four, and fewer than a helpdesk runs internally. The states a desk distinguishes for its own routing are not distinctions the person waiting can act on, and offering them invites a question about the difference that nobody wants to answer.

Selection

Support.Topics is a radio group: it wires accessibilityRole on itself and on each Support.Topic, and reports the selected one through accessibilityState. Leave value unset and it tracks its own; pass it and it stands back.

The thread is two components deep

Support.Agent, Support.Ticket, Support.Handoff, Support.Replies, Support.Resolution and Support.Note are the support-specific furniture around a transcript, not a transcript. Put them around MessageScroller and Message, and give the thread its own route: a conversation wants the whole screen and the keyboard handling that comes with AIInput.

Escalation

Put the way out to a person in Support.Replies from the first turn rather than revealing it once the assistant has failed. It is the one reply a reader may arrive already wanting, and a screen that hides it until it has had its turn is a screen people learn to distrust.

Public exports

Values: Support, useSupport

Types: SupportProps, SupportHeaderProps, SupportStatusProps, SupportGroupProps, SupportChannelProps, SupportTopicsProps, SupportTopicProps, SupportConversationProps, SupportArticleProps, SupportAgentProps, SupportTicketProps, SupportHandoffProps, SupportNoteProps, SupportRepliesProps, SupportReplyProps, SupportResolutionProps, SupportAvailability, SupportConversationStatus

On this page