Phase 8: Supporting Atoms, Table Cells & Light Palette - #8
Conversation
- Add _pumpLight helper that wraps child in ThemeData.light() with ScaffoldPalette.lightPalette + ScaffoldDimens.defaultDimens - Add 6 tests asserting on-status color for label + icon sites under both defaultPalette and lightPalette, covering bright fills (lightGreenPrimary) and dark fills (statusError) - 3 tests fail (dark-on-bright), 3 pass coincidentally (white==white); existing 12 tests still pass — RED state for WIDG-46
- Hand-written _SampleItem + _DataColumnConfig test doubles mirror the
post-change template shape (label/accessor/sortable/cellBuilder)
- _CellHost mirrors the post-change cell render branch exactly:
cellBuilder != null ? Builder(...) : Text(item.accessor?.toString() ?? '')
- Three tests: null builder -> string fallback, non-null builder -> custom
Key('custom-cell') widget (no string Text), and argument capture proves
the builder receives the same BuildContext + item
- Template not yet modified (RED state): grep -c 'cellBuilder'
templates/components/data_table.dart.jinja2 == 0
Refs: WIDG-43, 08-CONTEXT D-08/D-09
- Twelve widget tests covering slot ordering, submission, focus ring, disabled state, surface tokens, and lightPalette rendering (WIDG-42) - RED state: lib/components/scaffold_composer.dart does not yet exist
…Group - 10 chip tests: default/selected/disabled/tap/icon+label/icon+label+status/icon-only-assert/icon-only-semantics/button+selected-semantics/lightPalette - 8 chip-group tests: empty-shrink/single-select-replace/multi-select-toggle/single-re-tap/selected-propagation/Wrap-spacing/radiogroup+group-semantics/lightPalette - WIDG-40 RED state: implementations do not yet exist; tests fail at compile time.
…ldTraceList - 9 disclosure tests: controlled/uncontrolled, tap-to-expand, chevron rotation (0.0/0.25), chevron tint (textSecondary/lightGreenPrimary), Semantics expanded flag + label, light-palette body indent, reduced-motion - 6 trace-list tests: empty shrink, ordering, groupHeader titleSmall + space12 top padding, space8 separation, light-palette render, leading ScaffoldStatusIndicator for status items RED state confirmed — implementations do not yet exist. Refs: WIDG-41
- Additive change to templates/components/data_table.dart.jinja2:
DataColumnConfig gains 'final Widget Function(BuildContext, T item)?
cellBuilder' with a doc contract covering padding ownership and the
accessor-driven sort invariant (WIDG-43 / D-08)
- Cell render path branches on '{% if col.cellBuilder is defined and
col.cellBuilder %}': emits Builder(...) delegating to the configured
cellBuilder when the fixture marks the column, else the existing
Text(item.accessor?.toString() ?? '') fallback (unchanged)
- Fixture templates/components/data_table_vars.json untouched (D-09
fallback: function values cannot be expressed in JSON — the runtime
branch is verified by the widget test added in the previous commit)
- Locked labelLarge w600 column-header style at template line 245
(post-edit line 255) is byte-identical — verified via grep
Refs: WIDG-43, 08-CONTEXT D-08/D-09
…hite in ScaffoldBadge - Add static _resolveOnStatusColor(Color fill) helper that returns _kOnStatusDark (#17191E) when fill.computeLuminance() exceeds _kOnStatusLuminanceThreshold (0.40), else _kOnStatusLight (#FFFFFFFF) - Replace Colors.white at label-style site (was line 105) and icon-glyph site (was line 140) with _resolveOnStatusColor(resolvedBadgeColor) - Resolution lives inside ScaffoldBadge (D-10) so badges render correctly under both defaultPalette and lightPalette without consumer overrides - Update pre-existing 'count variant' test that codified the bug — it asserted white text on the bright default fill lightGreenPrimary; now asserts dark on-status color per WIDG-46 - No new palette tokens; scaffold_palette.dart untouched - All 18 badge tests pass; dart analyze --fatal-infos clean; grep for Colors.white returns no matches
- Composes ScaffoldSurface (radiusPill, deepBlueCardColor fill, 8h×8v padding) + ScaffoldPressable (supplies 48px ScaffoldTouchTarget, focus ring, disabled overlay) - Selected state = 2px lightGreenPrimary BORDER, fill unchanged (D-01, 60/30/10) - Slots: optional 16px leading Icon, labelMedium Text, 8px ScaffoldStatusIndicator joined by space2 (4px) separators - D-03 constructor assert: icon-only chips REQUIRE semanticLabel (WCAG 4.1.2) - Semantics(selected) wraps the pressable (which adds button:true) - All 10 chip widget tests GREEN; dart analyze --fatal-infos clean - WIDG-40
- ScaffoldSurface container with surfaceElevated fill, borderSubtle 1px border, radiusMd corner radius; space4 row gaps, space8 inner padding - Three named rows in order: optional badge Wrap, text-entry TextField, optional end-aligned action Row with space4 separation - Holds NO submission logic: onSubmit(String) fires once on TextField submission and clears the private controller (D-07) - ScaffoldFocusOutline bound to the text field's private FocusNode; disabled state wraps the surface in ScaffoldDisabledOverlay (D-06) - All twelve composer widget tests pass; dart analyze --fatal-infos clean
- Add 'lightPalette covers all tokens consumed by shipped widgets' test - Assert isNotNull for each of the 11 tokens consumed by shipped widgets per 08-UI-SPEC 'Color' section: surfaceElevated, deepBlueCardColor, lightGreenPrimary, textPrimary, textSecondary, borderSubtle, focusRingColor, statusSuccess, statusError, statusWarningText, blue500 - Assert surface-flip contract: lightPalette.surfaceElevated != defaultPalette.surfaceElevated and lightPalette.textPrimary != defaultPalette.textPrimary - Verification only — no changes to lib/theme/scaffold_palette.dart - All 13 palette tests pass; dart analyze --fatal-infos clean
- StatefulWidget composing ScaffoldPressable + AnimatedSize + AnimatedRotation + ScaffoldMotion - Controlled (expanded + onExpandedChanged) and uncontrolled (initiallyExpanded) modes — Flutter's standard idiom; assert guards the conflict case - Chevron rotates 0.0 → 0.25 turns (90°) with ScaffoldMotionDurations.short; AnimatedSize body uses .medium; both collapse to zero under reduced-motion - Chevron tint: textSecondary default; lightGreenPrimary when expanded AND highlightWhenExpanded:true - Body indent EdgeInsets.only(left: dimens.space6, top: dimens.space4) - Semantics(expanded, label: title) announces state - All 9 widget tests GREEN; dart analyze --fatal-infos clean - No hardcoded colors or dims — only context.palette / context.dimens Test fix (Rule 1): added per-case Keys in Test 6 to force new _ScaffoldDisclosureState across pumpWidget calls — without keys, the prior state's _internalExpanded persisted into the next case and broke the tint assertion. Refs: WIDG-41, D-04, D-05
- Default / with-badges / with-actions / badges+actions / disabled variants plus an interactive submission-log section demonstrating the onSubmit(String) contract - Demo NOT registered in example/lib/main.dart — registration lands in plan 08-06 with the barrel export
- Typed TraceItem model (title, body, optional StatusVariant, initiallyExpanded) — one-way data in, no trace-domain knowledge - Empty items → SizedBox.shrink() (UI-SPEC Copywriting Contract) - Optional groupHeader: titleSmall typography + space12 top padding - Items separated by dimens.space8 (16px) - StatusVariant items: ScaffoldStatusIndicator rendered as a leading slot OUTSIDE the disclosure title via a Row wrapper — keeps ScaffoldDisclosure untouched (UI-SPEC Status slot composition) - All 6 trace-list widget tests GREEN; dart analyze --fatal-infos clean - No hardcoded colors or dims Refs: WIDG-41, D-04, D-05
- Wrap layout with dimens.space8 (16px) spacing, dimens.space4 (8px) runSpacing - Consumer-owned selection: selected Set<int> in, onSelectionChanged callback out (D-02) - Single-select replaces the set; multi-select toggles the tapped index - Empty chips list renders SizedBox.shrink() - Semantics role: radioGroup (single) / list (multi) — Flutter SDK lacks a generic 'group' role; list is the closest container role available. Plan literal 'radiogroup'/'group' did not compile against the pinned SDK. - Forwards chip slots (label/icon/status/disabled/semanticLabel) into wrapped ScaffoldChip instances; per-index selected flag driven from selected set - All 8 chip-group widget tests GREEN; dart analyze --fatal-infos clean - WIDG-40
- Add 08-01-SUMMARY.md capturing the 3-task TDD execution - WIDG-46 closed: ScaffoldBadge on-status color resolution + lightPalette token coverage contract - Documents Rule 1 (pre-existing test codified hardcoded-white bug) and Rule 3 (plan path correction test/components -> test/theme) deviations
- 08-05-SUMMARY.md capturing TDD gates, one Rule 1 test-filter fix, verification results, and self-check
- disclosure_demo.dart: four sections — uncontrolled collapsed, uncontrolled expanded, highlightWhenExpanded, controlled (StatefulBuilder-owned truth) - trace_list_demo.dart: three sections — simple ordered trace, group header variant, empty-state shrink contract - Both import atoms directly (package:frontend_scaffold/components/...) — barrel export deferred to plan 08-06 to avoid same-wave file conflicts - Demos are NOT yet registered in example/lib/main.dart (plan 08-06 owns demo registration) - dart analyze --fatal-infos clean Refs: WIDG-41
- 6 single-chip sections: Default / Selected / Disabled / Leading icon / Status / Icon-only (icon-only passes semanticLabel per D-03) - 3 group sections: Single-select / Multi-select / Empty (zero-size) - StatefulBuilder-equivalent local state via _SingleSelectGroup and _MultiSelectGroup StatefulWidgets holding Set<int> - Imports direct component paths (not barrel) — registration in main.dart deferred to plan 08-06 to avoid same-wave conflicts - dart analyze --fatal-infos clean - WIDG-40
- 4/4 tasks complete, 15/15 widget tests passing, dart analyze clean - 1 Rule 1 deviation: per-case Keys in Test 6 to force new state across pumpWidget calls (test-only fix) - Barrel export + demo registration deferred to plan 08-06 per plan
- scaffold_chip, scaffold_chip_group (after scaffold_card_state) - scaffold_composer (after scaffold_color_swatch) - scaffold_disclosure (after scaffold_disabled_overlay) - scaffold_trace_list (after scaffold_touch_target) All five new atoms now reachable from package:frontend_scaffold/frontend_scaffold.dart Satisfies: WIDG-40, WIDG-41, WIDG-42 barrel surface
- Add 4 imports: chip_demo, composer_demo, disclosure_demo, trace_list_demo - Append 4 _DemoTile entries after Wallet connect sheet: Chip/ChipGroup, Composer, Disclosure, Trace list Satisfies: WIDG-40, WIDG-41, WIDG-42 demo surface
Phase 8 final gate green: - flutter test 269/269 - dart analyze --fatal-infos clean (package + example) - No hardcoded colors in any Phase 8 atom - No hardcoded EdgeInsets dims in any Phase 8 atom - WIDG-46 badge remediation holds (no Colors.white) Closes Phase 8 — all five new atoms (chip, chip_group, composer, disclosure, trace_list) reachable from package barrel; all four demos registered in example/lib/main.dart.
…d _CellHost test double
… (WIDG-42) UAT finding: tapping the composer body (off the text line) did not focus the text field — the surface is inert, only the dense single-line TextField carried a tap recognizer. Add an optional consumer-supplied focusNode (falls back to an internal node; composer disposes only its own). The composer never requests focus itself — behavior stays consumer-owned per D-07. The example's submission-log composer wires tap-to-focus with a HitTestBehavior.translucent GestureDetector so taps reach both the surface and the inner field. Tests: external node wired to field + focus outline; consumer node not disposed by composer; internal node disposed with tree.
…amp touch in Source A/B - Insert make_directory COMMAND for template_gen/scaffold/base in Source A add_custom_command - Insert make_directory COMMAND for template_gen/shared/base in Source B add_custom_command - Fixes fresh-tree failure where cmake -E touch could not create stamp under nonexistent nested dir - Matches existing component-target style (line 216); engine.py creates output dirs itself
…an, summary, state
UAT finding: clicking a composer's text field lit the focus ring but the caret/keyboard never attached until a second click. Root cause: the outline returned bare child when the ring was hidden and a Stack when shown — the root runtimeType change remounted the TextField/EditableText subtree the moment focus landed, tearing down the text input connection. Always return a Stack (fit: passthrough); the ring is conditionally added as a second child, so the child at slot 0 is preserved across the toggle. Layout is unchanged (passthrough matches the old bare-child path). Regression test: gaining focus under keyboard highlight must not remount the TextField/EditableText. Composer demo keeps the consumer-side tap-to-focus wiring (translucent GestureDetector -> FocusScope.requestFocus on the exposed focusNode) so taps anywhere on the surface focus the field.
flutter create --platforms=ios. Dropped the incompatible generated counter widget_test (references MyApp, not the demo app). Restored macos/web/linux entries in .metadata, which flutter create rewrites rather than appends. README updated.
|
@codex review |
💡 Codex ReviewWith the committed openapi-client-scaffold/lib/components/scaffold_slider.dart Lines 161 to 162 in ed98c55 When ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
P1 — data_table template evaluated cellBuilder at generation time: the Jinja condition keyed on a vars marker, so generated tables never invoked the consumer-supplied runtime DataColumnConfig.cellBuilder (and a marked column would have emitted an unconditional cellBuilder! that crashes when the runtime config omits it). Emit a Dart runtime null check instead — widget.columns[i].cellBuilder != null ? builder : accessor-Text fallback — matching the _CellHost test double the template points at. P2 — ScaffoldSlider painted the buffered band before super.paint, so the stock inactive segment (thumb → track end) covered the buffer interval under any sufficiently opaque inactive track color. Paint stock segments first, then draw only the thumb-to-buffer interval over the inactive segment; the leading edge-to-thumb interval keeps the plain inactive color (band reads as 'buffered ahead of the playhead'). RTL handled via the same textDirection switch. Gates: dart analyze clean, flutter test 273/273, ruff+pytest 12/12, template drift clean (data_table generates to consumer space; committed families regenerate byte-identical).
|
Both Codex findings verified against the code and fixed in 3134486: P1 — cellBuilder evaluated at generation time (data_table.dart.jinja2): confirmed real. The Jinja condition keyed on a vars marker that the committed data_table_vars.json never sets, so generated tables emitted the hard-coded Text branch for every column and never invoked the consumer-supplied runtime DataColumnConfig.cellBuilder. The template now emits a Dart runtime null check — widget.columns[i].cellBuilder != null ? builder(cellContext, item) : Text(accessor fallback) — matching the _CellHost test double in test/components/data_table_cell_builder_test.dart the template points at. P2 — buffered segment covered by inactive track (scaffold_slider.dart): confirmed real. The buffer band was painted before super.paint, so the stock inactive segment (thumb → track end) covered it under any sufficiently opaque inactiveTrackColor. Now the stock segments paint first and only the thumb-to-buffered-edge interval is drawn over the inactive segment; the leading edge-to-thumb interval keeps the plain inactive color (band reads as 'buffered ahead of the playhead'). RTL handled via the same textDirection switch. Gates green: dart analyze --fatal-infos clean, flutter test 273/273, ruff + pytest 12/12, template drift check clean. |
STATE.md: status -> Phase 8 merged; session continuity points at Phase 9 discuss as the next action.
Summary
Phase 8: Supporting Atoms, Table Cells & Light Palette
Goal: Low-dependency composition atoms (chip, disclosure, composer) ship alongside the DataTable cell-builder extension and the light default palette — every consumer can build chip groups, trace rows, composition areas, custom table cells, and render correctly under a light theme.
Status: Verified ✓ (5/5 must-haves) · UAT ✓ (7/7 on macOS + Chrome; iOS scaffolding added for manual check)
Ships five new/extended atoms (ScaffoldChip, ScaffoldChipGroup, ScaffoldDisclosure, ScaffoldTraceList, ScaffoldComposer), a
cellBuilderextension toDataColumnConfig<T>, and a full light default palette complementing the dark-seeded tokens. All atoms consume onlyTheme.of(context)— no Riverpod, no app-specific logic — and are exported via thefrontend_scaffoldbarrel with demos in the example app.Changes
Plan 08-01: Light palette + Badge on-status remediation
Light default palette with full token coverage (locked in CI by test);
ScaffoldBadge._resolveOnStatusColorWCAG-AA resolver used by label-style and icon-glyph sites — inherited by all Wave 1–3 plans composing ScaffoldBadge.Key files:
lib/theme/scaffold_palette.dart,lib/theme/scaffold_dimens.dart,lib/theme/scaffold_theme.dartPlan 08-02: DataColumnConfig cellBuilder template extension
Optional
cellBuilder(Widget Function(BuildContext, T item)?) onDataColumnConfig<T>so consumers render custom cells without forking the table; Jinja cell-render branch falls back to existing string-Text behavior. Template-generated (WIDG-43).Key files:
templates/components/data_table.dart.jinja2, generatedlib/components/data table outputPlan 08-03: ScaffoldDisclosure + ScaffoldTraceList
Generic expand/collapse row atom (controlled + uncontrolled modes, CR-01 regression covered) and an ordered trace list with status dots, group headers, and zero-height empty state (WIDG-41).
Key files:
lib/components/scaffold_disclosure.dart,lib/components/scaffold_trace_list.dartPlan 08-04: ScaffoldChip + ScaffoldChipGroup
Pressable token atom (icon/text + optional status indicator) with single-select, multi-select, and zero-size empty group layouts (WIDG-40).
Key files:
lib/components/scaffold_chip.dart,lib/components/scaffold_chip_group.dartPlan 08-05: ScaffoldComposer
Text-entry composition area with badge/action slots and disabled state; D-06 honored (ScaffoldSurface + 3 named rows, focus ring on the text field). Consumer-supplied
focusNodeexposure added during UAT (WIDG-42).Key files:
lib/components/scaffold_composer.dartPlan 08-06: Barrel exports + demo registration + final gate
Public barrel surface for all five atoms; example-app demo registration; full gate run (analyze, tests, ruff/pytest, template drift).
Key files:
lib/frontend_scaffold.dart,example/lib/main.dartUAT fixes (in-phase)
afb7ea6— ScaffoldComposer exposes optional consumer-suppliedfocusNode(D-07: atom never requests focus itself; focus policy stays with the consumer)5f0ef51— ScaffoldFocusOutline remount fix: the outline returned barechildwhen the ring was hidden and aStackwhen shown; the root runtimeType change remounted the entire child subtree on focus gain, tearing downEditableTextstate and the text-input connection (desktop: ring lit, caret never attached). Fixed with a stableStack(fit: StackFit.passthrough)root; regression test added.ed98c55— iOS platform scaffolding added to the example app for manual UATRequirements Addressed
Verification
dart analyze --fatal-infoscleanflutter test— 273/273 passingruff check && pytest— 12/12 passingKey Decisions
Theme.of(context); no Riverpod, no app-specific logic.focusNodebut never requests focus itself; tap-to-focus behavior is wired by the consumer (demonstrated in the example demo).