fix(ui): add top-inset awareness to PluriRootHeader

PluriRootHeader rendered its 56px title/actions row flush at y=0 on
every device, since app.dart's root SafeArea(top: false) deliberately
excludes the top inset (so each root's full-bleed background paints
edge-to-edge behind the status bar) but the header itself never added
MediaQuery.paddingOf(context).top anywhere. The header now wraps its
existing 56px content row in an outer top padding equal to that inset,
so total rendered height is height + topInset while `height` keeps
meaning the content row's own height (verified no call site did
total-height math against the old fixed constant).
This commit is contained in:
2026-08-01 12:00:48 +02:00
parent cfd8bc9e6a
commit 4d54908be6
2 changed files with 153 additions and 30 deletions
+46 -28
View File
@@ -29,41 +29,59 @@ class PluriRootHeader extends StatelessWidget {
/// needs nothing extra here).
final List<Widget> actions;
/// Fix `safearea-top-inset`: this is the CONTENT row's height only —
/// NOT this widget's total rendered height. `app.dart`'s root
/// `SafeArea(top: false, ...)` deliberately excludes the top inset (so
/// each root's own full-bleed background paints genuinely edge-to-edge
/// behind the status bar), which left this header's title/actions row
/// with zero top-inset awareness — flush at y=0 under the status bar /
/// camera cutout on every device. This widget now adds
/// `MediaQuery.paddingOf(context).top` ABOVE this content height itself
/// (see [build]), so the total rendered height is
/// `height + MediaQuery.paddingOf(context).top`. Callers doing
/// total-height math (none currently do — checked every `PluriRootHeader`
/// call site) must add that inset separately; this constant's MEANING
/// (content height) is unchanged.
static const double height = 56;
@override
Widget build(BuildContext context) {
final type = context.pluriType;
final l10n = AppLocalizations.of(context);
return SizedBox(
height: height,
child: Padding(
// S5: the prototype's own header padding is title-tier on the
// left, row-tier on the right (t4 e.g. Alarmas
// `padding:0 12px 0 20px`).
padding: const EdgeInsets.fromLTRB(
PluriLayout.titleHorizontal,
0,
PluriLayout.rowHorizontal,
0,
),
child: Row(
children: [
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: type.sectionTitle,
final topInset = MediaQuery.paddingOf(context).top;
return Padding(
padding: EdgeInsets.only(top: topInset),
child: SizedBox(
height: height,
child: Padding(
key: const ValueKey('pluri-root-header-content'),
// S5: the prototype's own header padding is title-tier on the
// left, row-tier on the right (t4 e.g. Alarmas
// `padding:0 12px 0 20px`).
padding: const EdgeInsets.fromLTRB(
PluriLayout.titleHorizontal,
0,
PluriLayout.rowHorizontal,
0,
),
child: Row(
children: [
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: type.sectionTitle,
),
),
),
...actions,
IconButton(
icon: const Icon(Icons.bedtime_outlined),
tooltip: l10n.sleepTimer,
onPressed: onSleepTimer,
),
],
...actions,
IconButton(
icon: const Icon(Icons.bedtime_outlined),
tooltip: l10n.sleepTimer,
onPressed: onSleepTimer,
),
],
),
),
),
);