Skip to content

Collapsed issue card for 1.0.0 - #24

Merged
Harrys76 merged 16 commits into
mainfrom
wp29/overlay-collapsed-card
Oct 26, 2026
Merged

Harrys76 merged 16 commits into
mainfrom
wp29/overlay-collapsed-card

Conversation

@Harrys76

@Harrys76 Harrys76 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Fourth PR of the 1.0.0 overlay stack, on top of #23. It redesigns the collapsed issue card (plan step 12). The expanded content below the meta line comes in the next PR.

What changes

  • Card surface. The card is flat (Card(elevation: 0)) with no source bar on its left edge.

  • Severity icon. Icons.error, Icons.warning or Icons.check_circle in severityTextColor replaces the emoji. It sits on the title's first line, placed by the measured line height.

  • Title. fontLg w600, 2 lines, or 3 above 1.3×. An expanded card shows the whole title.

  • Meta line. One paragraph replaces the badge line: category · confidence · "seen X of Y scans, label" · "N effects", in fontMd textTertiary with tabular figures.

    • The Jank and New tags lead the line. New shows for 2 seconds in place of the wider accent, and the card's "New" hint stays.
    • A no-break space keeps each dot with the part before it, so a wrapped line at large text never starts with a dot or leaves one between lines.
    • A screen reader hears the line as one phrase without the dots, for example "Jank, Build, Confirmed, seen 5 of 8 scans, flaky".
  • Debug timing tag. It takes the confidence slot, as in the mock, on a timing issue in a debug run. That means:

    • the issue carries debugModeDisclaimer;
    • its source is the VM timeline or FrameTiming;
    • its measurement is in milliseconds, seconds, percent or frames.

    Rates, counts and sizes stay untagged, and so do the raster cache issues, whose frames count cache changes. The per-card "Debug mode result" note is gone. The tag uses the badgeDbg* pair.

  • Crosshair. It replaces the highlight checkbox.

    • Icons.my_location 18 px, with a 32 px face in a 48 px Positioned hit box in the card corner. The face is centred on the title's first line.
    • The title row reserves 38 px for it, and the meta line ends 38 px early under a one-line title.
    • It shows only when the issue names a widget, labelled "Show on screen". _isLocatableIssue is now "has a widget name", which closes the gap Overlay data for the 1.0.0 card #22 left for layout and paint cards without one. Issues whose highlight matched by element only now carry their widget's name (see Review).
    • It keeps the checkbox's toggle behaviour, read as on or off, until the highlight flow (step 15) makes it a one-shot action.
  • Focus. The card body and the crosshair draw the 2 px checkboxActive outline in keyboard mode (SleuthFocusInkWell, which gains excludeFromSemantics). The InkWell's own focus fill comes from ThemeData.fallback() above the app's MaterialApp and measured 1.13:1 on the dark card.

  • Pin. The icon is gone. An expanded card's semantics hint reads "Pinned while expanded", after "New" when both apply.

  • Denylist. The overlay no longer uses Checkbox, so it leaves the framework section (51 entries, pinned by the growth test). Host-app checkboxes now count; the CHANGELOG says so.

  • Source metadata (own commit, 703f32d). jank_detected, sustained_jank and slow_startup_ttff carry ObservationSource.frameTiming. The jank issues had no source, and startup said structural although its time ends at the first frame's FrameTiming raster end. Detection is unchanged. The source shows in exports, the AI context and the expanded card, and the Debug timing tag reads it.

Different from the plan

  • Guide legend. The legend described the emoji, the coloured badges, the left bar and the checkbox. It now has Severity (icons), Meta line (the three tags), Confidence, Recurrence, Category (Startup added), Effort badges and Card colors. Per the PR3 choice, the rest of the guide rewrite stays in step 18.
  • Jank flash. The guide listed a "Jank flash" card colour the overlay never drew: the host always passed jankFlash: false. The row and the dead prop are gone. cardJankFlash stayed here for custom themes, and 1.0 public API, API enforcement and list-card crosshair #27 removes it with the other unread tokens.
  • Unused tokens. The confidence colours, the source accents, sourceAccentWidth, effectsBadge, disclaimerText and cardJankFlash are no longer read. Their docs say so, as Overlay chrome for the 1.0.0 card: run label, More menu, status row #23 did for the VM/FRAME badge tokens. The API PR removes them.
  • Build-cost budget. Without the 48 px checkbox setting its height, a collapsed card is shorter, and 4 fit on the 390 × 844 view instead of 3. A title change now rebuilds 8 issue cards instead of 6, so the budget rises from 300 to 400. Each card costs less: about 0.93 ms per card against 1.2 ms. The card itself still builds 0 times.

