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
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.
Blank renders no status. "Blocked" takes the danger treatment.
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 |