Part of #6
Question
The design has one app header; the Flutter shell has none. Where does it live, and what happens to it when a branch pushes?
Surfaced resolving Saved shelf, which needs somewhere to put SavedHeaderButton and found no header architecture to put it in.
What the source specifies — measured, do not re-derive
AppHeader (prototype/screens.jsx:671) is rendered once, at app level (prototype/app.jsx:1668), a sibling of the TabBar rather than anything inside a tab:
{showTabs && window.AppHeader && (
<AppHeader tab={tab} variant={...} scrolled={...} savedCount={savedCount} ... />
)}
It carries:
StickyHeaderChrome at height={116}, with HeaderCompactTitle collapsing an eyebrow/title pair on scroll
- Per-tab copy from
APP_HEADER_TITLES[tab] — the header knows which tab it is
- Right-side entries:
SavedHeaderButton (with count badge), DictHeaderButton, and DuelHeaderButton when showDuel
- A
profile variant that replaces all three with a single Settings gear
showTabs gating it away on sub-screens, exactly as it gates the TabBar
What the app has instead
AppShell (lib/app/app_shell.dart) is a bare Scaffold — body: navigationShell, bottomNavigationBar: NavigationBar, and no appBar. Each branch builds its own:
| Branch |
Header today |
| Learn |
AppBar(title: Text(AppLabels.tabLearn)) |
| Path |
AppBar(title: Text(AppLabels.tabPath)) |
| Cards |
AppBar(title: Text(AppLabels.tabCards)) |
| Profile |
SliverPersistentHeader + ProfileHeaderDelegate |
Three stock Material AppBars and one real one. None of them is the design's header.
Decide
- Where the slot goes. Above the branch navigators in
AppShell — one instance, tab-aware, matching the prototype — or inside each branch, four instances kept in sync. The prototype's shape argues for the shell, but StatefulShellRoute gives each branch its own Navigator, so a shell-level header persists across pushes within a branch unless something hides it.
- How it hides on sub-screens. The prototype uses one
showTabs flag for both header and TabBar. The Flutter equivalent has to be decided: a route-level flag the shell reads, nested navigation that escapes the shell, or per-route AppBar suppression.
- Where the collapsing scroll state lives. The header collapses on the active tab's scroll. With four independent branch navigators, that means either the shell observes the active branch's scroll or each branch reports upward.
- What happens to
ProfileHeaderDelegate. Profile is the one branch with a real header today and the one the design gives a different variant. Does it fold into the shared header's profile variant, or stay separate?
Not in scope
The scroll / BackdropFilter behaviour of StickyHeaderChrome itself, which stays fog on the map — this ticket decides the slot and its lifecycle, not the chrome's paint.
Resolved when the header's owner, its hide rule, its scroll source and Profile's treatment are all named.
Part of #6
Question
The design has one app header; the Flutter shell has none. Where does it live, and what happens to it when a branch pushes?
Surfaced resolving Saved shelf, which needs somewhere to put
SavedHeaderButtonand found no header architecture to put it in.What the source specifies — measured, do not re-derive
AppHeader(prototype/screens.jsx:671) is rendered once, at app level (prototype/app.jsx:1668), a sibling of the TabBar rather than anything inside a tab:It carries:
StickyHeaderChromeatheight={116}, withHeaderCompactTitlecollapsing an eyebrow/title pair on scrollAPP_HEADER_TITLES[tab]— the header knows which tab it isSavedHeaderButton(with count badge),DictHeaderButton, andDuelHeaderButtonwhenshowDuelprofilevariant that replaces all three with a single Settings gearshowTabsgating it away on sub-screens, exactly as it gates the TabBarWhat the app has instead
AppShell(lib/app/app_shell.dart) is a bareScaffold—body: navigationShell,bottomNavigationBar: NavigationBar, and noappBar. Each branch builds its own:AppBar(title: Text(AppLabels.tabLearn))AppBar(title: Text(AppLabels.tabPath))AppBar(title: Text(AppLabels.tabCards))SliverPersistentHeader+ProfileHeaderDelegateThree stock Material
AppBars and one real one. None of them is the design's header.Decide
AppShell— one instance, tab-aware, matching the prototype — or inside each branch, four instances kept in sync. The prototype's shape argues for the shell, butStatefulShellRoutegives each branch its ownNavigator, so a shell-level header persists across pushes within a branch unless something hides it.showTabsflag for both header and TabBar. The Flutter equivalent has to be decided: a route-level flag the shell reads, nested navigation that escapes the shell, or per-routeAppBarsuppression.ProfileHeaderDelegate. Profile is the one branch with a real header today and the one the design gives a different variant. Does it fold into the shared header'sprofilevariant, or stay separate?Not in scope
The scroll /
BackdropFilterbehaviour ofStickyHeaderChromeitself, which stays fog on the map — this ticket decides the slot and its lifecycle, not the chrome's paint.Resolved when the header's owner, its hide rule, its scroll source and Profile's treatment are all named.