Compare commits

...
2 Commits
Author SHA1 Message Date
Jeroen SchweitzerandClaude Opus 4.5 a376482cd1 feat: align horizon line at 50px across all environment widgets
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m24s
- 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 <noreply@anthropic.com>
2026-01-08 19:50:13 +01:00
Jeroen SchweitzerandClaude Opus 4.5 f31967aa3e feat: bottom-aligned widgets and wind direction
Build and Push / release (push) Successful in 2s
Build and Push / build (push) Successful in 3m27s
- All environment widgets align content from bottom for visual harmony
- Wind chip now shows direction (e.g., "SE 14 km/h")

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 19:10:24 +01:00
5 changed files with 219 additions and 178 deletions
+21
View File
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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
- **Bottom-aligned widget content** - All environment widgets now align content from the bottom
- Creates consistent visual baseline across Sun Position, Weather, Air Quality, and Forecast cards
- Footers (weather details, pollutants) sit at the same level across cards
### Added
- **Wind direction in Weather** - Wind chip now shows direction (e.g., "SE 14 km/h")
## [1.5.7] - 2026-01-08 ## [1.5.7] - 2026-01-08
### Fixed ### Fixed
+80 -71
View File
@@ -69,88 +69,97 @@ class AirQualityWidget extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Content area with minimum height to match other widgets // Content area with fixed height, horizon line at 50px from bottom
ConstrainedBox( SizedBox(
constraints: const BoxConstraints(minHeight: 170), height: 170,
child: Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(height: 8), // Main content positioned above horizon
// AQI Display Positioned(
Row( left: 0,
children: [ right: 0,
// AQI number with colored background bottom: 50,
Container( child: Row(
width: 64, children: [
height: 64, // AQI number with colored background
decoration: BoxDecoration( Container(
color: aqi.level.color.withValues(alpha: 0.15), width: 64,
borderRadius: BorderRadius.circular(12), height: 64,
border: Border.all( decoration: BoxDecoration(
color: aqi.level.color.withValues(alpha: 0.3), color: aqi.level.color.withValues(alpha: 0.15),
width: 2, borderRadius: BorderRadius.circular(12),
border: Border.all(
color: aqi.level.color.withValues(alpha: 0.3),
width: 2,
),
), ),
), child: Center(
child: Center( child: Text(
child: Text( '${aqi.index}',
'${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,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium .headlineMedium
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.bold,
color: aqi.level.color, 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(),
), ),
],
], ],
), ),
), ),
+17 -13
View File
@@ -108,19 +108,23 @@ class ForecastWidget extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Content area with minimum height to match other widgets // Content area with fixed height, card bottoms at horizon (50px from bottom)
ConstrainedBox( SizedBox(
constraints: const BoxConstraints(minHeight: 170), height: 170,
child: Center( child: Padding(
child: SizedBox( padding: const EdgeInsets.only(bottom: 50),
height: 110, child: Align(
child: ListView.separated( alignment: Alignment.bottomCenter,
scrollDirection: Axis.horizontal, child: SizedBox(
itemCount: forecast!.length, height: 110,
separatorBuilder: (_, i) => const SizedBox(width: 12), child: ListView.separated(
itemBuilder: (context, index) { scrollDirection: Axis.horizontal,
return _ForecastDayCard(day: forecast![index]); itemCount: forecast!.length,
}, separatorBuilder: (_, i) => const SizedBox(width: 12),
itemBuilder: (context, index) {
return _ForecastDayCard(day: forecast![index]);
},
),
), ),
), ),
), ),
+100 -93
View File
@@ -71,114 +71,118 @@ class WeatherWidget extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Content area with minimum height to match other widgets // Content area with fixed height, horizon line at 50px from bottom
ConstrainedBox( SizedBox(
constraints: const BoxConstraints(minHeight: 170), height: 170,
child: Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(height: 8), // Main content positioned above horizon
// Main weather display (matching AQI layout) Positioned(
Row( left: 0,
children: [ right: 0,
// Weather icon in box (like AQI number box) bottom: 50,
Container( child: Row(
width: 64, children: [
height: 64, // Weather icon in box
decoration: BoxDecoration( Container(
color: (weather.iconColor ?? colorScheme.primary) width: 64,
.withValues(alpha: 0.15), height: 64,
borderRadius: BorderRadius.circular(12), decoration: BoxDecoration(
border: Border.all(
color: (weather.iconColor ?? colorScheme.primary) color: (weather.iconColor ?? colorScheme.primary)
.withValues(alpha: 0.3), .withValues(alpha: 0.15),
width: 2, 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( const SizedBox(width: 16),
child: Icon( // Temperature and condition
weather.icon, Expanded(
size: 32, child: Column(
color: weather.iconColor ?? colorScheme.primary, mainAxisSize: MainAxisSize.min,
), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
),
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),
Text( Text(
weather.location, '${weather.temperature.round()}°${weather.unit.symbol}',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.labelSmall .titleMedium
?.copyWith( ?.copyWith(fontWeight: FontWeight.w600),
color: colorScheme.outline, ),
const SizedBox(height: 4),
Text(
weather.condition,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, 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.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}',
),
],
),
),
], ],
), ),
), ),
@@ -341,6 +345,7 @@ WeatherData _fromApiData(api.WeatherData data) {
icon: _getWeatherIcon(data.conditions, data.icon), icon: _getWeatherIcon(data.conditions, data.icon),
humidity: data.humidity, humidity: data.humidity,
windSpeed: data.windSpeed, windSpeed: data.windSpeed,
windDirection: data.windDirection,
feelsLike: data.feelsLike, feelsLike: data.feelsLike,
iconColor: _getWeatherColor(data.conditions), iconColor: _getWeatherColor(data.conditions),
); );
@@ -405,6 +410,7 @@ class WeatherData {
this.unit = TemperatureUnit.celsius, this.unit = TemperatureUnit.celsius,
this.humidity, this.humidity,
this.windSpeed, this.windSpeed,
this.windDirection,
this.windUnit = 'km/h', this.windUnit = 'km/h',
this.feelsLike, this.feelsLike,
this.iconColor, this.iconColor,
@@ -417,6 +423,7 @@ class WeatherData {
final TemperatureUnit unit; final TemperatureUnit unit;
final int? humidity; final int? humidity;
final double? windSpeed; final double? windSpeed;
final String? windDirection;
final String windUnit; final String windUnit;
final double? feelsLike; final double? feelsLike;
final Color? iconColor; final Color? iconColor;
+1 -1
View File
@@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.5.7+1 version: 1.5.9+1
environment: environment:
sdk: ^3.10.4 sdk: ^3.10.4