Theme
Customize the design system

Every control starts at the preset. Change one and every preview on the site follows, on this page and the next, until you reset it.

Contrast

Text against what it sits on. Below 4.5:1 fails WCAG AA for body text — the check a reviewer runs on any surface that takes a tone.

Waiting for the first preview to report its palette.

Corners

Sets all three radii at once. The individual values are under Geometry.

Brand 5

The accent seam an app actually brands itself through. Change these and you have made the decision a real `DesignTokens.graphite(...)` call makes.

Geometry 15

Square or rounded, and how heavy every line is. The radius row above the controls sets all three corners at once, because "what does square feel like" is one decision rather than three numbers.

Surfaces 17

Everything the app is painted ON — the ground behind the content (the frame paints it too), the cards over it, the well inset in a card, and the washes that show state.

Bands 10

The rail's head and the view bar beside it (one band across the top at 960 and wider), a pushed screen's bar, the compact bar and the compact nav bar — the band's fill, its rule, its hover and active faces, the resting field box on it, and the ink and accent control that sit on it. The rail's tree and its footer stay on the ground.

Text and controls 11

The ink ramp, the control outlines and the field's focus halo. Every pair the panel can break is watched by a contrast badge above.

Status 7

The three voices that mean something — danger, caution and ok — each as a foreground and a surface wash, danger with a border as well.

Data 5

The default pin head, its glyph and its halo, and the group bands. A pin's category color comes from the palette, named in the theme seed.

Map 11

The basemap's roles — land, water, parks, roads and their casings, buildings, labels — and the pending pin's gray. A raster basemap keeps its own imagery; these paint the map's plate and any kit-drawn map.

Scrollbars 6

Every scroller's bar: the thumb at rest and while hovered or dragged, the strip it rides, and the desk bar's thickness, side gap and corners. The desk half shows only while a fine pointer is attached; at touch the platform's own overlay keeps its size.

Advanced 6

Real controls that are not decisions — motion durations, the focus ring offset, the touch floor. Opens collapsed on purpose.

Take it with you

The Dart that reproduces what you are looking at. It is a real constructor call, because the configurator edits a real preset — what you copy is what an app can paste.

Screens

Thread

Reach for this for a feed or an open conversation — posts, replies, system events and the composer, as one grammar. The reply affordance is the surface's one loud action.

Spec of record: design-system/components/data/Thread.d.ts

11
Knobs

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.

screen_thread
Knobs
Theme

Blank renders no status. "Blocked" takes the danger treatment.

Assembly

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.

Dart for these values
screen_thread()

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.

ThreadScreen widget

The FLOORPLAN for conversational records (`design-system/components/floorplans/ThreadScreen.d.ts`): a feed of post blocks with their replies, a composer, and the events and rules between them. What it always owns: * **The bar, on every state, empty included.** In a destinations-mode AppScaffold the…

Full dartdoc

The FLOORPLAN for conversational records (`design-system/components/floorplans/ThreadScreen.d.ts`): a feed of post blocks with their replies, a composer, and the events and rules between them. What it always owns: * **The bar, on every state, empty included.** In a destinations-mode AppScaffold the chrome projects into the shell's bar through the contributor tier (AppHeaderActions, via RecordsChrome) — the resting search glyph below 960, the resting field at ≥960, search MODE in between; the shell owns the title, the scope and sync. Pushed (no bar host) the floorplan stands up its OWN bar, an AppHeaderBar carrying title in a body-mode AppScaffold, so the contributed chrome still lands in a bar rather than resting a toolbar in the body. * **The composer's placement** (composer) — the pinned bar at the block-end edge, or the resting strip at the head of the feed opening the focused compose surface. Never both; a new entry is never drafted inline in the feed. A send that FAILS keeps its draft and says so. * **The faces** — error (error + onRetry), loading (skeleton blocks — loading, ORed with a null items), empty (empty) and narrowed-to-zero, each in the body UNDER the chrome with the composer still present, and in the contract's RANK: ERROR > LOADING > EMPTY > NARROWED-TO-ZERO > the feed. EMPTY is decided by the DATA (a zero-length feed — nothing to narrow) and narrowed-to-zero by the NARROWING. * **The zone grammar** — paper blocks on the `sur` canvas, the 720 reading measure (kAppThreadMeasure) centered. * **The scroll** — the feed scrolls; the composer bar and the chrome do not. ```dart ThreadScreen<PostRow>( title: 'Discussion', items: posts, // null = loading · [] = empty entry: (p) => ThreadEntry(key: p.id, author: nameOf(p), text: p.comment, timestamp: p.at), empty: const ThreadEmpty(title: 'No posts yet'), composer: ThreadComposer.strip(onPost: post, onReply: reply), cardMenu: (p) => postMenu(p), syncStatus: const SyncStatusButton(), // required; null = no channel ) ``` **Every prop here is the contract's own** since pin e04afca: entry — the item adapter both kits minted independently and the contract adopted verbatim — plus syncStatus, loading, error and onRetry.

ThreadScreendefault constructor

