# Inbox Support

A support ticket inbox with status views, a ticket list with priorities, and a detail pane for replying and resolving.

Inbox Support is an open-source block from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/blocks/inbox-support

**React**

**Example**

```tsx title="components/blocks/inbox-support.tsx"
"use client"

import { ChatCircleIcon, CheckIcon, TrayIcon } from "@phosphor-icons/react"
import * as React from "react"

import {
  SplitView,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
  SplitViewSidebar,
} from "@/components/patterns/split-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Status = "Open" | "Pending" | "Solved"

type Priority = "High" | "Medium" | "Low"

type Ticket = {
  id: string
  title: string
  device: string
  status: Status
  priority: Priority
  requester: string
  hue: number
}

const initialTickets: Ticket[] = [
  {
    id: "#4821",
    title: "Screen flickers",
    device: "Phone Pro",
    status: "Open",
    priority: "High",
    requester: "Jamie Rivera",
    hue: 30,
  },
  {
    id: "#4820",
    title: "Battery drains fast",
    device: "Book Air",
    status: "Pending",
    priority: "Medium",
    requester: "Morgan Lee",
    hue: 200,
  },
  {
    id: "#4819",
    title: "Pods won't pair",
    device: "Pods Studio",
    status: "Open",
    priority: "Low",
    requester: "Avery Brooks",
    hue: 330,
  },
  {
    id: "#4818",
    title: "Cloud backup error",
    device: "Phone Air",
    status: "Solved",
    priority: "Medium",
    requester: "Taylor Kim",
    hue: 120,
  },
]

const views: { value: Status | "All"; icon: React.ReactNode }[] = [
  {
    value: "Open",
    icon: (
      <ChatCircleIcon className="size-4.5 text-primary" />
    ),
  },
  {
    value: "Pending",
    icon: (
      <ChatCircleIcon className="size-4.5 text-primary" />
    ),
  },
  {
    value: "Solved",
    icon: (
      <CheckIcon className="size-4.5 text-primary" />
    ),
  },
  {
    value: "All",
    icon: (
      <TrayIcon className="size-4.5 text-primary" />
    ),
  },
]

const priorityClass: Record<Priority, string> = {
  High: "bg-destructive/14 text-[color-mix(in_oklab,var(--destructive),var(--foreground)_30%)] dark:text-destructive",
  Medium:
    "bg-orange-500/14 text-[color-mix(in_oklab,var(--color-orange-500),var(--foreground)_50%)] dark:text-orange-500",
  Low: "bg-secondary text-muted-foreground",
}

function Requester({
  ticket,
  className,
}: {
  ticket: Ticket
  className?: string
}) {
  return (
    <Avatar className={className}>
      <AvatarFallback
        style={{
          background: `linear-gradient(160deg, oklch(0.78 0.1 ${ticket.hue}), oklch(0.6 0.14 ${ticket.hue + 30}))`,
        }}
        className="text-white"
      >
        {ticket.requester
          .split(" ")
          .map((part) => part[0])
          .join("")}
      </AvatarFallback>
    </Avatar>
  )
}

export function InboxSupport() {
  const [tickets, setTickets] = React.useState(initialTickets)
  const [view, setView] = React.useState<Status | "All">("Open")
  const [selected, setSelected] = React.useState("#4821")
  const [reply, setReply] = React.useState("")
  const list = tickets.filter(
    (ticket) => view === "All" || ticket.status === view
  )
  const current = tickets.find((ticket) => ticket.id === selected)

  const setStatus = (status: Status) =>
    setTickets((items) =>
      items.map((ticket) =>
        ticket.id === selected ? { ...ticket, status } : ticket
      )
    )

  return (
    <SplitView
      defaultColumn="list"
      className="h-155 rounded-none border-0 max-md:h-auto max-md:min-h-155 max-md:p-4"
    >
      <SplitViewSidebar
        title="Support"
        className="min-h-full md:bg-muted [&>h2]:md:px-3 [&>h2]:md:pt-3 [&>h2]:md:text-lg [&>h2]:md:text-foreground"
      >
        {views.map((item) => (
          <SplitViewItem
            key={item.value}
            isActive={view === item.value}
            onClick={() => setView(item.value)}
          >
            <span className="flex items-center gap-2.5">
              {item.icon}
              <span className="flex-1">{item.value}</span>
              <span className="text-xs text-muted-foreground tabular-nums">
                {item.value === "All"
                  ? tickets.length
                  : tickets.filter((ticket) => ticket.status === item.value)
                      .length}
              </span>
            </span>
          </SplitViewItem>
        ))}
      </SplitViewSidebar>
      <SplitViewList title={view} className="gap-1 md:p-2">
        {list.map((ticket) => (
          <SplitViewItem
            key={ticket.id}
            isActive={ticket.id === selected}
            onClick={() => setSelected(ticket.id)}
            className="gap-1 py-3"
          >
            <span className="flex items-center gap-2">
              <span className="flex-1 text-xs text-muted-foreground">
                {ticket.id} · {ticket.device}
              </span>
              <span
                className={`rounded-md px-2 py-0.5 text-xs font-semibold ${priorityClass[ticket.priority]}`}
              >
                {ticket.priority}
              </span>
            </span>
            <span className="text-base font-semibold">{ticket.title}</span>
            <span className="mt-1 flex items-center gap-1.5">
              <Requester
                ticket={ticket}
                className="size-4.5 *:text-[0.4375rem]"
              />
              <span className="text-xs text-muted-foreground">
                {ticket.requester}
              </span>
            </span>
          </SplitViewItem>
        ))}
        {list.length === 0 && (
          <p className="p-6 text-center text-sm text-muted-foreground">
            No tickets
          </p>
        )}
      </SplitViewList>
      <SplitViewDetail
        title={current?.title}
        className="md:p-5.5 [&>h2]:sr-only"
      >
        {current && (
          <div
            key={current.id}
            className="flex flex-col gap-4 transition-[opacity,translate] duration-800 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
          >
            <span className="text-sm text-muted-foreground">
              {current.id} · {current.device}
            </span>
            <ToggleGroup
              spacing={0}
              value={[current.status]}
              onValueChange={(value: string[]) => {
                if (value[0]) setStatus(value[0] as Status)
              }}
              aria-label="Status"
            >
              {(["Open", "Pending", "Solved"] as const).map((status) => (
                <ToggleGroupItem key={status} value={status} size="sm">
                  {status}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
            <h3 className="text-3xl font-semibold tracking-tight">
              {current.title}
            </h3>
            <div className="flex items-center gap-2.5">
              <Requester ticket={current} className="size-8.5" />
              <div className="flex flex-col">
                <span className="text-sm font-semibold">
                  {current.requester}
                </span>
                <span className="text-xs text-muted-foreground">
                  Today 8:12 AM
                </span>
              </div>
            </div>
            <p className="max-w-130 rounded-2xl rounded-ss-sm bg-muted px-3.5 py-3 text-base leading-relaxed">
              Hi, this started right after the latest update. I tried restarting
              but it keeps happening. Can you help?
            </p>
            <form
              onSubmit={(event) => {
                event.preventDefault()
                setReply("")
                setStatus("Pending")
              }}
              className="flex flex-col gap-2.5"
            >
              <Textarea
                rows={3}
                value={reply}
                onChange={(event) => setReply(event.target.value)}
                placeholder="Write a reply…"
                aria-label="Reply"
              />
              <div className="flex flex-wrap gap-2.5">
                <Button type="submit" size="sm" disabled={!reply}>
                  Send and set pending
                </Button>
                <Button
                  type="button"
                  size="sm"
                  variant="outline"
                  onClick={() => setStatus("Solved")}
                >
                  Mark as solved
                </Button>
              </div>
            </form>
          </div>
        )}
      </SplitViewDetail>
    </SplitView>
  )
}
```

