b9e68e3bc8
Per-screen application of the design system to the Flutter app. Mirrors the web's surface phase landed in FabledScribe v26.04.28.1. Foundation port shipped in 0f05f47; this is the surface work. Lucide icon migration - Added lucide_icons ^0.257.0 dependency - 107 Material Icons references → LucideIcons across 21 files. Drop-in IconData swap (Icon(LucideIcons.X) instead of Icon(Icons.x)). - Lucide import added to each touched file. Input border radius - theme.dart inputDecorationTheme borderRadius 24 → 8 in both light and dark themes. Doc says radius-md (8px) for inputs; previous pill shape was Material default that the doc deviates from. Illuminated Transcript pattern (ChatMessageBubble) - User bubble: accent-tinted border → neutral Pewter (scheme.outline). Asymmetric corner already correct (bottomRight 4px). - Assistant bubble: topLeft corner 4 → 16; only bottomLeft stays 4 (the "tail" effect, mirroring web's `border-bottom-left-radius: 4px`). Background switched from surfaceContainerHighest (Slate) to surface (Iron) per the doc spec "card surface". - Assistant bubble glow shadow added — accent-tinted blur (28px alpha 0.14) + depth shadow (8px alpha 0.4 black). Mirrors web's --color-bubble-asst-shadow. ActionColors wiring (Hybrid rule) - 5 'Delete' confirm buttons across notes / tasks / chat conversations / calendar event sheet → Oxblood action-destructive via the ActionColors ThemeExtension defined in the foundation port. Foreground for ghost/text variants, backgroundColor for filled. - Calendar event Save button → Moss action-primary. The first call site to wire ActionColors.primary; serves as the pattern for future Save reclassifications. - Other Save buttons (note edit, task edit, project edit, etc.) still flow through colorScheme.primary (dusty violet) and read as brand-moment. Reclassifying those is deferred — the wiring pattern is established and can be applied incrementally as files are touched. Indigo cleanup - 4 hardcoded #7C3AED / #5B21B6 literals → dusty-violet equivalents (#5B4A8A / #3F3560). Spots: project_tasks_screen color fallback (×2), journal_screen gradient. Verification - flutter analyze: No issues found What's deferred - Per-screen Save / Cancel reclassification beyond the calendar event Save button. Wiring pattern established; rollout opportunistic. - Long-form 1.7 line-height on assistant Markdown content (would require MarkdownStyleSheet work; minor). - Surface walk on Knowledge / Projects / Settings screens for any hardcoded styling that needs touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
175 lines
5.7 KiB
Dart
175 lines
5.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:table_calendar/table_calendar.dart';
|
|
|
|
import '../../data/models/calendar_event.dart';
|
|
import '../../providers/calendar_provider.dart';
|
|
import 'event_form_sheet.dart';
|
|
|
|
class CalendarScreen extends ConsumerWidget {
|
|
const CalendarScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final calAsync = ref.watch(calendarProvider);
|
|
final notifier = ref.read(calendarProvider.notifier);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Calendar')),
|
|
body: calAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('Could not load calendar.'),
|
|
const SizedBox(height: 12),
|
|
FilledButton.tonal(
|
|
onPressed: () => ref.invalidate(calendarProvider),
|
|
child: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
data: (cal) => Column(
|
|
children: [
|
|
TableCalendar<CalendarEvent>(
|
|
firstDay: DateTime(2020),
|
|
lastDay: DateTime(2030),
|
|
focusedDay: cal.focusedMonth,
|
|
selectedDayPredicate: (day) => isSameDay(day, cal.selectedDay),
|
|
eventLoader: (day) =>
|
|
cal.eventsByDay[dateOnly(day)] ?? [],
|
|
calendarFormat: CalendarFormat.month,
|
|
headerStyle: const HeaderStyle(
|
|
formatButtonVisible: false,
|
|
titleCentered: true,
|
|
),
|
|
calendarStyle: CalendarStyle(
|
|
selectedDecoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
shape: BoxShape.circle,
|
|
),
|
|
todayDecoration: BoxDecoration(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.primary
|
|
.withValues(alpha: 0.3),
|
|
shape: BoxShape.circle,
|
|
),
|
|
markerDecoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
onDaySelected: (selected, _) => notifier.selectDay(selected),
|
|
onPageChanged: (focused) => notifier.loadMonth(focused),
|
|
),
|
|
const Divider(height: 1),
|
|
Expanded(
|
|
child: RefreshIndicator(
|
|
onRefresh: () => ref.read(calendarProvider.notifier).refresh(),
|
|
child: _AgendaList(
|
|
events:
|
|
cal.eventsByDay[dateOnly(cal.selectedDay)] ?? [],
|
|
notifier: notifier,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
floatingActionButton: calAsync.hasValue
|
|
? FloatingActionButton(
|
|
onPressed: () => showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
useSafeArea: true,
|
|
builder: (_) => EventFormSheet(
|
|
event: null,
|
|
initialDate: calAsync.value!.selectedDay,
|
|
notifier: notifier,
|
|
),
|
|
),
|
|
child: const Icon(LucideIcons.plus),
|
|
)
|
|
: null,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Agenda list ───────────────────────────────────────────────────────────────
|
|
|
|
class _AgendaList extends StatelessWidget {
|
|
final List<CalendarEvent> events;
|
|
final CalendarNotifier notifier;
|
|
|
|
const _AgendaList({required this.events, required this.notifier});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (events.isEmpty) {
|
|
return ListView(
|
|
children: const [
|
|
SizedBox(height: 80),
|
|
Center(child: Text('No events')),
|
|
],
|
|
);
|
|
}
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
itemCount: events.length,
|
|
itemBuilder: (_, i) =>
|
|
_EventTile(event: events[i], notifier: notifier),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Event tile ────────────────────────────────────────────────────────────────
|
|
|
|
class _EventTile extends StatelessWidget {
|
|
final CalendarEvent event;
|
|
final CalendarNotifier notifier;
|
|
|
|
const _EventTile({required this.event, required this.notifier});
|
|
|
|
Color _dotColor(BuildContext context) {
|
|
if (event.color.isEmpty) return Theme.of(context).colorScheme.primary;
|
|
try {
|
|
return Color(int.parse(event.color.replaceFirst('#', '0xFF')));
|
|
} catch (_) {
|
|
return Theme.of(context).colorScheme.primary;
|
|
}
|
|
}
|
|
|
|
String _timeLabel() {
|
|
if (event.allDay) return 'All day';
|
|
final h = event.startDt.hour.toString().padLeft(2, '0');
|
|
final m = event.startDt.minute.toString().padLeft(2, '0');
|
|
return '$h:$m';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListTile(
|
|
leading: CircleAvatar(
|
|
radius: 6,
|
|
backgroundColor: _dotColor(context),
|
|
),
|
|
title: Text(event.title),
|
|
subtitle: Text(_timeLabel()),
|
|
onTap: () => showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
useSafeArea: true,
|
|
builder: (_) => EventFormSheet(
|
|
event: event,
|
|
initialDate: null,
|
|
notifier: notifier,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|