Knob Type Default What it does
band FloorplanBand? null The surface's ONE TRANSIENT BAND (`ThreadScreen.d.ts`, THE SCREEN CARRIES THE TRANSIENT BAND, 2026-10-03) — a confirmation that clears itself ("Post flagged") or a refusal the surface must not swallow ("Could not resolve the post"), never a standing notice (that is banner). `Scaffold.d.ts`'s own…
banner Widget? null An operational Banner at the head of the feed — the ORPHAN NOTICE ("the record this discussion belongs to was deleted"), a stale-data line. One at a time. IT SURVIVES EVERY FACE (`RecordsScreen.d.ts`, THE BANNER'S RANK AGAINST THE FACES, cited and not restated) — and that matters more here than…
cardMenu Widget? Function(T item)? null The ⋯ menu for one post block.
columns List<RecordsColumn<T>>? null THE NARROWING PROJECTION of an item — the same RecordsColumn shape `RecordsScreen` takes, deliberately, because a thread IS a records view and a second column vocabulary for one job is two rules to keep in step (`ThreadScreen.d.ts`, 2026-09-21). NULL IS TODAY'S BEHAVIOR: search sweeps the projected…
composer * ThreadComposer<T> — THE COMPOSER — where it sits and whose it is (ThreadComposer). REQUIRED, no default (#1639): the old default `bar` with no `onPost` drew a live Post that cleared the draft and sent nothing.
conversationItems List<T> Function(T root)? null EVERY ITEM IN ONE ROOT'S CONVERSATION — the root and its replies, in the host's own type — read ONLY by the columns narrowing. It exists because this floorplan holds ROOTS: entry projects a root into a ThreadEntry whose replies are ThreadReplyEntry PROJECTIONS, which carry rendered text and no typed…
empty * ThreadEmpty — The empty face's copy.
entry * ThreadEntry Function(T item) — Projects one item into the kit's ThreadEntry shape. REQUIRED: an item the floorplan cannot project is an item it cannot draw.
error FloorplanError? null The load failed: `title` is the cause, `detail` one quiet line under it. Non-null outranks every other face; null means no error. THE COMPOSER STAYS PRESENT on this face exactly as on the empty one — a feed that could not load is still a place someone may post.
filters List<String> const [] The filterable columns keys, in facet order; each becomes a facet in the Filter tab. Empty — or columns null — and the Filter tab never renders (THE EARNED AFFORDANCE, `RecordsScreen`'s own rule and its own two names). A column that is not a facet is still SEARCHED.
furniture Widget? Function(T item, int index)? null The furniture rendered in the gap AFTER `item` — a AppThreadEvent line, an AppThreadRule, a link. Null renders nothing.
hero ThreadHero? null The ConversationHeader in its RECORD-BANNER posture, stated ONCE at the top of the screen — the scope a drilled-into thread belongs to. Null renders no banner. IGNORED, and reported, when NESTED: the hosting screen already states the scope.
items * List<T>? — The root entries, in the APP's own type. Null = still loading (the shorthand for loading, ORed with it); empty = loaded and empty.
loading bool false The canonical loading signal (THE LOAD FACES). True renders the skeleton blocks whatever items holds. Default false.
onBack VoidCallback? null The leading back affordance of this floorplan's OWN bar — the pushed posture. Null renders NONE. Ignored as a scaffold destination (the shell owns that leading) and when nested (no bar).
onRetry VoidCallback? null The error face's Retry. Null renders the cause with no way out.
order ThreadOrder
newest · oldest
ThreadOrder.newest Root order. Default ThreadOrder.newest.
orphans ThreadOrphanZone? null Orphan replies — items whose parent chain is broken — rendered under ONE banner AFTER the feed's own roots and zones, through the SAME reply-row renderer as an ordinary conversation (THE ORPHAN ZONE). Null or an empty ThreadOrphanZone.items renders nothing. NOT banner: that is the head-of-feed…
presence Widget? null THE PRESENCE STACK (`ThreadScreen.d.ts` `presence`; `Presence.d.ts` EVERY FLOORPLAN CARRIES IT AS A KNOB, steady-core #1331): the `AppPresenceStack` of the scope this thread joins. The HOST builds it; the screen never joins a channel. As a scaffold DESTINATION it is CONTRIBUTED into the shell's bar…
recordNoun String 'entry' The singular noun the kit's OWN copy uses for this thread's rows (`ThreadScreenKnobs.recordNoun`, THE KIT'S COPY SPEAKS THE HOST'S NOUN, 2026-10-03). Default `'entry'`. `RecordsScreen.recordNoun`'s knob, in the same shape and with the same default-bearing behavior: ONE singular noun, and the kit…
replies ThreadReplies
inline · collapsed
ThreadReplies.collapsed Whether reply runs rest open. Default ThreadReplies.collapsed.
searchable bool true Whether the bar carries search. Default true; false removes the search affordance at every window class.
syncStatus * Widget? — The shell's sync control, last inline-end in the bar — the scaffold's own when this screen is a destination, this screen's own bar when it is pushed. REQUIRED BUT NULLABLE: null states that the surface has no sync channel and renders no control; omitting it is a compile error, so no screen loses the…
title * String — The view's name, in the headline role — the bar's title in the pushed posture (a destination's title belongs to the shell).
zones List<ThreadScreenZone> const [] Collapsible zones of SECONDARY thread content rendered AFTER the feed's own roots, in array order ("More posts on this record (2)"). They are the HOST's own content rather than a projection of items, so a face about `items` says nothing about them and they render on every one — which is the…

AppThreadCard widget

The design-system ThreadCard — one feed post block.

Full dartdoc

The design-system ThreadCard — one feed post block. ```dart AppThreadCard( author: 'R. Barnes', meta: 'Supervisor · 08-10 08:12 · edited', footerChips: const [AppMetaChip(label: 'Issue')], replyCount: 2, onReply: reply, onOpen: openConversation, body: const [AppThreadSegment('Forklift left in aisle 4 again.')], ) ```

AppThreadCarddefault constructor

Knob Type Default What it does
actions List<Widget> const [] Actions row — data-font `AppThreadLink`s. Hosted in a Wrap, so links wrap WHOLE under pressure rather than ellipsize; flex children (Spacer, Expanded) are unsupported and throw at render.
author * String — Author display name — also the default AppAvatar source.
avatar Widget? null Overrides the leading identity block (defaults to an AppAvatar of author at the feed's 32 step).
body * List<AppThreadSegment> — Body text as segment runs; mention runs render 500 accent.
chips List<Widget> const [] The header chip cluster (`AppMetaChip`s, an ok `AppStatusPill`) — trailing in the header at ≥600, its own row under the header below. Hosted in an AppChipRow (D5): pass in IMPORTANCE order — collapse takes from the end, and the `+N` pill's tap opens the card (onOpen).
danger bool false Flagged: danger border-block + the 2px danger start-edge overlay.
files List<Widget> const [] File chip row (file `AppMetaChip`s).
footerChips List<Widget> const [] Leading footer chips (type · ref), hosted in an AppChipRow behind the footer's block-start hairline. Pass in IMPORTANCE order; overflow collapses into `+N`, whose tap opens the card (onOpen).
media Widget? null Media region — full-bleed, 280 high (170 below 600).
menu Widget? null Trailing ⋯ control (an `AppIconButton` menu anchor).
meta String? null Sub-line, data font ink40 — `Role · 08-10 08:12 · edited`.
note String? null Danger mono note line (e.g. the removal reason).
onFooterChipsOverflowTap VoidCallback? null Overrides the footer chip row's `+N` tap (`Thread.d.ts` chip-row overflow: "the host's call") — a host that reveals the hidden chips in its own surface passes the opener here. Null keeps the default: the pill opens the card (onOpen). Header chips keep onOpen.
onLongPress VoidCallback? null Long-press on a quiet region — the actions-door accelerator, the same grammar the message row carries (`Thread.d.ts` `onLongPress`). With onOpen it rides the open target's own gesture arena; alone it wraps a translucent recognizer so inner affordances still win. Null leaves the resting tree…
onMediaTap VoidCallback? null Tap on the media region — opens the host's fullscreen viewer (`Thread.d.ts` `ThreadCardProps.onMediaTap`, #1120), the same grammar `AppMessageRow.onMediaTap` carries on a reply block. Wired, the region is ONE "View image" control whose whole box is the target, and it wins the hit test over the…
onOpen VoidCallback? null Opens the entry's own surface (`Thread.d.ts` `onOpen`) — the whole card becomes a quiet open affordance: inner interactive elements (links, ref/file chips, the ⋯ menu) win the hit test, and taps on the card's quiet regions open. State contract applies (focus-visible ring, pressed 1px translateY).…
onReply VoidCallback? null The footer's Reply affordance (an AppThreadLink the card renders). Null renders no link — a read-only surface keeps the count alone.
onToggleReplies VoidCallback? null Makes the reply count the reply-run DISCLOSURE (`Thread.d.ts` 2026-08-16): the count becomes a quiet tap target carrying a trailing ▸ (▾ when repliesExpanded) that expands/collapses the run IN PLACE — tapping it never navigates, and onOpen never fires from it (the count is its own hit target inside…
repliesExpanded bool false Whether the conversation's reply run (host-rendered, under the card) is open — the disclosure state the count states with its ▸/▾ chevron. Host-owned (controlled); meaningless without onToggleReplies.
replyCount int? null Reply count, rendered as ONE unbreakable data-role unit (`2 replies`) in the footer's trailing cluster — never squeezed, never wrapped (D5: counts are single unbreakable units). Null renders no count; hosts show `0 replies` rather than omitting it (the spec's voice note: reply counts are ALWAYS…
statusDanger bool false Renders statusWord in danger ink.
statusOk bool false Renders statusWord in the ok green.
statusWord String? null Trailing sub-line status word: `✓ RESOLVED` / `REMOVE`.
tagged List<Widget> const [] TAGGED chip row content (tag `AppMetaChip`s behind the Tagged label).

AppMessageRow widget

The design-system MessageRow — one message in an open conversation, a bounded block (`Thread.d.ts` `MessageRowProps`). The host renders a conversation as a flat time-ordered column of these with 8px gaps (an AppReplyRun provides the gaps for the reply run).

Full dartdoc

The design-system MessageRow — one message in an open conversation, a bounded block (`Thread.d.ts` `MessageRowProps`). The host renders a conversation as a flat time-ordered column of these with 8px gaps (an AppReplyRun provides the gaps for the reply run). Actions ride the ALWAYS-VISIBLE menu ⋯ on the header line — the actions door — with onLongPress as an accelerator opening the same sheet (exposed to assistive tech through a labeled custom action). A gesture is never the only path to an action: the 2026-08-15 hover-revealed trailing control was undiscoverable to non-technical users, and replying to a reply had no visible entry at all. ```dart AppMessageRow( author: 'R. Barnes', time: '09:41', quote: const AppMessageQuote(author: 'Carla', excerpt: 'Tomorrow…'), onQuoteTap: scrollToQuoted, onReply: targetReply, menu: AppIconButton(glyph: KitGlyph.dots, onPressed: openActionsSheet), onLongPress: openActionsSheet, body: const [AppThreadSegment('Agreed, moving it up.')], ) ```

AppMessageRowdefault constructor

Knob Type Default What it does
actionsLabel String 'Message actions' Accessible name for the actions door (onLongPress), surfaced as a labeled custom semantics action — it appears BY NAME in TalkBack's actions menu and VoiceOver's rotor. Defaulted and host-overridable so a localizing host has a real seam.
author * String — Author display name — also the default AppAvatar source.
avatar Widget? null Overrides the leading identity block (defaults to an AppAvatar of author at the block's 24 step).
body * List<AppThreadSegment> — The message text as segment runs; mention runs render 500 accent.
danger bool false Flagged: the 2px danger start-edge.
files List<Widget> const [] File attachments — `AppFileAttachmentCard`s, stacked full-width under the body. Every card stays visible and tappable: an attachment's only open affordance is its own card.
media Widget? null Media region — rendered aspect-fit inside the block's max media height, never cropped. Supply a widget that sizes to its intrinsic aspect (e.g. `AppImage` with a contain fit).
menu Widget? null The always-visible trailing ⋯ on the header line — the actions door (an `AppIconButton` menu anchor). Null renders a read-only block.
onLongPress VoidCallback? null Long-press on the block — an ACCELERATOR opening the same actions sheet the menu ⋯ opens; also exposed as a labeled semantics action.
onMediaTap VoidCallback? null Tap on the media region — the host opens its fullscreen viewer. The image is the affordance; null renders the media inert.
onQuoteTap VoidCallback? null Tap on the quote block — the host scrolls to the quoted message. Null renders the quote inert.
onReply VoidCallback? null Targets this message for a reply — rendered as an always-visible quiet Reply link on the header line. Null renders none (a surface with no reply affordance at all, e.g. a read-only posture). This is NOT the actions door (menu): it is the one action a reader reaches for often enough to earn its own…
quote AppMessageQuote? null The quoted-message reference rendered as the quote block above the body. The slot stands DesignTokens.touch tall — the drawn line stays dense; the box is the tap target.
time String? null Header time, data font ink40 — `09:41`.

AppReplyRun widget

The replies to one post, bound by a single inline-start rule (`Thread.d.ts` `ReplyRun`). **One level, never a ladder.** The retired `AppThreadReplies` nested a level per depth and produced the staircase this family deleted; this binds the WHOLE reply run at ONE inset regardless of how deep any…

Full dartdoc

The replies to one post, bound by a single inline-start rule (`Thread.d.ts` `ReplyRun`). **One level, never a ladder.** The retired `AppThreadReplies` nested a level per depth and produced the staircase this family deleted; this binds the WHOLE reply run at ONE inset regardless of how deep any reply's parent chain runs. Flat rows alone left a conversation reading as N identical blocks with nothing saying which post it is *about* — the rule is what makes "this is the post, these are the replies" legible without reintroducing depth. **The run states itself in words.** label is a quiet head above the rule, and a host that has a count always passes one. The rule and the inset are a *drawn* claim about structure, and a drawn claim is the weakest thing on a screen someone is meeting for the first time — unlabeled, the opening message and its replies are typographically identical, and the only thing separating "the post" from "the chatter" is a 2px line and 20px of inset. Deliberately not solved by inflating the root message's type: the constitution puts emphasis in weight, ink and position, never in size. **The head can be the toggle.** With onToggle set the label becomes the expand/collapse tap target (state contract, DesignTokens.touch floor, a trailing ▸/▾ data-font chevron); collapsed renders the head ALONE — the rule and children are hidden. A run that can hide its replies must say how many it is hiding, so collapse requires label. ```dart AppReplyRun( label: '5 replies', collapsed: closed, onToggle: toggle, children: [for (final r in replies) AppMessageRow(...)], ) ```

AppReplyRundefault constructor

Knob Type Default What it does
children * List<Widget> — The reply rows (and any interposed event lines or unread rule), in render order.
collapsed bool false Renders the head only — rule and children hidden. Requires label and is only reachable when onToggle can bring them back.
label String? null The run's head — the reply count as machine text (`5 replies`). Null renders the bare rule, for a run whose size the host's own chrome already states.
onToggle VoidCallback? null Makes the head the expand/collapse tap target. Null keeps the static head (the pre-collapse posture, unchanged).

AppThreadEvent widget

The system-event line item — a status change reads as timeline furniture, never a card (`Thread.d.ts` `ThreadEvent`). **An event never truncates** — the text wraps. An event line exists to state what happened, and "Marked this post as r…" states nothing (observed on device 2026-08-16). **An event is…

Full dartdoc

The system-event line item — a status change reads as timeline furniture, never a card (`Thread.d.ts` `ThreadEvent`). **An event never truncates** — the text wraps. An event line exists to state what happened, and "Marked this post as r…" states nothing (observed on device 2026-08-16). **An event is not a message**: a host never counts one in a reply count and never renders one inside a reply run as a reply — the count and the rows agree by excluding events from both. Pass pill only when the surface's own chrome does NOT already state the status; an event line under a header wearing ✓ RESOLVED duplicates the encoding. ```dart AppThreadEvent(text: 'Adeyemi marked this resolved', time: '09:41') ```

AppThreadEventdefault constructor

Knob Type Default What it does
glyph String '✓' The 22px accent glyph column's character.
pill Widget? null Optional trailing annotation (an ok `AppStatusPill`).
text * String — Data-font event text — `Adeyemi set status`.
time String? null Trailing time, data font ink40.

AppThreadRule widget

The unread rule — accent hairlines flanking the NEW label; sits before the first unseen item (`Thread.d.ts` `ThreadRule`). The host computes unread position (per-user last-read); this is only the mark.

AppThreadRuledefault constructor

Knob Type Default What it does
label String 'New' The rule label, uppercased.

AppThreadZone widget

The design-system ThreadZone — a full-width collapsible zone of SECONDARY thread content (`Thread.d.ts` `ThreadZoneProps`): the drill-through's "More posts on this record (N)". Deliberately NOT an AppReplyRun: the run's rule + inset are a drawn claim of a reply relationship, and this zone's children…

Full dartdoc

The design-system ThreadZone — a full-width collapsible zone of SECONDARY thread content (`Thread.d.ts` `ThreadZoneProps`): the drill-through's "More posts on this record (N)". Deliberately NOT an AppReplyRun: the run's rule + inset are a drawn claim of a reply relationship, and this zone's children are full post blocks — no rule, no inset, just a quiet micro-label head with a leading ▸/▾ data-font chevron. With onToggle the head is the expand/collapse tap target (state contract, DesignTokens.touch floor); collapsed renders the head alone. ```dart AppThreadZone( label: 'More posts on this record (2)', collapsed: closed, onToggle: toggle, children: [for (final c in siblings) ...blocks], ) ```

AppThreadZonedefault constructor

Knob Type Default What it does
children * List<Widget> — The zone's blocks, in render order.
collapsed bool false Renders the head only.
label * String — The head line — `More posts on this record (2)`.
onToggle VoidCallback? null Makes the head the expand/collapse tap target. Null keeps a static head.

AppThreadComposerBar widget

The pinned composer bar — paper behind a block-start structural hairline (`Thread.d.ts` `ThreadComposerBar`).

Full dartdoc

The pinned composer bar — paper behind a block-start structural hairline (`Thread.d.ts` `ThreadComposerBar`). ```dart AppThreadComposerBar( avatar: const AppAvatar(name: 'You', hue: AppAvatarHue.mono), actions: [photoButton, tagButton, const Spacer(), postSlab], child: AppTextArea(label: null, minLines: 2, maxLines: 5, ...), ) ```

AppThreadComposerBardefault constructor

Knob Type Default What it does
actions List<Widget> const [] The actions row under the field.
avatar Widget? null Leading identity block (the signed-in identity, 28).
child * Widget — The host's field — an `AppTextArea` (2→5 rows), or a single-line field in the compact posture.

AppThreadComposeStrip widget

The feed's collapsed compose entry — avatar + a GHOST FIELD (a button wearing field anatomy) + one trailing control (`Thread.d.ts` `ThreadComposeStrip`).

AppThreadComposeStripdefault constructor

Knob Type Default What it does
avatar Widget? null Leading identity block (the signed-in identity, 32).
onOpen VoidCallback? null Fired when the ghost field is tapped.
placeholder * String — Ghost-field placeholder — `Post an update…`.
trailing Widget? null One trailing control (a photo `AppIconButton`).

AppConversationHeader widget

The design-system ConversationHeader — the scope stated once at the top of its screen (`Thread.d.ts` `ConversationHeaderProps`). chips render through an AppChipRow whose `+N` pill EXPANDS IN PLACE — the header is the surface where every chip is shown, so the tap reveals the full set as a plain wrap…

Full dartdoc

The design-system ConversationHeader — the scope stated once at the top of its screen (`Thread.d.ts` `ConversationHeaderProps`). chips render through an AppChipRow whose `+N` pill EXPANDS IN PLACE — the header is the surface where every chip is shown, so the tap reveals the full set as a plain wrap and the pill goes away. The expansion is one-way and ephemeral: there is no collapse-again control, and a remounted header starts collapsed. The record-banner posture (title + description + thumb + onOpen) makes the whole band a quiet open affordance to the record; the menu ⋯ and any tappable chips win the hit test over it. ```dart AppConversationHeader( title: 'Obs #72', description: 'Spill near dock 4 — north apron containment breached.', thumb: AppImage(...), onOpen: openObservation, chips: const [AppMetaChip(label: 'Issue')], menu: rootActionsMenu, ) ```

AppConversationHeaderdefault constructor

Knob Type Default What it does
chips List<Widget> const [] The chip cluster, hosted in an AppChipRow until expanded. Pass in IMPORTANCE order — status marks first — because the row collapses strictly from the end and never reorders.
description String? null The record's description, 2-line clamp (record-banner posture).
kind String? null The record type's display name — rendered as the accent micro-label above title ("Observation"). Any of kind, description, or a thumb slot makes the banner the HERO accent card; a bare title keeps the quiet channel band.
menu Widget? null Trailing ⋯ control carrying the ROOT post's actions (an `AppIconButton` menu anchor).
meta String? null Quiet inline-start meta text, data font ink40. NOT for a freestanding callout — an unattached conversation states its scope by absence.
onOpen VoidCallback? null Tap-through to the record — the whole banner opens. Inner interactive elements (chips, the ⋯) win the hit test.
showThumb bool false Renders the thumb frame even when thumb is null (glyph fallback) — a record scope whose record HAS an image slot states it either way.
thumb Widget? null The record's thumbnail, framed at the 44 step. Null with showThumb renders the glyph fallback frame (the tables image-cell grammar).
title String? null The scope's name — a record's default label (`Obs #72`) or the channel name. Present = the banner posture.

AppChipRow widget

The chip-zone contract (`Thread.d.ts` `ChipRow`): greedy row packing at a 6px gap, at most maxRows rows, tail overflow collapsed into a `+N` data-font outline pill (AppMetaChip in the file face) sized to the exact hidden count. A chip renders whole or not at all — never clipped mid-label; collapsed…

Full dartdoc

The chip-zone contract (`Thread.d.ts` `ChipRow`): greedy row packing at a 6px gap, at most maxRows rows, tail overflow collapsed into a `+N` data-font outline pill (AppMetaChip in the file face) sized to the exact hidden count. A chip renders whole or not at all — never clipped mid-label; collapsed chips are absent from paint, semantics, and hit testing. The pill packs like one more chip — inline, or opening a fresh row inside the budget; only when neither placement exists does it evict trailing chips (the count grows instead) — it is never dropped, and a chip that fits alongside it stays visible. Collapsed chips stay INFLATED by design — their state survives a resize, and any ticker or animation inside one keeps running; they are only withheld from paint, semantics, and hit testing. Tests therefore assert visibility with `.hitTestable()` or measured geometry, never a bare finder, which still matches a collapsed chip's widget. The row computes no intrinsic dimensions — packing (and the pill it mints) exists only in a real layout pass — so wrapping it in IntrinsicHeight or IntrinsicWidth throws in debug, exactly as LayoutBuilder does. Mount it in a normally bounded slot (a column, a card body) instead.

AppChipRowdefault constructor

Knob Type Default What it does
chips * List<Widget> — Priority order — collapse takes from the end.
maxRows int 2 The row budget (`ChipRowProps.maxRows`, default 2). Hosts that share a line with other content pass 1 — the feed block's header cluster and its footer chips both do, so a second chip row cannot stretch the band they sit in. A chip zone that owns its own row takes the default 2: the conversation…
onOverflowTap VoidCallback? null `+N` tap — cards open their surface, headers expand in place (the host's call). Null renders the pill inert.

AppMetaChip widget

The design-system Thread meta chip — a bordered micro-label in a card's metadata row. Inert by default; onSelect (for AppMetaChipVariant.ref and AppMetaChipVariant.file) makes it a real focusable target with the state contract, tap target expanded to DesignTokens.touch.

Full dartdoc

The design-system Thread meta chip — a bordered micro-label in a card's metadata row. Inert by default; onSelect (for AppMetaChipVariant.ref and AppMetaChipVariant.file) makes it a real focusable target with the state contract, tap target expanded to DesignTokens.touch. ```dart const AppMetaChip(label: 'Issue') const AppMetaChip(variant: AppMetaChipVariant.fill, glyph: '◈', label: 'Internal') AppMetaChip( variant: AppMetaChipVariant.ref, glyph: '⌖', label: 'OBS-0142', detail: 'Spill near dock', onSelect: openRecord, ) ```

AppMetaChipdefault constructor

Knob Type Default What it does
detail String? null Trailing `· detail` segment (`MetaChipProps.detail` — ref chips: the record title beside the id). Under pressure the detail end-ellipsizes; the label (id) segment never truncates — the one sanctioned truncation in the chip vocabulary.
glyph String? null Leading data-font micro-glyph character (◈ ⚑ ⌖ ▤).
label * String — The chip text, rendered as written on every face.
leading Widget? null Leading widget slot — the tag variant's 16px `AppAvatar`.
onSelect VoidCallback? null Tap action (ref navigates, file opens). Null renders the inert form.
title String? null Tooltip + accessible name override (e.g. the file chip's filename, the removal chip's who/when).
variant AppMetaChipVariant
outline · fill · danger · ref · file · tag
AppMetaChipVariant.outline The face. Defaults to AppMetaChipVariant.outline.

AppFileAttachmentCard widget

The design-system AttachmentCard — one file attachment, stated in full (`Thread.d.ts` `AttachmentCardProps`).

Full dartdoc

The design-system AttachmentCard — one file attachment, stated in full (`Thread.d.ts` `AttachmentCardProps`). ```dart AppFileAttachmentCard( name: 'aisle-4-walk.pdf', detail: 'PDF · 1.2 MB', onOpen: openFile, ) ```

AppFileAttachmentCarddefault constructor

Knob Type Default What it does
detail String? null Sub-line — `PDF · 1.2 MB` (kind and/or size, machine text).
glyph String '▤' Kind glyph (data-font text, the sanctioned micro-glyph set).
name * String — Filename — the card's one line of identity; end-ellipsized.
onOpen VoidCallback? null Open/download — the whole card taps. Null renders the card inert.

AppComposeScaffold widget

The design-system ComposeSurface — the focused drafting anatomy (`Thread.d.ts` `ComposeSurfaceProps`): ✕ · optional title · the surface's ONE accent slab up top, the expanding body, and the optionsBar pinned above the keyboard behind a block-start hairline. Host it through showAppComposeSurface,…

Full dartdoc

The design-system ComposeSurface — the focused drafting anatomy (`Thread.d.ts` `ComposeSurfaceProps`): ✕ · optional title · the surface's ONE accent slab up top, the expanding body, and the optionsBar pinned above the keyboard behind a block-start hairline. Host it through showAppComposeSurface, which morphs by window class; the widget itself is a plain body that never knows which host it is in, so a test (or a composed call site) can mount it directly. **Focus is the host's.** The whole point of the surface is that drafting is frictionless, so the body's field should carry `autofocus: true` — the spec says "the host's autofocused field". The scaffold never forces focus on a body it doesn't own. **Draft protection is the host's, via `PopScope`.** The dialog's barrier tap, Esc, and Android (predictive) back all resolve through the hosting route's `maybePop`, so a host with a dirty draft wraps its body in a `PopScope` (or reuses its form machinery) and gates those at once. The surface itself cannot know whether the draft is dirty. **The ✕ is only gated if onClose routes through `maybePop`.** `Navigator.pop()` BYPASSES `PopScope` entirely, so a host that wires the close affordance to `pop()` gets a guarded barrier and a ✕ that discards a dirty draft without asking — the one dismissal route the guard cannot reach on its own. Wire `maybePop()`, as the example does. ```dart showAppComposeSurface<void>( context, builder: (context) => AppComposeScaffold( // maybePop, never pop: pop() skips the PopScope below. onClose: () => Navigator.of(context).maybePop(), title: 'New post', submit: AppButton(variant: AppButtonVariant.slab, label: 'Post', ...), optionsBar: typeSegmentsAndAttachControls, body: PopScope( canPop: !draftIsDirty, onPopInvokedWithResult: (didPop, _) { if (!didPop) confirmDiscard(context); }, child: draftField, // autofocus: true ), ), ) ```

AppComposeScaffolddefault constructor

Knob Type Default What it does
body * Widget — The draft — the host's field, DOCKED at the block end directly above the options bar (one composer cluster above the keyboard), growing upward as the draft lengthens; the region above is quiet context. Should carry `autofocus: true` (the spec's "the host's autofocused field"); the scaffold never…
onClose * VoidCallback — Fires when the ✕ is tapped. The host owns dismissal (typically `Navigator.of(context).pop()`, or its own discard-confirm flow when the draft is dirty) — the scaffold never pops.
optionsBar Widget? null The options bar revealed with the surface — type segments and attach/tag controls — pinned above the keyboard behind a block-start hairline. The slot hosts whatever the host composes (a `Wrap` when the controls need to); absent means no bar renders.
submit * Widget — The surface's ONE accent slab (e.g. a slab `AppButton` labeled "Post"). Nothing else on the surface may be a slab — the ✕ is a quiet icon button.
title String? null Optional title in the title role (600 at 14), e.g. "New post".

Types its knobs take

These are not things you choose — they are the shapes the knobs above expect. They live under this entry rather than in the navigation for exactly that reason.

AppMessageQuote support

The quoted-message reference a reply carries (`MessageRowProps.quote`) — a plain value pair the row renders as the one-line quote block.

Field Type Default What it does
author * String — The quoted message's author display name.
excerpt * String — A short excerpt of the quoted message; end-ellipsized under pressure.

AppThreadSegment support

One run of thread body text. The HOST owns mention detection (it knows the tagged users); the card owns the paint — mention runs render 500 in accent, per the spec's TYPE MAP.

Field Type Default What it does
text * String — The run's text.
Field Type Default What it does
text * String — The run's text.

FloorplanBand support

A floorplan's ONE TRANSIENT BAND — the contract's `ScaffoldHeaderBandProps` on both floorplans that declare one, stated once here rather than twice. `DetailScreen.d.ts` and `RecordsScreen.d.ts` each type their `band` member as that shared props type and forward it VERBATIM, so a Dart type per…

Field Type Default What it does
actionLabel String? null Label for the ONE action beside message (the Undo pattern).
message String? null The transient message. Null draws nothing.
onAction VoidCallback? null Callback for that single action.

RecordsColumn support

A filterable column of a records surface — the display column (AppColumn, the sole carrier of cell grammar) plus the paint-free filter metadata the records filter pane needs. Kept map-free: filterValue (when set) and column.value are typed accessors over T, never a string-keyed lookup.

Field Type Default What it does
column * AppColumn<T> — The display column — carries the label, cell type, and value accessor.
compactVisible bool? null Whether this column belongs to the REDUCED set a records table renders below the medium window class (the spec's adaptation contract: the host passes fewer columns below 600 — id + primary + status; horizontal scroll is the fallback, never the primary posture). Null = no declaration. When no column…
filterValue Object? Function(T row)? null Typed raw value for filtering, when column.value returns a display-formatted string (e.g. a formatted date). Falls back to column.value when null.
filterable bool true Whether this column is offered as a facet in the filter pane. Free-text columns typically set this false and rely on the search field. Read by the PATTERNS (`RecordsTableScaffold`, `RecordsMapView`, `RecordsChrome`).

ThreadComposer support

THE COMPOSER (`ThreadScreenKnobs.composer`, REQUIRED since #1639): where it sits AND whose it is, as ONE value, so a combination the floorplan would have to ignore cannot be spelled. It replaced the enum `composer` plus the five loose parameters that only some of its values read (`composerBar`,…

Field Type Default What it does
attachments Widget? null
onPost * Future<void> Function(String body) —
onReply Future<void> Function(T item, String body)? null
Field Type Default What it does
bar * Widget —
Field Type Default What it does
attachments Widget? null
onPost * Future<void> Function(String body) —
onReply Future<void> Function(T item, String body)? null
Field Type Default What it does
strip * Widget —

ThreadEmpty support

The empty face's copy — what the view shows, stated flat (`ThreadScreenProps.empty`).

Field Type Default What it does
detail String? null One supporting line. Null renders the title alone.
title * String — The face's headline — "No posts yet".

ThreadEntry support

One root entry — everything the floorplan needs to render an item as the feed's AppThreadCard and its reply run. This is the floorplan's ITEM ADAPTER: `items` stays the app's own type (so every slot is typed in the app's vocabulary) and ThreadScreen.entry projects it into this kit-vocabulary shape.

Field Type Default What it does
author * String — Author display name — also the default avatar source.
avatar Widget? null Override the leading identity block; null is the avatar minted from author (AppThreadCard.avatar).
body List<AppThreadSegment>? null The rendered body, when the host marks mentions up. Null renders text as one plain run.
chips List<Widget> const [] The header chip cluster, in importance order.
danger bool false Flagged — the 2px danger start edge.
files List<Widget> const [] File attachments.
footerChips List<Widget> const [] The footer chip row, in importance order.
key * Object — Stable identity — the reply-run disclosure and the sort's tie-break read it; never rendered.
media Widget? null Media region — full-bleed, aspect-fit.
meta String? null Sub-line — `Role · 08-10 08:12 · edited`.
onFooterChipsOverflowTap VoidCallback? null The footer row's OVERFLOW tap — the "+N" the chip row collapses into, opening the sheet that lists every chip (AppThreadCard's own `onFooterChipsOverflowTap`, which this floorplan simply did not forward until 2026-09-20). Without it the "+N" is a COUNT and not a control, which is worse than…
onLongPress VoidCallback? null The ACCELERATOR onto this block's actions — AppThreadCard.onLongPress, forwarded (`ThreadScreen.d.ts`, THE ROOT CARD TAKES THE ACCELERATOR TOO, 2026-10-03): a long press opens the same door ThreadScreen.cardMenu's ⋯ does. NEVER THE ONLY PATH, exactly as ThreadReplyEntry.onLongPress states it: the…
onMediaTap VoidCallback? null The media region's tap — AppThreadCard.onMediaTap, forwarded (`ThreadScreen.d.ts`, A HOSTED IMAGE OPENS, #1120): the host opens its viewer from it, and the card's onOpen does not fire from the image. Null leaves the media inert. Ignored without media.
onOpen VoidCallback? null The entry's QUIET-REGION TAP — AppThreadCard.onOpen, forwarded. A FEED uses it to drill into the conversation the block stands for; a DRILLED thread leaves it null, because the conversation is already open and a card that navigated to itself would be a dead affordance. The component tier has carried…
replies List<ThreadReplyEntry> const [] The conversation's replies. The floorplan renders them oldest first.
searchText String? null Overrides what search reads for this entry. Null searches the author, text, and every reply's author and text.
text * String — The entry as PLAIN text: what search reads, and the fallback body.
timestamp * DateTime — Sort key for ThreadOrder.

ThreadHero support

The RECORD-BANNER at the top of a thread screen — the scope a drilled-into conversation belongs to, stated ONCE (`Thread.d.ts` `ConversationHeaderProps` minus the paint escape hatches, forwarded verbatim to AppConversationHeader). The POSTURE is the floorplan's, which is why this is a typed props…

Field Type Default What it does
chips List<Widget> const [] The chip cluster, in IMPORTANCE order — status marks first.
description String? null The record's description, 2-line clamp.
kind String? null The record type's display name, the accent micro-label above title ("Work order").
menu Widget? null The trailing ⋯ carrying the ROOT's actions.
meta String? null Quiet inline-start meta text.
onOpen VoidCallback? null Tap-through to the record — the whole banner opens.
thumb Widget? null The record's 56 hairline-framed thumbnail.
title String? null The scope's name — the record's default label, or the channel name.

ThreadOrphanZone support

Orphan replies — items whose parent chain is broken (offline-first, first-class) — rendered under ONE banner AFTER the feed's own roots and ThreadScreenZones, through the SAME reply-row renderer as an ordinary conversation (`ThreadScreen.d.ts`, THE ORPHAN ZONE — a module-tier list already carried…

Field Type Default What it does
items * List<ThreadReplyEntry> — The orphan replies, in any order; rendered oldest first, exactly as a ThreadEntry.replies run.
label * String — The zone's head line — "3 messages whose original post is missing."

ThreadScreenZone support

One THREAD ZONE after the feed — `ThreadZoneProps` plus the identity a list of them needs and the children spelled as a slot. A zone's content is FULL POST BLOCKS and never a reply run, which is why AppThreadZone carries no inline-start rule and no inset: indenting them would claim a reply…

Field Type Default What it does
collapsed bool false Collapsed renders the head only. Default false.
content * Widget — The zone's content — full post blocks, never a reply run.
key * String — Stable identity — what the zone's collapse state keys on. Never rendered; unique across zones.
label * String — The head line — "More posts on this record (2)".
onToggle VoidCallback? null Makes the head the expand/collapse tap target; null = a static head.

Where it lives

Class File
ThreadScreen packages/app_widgets/lib/src/floorplans/thread_screen.dart
AppThreadCard packages/app_widgets/lib/src/kit/data/thread_card.dart
AppMessageRow packages/app_widgets/lib/src/kit/data/message_row.dart
AppReplyRun packages/app_widgets/lib/src/kit/data/thread_furniture.dart
AppThreadEvent packages/app_widgets/lib/src/kit/data/thread_furniture.dart
AppThreadRule packages/app_widgets/lib/src/kit/data/thread_furniture.dart
AppThreadZone packages/app_widgets/lib/src/kit/data/thread_furniture.dart
AppThreadComposerBar packages/app_widgets/lib/src/kit/data/thread_composer.dart
AppThreadComposeStrip packages/app_widgets/lib/src/kit/data/thread_composer.dart
AppConversationHeader packages/app_widgets/lib/src/kit/data/conversation_header.dart
AppChipRow packages/app_widgets/lib/src/kit/data/chip_row.dart
AppMetaChip packages/app_widgets/lib/src/kit/data/meta_chip.dart
AppFileAttachmentCard packages/app_widgets/lib/src/kit/data/attachment_card.dart
AppComposeScaffold packages/app_widgets/lib/src/kit/surfaces/compose_surface.dart