Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Status legend: ⬜ Todo · ✅ Done
| 16 | Preview | full-screen image viewer: zoom, paging | ✅ |
| 17 | Tool | TBD | ⬜ |
| 18 | Suggestion & Suggestion Group | plain & outlined rows; scroll, wrap, column | ✅ |
| 19 | Confirmation | default, approved, rejected | ⬜ |
| 19 | Confirmation | pending, approved, rejected; approve/reject buttons; parts render in a thread | ✅ |
| 20 | Error state | failure card + retry pill; failed assistant turns render it automatically | ✅ |
| 21 | Code block | built-in synchronous highlighter; languages host-extensible | ✅ |
| 22 | Thinking indicator | turning, breathing asterisk + shimmer label; active & settled | ✅ |
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## 0.3.0 (unreleased)

- **Confirmation** — `FlowConfirmation`, the approval card: an
asterisk-marked request with approve and reject buttons that settle
into the outcome, every label host-localized. `FlowConfirmationPart`
renders it in a thread, reporting through
`FlowThread.onConfirmationRespond`; `FlowConfirmationStyle` joins the
component styles with a `FlowTheme.confirmationStyle` default.
- **Thread list** — `FlowThreadList`, the side panel's conversation
history: host-labeled sections of title-only rows, single selection by
id, an unread dot and pinned glyph, and a leading icon slot. Metrics
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Values come from the Flow UI Figma file. Role names follow Material 3's `ColorSc
| 16 | Preview | full-screen image viewer: zoom, paging | ✅ |
| 17 | Tool | TBD | ⬜ |
| 18 | Suggestion & Suggestion Group | plain & outlined rows; scroll, wrap, column | ✅ |
| 19 | Confirmation | default, approved, rejected | ⬜ |
| 19 | Confirmation | pending, approved, rejected; approve/reject buttons; parts render in a thread | ✅ |
| 20 | Error state | failure card + retry pill; failed assistant turns render it automatically | ✅ |
| 21 | Code block | built-in synchronous highlighter; languages host-extensible | ✅ |
| 22 | Thinking indicator | turning, breathing asterisk + shimmer label; active & settled | ✅ |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
| [`FlowCodeBlock`](https://flowui.stac.dev/components/code-block) | Fenced code with built-in synchronous highlighting, a header label, and a copy affordance — languages host-extensible |
| [`FlowMarkdown`](https://flowui.stac.dev/components/markdown) | Assistant prose typeset from a built-in parser — headings, emphasis, lists, quotes, tables, links, and fences composing the code block; assistant turns render it by default and it streams gracefully |
| [`FlowErrorState`](https://flowui.stac.dev/components/error-state) | Failure card with a host-written message and retry pill — failed turns render it automatically |
| [`FlowConfirmation`](https://flowui.stac.dev/components/confirmation) | Approval card — an asterisk-marked request with approve and reject buttons that settles into the outcome; confirmation parts render it in a thread |
| [`FlowMessageActions`](https://flowui.stac.dev/components/message-actions) | Copy / regenerate / edit / feedback row under a message |
| [`FlowComposer`](https://flowui.stac.dev/components/composer) | Multiline input with send/stop, attachments strip, the platform's file dialog (`showFlowAttachmentPicker` from your own menu, or a built-in attach button), image paste and card-scoped drop (web), and leading/trailing action slots |
| [`FlowMenu`](https://flowui.stac.dev/components/menu) | Icon-triggered menu with groups, submenus, and toggles — anchored card on desktop, bottom sheet on phones |
Expand Down
1 change: 1 addition & 0 deletions docs/public/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
/playground/code-block /playground/ 200
/playground/markdown /playground/ 200
/playground/error-state /playground/ 200
/playground/confirmation /playground/ 200
/playground/add-to-chat /playground/ 200
/playground/pill /playground/ 200
/playground/attachments /playground/ 200
Expand Down
146 changes: 146 additions & 0 deletions docs/src/content/docs/components/confirmation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
title: Confirmation
description: The approval card — an asterisk-marked request with approve and reject buttons that settle into the outcome.
sidebar:
order: 18
---

import FlowDemo from '../../../components/FlowDemo.astro';

`FlowConfirmation` is the approval card: an asterisk-marked request on a
raised card, with approve and reject buttons that settle into the
outcome. It renders state and reports intent — recording the decision,
and re-rendering the card settled, is the host's business. The package
ships no strings, so the title, the request and every button label are
host-localized; the request announces to assistive tech as a live
region, since it arrives unprompted.

The card is *runtime chrome*, not content: the asterisk header and the
raised, hairline-and-shadow surface are its identity, the mark that
distinguishes the host's own gate — a destructive tool call, a guarded
action — from anything composed into the conversation. That is why the
glyphs are fixed and only their colors restyle.

## Pending

The default state, straight from the design: the warning accent, the
request, and the two buttons. A button renders only when both its label
and its callback are set — a pending card with neither pair is a
read-only notice, for chrome staged before the request is answerable.

<FlowDemo demo="confirmation" variant="pending" height={220} title="The pending card — the buttons settle it" />

```dart title="The full anatomy"
FlowConfirmation(
title: 'Approval required',
message: 'Delete 3 files in drafts/. This cannot be undone.',
approveLabel: 'Approve',
rejectLabel: 'Reject',
onApprove: () => respond(true),
onReject: () => respond(false),
)
```

## Approved

The widget holds no state: a tap reports intent, and the card settles
only when the host passes the new status back. The buttons collapse into
one row of the same footprint — so the card's height holds — and the
accent flips to success:

<FlowDemo demo="confirmation" variant="approved" height={220} title="Settled, approved" />

```dart title="The settled card"
FlowConfirmation(
title: 'Approval required',
message: 'Delete 3 files in drafts/. This cannot be undone.',
status: FlowConfirmationStatus.approved,
approvedLabel: 'Approved',
)
```

## Rejected

The rejected outcome carries the error accent:

<FlowDemo demo="confirmation" variant="rejected" height={220} title="Settled, rejected" />

```dart title="The declined request"
FlowConfirmation(
title: 'Approval required',
message: 'Delete 3 files in drafts/. This cannot be undone.',
status: FlowConfirmationStatus.rejected,
rejectedLabel: 'Rejected',
)
```

## In a thread

A `FlowConfirmationPart` in any turn becomes this card. The buttons hand
the message, the part and the decision back through
`FlowThread.onConfirmationRespond`, so the host can find the request
they belong to and re-render the part settled; the labels are
thread-level, since they are the same words on every card. Keep the
message's own status `complete` while the confirmation is pending — the
wait belongs to the part:

<FlowDemo demo="confirmation" variant="thread" height={480} title="A gated action in a turn" />

```dart title="The host contract"
FlowThread(
messages: messages,
approveLabel: 'Approve',
rejectLabel: 'Reject',
approvedLabel: 'Approved',
rejectedLabel: 'Rejected',
// Typically: record the decision, run or skip the action, and
// re-render the part with the settled status.
onConfirmationRespond: (message, part, approved) =>
record(message, part, approved),
)

FlowMessageData(
id: 'a1',
role: FlowMessageRole.assistant,
parts: [
FlowTextPart('I can clear those drafts for you.'),
FlowConfirmationPart(
title: 'Approval required',
message: 'Delete 3 files in drafts/. This cannot be undone.',
),
],
)
```

## Restyling

`FlowConfirmationStyle` carries the card's overrides — install one on
`FlowTheme.confirmationStyle` for every card, or pass `style:` to one
widget; a widget's own style wins field by field, and nulls fall through
to the tokens. The three accents color a state's marks — the asterisk,
the settled glyph and its wash — so one override recolors it coherently.
The title and the settled label stay in the ink ramp: the light accents
fall short of WCAG AA for text on the card, so the words keep their
contrast and the accent signals beside them:

```dart title="A different pending accent"
FlowConfirmation(
title: 'Approval required',
message: 'Delete 3 files in drafts/?',
style: const FlowConfirmationStyle(pendingColor: Color(0xFFB65C33)),
)
```

Beyond the style class, `padding:` and `borderRadius:` override the
card's own metrics, the per-component convention.

## Key API

| Member | What it does |
|---|---|
| `title` / `message` | Host-localized header and request; the message is a live region |
| `status` | `FlowConfirmationStatus.pending` shows the buttons; `approved` / `rejected` settle the card |
| `approveLabel` / `onApprove` | The filled button — renders only when both are set |
| `rejectLabel` / `onReject` | The outlined button — same rule |
| `approvedLabel` / `rejectedLabel` | The settled row's text; null leaves the outcome glyph alone |
| `style` | `FlowConfirmationStyle` overrides, merged over `FlowTheme.confirmationStyle` |
3 changes: 2 additions & 1 deletion docs/src/content/docs/components/message-thread.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ FlowMessage(

Message content is typed parts, not strings: a sealed `FlowMessagePart`
with `FlowTextPart`, `FlowAttachmentPart`, `FlowImagePart`, and
`FlowCustomPart` subtypes (plus `FlowCodePart` and `FlowErrorPart`).
`FlowCustomPart` subtypes (plus `FlowCodePart`, `FlowErrorPart` and
`FlowConfirmationPart`).
`FlowAttachmentPart` renders sent files — lifted above a user bubble as
image cards, tiles elsewhere —
while `FlowImagePart` is the large-format picture, an AI-generated image
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ elements and the remaining AI states are on the way.
| Attachment preview | <span class="badge-done">Shipped</span> |
| Tool | <span class="badge-todo">Planned</span> |
| Suggestions | <span class="badge-done">Shipped</span> |
| Confirmation | <span class="badge-todo">Planned</span> |
| Confirmation | <span class="badge-done">Shipped</span> |
| Error state | <span class="badge-done">Shipped</span> |
| Code block | <span class="badge-done">Shipped</span> |
| Markdown | <span class="badge-done">Shipped</span> |
Expand Down
3 changes: 2 additions & 1 deletion docs/src/content/docs/theming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ Between the tokens and a single widget sit the component styles —
Material's component-theme tier. Each major widget has a `FlowXStyle`
data bag of optional overrides (`FlowMenuStyle`, `FlowMarkdownStyle`,
`FlowComposerStyle`, `FlowMessageStyle`, `FlowCodeBlockStyle`,
`FlowErrorStateStyle`, `FlowMessageActionsStyle`, `FlowPillStyle`,
`FlowConfirmationStyle`, `FlowErrorStateStyle`,
`FlowMessageActionsStyle`, `FlowPillStyle`,
`FlowSuggestionStyle`), and the theme can carry an app-wide default for
each:

Expand Down
2 changes: 2 additions & 0 deletions lib/flow_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export 'src/widgets/flow_code_block.dart';
export 'src/styles/flow_code_block_style.dart';
export 'src/widgets/flow_composer.dart';
export 'src/styles/flow_composer_style.dart';
export 'src/widgets/flow_confirmation.dart';
export 'src/styles/flow_confirmation_style.dart';
export 'src/widgets/flow_drop_target.dart';
export 'src/widgets/flow_error_state.dart';
export 'src/styles/flow_error_state_style.dart';
Expand Down
36 changes: 36 additions & 0 deletions lib/src/models/flow_message_part.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,42 @@ class FlowErrorPart extends FlowMessagePart {
final bool retryable;
}

/// Where a confirmation request stands.
///
/// The host owns the transition: a tap on the card reports intent, and the
/// card renders settled only when the host passes the new status back.
enum FlowConfirmationStatus { pending, approved, rejected }

/// A request for the user's go-ahead, rendered by a `FlowConfirmation`.
///
/// Runtime chrome, not model content: the host — a tool gate, a
/// destructive-action guard — constructs it from facts it resolved itself,
/// and flips [status] when the user answers. The buttons' labels are
/// thread-level (`FlowThread.approveLabel` and friends), since they are
/// the same words on every card.
class FlowConfirmationPart extends FlowMessagePart {
const FlowConfirmationPart({
this.title,
this.message,
this.status = FlowConfirmationStatus.pending,
});

/// Host-localized header label, e.g. 'Approval required'. Null renders
/// the asterisk alone.
final String? title;

/// What is being asked, host-written and sentence-case. Announced as a
/// live region, since requests arrive unprompted.
final String? message;

/// Pending shows the buttons; approved and rejected settle the card.
///
/// Keep the message's own status `complete` while the confirmation is
/// pending — the wait belongs to this part, and a `pending` message
/// renders the thinking indicator instead of its parts.
final FlowConfirmationStatus status;
}

/// Host-defined content, rendered through a `FlowCustomPartBuilder`.
class FlowCustomPart extends FlowMessagePart {
const FlowCustomPart({required this.type, this.data});
Expand Down
Loading