Verification

  • fvm flutter analyze: 0 issues. Format check clean. fvm dart doc --dry-run: 0 warnings.
  • fvm flutter test --exclude-tags benchmark: 4,899 passed.
  • Benchmarks (--tags benchmark --concurrency=1): 44 passed.
  • Floor lane on Flutter 3.32.8: analyze clean, 4,899 passed.
  • Example app tests: 348 passed. Sidecar: analyze clean, 653 passed.
  • New tests cover:
    • icon, title and meta line at 1.0×, 1.3× and 2.0×, with the icon on the first line and the meta line under the title;
    • the full title while expanded;
    • the crosshair: absent without a widget name, its 48 px hit box in the corner taking a tap at its edge, its face on the first title line, the title's room, on and off read through toggled semantics, its glyph at 3:1 off and on in every preset;
    • the meta line: order (tags, then words, each dot kept with its word), the Debug timing tag in the confidence slot and its absence for profile runs, structural issues, rates, sizes and issues without a measurement, effects, the spoken phrase;
    • New tag timing in the held-order tests, the Jank tag by the step 3 rule, the pin hint;
    • the card's focus outline;
    • contrast rows for the three tags, the severity icons and the crosshair on the card fills;
    • the frameTiming source on real FrameTiming and Startup detector output.
  • The UI and UX design reviews of this change were applied before opening: card focus outline, toggled crosshair semantics, the Debug timing source gap, the guide tile name, the full expanded title, the meta line as one paragraph, the crosshair's vertical centre, tag padding and the legend's indent and spelling.

Device feedback (735b30b)

Tried on the iPhone 12 in profile mode with --no-dds:

  • Severity marks. The card and the guide legend draw the mock's filled marks (SleuthSeverityIcon): a disc or triangle in the bright severity colour with a white or near-black mark inside at 3:1 or more against the fill. The tinted Material icons read pale.
  • Maximized card resize. The plan had the grip draw dots only on a maximized card. It looked resizable, so it now resizes: the first drag or resize action makes the card normal at the size and place it has, then resizes that.
  • Quiet presses. SleuthFocusInkWell draws no ripple, only a light textPrimary tint while pressed. Above the app's MaterialApp, the fallback theme's 40 % grey splash flooded the card and the menu.
  • Expanded cards stay. An expanded card stays until it is collapsed, even when its issue stops being reported or folds under another card. A card whose issue is gone leads its meta line with "Not detected right now" and has no crosshair, and the empty states wait for the collapse. applyFreezeZone keeps expanded entries from the snapshot, and the prune no longer drops them.

Review

Codex reviewed the branch nine times; the ninth round, on the frozen-zone rewrite, found nothing. The first round found one defect, fixed in 35685b3; the second found none. The third reviewed the device-feedback commit and found three edge cases in the kept expanded card, fixed in 68adf2e with a regression test each:

  • a hidden card stayed when its twin with the same hide key was hidden;
  • of two cards sharing a key, the stale check matched the survivor, so the gone one kept its crosshair;
  • a kept card showed the instance it was expanded with, not the last report.

The fourth round found three more, fixed in 35332bc with a regression test each:

  • when the first of two same-key cards went, the second moved onto its occurrence key, so a kept card could be replaced by its twin and the twin shown twice; kept cards now match reports by element, and the freeze zone shows each issue once by identity;
  • the hide check used the kept instance, so a card hidden at warning that came back critical, was expanded and fell back to warning stayed;
  • the "Not detected right now" tag text failed 4.5:1 on the high-contrast presets' opaque fill; it now uses badgeTextOn.

The fifth round found two more twin cases, fixed in 1db254b with a regression test each:

  • expanding only the second of two same-key cards, then losing the first, moved the second onto the first's key, so it showed collapsed; expanded slots now claim their issues first, a kept card keeps its expansion key, and a re-captured snapshot repoints expansions by identity;
  • hiding the live twin left a kept, no-longer-reported twin on screen; the hide check falls back to the kept instance.

