Components

Data Gallery

A standalone, virtualized card gallery view for record lists. Toolbar-driven card design (variant, cover style, density, field bindings) plus full TanStack Table integration for sorting, filtering, grouping, and saved views.

Client Only

Installation

pnpm dlx @docyrus/cli add @docyrus/ui-data-gallery
UI Primitives(14 components)
npx shadcn@latest add action-bar avatar badge button card checkbox input label popover select separator skeleton switch toggle-group
Required Packages(3 packages)
pnpm add @tanstack/react-table @tanstack/react-virtual react-querybuilder

Usage

import {
  DataGallery,
  DataGalleryToolbar,
  useDataGallery
} from '@docyrus/ui/components/data-gallery';
import type {
  ColumnDef
} from '@docyrus/ui/components/data-grid';
import type {
  DataGalleryCardConfigSerializable
} from '@docyrus/ui/components/data-gallery';

const columns: ColumnDef<Product>[] = [
  { accessorKey: 'name', meta: { label: 'Name', cell: { variant: 'short-text' } } },
  { accessorKey: 'cover', meta: { label: 'Cover', cell: { variant: 'image' } } },
  { accessorKey: 'status', meta: {
    label: 'Status',
    cell: { variant: 'status', options: statusOptions }
  } }
  // ...
];

function MyGallery() {
  const [cardConfig, setCardConfig] = useState<DataGalleryCardConfigSerializable>({
    titleField: 'name',
    descriptionField: 'tagline',
    coverImageField: 'cover',
    badgeField: 'status'
  });

  const galleryProps = useDataGallery({
    data,
    columns,
    enableSearch: true,
    enableGrouping: true,
    displayConfig: {
      cardVariant: 'detailed',
      coverStyle: 'top-md',
      columnCount: 'flex'
    }
  });

  const {
    table, displayConfig, setDisplayConfig, searchQuery, setSearchQuery
  } = galleryProps;

  return (
    <div className="flex flex-col gap-3">
      <DataGalleryToolbar
        table={table}
        displayConfig={displayConfig}
        onDisplayConfigChange={setDisplayConfig}
        cardConfig={cardConfig}
        onCardConfigChange={setCardConfig}
        searchQuery={searchQuery}
        onSearchQueryChange={setSearchQuery}
        enableFilter
        enableSort
        enableGroup
        enableDisplay
        enableCardConfig />

      <DataGallery
        {...galleryProps}
        cardConfig={cardConfig}
        height={600} />
    </div>
  );
}

Card Variants

The card variant controls the default layout. Each variant ships with sensible defaults that the display menu can still override. Pick the variant via displayConfig.cardVariant or per-card via cardConfig.variant. The demo above exposes the variant picker in its Display toolbar menu — try switching between them live.

VariantUse caseDefaults
detailedRecords with field labels (CRM, inventory)top-md cover, comfortable density, labels on
compactDense grids with many itemstop-sm cover, compact density, labels off
mediaImage-led catalogstop-lg cover, minimal text
profilePeople / contactsNo cover, centered avatar + name
productE-commerce catalogstop-md cover, footer chip enabled

Card Slots

Each card composes from a fixed set of slots. Bind a column ID to a slot via cardConfig, or supply a custom renderer (renderAvatar, renderCover, renderBadge, renderTimeline, renderActions). For complete control, return your own JSX from renderCard.

SlotField propertyCustom renderer
TitletitleField—
DescriptiondescriptionField / subtitleField—
AvataravatarFieldrenderAvatar
Cover imagecoverImageFieldrenderCover
Badge (corner chip)badgeFieldrenderBadge
Timeline (footer date)timelineFieldrenderTimeline
Actions menu (header)—renderActions
Body fieldsbodyFields— (uses cell renderer)
Footer fieldsfooterFields— (uses cell renderer)

bodyFields defaults to every visible column the table exposes minus the ones already bound to a dedicated slot.

Cover & avatar value resolution

coverImageField and avatarField read from row.original[slug] directly rather than through TanStack's accessorFn. Docyrus columns run a normalizeFieldValue step inside the accessor that flattens field-image arrays ([{ signed_url, … }]) to a JSON string and field-userSelect objects ({ id, name, avatar_url }) to a UUID — both lose the URL the card needs. The raw read keeps the original shape so:

  • Cover images resolve through resolveImageUrl, which walks array → object → first usable URL key (signed_url, signedUrl, url, src, image_url, thumb_url).
  • Avatars resolve through resolveAvatarUrlAndLabel. When the bound column has cellOpts.variant === 'user', the helper looks the row's user id up in cellOpts.options (the tenant-wide users list useDocyrusDataGrid injects), picking the avatar URL + display name from the option entry — so users still get their real photos even when the expanded row payload only carries { id, name }.

Avatar / cover duplicate guard

When avatarField and coverImageField are bound to the same column (typically from an old auto-detect or saved view that picked company_logo for both), the avatar slot is suppressed automatically. The cover already carries the image; rendering a thumbnail of it next to the checkbox just doubles the visual.

Layout lockstep

