Components
Conditions card
The headline card: a title and advice from the condition result, the Explore button, the cloud bar and four rise and set cells.
Preview
Clear Skies
Perfect visibility for observation
SUNRISE06:12
SUNSET18:47
MOONRISE21:30
MOONSET08:02
Variants
Pick a variant with the controls. The preview and both code blocks follow.
Night
Clear Skies
Perfect visibility for observation
SUNRISE06:12
SUNSET18:47
MOONRISE21:30
MOONSET08:02
Variant.tsx
<AppConditionsCard title="Clear Skies" subtitle="Perfect visibility for observation" cloud={12}/>Implementation
The React tab is this site's own component, the same source that draws the preview, styled by the CSS below it. The Flutter tab is the Dart a screen builds it with, from the app's tokens.
Usage
<AppConditionsCard cloud={12} title="Clear Skies" subtitle="Perfect visibility for observation" />app-widgets.tsx · AppTimeCell
/** One of the four rise and set cells of the conditions card. */export function AppTimeCell({ label, time }: { label: string; time?: string }) { return ( <div className="app-cell"> <span className="app-label-sm">{label}</span> <span className="app-label-md">{time ?? "--:--"}</span> </div> );}app-widgets.tsx · AppConditionsCard
/** ConditionsCard: headline, the Explore button, the cloud bar and the four time cells. */export function AppConditionsCard({ title = "Clear Skies", subtitle = "Perfect visibility for observation", cloud = 12, times = { sunrise: "06:12", sunset: "18:47", moonrise: "21:30", moonset: "08:02" },}: { title?: string; subtitle?: string; cloud?: number; times?: { sunrise?: string; sunset?: string; moonrise?: string; moonset?: string };}) { return ( <div className="app-ui app-card is-conditions"> <div className="app-card-head"> <div> <div className="app-heading">{title}</div> <div className="app-label-sm">{subtitle}</div> </div> <AppButton size="sm">Explore</AppButton> </div> <AppCloudBar value={cloud} /> <div className="app-cells"> <AppTimeCell label="SUNRISE" time={times.sunrise} /> <AppTimeCell label="SUNSET" time={times.sunset} /> <AppTimeCell label="MOONRISE" time={times.moonrise} /> <AppTimeCell label="MOONSET" time={times.moonset} /> </div> </div> );}app-ui.css
.app-card { position: relative; box-sizing: border-box; display: flex; flex-direction: column; padding: var(--spacing-f13); border-radius: var(--radius-f21); border: var(--spacing-f1) solid var(--hair-strong); background: var(--sheen), var(--fill); box-shadow: var(--highlight); backdrop-filter: blur(var(--spacing-f13)); overflow: hidden;}.app-card.is-conditions { width: min(100%, var(--container-f377)); gap: var(--spacing-f13); }.app-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--spacing-f8); }.app-cells { display: flex; gap: var(--spacing-f8); }.app-cell { flex: 1; display: grid; align-content: center; justify-items: center; height: var(--spacing-f55); border-radius: var(--radius-f13); border: var(--spacing-f1) solid var(--hair); background: var(--sheen), var(--fill); box-shadow: var(--highlight); box-sizing: border-box;}Usage
const ConditionsCard() // reads weatherProvider and conditionQualityProviderastr_glass.dart · AstrGlassCard
/// The glass surface for cards: a sheen over the surface and a hairline edge./// Cards do not glow; glow belongs to buttons and tiles. Mini cards are/// [AstrSpace.f144] wide and 144 + 21 tall.class AstrGlassCard extends StatelessWidget { const AstrGlassCard({ super.key, required this.child, this.width, this.height, }); final Widget child; final double? width; final double? height; @override Widget build(BuildContext context) { final Color text = AstrColors.spaceGrey[50]!; final BorderRadius radius = BorderRadius.circular(AstrRadius.f21); return RepaintBoundary( child: ClipRRect( borderRadius: radius, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: AstrSpace.f13, sigmaY: AstrSpace.f13), child: Container( width: width, height: height, padding: const EdgeInsets.all(AstrSpace.f13), decoration: BoxDecoration( borderRadius: radius, border: Border.all(color: text.withValues(alpha: Mag.m5)), color: _ink.withValues(alpha: Mag.m1), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: <Color>[ text.withValues(alpha: Mag.m5), text.withValues(alpha: Mag.m8), ], ), ), child: child, ), ), ), ); }}conditions_card.dart · ConditionsCard
class ConditionsCard extends ConsumerWidget { const ConditionsCard({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { // Watch weatherProvider for cloud cover final AsyncValue<Weather> weatherAsync = ref.watch(weatherProvider); // Watch conditionQualityProvider for title/subtitle final AsyncValue<ConditionResult> conditionQualityAsync = ref.watch(conditionQualityProvider); final double cloudCover = weatherAsync.valueOrNull?.cloudCover ?? 0.0; final bool isWeatherLoading = weatherAsync.isLoading; final String? weatherError = weatherAsync.hasError ? weatherAsync.error.toString() : null; final String titleText = conditionQualityAsync.valueOrNull?.shortSummary ?? 'Clear Skies'; final String subtitleText = conditionQualityAsync.valueOrNull?.detailedAdvice ?? 'Perfect visibility for observation'; return RepaintBoundary( child: ClipRRect( borderRadius: BorderRadius.circular(AppSpacing.kCardRadius), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 2, sigmaY: 2), child: Container( width: double.infinity, height: AppSpacing.kConditionsCardHeight, decoration: BoxDecoration( color: AppColors.surfaceOverlay, border: Border.all(color: AppColors.borderPrimary), borderRadius: BorderRadius.circular(AppSpacing.kCardRadius), ), child: Stack( children: <Widget>[ // Header row: left=16, top=16, right=16 Positioned( left: AppSpacing.kCardPadding, top: AppSpacing.kCardPadding, right: AppSpacing.kCardPadding, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ SizedBox( width: 180, // slightly wider to accommodate longer titles/subtitles comfortably child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: <Widget>[ Text( titleText, style: AppTypography.heading.copyWith(color: AppColors.textPrimary), softWrap: true, ), const SizedBox(height: 4), Text( subtitleText, style: AppTypography.labelSm.copyWith(color: AppColors.textMuted), softWrap: true, ), ], ), ), // Explore Button GestureDetector( onTap: () { showModalBottomSheet<void>( context: context, useRootNavigator: true, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (BuildContext context) => const AtmosphericsSheet(), ); }, child: Container( width: AppSpacing.kExploreButtonWidth, height: AppSpacing.kExploreButtonHeight, decoration: BoxDecoration( color: AppColors.accent, borderRadius: BorderRadius.circular(AppSpacing.kExploreButtonRadius), ), alignment: Alignment.center, child: const Text( 'Explore', style: AppTypography.labelMd, ), ), ), ], ), ), // Cloud Cover: left=16, top=69, right=16 Positioned( left: AppSpacing.kCardPadding, top: AppSpacing.kCloudSectionTop, right: AppSpacing.kCardPadding, child: CloudBar( cloudCoverPercentage: cloudCover, isLoading: isWeatherLoading, errorMessage: weatherError, ), ), // Rise/Set cells: left=16, top=162, right=16, height=64 Positioned( left: AppSpacing.kCardPadding, top: 162, right: AppSpacing.kCardPadding, height: AppSpacing.kSunMoonCellHeight, child: Consumer( builder: (BuildContext context, WidgetRef ref, Widget? child) { final ObjectDetailState sunState = ref.watch(objectDetailNotifierProvider('sun')); final ObjectDetailState moonState = ref.watch(objectDetailNotifierProvider('moon')); final CelestialObject? sun = sunState.object; final CelestialObject? moon = moonState.object; DateTime? sunRise; DateTime? sunSet; DateTime? moonRise; DateTime? moonSet; if (sun != null) { final Map<String, DateTime?>? sunTimes = ref.watch(riseSetProvider(sun)).valueOrNull; if (sunTimes != null) { sunRise = sunTimes['rise']; sunSet = sunTimes['set']; } } if (moon != null) { final Map<String, DateTime?>? moonTimes = ref.watch(riseSetProvider(moon)).valueOrNull; if (moonTimes != null) { moonRise = moonTimes['rise']; moonSet = moonTimes['set']; } } return Row( children: <Widget>[ Expanded(child: _TimeCell(label: 'SUNRISE', time: sunRise)), const SizedBox(width: 8), Expanded(child: _TimeCell(label: 'SUNSET', time: sunSet)), const SizedBox(width: 8), Expanded(child: _TimeCell(label: 'MOONRISE', time: moonRise)), const SizedBox(width: 8), Expanded(child: _TimeCell(label: 'MOONSET', time: moonSet)), ], ); }, ), ), ], ), ), ), ), ); }}conditions_card.dart · _TimeCell
class _TimeCell extends StatelessWidget { const _TimeCell({ required this.label, required this.time, }); final String label; final DateTime? time; @override Widget build(BuildContext context) { // Convert to local time: astronomical calculations return UTC DateTimes; // displaying hour/minute without toLocal() shows wrong times outside UTC. final DateTime? localTime = time?.toLocal(); final String timeStr = localTime != null ? '${localTime.hour.toString().padLeft(2, '0')}:${localTime.minute.toString().padLeft(2, '0')}' : '--:--'; return Container( height: AppSpacing.kSunMoonCellHeight, decoration: BoxDecoration( color: AppColors.surfaceGlass, border: Border.all(color: AppColors.borderSubtle), borderRadius: BorderRadius.circular(AppSpacing.kInnerRadius), ), child: Stack( children: <Widget>[ Positioned( top: AppSpacing.kSunMoonLabelTop, left: 0, right: 0, child: Center( child: Text( label, style: AppTypography.micro.copyWith(color: AppColors.textMuted), ), ), ), Positioned( top: AppSpacing.kSunMoonTimeTop, left: 0, right: 0, child: Center( child: Text( timeStr, style: AppTypography.labelMd.copyWith(color: AppColors.textPrimary), ), ), ), ], ), ); }}API reference
| React prop | Flutter parameter | Type | Default | Meaning |
|---|---|---|---|---|
| title, subtitle | conditionQuality.shortSummary, detailedAdvice | string | Clear Skies | Headline and advice. |
| cloud | weather.cloudCover | number | 12 | Cloud bar value. |
| times | riseSetProvider | object | sample | Sunrise, sunset, moonrise, moonset. |
Design tokens
Every value on this component is a system token. The last column is what the app widget uses today, so the gap to close is visible.
| Role | CSS token | Dart token | App widget today |
|---|---|---|---|
| Width | min(100%, --container-f377) | AstrLayout (377) | 345 |
| Padding, gap | --spacing-f13 | AstrSpace.f13 | 16, absolute offsets 69 and 162 |
| Title, advice | --text-s0, --text-s-2 | AstrType.s0, sNeg2 | heading 16, labelSm 10 |
| Explore | button, compact | AstrGlassButton(compact: true) | accent pill 100 by 27 |
| Time cells | --spacing-f55 high, --radius-f13 | AstrSize.control, AstrRadius.f13 | 64 high, 12 |