Skip to content

Phase 8: Supporting Atoms, Table Cells & Light Palette - #8

Merged
Super-Genius merged 47 commits into
developfrom
gsd/phase-08-supporting-atoms-table-cells-light-palette
Aug 19, 2026
Merged

Phase 8: Supporting Atoms, Table Cells & Light Palette#8
Super-Genius merged 47 commits into
developfrom
gsd/phase-08-supporting-atoms-table-cells-light-palette

Conversation

@Super-Genius

Copy link
Copy Markdown
Contributor

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 cellBuilder extension to DataColumnConfig<T>, and a full light default palette complementing the dark-seeded tokens. All atoms consume only Theme.of(context) — no Riverpod, no app-specific logic — and are exported via the frontend_scaffold barrel 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._resolveOnStatusColor WCAG-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.dart

Plan 08-02: DataColumnConfig cellBuilder template extension

Optional cellBuilder (Widget Function(BuildContext, T item)?) on DataColumnConfig<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, generated lib/components/ data table output

Plan 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.dart

Plan 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.dart

Plan 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 focusNode exposure added during UAT (WIDG-42).

Key files: lib/components/scaffold_composer.dart

Plan 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.dart

UAT fixes (in-phase)

  • afb7ea6 — ScaffoldComposer exposes optional consumer-supplied focusNode (D-07: atom never requests focus itself; focus policy stays with the consumer)
  • 5f0ef51ScaffoldFocusOutline remount fix: the outline returned bare child when the ring was hidden and a Stack when shown; the root runtimeType change remounted the entire child subtree on focus gain, tearing down EditableText state and the text-input connection (desktop: ring lit, caret never attached). Fixed with a stable Stack(fit: StackFit.passthrough) root; regression test added.
  • ed98c55 — iOS platform scaffolding added to the example app for manual UAT

Requirements Addressed

  • WIDG-40 — ScaffoldChip + ScaffoldChipGroup ✓
  • WIDG-41 — ScaffoldDisclosure + ScaffoldTraceList ✓
  • WIDG-42 — ScaffoldComposer ✓
  • WIDG-43 — DataColumnConfig.cellBuilder ✓
  • WIDG-46 — Light default palette (carried from v1.0) ✓

Verification

  • Automated verification: passed — 5/5 must-haves (08-VERIFICATION.md)
  • dart analyze --fatal-infos clean
  • flutter test — 273/273 passing
  • ruff check && pytest — 12/12 passing
  • Template drift check clean (5 generated families)
  • Human UAT: 7/7 pass on macOS + Chrome (08-UAT.md); 2 gaps found and fixed in-phase (composer focusNode exposure, focus-outline remount)
  • Code review: 08-REVIEW.md — findings resolved

Key Decisions

  • Atoms are primitives, composites are recipes — new atoms are plain parameterized widgets consuming only Theme.of(context); no Riverpod, no app-specific logic.
  • D-07 consumer-owned focus policy — ScaffoldComposer exposes its focusNode but never requests focus itself; tap-to-focus behavior is wired by the consumer (demonstrated in the example demo).
  • Focus ring must never remount its child — ScaffoldFocusOutline now returns a stable Stack root regardless of ring visibility; conditional children only.
  • v1.2 verification gate is its own phase (Phase 11) — WIDG-44/45 sweep executes over all atoms shipped in Phases 8–10.

- 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.
… (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
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.
Composer focus issues found during UAT: consumer-side focusNode exposure
(afb7ea6) + ScaffoldFocusOutline remount fix (5f0ef51). Chrome target
approved but not exercised.
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.
@Super-Genius

Copy link
Copy Markdown
Contributor Author

@codex review

@Super-Genius
Super-Genius changed the base branch from main to develop August 19, 2026 01:10
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

{% if col.cellBuilder is defined and col.cellBuilder %}

P1 Badge Evaluate cellBuilder at runtime

With the committed data_table_vars.json, no column has a generation-time cellBuilder marker, so this Jinja condition emits the hard-coded Text(...) branch for every column. A consumer can then supply the newly advertised DataColumnConfig.cellBuilder, but the generated table never reads or invokes it; conversely, marking a column in the vars would emit an unconditional cellBuilder! that crashes when a runtime config omits it. Emit a Dart runtime null check against widget.columns[index].cellBuilder instead.


// Stock played/inactive segments (and secondary track) on top.
super.paint(

P2 Badge Paint the buffered segment above the inactive track

When bufferedValue > value, the buffered rectangle is drawn first and super.paint subsequently paints the stock inactive segment from the thumb to the track end over it. Under any ambient SliderTheme with an opaque inactive-track color, the entire visible buffered interval is covered, so buffering is not shown at all; even translucent defaults blend over and distort the requested color. Paint the stock track first and then draw only the thumb-to-buffer interval, or use the slider's secondary-track mechanism.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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).
@Super-Genius

Copy link
Copy Markdown
Contributor Author

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.

@Super-Genius
Super-Genius merged commit 4854fe2 into develop Aug 19, 2026
2 checks passed
Super-Genius added a commit that referenced this pull request Aug 19, 2026
@Super-Genius
Super-Genius deleted the gsd/phase-08-supporting-atoms-table-cells-light-palette branch August 19, 2026 01:38
Super-Genius added a commit that referenced this pull request Aug 19, 2026
STATE.md: status -> Phase 8 merged; session continuity points at Phase 9
discuss as the next action.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant