part of '../custom_workweek_calendar.dart'; class _OutsideHoursStrip extends StatelessWidget { final DateTime weekStart; final List appointments; final double rulerWidth; final void Function(Appointment) onAppointmentTap; final bool Function(Appointment) isCrossedOut; const _OutsideHoursStrip({ super.key, required this.weekStart, required this.appointments, required this.rulerWidth, required this.onAppointmentTap, required this.isCrossedOut, }); @override Widget build(BuildContext context) { final outside = partitionAppointmentsForWeek( appointments, weekStart, ).outside; if (outside.every((day) => day.isEmpty)) return const SizedBox.shrink(); final theme = Theme.of(context); final maxChipsPerDay = outside .map( (day) => day.length > kOutsideChipsMaxVisible ? kOutsideChipsMaxVisible : day.length, ) .fold(0, (m, c) => c > m ? c : m); final stripHeight = kOutsideStripVerticalPadding * 2 + maxChipsPerDay * kOutsideChipHeight + (maxChipsPerDay > 1 ? (maxChipsPerDay - 1) * kOutsideChipSpacing : 0); return Container( // Scaffold-Background, damit die Ganztagestermine-Leiste optisch nahtlos // an Header und Stundenplan-Hintergrund anschließt; surfaceContainerLowest // ist in M3-Light reinweiß und sticht gegen die getönte Seed-Surface ab. color: theme.scaffoldBackgroundColor, padding: const EdgeInsets.symmetric( vertical: kOutsideStripVerticalPadding, ), child: SizedBox( height: stripHeight - kOutsideStripVerticalPadding * 2, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: rulerWidth), for (var d = 0; d < 5; d++) Expanded( child: _OutsideDayColumn( appointments: outside[d], onAppointmentTap: onAppointmentTap, isCrossedOut: isCrossedOut, ), ), ], ), ), ); } } class _OutsideDayColumn extends StatelessWidget { final List appointments; final void Function(Appointment) onAppointmentTap; final bool Function(Appointment) isCrossedOut; const _OutsideDayColumn({ required this.appointments, required this.onAppointmentTap, required this.isCrossedOut, }); void _showOverflow(BuildContext context, List hidden) { _showAppointmentOverflowSheet( context, hidden, onAppointmentTap: onAppointmentTap, isCrossedOut: isCrossedOut, subtitle: _subtitleFor, ); } static String _subtitleFor(Appointment a) { if (isAllDayLike(a)) return A11yLabels.allDay; return '${a.startTime.formatHm()}–${a.endTime.formatHm()}'; } @override Widget build(BuildContext context) { if (appointments.isEmpty) return const SizedBox.shrink(); final sorted = [...appointments] ..sort((a, b) { final aLike = isAllDayLike(a); final bLike = isAllDayLike(b); if (aLike && !bLike) return -1; if (!aLike && bLike) return 1; return a.startTime.compareTo(b.startTime); }); final visible = sorted.length <= kOutsideChipsMaxVisible ? sorted : sorted.take(kOutsideChipsMaxVisible - 1).toList(); final overflow = sorted.length <= kOutsideChipsMaxVisible ? const [] : sorted.skip(kOutsideChipsMaxVisible - 1).toList(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < visible.length; i++) ...[ if (i > 0) const SizedBox(height: kOutsideChipSpacing), SizedBox( height: kOutsideChipHeight, child: _OutsideChip( appointment: visible[i], crossedOut: isCrossedOut(visible[i]), onTap: () => onAppointmentTap(visible[i]), ), ), ], if (overflow.isNotEmpty) ...[ const SizedBox(height: kOutsideChipSpacing), SizedBox( height: kOutsideChipHeight, child: _OutsideOverflowChip( count: overflow.length, onTap: () => _showOverflow(context, overflow), ), ), ], ], ), ); } } class _OutsideChip extends StatelessWidget { final Appointment appointment; final bool crossedOut; final VoidCallback onTap; const _OutsideChip({ required this.appointment, required this.crossedOut, required this.onTap, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final allDay = isAllDayLike(appointment); final timeLabel = allDay ? null : appointment.startTime.formatHm(); final semanticsLabel = [ appointment.subject, if (allDay) A11yLabels.allDay else '${appointment.startTime.formatHm()}–${appointment.endTime.formatHm()}', if (crossedOut) A11yLabels.cancelled, ].join(', '); // Past chips fade further, future/ongoing ones get a more saturated tint // so the strip no longer reads as one uniform grey block. final isPast = appointment.endTime.isBefore(DateTime.now()); final backgroundAlpha = isPast ? 38 : 120; final subjectColor = isPast ? theme.colorScheme.onSurfaceVariant : theme.colorScheme.onSurface; final subjectWeight = isPast ? FontWeight.w400 : FontWeight.w600; return Semantics( button: true, label: semanticsLabel, onTap: onTap, child: ExcludeSemantics( child: Material( color: appointment.color.withAlpha(backgroundAlpha), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(7)), ), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), child: Row( mainAxisSize: MainAxisSize.max, children: [ Expanded( child: Text( appointment.subject, maxLines: 1, overflow: TextOverflow.ellipsis, softWrap: false, style: theme.textTheme.labelSmall?.copyWith( color: subjectColor, fontWeight: subjectWeight, ), ), ), if (timeLabel != null) ...[ const SizedBox(width: 4), Flexible( child: Text( timeLabel, maxLines: 1, overflow: TextOverflow.fade, softWrap: false, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontSize: 10, ), ), ), ], ], ), ), ), ), ), ); } } class _OutsideOverflowChip extends StatelessWidget { final int count; final VoidCallback onTap; const _OutsideOverflowChip({required this.count, required this.onTap}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Semantics( button: true, label: A11yLabels.moreAppointments(count), onTap: onTap, child: ExcludeSemantics( child: Material( color: theme.colorScheme.secondaryContainer, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Center( child: Text( '+$count weitere', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, ), ), ), ), ), ), ); } }