fix(chrome): retire PluriScreenHeader, it has no equivalent in the prototype

PluriScreenHeader was a 38-radius glass hero: an aurora banner at 24%
opacity, a black-to-transparent scrim, two radial orbs, a 120px app-mark
watermark and a 56px tri-gradient glyph badge. None of it is in the
prototype (t4) -- every root's title is plain text in its own 56px
row, which PluriRootHeader (S1) already provides. Delete the class
(and its now-orphaned _Orb helper) and its four call sites (Buscar,
Favoritos x2, Alarmas, Ajustes landed with S8).

Two of those call sites carried the hero's only functional bit besides
the title, so PluriRootHeader gains an `actions` slot (rendered before
the shared bedtime button) to keep them reachable:
  - Alarmas' create-alarm button (FilledButton.tonalIcon, unchanged
    shape, just relocated)
  - Buscar's filters entry point (the same tappable PluriStatusPill,
    just relocated)
Every other retired trailing pill (Ajustes' "Secure" status,
Favoritos' collection-count badges, Alarmas' alarm-count badge) was
purely decorative and matches the prototype by simply disappearing.

S2, Tier 1 visual-fidelity pass (audit id 2521).
This commit is contained in:
2026-07-29 21:29:49 +02:00
parent 7ebe0b77a4
commit 97e38becfe
8 changed files with 268 additions and 282 deletions
-229
View File
@@ -1,217 +1,9 @@
import 'package:flutter/material.dart';
import '../tema/pluriwave_theme.dart';
import '../tema/pluriwave_tokens.dart';
import 'pluri_glass_surface.dart';
import 'pluri_icon.dart';
class PluriScreenHeader extends StatelessWidget {
const PluriScreenHeader({
super.key,
required this.title,
required this.subtitle,
required this.glyph,
this.primaryActionLabel,
this.onPrimaryAction,
this.trailing,
});
final String title;
final String subtitle;
final PluriIconGlyph glyph;
final String? primaryActionLabel;
final VoidCallback? onPrimaryAction;
final Widget? trailing;
@override
Widget build(BuildContext context) {
final t = context.pluriTokens;
final theme = Theme.of(context);
final width = MediaQuery.sizeOf(context).width;
final scale = MediaQuery.textScalerOf(context).scale(1);
final compact = width < 380 || scale >= 1.25;
final iconSize = compact ? 50.0 : 56.0;
Widget glyphBadge() => Container(
width: iconSize,
height: iconSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
PluriWaveTokens.brightCyan.withValues(alpha: 0.95),
t.electricMagenta,
t.warmCoral,
],
),
boxShadow: [
BoxShadow(color: t.glowColor, blurRadius: 28, spreadRadius: 2),
],
),
child: Center(
child: PluriIcon(
glyph: glyph,
variant: PluriIconVariant.filled,
size: compact ? 25 : 28,
),
),
);
Widget textBlock() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: compact ? 2 : 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w900,
letterSpacing: -0.7,
height: 1.05,
),
),
const SizedBox(height: 6),
Text(
subtitle,
maxLines: compact ? 4 : 3,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.78),
height: 1.28,
),
),
if (primaryActionLabel != null) ...[
const SizedBox(height: 12),
FilledButton.tonalIcon(
onPressed: onPrimaryAction,
icon: const Icon(Icons.auto_awesome_rounded, size: 18),
label: Text(primaryActionLabel!),
),
],
],
);
Widget foreground() {
if (compact) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
glyphBadge(),
const SizedBox(width: 14),
Expanded(child: textBlock()),
],
),
if (trailing != null) ...[
const SizedBox(height: 14),
Align(alignment: Alignment.centerLeft, child: trailing!),
],
],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
glyphBadge(),
const SizedBox(width: 14),
Expanded(child: textBlock()),
if (trailing != null) ...[
const SizedBox(width: 12),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 220),
child: trailing!,
),
],
],
);
}
return Padding(
padding: EdgeInsets.fromLTRB(
t.spacingMd,
t.spacingSm,
t.spacingMd,
t.spacingSm,
),
child: PluriGlassSurface(
borderRadius: BorderRadius.circular(t.radiusLg + 8),
padding: EdgeInsets.symmetric(
horizontal: compact ? 16 : 20,
vertical: compact ? 18 : 20,
),
child: Stack(
children: [
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(t.radiusLg + 8),
child: Opacity(
opacity: 0.24,
child: Image.asset(
'assets/images/aurora_wave_banner.png',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
),
),
),
),
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(t.radiusLg + 8),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.black.withValues(alpha: 0.58),
Colors.black.withValues(alpha: 0.18),
],
),
),
),
),
Positioned(
right: -36,
top: -42,
child: _Orb(
color: t.electricMagenta.withValues(alpha: 0.38),
size: 128,
),
),
Positioned(
right: 10,
top: 10,
child: Opacity(
opacity: 0.18,
child: Image.asset(
'assets/icons/pluriwave_app_mark.png',
width: 120,
height: 120,
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
),
),
),
Positioned(
right: 44,
bottom: -54,
child: _Orb(
color: PluriWaveTokens.brightCyan.withValues(alpha: 0.22),
size: 116,
),
),
Padding(
padding: EdgeInsets.all(compact ? 2 : 4),
child: foreground(),
),
],
),
),
);
}
}
class PluriStatusPill extends StatelessWidget {
const PluriStatusPill({
super.key,
@@ -313,24 +105,3 @@ class PluriEmptyState extends StatelessWidget {
);
}
}
class _Orb extends StatelessWidget {
const _Orb({required this.color, required this.size});
final Color color;
final double size;
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(colors: [color, color.withValues(alpha: 0)]),
),
),
);
}
}
+19 -1
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../l10n/gen/app_localizations.dart';
import '../tema/pluriwave_theme.dart';
import 'pluri_layout.dart';
/// S1 (Tier 1 visual fidelity): the prototype (`t4`) draws no global
/// `AppBar` — each root paints its own 56px title row inside its own
@@ -15,11 +16,19 @@ class PluriRootHeader extends StatelessWidget {
super.key,
required this.title,
required this.onSleepTimer,
this.actions = const <Widget>[],
});
final String title;
final VoidCallback onSleepTimer;
/// S2 (Tier 1 visual fidelity): the now-retired `PluriScreenHeader` was
/// also the only home for a couple of roots' SINGLE functional action —
/// Alarmas' create-alarm button, Buscar's filters entry point. Rendered
/// before the shared bedtime button; empty by default (every other root
/// needs nothing extra here).
final List<Widget> actions;
static const double height = 56;
@override
@@ -29,7 +38,15 @@ class PluriRootHeader extends StatelessWidget {
return SizedBox(
height: height,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 12, 0),
// 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(
@@ -40,6 +57,7 @@ class PluriRootHeader extends StatelessWidget {
style: type.sectionTitle,
),
),
...actions,
IconButton(
icon: const Icon(Icons.bedtime_outlined),
tooltip: l10n.sleepTimer,