Records gallery
Reach for this when records are primarily images and the grid of tiles IS the collection. Rendered here with no image value, which is the real fallback state rather than a broken one. A verb on this screen has three homes and none of them is the bar: the primary one is the Fab (fab), view verbs like Export are the view-options surface's header affordance, and a record's own actions belong to the pushed record surface — see Detail's hosting knob.
Spec of record: design-system/components/data/Gallery.d.ts
Live
Real widgets, built from the real kit. Turn a knob and it rebuilds; the theme bar above applies the design system's own tokens — including anything you changed under Customize — and the device selector uses the same sizes the golden baselines gate on, so what you see here is what CI holds the widget to.
Press Focus on any preview to take it to the full window — at a desktop width that renders most devices at 1:1, which is when the widget's own controls become clickable. The bar always states the device and the scale, so a shrunk preview says it is shrunk. Esc leaves.
Tile edge in logical pixels, clamped to 96–320.
Renders each tile's badge.
Shows when you tap a tile — selection paints on the ones you pick, and starts empty — press Focus to make the preview interactive.
Which nav the compact posture mounts — the drawer behind the hamburger, or a bottom bar.
The primary tier, and on a records destination the ONLY loud action the bar's neighborhood has. The scaffold renders it in the thumb zone at every window class; a screen never places it itself.
Actions in the drawer's own footer. The drawer carries them at EVERY window class — at 960 and wider it exists only because they do, since the expanded shell otherwise mounts a standing inline rail instead.
Shows when you open the drawer — the hamburger below 960, an edge swipe at 960 and wider, where the rail head's menu button toggles the rail instead. The actions sit in the drawer's own footer; the resting frame is identical either way. — press Focus to make the preview interactive.
The app's identity mark, and nothing else — it never opens a picker. At 960 and wider it sits beside the menu button in the rail's head at the same place in both postures, overhanging the collapsed rail so the bar beside it steps in. Below 960 the drawer's header band states it.
The scope — one value of the app's own kind (here an Area) — as the FIRST item of the bar's subtitle, ahead of the meta. With a picker it is a quiet text button with a chevron; on a phone the whole title block opens the picker as a bottom sheet. A pushed record's bar shows it read-only.
Global commands with no page ("Send feedback", "Help") pinned in the rail's footer under the footer destinations — the same row as a destination, but never lit and never counted in the destination index.
An organization's or partner's logo at the very bottom of the rail and the drawer — identity, never a control. Collapsed it shows its compact square, or nothing when the host gives none.
The Group and Columns panels beside the built-in Filter. `RecordsChrome.panels` is host-contributed and view-agnostic — the same cluster a table gets. OFF leaves Filter standing alone, which is a real posture: the tab row is absent at a single available control, because one control needs no tabs.
Shows when you open the view-options pane (tap Filter's control) — panels are TABS of that one pane, so the resting bar looks identical either way and only the pane's content differs — press Focus to make the preview interactive.
Dart for these values
screen_records_gallery() What you can do in it
Capability the preview already carries that no knob can express — you reach it by using the widget. Worth knowing before concluding something is missing: most of this is only clickable once the preview is at Focus size.
- **Search** — type in the field; the table narrows live. Collapsing search never clears the query, which survives as a removable
search:…chip. - **Filter** — the funnel opens the view-options pane. Facets are type-appropriate: enums and text offer distinct values, numbers a min/max, dates a From/To.
- **Chips** — every applied filter renders as a removable chip, and Clear all removes the lot.
- **Narrowed to zero** states the narrowing and offers Clear, rather than the author-a-record empty state.
What it is made of
The widgets this screen assembles, with their full API below. They used to live on a second entry of their own; one thing you choose gets one page.
AppGallery widget
The design-system Gallery — a responsive grid of image tiles, generic over the row type T. Any data, zero domain knowledge: every tile value comes from a typed accessor closure over T — imageValue (the stored image reference, fed straight to AppImage's resolution ladder), an optional caption…
Full dartdoc
The design-system Gallery — a responsive grid of image tiles, generic over the row type T. Any data, zero domain knowledge: every tile value comes from a typed accessor closure over T — imageValue (the stored image reference, fed straight to AppImage's resolution ladder), an optional caption (DesignTokens.dataS — identity text, not free prose), and an optional badge (a short count/status word rendered in the corner). There is no metadata blob and no string-keyed lookup anywhere in this file. Anatomy per tile: a hairline-framed (DesignTokens.hairS) thumb at radius DesignTokens.rS over an DesignTokens.ink4 void, a glyph fallback (KitGlyph.photo) when imageValue resolves to no image or fails to load, an optional caption line below the thumb, and an optional corner badge — mirrors the constitution's image-cell rule ("hairline-framed thumbs with glyph fallback, identity only") at gallery scale. Selection grammar (constitution, shared with `AppListItem` / `AppDataTable`'s row grammar): a selected tile paints a 2px DesignTokens.edge accent edge over a DesignTokens.rowSel tint wash — drawn as a frame around the whole tile (image + caption), since a gallery tile (unlike a row) has no start-edge to claim alone. State contract on every tile: enabled · hover (DesignTokens.ink4 wash, pointer enhancement only, suppressed while selected) · focus-visible (1px solid accent, zero offset) · pressed (1px translateY) · selected (accent edge + tint) · disabled (onTap null → not tappable, no hover/focus/press paint). Tap targets are at least DesignTokens.touch (44 logical px). No ripple. Selection: selectable + the controlled selected set + onSelectionChanged follow the same `initial*`-free controlled-set idiom as `AppDataTable`. A tile's onTap fires unconditionally (identity is verbatim T equality — T must have value/identity equality suitable for a `Set`); when selectable is true the tap also toggles the tile's membership by calling onSelectionChanged with the next set. Empty: when items is empty, renders an AppEmptyState built from emptyLabel + emptyActionLabel / onEmptyAction instead of a bare grid. Archetypes: the default constructor is the **browse grid** — a vertical `GridView` that fills the space it is given. AppGallery.rail is the **filmstrip** — a horizontally-scrolling, virtualized `ListView` of the same tiles at a fixed tileExtent, sized to exactly one tile tall so it drops into a bounded slot (a detail section, a header strip) without the host computing a height. One tile primitive, two thin layout policies: a tile reads, behaves, and sounds identical in either. Sizing: tileExtent is the target square edge for SliverGridDelegateWithMaxCrossAxisExtent.maxCrossAxisExtent — the grid packs as many columns as fit at or under that extent, so it is inherently responsive across the M3 window classes without a breakpoint switch (adaptation strategy: **scale**, density only — the same anatomy at every class). On the rail the same value is the tile's *exact* width, and the rail's height falls out of it, so the two archetypes share one geometry knob. ```dart AppGallery<Photo>( items: photos, imageValue: (p) => p.storedRef, caption: (p) => p.label, badge: (p) => p.pinCount > 0 ? '${p.pinCount}' : null, selectable: true, selected: selection, onSelectionChanged: (s) => setState(() => selection = s), onTap: (p) => openViewer(p), emptyLabel: 'No photos on this record.', ) ```
AppGallerydefault constructor
| Knob | Type | Default | What it does |
|---|---|---|---|
| badge | String? Function(T item)? | null | Typed accessor for a short corner badge word (a count or status word — "encode once," never a re-encoding of the caption). Returning null for a given row renders no badge on that tile; the accessor itself may also be null to disable badges entirely. |
| caption | String Function(T item)? | null | Typed accessor for the tile's identity caption (DesignTokens.dataS — machine/identity text, e.g. a filename or page label, not free prose). Null renders no caption line. |
| emptyActionLabel | String? | null | Empty-state single action label. Renders only when onEmptyAction is also non-null. |
| emptyActionNavigates | bool | false | THE ACTION THAT ONLY OPENS A PAGE (`ActionStrip.d.ts`, #1749): the empty face's action pushes another screen and writes nothing, so it reports a link to assistive technology. Declared, never inferred from onEmptyAction. Default false. |
| emptyLabel | String | 'No images.' | Empty-state title — what the view would show. Rendered via AppEmptyState when items is empty. |
| imageValue * | String Function(T item) | — | Typed accessor — the stored image reference for a row, fed verbatim to AppImage's resolution ladder (`staged://` / `asset:` / `http(s)://` / origin-relative / blank). A closure over the typed row — never a map lookup. |
| items * | List<T> | — | The rows to render. Empty renders the AppEmptyState. |
| onEmptyAction | VoidCallback? | null | Empty-state single action callback. Renders only when emptyActionLabel is also non-null. |
| onSelectionChanged | ValueChanged<Set<T>>? | null | Fires with the next selection set when a tile is tapped while selectable is true. Null renders read-only selection highlighting (a tile in selected still shows the accent edge, but tapping does not mutate it). |
| onTap | void Function(T item)? | null | Fires with the tapped row. When selectable is true this also drives the selection toggle (see onSelectionChanged); when false it is the sole tap channel (e.g. opening a viewer). Null renders non-interactive tiles (no hover/focus/press paint, no Semantics tap action). |
| selectable | bool | false | When true, tapping a tile also toggles its membership in selected via onSelectionChanged, and a selected tile paints the accent edge + tint. When false, selected and onSelectionChanged are ignored. |
| selected | Set<T> | const {} | The controlled selection set. Ignored when selectable is false. |
| tileExtent | double | _defaultTileExtent | The target tile edge in logical pixels (SliverGridDelegateWithMaxCrossAxisExtent.maxCrossAxisExtent) — the grid packs as many columns as fit at or under this extent. On AppGallery.rail it is the tile's exact width, and the rail's height is derived from it. Defaults to _defaultTileExtent (112). |
AppGallery.rail
| Knob | Type | Default | What it does |
|---|---|---|---|
| badge | String? Function(T item)? | null | Typed accessor for a short corner badge word (a count or status word — "encode once," never a re-encoding of the caption). Returning null for a given row renders no badge on that tile; the accessor itself may also be null to disable badges entirely. |
| caption | String Function(T item)? | null | Typed accessor for the tile's identity caption (DesignTokens.dataS — machine/identity text, e.g. a filename or page label, not free prose). Null renders no caption line. |
| emptyActionLabel | String? | null | Empty-state single action label. Renders only when onEmptyAction is also non-null. |
| emptyActionNavigates | bool | false | THE ACTION THAT ONLY OPENS A PAGE (`ActionStrip.d.ts`, #1749): the empty face's action pushes another screen and writes nothing, so it reports a link to assistive technology. Declared, never inferred from onEmptyAction. Default false. |
| emptyLabel | String | 'No images.' | Empty-state title — what the view would show. Rendered via AppEmptyState when items is empty. |
| imageValue * | String Function(T item) | — | Typed accessor — the stored image reference for a row, fed verbatim to AppImage's resolution ladder (`staged://` / `asset:` / `http(s)://` / origin-relative / blank). A closure over the typed row — never a map lookup. |
| items * | List<T> | — | The rows to render. Empty renders the AppEmptyState. |
| onEmptyAction | VoidCallback? | null | Empty-state single action callback. Renders only when emptyActionLabel is also non-null. |
| onSelectionChanged | ValueChanged<Set<T>>? | null | Fires with the next selection set when a tile is tapped while selectable is true. Null renders read-only selection highlighting (a tile in selected still shows the accent edge, but tapping does not mutate it). |
| onTap | void Function(T item)? | null | Fires with the tapped row. When selectable is true this also drives the selection toggle (see onSelectionChanged); when false it is the sole tap channel (e.g. opening a viewer). Null renders non-interactive tiles (no hover/focus/press paint, no Semantics tap action). |
| selectable | bool | false | When true, tapping a tile also toggles its membership in selected via onSelectionChanged, and a selected tile paints the accent edge + tint. When false, selected and onSelectionChanged are ignored. |
| selected | Set<T> | const {} | The controlled selection set. Ignored when selectable is false. |
| tileExtent | double | _defaultTileExtent | The target tile edge in logical pixels (SliverGridDelegateWithMaxCrossAxisExtent.maxCrossAxisExtent) — the grid packs as many columns as fit at or under this extent. On AppGallery.rail it is the tile's exact width, and the rail's height is derived from it. Defaults to _defaultTileExtent (112). |
Where it lives
| Class | File |
|---|---|
| AppGallery | packages/app_widgets/lib/src/kit/data/gallery.dart |