The sixth round found one more: after a twin was replaced by a new occurrence under the same key, tapping the new card stored its expansion against the old twin's slot, which came back while the new card showed collapsed. Fixed in 485e5cd: the tap records the tapped issue as its key's kept instance.

The seventh round found that the crosshair selection was pruned by its positional key, so a kept card showing under a key that differs from its place in the visible list could lose its highlight on the next update. Fixed in d98691a with a regression test: the card keeps the selected issue and prunes the selection when that issue has no report in the list.

The eighth round found three more twin cases in the same bookkeeping: a re-captured snapshot renumbered keys, so a new twin took over a kept card's expanded state; a kept card whose report had been replaced could be dropped; a card expanded next to a kept twin was left out of the snapshot and vanished when its issue went. Each came from three structures (snapshot, key-to-index map, kept issues) keyed by position among twins. 023a479 replaces them with one frozen-zone list: each card holds the key it rendered under when the zone was captured, its latest report and its expanded flag, and every update matches cards to reports by list key and occurrence id. A regression test for each case fails on the previous code; every earlier twin test passes unchanged.

After a look on the device, the disc marks gained a ring (b038fec), and the Theme row became the mock's single outlined bar with thin dividers, a filled bold current choice and no leading icon (c19d235). wrap_layout_bottleneck, excessive_repaint_boundary and issues from SimpleStructuralDetector.report set an occurrence id that matches their highlight, but no widgetName, so the new crosshair rule dropped a highlight that worked. Each now carries the name its highlight carries: Wrap, the scrollable's type, or the reported widget's runtime type. Their overlay keys gain the name, and a store written before 1.0 keeps hiding them (next section).

In light mode the severity chips read as two dark browns, and in dark mode the run label dot was pale (1ff53d8). Both drew in the severity text shades. Optional severityCriticalGraphic, severityWarningGraphic and severityOkGraphic tokens, read through severityGraphicColor, now colour chip borders, chip icons and the dot: the mock's severity fills in the dark default, red-600, amber-600 and emerald-600 in the light preset, each at 3:1 or more on the card. The high-contrast presets and fromColorScheme keep the text shades. The disc ring is white in both themes; the text colour drew it black on a light card.

Hidden cards across the upgrade (8a43ecb)

Cards of those three families hidden under 0.38 showed again after the upgrade, because their stored hide keys named no widget. OverlayUiState.schemaVersion is now 2. loadJson reads a version 1 hide key that names no widget as an any-widget key (legacyHideKey: id|*, keeping !critical), and isHiddenBy and the Hidden page match it against every card of that id. A version 2 store's keys are read as written. 0.38 reads a version 2 store as empty and leaves it as it is, as it does for any newer version. A card shown again while the store read is pending stays shown, whether the app unhid the stored key or a key of the same stable id with a widget name, and the Hidden page names a card the key still hides before one that turned critical. Tests cover the version 1 read, the version 2 read, both pending-read cases, the severity rule for an any-widget key and the Hidden page; each fails without its part of the change.

Not in this PR

  • The expanded card (fix code block, labelled buttons, measurement gauge, Why it rebuilds) is the next PR. It also removes the last ShaderMask.
  • The crosshair's Finding / Found / bar flow is step 15.
  • The device parity walk (step 10) ran on the finished stack; 1.0.0 release prep: docs, sidecar 0.10.0, CHANGELOG #28 has the results.

Hidden page labels (Galaxy follow-up)

The Hidden page printed hide keys raw: raster_hot_spot|* for a key migrated from 0.38, and id|Widget!critical for others. hideKeyLabel shows id · Widget or id · every widget and leaves out the critical suffix, since the row already says when a key stops hiding a card that turned critical. A key with nothing detected uses the label as its title. The subtitle takes two lines, so a long stable id keeps the widget name at its end. Tests cover the label, the row with nothing detected and the two-line fit; each fails without the change.

On the Galaxy A07, keys saved by a real 0.38.0 build (raster_hot_spot and tooltip_usage::This tooltip triggers cookbook detector 01) loaded as |* keys and kept both Cookbook cards hidden, including the tooltip card that now names Tooltip. Restore wrote the store as schemaVersion 2.

Guide text (doc audit follow-up)

