From a376482cd126140f6213a6a818d6dc8661f3b389 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 8 Jan 2026 19:50:13 +0100 Subject: [PATCH] feat: align horizon line at 50px across all environment widgets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Weather/Air Quality dividers align with Sun Position horizon - Forecast card bottoms align with same horizon line - Unified visual rhythm across all cards 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 9 + lib/shared/widgets/air_quality_widget.dart | 146 ++++++++-------- lib/shared/widgets/forecast_widget.dart | 27 +-- lib/shared/widgets/weather_widget.dart | 186 +++++++++++---------- pubspec.yaml | 2 +- 5 files changed, 198 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5add9e6..840f81e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.5.9] - 2026-01-08 + +### Changed + +- **Aligned horizon line across all widgets** - Consistent visual baseline at 50px from bottom + - Weather and Air Quality dividers now align with Sun Position horizon line + - Forecast card bottoms align with the same horizon + - Creates unified visual rhythm across all environment cards + ## [1.5.8] - 2026-01-08 ### Changed diff --git a/lib/shared/widgets/air_quality_widget.dart b/lib/shared/widgets/air_quality_widget.dart index c80c122..5f46e64 100644 --- a/lib/shared/widgets/air_quality_widget.dart +++ b/lib/shared/widgets/air_quality_widget.dart @@ -69,87 +69,97 @@ class AirQualityWidget extends StatelessWidget { ), const SizedBox(height: 8), - // Content area with fixed height, content aligned to bottom + // Content area with fixed height, horizon line at 50px from bottom SizedBox( height: 170, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, + child: Stack( children: [ - // AQI Display - Row( - children: [ - // AQI number with colored background - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: aqi.level.color.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: aqi.level.color.withValues(alpha: 0.3), - width: 2, + // Main content positioned above horizon + Positioned( + left: 0, + right: 0, + bottom: 50, + child: Row( + children: [ + // AQI number with colored background + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: aqi.level.color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: aqi.level.color.withValues(alpha: 0.3), + width: 2, + ), ), - ), - child: Center( - child: Text( - '${aqi.index}', - style: Theme.of(context) - .textTheme - .headlineMedium - ?.copyWith( - fontWeight: FontWeight.bold, - color: aqi.level.color, - ), - ), - ), - ), - const SizedBox(width: 16), - - // Level info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - aqi.level.label, + child: Center( + child: Text( + '${aqi.index}', style: Theme.of(context) .textTheme - .titleMedium + .headlineMedium ?.copyWith( - fontWeight: FontWeight.w600, + fontWeight: FontWeight.bold, color: aqi.level.color, ), ), - const SizedBox(height: 4), - Text( - aqi.level.description, - style: - Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], + ), ), + const SizedBox(width: 16), + // Level info + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + aqi.level.label, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + color: aqi.level.color, + ), + ), + const SizedBox(height: 4), + Text( + aqi.level.description, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + // Horizon divider at fixed position + if (aqi.pollutants.isNotEmpty) + Positioned( + left: 0, + right: 0, + bottom: 38, + child: const Divider(height: 1), + ), + // Footer below horizon + if (aqi.pollutants.isNotEmpty) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Wrap( + spacing: 16, + runSpacing: 8, + children: aqi.pollutants + .map((p) => _PollutantChip(pollutant: p)) + .toList(), ), - ], - ), - - // Pollutants - if (aqi.pollutants.isNotEmpty) ...[ - const SizedBox(height: 16), - const Divider(height: 1), - const SizedBox(height: 12), - Wrap( - spacing: 16, - runSpacing: 8, - children: aqi.pollutants - .map((p) => _PollutantChip(pollutant: p)) - .toList(), ), - ], ], ), ), diff --git a/lib/shared/widgets/forecast_widget.dart b/lib/shared/widgets/forecast_widget.dart index dc66d54..1e4ed92 100644 --- a/lib/shared/widgets/forecast_widget.dart +++ b/lib/shared/widgets/forecast_widget.dart @@ -108,20 +108,23 @@ class ForecastWidget extends StatelessWidget { ), const SizedBox(height: 8), - // Content area with fixed height, content aligned to bottom + // Content area with fixed height, card bottoms at horizon (50px from bottom) SizedBox( height: 170, - child: Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 110, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: forecast!.length, - separatorBuilder: (_, i) => const SizedBox(width: 12), - itemBuilder: (context, index) { - return _ForecastDayCard(day: forecast![index]); - }, + child: Padding( + padding: const EdgeInsets.only(bottom: 50), + child: Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + height: 110, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: forecast!.length, + separatorBuilder: (_, i) => const SizedBox(width: 12), + itemBuilder: (context, index) { + return _ForecastDayCard(day: forecast![index]); + }, + ), ), ), ), diff --git a/lib/shared/widgets/weather_widget.dart b/lib/shared/widgets/weather_widget.dart index 63836ca..83e7b12 100644 --- a/lib/shared/widgets/weather_widget.dart +++ b/lib/shared/widgets/weather_widget.dart @@ -71,114 +71,118 @@ class WeatherWidget extends StatelessWidget { ), const SizedBox(height: 8), - // Content area with fixed height, content aligned to bottom + // Content area with fixed height, horizon line at 50px from bottom SizedBox( height: 170, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, + child: Stack( children: [ - // Main weather display (matching AQI layout) - Row( - children: [ - // Weather icon in box (like AQI number box) - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: (weather.iconColor ?? colorScheme.primary) - .withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - border: Border.all( + // Main content positioned above horizon + Positioned( + left: 0, + right: 0, + bottom: 50, + child: Row( + children: [ + // Weather icon in box + Container( + width: 64, + height: 64, + decoration: BoxDecoration( color: (weather.iconColor ?? colorScheme.primary) - .withValues(alpha: 0.3), - width: 2, + .withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: (weather.iconColor ?? colorScheme.primary) + .withValues(alpha: 0.3), + width: 2, + ), + ), + child: Center( + child: Icon( + weather.icon, + size: 32, + color: weather.iconColor ?? colorScheme.primary, + ), ), ), - child: Center( - child: Icon( - weather.icon, - size: 32, - color: weather.iconColor ?? colorScheme.primary, - ), - ), - ), - const SizedBox(width: 16), - - // Temperature and condition - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${weather.temperature.round()}°${weather.unit.symbol}', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Text( - weather.condition, - style: - Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - if (weather.location.isNotEmpty) ...[ - const SizedBox(height: 2), + const SizedBox(width: 16), + // Temperature and condition + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - weather.location, + '${weather.temperature.round()}°${weather.unit.symbol}', style: Theme.of(context) .textTheme - .labelSmall - ?.copyWith( - color: colorScheme.outline, + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + Text( + weather.condition, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), + if (weather.location.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + weather.location, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: colorScheme.outline, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ], - ], + ), ), - ), - ], - ), - - // Details - if (weather.humidity != null || weather.windSpeed != null) ...[ - const SizedBox(height: 16), - const Divider(height: 1), - const SizedBox(height: 12), - Wrap( - spacing: 16, - runSpacing: 8, - children: [ - if (weather.humidity != null) - _DetailChip( - label: 'Humidity', - value: '${weather.humidity}%', - ), - if (weather.windSpeed != null) - _DetailChip( - label: 'Wind', - value: weather.windDirection != null - ? '${weather.windDirection} ${weather.windSpeed!.round()} ${weather.windUnit}' - : '${weather.windSpeed!.round()} ${weather.windUnit}', - ), - if (weather.feelsLike != null) - _DetailChip( - label: 'Feels', - value: - '${weather.feelsLike!.round()}°${weather.unit.symbol}', - ), ], ), - ], + ), + // Horizon divider at fixed position + if (weather.humidity != null || weather.windSpeed != null) + Positioned( + left: 0, + right: 0, + bottom: 38, + child: const Divider(height: 1), + ), + // Footer below horizon + if (weather.humidity != null || weather.windSpeed != null) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Wrap( + spacing: 16, + runSpacing: 8, + children: [ + if (weather.humidity != null) + _DetailChip( + label: 'Humidity', + value: '${weather.humidity}%', + ), + if (weather.windSpeed != null) + _DetailChip( + label: 'Wind', + value: weather.windDirection != null + ? '${weather.windDirection} ${weather.windSpeed!.round()} ${weather.windUnit}' + : '${weather.windSpeed!.round()} ${weather.windUnit}', + ), + if (weather.feelsLike != null) + _DetailChip( + label: 'Feels', + value: '${weather.feelsLike!.round()}°${weather.unit.symbol}', + ), + ], + ), + ), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 14fb5e4..3b26086 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.5.8+1 +version: 1.5.9+1 environment: sdk: ^3.10.4