A help desk inbox with Open, Pending, Solved and All views and live counts, a ticket list with priority badges and a detail pane for replying or marking a ticket solved. Sending a reply moves the ticket to Pending. On mobile the panes show one at a time with a back button. A good base for support queues, moderation queues or any triage workflow.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/inbox-support
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add blocks inbox-support
```

### Manual

1. Install the dependencies:

```bash title="Terminal"
npm install @phosphor-icons/react
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Textarea](https://ui.suiss.app/docs/components/textarea), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/blocks/inbox-support.tsx`:

```tsx title="components/blocks/inbox-support.tsx"
"use client"

import { ChatCircleIcon, CheckIcon, TrayIcon } from "@phosphor-icons/react"
import * as React from "react"

import {
  SplitView,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
  SplitViewSidebar,
} from "@/components/patterns/split-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Status = "Open" | "Pending" | "Solved"

type Priority = "High" | "Medium" | "Low"

type Ticket = {
  id: string
  title: string
  device: string
  status: Status
  priority: Priority
  requester: string
  hue: number
}

const initialTickets: Ticket[] = [
  {
    id: "#4821",
    title: "Screen flickers",
    device: "Phone Pro",
    status: "Open",
    priority: "High",
    requester: "Jamie Rivera",
    hue: 30,
  },
  {
    id: "#4820",
    title: "Battery drains fast",
    device: "Book Air",
    status: "Pending",
    priority: "Medium",
    requester: "Morgan Lee",
    hue: 200,
  },
  {
    id: "#4819",
    title: "Pods won't pair",
    device: "Pods Studio",
    status: "Open",
    priority: "Low",
    requester: "Avery Brooks",
    hue: 330,
  },
  {
    id: "#4818",
    title: "Cloud backup error",
    device: "Phone Air",
    status: "Solved",
    priority: "Medium",
    requester: "Taylor Kim",
    hue: 120,
  },
]

const views: { value: Status | "All"; icon: React.ReactNode }[] = [
  {
    value: "Open",
    icon: (
      <ChatCircleIcon className="size-4.5 text-primary" />
    ),
  },
  {
    value: "Pending",
    icon: (
      <ChatCircleIcon className="size-4.5 text-primary" />
    ),
  },
  {
    value: "Solved",
    icon: (
      <CheckIcon className="size-4.5 text-primary" />
    ),
  },
  {
    value: "All",
    icon: (
      <TrayIcon className="size-4.5 text-primary" />
    ),
  },
]

const priorityClass: Record<Priority, string> = {
  High: "bg-destructive/14 text-[color-mix(in_oklab,var(--destructive),var(--foreground)_30%)] dark:text-destructive",
  Medium:
    "bg-orange-500/14 text-[color-mix(in_oklab,var(--color-orange-500),var(--foreground)_50%)] dark:text-orange-500",
  Low: "bg-secondary text-muted-foreground",
}

function Requester({
  ticket,
  className,
}: {
  ticket: Ticket
  className?: string
}) {
  return (
    <Avatar className={className}>
      <AvatarFallback
        style={{
          background: `linear-gradient(160deg, oklch(0.78 0.1 ${ticket.hue}), oklch(0.6 0.14 ${ticket.hue + 30}))`,
        }}
        className="text-white"
      >
        {ticket.requester
          .split(" ")
          .map((part) => part[0])
          .join("")}
      </AvatarFallback>
    </Avatar>
  )
}

export function InboxSupport() {
  const [tickets, setTickets] = React.useState(initialTickets)
  const [view, setView] = React.useState<Status | "All">("Open")
  const [selected, setSelected] = React.useState("#4821")
  const [reply, setReply] = React.useState("")
  const list = tickets.filter(
    (ticket) => view === "All" || ticket.status === view
  )
  const current = tickets.find((ticket) => ticket.id === selected)

  const setStatus = (status: Status) =>
    setTickets((items) =>
      items.map((ticket) =>
        ticket.id === selected ? { ...ticket, status } : ticket
      )
    )

  return (
    <SplitView
      defaultColumn="list"
      className="h-155 rounded-none border-0 max-md:h-auto max-md:min-h-155 max-md:p-4"
    >
      <SplitViewSidebar
        title="Support"
        className="min-h-full md:bg-muted [&>h2]:md:px-3 [&>h2]:md:pt-3 [&>h2]:md:text-lg [&>h2]:md:text-foreground"
      >
        {views.map((item) => (
          <SplitViewItem
            key={item.value}
            isActive={view === item.value}
            onClick={() => setView(item.value)}
          >
            <span className="flex items-center gap-2.5">
              {item.icon}
              <span className="flex-1">{item.value}</span>
              <span className="text-xs text-muted-foreground tabular-nums">
                {item.value === "All"
                  ? tickets.length
                  : tickets.filter((ticket) => ticket.status === item.value)
                      .length}
              </span>
            </span>
          </SplitViewItem>
        ))}
      </SplitViewSidebar>
      <SplitViewList title={view} className="gap-1 md:p-2">
        {list.map((ticket) => (
          <SplitViewItem
            key={ticket.id}
            isActive={ticket.id === selected}
            onClick={() => setSelected(ticket.id)}
            className="gap-1 py-3"
          >
            <span className="flex items-center gap-2">
              <span className="flex-1 text-xs text-muted-foreground">
                {ticket.id} · {ticket.device}
              </span>
              <span
                className={`rounded-md px-2 py-0.5 text-xs font-semibold ${priorityClass[ticket.priority]}`}
              >
                {ticket.priority}
              </span>
            </span>
            <span className="text-base font-semibold">{ticket.title}</span>
            <span className="mt-1 flex items-center gap-1.5">
              <Requester
                ticket={ticket}
                className="size-4.5 *:text-[0.4375rem]"
              />
              <span className="text-xs text-muted-foreground">
                {ticket.requester}
              </span>
            </span>
          </SplitViewItem>
        ))}
        {list.length === 0 && (
          <p className="p-6 text-center text-sm text-muted-foreground">
            No tickets
          </p>
        )}
      </SplitViewList>
      <SplitViewDetail
        title={current?.title}
        className="md:p-5.5 [&>h2]:sr-only"
      >
        {current && (
          <div
            key={current.id}
            className="flex flex-col gap-4 transition-[opacity,translate] duration-800 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
          >
            <span className="text-sm text-muted-foreground">
              {current.id} · {current.device}
            </span>
            <ToggleGroup
              spacing={0}
              value={[current.status]}
              onValueChange={(value: string[]) => {
                if (value[0]) setStatus(value[0] as Status)
              }}
              aria-label="Status"
            >
              {(["Open", "Pending", "Solved"] as const).map((status) => (
                <ToggleGroupItem key={status} value={status} size="sm">
                  {status}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
            <h3 className="text-3xl font-semibold tracking-tight">
              {current.title}
            </h3>
            <div className="flex items-center gap-2.5">
              <Requester ticket={current} className="size-8.5" />
              <div className="flex flex-col">
                <span className="text-sm font-semibold">
                  {current.requester}
                </span>
                <span className="text-xs text-muted-foreground">
                  Today 8:12 AM
                </span>
              </div>
            </div>
            <p className="max-w-130 rounded-2xl rounded-ss-sm bg-muted px-3.5 py-3 text-base leading-relaxed">
              Hi, this started right after the latest update. I tried restarting
              but it keeps happening. Can you help?
            </p>
            <form
              onSubmit={(event) => {
                event.preventDefault()
                setReply("")
                setStatus("Pending")
              }}
              className="flex flex-col gap-2.5"
            >
              <Textarea
                rows={3}
                value={reply}
                onChange={(event) => setReply(event.target.value)}
                placeholder="Write a reply…"
                aria-label="Reply"
              />
              <div className="flex flex-wrap gap-2.5">
                <Button type="submit" size="sm" disabled={!reply}>
                  Send and set pending
                </Button>
                <Button
                  type="button"
                  size="sm"
                  variant="outline"
                  onClick={() => setStatus("Solved")}
                >
                  Mark as solved
                </Button>
              </div>
            </form>
          </div>
        )}
      </SplitViewDetail>
    </SplitView>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { InboxSupport } from "@/components/blocks/inbox-support"
```

```tsx
<InboxSupport />
```

Replace `initialTickets` with your tickets, each with an id, title, device, status, priority, requester and avatar hue; `priorityClass` maps priorities to badge colors. Connect the reply and Mark as solved actions to your help desk API. Every ticket shows the same hard-coded customer message and timestamp, so load the real conversation.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/blocks-inbox-support--default.light.webp)