coverStyle: 'left' and layoutOrientation: 'horizontal' are coupled — picking one auto-promotes the other via resolveCardLayout. Setting coverStyle: 'top-md' together with layoutOrientation: 'horizontal' falls back to 'vertical' so the cover doesn't stretch awkwardly alongside the body.

Display Config

The displayConfig object is owned by useDataGallery and mutated by the toolbar's display menu. Pass displayConfig as the initial state; the hook re-syncs only when you swap the prop identity (e.g. switching saved views).

FieldTypeDefaultDescription
columnCount'flex' | 1 | 2 | 3 | 4 | 5 | 6'flex'Auto-fits cards to container when 'flex'.
cardSize'full' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl''full'Caps individual card max width.
density'compact' | 'comfortable''comfortable'Padding + font size.
coverStyle'none' | 'top-sm' | 'top-md' | 'top-lg' | 'left''top-md'Position + height of the cover image.
layoutOrientation'vertical' | 'horizontal''vertical'Card-internal layout. Auto-promoted to horizontal when coverStyle === 'left'.
showFieldLabelsbooleantrueShow field name next to each body value.
showCardHeaderbooleantrueShow the title / avatar / badge header.
showCardFooterbooleanfalseShow the timeline + footer fields strip.
cardVariantDataGalleryCardVariant'detailed'Preset selector.

Variant changes are preset applications

Picking a new cardVariant from the display menu batch-updates every display field defined in VARIANT_DISPLAY_PRESETS (cover style, density, label visibility, footer toggle, …) rather than just renaming the variant. After application the display config stays the single source of truth — cardVariant is a "last preset applied" label rather than a runtime override. The variant additionally drives non-display visual details (avatar / title sizing) via VARIANT_VISUAL_DEFAULTS at render time.

Dynamic card height

Cards size themselves to their content; the virtualizer reads each row's real height after mount via measureElement and updates row positions in place. estimatedCardHeight is just the initial guess used for the first paint — undershoot is fine, the measured value replaces it once the row renders. This keeps tall content (extra body fields, wrapped descriptions, large covers) from getting clipped inside a fixed-height row band.

Color treatment

Every option-backed chip — status, select, enum, multi-select, tag-select, relation — renders with the same neutral surface: shared border, transparent background, plain foreground text. The option's color appears only as a small dot before the label, run through humanizeColor() (a color-mix in OKLab that pulls Tailwind-500 defaults toward an earthy warm-neutral so cards don't pick up a "designer palette" of saturated greens / purples / cyans). It's the Linear / Notion / Tana pattern — the hue identity survives, but it doesn't dominate.

Toolbar

The toolbar is a thin orchestrator over the standalone menu components. Enable each section via a feature flag.

SectionFlagNotes
SearchenableSearchRenders DataGallerySearch (debounced) when onSearchQueryChange is provided.
FilterenableFilterReuses DataGridFilterMenu — same filter UX as the data grid.
SortenableSortReuses DataGridSortMenu.
GroupenableGroupReuses DataGridGroupMenu.
DisplayenableDisplayGallery-specific menu (variant, columns, cover style, density).
Card fieldsenableCardConfigGallery-specific menu for slot bindings.
Saved viewsenableViewReuses DataGridViewMenu (grid + gallery storage is shared).

The <DataGalleryToolbar> also supports startContent and endContent slots for custom controls, plus an onAdd handler that renders a primary Add button on the right edge.

Selection & Actions

Set enableRowSelection on useDataGallery (TanStack default applies). Each card shows a hover-revealed checkbox; multi-selection raises an ActionBar. Pass actions via the actions prop on <DataGallery>:

<DataGallery
  {...galleryProps}
  actions={[
    { key: 'archive', label: 'Archive', onAction: rows => archive(rows) },
    { key: 'delete', label: 'Delete', variant: 'destructive', onAction: rows => deleteAll(rows) }
  ]} />

Pagination

Set pagingMode: 'standard' to enable a paginated footer instead of virtual scrolling:

const galleryProps = useDataGallery({
  data,
  columns,
  pagingMode: 'standard',
  pageSize: 24
});

API Reference

useDataGallery(options)

OptionTypeDefaultDescription
dataArray<TData>—Records to render.
columnsArray<ColumnDef<TData>>—TanStack column defs. Cell variants are read from meta.cell.
displayConfigPartial<DataGalleryDisplayConfig>—Initial display config (the hook owns it after mount).
onDisplayConfigChange(config) => void—Fires whenever the display config changes.
enableGroupingbooleanfalseAdds the grouped row model so the group menu can group cards.
enableSearchbooleanfalseEnables the TanStack global filter pipeline for the toolbar search input.
onSearch(query) => void—Side-channel for server-side keyword search.
minCardWidthnumber260Min width used by the 'flex' column count auto-fit.
estimatedCardHeightnumber360Initial virtualizer estimate. Replaced per-row by measureElement after first render — undershoot is preferred to overshoot.
overscannumber3Virtualizer overscan rows.
pagingMode'virtual-scroll' | 'standard''virtual-scroll'Standard adds a paging footer (see DataGridPaginationFooter).
pageSizenumber24Initial page size for standard pagination.
dir'ltr' | 'rtl'—Direction override. Defaults to the package's useDirection.

