Entity Grid
Renders rows from an entity source as EntityCard tiles: the thumbnail fills the top of each tile, the status sits on it, and one metadata line runs under the name.
paging says how the set is walked, and the source follows it: scroll loads the next page when
the scroller reaches the last loaded tile, more puts a load-more row under the tiles, and pages
draws the pagination footer. The grid defaults to scroll. A wall of pictures is browsed rather
than read, and the tile a reader wants is found by looking, not by page number.
more and scroll append: the tiles already loaded stay and the new page lands under them. A page
that fails leaves its tiles and puts one error line under them with a retry.
Install
Section titled “Install”Installing this pulls EntityCard, and StatusBadge and Thumbnail with it.
pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/entity-grid.jsonpnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/entity-grid.jsonReact
Loading the React demo…
Svelte
Loading the Svelte demo…
const context = createSgContext({ client });const source = createEntitySource({ client: context.client, entityType: 'Version', fields: ['code', 'image', 'sg_status_list', 'user'], pageSize: 12,});const [artist] = await resolveColumns(context.schema, 'Version', ['user']);The source reads the fields the tiles draw. The status badge needs the type’s status field in that
list; the metadata line needs whatever subLabelField and secondaryField name.
| prop | type | default | meaning |
|---|---|---|---|
context | SgContext | required | The widget context. Every tile reads its schema, its statuses and its links through it. |
thumbnail | string | false | 'image' | Field holding the thumbnail URL. false leaves every tile on the placeholder. |
labelField | string | null | null | Field shown as the tile's name. Defaults to the type's display name. |
subLabelField | string | CollectionColumn | null | null | The left of the metadata line. A resolved column renders it by type. |
subLabel | (row) => string | — | The caller's own sub-label. Wins over subLabelField. |
secondaryField | string | CollectionColumn | null | null | The right of the metadata line. |
secondary | (row) => string | — | The caller's own text on the right. Wins over secondaryField. |
showCode | boolean | false | Show the row's code beside the name when the two differ. |
statuses | Record<string, StatusRecord> | null | null | Status rows by code. Read through the context when not given. |
size | 'sm' | 'md' | 'lg' | 'md' | Tile size and column minimum: 160, 224 or 288 pixels. |
selectable | boolean | false | Draws a checkbox on each tile. |
pageSizes | number[] | [25, 50, 100] | Rows per page offered in the footer. pages only. |
maxHeight | string | '32rem' | Height of the scrolling body. |
virtualizeAfter | number | 100 | Rows above which the grid draws only the tiles on screen. |
emptyLabel | string | 'No rows' | Shown when the read returned nothing. |
errorLabel | string | — | Shown in place of what the failed read said. |
density | 'compact' | 'default' | 'default' | Compact halves the gap between tiles. |
class / className | string | — | Merged after the widget's own classes. |
source | EntitySource | required | The rows and the paging behind them. |
paging | 'pages' | 'more' | 'scroll' | 'scroll' | How the set is walked: a page number, a load-more row, or the scroller. |
sort | SortSpec[] | — | The source's sort, two-way, so a SortPicker drops into header. |
filters | FilterNode | WireGroup | null | — | The source's filter, two-way, so a FilterBar drops into header. |
selectionfrom CollectionControl | EntityRef[] | [] | The selected rows, two-way. Names loaded rows only. |
getRowIdfrom CollectionControl | (row) => string | — | How a row is keyed, in the DOM and in the selection. Default Type:id. |
isRowDisabled | (row) => boolean | — | True for a row the arrows skip and the selection refuses. |
loadingLabelfrom CollectionControl | string | 'Loading…' | Names the skeletons a read stands behind, for a screen reader. |
Nothing is on the metadata line by default, and the row’s id is never on it.
Events
Section titled “Events”| event | payload | when |
|---|---|---|
onSelect | EntityRow | A tile was activated, by click or by Enter. |
onSortChange | SortSpec[] | The source's sort changed. |
onFiltersChange | FilterNode | WireGroup | null | The source's filter changed. |
onSelectionChange | EntityRef[] | The selection changed. |
| slot | receives | draws |
|---|---|---|
card | { row, id, index, selected, disabled, active } | Draws one grid cell. The grid keeps the cell's box, its tab stop and its keys. |
header | — | Region above the grid. |
footer | — | Region below the footer. |
Keyboard
Section titled “Keyboard”The grid is one tab stop: Tab moves into the tile the cursor is on, and the arrows walk from there.
| key | does |
|---|---|
| Tab | Moves into the grid, then out of it. |
| Left, Right | Moves the cursor one tile. |
| Up, Down | Moves the cursor one row of tiles. |
| Home, End | Moves to the first or the last tile. |
| Right, Down past the last tile | In more and scroll, asks for the next page. The cursor stays where it is until the tiles arrive, then lands on the first of them. |
| Space | Selects the tile, when the grid is selectable. |
| Enter | Emits the select event. |
| Enter in the page number | Goes to that page. |
| Tabfrom CollectionControl | Reaches the collection. It is one tab stop, and the cursor holds the focus inside it. |
| ↑ ↓from CollectionControl | Moves the cursor over the rows it can land on, never a disabled one, and brings it into view. |
| ↓ on the last loaded rowfrom CollectionControl | In more and scroll, asks for the next page. The cursor waits where it is and lands on the first of the new rows. |
A disabled row takes no keys and no click: the arrows step over it and the cursor never lands on it.
Anatomy
Section titled “Anatomy”The tile follows the thumbnail view of the Flow PT web app and the asset grid of Frame.io: the picture first and at one aspect, the state on the picture rather than under it, one line of name and one of metadata, and the controls only while the pointer or the focus is on the tile.
API behaviour
Section titled “API behaviour”The value of an image field is a presigned URL re-signed on every read, and it is the only state marker there is: null means the row never had a thumbnail, and a URL under the transient status path means one is still transcoding. All three states render (field_types/image). A Version tile carries a play overlay.
Paging stops on a short page: links.next is emitted on every page forever, including empty ones, and
no total is in a read (006_pagination). The “of N” in the footer comes from one _summarize call
counting id; a site that does not answer that key leaves the range without a total (020_summarize).