```tsx title="uniwind/components/blocks/inbox-support.tsx"
import { cn } from "cn"
import {
  ChatCircleIcon,
  CheckIcon,
  type Icon as PhosphorIcon,
  TrayIcon,
} from "phosphor-react-native"
import * as React from "react"
import { useWindowDimensions, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import {
  SplitView,
  type SplitViewColumn,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
  SplitViewSidebar,
} from "@/components/patterns/split-view"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
import { Textarea } from "@/components/ui/textarea"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Status = "Open" | "Pending" | "Solved"

type Priority = "High" | "Medium" | "Low"

type Queue = Status | "All"

type Ticket = {
  id: string
  title: string
  device: string
  status: Status
  priority: Priority
  requester: string
  tint: string
}

const INITIAL_TICKETS: Ticket[] = [
  {
    id: "#4821",
    title: "Screen flickers",
    device: "Phone Pro",
    status: "Open",
    priority: "High",
    requester: "Jamie Rivera",
    tint: "bg-orange-500",
  },
  {
    id: "#4820",
    title: "Battery drains fast",
    device: "Book Air",
    status: "Pending",
    priority: "Medium",
    requester: "Morgan Lee",
    tint: "bg-cyan-500",
  },
  {
    id: "#4819",
    title: "Pods won't pair",
    device: "Pods Studio",
    status: "Open",
    priority: "Low",
    requester: "Avery Brooks",
    tint: "bg-pink-500",
  },
  {
    id: "#4818",
    title: "Cloud backup error",
    device: "Phone Air",
    status: "Solved",
    priority: "Medium",
    requester: "Taylor Kim",
    tint: "bg-green-500",
  },
]

const VIEWS: { value: Queue; icon: PhosphorIcon }[] = [
  { value: "Open", icon: ChatCircleIcon },
  { value: "Pending", icon: ChatCircleIcon },
  { value: "Solved", icon: CheckIcon },
  { value: "All", icon: TrayIcon },
]

const STATUSES: Status[] = ["Open", "Pending", "Solved"]

const PRIORITY_CLASS: Record<Priority, { box: string; text: string }> = {
  High: {
    box: "bg-destructive/10 dark:bg-destructive/20",
    text: "text-destructive",
  },
  Medium: {
    box: "bg-amber-500/10 dark:bg-amber-500/20",
    text: "text-amber-600 dark:text-amber-400",
  },
  Low: { box: "bg-secondary", text: "text-muted-foreground" },
}

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .join("")

function Requester({
  ticket,
  size,
  className,
}: {
  ticket: Ticket
  size: "sm" | "default"
  className?: string
}) {
  return (
    <Avatar size={size} className={className}>
      <AvatarFallback className={ticket.tint}>
        {initials(ticket.requester)}
      </AvatarFallback>
    </Avatar>
  )
}

function TicketDetail({
  ticket,
  onStatusChange,
  onReply,
}: {
  ticket: Ticket
  onStatusChange: (status: Status) => void
  onReply: (text: string) => void
}) {
  const [reply, setReply] = React.useState("")

  return (
    <AnimatedView
      key={ticket.id}
      entering={FadeInDown.duration(800).reduceMotion(ReduceMotion.System)}
      className="gap-4"
    >
      <Text className="text-sm text-muted-foreground">
        {`${ticket.id} · ${ticket.device}`}
      </Text>
      <ToggleGroup
        spacing={0}
        size="sm"
        value={[ticket.status]}
        onValueChange={(value) => {
          if (value[0]) onStatusChange(value[0] as Status)
        }}
        accessibilityLabel="Status"
      >
        {STATUSES.map((status) => (
          <ToggleGroupItem key={status} value={status}>
            <Text>{status}</Text>
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
      <Text role="heading" className="text-3xl font-semibold">
        {ticket.title}
      </Text>
      <View className="flex-row items-center gap-2.5">
        <Requester ticket={ticket} size="default" className="size-8.5" />
        <View>
          <Text className="text-sm font-semibold">{ticket.requester}</Text>
          <Text className="text-xs text-muted-foreground">Today 8:12 AM</Text>
        </View>
      </View>
      <View className="max-w-lg self-start rounded-2xl rounded-ss-sm bg-muted px-3.5 py-3">
        <Text className="text-base">
          Hi, this started right after the latest update. I tried restarting but
          it keeps happening. Can you help?
        </Text>
      </View>
      <View className="gap-2.5">
        <Textarea
          numberOfLines={3}
          value={reply}
          onChangeText={setReply}
          placeholder="Write a reply…"
          accessibilityLabel="Reply"
          className="min-h-20"
        />
        <View className="flex-row flex-wrap gap-2.5">
          <Button
            size="sm"
            disabled={!reply}
            onPress={() => {
              onReply(reply)
              setReply("")
              onStatusChange("Pending")
            }}
          >
            <Text>Send and set pending</Text>
          </Button>
          <Button
            size="sm"
            variant="outline"
            onPress={() => onStatusChange("Solved")}
          >
            <Text>Mark as solved</Text>
          </Button>
        </View>
      </View>
    </AnimatedView>
  )
}

function InboxSupport({
  onViewChange,
  onStatusChange,
  onReply,
}: {
  onViewChange?: (view: Status | "All") => void
  onStatusChange?: (id: string, status: Status) => void
  onReply?: (id: string, text: string) => void
}) {
  const { width } = useWindowDimensions()
  const wide = width >= 768
  const [tickets, setTickets] = React.useState(INITIAL_TICKETS)
  const [view, setView] = React.useState<Queue>("Open")
  const [selected, setSelected] = React.useState("#4821")
  const [column, setColumn] = React.useState<SplitViewColumn>("list")
  const list = tickets.filter(
    (ticket) => view === "All" || ticket.status === view
  )
  const current = tickets.find((ticket) => ticket.id === selected)

  const setStatus = (id: string, status: Status) => {
    onStatusChange?.(id, status)
    setTickets((items) =>
      items.map((ticket) => (ticket.id === id ? { ...ticket, status } : ticket))
    )
  }

  return (
    <Screen edges={["top", "right", "left"]}>
      <SplitView
        column={column}
        onColumnChange={setColumn}
        className={wide ? "m-4" : undefined}
      >
        <SplitViewSidebar
          title="Support"
          className={wide ? "min-h-full bg-muted" : "px-4 pb-8"}
        >
          {VIEWS.map((item) => (
            <SplitViewItem
              key={item.value}
              isActive={view === item.value}
              onPress={() => {
                setView(item.value)
                onViewChange?.(item.value)
              }}
              className={
                wide && view === item.value ? "bg-secondary" : undefined
              }
            >
              <View className="flex-row items-center gap-2.5">
                <Icon as={item.icon} className="size-5 text-primary" />
                <Text className="flex-1">{item.value}</Text>
                <Text className="text-xs text-muted-foreground tabular-nums">
                  {String(
                    item.value === "All"
                      ? tickets.length
                      : tickets.filter((ticket) => ticket.status === item.value)
                          .length
                  )}
                </Text>
              </View>
            </SplitViewItem>
          ))}
        </SplitViewSidebar>
        <SplitViewList
          title={view}
          className={wide ? "gap-1 p-2" : "gap-1 px-4 pb-8"}
        >
          {list.map((ticket) => {
            const priority = PRIORITY_CLASS[ticket.priority]
            return (
              <SplitViewItem
                key={ticket.id}
                isActive={ticket.id === selected}
                onPress={() => setSelected(ticket.id)}
                className="py-3"
              >
                <View className="flex-row items-center gap-2">
                  <Text className="flex-1 text-xs text-muted-foreground">
                    {`${ticket.id} · ${ticket.device}`}
                  </Text>
                  <View className={cn("rounded-md px-2 py-0.5", priority.box)}>
                    <Text
                      className={cn("text-xs font-semibold", priority.text)}
                    >
                      {ticket.priority}
                    </Text>
                  </View>
                </View>
                <Text className="text-base font-semibold">{ticket.title}</Text>
                <View className="mt-1 flex-row items-center gap-1.5">
                  <Requester ticket={ticket} size="sm" className="size-5" />
                  <Text className="text-xs text-muted-foreground">
                    {ticket.requester}
                  </Text>
                </View>
              </SplitViewItem>
            )
          })}
          {list.length === 0 ? (
            <Text className="p-6 text-center text-sm text-muted-foreground">
              No tickets
            </Text>
          ) : null}
        </SplitViewList>
        <SplitViewDetail className={wide ? "p-5.5" : "px-4 pb-8"}>
          {current ? (
            <TicketDetail
              ticket={current}
              onStatusChange={(status) => setStatus(current.id, status)}
              onReply={(text) => onReply?.(current.id, text)}
            />
          ) : null}
        </SplitViewDetail>
      </SplitView>
    </Screen>
  )
}

export { InboxSupport }
```

