FoodEase

Searchable Container

Drop-in searchable grid/list container — combines PaginatedCollection, ExpandableSearch, and useVisibleItems into one component

Overview

SearchableContainer is the primary way to render a filterable, auto-paginated grid or list in FoodEase apps. It wraps PaginatedCollection with an overlay ExpandableSearch and the useVisibleItems hook to handle filtering automatically.

You provide items, a renderItem function, and a gridConfig — it handles layout, pagination, and search.

Installation

npx shadcn@latest add https://foodease-dev-registry.cap.reachcinema.io/r/v1/searchable-container.json

This installs:

  • SearchableContainer component
  • PaginatedCollection component
  • ExpandableSearch component
  • useVisibleItems hook
  • CaseRender component
  • match-sorter npm package

Usage

Basic Grid

import SearchableContainer from "@/components/ui/searchable-container"

interface Product {
  id: string
  name: string
  price: number
}

export default function ProductGrid({ products }: { products: Product[] }) {
  return (
    <div className="flex-1 h-full">
      <SearchableContainer
        items={products}
        options={{ keys: ["name"] }}
        gridConfig={{ minCardWidth: 180, minCardHeight: 120, gap: 8 }}
        showSearch
        allowSearchAsYouType
        renderItem={(product) => (
          <button
            type="button"
            className="absolute inset-0 rounded-xl bg-primary/10 p-2 text-start"
          >
            <p className="font-bold truncate">{product.name}</p>
            <p className="text-sm">${product.price.toFixed(2)}</p>
          </button>
        )}
      />
    </div>
  )
}

Important: The container must have a defined height (e.g. h-full, h-[400px]) for the ResizeObserver to calculate layout correctly.

List Layout

Render items as a single-column list that fills the available height:

<SearchableContainer
  items={items}
  options={{ keys: ["name"] }}
  gridConfig={{ minCardHeight: 60, gap: 4 }}
  showSearch
  allowSearchAsYouType
  renderItem={(item) => (
    <div className="absolute inset-0 flex items-center px-3 border rounded-lg">
      {item.name}
    </div>
  )}
/>

Pass layout="list" to PaginatedCollection via a custom renderPagination if you need explicit list mode — or just set minCardWidth large enough that only one column fits.

Custom Empty State

<SearchableContainer
  items={items}
  options={{ keys: ["name"] }}
  renderItem={(item) => <ItemCard item={item} />}
  renderEmpty={() => (
    <div className="center flex-col gap-2 h-full">
      <p className="text-muted-foreground">No items found</p>
      <button onClick={refetch} className="text-primary underline text-sm">
        Retry
      </button>
    </div>
  )}
/>

Search with Multiple Keys

options accepts any match-sorter options — search across multiple fields:

<SearchableContainer
  items={staff}
  options={{ keys: ["firstName", "lastName", "roleName"] }}
  gridConfig={{ minCardWidth: 200, minCardHeight: 80, gap: 8 }}
  showSearch
  allowSearchAsYouType
  renderItem={(member) => <StaffCard member={member} />}
/>

Omit showSearch to render the grid without the search overlay — useful when you want external search control:

<SearchableContainer
  items={filteredItems}
  options={{ keys: ["name"] }}
  gridConfig={{ minCardWidth: 150, minCardHeight: 45, gap: 8 }}
  renderItem={(item) => <ItemClassButton item={item} />}
/>

Typical POS Pattern (Item Classes + Items)

This is the standard two-section layout used in FoodEase POS pages:

// Top section — item classes (categories), short cards
<section className="flex">
  <SearchableContainer
    items={classes}
    options={{ keys: ["name"] }}
    gridConfig={{ minCardWidth: 150, minCardHeight: 45, gap: 8 }}
    showSearch
    allowSearchAsYouType
    renderItem={(cls) => (
      <button
        type="button"
        className="absolute inset-0 rounded-xl p-1 text-sm truncate"
        style={{ backgroundColor: cls.color }}
      >
        {cls.name}
      </button>
    )}
  />
</section>

// Bottom section — items, taller cards
<section className="flex">
  <SearchableContainer
    items={filteredItems}
    options={{ keys: ["name"] }}
    gridConfig={{ minCardWidth: 180, minCardHeight: 105, gap: 8 }}
    showSearch
    allowSearchAsYouType
    renderItem={(item) => <ItemCard item={item} />}
  />
</section>

How It Works

  1. useVisibleItems filters items against the current search query using match-sorter
  2. ExpandableSearch (top-right overlay) calls setQuery as the user types
  3. PaginatedCollection receives the filtered list and uses ResizeObserver to measure its container, then auto-calculates how many columns and rows fit given minCardWidth / minCardHeight
  4. Items are sliced into pages based on the calculated cols × rows count
  5. Built-in page buttons appear when there's more than one page

API Reference

SearchableContainer Props

PropTypeDefaultDescription
itemsT[]Array of items to display
optionsMatchSorterOptions<T>match-sorter search options. keys specifies which fields to search
renderItem(item: T) => ReactNodeRender function for each item. Use absolute inset-0 inside for full card coverage
gridConfigGridConfigsee belowCard size and gap constraints
showSearchbooleanfalseShow the expandable search overlay
allowSearchAsYouTypebooleanfalseFilter on every keystroke (vs on Enter)
containerClassNamestringExtra classes on the outer <section>
expandedClassNamestringExtra classes applied to the search input when expanded

GridConfig

FieldTypeDefaultDescription
minCardWidthnumber320Minimum card width in px. More columns fit as the container grows
minCardHeightnumber400Minimum card height in px. More rows fit as the container grows
gapnumber16Gap between cards in px

renderItem Pattern

Each item is rendered inside a relative wrapper div that fills its grid cell. Use absolute inset-0 on your card element to fill the cell completely:

renderItem={(item) => (
  <button className="absolute inset-0 rounded-xl p-2 bg-card border">
    {item.name}
  </button>
)}

Dependencies

PackagePurpose
match-sorterFuzzy search filtering
lucide-reactSearch and Shrink icons in ExpandableSearch

Registry dependencies auto-installed: paginated-collection, expand-search, use-visible-items, case-render.

On this page