verdify_ui 0.1.0
verdify_ui: ^0.1.0 copied to clipboard
Verdify's Flutter design system: 36 token-driven, WCAG 2.2 AA widgets with light and dark themes, built on Material 3.
0.1.0 #
First public release on pub.dev — the complete Verdify Flutter design system:
36 token-driven widgets, light + dark themes (system brightness; ADR-0030),
WCAG 2.2 AA, all bound to a generated VerdifyTheme. Bundles the foundation
behavior layer (roving focus, anchored overlays, live regions, the two-tone
focus ring) and the cross-system a11y token fixes (ADR-0029).
Added #
-
VerdifyTrustScore (non-interactive status output, P3.15): displays the cross-ecosystem trust score — a calm, neutral readout that is never a verdict. Variants:
value(score + label + optional scale, default) /meter(score + neutral gauge track) /compact(dense inline). States: default (resolved) · updating (digit transition viaAnimatedBuilder+IntTweenonmotionDurationFast, NOT deliberate — reduced-motion guard jumps to end state instantly) · loading (VerdifySkeleton— infinite animation excluded from goldens) · unavailable (plain text, honest about absence). Non-interactive: noFocusableActionDetector, no focus ring, no target-size floor (spec §6). A11y (spec §7): polite live region viaVerdifyLiveRegion(announces updates without interruption); gaugeSemantics(value:)asaria-valuenowanalog (role=meter); score usesFontFeature.tabularFigures()(spec §2). Token bindings (zero raw colors): scorecolorTextPrimary/text.h2; label/scalecolorTextSecondary/text.label; description/unavailablecolorTextMuted/text.caption; gauge trackcolorControlBg/ bordercolorControlBorder/ fillcolorControlFg; gauge endsradiusFull. NocolorStatus*orcolorAction*— the score is neutral (spec §5). 14 component tests + 1 golden (8 scenarios). EXIT=0 on verify.sh (611 tests). -
VerdifyConsentToggle (compose-a-committed-control, P3.15): explicit, revocable affordance for granting / withdrawing a single data-use permission. Composes
VerdifySwitchvia its publiclabel/description/value/onChanged/loadingseam. Layout: scope as Switchlabel(primary accessible name), recipient + detail as Switchdescription(secondary AT text) — spec §7 accessible name binds scope AND recipient. Variants:grant(default) /withEvidence(adds evidence affordance link) /agentScoped(recipient is an AI agent). States: default (not granted, off) · granted (on) · hover/pressed/focus/disabled/loading (all delegated to VerdifySwitch). brand ≠ state: granted track usescolorActionPrimaryBg*(action color), NOTcolorStatusVerified*(verified green) — granting permission is a brand moment, not a verification result (spec §5/§8). Token bindings (zero raw colors): evidence linkcolorActionGhostFg; all Switch track/thumb/focus tokens come from VerdifySwitch. 18 component tests + 1 golden (8 scenarios). -
VerdifyIdentityChip (compose-a-committed-control, P3.15): compact inline reference to a single identity — person or AI agent. Composes committed widgets via public seams only:
VerdifyAvatar(decorative,decorative:true),VerdifyAgentBadge(whenisAgent:true),VerdifyVerifiedBadge(whenshowVerified:true),VerdifyButton(ghost, remove control onremovablevariant). Variants:static_(read-only, non-interactive default),interactive(the chip is a button with keyboard + focus ring + 44px tap-target),removable(trailing remove control named "Remove -
VerdifyDataGrid (Phase-3 assembly, the final complex widget): the interactive, roving sibling of
VerdifyTable. No brand-book spec exists (decision D-FW5) — ported from the COMMITTED REACT CODE (packages/ui/src/components/data-grid); gated on React-code PARITY + a11y, not a spec. API: a columns + rows DATA MODEL (VerdifyDataGrid(columns:, rows:, …), theVerdifyTableFlutter idiom) rather than React's compositional slots —VerdifyDataGridColumn(key/header/sortable/onSort/mono/flex),VerdifyDataGridRow(id + absoluterowIndex+ cells map),VerdifyDataGridCell(text/child +status),VerdifyDataGridBulkAction(label/onPressed/destructive). 2D cell roving (the defining behavior, ported from the React(row,col)roving model): the grid is a SINGLE tab stop (FocusTraversalGroup+ per-cellExcludeFocusTraversalroving tabindex — only the active cell is Tab-traversable); a(row,col)cursor over aMap<String,FocusNode>keyed by cell position; a grid-levelShortcuts→Actionsbinds ArrowRight/Left (inline axis, clamp = no wrap at the edge), ArrowUp/Down (between rendered rows), Home/End (row ends), Ctrl/Cmd+Home/End (grid corners), PageUp/PageDown (a 10-row viewport jump, clamped), and Ctrl/Cmd+A (onSelectAll); each moverequestFocuses the destination node and draws the visible 2px focus ring on the active cell; focusing a cell (pointer/Tab/arrow) re-seeds the cursor so click + Tab + arrows agree. Selection (Reactmultiple): leading box-only checkboxes (theVerdifyTable._TableCheckboxprecedent — composesVerdifyCheckbox's token model: brand action-primary fill on checked, never status-verified) + a parent select-all (indeterminate when partial) + a bulk-action bar (composed ofVerdifyButtons — primaryExport, destructiveRevoke) that appears only when ≥1 row is selected. Sort: a sortable header (VerdifyButton-less interactive control with focus ring + tap floor) whose accessible name carries the direction (Sort Status, ascending— thearia-sortanalog) + a non-color caret (Icons.arrow_upward/downward— shape, never color alone);onSort(requestedDirection)mirrorsVerdifyTable. States: default · hover (colorActionGhostBgHoverrow fill) · selected · sorted · focus (active-cell ring) · loading (a quiet spinner slot, grid name carries, loading) · empty (a plaincolorTextMutedline, NOT an error) · error (colorStatusCriticalFghonest copy). Status cell: the status fg paired with the cell's TEXT (colorStatus{Verified,Signal,Caution,Critical}OnSurface) — readable in grayscale, lives in the cell only (never the row/header/selection accent), never the brand (G-U2). Virtualization: body rows render through a lazyListView.builder(rows build on demand, not all eagerly); the full-set shape (rowCount/colCount+ each row's absoluterowIndex) rides on the grid Semantics name so "12,000 rows" survives a windowed tree. Full windowing/recycling beyondListView.builder's laziness, and per-cellaria-rowindex/colindex+ a nativerole=grid, are DEFERRED (documented partial — the same Flutter-has-no-grid-role limitationVerdifyTablecarries; the React cell-entry/exitcontrols=single|multipledelegation is also not ported — the data-model cells host values/status, not arbitrary nested controls). A11y (spec-less; React-parity + guidelines): grid name carries label + full-set shape + state;meetsGuideline(iOSTapTargetGuideline)(checkboxes get the 44px column viapadH:0so the cell padding never clamps the box's hit area; the sort header gets aConstrainedBox(minHeight: targetMobile));meetsGuideline(labeledTapTargetGuideline)(every control named);meetsGuideline(textContrastGuideline)AA over cell text. DEVIATION (a real finding the Flutter contrast gate caught that React's jsdom tests cannot): the React selected-row class isbg-action-secondary-bg-hover(#D6DCE8), but a status cell's on-surface text on that fill is 3.99:1 (sub-AA); we bind the RESTINGcolorActionSecondaryBg(#E8ECF4, ≥4.6:1, still the neutral action-secondary selection accent) and add a leadingcolorActionSecondaryBorderaccent BAR so a selected row stays distinct from a hovered one (selection is never the fill alone). A token-tierselection-bgguaranteed against status text would restore the darker fill (deferred, no version bumps this wave). Token bindings (zero raw colors, gate-enforced): canvascolorSurfaceCanvas/colorSurfaceBorder/radiusMd; headercolorSurfaceRaised+shadowSm+colorBorderDefaultrule, labelcolorTextSecondary@v.text.label; cell textcolorTextPrimary@v.text.body, monov.text.mono(LTR-isolated, G-U6); row hovercolorActionGhostBgHover, selectedcolorActionSecondaryBg+colorActionSecondaryBorderbar; active-cell ringcolorBorderFocus/focusRingWidth; statuscolorStatus*OnSurface; emptycolorTextMuted; errorcolorStatusCriticalFg; bulk barcolorSurfaceRaised+colorBorderDefault, actions viaVerdifyButton; sort caret/labelcolorActionGhostFg, sort hovercolorActionGhostBgHover; row transitionmotionDurationFast+motionEasingVerdify, instant under reduced motion. 21 component tests (renders columns+rows, grid name carries full-set shape, single tab stop, ArrowRight/Left no-wrap, ArrowUp/Down, Home/End, sort fires onSort + names direction, row-checkbox toggle, select-all, bulk bar appears/hides, bulk action fires, empty/loading/error slots, status-as-text, iOS tap-target, labeled controls, AA contrast, read-only has no selection chrome) + 1 golden (5 scenarios: comfortable/sorted, compact, selected+bulk-bar, empty, error). Gallery_DataGridGalleryadded. Barrel export:src/components/data_grid/verdify_data_grid.dart. -
VerdifyCommandPalette (Phase-3 assembly): a modal keyboard-first launcher opened via
showVerdifyCommandPalette(context, inputLabel:, items:, …)— type a few letters, run a command or jump to a place without leaving the keyboard (spec §1). Modal shell (spec §2/§7):showGeneralDialog(theshowDialoganalog that lets the panel drive its own fade) →ModalBarrier+BlockSemantics(page behind removed from a11y tree —aria-modalanalog), focus TRAPPED in the route'sFocusScope, returns focus to the opener on close (the route restores it), Esc + scrim-tap dismiss;Colors.black54scrim (named constant — no--color-scrim-*in the Flutter emit, deviation documented, same as Sheet/Dialog). The active-descendant model (spec §6/§7/§8 — the KEY behavior): the INPUT keeps DOM focus the whole time; the input's ownFocus(onKeyEvent:)handles Down/Up/Home/End to move a tracked_activeIdover the VISIBLE RUNNABLE rows (group labels + disabled rows skipped), wrapping at the ends; the active row is highlighted (secondary hover fill) and exposed viaSemantics(selected: true)(thearia-activedescendant+aria-selectedanalog — Flutter has noaria-activedescendant; modeled by the input keeping focus + the active row'sselected:Semantics, documented limitation); Enter activates the active row (runsonRun+ closes viaNavigator.pop); focus NEVER leaves the input (proven by a test: after each arrow key the input still has focus) — the OPPOSITE ofVerdifySelect/VerdifyMenu(which MOVE focus into the listbox), and §8 names focus-moving as the forbidden anti-pattern. Input (combobox, spec §2): aTextField(Material defaults bound away — cursorcolorBorderFocus, no underline border, content padding lifts the field to the 44px tap floor) focused on open (post-framerequestFocus); typing FILTERS the results live (label OR secondary contains the query, case-insensitive — the palette's type-ahead, NOT the foundationtype_aheadkeystroke-jump); leading search glyph (decorativeExcludeSemantics) + an optional scope prefix chip (spec §3 scoped). Results (spec §2): grouped rows (consecutive same-grouprows under one mutedheadergroup-label) + optional recent list (shown when query empty, under a muted "Recent" heading) + an empty state (centeredcolorTextSecondarytext — announced as text, never silence) + optional footer (key hints). Variants (spec §3):commands(default) /navigation/withRecent/scoped(scope chip). States (spec §4): default (open, input focused, first runnable row active) · hover (pointer hover makes the row active — kept in sync with keyboard so Enter runs what is highlighted) · pressed (tap runs) · disabled row (colorTextDisabled, skipped by arrow nav, not activatable). A11y (spec §7): input isSemantics(textField:, label: inputLabel)(combobox, names itself — placeholder is never the name); listbox isSemantics(container:, label:); each row isMergeSemantics+Semantics(selected:, enabled:, label:)(option); group labels areheadernodes skipped by nav;meetsGuideline(iOSTapTargetGuideline)over input + rows;textContrastGuidelineover the readable content (input + row labels =colorTextPrimaryoncolorSurfaceRaised, AA). Token bindings (spec §5, zero raw colors): panelcolorSurfaceRaised+colorSurfaceBorder+radiusLg+shadowLg; inputcolorControlFg(query) /colorControlPlaceholder(placeholder + search glyph) /colorBorderFocus(cursor); input-to-results + group + footer dividerscolorBorderDefault; active-row fillcolorActionSecondaryBgHover(spec §5 — NEVER a status/brand fill: the active row is location, not a verified result, brand != state §3/§8); row labelcolorTextPrimaryatv.text.body; row secondarycolorTextSecondaryatv.text.label; group label + recent heading + shortcut hintcolorTextMutedatv.text.label; disabled rowcolorTextDisabled(DEC-C); empty + footer textcolorTextSecondary; scope chipcolorSurfaceInput/colorSurfaceBorderMuted/radiusSm; row cornersradiusMd; tap floortargetMobile; DEC-A: the input value pins 16px (iOS no-zoom floor), the body role rides via leading/tracking. Open fademotionDurationBase(spec §5), collapsed to instant underMediaQuery.disableAnimations(reduced-motion guard) — never the deliberate check duration. 14 component tests (open-input-focused, return-focus-to-opener, live filter, empty state, recent→results, Down/Up/Home/End move active while focus STAYS on input, arrow nav skips group labels + disabled, Enter activates + closes, tap activates + closes, textField combobox, active-rowselected:tracks, iOS tap-target, AA contrast) + 1 golden (4 scenarios: commands+footer, scoped+chip, with-recent, empty). Gallery section_CommandPaletteGalleryadded toexample/lib/main.dart. Barrel exports the public API (VerdifyCommandPaletteItem,VerdifyCommandPaletteOptionRow,VerdifyCommandPalettePanel,VerdifyCommandPaletteVariant,showVerdifyCommandPalette) via ashowclause (private helpers stay internal). Concern (not re-decided here): group-label/shortcut-hintcolorTextMutedoncolorSurfaceRaisedis 2.55:1 — below WCAG AA 4.5:1 for normal text; the spec §5 mandates the muted token, and the same binding/contrast holds inVerdifySelect/VerdifyMenugroup labels (whose contrast tests, like this one, run over the closed/flat surface). This is a token-tier concern shared across the listbox family, surfaced for a holistic decision rather than unilaterally overridden in one component. -
VerdifyAccordion (Phase-3 assembly): stacks collapsible sections (spec §1). Anatomy (spec §2):
rootcontainer +itemlist, each with aheader(Semantics header: true — h2 analog) wrapping atriggerbutton (focusable control) +indicatorchevron (animated rotate, decorative ExcludeSemantics) +panel(collapsible region, role=region labeled by trigger). Variants (spec §3):single(default — only one open at a time) /multiple(any number open) /collapsible(single allows all-closed) / disabled per item. States (spec §4): default (collapsed) · hover (colorActionSecondaryBgHover) · pressed · focus (2 pxcolorBorderFocusring) · open (panel revealed + chevron rotated 180°) · disabled (colorTextDisabled, skipped by arrow). Height animation:AnimatedSizeatmotionDurationBase+motionEasingVerdify; reduced-motion guard collapses toDuration.zero(VerifiedBadge seed pattern). Roving model (spec §6):VerdifyRovingGroup(vertical, manual activation) — Down/Up move focus, Home/End jump, wrap, skip-disabled, Enter + Space toggle focused item. A11y (spec §7):Semantics(header: true, button: true, enabled:, expanded:, label:)on each trigger viaMergeSemantics; panelSemantics(container: true, label: title)(aria-labelledby analog; Flutter programmatic ID link documented as limitation); indicatorExcludeSemantics. Token bindings (spec §5, zero raw colors):colorControlBgtrigger rest ·colorControlFgtrigger text ·colorActionSecondaryBgHoverhover/pressed ·colorActionGhostFgchevron ·colorBorderDefaultitem dividers ·colorBorderFocus/focusRingWidthfocus ring ·colorSurfaceCanvaspanel bg ·colorTextPrimarypanel text ·colorTextDisableddisabled title + icon ·targetMobiletap floor ·radiusMdcorners ·motionDurationBase+motionEasingVerdify. 25 component tests + 1 golden (4 scenarios). Gallery_AccordionGalleryadded. Barrel export:src/components/accordion/verdify_accordion.dart. -
VerdifySidebar (Phase-3 assembly): primary navigation rail (spec §1). Anatomy (spec §2):
navlandmark + optionalheader+ optionalgroup(labeled cluster) +itemlist (icon + label + optional badge) + current indicator + optionalcollapse-toggle+ optionalfooter. Variants (spec §3):expanded(default, icon + label) /collapsed(icon only, label inTooltip); overlay deferred — callers useVerdifySheetfor the modal-drawer case (documented partial). States (spec §4): default · hover (colorActionGhostBgHover) · pressed · focus (2 pxcolorBorderFocusring viaBoxShadow) · current (indicator bar +colorActionPrimaryBg/Fg+Semantics(selected:true)—aria-current=pageanalog) · disabled (colorTextDisabled, skipped by arrow). Roving model (spec §6):VerdifyRovingGroup(vertical axis) — Down/Up move, Home/End jump, wrap, skip-disabled, Enter + Space activate. Collapse toggle:Semantics(button:, expanded:, label: 'Collapse sidebar'/'Expand sidebar'). Token bindings (spec §5, zero raw colors):colorSurfaceRaisedrail ·colorSurfaceBorderedge ·colorSurfaceBorderMuteddividers ·colorTextSecondaryrest label ·colorTextPrimarycurrent label ·colorTextMutedgroup heading ·colorTextDisableddisabled ·colorActionPrimaryBgcurrent indicator + fill ·colorActionPrimaryFgcurrent icon ·colorActionGhostFgresting icon + toggle glyph ·colorActionGhostBgHoverhover fill ·colorBorderFocus/focusRingWidthfocus ring ·shadowSmelevation ·radiusMditem corners ·targetMobiletap floor. 24 component tests + 1 golden (6 scenarios). Gallery_SidebarGalleryadded. Barrel export:src/components/sidebar/verdify_sidebar.dart. -
VerdifySheet (Phase-3 assembly): modal panel that slides in from a viewport edge to hold a focused secondary task (spec §1). MODAL — focus is trapped inside while open; returns to trigger on close. Uses
showGeneralDialog(theshowDialoganalog for custom animations):ModalBarrierwithColors.black54scrim (named constant;--color-scrim-darknot in token emit — deviation documented),BlockSemanticsremoves page from a11y tree, route'sFocusScopetraps Tab/Shift+Tab. Anatomy (spec §2): scrim + panel + header (title + REQUIRED close control) + body (scrollable) + optional footer (pinned). Variants (spec §3): side =inlineEnd(default, right)/inlineStart(left)/blockEnd(bottom); size =sm(360px)/md(480px, default)/lg(640px). States (spec §4): opening =SlideTransition+FadeTransitiondriven by internalAnimationControllerreadingv.motionDurationBase+v.motionEasingVerdify; reduced-motion guard collapses both to instant (MediaQuery.maybeDisableAnimationsOf). Close control (ghost icon,colorActionGhostFg/BgHover) ALWAYS present. Esc + scrim tap + close control all dismiss. A11y (spec §7):Semantics(scopesRoute: true, namesRoute: true, label: title)on the panel (role=dialog + aria-labelledby analog); close labeled "Close" withisButton; focus trap from route'sFocusScope; return-focus fromModalRoute. Header/footer pinned; only body scrolls (Expanded+SingleChildScrollView). Token bindings (spec §5, zero raw color constructors):colorSurfaceRaisedfill,colorSurfaceBorderpanel edge,colorSurfaceBorderMuteddividers,colorTextPrimarytitle (v.text.h2),colorTextSecondarybody,colorActionGhostFg/BgHoverclose,shadowLgelevation,radiusLgleading corners,motionDurationBase+motionEasingVerdify. 13 component tests + 1 golden (4 scenarios). Gallery section_SheetGalleryadded. Barrel export:src/components/sheet/verdify_sheet.dart. -
VerdifyTooltip (Phase-3 assembly): small floating label describing the control it is attached to (spec §1). NON-MODAL — focus never enters the tooltip; stays on the trigger the whole time. Variants (spec §3):
label(tooltip text IS the accessible name for icon-only triggers) /description(supplementary hint on a control that already has a visible label). States (spec §4): hidden by default; hover shows after a 500 ms delay; focus shows immediately, no delay; Escape dismisses without moving focus (WCAG 1.4.13 dismissible); hoverable — moving the pointer onto the tooltip keeps it open (WCAG 1.4.13 hoverable); persistent — stays until hover/focus leaves. UsesVerdifyAnchoredOverlay(NON-modal, no route, no focus trap) +VerdifyFadeIn(fast motion, instant under reduced-motion). A11y (spec §7): trigger is the only tab stop; tooltip content is in the a11y tree while shown;Semantics(tooltip: content)on the panel;ExcludeFocusremoves panel from traversal; focus never moves to tooltip; Escape handled on the Focus wrapper around the trigger. Token bindings (spec §5, zero raw colors):colorSurfaceRaisedfill,colorSurfaceBorderborder,colorTextPrimarytext,radiusSmcorner,shadowMdelevation,motionDurationFast+motionEasingVerdifyfade. 12 component tests + 1 golden (3 scenarios). Gallery section_TooltipGalleryadded. Barrel export:src/components/tooltip/verdify_tooltip.dart. -
VerdifyTextarea (Phase-3 assembly): multi-line text field for free text (notes, reasons, descriptions) when the answer runs to more than one line (spec §1). Anatomy (spec §2): label (required, accessible name via
InputDecoration.labelText) + multi-line field + optional placeholder (hint, never the label) + optional description + optional character counter + optional error. Variants (spec §3):resizevertical(default)/none/autoGrow (autoGrow:maxLines: nullso the field grows with content);counter(showCounter: truewithmaxLength). States (spec §4): default · hover (border darkens) · focus (2 pxcolorBorderFocusring) · disabled (enabled: false— removed from tab order) · error (errorText—colorStatusCriticalBorder+colorStatusCriticalFg) · read-only (readOnly: true— value stays selectable, stays tabbable). Keyboard (spec §6): Enter inserts a newline; Tab moves focus OUT — not trapped, never inserts a tab character; all platform text shortcuts pass through. A11y (spec §7): accessible name fromlabelText(neverhintText— the placeholder analog); error viaInputDecoration.errorTextso Flutter includes it in the semantics tree (aria-invalid + describedby analog); counter is a plainTextbelow the field (aria-live="polite" analog);enabled: falseremoves from tab order;readOnly: truekeeps field tabbable. Material defaults overridden: cursorcolorBorderFocus, stylecolorControlFg, fillcolorControlBg, all four border states (enabled/focused/error/focusedError/disabled). Counter uses own token-boundText(Material'scounterTextleaksColorScheme). Token bindings (spec §5, zero raw colors): control-bgcolorControlBg; resting bordercolorControlBorder; hover bordercolorBorderStrong; focused bordercolorBorderFocus; error bordercolorStatusCriticalBorder; error textcolorStatusCriticalFg; textcolorControlFg; placeholdercolorControlPlaceholder; label/descriptioncolorTextSecondary; countercolorTextMuted; disabledcolorTextDisabled; focus ringcolorBorderFocus/focusRingWidth; cursorcolorBorderFocus; radiusradiusMd; DEC-A text rolev.text.body; tap-floortargetMobile. 21 component tests + 1 golden (11 scenarios). Gallery section_TextareaGalleryadded toexample/lib/main.dart. Barrel export:src/components/textarea/verdify_textarea.dart. -
VerdifySwitch (Phase-3 assembly): toggles a single setting on/off with immediate effect; no Save step (spec §1). Anatomy (spec §2): track (rounded, changes color on/off) + thumb (slides) + label (required, accessible name) + optional description. Variants (spec §3):
sizemd/sm;labelPlacementbefore(default)/after. States (spec §4): off/on (track color changes), hover, pressed, focus (2 px ring viaBoxShadow(colorBorderFocus, spreadRadius: focusRingWidth)), disabled (DEC-C: label/thumb viacolorTextDisabled, never blanket opacity), loading (spinner in thumb, blocks toggling; Flutter limitation: no directaria-busy— document at consumption). Keyboard (spec §6): Tab/Shift+Tab navigate; Space and Enter toggle viaShortcuts→Actions→ActivateIntent(mandatory — copy of Checkbox pattern). A11y (spec §7):Semantics(toggled:, hasToggledState:, enabled:, label:)→ role=switch + checked-state;MergeSemanticsfor one a11y node; tap-target floorv.targetMobile(44 px). Thumb slide + track color animated withmotionDurationFast+motionEasingVerdify; reduced-motion guard (VerifiedBadge seed pattern). Token bindings (spec §5, zero raw colors): track-offcolorControlBg/colorControlBorder; track-oncolorActionPrimaryBg/BgHover/BgActive/Border; thumbcolorControlFg; labelcolorTextPrimary; descriptioncolorTextSecondary; disabledcolorTextDisabled; focuscolorBorderFocus/focusRingWidth; tap-floortargetMobile; radiusradiusFull; motionmotionDurationFast/motionEasingVerdify. 17 component tests + 1 golden (11 scenarios). Gallery section_SwitchGalleryadded toexample/lib/main.dart. Barrel export:src/components/switch/verdify_switch.dart. -
VerdifyTable (semantic data table, Phase-3 assembly): presents structured data in rows and columns for reading, comparing, and sorting (spec §1). API:
columns: List<VerdifyTableColumn>(key, header, sortable, onSort, flex) +rows: List<VerdifyTableRow>(id, cells map, selected);caption,density(comfortable/compact),rule(horizontal/grid/zebra),selectable/selectedIds/onSelectionChanged,sortedColumnKey/sortAscending,loading,emptyMessage,errorMessage. Core shipped (spec §3): density comfortable/compact, rule horizontal/grid/zebra, sortable headers (per-column toggle ascending/descending viaonSort), selectable rows (leading compact checkbox + select-all in header). Deferred / documented partials:sticky-header(FlutterTablelayout primitive requires custom overlay for fixed-header scroll — deferred post-P3; DataGrid is the right home for this complexity);row-actionstrailing cell (API stable for the add);tfoot(callers may append a footer widget below). Selection cells use an internal_TableCheckbox(box-only, no visible text label) to avoid overflow in the fixed 48px selection column — the accessible name ('Select all rows'/'Select row N') is carried by theSemanticslabel. A11y (spec §7, Flutter limitation): Flutter'sTableis a layout primitive with no native HTML<table>/<th scope>semantics; column headers wrapped inSemantics(header: true); caption accessible as visible text; sortable header button labeled with direction ("Sort Name, ascending");_TableCheckboxusesMergeSemantics(Semantics(checked:, mixed:, label:)); status cells use text + icon, never color alone. Sort direction: caret icon (Icons.arrow_upward/downward) + accessible label, never color-only. Empty:colorTextSecondarymessage. Error:colorStatusCriticalFgmessage paired with text. Loading: 3VerdifySkeleton.textrows. Row hover:colorSurfaceRaisedfill (no motion in table rows — noAnimatedContainer, plainBoxDecoration). Token bindings (spec §5, zero raw colors): canvascolorSurfaceCanvas; raisedcolorSurfaceRaised; header labelcolorTextSecondaryatv.text.label; cell textcolorTextPrimaryatv.text.body; rulescolorBorderDefault; sortable label/iconcolorActionGhostFg; sortable hovercolorActionGhostBgHover; focus ringcolorBorderFocus/focusRingWidth; tap floortargetMobile; errorcolorStatusCriticalFg. 16 component tests + 1 golden (8 scenarios, reduced-motion host). Gallery section added. Barrel export:src/components/table/verdify_table.dart. -
VerdifyAvatar (identity image placeholder, Phase-3 assembly): a small non-interactive image that stands in for an identity in lists, headers, and threads (spec §1). Anatomy (spec §2):
containerclipsimageorfallback(initials or generic glyph);adornmentslot is external (not part of Avatar). Variants (spec §3):shapecircle (default) / rounded;sizesm/md(default)/lg;showBorderhairlinecolorSurfaceBorderMuted. States (spec §4): default (image) / loading (VerdifySkeleton.circleor.block, reduced-motion guard viaMediaQueryData(disableAnimations:true)host in goldens) / fallback (initials from_initials()— up to two chars — orIcons.personwhen name is empty). Non-interactive: no focus, no tap, no hover/disabled (spec §6). A11y (spec §7): informative →Semantics(image: true, label: displayName)(the initials inside areExcludeSemantics— AT hears the name, never the letters);decorative: true→ExcludeSemantics; loading → skeleton (alreadyExcludeSemanticsinsideVerdifySkeleton). Image loaded via FlutterImage(imageProvider, loadingBuilder, errorBuilder)— error triggerssetState(() => _imageError = true)→ fallback. Token bindings (spec §5, zero raw colors): fallback fillcolorSurfaceRaised; initials/glyphcolorTextSecondary; bordercolorSurfaceBorderMuted; radiusradiusFull(circle) /radiusMd(rounded). 15 component tests + 1 golden (9 scenarios, reduced-motion host). Gallery rows added. Barrel export:src/components/avatar/verdify_avatar.dart. -
VerdifyPagination (nav landmark + page controls, Phase-3 assembly): splits a long result set into numbered pages with Prev, page-number run, and Next (spec §1). Variants (spec §3):
numbered(default — windowed page buttons ±2 around current + ellipsis for long ranges),prevNext("Page m of n" readout between Prev/Next for compact surfaces); sizessm/md/lg. States (spec §4): Prev/Next disabled at edges (non-operable,colorTextDisabled,Semantics(enabled: false)); current page non-interactive accent fill (colorActionPrimaryBg/colorActionPrimaryFg— brand ≠ state: this is a position marker, never the verified green); enabled controls hover/pressed/focus (2 px ring). Keyboard (spec §6): Tab visits each enabled control; Enter/Space activate viaShortcuts→Actions→ActivateIntent. A11y (spec §7):Semantics(container: true, label: 'Pagination', explicitChildNodes: true)landmark; Prev/NextSemantics(button:, enabled:, label: 'Previous page'/'Next page'); page buttonsSemantics(button:, label: 'Go to page N'); current page labeled 'Page N, current' (aria-current analog); ellipsisExcludeSemantics. Token bindings (spec §5, zero raw colors): restcolorTextSecondary; hovercolorActionGhostBgHover; Prev/Next iconcolorActionGhostFg; current fillcolorActionPrimaryBg, labelcolorActionPrimaryFg; disabledcolorTextDisabled; focuscolorBorderFocus/focusRingWidth; tap floortargetMobile; cornerradiusMd. 17 component tests + 1 golden (6 scenarios). Gallery rows added. Barrel export:src/components/pagination/verdify_pagination.dart. -
VerdifyBreadcrumb (nav landmark + trail, Phase-3 assembly): navigation landmark showing the current-page hierarchy and letting users step back up it (spec §1). Anatomy (spec §2):
Semantics(container: true, label: 'Breadcrumb', explicitChildNodes: true)landmark →RowofVerdifyBreadcrumbItemlink items interleaved with_Separatorglyphs (eachExcludeSemantics/ aria-hidden) →_CurrentItemplain text. Variants (spec §3):default(full trail),with-root-icon(supplyiconon root item),collapsed(collapsed: true— middle items behind ellipsis; full overflow-Menu deferred, documented). States (spec §4): link items support hover (colorActionGhostBgHoverfill +colorTextPrimarylabel) / pressed / focus (2 pxcolorBorderFocusring) / disabled (colorTextDisabled, excluded from Tab order viaExcludeFocusTraversal). Current page is plain non-interactive text. Keyboard (spec §6): Tab visits each enabled link item in trail order; Enter and Space activate viaShortcuts→Actions→ActivateIntent; disabled items skipped. A11y (spec §7): landmark labeled 'Breadcrumb'; link items areSemantics(button: true, enabled:); current page has no button role; separatorsExcludeSemantics. Token bindings (spec §5, zero raw colors): link restcolorTextSecondary; hover fillcolorActionGhostBgHover; hover/active textcolorTextPrimary; currentcolorTextPrimary; separatorcolorTextMuted; disabledcolorTextDisabled; focus ringcolorBorderFocus/focusRingWidth; tap floortargetMobile; radiusradiusSm. 14 component tests + 1 golden (5 scenarios). Gallery rows added. Barrel export:src/components/breadcrumb/verdify_breadcrumb.dart. -
VerdifyAgentBadge (AI-agent actor marker, Phase-3 assembly): non-interactive
StatelessWidgetthat marks an actor as an AI agent (spec §1). MirrorsVerdifyBadgestructure (same non-interactive template). Variants (spec §3): neutral (default, neutral surface palette — never action or status tier), caution (agent needs attention), critical (agent failed/revoked). Status color is ALWAYS paired with label text naming the state — human/agent distinction NEVER rests on color alone (WCAG 1.4.1, spec §8). Never-nameless assert: icon-only markers requiresemanticLabel. A11y (spec §7): visible label is accessible name (no role); iconExcludeSemantics(aria-hidden); icon-only →Semantics(image: true, label: semanticLabel). No focus, no focus ring, no tap-target floor (not interactive). Token bindings (spec §5, zero raw colors, zero action/verified-status tokens — brand != state): neutralcolorSurfaceRaised/colorSurfaceBorderMuted/colorTextSecondary; cautioncolorStatusCaution{Bg,Border,Fg}; criticalcolorStatusCritical{Bg,Border,Fg};radiusFullpill;v.text.caption. 18 component tests + 1 golden (8 scenarios). Gallery rows added toexample/lib/main.dart. Barrel export:src/components/agent_badge/verdify_agent_badge.dart. -
VerdifySeparator (structural divider, Phase-3 assembly): non-interactive
StatelessWidgetthat draws a 1px rule dividing content groups (spec §1). Variants (spec §3):orientationhorizontal (default) / vertical;decorative(removes from a11y tree viaExcludeSemantics);with-label(centered text breaks rule into two segments). A labeled separator is ALWAYS semantic —decorativeis ignored whenlabelis set (spec §3). A11y (spec §7): decorative →ExcludeSemantics(role="none" analog); semantic →Semantics()container; labeled →Semantics(label: label, excludeSemantics: true)so the label is the accessible name (mirrors React'saria-labelon the separator div). Flutter limitation documented: Flutter stable has noSemanticsRole.separator— the closest analog is a plainSemantics()container. No focus, no focus ring, never a tab stop. Token bindings (spec §5, zero raw colors): rulecolorBorderDefault; label textcolorTextSecondaryatv.text.caption. Layout spacing is plainEdgeInsetsconstants (not tokens — spacing is not emitted to Flutter). 15 component tests + 1 golden (5 scenarios: horizontal semantic, horizontal decorative, labeled, vertical, semantic-between-sections). Gallery rows added toexample/lib/main.dart. Barrel export:src/components/separator/verdify_separator.dart. -
VerdifyLabel (form-control label, Phase-3 assembly): non-interactive
StatelessWidgetthat names its associated control (spec §1). Three variants (spec §3): default (label text incolorTextPrimary), required (decorative*glyph inExcludeSemanticspaired with a visually-hidden "required" word so meaning is NEVER color alone — WCAG 1.4.1), optional (colorTextSecondaryhint). Disabled state: label renders incolorTextDisabledto reflect the control's state (DEC-C). A11y (spec §7): no focus, no focus ring, never a tab stop; visible label text is the accessible name in traversal order; Flutter limitation documented — no native<label for>association; callers must pass the same label text to the control's ownSemantics(label:)orVerdifyTextField(label:). Token bindings (spec §5, zero raw colors): label textcolorTextPrimary/ disabledcolorTextDisabled; optional hintcolorTextSecondaryatv.text.caption; required markcolorStatusCriticalFg; label type rolev.text.label. 13 component tests + 1 golden (5 scenarios: default, required, optional, disabled, required+disabled). Gallery rows added toexample/lib/main.dart. Barrel export:src/components/label/verdify_label.dart. -
VerdifySkeleton (loading placeholder, Phase-3 assembly): non-interactive
StatefulWidgetwith named constructors for each variant:VerdifySkeleton.text(width, height),.block(width, height),.circle(diameter).VerdifySkeletonGroupcomposes shapes into a silhouette (card, list row) with_kGap(8 px) between children. Shimmer: aCustomPaintersweeping acolorSurfaceRaisedband across thecolorSurfaceInputbase, driven bymotionDurationAmbient+motionEasingAmbient. Reduced-motion guard (VerifiedBadge seed pattern): controller never started whenMediaQuery.disableAnimationsis true — static neutral shape, no pending frames. A11y (spec §7):ExcludeSemanticswraps every shape — the skeleton is entirelyaria-hidden; the owner announces loading viaaria-busy/live-region (Flutter limitation documented:aria-busyhas no direct Flutter API; callers should wrap the skeleton's container inVerdifyLiveRegionand announce viaannounceToAssistiveTechnology). Token bindings (spec §5, neutral surface only — zero action/status colors, zero raw color literals): basecolorSurfaceInput; shimmercolorSurfaceRaised; block/group hairline bordercolorSurfaceBorderMuted; text cornerradiusSm; block cornerradiusSm(≤40 px height) orradiusMd(>40 px); circleradiusFull; shimmermotionDurationAmbient+motionEasingAmbient. 12 component tests + 1 golden (6 scenarios: text full/short, block, circle, group silhouette, reduced-motion static). Gallery rows added toexample/lib/main.dart. Barrel export:src/components/skeleton/verdify_skeleton.dart. -
VerdifyProgress (task-advancement bar, Phase-3 assembly): non-interactive
StatefulWidgetwith determinate (fill = value 0–1) and indeterminate (moving indicator) variants. [label] required (Progress is never unlabeled). Optional [valueText] (determinate-only), [description], and [error] (critical border + message text — never color alone). Reduced-motion guard (VerifiedBadge seed pattern): indeterminate controller never starts whenMediaQuery.disableAnimationsis true; determinate usesDuration.zerofill transition. A11y (spec §7):VerdifyLiveRegioncontainer (politerole="status"analog);Semantics(label:, value:)exposes the task name + progress value to AT (determinate only; indeterminate omits value per spec — noaria-valuenow); error text in semantics tree (Flutteraria-invalidlimitation documented). Token bindings (spec §5, zero raw colors): trackcolorControlBg/colorControlBorder; fill/indicatorcolorActionPrimaryBg; error bordercolorStatusCriticalBorder; error textcolorStatusCriticalFg; labelcolorTextPrimary; value-text + descriptioncolorTextSecondary; radiusradiusFull; determinate fill motionmotionDurationFast+motionEasingVerdify; indeterminatemotionDurationAmbient. Implementation note:FractionallySizedBox/AnimatedFractionallySizedBoxused instead ofLayoutBuilder(the latter doesn't support intrinsic dimensions — crashes in Alchemist golden host). 14 component tests + 1 golden (6 scenarios). Gallery rows added toexample/lib/main.dart. Barrel export:src/components/progress/verdify_progress.dart. -
VerdifySpinner (unmeasured-wait indicator, Phase-3 assembly): non-interactive
StatefulWidgetwith a continuously-rotating arc onmotionDurationAmbient+motionEasingAmbient. Sizessm/md/lg(16/20/32 px indicator). Reduced-motion guard (VerifiedBadge seed pattern): whenMediaQuery.disableAnimationsis true theAnimationControlleris never started — the arc renders as a static glyph with no pending frames. A11y (spec §7): indicator isExcludeSemantics(decorative); container isVerdifyLiveRegion(Semantics(liveRegion: true)— therole="status"analog); accessible name from visiblelabeloraccessibleLabel; Flutter limitation: no directaria-busyAPI — state is carried by the live-region label. Token bindings (spec §5, zero raw colors): arccolorActionPrimaryBorder(Sovereign Violet on surface — NOTcolorActionPrimaryFgwhich is near-white text-on-violet); trackcolorBorderDefault; labelcolorTextPrimaryatv.text.caption; motionmotionDurationAmbient+motionEasingAmbient. Gallery row added toexample/lib/main.dart. Barrel export:src/components/spinner/verdify_spinner.dart. 11 component tests + 1 golden (6 scenarios: sm/md/lg no-label, md/lg with-label, reduced-motion static). -
foundation/live_region.dart(live-region foundation, Phase-3 promotion): promoted fromVerdifyToastRegion's inlineSemanticsService.sendAnnouncementat the second consumer (VerdifyAlert), per the build-on-brand-flutter extract-at-second-use rule. Exposes two primitives:announceToAssistiveTechnology(context, message, {assertive})— wrapsSemanticsService.sendAnnouncement(View.of(context), …, assertiveness: assertive ? Assertiveness.assertive : Assertiveness.polite)— andVerdifyLiveRegion— a pure-semanticsStatelessWidgetwrapper addingSemantics(liveRegion: true)to its subtree. Toast refactored to consumeannounceToAssistiveTechnology(8/8 Toast tests stay green). 2 focused foundation unit tests (VerdifyLiveRegionrenders child + setsSemanticsFlag.isLiveRegion). Not exported from the public barrel (internal foundation, imported by consumers viasrc/foundation/). Zero raw color literals. -
VerdifyAlert (inline status message, Phase-3 assembly): an in-page message that holds its layout position until the condition changes or the reader dismisses it (spec §1). Anatomy (spec §2):
rootbordered container →icon(required, fixed per variant, decorativeExcludeSemantics) → optionaltitle→body(required) → optionalactions(≤ 2VerdifyButtoncontrols, composed) → optionaldismissicon-button (present whenonDismissis supplied). Variants (spec §3):verified/signal/caution/critical. States (spec §4): default only on the container; actions and dismiss carry their own interactive states via composed widgets. A11y (spec §7): container wrapped inVerdifyLiveRegion(Semantics(liveRegion: true)); on first appearance (indidChangeDependencies, post-frame) firesannounceToAssistiveTechnology— assertive forcritical(role=alert), polite for all others (role=status); status conveyed by icon + text, never color alone (WCAG 1.4.1) — the icon isExcludeSemantics, the variant meaning lives in the body/title text; dismiss icon-button has accessible name "Dismiss" (MergeSemantics+Semantics(button:, label: 'Dismiss')); alert container is not a tab stop (spec §6) — only composed actions/dismiss are focusable; dismiss tap-target floor isv.targetMobile(44 px). Token bindings (spec §5, zero raw colors, all SPEC-correct): per-variant container tintcolorStatus{Variant}Bg, bordercolorStatus{Variant}Border, icon colorcolorStatus{Variant}Fg; titlecolorTextPrimaryatv.text.h3; bodycolorTextPrimaryatv.text.body; dismiss glyphcolorActionGhostFg; cornerv.radiusMd. 16 component tests (body required, icon per variant, title optional, dismiss fires + named "Dismiss", dismiss absent when null, action fires, actions absent, color-not-alone text check per variant, liveRegion Semantics, critical liveRegion, polite variants not critical, not-a-tab-stop, iOS tap-target + labeled-tap-target, WCAG AA text contrast per variant). Golden (4 scenarios: verified body-only, signal title+body, caution title+body+dismiss, critical title+body+action+dismiss). Gallery section added toexample/lib/main.dart. Barrel export:src/components/alert/verdify_alert.dart. Flutter limitation note:aria-describedbyfield-error association is a Web ARIA concept; in Flutter, pair the alert body text with the field'serrorText(e.g.VerdifyTextField(errorText:)) — both surfaces state the same error, and AT reads both in traversal order. No Flutter API equivalent exists foraria-describedby; document this in the spec if wiring is expected. -
VerdifySelect (anchored-overlay + roving + type-ahead consumer, P3.15): pick ONE option from a known, finite list (spec §1) — a closed combobox trigger in the tab order showing the value or placeholder, opening a listbox popup of option rows (optional leading icon + a NEUTRAL selected check) with optional group headings + optional help/error text. Controlled API:
label(required, visible + associated — never replaced by the placeholder),options: List<VerdifySelectOption>(value + label + optionalicon+ optionalgroup+disabled),value(selected option's value, or null) +onChanged,placeholder,description,error,enabled,loading,size(sm/md/lg) andwidth(auto/full). Built on the shared foundations: the listbox is aVerdifyAnchoredOverlay(autoFlip: true— a trigger near the screen edge opens the other way; NON-modal — the page behind stays live) and option navigation is aVerdifyRovingGroup(vertical roving tabindex,wrap: falseper spec §6 — Down/Up move the active option by one with no wrap; Home/End jump to first/last enabled; disabled options skipped; the cursor seeds to the selected option or the first enabled) + the sharedVerdifyTypeAhead(printable keys jump to the next matching label). Focus model — the focus-moving APG variant (documented decision): select.md §7 describes thearia-activedescendantmodel (DOM focus stays on the trigger, the active option tracked viaaria-activedescendant). Flutter has noaria-activedescendant; the idiomatic + testable equivalent is the OTHER documented APG select-only-listbox variant — focus MOVES into the listbox (roving cursor on the active option) on open and RETURNS to the trigger on close, exactly asVerdifyMenudoes and exactly as the ReactSelect(radix-ui) does (see select.tsx's §7 focus-model deviation note + the React test "uses the focus-moving APG variant"). Both are documented APG variants; the keyboard model (incl. type-ahead), visible focus, name/role/value, and the target-size floor all hold. Sizes (spec §3, DEC-B): type role + vertical padding density ABOVE the shared target-size floor, never a fixed height below it — the option row's type role climbs caption → body → body-lg; the TRIGGER value, as a form field (DEC-A), holds the 16 px no-zoom font size on every size and climbs its role through the brand line-height + letter-spacing only. States: default · hover (trigger lifts to the secondary hover fill) · pressed · focus (2 px ring that PERSISTS while open) · open · disabled (out of the tab order,colorTextDisabled) · error (strong border + critical error text) · loading (non-blocking busy spinner in place of the chevron). Keyboard (spec §6): closed Enter/Space/Down/Up/Home/End open the listbox; open Enter/Space/tap commit the active option, close, and return focus to the trigger; Tab commits the active option then proceeds; Escape closes without changing the value and returns focus; type-ahead works while open. A11y (APG select-only combobox + listbox, spec §7): trigger is a combobox (Semantics(button: true)+expandeddisclosure state + the label-plus-value as its accessible name, never the placeholder alone, folded viaMergeSemantics); each option isMergeSemantics+Semantics(inMutuallyExclusiveGroup: true, selected:, enabled:, label:); group labels areheadernodes; a disabled trigger reportsaria-disabledand never opens;meetsGuideline(iOSTapTargetGuideline / labeledTapTargetGuideline / textContrastGuideline)on the trigger + the open rows. Token bindings (zero raw colors, control-* tier + neutral surface per spec §5; each verified SPEC-correct): trigger bgcolorControlBg, bordercolorControlBorder(rest),colorBorderStrong(error — §5),colorBorderFocus(focus/open — §5), hover fillcolorActionSecondaryBgHover, valuecolorControlFg, placeholdercolorControlPlaceholder, chevron/spinnercolorControlFg; listboxcolorSurfaceRaised+colorSurfaceBorder+v.shadowMd+v.radiusMd; option labelcolorTextPrimary, active (highlighted) fillcolorActionSecondaryBgHover(§5 — note this differs from Menu'scolorActionGhostBgHover), selected checkcolorTextPrimary(a NEUTRAL mark, never a status color — §8), disabled optioncolorTextDisabled(DEC-C); group labelcolorTextMutedatv.text.label; error-slot textcolorStatusCriticalFg(spec §5; note the React Select usesstatus-critical-on-surface— Flutter binds the SPEC token); descriptioncolorTextSecondaryatv.text.caption; open fadev.motionDurationBase+v.motionEasingVerdify(instant under reduced motion — never the deliberate check duration). 18 component tests (placeholder→value, combobox + expanded, open/list, select-updates-closes-returns-focus, Down/Up roving + Enter commit, Home/End, no-wrap, skip-disabled, type-ahead, Escape no-change + return-focus, disabled non-open +aria-disabled, error-slot text +colorStatusCriticalFg, group label, selected-optionaria-selected, guidelines) + a 2-group golden (select_closedsm/md/lg + selected + error + disabled;select_openthe open listbox). Gallery section added toexample/lib/main.dart. Barrel exports the public API (VerdifySelect,VerdifySelectOption,VerdifySelectSize,VerdifySelectWidth) via ashowclause. -
VerdifyMenu (anchored-overlay + roving + type-ahead consumer, P3.15): a popup list of ACTIONS a trigger opens (spec §1) — fires a command then closes. Declarative entry model (
VerdifyMenuEntrysealed family):VerdifyMenuItem(label + optional leadingicon+ optional trailingshortcuthint +destructive/disabled+onSelected),VerdifyMenuGroup(agroup-labelheading over a set of items),VerdifyMenuSeparator(a decorative divider; rendered byVerdifyMenuSeparatorWidget), andVerdifyMenuSubmenu(a row that opens a nested popup of its own items). Built on the shared foundations: the popup is aVerdifyAnchoredOverlay(autoFlip: true— a menu near the screen edge opens the other way; NON-modal — the page behind stays live, focus is not trapped, Tab leaves) and item navigation is aVerdifyRovingGroup(vertical roving tabindex: Up/Down between enabled items, wrapping; Home/End to first/last enabled; disabled rows + group labels + separators skipped). Type-ahead uses the sharedVerdifyTypeAheadfoundation (a timed keystroke buffer, ~700 ms reset, that yields the next enabled item whose label starts with the typed string, skipping disabled — promoted from this widget's former inline buffer at Select, the second consumer). Reaching the roving cursor from the type-ahead key handler required a small foundation seam:VerdifyRovingController(added toroving_focus.dart;moveCursorTo(index)+cursor) so an external handler moves the cursor while keeping it consistent with arrow navigation (Select reuses it). A11y (APG menu-button + menu, spec §7): trigger is a menu-button (Semantics(button: true)+expandeddisclosure state, folded viaMergeSemantics); each item/submenu row is amenuitem(MergeSemantics+Semantics(button:, enabled:, label:)); a disabled item reportsaria-disabledand is non-firing; group labels areheadernodes, separators areExcludeSemantics; Escape closes and returns focus to the trigger (aFocusScopearound the caller's control); selecting an item runs its callback and closes;meetsGuideline(iOSTapTargetGuideline / labeledTapTargetGuideline / textContrastGuideline)on the trigger + the open rows. Submenu (spec §2): aVerdifyMenuSubmenurow opens its own nestedVerdifyAnchoredOverlayto the side (Right / tap / Enter / Space opens; Escape / outside-tap closes and returns focus to the row); activating a nested leaf fires it and closes the whole menu. Token bindings (zero raw colors, neutral surface + destructive-as-the-one-color per spec §5): popupcolorSurfaceRaised+colorSurfaceBorder+v.shadowMd+v.radiusMd; item labelcolorTextPrimaryatv.text.body, active/hover fillcolorActionGhostBgHover; destructive item label/iconcolorActionDestructiveFg+ active fillcolorActionDestructiveBg; disabledcolorTextDisabled(DEC-C, no blanket opacity); group label + shortcut hintcolorTextMutedatv.text.label; separatorcolorBorderDefault; open fadev.motionDurationFast+v.motionEasingVerdify(instant under reduced motion — never the deliberate check duration). 16 component tests (open/render, menu-button + expanded, tap-activate-and-close, Down/Up roving + skip-disabled, Home/End, wrap, type-ahead match + skip-disabled, Escape + return-focus, disabled non-fire +aria-disabled, groups/separator, destructive color, submenu open + nested activate, guidelines) + a 2-scenario auto-open golden (menu_variants). Gallery row added toexample/lib/main.dart. Barrel exports the public API via ashowclause (internals stay private). -
foundation/type_ahead.dart—VerdifyTypeAhead(type-ahead foundation, P3.15): promoted fromVerdifyMenu's inline_onTypeAheadat the second consumer (Select), per the build-on-brand-flutter extract-at-second-use rule. The reusable timed-keystroke nucleus for any list control whose printable keys jump the cursor to the next matching item (Menu, Select, future listbox/combobox). It owns ONLY the buffer + reset-timer mechanism and the match math — never any focus, roving cursor, or widget tree:handle(char, matchStrings:, from:, isDisabled:)appends the lower-cased char to the buffer, (re)starts aresetDelaytimer (defaultdefaultResetDelay= 700 ms), and returns the next index inmatchStringswhose valuestartsWiththe buffer (case-insensitive), skipping anyisDisabledindex and wrapping past the end (-1when nothing matches). Match policy (WAI-ARIA type-ahead): a single repeated char CYCLES (the query collapses to that one char and the search advances from just pastfrom, so pressingbrepeatedly walks everyb…item); a mixed multi-char buffer REFINES (the query is the full buffer and the search starts ATfrom, narrowing the current match); with no cursor yet (from < 0) the search starts at the first item (offset 0) so the first keystroke lands on the first match, not the second.VerdifyMenunow consumes it (all 18 Menu tests stay green, including type-ahead match + skip-disabled). 12 focused foundation unit tests (single-char-next, repeated-char cycle + wrap, multi-char refine, longer-buffer refine-to-later, case-insensitive, skip-disabled, no-match-1, empty-list-1, buffer-resets-after-delay viafakeAsync, fast-second-char-holds, default-delay constant, dispose-cancels-timer). Not exported from the public barrel (internal foundation, imported by consumers viasrc/foundation/). Binds no colors (pure logic); the gate scanslib/src/foundationregardless. -
foundation/anchored_overlay.dart—VerdifyAnchoredOverlay+AnchoredSide(anchored-overlay foundation, P3.15): promoted fromVerdifyPopover's private_AnchoredOverlayat the second consumer (Menu), per the build-on-brand-flutter extract-at-second-use rule. The reusable NON-MODAL positioning + light-dismiss nucleus shared by every overlay whose panel floats next to a trigger and leaves the page behind LIVE (Popover, Menu, Tooltip, Select). ALayerLinkjoining aCompositedTransformTarget(trigger) to aCompositedTransformFollower(panel) inside anOverlayPortal; theside→ target/follower anchor + gap-offset map; aTapRegionlight-dismiss listener that observes outside taps WITHOUT swallowing them (the panel + trigger share agroupIdso a tap on either is not "outside"); anUnconstrainedBoxaligned to the resolved follower anchor so the panel sizes to its own content (else it fills the theater and swallows every tap, breaking the non-modal contract). Popover-review refinements applied: (1) thePopoverSide-typed param became the sharedAnchoredSideenum (top/right/bottom/left, withopposite/isVertical) so Menu/Tooltip/Select position a panel without importing Popover — Popover keepstypedef PopoverSide = AnchoredSideso its public API is unchanged; (2) auto-flip on viewport overflow (autoFlip, default OFF) — reads the trigger's global rect (off aGlobalKeyrender box, more reliable than theLeaderLayer's parent-relative offset) + the panel's measured size + the view size, and flips toAnchoredSide.oppositewhen the preferred side lacks room and the opposite has more (Popover keeps it OFF as its YAGNI default; Menu turns it ON). API:link,controller,side,barrierDismissible,onDismiss,overlayBuilder,child,autoFlip,gap.VerdifyPopovernow consumes it (all 12 Popover tests + golden stay green). 9 foundation tests: parameterized all-four-sides anchor mapping, auto-flip-at-edge, no-flip-with-room, autoFlip-off-stays, outside-tap dismiss, and theAnchoredSide.opposite/isVerticalenum invariants. Not exported from the public barrel (internal foundation, imported by consumers viasrc/foundation/). Binds no colors (positioning only); the gate scanslib/src/foundationregardless. -
foundation/roving_focus.dart—VerdifyRovingGroup(roving-focus foundation, P3.15): promoted fromVerdifyTabs's private_RovingTabListat the second consumer (Radio), per the build-on-brand-flutter extract-at-second-use rule. The reusable CORE keyboard + focus mechanism for any container that is ONE stop in the page tab order with arrow-key cursor movement (Tabs, Radio, Menu, Select, Sidebar). It owns ONLY the mechanism — aFocusNodeper item, aFocusTraversalGroup(ReadingOrderTraversalPolicy)+ per-itemExcludeFocusTraversal(the roving tabindex — only the cursor item is Tab-traversable), aShortcuts/Actionshandler for the orientation's arrow axis + Home/End (cross-axis arrows left UNBOUND so they bubble), wrap + skip-disabled navigation math, and the activation policy (manual = Enter/Space commit vs. selection-follows-focus = arrow commits). It owns NO chrome and NO layout: the caller supplies items viaitemBuilder(context, index, node, focused)and the Row/Column/scroll vialayoutBuilder(items), binding any visuals throughVerdifyThemeitself. Tabs-review refinements applied: the cursor seeds fromselectedonly when that index is enabled, else the first enabled item (so Tab never parks on a disabled/dead tab stop); an all-disabled group has no cursor (-1) and degrades gracefully — no tab stop, arrows are no-ops, no crash/hang. API:itemCount,isEnabled,orientation(Axis),wrap,manualActivation,selected(cursor seed),onActivate,itemBuilder,layoutBuilder,semanticLabel,debugLabelPrefix.VerdifyTabsnow consumes it (keeping its canvas/divider chrome + H/V layout + horizontal scroll in the Tabs file vialayoutBuilder); all 20 Tabs tests stay green. 16 focused foundation unit tests (arrows + wrap + Home/End + skip-disabled + manual-vs-auto + all-disabled + selected-on-disabled-seeds-to-firstEnabled). Not exported from the public barrel (internal foundation, imported by consumers viasrc/foundation/). Zero raw color literals (the gate scanslib/src/foundation). -
VerdifyRadio (interactive form control + first roving-foundation consumer, P3.15): a
radiogroupof mutually-exclusive options built onVerdifyRovingGroup. Anatomy (spec §2):VerdifyRadioGroup(group label + the roving group) +VerdifyRadioOptionvalue objects (value + label + optional per-option description +disabled); the control is an outer ring with a filled inner dot when selected. Variants (spec §3):standard(vertical stack — the "Default" variant),withDescription(secondarycaptionline per option),card(bordered selectable surface; restingcolorBorderDefault→ hovercolorBorderStrong→ selectedcolorActionPrimaryBg). States: default · hover · pressed · focus (2 px ring) · disabled; selection (checked/unchecked) is orthogonal and applies in every state. Roving (spec §6): one tab stop into the group landing on the selected option (or the first enabled option when nothing is selected); selection FOLLOWS focus — arrows move-and-select the next/prev enabled option, wrapping (VerdifyRovingGroupwithmanualActivation: falseandorientation: nullto bind ALL FOUR arrows per the radio APG — Down/Right next, Up/Left prev); Space selects the focused option; disabled options skipped. A11y (spec §7): groupSemantics(container:, label:); each optionMergeSemantics+Semantics(inMutuallyExclusiveGroup: true, checked:, enabled:, label:);meetsGuideline(iOSTapTargetGuideline / labeledTapTargetGuideline / textContrastGuideline). Token bindings (zero raw colors): unselected ring bordercolorControlBorder(hover/pressedcolorBorderStrong), control fillcolorControlBg, selected ring + inner dotcolorControlFg(neutral near-black ink per radio.md §5), disabled+selected dotcolorTextDisabled(notcolorControlFgbehind opacity — mirrors the React radio'sbg-text-disabledon the disabled+checked dot), focus ringcolorBorderFocus(BoxShadowspreadfocusRingWidth), labelcolorTextPrimary, descriptioncolorTextSecondary, disabled label/ringcolorTextDisabled(DEC-C, no blanket opacity), card bordercolorBorderDefault/colorBorderStrong/colorActionPrimaryBg, circleradiusFull, card cornerradiusMd; dot appear/disappear viaAnimatedScale(reducedMotion ? Duration.zero : v.motionDurationFast, v.motionEasingVerdify). Tap-target floortargetMobile(44 px). Note: Radio selection bindscolorControlFg(ink), intentionally distinct fromVerdifyCheckbox's brand-accent checked fill (colorActionPrimaryBg). A checkbox opt-in is a brand moment; a radio choice within a mutually-exclusive set is neutral (radio.md §5, also the React radio'speer-checked:border-control-fg). These are deliberately different — do not "reconcile" them. Gallery rows added toexample/lib/main.dart. -
VerdifyVerifiedBadge (motion-theatre exemplar + reduced-motion seed, P3.15): the ONE deliberate motion in the system — the verified check draws ONCE on first appearance via
CustomPainter+AnimationController(duration: v.motionDurationDeliberate)+v.motionEasingVerdify, sweeping stroke progress 0→1 viaPathMetric.extractPath; never loops or replays. Reduced-motion guard (seed for Skeleton/Spinner/Progress in Phase 3): whenMediaQuery.disableAnimationsis true,_controller.value = 1.0is set indidChangeDependenciesbeforeforward()is called — the check appears fully drawn on the first frame and the controller never ticks, leaving no pending timers. Variants:size=sm/md(caption/body type role; 16/20 px check). States: default (at rest), reveal (the draw moment), reduced-motion (draw suppressed). A11y (spec §7): check is decorative (ExcludeSemantics); standard variant wraps withMergeSemanticsso the label is the accessible name; icon-only (labelHidden=true) getsSemantics(image:true, label:ariaLabel)— mirrorsVerdifyBadgeicon-only pattern. Token bindings:colorStatusVerifiedBg/Fg/Border/Accent,radiusFull,motionDurationDeliberate,motionEasingVerdify— zero raw color literals. Gallery row added toexample/lib/main.dart. -
VerdifyToast (transient-queue + live-region exemplar, P3.15):
VerdifyToastController(ChangeNotifier, imperativeshow/dismiss/dismissAll/pause/resume; IDs from internal counter — no external deps; auto-dismissTimerstored per-entry; timer pauses on hover enter / resumes on hover exit using stored per-entry duration).VerdifyToastRegionhost (Stack overlay, bottom-right; announces to AT viaSemanticsService.sendAnnouncementon each new entry — assertive forcritical, polite otherwise; births the live-region pattern inline, promoted tofoundation/at second use in Phase 3).VerdifyToastsurface (slide+fade enter animation viav.motionDurationBase/v.motionEasingVerdify;Semantics(liveRegion: true); elevationv.shadowLg; bgcolorSurfaceRaised; status-accent border + icon + text — never color alone; optional single action; dismiss icon-button with tooltip). Variants:verified/signal/caution/critical. All color/shadow/motion bindings fromVerdifyTheme(zero raw literals). Gallery row added toexample/lib/main.dart. -
VerdifyPopover (non-modal anchored-overlay exemplar, P3.15): a floating panel a trigger opens next to itself, deliberately contrasted with the MODAL
showVerdifyDialog(Dialog =ModalRoute+ModalBarrier/BlockSemantics+ focus-trap; Popover = NON-modal, page stays live, focus NOT trapped). Anchored-overlay mechanism (the Phase-3foundation/anchored_overlay.dartextraction template): the private_AnchoredOverlayjoins aLayerLinkCompositedTransformTarget(trigger) to aCompositedTransformFollower(panel) inside anOverlayPortal; the overlay child is wrapped inUnconstrainedBoxso the panel sizes to its content (NOT the full theater — otherwise it absorbs every tap and breaks the non-modal contract); light dismiss is aTapRegion(onTapOutside:)listener that observes outside taps WITHOUT swallowing them, so the page behind stays hit-testable. API:trigger: (context, isOpen, toggle)builder (the one tab stop) +builder: WidgetBuilderpanel content +side(top/right/bottom/left → follower anchor + 6px gap) +showArrow+barrierDismissible+enabled. Subcomponents:VerdifyPopoverHeader(title + optional close + optional body),VerdifyPopoverTitle(h3/colorTextPrimary,MergeSemantics+header:trueso the titled panel reads as a labelled dialog-ish region),VerdifyPopoverBody(body/colorTextSecondary),VerdifyPopoverClose(neutral-ghost icon button, target floortargetMobile, invokesDismissIntent→ the panel'sActionsmaps it to close — same callback as Escape/toggle),VerdifyPopoverArrow(decorativeCustomPaintpointer,ExcludeSemantics). States: default · trigger hover/pressed/focus (the caller's control) · open (trigger exposesSemantics(expanded:)— the aria-expanded analog) · disabled (enabled:false, never opens). Keyboard: trigger toggles; on open focus moves into the panel (_panelFocusNode) without trapping (Tab leaves into the page — WCAG 2.1.2); Escape closes and returns focus to the trigger; click-away closes (light dismiss). Token bindings (spec §5, neutral surface only): panel fillcolorSurfaceRaised; bordercolorSurfaceBorder; cornerv.radiusMd; elevationv.shadowMd; titlecolorTextPrimary; bodycolorTextSecondary; close glyphcolorActionGhostFg+ hovercolorActionGhostBgHover; arrow fillcolorSurfaceRaised+ edgecolorSurfaceBorder. Open transition: a plain fade atv.motionDurationFast+v.motionEasingVerdify(_FadeIn), collapsed to instant underMediaQuery.disableAnimations(reduced motion) — never the deliberate check duration; exit is instant (controller-managed exit animation deferred with the auto-flip positioning YAGNI). A11y: trigger disclosure node carriesexpanded; title is anisHeadernode; close is a labelled button; arrow is AT-hidden;meetsGuideline(iOSTapTargetGuideline / labeledTapTargetGuideline / textContrastGuideline). NON-MODAL proven by tests: a background control stays reachable while the panel is open (unlike a modal Dialog), and a page control after the trigger is focusable while open (focus not trapped). Gallery rows added toexample/lib/main.dart. Golden auto-opens each variant into the overlay (popover_variants). -
VerdifyTabs (roving-focus exemplar, P3.15):
VerdifyTabsVariant(underline / pill) ×VerdifyTabsOrientation(horizontal / vertical) ×VerdifyTabsSize(md / sm) ×VerdifyTabsActivation(manual / automatic). Controlled API —tabs: List<VerdifyTab>(label + optional leading icon + optional trailing count + per-tabdisabled) +panels: List<Widget>+selectedindex +onChanged; one panel rendered at a time. Roving model: the tablist is ONE tab stop (FocusTraversalGroup+ReadingOrderTraversalPolicy), built as the self-contained private_RovingTabList(per-tabFocusNode,Shortcuts/Actionsfor arrow/Home/End, wrap-around_nextEnabledthat skips disabled tabs) — the extraction template for the Phase-3foundation/roving_focus.dart. Arrows move focus (Left/Right horizontal, Up/Down vertical); Home/End jump to first/last enabled; focus wraps; disabled tabs skipped. Manual mode (default): Enter/Space commit the focused tab; automatic mode: selection follows focus. States: default · hover (colorActionGhostBgHover) · pressed (no separate fill per spec §4) · focus (2 px ring viaBoxShadow(colorBorderFocus, spreadRadius: focusRingWidth), DISTINCT from selection) · selected (indicator +FontWeight.w600lift, not color alone) · disabled (colorTextDisabled, skipped in roving) · loading (panel busy spinner, tablist stays operable). A11y:Semantics(button:, selected:, label:)per tab; panel labelled by its tab;meetsGuideline(iOSTapTargetGuideline / labeledTapTargetGuideline / textContrastGuideline). Token bindings: selected indicator + pill fillcolorActionPrimaryBg; pill fgcolorActionPrimaryFg; selected textcolorTextPrimary; unselectedcolorTextSecondary; disabledcolorTextDisabled; hovercolorActionGhostBgHover; focus ringcolorBorderFocus; tablist/panel dividercolorBorderDefault; surfacecolorSurfaceCanvas; pill cornerv.radiusMd; indicator-slide viaAnimatedContainer(v.motionDurationFast, v.motionEasingVerdify). Horizontal tablist scrolls (spec §7 focus-not-obscured) when tabs exceed width. Gallery rows added toexample/lib/main.dart. -
VerdifyCheckbox (tri-state, P3.15):
VerdifyCheckboxVariant(standalone / parent) ×VerdifyCheckboxSize(sm / md). Anatomy: box + indicator (check glyph or indeterminate dash) + label + optional description + optional error-text. States: default · hover (border-strong) · pressed · focus (2 px ring viaBoxShadow(colorBorderFocus, spreadRadius: focusRingWidth)) · checked (action-primary fill + glyph) · indeterminate (parent-only resting state — dash glyph) · disabled (text-disabled, DEC-C fg-dim, no blanket opacity) · error (critical-border + critical-fg error text). Keyboard:SpaceandEntertoggle viaShortcuts/ActionswrappingFocusableActionDetector; indeterminate parent resolves tofalseon first Space. A11y:Semantics(checked:, mixed:)for tri-state;MergeSemanticsfolds label into the tappable node; tap-target floorv.targetMobile(44 px). Token bindings: box fillcolorControlBg/colorActionPrimaryBg; bordercolorBorderDefault/colorBorderStrong/colorStatusCriticalBorder; glyphcolorActionPrimaryFg; labelcolorTextPrimary/colorTextDisabled; descriptioncolorTextSecondary; errorcolorStatusCriticalFg; cornerv.radiusSm. Gallery row added toexample/lib/main.dart. -
VerdifyCredentialCard (compose-a-committed-control exemplar, P3.15): one row in the credential list — seeds the molecule/composition template. Composition discipline: composes committed primitives via public constructors ONLY —
VerdifyCard(static raised container),VerdifyButton(destructive remove),VerdifyCheckbox(selectable leading checkbox),VerdifyBadge(verified/primary status) — no internal duplication, no private reach-in; all focus rings, tap-target floors, keyboard models, and control animations come from those proven widgets. Anatomy: optional leading checkbox (selectable variant), optional kind icon (decorative,ExcludeSemantics), label (v.text.label+colorTextPrimary), identifier (v.text.mono+colorTextSecondary, wrapped inDirectionality(ltr)isolate for RTL-safe display of addresses/hashes/wallet strings), optional status badges (verified green / primary neutral viaVerdifyBadge), optional meta text (v.text.caption+colorTextMuted), required remove control (VerdifyButton.destructive), optional extra actions. Kind variants:email/phone/passkey/wallet/enterpriseSso. Selectable variant:VerdifyCheckbox(label: 'Select <credential>'). States: default, hover/pressed/focus (controls only — card surface is static), disabled (remove only, adjacent muted reason text), loading (remove showsCircularProgressIndicator, not re-triggerable — delegated toVerdifyButton(loading: true, onPressed: null)). A11y (spec §7): card is aSemantics(container: true)listitem; remove buttonsemanticLabelis credential-specific ("Remove email jordan@example.com"); kind + identifier reach AT as text, never icon alone; checkbox label tied to credential. Token bindings (zero raw colors): container viaVerdifyCardstatic (→colorSurfaceRaised/colorSurfaceBorder/shadowSm/radiusLg); labelcolorTextPrimary; identifiercolorTextSecondary; metacolorTextMuted; disabled-reasoncolorTextDisabled; status viaVerdifyBadge(→colorStatusVerified*/ neutral); remove viaVerdifyButton.destructive(→colorActionDestructive*); checkbox viaVerdifyCheckbox(→colorControl*). Deviation from plan: usesFlexible(loose) instead ofExpandedfor the content block so the widget works in Alchemist's intrinsic-width golden context without overflow. -
VerdifyCard (compound multi-slot, elevation consumer):
VerdifyCardHeader,VerdifyCardBody,VerdifyCardFootersub-widgets. Static variant = non-interactive grouping (v.shadowSm+colorSurfaceBorder). Interactive variant = button/link viaFocusableActionDetector+GestureDetector; hover lifts tov.shadowMd+colorBorderStrong; pressed settles tov.shadowSm; disabled bindsv.shadowNone+colorTextDisabled(DEC-C). Focus ring viaBoxShadow(color: colorBorderFocus, spreadRadius: focusRingWidth). Transitions:AnimatedContainer(v.motionDurationFast, v.motionEasingVerdify). Corner radius:v.radiusLg(spec §5--radius-lg). All elevation bindsv.shadow*— no hand-authoredBoxShadowlists.MergeSemanticsfolds header title into the interactive button node (WCAG 4.1.2). Gallery row added toexample/lib/main.dart. -
VerdifyButton, VerdifyTextField, showVerdifyDialog — the original foundation slice: the first three widgets, establishing the token-bound
VerdifyTheme, the brand focus ring, the 44 px tap-target floor, and the modal-overlay pattern the rest of the library builds on.
Fixed #
-
VerdifyRadio — selected ring/dot binds
colorControlFg(notcolorActionPrimaryBg): radio.md §5 and the React radio both specify--color-control-fg(neutral near-black ink) for the selected outer ring and inner dot. The original Flutter port incorrectly usedcolorActionPrimaryBg(Sovereign Violet). Fixed tocolorControlFgfor selected+enabled; disabled+selected dot correctly usescolorTextDisabled(mirroring the Reactbg-text-disabledon the disabled+checked dot — notcolorControlFgbehind opacity). This is intentionally distinct fromVerdifyCheckbox's brand-accent checked fill: a checkbox opt-in is a brand moment; a radio choice within a mutually-exclusive set is neutral. -
VerdifyBadge — icon-only branch now applies
IconTheme.mergeso the icon renders with the statusfgcolor instead of the ambient (black) color. -
VerdifyBadge — empty-label a11y assert strengthened:
label: ''withoutsemanticLabelnow fires the assert, closing a hole where an empty label passed the oldlabel != nullcheck but produced aSemantics(image: true, label: null)node. -
VerdifyBadge — pill radius:
BorderRadius.circularnow usesv.radiusFull(9999) instead ofv.radiusMd(8), matching spec §5 pill shape. Golden regenerated.