```swift title="swift/Blocks/InboxSupport.swift"
import PhosphorSwift
import SwiftUI

public struct InboxSupport: View {
  public enum Status: String, CaseIterable, Sendable {
    case open = "Open"
    case pending = "Pending"
    case solved = "Solved"
  }

  enum Priority: String {
    case high = "High"
    case medium = "Medium"
    case low = "Low"
  }

  struct Ticket: Identifiable, Equatable {
    let id: String
    let title: String
    let device: String
    var status: Status
    let priority: Priority
    let requester: String
    let tint: Color
  }

  static var initialTickets: [Ticket] {
    [
      Ticket(id: "#4821", title: "Screen flickers", device: "Phone Pro", status: .open, priority: .high, requester: "Jamie Rivera", tint: Palette.orange500),
      Ticket(id: "#4820", title: "Battery drains fast", device: "Book Air", status: .pending, priority: .medium, requester: "Morgan Lee", tint: Palette.blue500),
      Ticket(id: "#4819", title: "Pods won't pair", device: "Pods Studio", status: .open, priority: .low, requester: "Avery Brooks", tint: Palette.pink500),
      Ticket(id: "#4818", title: "Cloud backup error", device: "Phone Air", status: .solved, priority: .medium, requester: "Taylor Kim", tint: Palette.green500),
    ]
  }

  static let views = ["Open", "Pending", "Solved", "All"]

  private var onViewChange: ((String) -> Void)?
  private var onReply: ((String, String) -> Void)?
  private var onStatusChange: ((String, Status) -> Void)?
  private let swiped: String?
  @State private var tickets: [Ticket]
  @State private var view: String
  @State private var selected: String
  @State private var reply: String
  @State private var column: SplitViewColumn
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(
    onViewChange: ((_ view: String) -> Void)? = nil,
    onStatusChange: ((_ id: String, _ status: Status) -> Void)? = nil,
    onReply: ((_ id: String, _ text: String) -> Void)? = nil
  ) {
    self.init(view: "Open")
    self.onViewChange = onViewChange
    self.onReply = onReply
    self.onStatusChange = onStatusChange
  }

  init(
    view: String = "Open",
    selected: String = "#4821",
    reply: String = "",
    column: SplitViewColumn = .list,
    swiped: String? = nil
  ) {
    _tickets = State(initialValue: Self.initialTickets)
    _view = State(initialValue: view)
    _selected = State(initialValue: selected)
    _reply = State(initialValue: reply)
    _column = State(initialValue: column)
    self.swiped = swiped
  }

  private func count(_ value: String) -> Int {
    value == "All" ? tickets.count : tickets.filter { $0.status.rawValue == value }.count
  }

  public var body: some View {
    let list = tickets.filter { view == "All" || $0.status.rawValue == view }
    let current = tickets.first { $0.id == selected }
    Screen {
      SplitView(column: $column) {
        SplitViewSidebar("Support") {
          ForEach(Self.views, id: \.self) { item in
            SplitViewItem(isActive: view == item, action: {
              if view != item { onViewChange?(item) }
              view = item
            }) {
              HStack(spacing: 10) {
                (item == "Solved" ? Ph.check.regular : item == "All" ? Ph.tray.regular : Ph.chatCircle.regular)
                  .frame(width: 20, height: 20)
                  .foregroundStyle(EsuissColor.primary)
                Text(LocalizedStringKey(item))
                  .frame(maxWidth: .infinity, alignment: .leading)
                Text(verbatim: "\(count(item))")
                  .scaledFont(.xs)
                  .monospacedDigit()
                  .foregroundStyle(EsuissColor.mutedForeground)
              }
            }
          }
        }
        SplitViewList(LocalizedStringKey(view)) {
          VStack(spacing: 0) {
            ForEach(list) { ticket in
              SwipeActions(revealed: swiped == ticket.id ? -88 : 0) {
                SplitViewItem(isActive: ticket.id == selected, action: { selected = ticket.id }) {
                  InboxSupportRow(ticket: ticket)
                }
              } leading: {
                EmptyView()
              } trailing: {
                if ticket.status != .solved {
                  SwipeAction("Solved", icon: Ph.check.bold, variant: .default, fullSwipe: true) {
                    setStatus(.solved, for: ticket.id)
                  }
                }
              }
              .transition(.opacity)
            }
            if list.isEmpty {
              Text("No tickets")
                .scaledFont(.sm)
                .foregroundStyle(EsuissColor.mutedForeground)
                .frame(maxWidth: .infinity)
                .padding(24)
            }
          }
        }
        SplitViewDetail {
          if let current {
            detail(current)
              .id(current.id)
              .transition(.opacity.combined(with: .offset(y: 8)))
          }
        }
      }
    }
  }

  private func detail(_ ticket: Ticket) -> some View {
    let ready = !reply.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    return VStack(alignment: .leading, spacing: 16) {
      Text(verbatim: "\(ticket.id) · \(ticket.device)")
        .scaledFont(.sm)
        .foregroundStyle(EsuissColor.mutedForeground)
        .padding(.top, 8)
      ToggleGroup(
        selection: Binding<Status?>(
          get: { ticket.status },
          set: { value in if let value { setStatus(value, for: ticket.id) } }
        ),
        size: .sm,
        spacing: 0
      ) {
        ForEach(Status.allCases, id: \.self) { status in
          ToggleGroupItem(LocalizedStringKey(status.rawValue), value: status)
        }
      }
      .accessibilityLabel(Text("Status"))
      Text(verbatim: ticket.title)
        .scaledTextStyle(.xl3, weight: .semibold)
        .fixedSize(horizontal: false, vertical: true)
        .accessibilityAddTraits(.isHeader)
      HStack(spacing: 10) {
        InboxSupportAvatar(ticket: ticket, size: 34)
        VStack(alignment: .leading, spacing: 0) {
          Text(verbatim: ticket.requester)
            .scaledFont(.sm, weight: .semibold)
          Text("Today 8:12 AM")
            .scaledFont(.xs)
            .foregroundStyle(EsuissColor.mutedForeground)
        }
      }
      .accessibilityElement(children: .combine)
      Text("Hi, this started right after the latest update. I tried restarting but it keeps happening. Can you help?")
        .scaledTextStyle(.base)
        .lineSpacing(4)
        .fixedSize(horizontal: false, vertical: true)
        .padding(.horizontal, 14)
        .padding(.vertical, 12)
        .background(
          UnevenRoundedRectangle(
            topLeadingRadius: 4,
            bottomLeadingRadius: Radius.xl2,
            bottomTrailingRadius: Radius.xl2,
            topTrailingRadius: Radius.xl2,
            style: .continuous
          )
          .fill(EsuissColor.muted)
        )
      VStack(alignment: .leading, spacing: 10) {
        TextField("Write a reply…", text: $reply, axis: .vertical)
          .textFieldStyle(.textarea)
          .lineLimit(3...8)
          .accessibilityLabel(Text("Reply"))
        HStack(spacing: 10) {
          Button("Send and set pending") {
            onReply?(ticket.id, reply)
            reply = ""
            setStatus(.pending, for: ticket.id)
          }
          .buttonStyle(.variant(.default, size: .sm))
          .disabled(!ready)
          Button("Mark as solved") {
            setStatus(.solved, for: ticket.id)
          }
          .buttonStyle(.variant(.outline, size: .sm))
        }
      }
    }
    .foregroundStyle(EsuissColor.foreground)
  }

  private func setStatus(_ status: Status, for id: String) {
    guard let index = tickets.firstIndex(where: { $0.id == id }), tickets[index].status != status else { return }
    onStatusChange?(id, status)
    withAnimation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.35)) {
      tickets[index].status = status
    }
  }
}

private struct InboxSupportAvatar: View {
  let ticket: InboxSupport.Ticket
  let size: CGFloat

  var body: some View {
    Text(verbatim: ticket.requester.split(separator: " ").compactMap(\.first).map(String.init).joined())
      .font(.system(size: size * 0.4, weight: .semibold))
      .foregroundStyle(EsuissColor.primaryForeground)
      .frame(width: size, height: size)
      .background(ticket.tint, in: Circle())
      .accessibilityHidden(true)
  }
}

private struct InboxSupportRow: View {
  let ticket: InboxSupport.Ticket

  var body: some View {
    VStack(alignment: .leading, spacing: 4) {
      HStack(spacing: 8) {
        Text(verbatim: "\(ticket.id) · \(ticket.device)")
          .scaledFont(.xs)
          .foregroundStyle(EsuissColor.mutedForeground)
          .frame(maxWidth: .infinity, alignment: .leading)
        InboxSupportPriority(priority: ticket.priority)
      }
      Text(verbatim: ticket.title)
        .scaledFont(.base, weight: .semibold)
      HStack(spacing: 6) {
        InboxSupportAvatar(ticket: ticket, size: 18)
        Text(verbatim: ticket.requester)
          .scaledFont(.xs)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      .padding(.top, 2)
    }
    .accessibilityElement(children: .combine)
  }
}

private struct InboxSupportPriority: View {
  let priority: InboxSupport.Priority

  var body: some View {
    let tint: Color = switch priority {
    case .high: EsuissColor.destructive
    case .medium: Color.adaptive(light: Palette.amber600, dark: Palette.amber400)
    case .low: EsuissColor.mutedForeground
    }
    Text(LocalizedStringKey(priority.rawValue))
      .scaledFont(.xs, weight: .semibold)
      .foregroundStyle(tint)
      .padding(.horizontal, 8)
      .padding(.vertical, 2)
      .background(priority == .low ? EsuissColor.secondary : tint.opacity(0.14), in: RoundedRectangle(cornerRadius: Radius.md, style: .continuous))
  }
}
```