<DataGallery />

Accepts the spread return of useDataGallery plus:

PropTypeDefaultDescription
cardConfigDataGalleryCardConfig<TData>—Field bindings + custom renderers.
actionsArray<DataGalleryAction<TData>>—Multi-select action bar buttons.
onCardClick(record, index) => void—Fires when the card is activated (click / Enter / Space). Checkbox clicks are filtered out automatically.
onAdd() => void—Renders an "Add card" tile at the end of the grid.
addLabelstringAdd cardLabel for the add tile.
isReloadingbooleanfalseShows a translucent overlay + spinner over the body.
heightnumber | 'auto'600Fixed pixel height or stretch to container.
startContentReactNode—Rendered before the body (e.g. pivot filters).
endContentReactNode—Rendered after the body.

<DataGalleryToolbar />

PropTypeDescription
tableTable<TData>TanStack table from useDataGallery.
displayConfigDataGalleryDisplayConfigCurrent display config.
onDisplayConfigChange(updater) => voidSetter from useDataGallery.
cardConfigDataGalleryCardConfigSerializableCurrent card bindings (when enableCardConfig).
onCardConfigChange(updater) => voidCard config setter.
searchQuerystringCurrent search value.
onSearchQueryChange(value) => voidSearch setter from useDataGallery.
enableSearchbooleanToggle search input.
enableFilterbooleanToggle filter menu.
enableSortbooleanToggle sort menu.
enableGroupbooleanToggle group menu.
enableDisplaybooleanToggle gallery display menu.
enableCardConfigbooleanToggle card config menu.
enableViewbooleanToggle saved views menu.
viewStorageKeystringLocalStorage key for saved views.
onAdd() => voidPrimary "Add" button handler.
addLabelstringAdd button label.
startContentReactNodeRendered before the data manipulation group.
endContentReactNodeRendered before the add button.

Type Exports

TypeDescription
DataGalleryCardConfig<TData>Card configuration including non-serializable render hooks.
DataGalleryCardConfigSerializableSerializable subset of DataGalleryCardConfig (no functions). Safe for saved views.
DataGalleryDisplayConfigToolbar-driven display settings.
DataGalleryCardVariant'detailed' | 'compact' | 'media' | 'profile' | 'product'
DataGalleryCoverStyle'none' | 'top-sm' | 'top-md' | 'top-lg' | 'left'
DataGalleryCardSize'full' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
DataGalleryColumnCount'flex' | 1 | 2 | 3 | 4 | 5 | 6
DataGalleryDensity'compact' | 'comfortable'
DataGalleryLayoutOrientation'vertical' | 'horizontal'
DataGalleryAction<TData>Action bar button definition.
SavedDataGalleryViewSaved view payload (icon, filters, display + card config).

Components

ComponentDescription
DataGalleryMain virtualized card grid.
DataGalleryCardSingle card renderer (variant + slot composition).
DataGalleryToolbarOrchestrator for search / filter / sort / group / display / card config menus.
DataGallerySearchStandalone debounced search input.
DataGalleryDisplayMenuGallery-specific display options popover.
DataGalleryCardConfigMenuField-binding popover (title/description/avatar/cover/badge/timeline).
DataGallerySkeleton, DataGallerySkeletonGrid, DataGallerySkeletonToolbarLoading skeletons.
useDataGalleryHook that owns table state + display config + virtualization.

Helper Exports

Re-exported from @docyrus/ui/components/data-gallery:

ExportDescription
resolveColumnsPerRow({ columnCount, containerWidth, minCardWidth })Pure helper for the responsive column-count math. Returns the largest column count whose cards still satisfy minCardWidth.
resolveImageUrl(value)Walks Docyrus image payloads (string URL, file array, { signed_url, … } object) and returns the first usable URL.
getCardSizePx(size)Maps a DataGalleryCardSize enum to its pixel cap (null for 'full').
getCoverHeightPx(style)Maps 'top-sm' | 'top-md' | 'top-lg' → pixel heights used by the cover slot.
VARIANT_DISPLAY_PRESETSPer-variant display-config snapshot applied when the user picks a new variant.
VARIANT_VISUAL_DEFAULTSPer-variant render-time settings (avatar size, title size).
resolveCardLayout({ layoutOrientation, coverStyle })Keeps coverStyle === 'left' and layoutOrientation === 'horizontal' in lockstep — picking one auto-promotes the other.
DEFAULT_GAP, DEFAULT_MIN_CARD_WIDTHLayout constants.

Re-exported from @docyrus/ui/components/data-gallery/lib/data-gallery:

ExportDescription
getCellValue(row, table, columnId)Safe row.getValue wrapper that returns undefined for columns the table doesn't expose.
getRawCellValue(row, columnId)Reads from row.original[columnId] directly, bypassing TanStack's accessorFn. Used for cover / avatar slots whose full object/array shape would be flattened by Docyrus's normalizeFieldValue.
resolveBodyFields({ table, bodyFields, reservedColumnIds })Default body-field resolver.

On this page