streaming-ui-script · team directory ← Back to docs
Live demo · search + sheet + pagination

A searchable team directory with detail sheets and rich tooltips

Profile cards, avatar groups, a department toggle group, a paginated list, an empty state, and a slide-in Sheet for full member detail — all wired through a single search + pagination Query. Hover an avatar in the row of "popular" members to reveal a tooltip with the person's full title.

Live directory

Search by name or skill, switch departments, paginate through results, and click "View profile" to open a detail sheet. Reset the search to see the empty-state CTA.

$search = ""
$department = "all"
$page = 1
$selectedId = ""

data = Query("list_members", {q: $search, department: $department, page: $page}, {
  rows: [], total: 0, pages: 1, popular: []
})
detail = Query("get_member", {id: $selectedId}, {
  id: "", name: "", role: "", bio: "", tags: [], stats: [], avatar: ""
})

header = PageHeader(
  "Team directory",
  "Everyone at Acme Robotics, searchable in one place",
  Breadcrumb([BreadcrumbItem("Company", "#"), BreadcrumbItem("People")]),
  [
    Button("Export CSV", Action([@ToAssistant("Export the directory as CSV")]), "ghost"),
    Button("Invite",     Action([@ToAssistant("Open the invite-by-email form")]), "primary")
  ],
  Tag("" + data.total + " people", null, "sm", "primary")
)

searchField = FormControl(
  "Search",
  Input("search", "Name, role, skill…", "text", null, $search),
  "Filters update as you type"
)

departmentToggle = FormControl("Department", ToggleGroup("department", [
  {value: "all",   label: "All"},
  {value: "eng",   label: "Engineering", icon: "laptop-code"},
  {value: "ds",    label: "Design",      icon: "palette"},
  {value: "ops",   label: "Operations",  icon: "box"},
  {value: "sales", label: "Sales",       icon: "chart-line"}
], $department))

controls = Stack([searchField, departmentToggle], "row", "m", "stretch", "start", true)

popularRow = Card([
  CardHeader("Most-mentioned this week", "Hover an avatar to see their role"),
  AvatarGroup(data.popular, 6, "lg")
])

cardsGrid = Grid(@Each(data.rows, "u",
  Card([
    ProfileCard(u.name, u.role, u.avatar, u.bio, u.tags, [
      Button("View profile", Action([@Set($selectedId, u.id)]), "secondary", "button", "small"),
      Button("Message",      Action([@ToAssistant("Open a DM thread with " + u.name)]), "ghost", "button", "small")
    ])
  ])
), 3, "m")

emptyState = EmptyState(
  "No matches for your filters",
  "Try a different search term or pick another department.",
  "magnifying-glass",
  Button("Clear filters", Action([@Reset($search), @Reset($department), @Set($page, 1)]), "primary")
)

body = @Count(data.rows) > 0 ? cardsGrid : emptyState

pager = Pagination($page, data.pages, 1)

detailSheet = Sheet(
  detail.name == "" ? "Loading…" : detail.name,
  $selectedId != "",
  [
    Stack([
      Avatar(detail.name, detail.avatar, "xl"),
      Stack([
        TextContent(detail.role, "body-heavy"),
        TextContent(detail.bio, "body", "muted")
      ], "column", "xs")
    ], "row", "m", "start", "start"),
    Separator("horizontal", true),
    TagBlock(detail.tags, "primary", "sm"),
    Separator("horizontal", true),
    MetricGrid(@Each(detail.stats, "s",
      StatCard(s.label, s.value, s.trend, s.delta, s.icon)
    ), 3)
  ],
  "right",
  [
    Button("Close",   Action([@Reset($selectedId)]), "ghost"),
    Button("Message", Action([@ToAssistant("Open a DM thread with " + detail.name), @Reset($selectedId)]), "primary")
  ]
)

root = Stack([header, popularRow, controls, body, pager, detailSheet], "column", "l")

The two tools

list_members handles the paged grid and "popular" avatar row. get_member hydrates the detail Sheet. Both are tiny — the patterns do the visual heavy lifting.

el.setTools({
  list_members: ({ q, department, page }) => {
    const filtered = members.filter(matches({ q, department }));
    const pageSize = 6;
    const pages = Math.max(1, Math.ceil(filtered.length / pageSize));
    return {
      rows: filtered.slice((page - 1) * pageSize, page * pageSize),
      total: filtered.length,
      pages,
      popular: members.slice(0, 6).map(({ name }) => ({ name })),
    };
  },
  get_member: ({ id }) => members.find((m) => m.id === id) ?? emptyMember,
});