# Dash Store

A store admin overview with sales stats, an online versus in-store bar chart, and a filterable orders list.

Dash Store 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/dash-store

**React**

**Example**

```tsx title="components/blocks/dash-store.tsx"
"use client"

import {
  ChartBarIcon,
  HandbagIcon,
  HouseIcon,
  StorefrontIcon,
  UsersIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import {
  Bar,
  BarChart,
  CartesianGrid,
  Cell,
  useActiveTooltipDataPoints,
  XAxis,
} from "recharts"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
} from "@/components/ui/chart"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value: string
  onValueChange: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          onValueChange(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const items: NavItem[] = [
  {
    value: "overview",
    label: "Overview",
    icon: (
      <HouseIcon />
    ),
  },
  {
    value: "orders",
    label: "Orders",
    icon: (
      <HandbagIcon />
    ),
    badge: 12,
  },
  {
    value: "customers",
    label: "Customers",
    icon: (
      <UsersIcon />
    ),
  },
  {
    value: "reports",
    label: "Reports",
    icon: (
      <ChartBarIcon />
    ),
  },
  ...["Union Square", "Fifth Avenue", "Michigan Avenue"].map((store) => ({
    value: store,
    label: store,
    icon: (
      <StorefrontIcon />
    ),
    group: "Stores",
  })),
]

const ranges = [
  { value: "24h", label: "24 Hours", scale: 0.15 },
  { value: "7d", label: "7 Days", scale: 1 },
  { value: "30d", label: "30 Days", scale: 4.3 },
]

const week = [
  { day: "Mon", online: 42, store: 46 },
  { day: "Tue", online: 58, store: 44 },
  { day: "Wed", online: 50, store: 56 },
  { day: "Thu", online: 68, store: 54 },
  { day: "Fri", online: 64, store: 70 },
  { day: "Sat", online: 80, store: 62 },
  { day: "Sun", online: 72, store: 76 },
]

const chartConfig = {
  online: { label: "Online", color: "var(--chart-1)" },
  store: { label: "In store", color: "var(--chart-2)" },
} satisfies ChartConfig

const orders = [
  {
    id: "W1048",
    name: "Jamie Rivera",
    product: "Phone Pro",
    status: "Shipped",
    total: 999,
    hue: 250,
  },
  {
    id: "W1047",
    name: "Morgan Lee",
    product: "Book Air",
    status: "Processing",
    total: 1099,
    hue: 30,
  },
  {
    id: "W1046",
    name: "Riley Chen",
    product: "Watch Series 11",
    status: "Delivered",
    total: 399,
    hue: 150,
  },
  {
    id: "W1045",
    name: "Avery Brooks",
    product: "Pods Studio",
    status: "Returned",
    total: 549,
    hue: 330,
  },
  {
    id: "W1044",
    name: "Taylor Kim",
    product: "Pad Air",
    status: "Delivered",
    total: 599,
    hue: 200,
  },
]

const filters = ["All", "Shipped", "Delivered", "Returned"]

const money = (value: number) =>
  value.toLocaleString("en-US", {
    style: "currency",
    currency: "USD",
    maximumFractionDigits: 0,
  })

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

function ActiveDay({ onChange }: { onChange: (index: number | null) => void }) {
  const points = useActiveTooltipDataPoints<{ index?: number }>()
  const index = points?.[0]?.index ?? null

  React.useEffect(() => onChange(index), [index, onChange])

  return null
}

export function DashStore() {
  const [section, setSection] = React.useState("overview")
  const [range, setRange] = React.useState("7d")
  const [filter, setFilter] = React.useState("All")
  const [active, setActive] = React.useState<number | null>(null)
  const scale = ranges.find((item) => item.value === range)?.scale ?? 1
  const data = week.map((day, index) => ({ ...day, index }))
  const activeDay = active === null ? undefined : week[active]
  const title = items.find((item) => item.value === section)?.label

  const stats = [
    {
      label: "Revenue",
      value: money(1284500 * scale),
      change: "+12.4%",
      up: true,
    },
    {
      label: "Orders",
      value: Math.round(842 * scale).toLocaleString("en-US"),
      change: "+8.1%",
      up: true,
    },
    { label: "Average order", value: money(1525), change: "−2.3%", up: false },
    { label: "Conversion", value: "3.42%", change: "+0.4 pts", up: true },
  ]

  return (
    <SidebarProvider>
      <NavSidebar
        items={items}
        value={section}
        onValueChange={setSection}
        header={
          <p className="flex items-center gap-2 px-2 pt-1 text-base font-semibold">
            <span className="font-bold tracking-tight">suiss</span>
            Store Admin
          </p>
        }
        footer={
          <div className="flex items-center gap-2.5 px-2 pb-1">
            <Avatar>
              <AvatarFallback>JR</AvatarFallback>
            </Avatar>
            <div className="flex flex-col">
              <span className="text-sm font-semibold">Jamie Rivera</span>
              <span className="text-xs text-muted-foreground">Admin</span>
            </div>
          </div>
        }
      />
      <SidebarInset>
        <div className="flex flex-col gap-5 px-5 py-5.5 md:px-7">
          <div className="flex flex-wrap items-start gap-4">
            <SidebarTrigger className="-ms-2 mt-1" />
            <div className="flex flex-1 flex-col">
              <h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
              <p className="text-sm text-muted-foreground">All stores</p>
            </div>
            <ToggleGroup
              spacing={0}
              value={[range]}
              onValueChange={(value: string[]) => {
                if (value[0]) setRange(value[0])
              }}
              aria-label="Range"
            >
              {ranges.map((item) => (
                <ToggleGroupItem key={item.value} value={item.value} size="sm">
                  {item.label}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </div>
          <dl className="grid grid-cols-2 overflow-hidden rounded-xl bg-muted">
            {stats.map((stat, index) => (
              <div
                key={stat.label}
                data-index={index}
                className="flex flex-col gap-0.5 border-border px-4 py-3.5 data-[index='1']:border-s data-[index='2']:border-t data-[index='3']:border-s data-[index='3']:border-t"
              >
                <dt className="text-sm text-muted-foreground">{stat.label}</dt>
                <dd className="text-2xl font-semibold tracking-tight tabular-nums max-sm:text-xl">
                  {stat.value}
                </dd>
                <dd
                  data-up={stat.up ? "" : undefined}
                  className="text-xs text-[color-mix(in_oklab,var(--color-red-500),var(--foreground)_25%)] data-up:text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-red-500 dark:data-up:text-green-500"
                >
                  {stat.change}
                  <span className="max-sm:sr-only"> vs. previous period</span>
                </dd>
              </div>
            ))}
          </dl>
          <div className="rounded-xl bg-muted px-4 pt-3.5 pb-2">
            <div className="flex items-start gap-2">
              <div className="flex flex-1 flex-col" aria-live="polite">
                <span className="text-sm text-muted-foreground">
                  {activeDay ? activeDay.day : "Weekly sales"}
                </span>
                <span className="text-2xl font-semibold tabular-nums">
                  {activeDay
                    ? money((activeDay.online + activeDay.store) * 1000)
                    : money(1284500 * scale)}
                </span>
              </div>
              <div className="flex gap-3 text-xs text-muted-foreground">
                {(["online", "store"] as const).map((key) => (
                  <span key={key} className="flex items-center gap-1.25">
                    <span
                      style={{ background: chartConfig[key].color }}
                      className="size-2 rounded-full"
                    />
                    {chartConfig[key].label}
                  </span>
                ))}
              </div>
            </div>
            <ChartContainer
              config={chartConfig}
              className="mt-3.5 aspect-auto h-40 w-full"
            >
              <BarChart
                accessibilityLayer
                data={data}
                barGap={3}
                margin={{ top: 4, right: 0, left: 0, bottom: 0 }}
              >
                <CartesianGrid vertical={false} strokeDasharray="3 3" />
                <XAxis
                  dataKey="day"
                  tickLine={false}
                  axisLine={false}
                  tickMargin={6}
                />
                <ChartTooltip content={() => null} cursor={false} />
                {(["online", "store"] as const).map((key) => (
                  <Bar
                    key={key}
                    dataKey={key}
                    fill={`var(--color-${key})`}
                    radius={3}
                    maxBarSize={12}
                    isAnimationActive={false}
                  >
                    {data.map((day) => (
                      <Cell
                        key={day.day}
                        fillOpacity={
                          active === null || active === day.index ? 1 : 0.35
                        }
                        className="transition-[fill-opacity] duration-200"
                      />
                    ))}
                  </Bar>
                ))}
                <ActiveDay onChange={setActive} />
              </BarChart>
            </ChartContainer>
          </div>
          <div className="flex flex-wrap items-center gap-3">
            <h2 className="flex-1 px-1 text-sm font-semibold">Recent orders</h2>
            <ToggleGroup
              spacing={0}
              value={[filter]}
              onValueChange={(value: string[]) => {
                if (value[0]) setFilter(value[0])
              }}
              aria-label="Order status"
            >
              {filters.map((item) => (
                <ToggleGroupItem key={item} value={item} size="sm">
                  {item}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </div>
          <ItemGroup
            variant="inset"
            className="**:data-[slot=item-separator]:ms-15"
          >
            {orders
              .filter((order) => filter === "All" || order.status === filter)
              .map((order, index) => (
                <React.Fragment key={`${filter}-${order.id}`}>
                  {index > 0 && <ItemSeparator />}
                  <Item
                    size="sm"
                    role="listitem"
                    className="transition-[opacity,translate] duration-700 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-1.5 starting:opacity-0 motion-reduce:transition-none"
                  >
                    <Avatar>
                      <AvatarFallback
                        style={{
                          background: `linear-gradient(160deg, oklch(0.78 0.1 ${order.hue}), oklch(0.6 0.14 ${order.hue + 30}))`,
                        }}
                        className="text-white"
                      >
                        {initials(order.name)}
                      </AvatarFallback>
                    </Avatar>
                    <ItemContent className="gap-0">
                      <ItemTitle className="font-semibold">
                        {order.name}
                      </ItemTitle>
                      <ItemDescription className="text-sm">
                        {order.product} · {order.id}
                      </ItemDescription>
                    </ItemContent>
                    <ItemActions className="gap-4 text-sm">
                      <span
                        data-returned={
                          order.status === "Returned" ? "" : undefined
                        }
                        className="hidden w-24 text-end text-muted-foreground data-returned:text-destructive sm:block"
                      >
                        {order.status}
                      </span>
                      <span className="w-20 text-end text-base tabular-nums">
                        {money(order.total)}
                      </span>
                    </ItemActions>
                  </Item>
                </React.Fragment>
              ))}
          </ItemGroup>
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

A store admin home with sales stats for the chosen range, a bar chart comparing online and in-store sales by day and a recent orders list filtered by status. Hovering a day dims the other bars and updates the chart headline. Order status shows from small tablets up, and the `Sidebar` navigation opens as a sheet from the trigger on mobile. A good base for e-commerce, point-of-sale or bookings dashboards.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/dash-store
```

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 dash-store
```

### Manual

1. Install the dependencies:

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

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Chart](https://ui.suiss.app/docs/components/chart), [Item](https://ui.suiss.app/docs/components/item), [Sidebar](https://ui.suiss.app/docs/components/sidebar), [Toggle Group](https://ui.suiss.app/docs/components/toggle-group).

3. Copy the source into `components/blocks/dash-store.tsx`:

```tsx title="components/blocks/dash-store.tsx"
"use client"