The Guide's legend and tip said the Jank tag marks an issue that can explain "the latest slow frame". Since #22's follow-up the tag stays for 10 s after a slow frame names the issue and clears on a route change, so both now say "a slow frame from the last 10 seconds on this screen".

🤖 Generated with Claude Code

Harrys76 and others added 16 commits October 27, 2026 00:41
jank_detected and sustained_jank carried no observation source, and
slow_startup_ttff said structural although its time ends at the first
frame's FrameTiming raster end. All three now carry
ObservationSource.frameTiming. Detection is unchanged; the source shows
in exports, the AI context and the expanded card, and the collapsed
card's Debug timing tag reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The card is flat with no source bar. A severity icon in the severity's
text colour replaces the emoji, and the title runs to 2 lines (3 above
1.3x text), or in full while expanded. One meta line replaces the
badges: category, confidence, "seen X of Y scans, label" and effects,
led by the Jank and New tags, as one paragraph so a wrapped line keeps
each separator with the part before it. A Debug timing tag takes the
confidence slot on a time-based measurement from the VM timeline or
FrameTiming in a debug run, replacing the per-card debug note.

A crosshair replaces the highlight checkbox and shows only for an issue
that names a widget: a 48 px Positioned hit box in the card corner with
its face on the title's first line, read as on or off. The card body and
the crosshair draw the focus outline in keyboard mode. The pin icon is
gone; an expanded card's hint carries "Pinned while expanded". The
overlay no longer uses Checkbox, so it leaves the framework denylist.

