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.
Installation
pnpm dlx @docyrus/cli add @docyrus/ui-data-gallerynpx shadcn@latest add action-bar avatar badge button card checkbox input label popover select separator skeleton switch toggle-grouppnpm add @tanstack/react-table @tanstack/react-virtual react-querybuilderUsage
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.
| Variant | Use case | Defaults |
|---|---|---|
detailed | Records with field labels (CRM, inventory) | top-md cover, comfortable density, labels on |
compact | Dense grids with many items | top-sm cover, compact density, labels off |
media | Image-led catalogs | top-lg cover, minimal text |
profile | People / contacts | No cover, centered avatar + name |
product | E-commerce catalogs | top-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.
| Slot | Field property | Custom renderer |
|---|---|---|
| Title | titleField | — |
| Description | descriptionField / subtitleField | — |
| Avatar | avatarField | renderAvatar |
| Cover image | coverImageField | renderCover |
| Badge (corner chip) | badgeField | renderBadge |
| Timeline (footer date) | timelineField | renderTimeline |
| Actions menu (header) | — | renderActions |
| Body fields | bodyFields | — (uses cell renderer) |
| Footer fields | footerFields | — (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 hascellOpts.variant === 'user', the helper looks the row's user id up incellOpts.options(the tenant-wide users listuseDocyrusDataGridinjects), 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).
| Field | Type | Default | Description |
|---|---|---|---|
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'. |
showFieldLabels | boolean | true | Show field name next to each body value. |
showCardHeader | boolean | true | Show the title / avatar / badge header. |
showCardFooter | boolean | false | Show the timeline + footer fields strip. |
cardVariant | DataGalleryCardVariant | '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.
| Section | Flag | Notes |
|---|---|---|
| Search | enableSearch | Renders DataGallerySearch (debounced) when onSearchQueryChange is provided. |
| Filter | enableFilter | Reuses DataGridFilterMenu — same filter UX as the data grid. |
| Sort | enableSort | Reuses DataGridSortMenu. |
| Group | enableGroup | Reuses DataGridGroupMenu. |
| Display | enableDisplay | Gallery-specific menu (variant, columns, cover style, density). |
| Card fields | enableCardConfig | Gallery-specific menu for slot bindings. |
| Saved views | enableView | Reuses 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)
| Option | Type | Default | Description |
|---|---|---|---|
data | Array<TData> | — | Records to render. |
columns | Array<ColumnDef<TData>> | — | TanStack column defs. Cell variants are read from meta.cell. |
displayConfig | Partial<DataGalleryDisplayConfig> | — | Initial display config (the hook owns it after mount). |
onDisplayConfigChange | (config) => void | — | Fires whenever the display config changes. |
enableGrouping | boolean | false | Adds the grouped row model so the group menu can group cards. |
enableSearch | boolean | false | Enables the TanStack global filter pipeline for the toolbar search input. |
onSearch | (query) => void | — | Side-channel for server-side keyword search. |
minCardWidth | number | 260 | Min width used by the 'flex' column count auto-fit. |
estimatedCardHeight | number | 360 | Initial virtualizer estimate. Replaced per-row by measureElement after first render — undershoot is preferred to overshoot. |
overscan | number | 3 | Virtualizer overscan rows. |
pagingMode | 'virtual-scroll' | 'standard' | 'virtual-scroll' | Standard adds a paging footer (see DataGridPaginationFooter). |
pageSize | number | 24 | Initial page size for standard pagination. |
dir | 'ltr' | 'rtl' | — | Direction override. Defaults to the package's useDirection. |
<DataGallery />
Accepts the spread return of useDataGallery plus:
| Prop | Type | Default | Description |
|---|---|---|---|
cardConfig | DataGalleryCardConfig<TData> | — | Field bindings + custom renderers. |
actions | Array<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. |
addLabel | string | Add card | Label for the add tile. |
isReloading | boolean | false | Shows a translucent overlay + spinner over the body. |
height | number | 'auto' | 600 | Fixed pixel height or stretch to container. |
startContent | ReactNode | — | Rendered before the body (e.g. pivot filters). |
endContent | ReactNode | — | Rendered after the body. |
<DataGalleryToolbar />
| Prop | Type | Description |
|---|---|---|
table | Table<TData> | TanStack table from useDataGallery. |
displayConfig | DataGalleryDisplayConfig | Current display config. |
onDisplayConfigChange | (updater) => void | Setter from useDataGallery. |
cardConfig | DataGalleryCardConfigSerializable | Current card bindings (when enableCardConfig). |
onCardConfigChange | (updater) => void | Card config setter. |
searchQuery | string | Current search value. |
onSearchQueryChange | (value) => void | Search setter from useDataGallery. |
enableSearch | boolean | Toggle search input. |
enableFilter | boolean | Toggle filter menu. |
enableSort | boolean | Toggle sort menu. |
enableGroup | boolean | Toggle group menu. |
enableDisplay | boolean | Toggle gallery display menu. |
enableCardConfig | boolean | Toggle card config menu. |
enableView | boolean | Toggle saved views menu. |
viewStorageKey | string | LocalStorage key for saved views. |
onAdd | () => void | Primary "Add" button handler. |
addLabel | string | Add button label. |
startContent | ReactNode | Rendered before the data manipulation group. |
endContent | ReactNode | Rendered before the add button. |
Type Exports
| Type | Description |
|---|---|
DataGalleryCardConfig<TData> | Card configuration including non-serializable render hooks. |
DataGalleryCardConfigSerializable | Serializable subset of DataGalleryCardConfig (no functions). Safe for saved views. |
DataGalleryDisplayConfig | Toolbar-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. |
SavedDataGalleryView | Saved view payload (icon, filters, display + card config). |
Components
| Component | Description |
|---|---|
DataGallery | Main virtualized card grid. |
DataGalleryCard | Single card renderer (variant + slot composition). |
DataGalleryToolbar | Orchestrator for search / filter / sort / group / display / card config menus. |
DataGallerySearch | Standalone debounced search input. |
DataGalleryDisplayMenu | Gallery-specific display options popover. |
DataGalleryCardConfigMenu | Field-binding popover (title/description/avatar/cover/badge/timeline). |
DataGallerySkeleton, DataGallerySkeletonGrid, DataGallerySkeletonToolbar | Loading skeletons. |
useDataGallery | Hook that owns table state + display config + virtualization. |
Helper Exports
Re-exported from @docyrus/ui/components/data-gallery:
| Export | Description |
|---|---|
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_PRESETS | Per-variant display-config snapshot applied when the user picks a new variant. |
VARIANT_VISUAL_DEFAULTS | Per-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_WIDTH | Layout constants. |
Re-exported from @docyrus/ui/components/data-gallery/lib/data-gallery:
| Export | Description |
|---|---|
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. |