```kt title="kotlin/blocks/InboxSupport.kt"
package com.esuiss.ui.blocks

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.bold.Check as BoldCheck
import com.adamglin.phosphoricons.regular.ChatCircle
import com.adamglin.phosphoricons.regular.Check
import com.adamglin.phosphoricons.regular.Tray
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.Textarea
import com.esuiss.ui.components.ToggleGroup
import com.esuiss.ui.components.ToggleGroupItem
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.interactions.SwipeAction
import com.esuiss.ui.interactions.SwipeActions
import com.esuiss.ui.patterns.SplitView
import com.esuiss.ui.patterns.SplitViewColumn
import com.esuiss.ui.patterns.SplitViewItem
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

private val TicketRevealEasing = CubicBezierEasing(0.32f, 0.72f, 0f, 1f)

private class SupportTicket(
    val id: String,
    val title: String,
    val device: String,
    status: String,
    val priority: String,
    val requester: String,
    val hue: Int,
) {
    var status by mutableStateOf(status)
}

private fun supportTickets() = listOf(
    SupportTicket("#4821", "Screen flickers", "Phone Pro", "Open", "High", "Jamie Rivera", 30),
    SupportTicket("#4820", "Battery drains fast", "Book Air", "Pending", "Medium", "Morgan Lee", 200),
    SupportTicket("#4819", "Pods won't pair", "Pods Studio", "Open", "Low", "Avery Brooks", 330),
    SupportTicket("#4818", "Cloud backup error", "Phone Air", "Solved", "Medium", "Taylor Kim", 120),
)

private class SupportView(val value: String, val icon: ImageVector)

private val SupportViews = listOf(
    SupportView("Open", PhosphorIcons.Regular.ChatCircle),
    SupportView("Pending", PhosphorIcons.Regular.ChatCircle),
    SupportView("Solved", PhosphorIcons.Regular.Check),
    SupportView("All", PhosphorIcons.Regular.Tray),
)

private val TicketStatuses = listOf("Open", "Pending", "Solved")

private fun requesterTint(colors: Colors, hue: Int): Color = when (hue) {
    30 -> Palette.orange500
    200 -> Palette.blue500
    330 -> Palette.pink500
    else -> Palette.green500
}

private fun priorityColors(colors: Colors, priority: String): Pair<Color, Color> = when (priority) {
    "High" -> colors.destructive.copy(alpha = if (colors.isDark) 0.2f else 0.1f) to if (colors.isDark) colors.destructive else lerp(colors.destructive, colors.foreground, 0.3f)
    "Medium" -> Palette.amber500.copy(alpha = if (colors.isDark) 0.2f else 0.1f) to if (colors.isDark) Palette.orange500 else lerp(Palette.orange500, colors.foreground, 0.5f)
    else -> colors.secondary to colors.mutedForeground
}

@Composable
fun InboxSupport(
    modifier: Modifier = Modifier,
    onViewChange: (view: String) -> Unit = {},
    onStatusChange: (id: String, status: String) -> Unit = { _, _ -> },
    onReply: (id: String, text: String) -> Unit = { _, _ -> },
) {
    val colors = EsuissTheme.colors
    val tickets = remember { mutableStateListOf<SupportTicket>().apply { addAll(supportTickets()) } }
    var view by rememberSaveable { mutableStateOf("Open") }
    var selected by rememberSaveable { mutableStateOf("#4821") }
    var column by rememberSaveable { mutableStateOf(SplitViewColumn.List) }
    val list = tickets.filter { view == "All" || it.status == view }
    val current = tickets.firstOrNull { it.id == selected }

    fun setStatus(ticket: SupportTicket, status: String) {
        if (ticket.status == status) return
        onStatusChange(ticket.id, status)
        ticket.status = status
    }

    Screen(modifier) {
        SplitView(column = column, onColumnChange = { column = it }) {
            SplitViewSidebar(title = "Support") {
                SupportViews.forEach { item ->
                    SplitViewItem(
                        isActive = view == item.value,
                        onClick = {
                            onViewChange(item.value)
                            view = item.value
                        },
                    ) {
                        Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
                            Icon(item.icon, contentDescription = null, size = 18.dp, tint = colors.primary)
                            BasicText(item.value, modifier = Modifier.weight(1f), style = Typography.base.copy(color = colors.foreground))
                            val count = if (item.value == "All") tickets.size else tickets.count { it.status == item.value }
                            BasicText("$count", style = Typography.xs.copy(color = colors.mutedForeground, fontFeatureSettings = "tnum"))
                        }
                    }
                }
            }
            SplitViewList(title = view) {
                list.forEach { ticket ->
                    key(ticket.id) {
                        SwipeActions(
                            trailing = if (ticket.status == "Solved") {
                                emptyList()
                            } else {
                                listOf(
                                    SwipeAction(
                                        label = "Mark as solved",
                                        onClick = { setStatus(ticket, "Solved") },
                                        icon = PhosphorIcons.Bold.BoldCheck,
                                        fullSwipe = true,
                                    ),
                                )
                            },
                        ) {
                            TicketRow(ticket, active = ticket.id == selected) { selected = ticket.id }
                        }
                    }
                }
                if (list.isEmpty()) {
                    BasicText(
                        "No tickets",
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(24.dp),
                        style = Typography.sm.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                    )
                }
            }
            SplitViewDetail {
                if (current != null) {
                    key(current.id) {
                        TicketDetail(
                            ticket = current,
                            onStatus = { setStatus(current, it) },
                            onReply = { onReply(current.id, it) },
                        )
                    }
                }
            }
        }
    }
}

@Composable
private fun TicketRow(ticket: SupportTicket, active: Boolean, onClick: () -> Unit) {
    val colors = EsuissTheme.colors
    val (fill, ink) = priorityColors(colors, ticket.priority)
    SplitViewItem(isActive = active, onClick = onClick) {
        Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
            Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
                BasicText(
                    "${ticket.id} · ${ticket.device}",
                    modifier = Modifier.weight(1f),
                    style = Typography.xs.copy(color = colors.mutedForeground),
                )
                BasicText(
                    ticket.priority,
                    modifier = Modifier
                        .background(fill, RoundedCornerShape(Radius.md))
                        .padding(horizontal = 8.dp, vertical = 2.dp),
                    style = Typography.xs.copy(color = ink, fontWeight = FontWeight.SemiBold),
                )
            }
            BasicText(ticket.title, style = Typography.base.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
            Row(
                modifier = Modifier.padding(top = 4.dp),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                TicketRequester(ticket, 18.dp)
                BasicText(ticket.requester, style = Typography.xs.copy(color = colors.mutedForeground))
            }
        }
    }
}

@Composable
private fun TicketRequester(ticket: SupportTicket, size: Dp) {
    val colors = EsuissTheme.colors
    Box(
        Modifier
            .size(size)
            .background(requesterTint(colors, ticket.hue), CircleShape),
        contentAlignment = Alignment.Center,
    ) {
        BasicText(
            ticket.requester.split(" ").mapNotNull { it.firstOrNull() }.joinToString(""),
            style = Typography.xs.copy(color = Palette.white, fontWeight = FontWeight.Medium, fontSize = (size.value * 0.4f).sp),
        )
    }
}

@Composable
private fun TicketDetail(ticket: SupportTicket, onStatus: (String) -> Unit, onReply: (String) -> Unit) {
    val colors = EsuissTheme.colors
    var reply by rememberSaveable { mutableStateOf("") }
    val appear = remember { Animatable(0f) }
    LaunchedEffect(Unit) { appear.animateTo(1f, tween(800, easing = TicketRevealEasing)) }
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 8.dp)
            .graphicsLayer {
                alpha = appear.value
                translationY = (1f - appear.value) * 8.dp.toPx()
            },
        verticalArrangement = Arrangement.spacedBy(16.dp),
    ) {
        BasicText("${ticket.id} · ${ticket.device}", style = Typography.sm.copy(color = colors.mutedForeground))
        ToggleGroup(
            value = ticket.status,
            onValueChange = { if (it != null) onStatus(it) },
            modifier = Modifier.semantics { contentDescription = "Status" },
            size = ToggleSize.Sm,
            spacing = 0,
        ) {
            TicketStatuses.forEach { ToggleGroupItem(it, it) }
        }
        BasicText(
            ticket.title,
            modifier = Modifier.semantics { heading() },
            style = Typography.xl2.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
        )
        Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
            TicketRequester(ticket, 34.dp)
            Column {
                BasicText(ticket.requester, style = Typography.sm.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold))
                BasicText("Today 8:12 AM", style = Typography.xs.copy(color = colors.mutedForeground))
            }
        }
        BasicText(
            "Hi, this started right after the latest update. I tried restarting but it keeps happening. Can you help?",
            modifier = Modifier
                .widthIn(max = 520.dp)
                .background(colors.muted, RoundedCornerShape(topStart = 4.dp, topEnd = 16.dp, bottomEnd = 16.dp, bottomStart = 16.dp))
                .padding(horizontal = 14.dp, vertical = 12.dp),
            style = Typography.base.copy(color = colors.foreground, lineHeight = 26.sp),
        )
        Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
            Textarea(
                value = reply,
                onValueChange = { reply = it },
                modifier = Modifier.semantics { contentDescription = "Reply" },
                placeholder = "Write a reply…",
                minLines = 3,
                keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
            )
            Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
                Button(
                    text = "Send and set pending",
                    onClick = {
                        onReply(reply.trim())
                        reply = ""
                        onStatus("Pending")
                    },
                    size = ButtonSize.Sm,
                    enabled = reply.isNotBlank(),
                )
                Button(text = "Mark as solved", onClick = { onStatus("Solved") }, size = ButtonSize.Sm, variant = ButtonVariant.Outline)
            }
        }
    }
}
```