The guide legend describes the new card, without the never-drawn Jank
flash colour or the dead jankFlash prop. Collapsed cards are shorter, so
the build-cost budget for a title change rises to 400 issue-card builds
(8 per change instead of 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wrap_layout_bottleneck, excessive_repaint_boundary and issues from
SimpleStructuralDetector.report set an occurrence id that matches their
highlight but no widgetName, so the collapsed card, which shows the
crosshair only for an issue that names a widget, dropped a highlight that
worked. Each now carries the name its highlight carries: Wrap, the
scrollable's type, or the reported widget's runtime type. Their overlay
keys gain the name, so such a card hidden under an older release shows
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…expanded cards stay

Device feedback on the 1.0.0 card:

- The severity mark is a disc or triangle filled with the severity
  colour, with a white or near-black mark inside at 3:1 or more against
  the fill (SleuthSeverityIcon), in the card and the guide legend. The
  tinted Material icons read pale on the card.
- The resize grip works on a maximized card: the first drag or resize
  step makes it a normal card at the size and place it has, then resizes
  that.
- SleuthFocusInkWell draws no ripple, only a light textPrimary tint while
  pressed. Above the app's MaterialApp the fallback theme's 40 % grey
  splash flooded the card and the menu.
- An expanded card stays until it is collapsed, even when its issue
  stops being reported or folds under another card. A card whose issue
  is gone leads its meta line with "Not detected right now" and offers
  no crosshair, and the empty states wait for the collapse.
  applyFreezeZone keeps expanded entries from the snapshot, and the
  prune no longer drops them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An expanded card that outlives its issue now:

- goes when the user hides it, also through a twin card sharing its hide
  key; the prune drops hidden expanded cards before keeping the rest;
- is stale only when its own occurrence is gone: it is matched to the
  visible list by occurrence key, else to the reported issues by list
  key and element identity, so of two cards sharing a key only the one
  that went loses its crosshair;
- shows its last reported instance, not the one it was expanded with:
  the host keeps the latest instance per expanded card and
  applyFreezeZone takes it in place of the snapshot's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As the design draws them: a 1.8-unit ring in textPrimary around the
critical and OK discs, which reads white on the dark card and also sets
the disc's edge apart from it. The warning triangle keeps its own edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t report

- With two cards sharing a list key, the first one going moved the
  second onto its occurrence key, so a kept card could be replaced by its
  twin and the twin shown twice. Kept cards are now matched to reports
  by list key and element identity, and applyFreezeZone shows each issue
  once, excluding by identity what the frozen zone already shows.
- The prune judged hiding on the kept instance. A card hidden at warning
  that came back critical, was expanded and fell back to warning stayed.
  The check now resolves the current report first.
- The "Not detected right now" tag text uses badgeTextOn, so it keeps
  4.5:1 on the high-contrast presets' opaque badge fill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Expanding only the second of two cards sharing a list key, then losing
  the first, moved the second onto the first's occurrence key: it showed
  collapsed and its expansion entry lingered. Expanded slots now claim
  their issues before collapsed ones, a kept expanded card keeps the key
  its expansion is stored under (_cardKeys), and a re-captured snapshot
  repoints expansions by identity, renaming a key that moved.
- A kept card no longer reported was skipped by the hide check, so
  hiding its live twin left it on screen. The check falls back to the
  kept instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Theme row matches the design: no icon before the label, and Auto,
Light and Dark in one outlined bar split by thin lines. The current
choice fills its cell edge to edge and reads in bold, with no border of
its own, and Light and Dark keep their outline icons in both states.
Each choice is still a 48 x 48 target with a 32 px tall cell, and draws
the focus outline in keyboard mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a card's twin was replaced by a new occurrence under the same list
key, tapping the new card stored its expansion under a key whose
snapshot slot still held the old twin: the old twin came back and the
new card showed collapsed. The tap now records the tapped issue as the
kept instance of its key, and a collapse drops it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The light preset drew chip borders, chip icons and the run label dot in
its severity text shades, red-800 and amber-800, which read as two dark
browns. The dark default drew them in its pale text shades. New optional
severityCriticalGraphic, severityWarningGraphic and severityOkGraphic
tokens, read through severityGraphicColor, colour these graphics: the
dark default uses the severity fills and the light preset red-600,
amber-600 and emerald-600, each at 3:1 or more on the card. The
high-contrast presets and fromColorScheme keep the text shades.

The ring on the critical and OK discs is white in both themes. It was
the text colour, so a light card drew it black.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Occurrence keys are positional, so a kept expanded card can show under a
key that differs from its place in the visible list. Pruning checked the
selected key against the visible keys and could clear the crosshair of
a card that was still on screen. The card now keeps the selected issue
and prunes the selection when that issue has no report in the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a card was expanded, the cards above it lived in three places: a
list snapshot, a map from occurrence key to snapshot index, and a map of
kept issues. Occurrence keys number the cards sharing a list key by
position, so when a twin ahead of an expanded card went, the three
drifted apart. A re-captured snapshot renumbered the keys, so a new twin
took over the kept card's expanded state. A kept card whose report had
been replaced could be dropped. A card expanded next to a kept twin was
left out of the snapshot and vanished when its issue went.

The frozen zone is now one list of cards. Each card holds the key it
rendered under when the zone was captured, its latest report, and
whether it is expanded. Every update matches the cards to their reports
by list key and occurrence id, and the cards below take keys that skip
the frozen ones. Expanding a card below the zone captures the rows on
screen down to it, under the keys they rendered with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wrap layout, excessive RepaintBoundary and custom structural issues
now carry a widget name, which their list keys include, so a key a
0.38 store held for them ("wrap_layout_bottleneck") no longer matched
and the card showed again. The overlay state moves to schemaVersion 2.
Loading a version 1 store turns each key without a widget name into
one that matches any widget of its stable id ("wrap_layout_bottleneck|*"),
the Hidden issues list names a card such a key still hides, and
Restore removes it. Keys written by 1.0 are read as stored. A card
shown again while the store read was pending stays shown, whether the
app unhid the stored key or a key of the same stable id with a widget
name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A key migrated from a version 1 store showed as `raster_hot_spot|*` on
the Hidden page, and widget keys as `id|Widget!critical`. hideKeyLabel
reads them as `id · Widget` or `id · every widget`; the critical suffix
is left out, since the row already says when a key stops hiding a card
that turned critical. A key with nothing detected uses the label as its
title, and the label gets two lines, so a long stable id keeps the
widget name at its end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Guide's legend and tip said the Jank tag marks an issue that can
explain "the latest slow frame". The tag now stays for 10 s after a slow
frame names the issue and clears on a route change, so both say "a slow
frame from the last 10 seconds on this screen".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Harrys76
Harrys76 force-pushed the wp29/overlay-collapsed-card branch from 10c07b5 to 22f281a Compare October 26, 2026 17:59
Base automatically changed from wp28/overlay-chrome to main October 26, 2026 19:02
@Harrys76
Harrys76 merged commit eafccaa into main Oct 26, 2026
5 of 10 checks passed
@Harrys76
Harrys76 deleted the wp29/overlay-collapsed-card branch October 26, 2026 19:02
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