import {
  ChartBarIcon,
  HandbagIcon,
  HouseIcon,
  StorefrontIcon,
  UsersIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import {
  Bar,
  BarChart,
  CartesianGrid,
  Cell,
  useActiveTooltipDataPoints,
  XAxis,
} from "recharts"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
} from "@/components/ui/chart"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value: string
  onValueChange: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          onValueChange(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const items: NavItem[] = [
  {
    value: "overview",
    label: "Overview",
    icon: (
      <HouseIcon />
    ),
  },
  {
    value: "orders",
    label: "Orders",
    icon: (
      <HandbagIcon />
    ),
    badge: 12,
  },
  {
    value: "customers",
    label: "Customers",
    icon: (
      <UsersIcon />
    ),
  },
  {
    value: "reports",
    label: "Reports",
    icon: (
      <ChartBarIcon />
    ),
  },
  ...["Union Square", "Fifth Avenue", "Michigan Avenue"].map((store) => ({
    value: store,
    label: store,
    icon: (
      <StorefrontIcon />
    ),
    group: "Stores",
  })),
]

const ranges = [
  { value: "24h", label: "24 Hours", scale: 0.15 },
  { value: "7d", label: "7 Days", scale: 1 },
  { value: "30d", label: "30 Days", scale: 4.3 },
]

const week = [
  { day: "Mon", online: 42, store: 46 },
  { day: "Tue", online: 58, store: 44 },
  { day: "Wed", online: 50, store: 56 },
  { day: "Thu", online: 68, store: 54 },
  { day: "Fri", online: 64, store: 70 },
  { day: "Sat", online: 80, store: 62 },
  { day: "Sun", online: 72, store: 76 },
]

const chartConfig = {
  online: { label: "Online", color: "var(--chart-1)" },
  store: { label: "In store", color: "var(--chart-2)" },
} satisfies ChartConfig

const orders = [
  {
    id: "W1048",
    name: "Jamie Rivera",
    product: "Phone Pro",
    status: "Shipped",
    total: 999,
    hue: 250,
  },
  {
    id: "W1047",
    name: "Morgan Lee",
    product: "Book Air",
    status: "Processing",
    total: 1099,
    hue: 30,
  },
  {
    id: "W1046",
    name: "Riley Chen",
    product: "Watch Series 11",
    status: "Delivered",
    total: 399,
    hue: 150,
  },
  {
    id: "W1045",
    name: "Avery Brooks",
    product: "Pods Studio",
    status: "Returned",
    total: 549,
    hue: 330,
  },
  {
    id: "W1044",
    name: "Taylor Kim",
    product: "Pad Air",
    status: "Delivered",
    total: 599,
    hue: 200,
  },
]

const filters = ["All", "Shipped", "Delivered", "Returned"]

const money = (value: number) =>
  value.toLocaleString("en-US", {
    style: "currency",
    currency: "USD",
    maximumFractionDigits: 0,
  })

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

function ActiveDay({ onChange }: { onChange: (index: number | null) => void }) {
  const points = useActiveTooltipDataPoints<{ index?: number }>()
  const index = points?.[0]?.index ?? null

  React.useEffect(() => onChange(index), [index, onChange])

  return null
}

export function DashStore() {
  const [section, setSection] = React.useState("overview")
  const [range, setRange] = React.useState("7d")
  const [filter, setFilter] = React.useState("All")
  const [active, setActive] = React.useState<number | null>(null)
  const scale = ranges.find((item) => item.value === range)?.scale ?? 1
  const data = week.map((day, index) => ({ ...day, index }))
  const activeDay = active === null ? undefined : week[active]
  const title = items.find((item) => item.value === section)?.label

  const stats = [
    {
      label: "Revenue",
      value: money(1284500 * scale),
      change: "+12.4%",
      up: true,
    },
    {
      label: "Orders",
      value: Math.round(842 * scale).toLocaleString("en-US"),
      change: "+8.1%",
      up: true,
    },
    { label: "Average order", value: money(1525), change: "−2.3%", up: false },
    { label: "Conversion", value: "3.42%", change: "+0.4 pts", up: true },
  ]

  return (
    <SidebarProvider>
      <NavSidebar
        items={items}
        value={section}
        onValueChange={setSection}
        header={
          <p className="flex items-center gap-2 px-2 pt-1 text-base font-semibold">
            <span className="font-bold tracking-tight">suiss</span>
            Store Admin
          </p>
        }
        footer={
          <div className="flex items-center gap-2.5 px-2 pb-1">
            <Avatar>
              <AvatarFallback>JR</AvatarFallback>
            </Avatar>
            <div className="flex flex-col">
              <span className="text-sm font-semibold">Jamie Rivera</span>
              <span className="text-xs text-muted-foreground">Admin</span>
            </div>
          </div>
        }
      />
      <SidebarInset>
        <div className="flex flex-col gap-5 px-5 py-5.5 md:px-7">
          <div className="flex flex-wrap items-start gap-4">
            <SidebarTrigger className="-ms-2 mt-1" />
            <div className="flex flex-1 flex-col">
              <h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
              <p className="text-sm text-muted-foreground">All stores</p>
            </div>
            <ToggleGroup
              spacing={0}
              value={[range]}
              onValueChange={(value: string[]) => {
                if (value[0]) setRange(value[0])
              }}
              aria-label="Range"
            >
              {ranges.map((item) => (
                <ToggleGroupItem key={item.value} value={item.value} size="sm">
                  {item.label}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </div>
          <dl className="grid grid-cols-2 overflow-hidden rounded-xl bg-muted">
            {stats.map((stat, index) => (
              <div
                key={stat.label}
                data-index={index}
                className="flex flex-col gap-0.5 border-border px-4 py-3.5 data-[index='1']:border-s data-[index='2']:border-t data-[index='3']:border-s data-[index='3']:border-t"
              >
                <dt className="text-sm text-muted-foreground">{stat.label}</dt>
                <dd className="text-2xl font-semibold tracking-tight tabular-nums max-sm:text-xl">
                  {stat.value}
                </dd>
                <dd
                  data-up={stat.up ? "" : undefined}
                  className="text-xs text-[color-mix(in_oklab,var(--color-red-500),var(--foreground)_25%)] data-up:text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-red-500 dark:data-up:text-green-500"
                >
                  {stat.change}
                  <span className="max-sm:sr-only"> vs. previous period</span>
                </dd>
              </div>
            ))}
          </dl>
          <div className="rounded-xl bg-muted px-4 pt-3.5 pb-2">
            <div className="flex items-start gap-2">
              <div className="flex flex-1 flex-col" aria-live="polite">
                <span className="text-sm text-muted-foreground">
                  {activeDay ? activeDay.day : "Weekly sales"}
                </span>
                <span className="text-2xl font-semibold tabular-nums">
                  {activeDay
                    ? money((activeDay.online + activeDay.store) * 1000)
                    : money(1284500 * scale)}
                </span>
              </div>
              <div className="flex gap-3 text-xs text-muted-foreground">
                {(["online", "store"] as const).map((key) => (
                  <span key={key} className="flex items-center gap-1.25">
                    <span
                      style={{ background: chartConfig[key].color }}
                      className="size-2 rounded-full"
                    />
                    {chartConfig[key].label}
                  </span>
                ))}
              </div>
            </div>
            <ChartContainer
              config={chartConfig}
              className="mt-3.5 aspect-auto h-40 w-full"
            >
              <BarChart
                accessibilityLayer
                data={data}
                barGap={3}
                margin={{ top: 4, right: 0, left: 0, bottom: 0 }}
              >
                <CartesianGrid vertical={false} strokeDasharray="3 3" />
                <XAxis
                  dataKey="day"
                  tickLine={false}
                  axisLine={false}
                  tickMargin={6}
                />
                <ChartTooltip content={() => null} cursor={false} />
                {(["online", "store"] as const).map((key) => (
                  <Bar
                    key={key}
                    dataKey={key}
                    fill={`var(--color-${key})`}
                    radius={3}
                    maxBarSize={12}
                    isAnimationActive={false}
                  >
                    {data.map((day) => (
                      <Cell
                        key={day.day}
                        fillOpacity={
                          active === null || active === day.index ? 1 : 0.35
                        }
                        className="transition-[fill-opacity] duration-200"
                      />
                    ))}
                  </Bar>
                ))}
                <ActiveDay onChange={setActive} />
              </BarChart>
            </ChartContainer>
          </div>
          <div className="flex flex-wrap items-center gap-3">
            <h2 className="flex-1 px-1 text-sm font-semibold">Recent orders</h2>
            <ToggleGroup
              spacing={0}
              value={[filter]}
              onValueChange={(value: string[]) => {
                if (value[0]) setFilter(value[0])
              }}
              aria-label="Order status"
            >
              {filters.map((item) => (
                <ToggleGroupItem key={item} value={item} size="sm">
                  {item}
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </div>
          <ItemGroup
            variant="inset"
            className="**:data-[slot=item-separator]:ms-15"
          >
            {orders
              .filter((order) => filter === "All" || order.status === filter)
              .map((order, index) => (
                <React.Fragment key={`${filter}-${order.id}`}>
                  {index > 0 && <ItemSeparator />}
                  <Item
                    size="sm"
                    role="listitem"
                    className="transition-[opacity,translate] duration-700 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-1.5 starting:opacity-0 motion-reduce:transition-none"
                  >
                    <Avatar>
                      <AvatarFallback
                        style={{
                          background: `linear-gradient(160deg, oklch(0.78 0.1 ${order.hue}), oklch(0.6 0.14 ${order.hue + 30}))`,
                        }}
                        className="text-white"
                      >
                        {initials(order.name)}
                      </AvatarFallback>
                    </Avatar>
                    <ItemContent className="gap-0">
                      <ItemTitle className="font-semibold">
                        {order.name}
                      </ItemTitle>
                      <ItemDescription className="text-sm">
                        {order.product} · {order.id}
                      </ItemDescription>
                    </ItemContent>
                    <ItemActions className="gap-4 text-sm">
                      <span
                        data-returned={
                          order.status === "Returned" ? "" : undefined
                        }
                        className="hidden w-24 text-end text-muted-foreground data-returned:text-destructive sm:block"
                      >
                        {order.status}
                      </span>
                      <span className="w-20 text-end text-base tabular-nums">
                        {money(order.total)}
                      </span>
                    </ItemActions>
                  </Item>
                </React.Fragment>
              ))}
          </ItemGroup>
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { DashStore } from "@/components/blocks/dash-store"
```

```tsx
<DashStore />
```

Replace `week` with your daily online and in-store series and `orders` with recent orders, each with an id, customer name, product, status, total and avatar hue. The stats and change percentages are fixed numbers multiplied by the range factor in `ranges`, so fetch real values per range from your backend. `items` sets the navigation.

## Notes

The chart supports keyboard navigation, and its headline is announced as you move through the days.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx title="uniwind/components/blocks/dash-store.tsx"
import { cn } from "cn"
import {
  CaretDownIcon,
  ChartBarIcon,
  HandbagIcon,
  HouseIcon,
  type Icon as PhosphorIcon,
  StorefrontIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Bar,
  BarChart,
  CartesianGrid,
  Cell,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  XAxis,
} from "@/components/ui/chart"
import { Chip, ChipGroup } from "@/components/ui/chip"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Header,
  HeaderActions,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const SECTIONS: {
  value: string
  label: string
  icon: PhosphorIcon
  badge?: number
}[] = [
  { value: "overview", label: "Overview", icon: HouseIcon },
  { value: "orders", label: "Orders", icon: HandbagIcon, badge: 12 },
  { value: "customers", label: "Customers", icon: UsersIcon },
  { value: "reports", label: "Reports", icon: ChartBarIcon },
]

const STORES = ["All stores", "Union Square", "Fifth Avenue", "Michigan Avenue"]

const ranges = [
  { value: "24h", label: "24 Hours", scale: 0.15 },
  { value: "7d", label: "7 Days", scale: 1 },
  { value: "30d", label: "30 Days", scale: 4.3 },
]

const week = [
  { day: "Mon", online: 42, store: 46 },
  { day: "Tue", online: 58, store: 44 },
  { day: "Wed", online: 50, store: 56 },
  { day: "Thu", online: 68, store: 54 },
  { day: "Fri", online: 64, store: 70 },
  { day: "Sat", online: 80, store: 62 },
  { day: "Sun", online: 72, store: 76 },
]

const chartConfig = {
  online: { label: "Online", color: "chart-1" },
  store: { label: "In store", color: "chart-2" },
} satisfies ChartConfig

const SERIES = [
  { key: "online", swatch: "bg-chart-1" },
  { key: "store", swatch: "bg-chart-2" },
] as const

const orders = [
  {
    id: "W1048",
    name: "Jamie Rivera",
    product: "Phone Pro",
    status: "Shipped",
    total: 999,
    tint: "bg-blue-500",
  },
  {
    id: "W1047",
    name: "Morgan Lee",
    product: "Book Air",
    status: "Processing",
    total: 1099,
    tint: "bg-orange-500",
  },
  {
    id: "W1046",
    name: "Riley Chen",
    product: "Watch Series 11",
    status: "Delivered",
    total: 399,
    tint: "bg-green-500",
  },
  {
    id: "W1045",
    name: "Avery Brooks",
    product: "Pods Studio",
    status: "Returned",
    total: 549,
    tint: "bg-pink-500",
  },
  {
    id: "W1044",
    name: "Taylor Kim",
    product: "Pad Air",
    status: "Delivered",
    total: 599,
    tint: "bg-cyan-500",
  },
]

const filters = ["All", "Shipped", "Delivered", "Returned"]

const THOUSANDS = /\B(?=(\d{3})+(?!\d))/g

function money(value: number) {
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
      maximumFractionDigits: 0,
    }).format(value)
  } catch {
    return `$${Math.round(value).toString().replace(THOUSANDS, ",")}`
  }
}

function count(value: number) {
  try {
    return new Intl.NumberFormat("en-US").format(value)
  } catch {
    return String(value).replace(THOUSANDS, ",")
  }
}

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

function StoreMenu({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        render={<Button variant="secondary" size="sm" />}
        accessibilityLabel={`Store, ${value}`}
      >
        <Icon as={StorefrontIcon} className="size-4" />
        <Icon as={CaretDownIcon} weight="bold" className="size-3" />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuRadioGroup
          value={value}
          onValueChange={(next) => {
            if (typeof next === "string") onValueChange(next)
          }}
        >
          {STORES.map((store) => (
            <DropdownMenuRadioItem key={store} value={store}>
              <Text>{store}</Text>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function OtherSection({ value }: { value: string }) {
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View className="flex-1 gap-2 px-4 pt-13">
      <Text role="heading" className="text-3xl font-bold">
        {label}
      </Text>
      <Text className="text-lg text-muted-foreground">
        This section isn't part of the sample. Choose Overview.
      </Text>
    </View>
  )
}

function DashStore({
  onRangeChange,
  onFilterChange,
  onStoreChange,
  onOrderPress,
  onNavigate,
}: {
  onRangeChange?: (range: string) => void
  onFilterChange?: (filter: string) => void
  onStoreChange?: (store: string) => void
  onOrderPress?: (id: string) => void
  onNavigate?: (section: string) => void
}) {
  const [section, setSection] = React.useState("overview")
  const [store, setStore] = React.useState("All stores")
  const [range, setRange] = React.useState("7d")
  const [filter, setFilter] = React.useState("All")
  const [active, setActive] = React.useState<number | null>(null)
  const scroll = useHeaderScroll()
  const scale = ranges.find((item) => item.value === range)?.scale ?? 1
  const data = week.map((day, index) => ({ ...day, index }))
  const activeDay = active === null ? undefined : week[active]
  const list = orders.filter(
    (order) => filter === "All" || order.status === filter
  )

  const stats = [
    {
      label: "Revenue",
      value: money(1284500 * scale),
      change: "+12.4%",
      up: true,
    },
    {
      label: "Orders",
      value: count(Math.round(842 * scale)),
      change: "+8.1%",
      up: true,
    },
    { label: "Average order", value: money(1525), change: "−2.3%", up: false },
    { label: "Conversion", value: "3.42%", change: "+0.4 pts", up: true },
  ]

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "overview" ? (
        <>
          <Header scroll={scroll}>
            <HeaderTitle>Overview</HeaderTitle>
            <HeaderActions>
              <StoreMenu
                value={store}
                onValueChange={(value) => {
                  setStore(value)
                  onStoreChange?.(value)
                }}
              />
            </HeaderActions>
          </Header>
          <HeaderScrollView scroll={scroll}>
            <HeaderLargeTitle subtitle={store}>Overview</HeaderLargeTitle>
            <View className="gap-5 px-4 pt-3 pb-8">
              <ToggleGroup
                spacing={0}
                value={[range]}
                onValueChange={(value) => {
                  if (!value[0]) return
                  setRange(value[0])
                  onRangeChange?.(value[0])
                }}
                accessibilityLabel="Range"
                className="self-stretch"
              >
                {ranges.map((item) => (
                  <ToggleGroupItem
                    key={item.value}
                    value={item.value}
                    size="sm"
                    className="flex-1"
                  >
                    <Text>{item.label}</Text>
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
              <View className="flex-row flex-wrap overflow-hidden rounded-xl bg-muted">
                {stats.map((stat, index) => (
                  <View
                    key={stat.label}
                    accessible
                    className={cn(
                      "w-1/2 gap-0.5 border-border px-4 py-3.5",
                      index % 2 === 1 && "border-s",
                      index > 1 && "border-t"
                    )}
                  >
                    <Text className="text-sm text-muted-foreground">
                      {stat.label}
                    </Text>
                    <Text
                      numberOfLines={1}
                      adjustsFontSizeToFit
                      className="text-2xl font-semibold tracking-tight tabular-nums"
                    >
                      {stat.value}
                    </Text>
                    <Text
                      className={cn(
                        "text-xs",
                        stat.up ? "text-green-500" : "text-red-500"
                      )}
                    >
                      {`${stat.change} vs. previous period`}
                    </Text>
                  </View>
                ))}
              </View>
              <View className="rounded-xl bg-muted px-4 pt-3.5 pb-2">
                <View className="flex-row items-start gap-2">
                  <View
                    accessibilityLiveRegion="polite"
                    className="min-w-0 flex-1"
                  >
                    <Text className="text-sm text-muted-foreground">
                      {activeDay ? activeDay.day : "Weekly sales"}
                    </Text>
                    <Text className="text-2xl font-semibold tabular-nums">
                      {activeDay
                        ? money((activeDay.online + activeDay.store) * 1000)
                        : money(1284500 * scale)}
                    </Text>
                  </View>
                  <View className="flex-row gap-3">
                    {SERIES.map((series) => (
                      <View
                        key={series.key}
                        className="flex-row items-center gap-1.25"
                      >
                        <View
                          className={cn("size-2 rounded-full", series.swatch)}
                        />
                        <Text className="text-xs text-muted-foreground">
                          {chartConfig[series.key].label}
                        </Text>
                      </View>
                    ))}
                  </View>
                </View>
                <ChartContainer
                  config={chartConfig}
                  accessibilityLabel="Weekly sales"
                  className="mt-3.5 h-40 w-full"
                >
                  <BarChart
                    data={data}
                    barGap={3}
                    margin={{ top: 4, right: 0, left: 0, bottom: 0 }}
                    onActiveIndexChange={setActive}
                  >
                    <CartesianGrid vertical={false} strokeDasharray="3 3" />
                    <XAxis dataKey="day" tickMargin={6} />
                    <ChartTooltip content={() => null} cursor={false} />
                    {SERIES.map((series) => (
                      <Bar
                        isAnimationActive={false}
                        key={series.key}
                        dataKey={series.key}
                        fill={chartConfig[series.key].color}
                        radius={3}
                        maxBarSize={12}
                      >
                        {data.map((day) => (
                          <Cell
                            key={day.day}
                            fillOpacity={
                              active === null || active === day.index ? 1 : 0.35
                            }
                          />
                        ))}
                      </Bar>
                    ))}
                  </BarChart>
                </ChartContainer>
              </View>
              <View className="-mb-2 gap-3">
                <Text role="heading" className="px-1 text-sm font-semibold">
                  Recent orders
                </Text>
                <ChipGroup
                  value={[filter]}
                  onValueChange={(value) => {
                    if (!value[0]) return
                    setFilter(value[0])
                    onFilterChange?.(value[0])
                  }}
                  accessibilityLabel="Order status"
                >
                  {filters.map((item) => (
                    <Chip key={item} value={item}>
                      {item}
                    </Chip>
                  ))}
                </ChipGroup>
              </View>
              <AnimatedView
                key={filter}
                entering={FadeInDown.duration(700).reduceMotion(
                  ReduceMotion.System
                )}
              >
                <ItemGroup variant="inset">
                  {list.map((order, index) => (
                    <React.Fragment key={order.id}>
                      {index > 0 ? <ItemSeparator className="ms-15" /> : null}
                      <Item size="sm" onPress={() => onOrderPress?.(order.id)}>
                        <Avatar>
                          <AvatarFallback className={order.tint}>
                            {initials(order.name)}
                          </AvatarFallback>
                        </Avatar>
                        <ItemContent className="gap-0">
                          <ItemTitle className="font-semibold">
                            {order.name}
                          </ItemTitle>
                          <ItemDescription className="text-sm">
                            {`${order.product} · ${order.id}`}
                          </ItemDescription>
                        </ItemContent>
                        <ItemActions className="flex-col items-end gap-0">
                          <Text className="text-base tabular-nums">
                            {money(order.total)}
                          </Text>
                          <Text
                            className={cn(
                              "text-xs",
                              order.status === "Returned"
                                ? "text-destructive"
                                : "text-muted-foreground"
                            )}
                          >
                            {order.status}
                          </Text>
                        </ItemActions>
                      </Item>
                    </React.Fragment>
                  ))}
                </ItemGroup>
              </AnimatedView>
            </View>
          </HeaderScrollView>
        </>
      ) : (
        <OtherSection value={section} />
      )}
      <TabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      >
        <TabBarList>
          {SECTIONS.map((item) => (
            <TabBarItem
              key={item.value}
              value={item.value}
              icon={item.icon}
              badge={item.badge}
            >
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Screen>
  )
}

export { DashStore }
```

```swift title="swift/Blocks/DashStore.swift"
import Charts
import PhosphorSwift
import SwiftUI

public struct DashStore: View {
  struct Order: Identifiable {
    let id: String
    let name: String
    let product: String
    let status: String
    let total: Double
    let tint: Color
  }

  static let ranges: [(value: String, label: String, scale: Double)] = [
    ("24h", "24 Hours", 0.15), ("7d", "7 Days", 1), ("30d", "30 Days", 4.3),
  ]

  static let week: [(day: String, online: Double, store: Double)] = [
    ("Mon", 42, 46), ("Tue", 58, 44), ("Wed", 50, 56), ("Thu", 68, 54),
    ("Fri", 64, 70), ("Sat", 80, 62), ("Sun", 72, 76),
  ]

  static let stores = ["Union Square", "Fifth Avenue", "Michigan Avenue"]

  static var orders: [Order] {
    [
      Order(id: "W1048", name: "Jamie Rivera", product: "Phone Pro", status: "Shipped", total: 999, tint: Palette.indigo500),
      Order(id: "W1047", name: "Morgan Lee", product: "Book Air", status: "Processing", total: 1099, tint: Palette.orange500),
      Order(id: "W1046", name: "Riley Chen", product: "Watch Series 11", status: "Delivered", total: 399, tint: Palette.green500),
      Order(id: "W1045", name: "Avery Brooks", product: "Pods Studio", status: "Returned", total: 549, tint: Palette.pink500),
      Order(id: "W1044", name: "Taylor Kim", product: "Pad Air", status: "Delivered", total: 599, tint: Palette.cyan500),
    ]
  }

  static let filters = ["All", "Shipped", "Delivered", "Returned"]

  static func filtered(_ filter: String) -> [Order] {
    orders.filter { filter == "All" || $0.status == filter }
  }

  private let chartConfig: ChartConfig = [
    "online": .init("Online", color: Palette.blue500),
    "store": .init("In store", color: Palette.teal500),
  ]

  private var onRangeChange: ((String) -> Void)?
  private var onFilterChange: ((String) -> Void)?
  private var onStoreChange: ((String?) -> Void)?
  private var onOrderPress: ((String) -> Void)?
  private var onNavigate: ((String) -> Void)?
  @State private var range: String?
  @State private var filter: String?
  @State private var store: String
  @State private var active: Int?
  @Environment(\.locale) private var locale
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(
    onRangeChange: ((_ range: String) -> Void)? = nil,
    onFilterChange: ((_ status: String) -> Void)? = nil,
    onStoreChange: ((_ store: String?) -> Void)? = nil,
    onOrderPress: ((_ id: String) -> Void)? = nil,
    onNavigate: ((_ section: String) -> Void)? = nil
  ) {
    self.init(range: "7d", filter: "All")
    self.onRangeChange = onRangeChange
    self.onFilterChange = onFilterChange
    self.onStoreChange = onStoreChange
    self.onOrderPress = onOrderPress
    self.onNavigate = onNavigate
  }

  init(range: String, filter: String, active: Int? = nil) {
    _range = State(initialValue: range)
    _filter = State(initialValue: filter)
    _store = State(initialValue: "")
    _active = State(initialValue: active)
  }

  private var animation: Animation? {
    reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.5)
  }

  private var rangeBinding: Binding<String?> {
    Binding(
      get: { range },
      set: { value in
        guard let value, value != range else { return }
        onRangeChange?(value)
        withAnimation(animation) { range = value }
      }
    )
  }

  private var filterBinding: Binding<String?> {
    Binding(
      get: { filter },
      set: { value in
        guard let value, value != filter else { return }
        onFilterChange?(value)
        withAnimation(animation) { filter = value }
      }
    )
  }

  private var storeBinding: Binding<String> {
    Binding(
      get: { store },
      set: { value in
        guard value != store else { return }
        onStoreChange?(value.isEmpty ? nil : value)
        store = value
      }
    )
  }

  private func money(_ value: Double) -> String {
    value.formatted(.currency(code: "USD").precision(.fractionLength(0)).locale(locale))
  }

  public var body: some View {
    DashStoreShell(onNavigate: onNavigate) {
      page
    }
  }

  private var page: some View {
    let scale = Self.ranges.first { $0.value == range }?.scale ?? 1
    let stats: [(label: String, value: String, change: String, up: Bool)] = [
      ("Revenue", money(1284500 * scale), "+12.4%", true),
      ("Orders", (842 * scale).rounded().formatted(.number.locale(locale)), "+8.1%", true),
      ("Average order", money(1525), "−2.3%", false),
      ("Conversion", "3.42%", "+0.4 pts", true),
    ]
    return Screen {
      HeaderScrollView {
        HeaderLargeTitle("Overview", subtitle: store.isEmpty ? "All stores" : LocalizedStringKey(store))
        VStack(alignment: .leading, spacing: 20) {
          ToggleGroup(selection: rangeBinding, size: .sm, spacing: 0) {
            ForEach(Self.ranges, id: \.value) { item in
              ToggleGroupItem(LocalizedStringKey(item.label), value: item.value)
            }
          }
          .accessibilityLabel("Range")
          Grid(alignment: .topLeading, horizontalSpacing: 0, verticalSpacing: 0) {
            GridRow {
              stat(stats[0])
              stat(stats[1]).overlay(alignment: .leading) { EsuissColor.border.frame(width: 1) }
            }
            GridRow {
              stat(stats[2]).overlay(alignment: .top) { EsuissColor.border.frame(height: 1) }
              stat(stats[3])
                .overlay(alignment: .top) { EsuissColor.border.frame(height: 1) }
                .overlay(alignment: .leading) { EsuissColor.border.frame(width: 1) }
            }
          }
          .background(EsuissColor.muted, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
          .clipShape(RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
          sales(scale: scale)
          Text("Recent orders")
            .textStyle(.sm, weight: .semibold)
            .padding(.horizontal, 4)
            .accessibilityAddTraits(.isHeader)
          ScrollView(.horizontal, showsIndicators: false) {
            ToggleGroup(selection: filterBinding, size: .sm, spacing: 0) {
              ForEach(Self.filters, id: \.self) { item in
                ToggleGroupItem(LocalizedStringKey(item), value: item)
              }
            }
            .accessibilityLabel("Order status")
          }
          .padding(.top, -8)
          ItemGroup(.inset) {
            ForEach(Array(Self.filtered(filter ?? "All").enumerated()), id: \.element.id) { offset, order in
              if offset > 0 {
                Separator().padding(.leading, 60)
              }
              orderRow(order)
                .transition(.opacity.combined(with: .offset(y: 6)))
            }
          }
        }
        .padding(.horizontal, 16)
        .padding(.top, 4)
        .padding(.bottom, 24)
      } header: {
        Header {
          HeaderTitle("Overview")
        } trailing: {
          DropdownMenu(align: .end) {
            DropdownMenuLabel("Stores")
            DropdownMenuRadioGroup(selection: storeBinding) {
              DropdownMenuRadioItem("All stores", value: "")
              ForEach(Self.stores, id: \.self) { item in
                DropdownMenuRadioItem(LocalizedStringKey(item), value: item)
              }
            }
          } label: {
            Ph.storefront.regular
              .frame(width: 22, height: 22)
              .foregroundStyle(EsuissColor.foreground)
              .frame(width: 44, height: 44)
              .contentShape(Rectangle())
          }
          .buttonStyle(.plain)
          .accessibilityLabel(Text("Stores"))
        }
      }
    }
  }

  private func stat(_ stat: (label: String, value: String, change: String, up: Bool)) -> some View {
    VStack(alignment: .leading, spacing: 2) {
      Text(LocalizedStringKey(stat.label))
        .textStyle(.sm)
        .foregroundStyle(EsuissColor.mutedForeground)
      Text(verbatim: stat.value)
        .textStyle(.xl2, weight: .semibold)
        .monospacedDigit()
        .lineLimit(1)
        .minimumScaleFactor(0.7)
        .contentTransition(.numericText())
      Text("\(stat.change) vs. previous period")
        .textStyle(.xs)
        .foregroundStyle(stat.up ? Color.adaptive(light: Palette.green600, dark: Palette.green400) : EsuissColor.destructive)
    }
    .padding(.horizontal, 16)
    .padding(.vertical, 14)
    .frame(maxWidth: .infinity, alignment: .leading)
    .accessibilityElement(children: .combine)
  }

  private func sales(scale: Double) -> some View {
    let keys = ["online", "store"]
    let day = active.map { Self.week[$0] }
    return VStack(alignment: .leading, spacing: 14) {
      HStack(alignment: .top, spacing: 8) {
        VStack(alignment: .leading, spacing: 0) {
          Text(LocalizedStringKey(day?.day ?? "Weekly sales"))
            .textStyle(.sm)
            .foregroundStyle(EsuissColor.mutedForeground)
          Text(verbatim: day.map { money(($0.online + $0.store) * 1000) } ?? money(1284500 * scale))
            .textStyle(.xl2, weight: .semibold)
            .monospacedDigit()
            .contentTransition(.numericText())
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .accessibilityElement(children: .combine)
        HStack(spacing: 12) {
          ForEach(keys, id: \.self) { key in
            HStack(spacing: 5) {
              Circle()
                .fill(chartConfig.color(key))
                .frame(width: 8, height: 8)
              Text(LocalizedStringKey(chartConfig.label(key)))
            }
          }
        }
        .textStyle(.xs)
        .foregroundStyle(EsuissColor.mutedForeground)
        .accessibilityHidden(true)
      }
      ChartContainer(config: chartConfig) {
        Chart {
          ForEach(keys, id: \.self) { key in
            ForEach(Self.week.indices, id: \.self) { index in
              let item = Self.week[index]
              BarMark(x: .value("Day", item.day), y: .value("Sales", key == "online" ? item.online : item.store), width: .fixed(12))
                .position(by: .value("Channel", key), span: .fixed(27))
                .clipShape(RoundedRectangle(cornerRadius: Radius.xs, style: .continuous))
                .foregroundStyle(chartConfig.color(key).opacity(active == nil || active == index ? 1 : 0.35))
            }
          }
        }
        .chartXAxis { ChartAxis.x(values: Self.week.map(\.day)) }
        .chartYAxis { ChartAxis.grid() }
        .animation(reduceMotion ? nil : .easeOut(duration: 0.2), value: active)
        .chartTooltip(
          x: Self.week.map(\.day),
          selection: $active,
          cursor: false,
          accessibilityLabel: "Weekly sales",
          items: { index in
            [
              ChartTooltipItem("online", value: Self.week[index].online * 1000),
              ChartTooltipItem("store", value: Self.week[index].store * 1000),
            ]
          }
        ) { _ in
          EmptyView()
        }
      }
      .frame(height: 160)
    }
    .padding(.horizontal, 16)
    .padding(.top, 14)
    .padding(.bottom, 8)
    .background(EsuissColor.muted, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
  }

  private func orderRow(_ order: Order) -> some View {
    Item(size: .sm, action: { onOrderPress?(order.id) }) {
      ItemMedia {
        Text(verbatim: order.name.split(separator: " ").compactMap(\.first).map(String.init).joined())
          .scaledFont(.xs, weight: .semibold)
          .lineLimit(1)
          .minimumScaleFactor(0.5)
          .foregroundStyle(EsuissColor.primaryForeground)
          .frame(width: 32, height: 32)
          .background(order.tint, in: Circle())
          .accessibilityHidden(true)
      }
      ItemContent {
        ItemTitle(verbatim: order.name)
        ItemDescription(verbatim: "\(order.product) · \(order.id)")
      }
      ItemActions {
        VStack(alignment: .trailing, spacing: 0) {
          Text(verbatim: money(order.total))
            .textStyle(.base)
            .monospacedDigit()
            .foregroundStyle(EsuissColor.foreground)
          Text(LocalizedStringKey(order.status))
            .textStyle(.sm)
            .foregroundStyle(order.status == "Returned" ? EsuissColor.destructive : EsuissColor.mutedForeground)
        }
      }
    }
  }
}

private struct DashStoreShell<Content: View>: View {
  let onNavigate: ((String) -> Void)?
  let content: Content

  @State private var tab = "overview"

  init(onNavigate: ((String) -> Void)?, @ViewBuilder content: () -> Content) {
    self.onNavigate = onNavigate
    self.content = content()
  }

  private static var sections: [(value: String, label: LocalizedStringKey, icon: Image, active: Image, badge: TabBarBadge?)] {
    [
      ("overview", "Overview", Ph.house.regular, Ph.house.fill, nil),
      ("orders", "Orders", Ph.handbag.regular, Ph.handbag.fill, 12),
      ("customers", "Customers", Ph.users.regular, Ph.users.fill, nil),
      ("reports", "Reports", Ph.chartBar.regular, Ph.chartBar.fill, nil),
    ]
  }

  var body: some View {
    let selection = Binding<String>(
      get: { tab },
      set: { value in
        guard value != tab else { return }
        onNavigate?(value)
        tab = value
      }
    )
    Screen {
      ZStack {
        content
          .opacity(tab == "overview" ? 1 : 0)
          .allowsHitTesting(tab == "overview")
          .accessibilityHidden(tab != "overview")
        if tab != "overview" {
          Empty {
            EmptyHeader {
              EmptyDescription("This section isn't part of the sample. Choose \(Text("Overview")).")
            }
          }
          .background(EsuissColor.background)
        }
      }
      .frame(maxHeight: .infinity)
      TabBar(selection: selection) {
        ForEach(Self.sections, id: \.value) { section in
          TabBarItem(section.label, value: section.value, icon: section.icon, activeIcon: section.active, badge: section.badge)
        }
      }
    }
  }
}
```

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

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.fill.ChartBar as FillChartBar
import com.adamglin.phosphoricons.fill.Handbag as FillHandbag
import com.adamglin.phosphoricons.fill.House as FillHouse
import com.adamglin.phosphoricons.fill.Users as FillUsers
import com.adamglin.phosphoricons.regular.ChartBar
import com.adamglin.phosphoricons.regular.Handbag
import com.adamglin.phosphoricons.regular.House
import com.adamglin.phosphoricons.regular.Storefront
import com.adamglin.phosphoricons.regular.Users
import com.esuiss.ui.components.BarChart
import com.esuiss.ui.components.ChartCell
import com.esuiss.ui.components.ChartConfig
import com.esuiss.ui.components.ChartConfigItem
import com.esuiss.ui.components.ChartContainer
import com.esuiss.ui.components.ChartMargin
import com.esuiss.ui.components.ChartRadius
import com.esuiss.ui.components.DropdownMenu
import com.esuiss.ui.components.DropdownMenuLabel
import com.esuiss.ui.components.DropdownMenuRadioGroup
import com.esuiss.ui.components.DropdownMenuRadioItem
import com.esuiss.ui.components.Header
import com.esuiss.ui.components.HeaderActions
import com.esuiss.ui.components.HeaderLargeTitle
import com.esuiss.ui.components.HeaderTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Item
import com.esuiss.ui.components.ItemActions
import com.esuiss.ui.components.ItemContent
import com.esuiss.ui.components.ItemDescription
import com.esuiss.ui.components.ItemGroup
import com.esuiss.ui.components.ItemGroupVariant
import com.esuiss.ui.components.ItemMedia
import com.esuiss.ui.components.ItemSize
import com.esuiss.ui.components.ItemTitle
import com.esuiss.ui.components.LocalContentStyle
import com.esuiss.ui.components.PopupAlign
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.TabBarItem
import com.esuiss.ui.components.ToggleGroup
import com.esuiss.ui.components.ToggleGroupItem
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.rememberHeaderState
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
import java.text.NumberFormat
import java.util.Locale
import kotlin.math.roundToLong

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

private class StoreRange(val value: String, val label: String, val scale: Double)

private val StoreRanges = listOf(
    StoreRange("24h", "24 Hours", 0.15),
    StoreRange("7d", "7 Days", 1.0),
    StoreRange("30d", "30 Days", 4.3),
)

private class StoreDay(val day: String, val online: Double, val store: Double)

private val StoreWeek = listOf(
    StoreDay("Mon", 42.0, 46.0),
    StoreDay("Tue", 58.0, 44.0),
    StoreDay("Wed", 50.0, 56.0),
    StoreDay("Thu", 68.0, 54.0),
    StoreDay("Fri", 64.0, 70.0),
    StoreDay("Sat", 80.0, 62.0),
    StoreDay("Sun", 72.0, 76.0),
)

private val StoreNames = listOf("Union Square", "Fifth Avenue", "Michigan Avenue")

private class StoreOrder(
    val id: String,
    val name: String,
    val product: String,
    val status: String,
    val total: Double,
    val tint: Colors.() -> Color,
)

private val StoreOrders = listOf(
    StoreOrder("W1048", "Jamie Rivera", "Phone Pro", "Shipped", 999.0) { Palette.indigo500 },
    StoreOrder("W1047", "Morgan Lee", "Book Air", "Processing", 1099.0) { Palette.orange500 },
    StoreOrder("W1046", "Riley Chen", "Watch Series 11", "Delivered", 399.0) { Palette.green500 },
    StoreOrder("W1045", "Avery Brooks", "Pods Studio", "Returned", 549.0) { Palette.pink500 },
    StoreOrder("W1044", "Taylor Kim", "Pad Air", "Delivered", 599.0) { Palette.cyan500 },
)

private val StoreFilters = listOf("All", "Shipped", "Delivered", "Returned")

private val StoreConfig = ChartConfig(
    "online" to ChartConfigItem("Online") { chart1 },
    "store" to ChartConfigItem("In store") { chart2 },
)

private class StoreStat(val label: String, val value: String, val change: String, val up: Boolean)

private fun money(value: Double): String {
    val format = NumberFormat.getCurrencyInstance(Locale.US)
    format.maximumFractionDigits = 0
    format.minimumFractionDigits = 0
    return format.format(value)
}

@Composable
fun DashStore(
    modifier: Modifier = Modifier,
    onRangeChange: (range: String) -> Unit = {},
    onFilterChange: (status: String) -> Unit = {},
    onStoreChange: (store: String?) -> Unit = {},
    onOrderPress: (id: String) -> Unit = {},
    onNavigate: (section: String) -> Unit = {},
) {
    val colors = EsuissTheme.colors
    val header = rememberHeaderState()
    var range by rememberSaveable { mutableStateOf("7d") }
    var filter by rememberSaveable { mutableStateOf("All") }
    var store by rememberSaveable { mutableStateOf("") }
    var menu by remember { mutableStateOf(false) }
    val scale = StoreRanges.firstOrNull { it.value == range }?.scale ?: 1.0
    val stats = listOf(
        StoreStat("Revenue", money(1284500 * scale), "+12.4%", true),
        StoreStat("Orders", NumberFormat.getNumberInstance(Locale.US).format((842 * scale).roundToLong()), "+8.1%", true),
        StoreStat("Average order", money(1525.0), "−2.3%", false),
        StoreStat("Conversion", "3.42%", "+0.4 pts", true),
    )

    Screen(modifier, edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        DashTabs(
            home = "overview",
            homeLabel = "Overview",
            onNavigate = onNavigate,
            tabs = {
                TabBarItem("overview", "Overview", PhosphorIcons.Regular.House, selectedIcon = PhosphorIcons.Fill.FillHouse)
                TabBarItem("orders", "Orders", PhosphorIcons.Regular.Handbag, selectedIcon = PhosphorIcons.Fill.FillHandbag, badge = "12")
                TabBarItem("customers", "Customers", PhosphorIcons.Regular.Users, selectedIcon = PhosphorIcons.Fill.FillUsers)
                TabBarItem("reports", "Reports", PhosphorIcons.Regular.ChartBar, selectedIcon = PhosphorIcons.Fill.FillChartBar)
            },
        ) {
            Column(
                Modifier
                    .fillMaxSize()
                    .nestedScroll(header.nestedScrollConnection),
            ) {
                Header(state = header) {
                    HeaderTitle("Overview")
                    HeaderLargeTitle("Overview", subtitle = store.ifEmpty { "All stores" })
                    HeaderActions {
                        DropdownMenu(
                            expanded = menu,
                            onExpandedChange = { menu = it },
                            align = PopupAlign.End,
                            trigger = {
                                Box(
                                    Modifier
                                        .size(44.dp)
                                        .clickable(
                                            interactionSource = remember { MutableInteractionSource() },
                                            indication = null,
                                            role = Role.Button,
                                        ) { menu = true }
                                        .semantics { contentDescription = "Stores" },
                                    contentAlignment = Alignment.Center,
                                ) {
                                    Icon(PhosphorIcons.Regular.Storefront, contentDescription = null, size = 22.dp, tint = colors.foreground)
                                }
                            },
                        ) {
                            DropdownMenuLabel("Stores")
                            DropdownMenuRadioGroup(
                                value = store,
                                onValueChange = { value ->
                                    if (value != store) {
                                        onStoreChange(value.ifEmpty { null })
                                        store = value
                                    }
                                },
                            ) {
                                DropdownMenuRadioItem("", closeOnClick = true) {
                                    BasicText("All stores", style = LocalContentStyle.current, maxLines = 1)
                                }
                                for (name in StoreNames) {
                                    DropdownMenuRadioItem(name, closeOnClick = true) {
                                        BasicText(name, style = LocalContentStyle.current, maxLines = 1)
                                    }
                                }
                            }
                        }
                    }
                }
                Column(
                    Modifier
                        .weight(1f)
                        .fillMaxWidth()
                        .verticalScroll(rememberScrollState())
                        .padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 24.dp),
                    verticalArrangement = Arrangement.spacedBy(20.dp),
                ) {
                    ToggleGroup(
                        value = range,
                        onValueChange = { value ->
                            if (value != null && value != range) {
                                onRangeChange(value)
                                range = value
                            }
                        },
                        modifier = Modifier.semantics { contentDescription = "Range" },
                        size = ToggleSize.Sm,
                        spacing = 0,
                    ) {
                        for (item in StoreRanges) ToggleGroupItem(item.label, item.value)
                    }
                    StatGrid(stats)
                    WeeklySales(scale)
                    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
                        BasicText(
                            "Recent orders",
                            Modifier
                                .padding(horizontal = 4.dp)
                                .semantics { heading() },
                            style = Typography.sm.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
                        )
                        Row(Modifier.horizontalScroll(rememberScrollState())) {
                            ToggleGroup(
                                value = filter,
                                onValueChange = { value ->
                                    if (value != null && value != filter) {
                                        onFilterChange(value)
                                        filter = value
                                    }
                                },
                                modifier = Modifier.semantics { contentDescription = "Order status" },
                                size = ToggleSize.Sm,
                                spacing = 0,
                            ) {
                                for (item in StoreFilters) ToggleGroupItem(item, item)
                            }
                        }
                    }
                    ItemGroup(variant = ItemGroupVariant.Inset) {
                        val visible = StoreOrders.filter { filter == "All" || it.status == filter }
                        for (order in StoreOrders) {
                            key(order.id) {
                                AnimatedVisibility(
                                    visible = order in visible,
                                    enter = fadeIn(tween(300)) + expandVertically(tween(500, easing = StoreEasing)),
                                    exit = fadeOut(tween(200)) + shrinkVertically(tween(500, easing = StoreEasing)),
                                ) {
                                    Column {
                                        if (visible.firstOrNull() != order) {
                                            Box(
                                                Modifier
                                                    .padding(start = 60.dp)
                                                    .fillMaxWidth()
                                                    .height(1.dp)
                                                    .background(colors.border),
                                            )
                                        }
                                        OrderRow(order) { onOrderPress(order.id) }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun StatGrid(stats: List<StoreStat>) {
    val colors = EsuissTheme.colors
    val divider = Modifier
        .background(colors.border)
    Column(
        Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(Radius.xl))
            .background(colors.muted),
    ) {
        for (row in 0 until 2) {
            if (row > 0) Box(divider.fillMaxWidth().height(1.dp))
            Row(Modifier.height(IntrinsicSize.Min)) {
                StatCell(stats[row * 2], Modifier.weight(1f))
                Box(divider.width(1.dp).fillMaxHeight())
                StatCell(stats[row * 2 + 1], Modifier.weight(1f))
            }
        }
    }
}

@Composable
private fun StatCell(stat: StoreStat, modifier: Modifier) {
    val colors = EsuissTheme.colors
    Column(
        modifier
            .semantics(mergeDescendants = true) {}
            .padding(horizontal = 16.dp, vertical = 14.dp),
        verticalArrangement = Arrangement.spacedBy(2.dp),
    ) {
        BasicText(stat.label, style = Typography.sm.copy(color = colors.mutedForeground))
        BasicText(
            stat.value,
            style = Typography.xl2.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
            maxLines = 1,
            autoSize = TextAutoSize.StepBased(
                minFontSize = Typography.lg.fontSize,
                maxFontSize = Typography.xl2.fontSize,
            ),
        )
        BasicText(
            "${stat.change} vs. previous period",
            style = Typography.xs.copy(color = if (stat.up) (if (colors.isDark) Palette.green400 else Palette.green600) else colors.destructive),
        )
    }
}

@Composable
private fun WeeklySales(scale: Double) {
    val colors = EsuissTheme.colors
    var active by remember { mutableStateOf<Int?>(null) }
    val day = active?.let { StoreWeek[it] }
    Column(
        Modifier
            .fillMaxWidth()
            .background(colors.muted, RoundedCornerShape(Radius.xl))
            .padding(start = 16.dp, end = 16.dp, top = 14.dp, bottom = 8.dp),
        verticalArrangement = Arrangement.spacedBy(14.dp),
    ) {
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Top) {
            Column(
                Modifier
                    .weight(1f)
                    .semantics(mergeDescendants = true) { liveRegion = LiveRegionMode.Polite },
            ) {
                BasicText(day?.day ?: "Weekly sales", style = Typography.sm.copy(color = colors.mutedForeground))
                BasicText(
                    day?.let { money((it.online + it.store) * 1000) } ?: money(1284500 * scale),
                    style = Typography.xl2.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, fontFeatureSettings = "tnum"),
                )
            }
            Row(Modifier.clearAndSetSemantics {}, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
                for (key in StoreConfig.keys) {
                    Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically) {
                        Box(
                            Modifier
                                .size(8.dp)
                                .background(StoreConfig[key]?.color?.invoke(colors) ?: colors.mutedForeground, CircleShape),
                        )
                        BasicText(StoreConfig[key]?.label ?: key, style = Typography.xs.copy(color = colors.mutedForeground))
                    }
                }
            }
        }
        ChartContainer(
            StoreConfig,
            Modifier
                .fillMaxWidth()
                .height(160.dp),
        ) {
            BarChart(
                StoreWeek,
                margin = ChartMargin(top = 5.dp, right = 0.dp, bottom = 0.dp, left = 0.dp),
                barGap = 3.dp,
                accessibilityLabel = "Weekly sales",
                selectedIndex = active,
                onSelectedIndexChange = { active = it },
            ) {
                CartesianGrid(strokeDasharray = listOf(2f, 4f))
                XAxis(label = { it.day }, tickMargin = 8.dp, interval = 0)
                for (series in listOf("online", "store")) {
                    Bar(
                        series,
                        value = { if (series == "online") it.online * 1000 else it.store * 1000 },
                        radius = ChartRadius(3.dp),
                        barSize = 12.dp,
                        cells = { index -> ChartCell(alpha = if (active == null || active == index) 1f else 0.35f) },
                        isAnimationActive = false,
                    )
                }
            }
        }
    }
}

@Composable
private fun OrderRow(order: StoreOrder, onClick: () -> Unit) {
    val colors = EsuissTheme.colors
    Item(size = ItemSize.Sm, onClick = onClick) {
        ItemMedia {
            Box(
                Modifier
                    .size(32.dp)
                    .background(order.tint(colors), CircleShape)
                    .clearAndSetSemantics {},
                contentAlignment = Alignment.Center,
            ) {
                BasicText(
                    order.name.split(" ").mapNotNull { it.firstOrNull() }.joinToString(""),
                    style = Typography.sm.copy(color = colors.primaryForeground, fontWeight = FontWeight.SemiBold),
                )
            }
        }
        ItemContent {
            ItemTitle(order.name)
            ItemDescription("${order.product} · ${order.id}")
        }
        ItemActions {
            Column(horizontalAlignment = Alignment.End) {
                BasicText(
                    money(order.total),
                    style = Typography.base.copy(color = colors.foreground, fontFeatureSettings = "tnum"),
                    maxLines = 1,
                    overflow = TextOverflow.Clip,
                )
                BasicText(
                    order.status,
                    style = Typography.sm.copy(color = if (order.status == "Returned") colors.destructive else colors.mutedForeground),
                )
            }
        }
    }
}
```

Composes `Screen`, `Header`, `DropdownMenu` for picking the store, `ToggleGroup`, the `BarChart` from `Chart`, `Chip` filters, `Avatar`, `Item` rows and `TabBar`. The bar chart compares online and in-store sales per day, and pressing a day highlights it and shows its total. Order status chips filter the list.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/dash-store
```

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 dash-store
```

**SwiftUI**

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

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add dash-store --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), [Chart](https://ui.suiss.app/docs/components/chart), [Chip](https://ui.suiss.app/docs/components/chip), [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [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), [Chart](https://ui.suiss.app/docs/components/chart), [Chip](https://ui.suiss.app/docs/components/chip), [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [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/dash-store.tsx`:

- `uniwind/components/blocks/dash-store.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), [Chart](https://ui.suiss.app/docs/components/chart), [Chip](https://ui.suiss.app/docs/components/chip), [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [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/dash-store.tsx`:

```tsx title="nativewind/components/blocks/dash-store.tsx"
import { cn } from "cn"
import {
  CaretDownIcon,
  ChartBarIcon,
  HandbagIcon,
  HouseIcon,
  type Icon as PhosphorIcon,
  StorefrontIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Bar,
  BarChart,
  CartesianGrid,
  Cell,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  XAxis,
} from "@/components/ui/chart"
import { Chip, ChipGroup } from "@/components/ui/chip"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Header,
  HeaderActions,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

const SECTIONS: {
  value: string
  label: string
  icon: PhosphorIcon
  badge?: number
}[] = [
  { value: "overview", label: "Overview", icon: HouseIcon },
  { value: "orders", label: "Orders", icon: HandbagIcon, badge: 12 },
  { value: "customers", label: "Customers", icon: UsersIcon },
  { value: "reports", label: "Reports", icon: ChartBarIcon },
]

const STORES = ["All stores", "Union Square", "Fifth Avenue", "Michigan Avenue"]

const ranges = [
  { value: "24h", label: "24 Hours", scale: 0.15 },
  { value: "7d", label: "7 Days", scale: 1 },
  { value: "30d", label: "30 Days", scale: 4.3 },
]

const week = [
  { day: "Mon", online: 42, store: 46 },
  { day: "Tue", online: 58, store: 44 },
  { day: "Wed", online: 50, store: 56 },
  { day: "Thu", online: 68, store: 54 },
  { day: "Fri", online: 64, store: 70 },
  { day: "Sat", online: 80, store: 62 },
  { day: "Sun", online: 72, store: 76 },
]

const chartConfig = {
  online: { label: "Online", color: "chart-1" },
  store: { label: "In store", color: "chart-2" },
} satisfies ChartConfig

const SERIES = [
  { key: "online", swatch: "bg-chart-1" },
  { key: "store", swatch: "bg-chart-2" },
] as const

const orders = [
  {
    id: "W1048",
    name: "Jamie Rivera",
    product: "Phone Pro",
    status: "Shipped",
    total: 999,
    tint: "bg-blue-500",
  },
  {
    id: "W1047",
    name: "Morgan Lee",
    product: "Book Air",
    status: "Processing",
    total: 1099,
    tint: "bg-orange-500",
  },
  {
    id: "W1046",
    name: "Riley Chen",
    product: "Watch Series 11",
    status: "Delivered",
    total: 399,
    tint: "bg-green-500",
  },
  {
    id: "W1045",
    name: "Avery Brooks",
    product: "Pods Studio",
    status: "Returned",
    total: 549,
    tint: "bg-pink-500",
  },
  {
    id: "W1044",
    name: "Taylor Kim",
    product: "Pad Air",
    status: "Delivered",
    total: 599,
    tint: "bg-cyan-500",
  },
]

const filters = ["All", "Shipped", "Delivered", "Returned"]

const THOUSANDS = /\B(?=(\d{3})+(?!\d))/g

function money(value: number) {
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
      maximumFractionDigits: 0,
    }).format(value)
  } catch {
    return `$${Math.round(value).toString().replace(THOUSANDS, ",")}`
  }
}

function count(value: number) {
  try {
    return new Intl.NumberFormat("en-US").format(value)
  } catch {
    return String(value).replace(THOUSANDS, ",")
  }
}

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

function StoreMenu({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        render={<Button variant="secondary" size="sm" />}
        accessibilityLabel={`Store, ${value}`}
      >
        <Icon as={StorefrontIcon} className="size-4" />
        <Icon as={CaretDownIcon} weight="bold" className="size-3" />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuRadioGroup
          value={value}
          onValueChange={(next) => {
            if (typeof next === "string") onValueChange(next)
          }}
        >
          {STORES.map((store) => (
            <DropdownMenuRadioItem key={store} value={store}>
              <Text>{store}</Text>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function OtherSection({ value }: { value: string }) {
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View className="flex-1 gap-2 px-4 pt-[3.25rem]">
      <Text role="heading" className="text-3xl font-bold">
        {label}
      </Text>
      <Text className="text-lg text-muted-foreground">
        This section isn't part of the sample. Choose Overview.
      </Text>
    </View>
  )
}

function DashStore({
  onRangeChange,
  onFilterChange,
  onStoreChange,
  onOrderPress,
  onNavigate,
}: {
  onRangeChange?: (range: string) => void
  onFilterChange?: (filter: string) => void
  onStoreChange?: (store: string) => void
  onOrderPress?: (id: string) => void
  onNavigate?: (section: string) => void
}) {
  const [section, setSection] = React.useState("overview")
  const [store, setStore] = React.useState("All stores")
  const [range, setRange] = React.useState("7d")
  const [filter, setFilter] = React.useState("All")
  const [active, setActive] = React.useState<number | null>(null)
  const scroll = useHeaderScroll()
  const scale = ranges.find((item) => item.value === range)?.scale ?? 1
  const data = week.map((day, index) => ({ ...day, index }))
  const activeDay = active === null ? undefined : week[active]
  const list = orders.filter(
    (order) => filter === "All" || order.status === filter
  )

  const stats = [
    {
      label: "Revenue",
      value: money(1284500 * scale),
      change: "+12.4%",
      up: true,
    },
    {
      label: "Orders",
      value: count(Math.round(842 * scale)),
      change: "+8.1%",
      up: true,
    },
    { label: "Average order", value: money(1525), change: "−2.3%", up: false },
    { label: "Conversion", value: "3.42%", change: "+0.4 pts", up: true },
  ]

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "overview" ? (
        <>
          <Header scroll={scroll}>
            <HeaderTitle>Overview</HeaderTitle>
            <HeaderActions>
              <StoreMenu
                value={store}
                onValueChange={(value) => {
                  setStore(value)
                  onStoreChange?.(value)
                }}
              />
            </HeaderActions>
          </Header>
          <HeaderScrollView scroll={scroll}>
            <HeaderLargeTitle subtitle={store}>Overview</HeaderLargeTitle>
            <View className="gap-5 px-4 pt-3 pb-8">
              <ToggleGroup
                spacing={0}
                value={[range]}
                onValueChange={(value) => {
                  if (!value[0]) return
                  setRange(value[0])
                  onRangeChange?.(value[0])
                }}
                accessibilityLabel="Range"
                className="self-stretch"
              >
                {ranges.map((item) => (
                  <ToggleGroupItem
                    key={item.value}
                    value={item.value}
                    size="sm"
                    className="flex-1"
                  >
                    <Text>{item.label}</Text>
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
              <View className="flex-row flex-wrap overflow-hidden rounded-xl bg-muted">
                {stats.map((stat, index) => (
                  <View
                    key={stat.label}
                    accessible
                    className={cn(
                      "w-1/2 gap-0.5 border-border px-4 py-3.5",
                      index % 2 === 1 && "border-s",
                      index > 1 && "border-t"
                    )}
                  >
                    <Text className="text-sm text-muted-foreground">
                      {stat.label}
                    </Text>
                    <Text
                      numberOfLines={1}
                      adjustsFontSizeToFit
                      className="text-2xl font-semibold tracking-tight tabular-nums"
                    >
                      {stat.value}
                    </Text>
                    <Text
                      className={cn(
                        "text-xs",
                        stat.up ? "text-green-500" : "text-red-500"
                      )}
                    >
                      {`${stat.change} vs. previous period`}
                    </Text>
                  </View>
                ))}
              </View>
              <View className="rounded-xl bg-muted px-4 pt-3.5 pb-2">
                <View className="flex-row items-start gap-2">
                  <View
                    accessibilityLiveRegion="polite"
                    className="min-w-0 flex-1"
                  >
                    <Text className="text-sm text-muted-foreground">
                      {activeDay ? activeDay.day : "Weekly sales"}
                    </Text>
                    <Text className="text-2xl font-semibold tabular-nums">
                      {activeDay
                        ? money((activeDay.online + activeDay.store) * 1000)
                        : money(1284500 * scale)}
                    </Text>
                  </View>
                  <View className="flex-row gap-3">
                    {SERIES.map((series) => (
                      <View
                        key={series.key}
                        className="flex-row items-center gap-[0.3125rem]"
                      >
                        <View
                          className={cn("size-2 rounded-full", series.swatch)}
                        />
                        <Text className="text-xs text-muted-foreground">
                          {chartConfig[series.key].label}
                        </Text>
                      </View>
                    ))}
                  </View>
                </View>
                <ChartContainer
                  config={chartConfig}
                  accessibilityLabel="Weekly sales"
                  className="mt-3.5 h-40 w-full"
                >
                  <BarChart
                    data={data}
                    barGap={3}
                    margin={{ top: 4, right: 0, left: 0, bottom: 0 }}
                    onActiveIndexChange={setActive}
                  >
                    <CartesianGrid vertical={false} strokeDasharray="3 3" />
                    <XAxis dataKey="day" tickMargin={6} />
                    <ChartTooltip content={() => null} cursor={false} />
                    {SERIES.map((series) => (
                      <Bar
                        isAnimationActive={false}
                        key={series.key}
                        dataKey={series.key}
                        fill={chartConfig[series.key].color}
                        radius={3}
                        maxBarSize={12}
                      >
                        {data.map((day) => (
                          <Cell
                            key={day.day}
                            fillOpacity={
                              active === null || active === day.index ? 1 : 0.35
                            }
                          />
                        ))}
                      </Bar>
                    ))}
                  </BarChart>
                </ChartContainer>
              </View>
              <View className="-mb-2 gap-3">
                <Text role="heading" className="px-1 text-sm font-semibold">
                  Recent orders
                </Text>
                <ChipGroup
                  value={[filter]}
                  onValueChange={(value) => {
                    if (!value[0]) return
                    setFilter(value[0])
                    onFilterChange?.(value[0])
                  }}
                  accessibilityLabel="Order status"
                >
                  {filters.map((item) => (
                    <Chip key={item} value={item}>
                      {item}
                    </Chip>
                  ))}
                </ChipGroup>
              </View>
              <AnimatedView
                key={filter}
                entering={FadeInDown.duration(700).reduceMotion(
                  ReduceMotion.System
                )}
              >
                <ItemGroup variant="inset">
                  {list.map((order, index) => (
                    <React.Fragment key={order.id}>
                      {index > 0 ? (
                        <ItemSeparator className="ms-[3.75rem]" />
                      ) : null}
                      <Item size="sm" onPress={() => onOrderPress?.(order.id)}>
                        <Avatar>
                          <AvatarFallback className={order.tint}>
                            {initials(order.name)}
                          </AvatarFallback>
                        </Avatar>
                        <ItemContent className="gap-0">
                          <ItemTitle className="font-semibold">
                            {order.name}
                          </ItemTitle>
                          <ItemDescription className="text-sm">
                            {`${order.product} · ${order.id}`}
                          </ItemDescription>
                        </ItemContent>
                        <ItemActions className="flex-col items-end gap-0">
                          <Text className="text-base tabular-nums">
                            {money(order.total)}
                          </Text>
                          <Text
                            className={cn(
                              "text-xs",
                              order.status === "Returned"
                                ? "text-destructive"
                                : "text-muted-foreground"
                            )}
                          >
                            {order.status}
                          </Text>
                        </ItemActions>
                      </Item>
                    </React.Fragment>
                  ))}
                </ItemGroup>
              </AnimatedView>
            </View>
          </HeaderScrollView>
        </>
      ) : (
        <OtherSection value={section} />
      )}
      <TabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      >
        <TabBarList>
          {SECTIONS.map((item) => (
            <TabBarItem
              key={item.value}
              value={item.value}
              icon={item.icon}
              badge={item.badge}
            >
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Screen>
  )
}

export { DashStore }
```

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), [Chart](https://ui.suiss.app/docs/components/chart), [Chip](https://ui.suiss.app/docs/components/chip), [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu), [Header](https://ui.suiss.app/docs/components/header), [Icon](https://ui.suiss.app/docs/components/icon), [Item](https://ui.suiss.app/docs/components/item), [Screen](https://ui.suiss.app/docs/components/screen), [Tab Bar](https://ui.suiss.app/docs/components/tab-bar), [Text](https://ui.suiss.app/docs/components/text), [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/dash-store.tsx`:

```tsx title="stylesheet/components/blocks/dash-store.tsx"
import {
  CaretDownIcon,
  ChartBarIcon,
  HandbagIcon,
  HouseIcon,
  type Icon as PhosphorIcon,
  StorefrontIcon,
  UsersIcon,
} from "phosphor-react-native"
import * as React from "react"
import { StyleSheet, View } from "react-native"
import { FadeInDown, ReduceMotion } from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Bar,
  BarChart,
  CartesianGrid,
  Cell,
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  XAxis,
} from "@/components/ui/chart"
import { Chip, ChipGroup } from "@/components/ui/chip"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Header,
  HeaderActions,
  HeaderLargeTitle,
  HeaderScrollView,
  HeaderTitle,
  useHeaderScroll,
} from "@/components/ui/header"
import { Icon } from "@/components/ui/icon"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
} from "@/components/ui/item"
import { Screen } from "@/components/ui/screen"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"
import {
  fontSize,
  palette,
  radius,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

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

type Tone = keyof ThemeColors | keyof typeof palette

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

const SECTIONS: {
  value: string
  label: string
  icon: PhosphorIcon
  badge?: number
}[] = [
  { value: "overview", label: "Overview", icon: HouseIcon },
  { value: "orders", label: "Orders", icon: HandbagIcon, badge: 12 },
  { value: "customers", label: "Customers", icon: UsersIcon },
  { value: "reports", label: "Reports", icon: ChartBarIcon },
]

const STORES = ["All stores", "Union Square", "Fifth Avenue", "Michigan Avenue"]

const ranges = [
  { value: "24h", label: "24 Hours", scale: 0.15 },
  { value: "7d", label: "7 Days", scale: 1 },
  { value: "30d", label: "30 Days", scale: 4.3 },
]

const week = [
  { day: "Mon", online: 42, store: 46 },
  { day: "Tue", online: 58, store: 44 },
  { day: "Wed", online: 50, store: 56 },
  { day: "Thu", online: 68, store: 54 },
  { day: "Fri", online: 64, store: 70 },
  { day: "Sat", online: 80, store: 62 },
  { day: "Sun", online: 72, store: 76 },
]

const chartConfig = {
  online: { label: "Online", color: "chart-1" },
  store: { label: "In store", color: "chart-2" },
} satisfies ChartConfig

const SERIES = [
  { key: "online", swatch: "chart1" },
  { key: "store", swatch: "chart2" },
] as const

const orders: {
  id: string
  name: string
  product: string
  status: string
  total: number
  tint: Tone
}[] = [
  {
    id: "W1048",
    name: "Jamie Rivera",
    product: "Phone Pro",
    status: "Shipped",
    total: 999,
    tint: "blue500",
  },
  {
    id: "W1047",
    name: "Morgan Lee",
    product: "Book Air",
    status: "Processing",
    total: 1099,
    tint: "orange500",
  },
  {
    id: "W1046",
    name: "Riley Chen",
    product: "Watch Series 11",
    status: "Delivered",
    total: 399,
    tint: "green500",
  },
  {
    id: "W1045",
    name: "Avery Brooks",
    product: "Pods Studio",
    status: "Returned",
    total: 549,
    tint: "pink500",
  },
  {
    id: "W1044",
    name: "Taylor Kim",
    product: "Pad Air",
    status: "Delivered",
    total: 599,
    tint: "cyan500",
  },
]

const filters = ["All", "Shipped", "Delivered", "Returned"]

const styles = StyleSheet.create({
  other: { flex: 1, gap: 8, paddingHorizontal: 16, paddingTop: 52 },
  otherTitle: { ...fontSize["3xl"], fontWeight: "700" },
  body: { gap: 20, paddingHorizontal: 16, paddingTop: 12, paddingBottom: 32 },
  ranges: { alignSelf: "stretch" },
  range: { flex: 1 },
  stats: {
    flexDirection: "row",
    flexWrap: "wrap",
    overflow: "hidden",
    borderRadius: radius.xl,
  },
  stat: {
    width: "50%",
    gap: 2,
    paddingHorizontal: 16,
    paddingVertical: 14,
  },
  caption: { ...fontSize.sm },
  statValue: {
    ...fontSize["2xl"],
    fontWeight: "600",
    letterSpacing: -0.7,
    fontVariant: ["tabular-nums"],
  },
  card: {
    borderRadius: radius.xl,
    paddingHorizontal: 16,
    paddingTop: 14,
    paddingBottom: 8,
  },
  summary: { flexDirection: "row", alignItems: "flex-start", gap: 8 },
  reading: { minWidth: 0, flex: 1 },
  total: {
    ...fontSize["2xl"],
    fontWeight: "600",
    fontVariant: ["tabular-nums"],
  },
  legend: { flexDirection: "row", gap: 12 },
  legendItem: { flexDirection: "row", alignItems: "center", gap: 5 },
  dot: { width: 8, height: 8, borderRadius: 9999 },
  chart: { marginTop: 14, height: 160 },
  orders: { marginBottom: -8, gap: 12 },
  heading: { paddingHorizontal: 4, ...fontSize.sm, fontWeight: "600" },
  price: { ...fontSize.base, fontVariant: ["tabular-nums"] },
  actions: { flexDirection: "column", alignItems: "flex-end", gap: 0 },
})

const THOUSANDS = /\B(?=(\d{3})+(?!\d))/g

function money(value: number) {
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
      maximumFractionDigits: 0,
    }).format(value)
  } catch {
    return `$${Math.round(value).toString().replace(THOUSANDS, ",")}`
  }
}

function count(value: number) {
  try {
    return new Intl.NumberFormat("en-US").format(value)
  } catch {
    return String(value).replace(THOUSANDS, ",")
  }
}

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

function StoreMenu({
  value,
  onValueChange,
}: {
  value: string
  onValueChange: (value: string) => void
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        render={<Button variant="secondary" size="sm" />}
        accessibilityLabel={`Store, ${value}`}
      >
        <Icon as={StorefrontIcon} size={16} />
        <Icon as={CaretDownIcon} weight="bold" size={12} />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuRadioGroup
          value={value}
          onValueChange={(next) => {
            if (typeof next === "string") onValueChange(next)
          }}
        >
          {STORES.map((store) => (
            <DropdownMenuRadioItem key={store} value={store}>
              <Text>{store}</Text>
            </DropdownMenuRadioItem>
          ))}
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function OtherSection({ value }: { value: string }) {
  const { colors } = useTheme()
  const label = SECTIONS.find((section) => section.value === value)?.label
  return (
    <View style={styles.other}>
      <Text role="heading" style={styles.otherTitle}>
        {label}
      </Text>
      <Text style={[fontSize.lg, { color: colors.mutedForeground }]}>
        This section isn't part of the sample. Choose Overview.
      </Text>
    </View>
  )
}

function DashStore({
  onRangeChange,
  onFilterChange,
  onStoreChange,
  onOrderPress,
  onNavigate,
}: {
  onRangeChange?: (range: string) => void
  onFilterChange?: (filter: string) => void
  onStoreChange?: (store: string) => void
  onOrderPress?: (id: string) => void
  onNavigate?: (section: string) => void
}) {
  const { colors } = useTheme()
  const [section, setSection] = React.useState("overview")
  const [store, setStore] = React.useState("All stores")
  const [range, setRange] = React.useState("7d")
  const [filter, setFilter] = React.useState("All")
  const [active, setActive] = React.useState<number | null>(null)
  const scroll = useHeaderScroll()
  const scale = ranges.find((item) => item.value === range)?.scale ?? 1
  const data = week.map((day, index) => ({ ...day, index }))
  const activeDay = active === null ? undefined : week[active]
  const list = orders.filter(
    (order) => filter === "All" || order.status === filter
  )

  const stats = [
    {
      label: "Revenue",
      value: money(1284500 * scale),
      change: "+12.4%",
      up: true,
    },
    {
      label: "Orders",
      value: count(Math.round(842 * scale)),
      change: "+8.1%",
      up: true,
    },
    { label: "Average order", value: money(1525), change: "−2.3%", up: false },
    { label: "Conversion", value: "3.42%", change: "+0.4 pts", up: true },
  ]

  return (
    <Screen edges={["top", "right", "left"]}>
      {section === "overview" ? (
        <>
          <Header scroll={scroll}>
            <HeaderTitle>Overview</HeaderTitle>
            <HeaderActions>
              <StoreMenu
                value={store}
                onValueChange={(value) => {
                  setStore(value)
                  onStoreChange?.(value)
                }}
              />
            </HeaderActions>
          </Header>
          <HeaderScrollView scroll={scroll}>
            <HeaderLargeTitle subtitle={store}>Overview</HeaderLargeTitle>
            <View style={styles.body}>
              <ToggleGroup
                spacing={0}
                value={[range]}
                onValueChange={(value) => {
                  if (!value[0]) return
                  setRange(value[0])
                  onRangeChange?.(value[0])
                }}
                accessibilityLabel="Range"
                style={styles.ranges}
              >
                {ranges.map((item) => (
                  <ToggleGroupItem
                    key={item.value}
                    value={item.value}
                    size="sm"
                    style={styles.range}
                  >
                    <Text>{item.label}</Text>
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
              <View style={[styles.stats, { backgroundColor: colors.muted }]}>
                {stats.map((stat, index) => (
                  <View
                    key={stat.label}
                    accessible
                    style={[
                      styles.stat,
                      { borderColor: colors.border },
                      index % 2 === 1 && {
                        borderStartWidth: StyleSheet.hairlineWidth,
                      },
                      index > 1 && { borderTopWidth: StyleSheet.hairlineWidth },
                    ]}
                  >
                    <Text
                      style={[
                        styles.caption,
                        { color: colors.mutedForeground },
                      ]}
                    >
                      {stat.label}
                    </Text>
                    <Text
                      numberOfLines={1}
                      adjustsFontSizeToFit
                      style={styles.statValue}
                    >
                      {stat.value}
                    </Text>
                    <Text
                      style={[
                        fontSize.xs,
                        { color: stat.up ? palette.green500 : palette.red500 },
                      ]}
                    >
                      {`${stat.change} vs. previous period`}
                    </Text>
                  </View>
                ))}
              </View>
              <View style={[styles.card, { backgroundColor: colors.muted }]}>
                <View style={styles.summary}>
                  <View accessibilityLiveRegion="polite" style={styles.reading}>
                    <Text
                      style={[
                        styles.caption,
                        { color: colors.mutedForeground },
                      ]}
                    >
                      {activeDay ? activeDay.day : "Weekly sales"}
                    </Text>
                    <Text style={styles.total}>
                      {activeDay
                        ? money((activeDay.online + activeDay.store) * 1000)
                        : money(1284500 * scale)}
                    </Text>
                  </View>
                  <View style={styles.legend}>
                    {SERIES.map((series) => (
                      <View key={series.key} style={styles.legendItem}>
                        <View
                          style={[
                            styles.dot,
                            { backgroundColor: tone(colors, series.swatch) },
                          ]}
                        />
                        <Text
                          style={[
                            fontSize.xs,
                            { color: colors.mutedForeground },
                          ]}
                        >
                          {chartConfig[series.key].label}
                        </Text>
                      </View>
                    ))}
                  </View>
                </View>
                <ChartContainer
                  config={chartConfig}
                  accessibilityLabel="Weekly sales"
                  style={styles.chart}
                >
                  <BarChart
                    data={data}
                    barGap={3}
                    margin={{ top: 4, right: 0, left: 0, bottom: 0 }}
                    onActiveIndexChange={setActive}
                  >
                    <CartesianGrid vertical={false} strokeDasharray="3 3" />
                    <XAxis dataKey="day" tickMargin={6} />
                    <ChartTooltip content={() => null} cursor={false} />
                    {SERIES.map((series) => (
                      <Bar
                        isAnimationActive={false}
                        key={series.key}
                        dataKey={series.key}
                        fill={chartConfig[series.key].color}
                        radius={3}
                        maxBarSize={12}
                      >
                        {data.map((day) => (
                          <Cell
                            key={day.day}
                            fillOpacity={
                              active === null || active === day.index ? 1 : 0.35
                            }
                          />
                        ))}
                      </Bar>
                    ))}
                  </BarChart>
                </ChartContainer>
              </View>
              <View style={styles.orders}>
                <Text role="heading" style={styles.heading}>
                  Recent orders
                </Text>
                <ChipGroup
                  value={[filter]}
                  onValueChange={(value) => {
                    if (!value[0]) return
                    setFilter(value[0])
                    onFilterChange?.(value[0])
                  }}
                  accessibilityLabel="Order status"
                >
                  {filters.map((item) => (
                    <Chip key={item} value={item}>
                      {item}
                    </Chip>
                  ))}
                </ChipGroup>
              </View>
              <AnimatedView
                key={filter}
                entering={FadeInDown.duration(700).reduceMotion(
                  ReduceMotion.System
                )}
              >
                <ItemGroup variant="inset">
                  {list.map((order, index) => (
                    <React.Fragment key={order.id}>
                      {index > 0 ? (
                        <ItemSeparator style={{ marginStart: 60 }} />
                      ) : null}
                      <Item size="sm" onPress={() => onOrderPress?.(order.id)}>
                        <Avatar>
                          <AvatarFallback
                            style={{
                              backgroundColor: tone(colors, order.tint),
                            }}
                          >
                            {initials(order.name)}
                          </AvatarFallback>
                        </Avatar>
                        <ItemContent style={{ gap: 0 }}>
                          <ItemTitle style={{ fontWeight: "600" }}>
                            {order.name}
                          </ItemTitle>
                          <ItemDescription style={fontSize.sm}>
                            {`${order.product} · ${order.id}`}
                          </ItemDescription>
                        </ItemContent>
                        <ItemActions style={styles.actions}>
                          <Text style={styles.price}>{money(order.total)}</Text>
                          <Text
                            style={[
                              fontSize.xs,
                              {
                                color:
                                  order.status === "Returned"
                                    ? colors.destructive
                                    : colors.mutedForeground,
                              },
                            ]}
                          >
                            {order.status}
                          </Text>
                        </ItemActions>
                      </Item>
                    </React.Fragment>
                  ))}
                </ItemGroup>
              </AnimatedView>
            </View>
          </HeaderScrollView>
        </>
      ) : (
        <OtherSection value={section} />
      )}
      <TabBar
        value={section}
        onValueChange={(value) => {
          setSection(value)
          onNavigate?.(value)
        }}
      >
        <TabBarList>
          {SECTIONS.map((item) => (
            <TabBarItem
              key={item.value}
              value={item.value}
              icon={item.icon}
              badge={item.badge}
            >
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Screen>
  )
}

export { DashStore }
```

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/DashStore.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/Chart.swift`: see [Chart](https://ui.suiss.app/docs/components/chart.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/Empty.swift`: see [Empty](https://ui.suiss.app/docs/components/empty.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/Header.swift`: see [Header](https://ui.suiss.app/docs/components/header.md)

- `swift/Components/Item.swift`: see [Item](https://ui.suiss.app/docs/components/item.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/TabBar.swift`: see [Tab Bar](https://ui.suiss.app/docs/components/tab-bar.md)

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

- `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)

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/DashStore.kt`: shown above

- `kotlin/blocks/DashAnalytics.kt`: see [Dash Analytics](https://ui.suiss.app/docs/blocks/dash-analytics.md)

- `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/Chart.kt`: see [Chart](https://ui.suiss.app/docs/components/chart.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/DropdownMenu.kt`: see [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu.md)

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

- `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/Header.kt`: see [Header](https://ui.suiss.app/docs/components/header.md)

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

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

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

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

- `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/TabBar.kt`: see [Tab Bar](https://ui.suiss.app/docs/components/tab-bar.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)

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

## Usage

```tsx
import { DashStore } from "@/components/blocks/dash-store"
```

```tsx
<DashStore />
```

Replace `STORES`, `ranges`, `week`, `orders` and `filters` with your store data. Pass `onRangeChange`, `onFilterChange`, `onStoreChange`, `onOrderPress` and `onNavigate` to react to each control.

