# Data Gallery URL: /docs/web/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. **Demo:** ```tsx 'use client'; // @custom-demo import { useCallback, useState } from 'react'; import { DataGallery, DataGalleryToolbar, useDataGallery, type DataGalleryCardConfigSerializable } from '@docyrus/ui/components/data-gallery'; import { getDataGridSelectColumn } from '@docyrus/ui/components/data-grid'; import { Badge } from '@docyrus/ui/primitives/ui/badge'; import { TooltipProvider } from '@docyrus/ui/primitives/ui/tooltip'; import { useGalleryData } from '@/data/data-gallery-data'; const DEFAULT_CARD_CONFIG: DataGalleryCardConfigSerializable = { titleField: 'name', descriptionField: 'tagline', coverImageField: 'cover', badgeField: 'status', avatarField: 'owner', timelineField: 'launchDate', bodyFields: ['price', 'rating', 'inventory'], footerFields: ['category'] }; function DataGalleryDemoInner() { const { data, columns: columnDefs } = useGalleryData(); type Row = typeof data[number]; const columns = [getDataGridSelectColumn ); } }} actions={[ { key: 'archive', label: 'Archive', onAction: (rows) => { // eslint-disable-next-line no-console -- demo console.log('Archive', rows); } }, { key: 'delete', label: 'Delete', variant: 'destructive', onAction: (rows) => { // eslint-disable-next-line no-console -- demo console.log('Delete', rows); } } ]} onCardClick={(record) => { // eslint-disable-next-line no-console -- demo console.log('Open detail', record); }} height={680} /> ); } export function DataGalleryDemo() { return ( ); } ``` ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-data-gallery ``` **Dependencies:** - [lucide-react](https://www.npmjs.com/package/lucide-react) - [@tanstack/react-table](https://www.npmjs.com/package/@tanstack/react-table) - [@tanstack/react-virtual](https://www.npmjs.com/package/@tanstack/react-virtual) - [react-querybuilder](https://www.npmjs.com/package/react-querybuilder) ## Usage ```tsx 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[] = [ { 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({ 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 (
); } ``` ## 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 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). | 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 `` 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 ``: ```tsx 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: ```tsx const galleryProps = useDataGallery({ data, columns, pagingMode: 'standard', pageSize: 24 }); ``` ## API Reference ### `useDataGallery(options)` | Option | Type | Default | Description | |--------|------|---------|-------------| | `data` | `Array` | — | Records to render. | | `columns` | `Array>` | — | TanStack column defs. Cell variants are read from `meta.cell`. | | `displayConfig` | `Partial` | — | 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`. | ### `` Accepts the spread return of `useDataGallery` plus: | Prop | Type | Default | Description | |------|------|---------|-------------| | `cardConfig` | `DataGalleryCardConfig` | — | Field bindings + custom renderers. | | `actions` | `Array>` | — | 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. | ### `` | Prop | Type | Description | |------|------|-------------| | `table` | `Table` | 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` | 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` | 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. |