A help desk screen built on `SplitView`, with a sidebar of queues, a list of tickets showing the requester `Avatar`, priority and device, and a detail view. In the detail view a `ToggleGroup` changes the ticket status, a `Textarea` takes a reply, Send and set pending sends it and moves the ticket to Pending, and Mark as solved closes it.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/inbox-support
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add inbox-support
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add inbox-support --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add inbox-support --platform kotlin
```

The CLI also adds what it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Screen](https://ui.suiss.app/docs/components/screen), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Text](https://ui.suiss.app/docs/components/text), [Textarea](https://ui.suiss.app/docs/components/textarea), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group). On React Native it also adds the animated view `components/ui/animated-view.tsx`.

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Screen](https://ui.suiss.app/docs/components/screen), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Text](https://ui.suiss.app/docs/components/text), [Textarea](https://ui.suiss.app/docs/components/textarea), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `uniwind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/uniwind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/inbox-support.tsx`:

- `uniwind/components/blocks/inbox-support.tsx`: shown above

5. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Screen](https://ui.suiss.app/docs/components/screen), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Text](https://ui.suiss.app/docs/components/text), [Textarea](https://ui.suiss.app/docs/components/textarea), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `nativewind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/nativewind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/inbox-support.tsx`:

```tsx title="nativewind/components/blocks/inbox-support.tsx"
import { cn } from "cn"
import {
  ChatCircleIcon,
  CheckIcon,
  type Icon as PhosphorIcon,
  TrayIcon,
} from "phosphor-react-native"
import * as React from "react"
import { useWindowDimensions, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import {
  SplitView,
  type SplitViewColumn,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
  SplitViewSidebar,
} from "@/components/patterns/split-view"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
import { Textarea } from "@/components/ui/textarea"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Status = "Open" | "Pending" | "Solved"

type Priority = "High" | "Medium" | "Low"

type Queue = Status | "All"

type Ticket = {
  id: string
  title: string
  device: string
  status: Status
  priority: Priority
  requester: string
  tint: string
}

const INITIAL_TICKETS: Ticket[] = [
  {
    id: "#4821",
    title: "Screen flickers",
    device: "Phone Pro",
    status: "Open",
    priority: "High",
    requester: "Jamie Rivera",
    tint: "bg-orange-500",
  },
  {
    id: "#4820",
    title: "Battery drains fast",
    device: "Book Air",
    status: "Pending",
    priority: "Medium",
    requester: "Morgan Lee",
    tint: "bg-cyan-500",
  },
  {
    id: "#4819",
    title: "Pods won't pair",
    device: "Pods Studio",
    status: "Open",
    priority: "Low",
    requester: "Avery Brooks",
    tint: "bg-pink-500",
  },
  {
    id: "#4818",
    title: "Cloud backup error",
    device: "Phone Air",
    status: "Solved",
    priority: "Medium",
    requester: "Taylor Kim",
    tint: "bg-green-500",
  },
]

const VIEWS: { value: Queue; icon: PhosphorIcon }[] = [
  { value: "Open", icon: ChatCircleIcon },
  { value: "Pending", icon: ChatCircleIcon },
  { value: "Solved", icon: CheckIcon },
  { value: "All", icon: TrayIcon },
]

const STATUSES: Status[] = ["Open", "Pending", "Solved"]

const PRIORITY_CLASS: Record<Priority, { box: string; text: string }> = {
  High: {
    box: "bg-destructive-10 dark:bg-destructive-20",
    text: "text-destructive",
  },
  Medium: {
    box: "bg-amber-500/10 dark:bg-amber-500/20",
    text: "text-amber-600 dark:text-amber-400",
  },
  Low: { box: "bg-secondary", text: "text-muted-foreground" },
}

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .join("")

function Requester({
  ticket,
  size,
  className,
}: {
  ticket: Ticket
  size: "sm" | "default"
  className?: string
}) {
  return (
    <Avatar size={size} className={className}>
      <AvatarFallback className={ticket.tint}>
        {initials(ticket.requester)}
      </AvatarFallback>
    </Avatar>
  )
}

function TicketDetail({
  ticket,
  onStatusChange,
  onReply,
}: {
  ticket: Ticket
  onStatusChange: (status: Status) => void
  onReply: (text: string) => void
}) {
  const [reply, setReply] = React.useState("")

  return (
    <AnimatedView
      key={ticket.id}
      entering={FadeInDown.duration(800).reduceMotion(ReduceMotion.System)}
      className="gap-4"
    >
      <Text className="text-sm text-muted-foreground">
        {`${ticket.id} · ${ticket.device}`}
      </Text>
      <ToggleGroup
        spacing={0}
        size="sm"
        value={[ticket.status]}
        onValueChange={(value) => {
          if (value[0]) onStatusChange(value[0] as Status)
        }}
        accessibilityLabel="Status"
      >
        {STATUSES.map((status) => (
          <ToggleGroupItem key={status} value={status}>
            <Text>{status}</Text>
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
      <Text role="heading" className="text-3xl font-semibold">
        {ticket.title}
      </Text>
      <View className="flex-row items-center gap-2.5">
        <Requester ticket={ticket} size="default" className="size-[2.125rem]" />
        <View>
          <Text className="text-sm font-semibold">{ticket.requester}</Text>
          <Text className="text-xs text-muted-foreground">Today 8:12 AM</Text>
        </View>
      </View>
      <View className="max-w-lg self-start rounded-2xl rounded-ss-sm bg-muted px-3.5 py-3">
        <Text className="text-base">
          Hi, this started right after the latest update. I tried restarting but
          it keeps happening. Can you help?
        </Text>
      </View>
      <View className="gap-2.5">
        <Textarea
          numberOfLines={3}
          value={reply}
          onChangeText={setReply}
          placeholder="Write a reply…"
          accessibilityLabel="Reply"
          className="min-h-20"
        />
        <View className="flex-row flex-wrap gap-2.5">
          <Button
            size="sm"
            disabled={!reply}
            onPress={() => {
              onReply(reply)
              setReply("")
              onStatusChange("Pending")
            }}
          >
            <Text>Send and set pending</Text>
          </Button>
          <Button
            size="sm"
            variant="outline"
            onPress={() => onStatusChange("Solved")}
          >
            <Text>Mark as solved</Text>
          </Button>
        </View>
      </View>
    </AnimatedView>
  )
}

function InboxSupport({
  onViewChange,
  onStatusChange,
  onReply,
}: {
  onViewChange?: (view: Status | "All") => void
  onStatusChange?: (id: string, status: Status) => void
  onReply?: (id: string, text: string) => void
}) {
  const { width } = useWindowDimensions()
  const wide = width >= 768
  const [tickets, setTickets] = React.useState(INITIAL_TICKETS)
  const [view, setView] = React.useState<Queue>("Open")
  const [selected, setSelected] = React.useState("#4821")
  const [column, setColumn] = React.useState<SplitViewColumn>("list")
  const list = tickets.filter(
    (ticket) => view === "All" || ticket.status === view
  )
  const current = tickets.find((ticket) => ticket.id === selected)

  const setStatus = (id: string, status: Status) => {
    onStatusChange?.(id, status)
    setTickets((items) =>
      items.map((ticket) => (ticket.id === id ? { ...ticket, status } : ticket))
    )
  }

  return (
    <Screen edges={["top", "right", "left"]}>
      <SplitView
        column={column}
        onColumnChange={setColumn}
        className={wide ? "m-4" : undefined}
      >
        <SplitViewSidebar
          title="Support"
          className={wide ? "min-h-full bg-muted" : "px-4 pb-8"}
        >
          {VIEWS.map((item) => (
            <SplitViewItem
              key={item.value}
              isActive={view === item.value}
              onPress={() => {
                setView(item.value)
                onViewChange?.(item.value)
              }}
              className={
                wide && view === item.value ? "bg-secondary" : undefined
              }
            >
              <View className="flex-row items-center gap-2.5">
                <Icon as={item.icon} className="size-5 text-primary" />
                <Text className="flex-1">{item.value}</Text>
                <Text className="text-xs text-muted-foreground tabular-nums">
                  {String(
                    item.value === "All"
                      ? tickets.length
                      : tickets.filter((ticket) => ticket.status === item.value)
                          .length
                  )}
                </Text>
              </View>
            </SplitViewItem>
          ))}
        </SplitViewSidebar>
        <SplitViewList
          title={view}
          className={wide ? "gap-1 p-2" : "gap-1 px-4 pb-8"}
        >
          {list.map((ticket) => {
            const priority = PRIORITY_CLASS[ticket.priority]
            return (
              <SplitViewItem
                key={ticket.id}
                isActive={ticket.id === selected}
                onPress={() => setSelected(ticket.id)}
                className="py-3"
              >
                <View className="flex-row items-center gap-2">
                  <Text className="flex-1 text-xs text-muted-foreground">
                    {`${ticket.id} · ${ticket.device}`}
                  </Text>
                  <View className={cn("rounded-md px-2 py-0.5", priority.box)}>
                    <Text
                      className={cn("text-xs font-semibold", priority.text)}
                    >
                      {ticket.priority}
                    </Text>
                  </View>
                </View>
                <Text className="text-base font-semibold">{ticket.title}</Text>
                <View className="mt-1 flex-row items-center gap-1.5">
                  <Requester ticket={ticket} size="sm" className="size-5" />
                  <Text className="text-xs text-muted-foreground">
                    {ticket.requester}
                  </Text>
                </View>
              </SplitViewItem>
            )
          })}
          {list.length === 0 ? (
            <Text className="p-6 text-center text-sm text-muted-foreground">
              No tickets
            </Text>
          ) : null}
        </SplitViewList>
        <SplitViewDetail className={wide ? "p-[1.375rem]" : "px-4 pb-8"}>
          {current ? (
            <TicketDetail
              ticket={current}
              onStatusChange={(status) => setStatus(current.id, status)}
              onReply={(text) => onReply?.(current.id, text)}
            />
          ) : null}
        </SplitViewDetail>
      </SplitView>
    </Screen>
  )
}

export { InboxSupport }
```

5. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Screen](https://ui.suiss.app/docs/components/screen), [Split View](https://ui.suiss.app/docs/patterns/split-view), [Text](https://ui.suiss.app/docs/components/text), [Textarea](https://ui.suiss.app/docs/components/textarea), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `stylesheet/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/stylesheet/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/inbox-support.tsx`:

```tsx title="stylesheet/components/blocks/inbox-support.tsx"
import {
  ChatCircleIcon,
  CheckIcon,
  type Icon as PhosphorIcon,
  TrayIcon,
} from "phosphor-react-native"
import * as React from "react"
import {
  StyleSheet,
  useWindowDimensions,
  View,
  type ViewStyle,
} from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import {
  SplitView,
  type SplitViewColumn,
  SplitViewDetail,
  SplitViewItem,
  SplitViewList,
  SplitViewSidebar,
} from "@/components/patterns/split-view"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
import { Textarea } from "@/components/ui/textarea"
import {
  fontSize,
  palette,
  radius,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

function withAlpha(color: string, alpha: number) {
  if (!color.startsWith("#") || color.length !== 7) return color
  const r = Number.parseInt(color.slice(1, 3), 16)
  const g = Number.parseInt(color.slice(3, 5), 16)
  const b = Number.parseInt(color.slice(5, 7), 16)
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

type Tone = keyof ThemeColors | keyof typeof palette

function tone(colors: ThemeColors, key: Tone) {
  return { ...colors, ...palette }[key]
}

type Tint = Tone

type Status = "Open" | "Pending" | "Solved"

type Priority = "High" | "Medium" | "Low"

type Queue = Status | "All"

type Ticket = {
  id: string
  title: string
  device: string
  status: Status
  priority: Priority
  requester: string
  tint: Tint
}

const INITIAL_TICKETS: Ticket[] = [
  {
    id: "#4821",
    title: "Screen flickers",
    device: "Phone Pro",
    status: "Open",
    priority: "High",
    requester: "Jamie Rivera",
    tint: "orange500",
  },
  {
    id: "#4820",
    title: "Battery drains fast",
    device: "Book Air",
    status: "Pending",
    priority: "Medium",
    requester: "Morgan Lee",
    tint: "cyan500",
  },
  {
    id: "#4819",
    title: "Pods won't pair",
    device: "Pods Studio",
    status: "Open",
    priority: "Low",
    requester: "Avery Brooks",
    tint: "pink500",
  },
  {
    id: "#4818",
    title: "Cloud backup error",
    device: "Phone Air",
    status: "Solved",
    priority: "Medium",
    requester: "Taylor Kim",
    tint: "green500",
  },
]

const VIEWS: { value: Queue; icon: PhosphorIcon }[] = [
  { value: "Open", icon: ChatCircleIcon },
  { value: "Pending", icon: ChatCircleIcon },
  { value: "Solved", icon: CheckIcon },
  { value: "All", icon: TrayIcon },
]

const STATUSES: Status[] = ["Open", "Pending", "Solved"]

function priorityColors(
  priority: Priority,
  colors: ThemeColors,
  isDark: boolean
) {
  if (priority === "High") {
    return {
      box: withAlpha(colors.destructive, isDark ? 0.2 : 0.1),
      text: colors.destructive,
    }
  }
  if (priority === "Medium") {
    return {
      box: withAlpha(palette.amber500, isDark ? 0.2 : 0.1),
      text: isDark ? palette.amber400 : palette.amber600,
    }
  }
  return { box: colors.secondary, text: colors.mutedForeground }
}

const styles = StyleSheet.create({
  detail: { gap: 16 },
  title: { ...fontSize["3xl"], fontWeight: "600" },
  requester: { flexDirection: "row", alignItems: "center", gap: 10 },
  semibold: { ...fontSize.sm, fontWeight: "600" },
  bubble: {
    maxWidth: 512,
    alignSelf: "flex-start",
    borderRadius: radius["2xl"],
    borderTopStartRadius: radius.sm,
    paddingHorizontal: 14,
    paddingVertical: 12,
  },
  replyBox: { gap: 10 },
  textarea: { minHeight: 80 },
  buttons: { flexDirection: "row", flexWrap: "wrap", gap: 10 },
  wide: { margin: 16 },
  sidebar: { paddingHorizontal: 16, paddingBottom: 32 },
  view: { flexDirection: "row", alignItems: "center", gap: 10 },
  fill: { flex: 1 },
  count: { ...fontSize.xs, fontVariant: ["tabular-nums"] },
  listWide: { gap: 4, padding: 8 },
  list: { gap: 4, paddingHorizontal: 16, paddingBottom: 32 },
  ticket: { paddingVertical: 12 },
  ticketTop: { flexDirection: "row", alignItems: "center", gap: 8 },
  ticketId: { ...fontSize.xs, flex: 1 },
  priority: {
    borderRadius: radius.md,
    paddingHorizontal: 8,
    paddingVertical: 2,
  },
  priorityText: { ...fontSize.xs, fontWeight: "600" },
  ticketTitle: { ...fontSize.base, fontWeight: "600" },
  ticketFooter: {
    marginTop: 4,
    flexDirection: "row",
    alignItems: "center",
    gap: 6,
  },
  empty: { ...fontSize.sm, padding: 24, textAlign: "center" },
  detailWide: { padding: 22 },
  detailCompact: { paddingHorizontal: 16, paddingBottom: 32 },
})

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .join("")

function Requester({
  ticket,
  size,
  style,
}: {
  ticket: Ticket
  size: "sm" | "default"
  style?: ViewStyle
}) {
  const { colors } = useTheme()
  return (
    <Avatar size={size} style={style}>
      <AvatarFallback style={{ backgroundColor: tone(colors, ticket.tint) }}>
        {initials(ticket.requester)}
      </AvatarFallback>
    </Avatar>
  )
}

function TicketDetail({
  ticket,
  onStatusChange,
  onReply,
}: {
  ticket: Ticket
  onStatusChange: (status: Status) => void
  onReply: (text: string) => void
}) {
  const { colors } = useTheme()
  const [reply, setReply] = React.useState("")

  return (
    <AnimatedView
      key={ticket.id}
      entering={FadeInDown.duration(800).reduceMotion(ReduceMotion.System)}
      style={styles.detail}
    >
      <Text style={[fontSize.sm, { color: colors.mutedForeground }]}>
        {`${ticket.id} · ${ticket.device}`}
      </Text>
      <ToggleGroup
        spacing={0}
        size="sm"
        value={[ticket.status]}
        onValueChange={(value) => {
          if (value[0]) onStatusChange(value[0] as Status)
        }}
        accessibilityLabel="Status"
      >
        {STATUSES.map((status) => (
          <ToggleGroupItem key={status} value={status}>
            <Text>{status}</Text>
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
      <Text role="heading" style={styles.title}>
        {ticket.title}
      </Text>
      <View style={styles.requester}>
        <Requester
          ticket={ticket}
          size="default"
          style={{ width: 34, height: 34 }}
        />
        <View>
          <Text style={styles.semibold}>{ticket.requester}</Text>
          <Text style={[fontSize.xs, { color: colors.mutedForeground }]}>
            Today 8:12 AM
          </Text>
        </View>
      </View>
      <View style={[styles.bubble, { backgroundColor: colors.muted }]}>
        <Text style={fontSize.base}>
          Hi, this started right after the latest update. I tried restarting but
          it keeps happening. Can you help?
        </Text>
      </View>
      <View style={styles.replyBox}>
        <Textarea
          numberOfLines={3}
          value={reply}
          onChangeText={setReply}
          placeholder="Write a reply…"
          accessibilityLabel="Reply"
          style={styles.textarea}
        />
        <View style={styles.buttons}>
          <Button
            size="sm"
            disabled={!reply}
            onPress={() => {
              onReply(reply)
              setReply("")
              onStatusChange("Pending")
            }}
          >
            <Text>Send and set pending</Text>
          </Button>
          <Button
            size="sm"
            variant="outline"
            onPress={() => onStatusChange("Solved")}
          >
            <Text>Mark as solved</Text>
          </Button>
        </View>
      </View>
    </AnimatedView>
  )
}

function InboxSupport({
  onViewChange,
  onStatusChange,
  onReply,
}: {
  onViewChange?: (view: Status | "All") => void
  onStatusChange?: (id: string, status: Status) => void
  onReply?: (id: string, text: string) => void
}) {
  const { colors, isDark } = useTheme()
  const { width } = useWindowDimensions()
  const wide = width >= 768
  const [tickets, setTickets] = React.useState(INITIAL_TICKETS)
  const [view, setView] = React.useState<Queue>("Open")
  const [selected, setSelected] = React.useState("#4821")
  const [column, setColumn] = React.useState<SplitViewColumn>("list")
  const list = tickets.filter(
    (ticket) => view === "All" || ticket.status === view
  )
  const current = tickets.find((ticket) => ticket.id === selected)

  const setStatus = (id: string, status: Status) => {
    onStatusChange?.(id, status)
    setTickets((items) =>
      items.map((ticket) => (ticket.id === id ? { ...ticket, status } : ticket))
    )
  }

  return (
    <Screen edges={["top", "right", "left"]}>
      <SplitView
        column={column}
        onColumnChange={setColumn}
        style={wide ? styles.wide : undefined}
      >
        <SplitViewSidebar
          title="Support"
          style={
            wide
              ? { minHeight: "100%", backgroundColor: colors.muted }
              : styles.sidebar
          }
        >
          {VIEWS.map((item) => (
            <SplitViewItem
              key={item.value}
              isActive={view === item.value}
              onPress={() => {
                setView(item.value)
                onViewChange?.(item.value)
              }}
              style={
                wide && view === item.value
                  ? { backgroundColor: colors.secondary }
                  : undefined
              }
            >
              <View style={styles.view}>
                <Icon as={item.icon} size={20} color={colors.primary} />
                <Text style={styles.fill}>{item.value}</Text>
                <Text style={[styles.count, { color: colors.mutedForeground }]}>
                  {String(
                    item.value === "All"
                      ? tickets.length
                      : tickets.filter((ticket) => ticket.status === item.value)
                          .length
                  )}
                </Text>
              </View>
            </SplitViewItem>
          ))}
        </SplitViewSidebar>
        <SplitViewList
          title={view}
          style={wide ? styles.listWide : styles.list}
        >
          {list.map((ticket) => {
            const priority = priorityColors(ticket.priority, colors, isDark)
            return (
              <SplitViewItem
                key={ticket.id}
                isActive={ticket.id === selected}
                onPress={() => setSelected(ticket.id)}
                style={styles.ticket}
              >
                <View style={styles.ticketTop}>
                  <Text
                    style={[styles.ticketId, { color: colors.mutedForeground }]}
                  >
                    {`${ticket.id} · ${ticket.device}`}
                  </Text>
                  <View
                    style={[styles.priority, { backgroundColor: priority.box }]}
                  >
                    <Text
                      style={[styles.priorityText, { color: priority.text }]}
                    >
                      {ticket.priority}
                    </Text>
                  </View>
                </View>
                <Text style={styles.ticketTitle}>{ticket.title}</Text>
                <View style={styles.ticketFooter}>
                  <Requester
                    ticket={ticket}
                    size="sm"
                    style={{ width: 20, height: 20 }}
                  />
                  <Text
                    style={[fontSize.xs, { color: colors.mutedForeground }]}
                  >
                    {ticket.requester}
                  </Text>
                </View>
              </SplitViewItem>
            )
          })}
          {list.length === 0 ? (
            <Text style={[styles.empty, { color: colors.mutedForeground }]}>
              No tickets
            </Text>
          ) : null}
        </SplitViewList>
        <SplitViewDetail
          style={wide ? styles.detailWide : styles.detailCompact}
        >
          {current ? (
            <TicketDetail
              ticket={current}
              onStatusChange={(status) => setStatus(current.id, status)}
              onReply={(text) => onReply?.(current.id, text)}
            />
          ) : null}
        </SplitViewDetail>
      </SplitView>
    </Screen>
  )
}

export { InboxSupport }
```

5. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

- `swift/Blocks/InboxSupport.swift`: shown above

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/Checkbox.swift`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `swift/Components/DropdownMenu.swift`: see [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu.md)

- `swift/Components/Field.swift`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `swift/Components/Input.swift`: see [Input](https://ui.suiss.app/docs/components/input.md)

- `swift/Components/KeyboardScrollView.swift`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

- `swift/Components/Overlay.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/Overlay.swift)

- `swift/Components/Screen.swift`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `swift/Components/Select.swift`: see [Select](https://ui.suiss.app/docs/components/select.md)

- `swift/Components/Separator.swift`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

- `swift/Components/Textarea.swift`: see [Textarea](https://ui.suiss.app/docs/components/textarea.md)

- `swift/Components/Toggle.swift`: see [Toggle](https://ui.suiss.app/docs/components/toggle.md)

- `swift/Components/ToggleGroup.swift`: see [Toggle Group](https://ui.suiss.app/docs/components/toggle-group.md)

- `swift/Interactions/SwipeActions.swift`: see [Swipe Actions](https://ui.suiss.app/docs/interactions/swipe-actions.md)

- `swift/Patterns/SplitView.swift`: see [Split View](https://ui.suiss.app/docs/patterns/split-view.md)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

- `kotlin/blocks/InboxSupport.kt`: shown above

- `kotlin/components/Button.kt`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Checkbox.kt`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Field.kt`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `kotlin/components/FormSupport.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/FormSupport.kt)

- `kotlin/components/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/Label.kt`: see [Label](https://ui.suiss.app/docs/components/label.md)

- `kotlin/components/Screen.kt`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `kotlin/components/Separator.kt`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `kotlin/components/Textarea.kt`: see [Textarea](https://ui.suiss.app/docs/components/textarea.md)

- `kotlin/components/Toggle.kt`: see [Toggle](https://ui.suiss.app/docs/components/toggle.md)

- `kotlin/components/ToggleGroup.kt`: see [Toggle Group](https://ui.suiss.app/docs/components/toggle-group.md)

- `kotlin/interactions/SwipeActions.kt`: see [Swipe Actions](https://ui.suiss.app/docs/interactions/swipe-actions.md)

- `kotlin/patterns/SplitView.kt`: see [Split View](https://ui.suiss.app/docs/patterns/split-view.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { InboxSupport } from "@/components/blocks/inbox-support"
```

```tsx
<InboxSupport />
```

Replace `INITIAL_TICKETS` with your tickets. Wire `onViewChange`, `onStatusChange` and `onReply` to your help desk backend; the screen also updates its own ticket list.

