Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 39 additions & 36 deletions app/lib/widgets/auth/auth_page_layouts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,48 +32,51 @@ class MobileAuthLayout extends StatelessWidget {

return Scaffold(
backgroundColor: theme.colorScheme.surface,
body: Column(
children: [
CompactAuthHeader(isDark: isDark, height: ComponentSizes.mobileHeroHeight),
Expanded(
child: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Spacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: Spacing.xl),
if (showHeader) ...[
// const AuthBranding(),
const SizedBox(height: Spacing.md),
Text(
heading,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
body: SafeArea(
top: false,
Comment on lines +35 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit the safe area to the bottom inset

On devices with nonzero horizontal safe-area padding that still select MobileAuthLayout—for example, a notched phone in landscape at 600–839 logical pixels—the default left and right settings also inset the entire column. This narrows CompactAuthHeader and leaves surface-colored gutters beside the formerly full-width clipped hero, even though this change only needs bottom protection; disable the horizontal insets or apply the bottom safe area below the hero.

AGENTS.md reference: AGENTS.md:L22-L24

Useful? React with 👍 / 👎.

child: Column(
children: [
CompactAuthHeader(isDark: isDark, height: ComponentSizes.mobileHeroHeight),
Expanded(
child: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Spacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: Spacing.xl),
if (showHeader) ...[
// const AuthBranding(),
const SizedBox(height: Spacing.md),
Text(
heading,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
Text(
subtitle,
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant),
),
),
Text(
subtitle,
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurfaceVariant),
),
const SizedBox(height: Spacing.lg),
],
form,
const Spacer(),
...footer,
const SizedBox(height: Spacing.lg),
],
form,
const Spacer(),
...footer,
const SizedBox(height: Spacing.lg),
],
),
),
),
),
],
],
),
),
),
],
],
),
),
);
}
Expand Down
50 changes: 13 additions & 37 deletions app/lib/widgets/goals/active_goal_details_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,29 +74,8 @@ class _ActiveGoalDetailsSheetState extends State<ActiveGoalDetailsSheet> {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return AppBottomSheet(
header: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Icon(_getIconForType(widget.goal.type), size: 24, color: colorScheme.onPrimaryContainer),
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.goal.description, style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
Text(_getGoalTypeLabel(), style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
],
),
),
],
),
title: widget.goal.description,
onClose: () => Navigator.of(context).pop(),
footer: BottomSheetActions(
secondary: OutlinedButton(
onPressed: () {
Expand All @@ -115,6 +94,8 @@ class _ActiveGoalDetailsSheetState extends State<ActiveGoalDetailsSheet> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_getGoalTypeLabel(), style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
const SizedBox(height: Spacing.lg),
// Progress section
_buildProgressSection(context, colorScheme, textTheme),
const SizedBox(height: Spacing.lg),
Expand Down Expand Up @@ -168,7 +149,9 @@ class _ActiveGoalDetailsSheetState extends State<ActiveGoalDetailsSheet> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Progress', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
Expanded(
child: Text('Progress', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
),
if (!_isEditingProgress)
TextButton.icon(
onPressed: () => setState(() => _isEditingProgress = true),
Expand Down Expand Up @@ -289,7 +272,9 @@ class _ActiveGoalDetailsSheetState extends State<ActiveGoalDetailsSheet> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Target', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
Expanded(
child: Text('Target', style: textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
),
if (!_isEditingTarget)
TextButton.icon(
onPressed: () => setState(() => _isEditingTarget = true),
Expand Down Expand Up @@ -381,7 +366,9 @@ class _ActiveGoalDetailsSheetState extends State<ActiveGoalDetailsSheet> {
children: [
Icon(icon, size: 20, color: colorScheme.onSurfaceVariant),
const SizedBox(width: Spacing.sm),
Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
Expanded(
child: Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Text(
Expand Down Expand Up @@ -560,17 +547,6 @@ class _ActiveGoalDetailsSheetState extends State<ActiveGoalDetailsSheet> {
return '${months[date.month - 1]} ${date.year}';
}

IconData _getIconForType(GoalType type) {
switch (type) {
case GoalType.books:
return Icons.menu_book_outlined;
case GoalType.pages:
return Icons.article_outlined;
case GoalType.minutes:
return Icons.schedule_outlined;
}
}

IconData _getRecurrenceIcon() {
if (widget.goal.isCustomPeriod) return Icons.date_range;
if (widget.goal.isRecurring) return Icons.repeat;
Expand Down
31 changes: 9 additions & 22 deletions app/lib/widgets/goals/completed_goal_chip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -281,28 +281,10 @@ class _CompletedGoalDetailsSheet extends StatelessWidget {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return AppBottomSheet(
header: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(color: colorScheme.tertiaryContainer, shape: BoxShape.circle),
child: Icon(Icons.emoji_events, size: 24, color: colorScheme.onTertiaryContainer),
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Goal completed!', style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
Text(goal.description, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
],
),
),
],
),
title: 'Goal completed',
onClose: () => Navigator.of(context).pop(),
footer: BottomSheetActions(
primary: OutlinedButton(
secondary: OutlinedButton(
onPressed: () {
Navigator.of(context).pop();
_showDeleteConfirmation(context);
Expand All @@ -313,11 +295,14 @@ class _CompletedGoalDetailsSheet extends StatelessWidget {
),
child: const Text('Delete goal'),
),
primary: FilledButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Done')),
),
body: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(goal.description, style: textTheme.titleMedium),
const SizedBox(height: Spacing.lg),
// Stats grid
Container(
padding: const EdgeInsets.all(Spacing.md),
Expand Down Expand Up @@ -377,7 +362,9 @@ class _CompletedGoalDetailsSheet extends StatelessWidget {
children: [
Icon(icon, size: 20, color: colorScheme.onSurfaceVariant),
const SizedBox(width: Spacing.sm),
Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
Expanded(
child: Text(label, style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Text(
Expand Down
4 changes: 2 additions & 2 deletions app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -555,8 +555,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: 7992c8b518fcb900f941217a7ea8455d001c1aea
resolved-ref: 7992c8b518fcb900f941217a7ea8455d001c1aea
ref: "7992c8b518fcb900f941217a7ea8455d001c1aea"
resolved-ref: "7992c8b518fcb900f941217a7ea8455d001c1aea"
url: "https://github.com/PapyrusReader/reader.git"
source: git
version: "0.0.1"
Expand Down
32 changes: 32 additions & 0 deletions app/test/widgets/auth/auth_page_layouts_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@ void main() {
expect(tester.getTopLeft(branding).dy, Spacing.lg);
});

for (final keyboard in [false, true]) {
testWidgets('mobile auth footer stays above ${keyboard ? 'the keyboard' : 'system navigation'}', (tester) async {
setViewport(tester, const Size(390, 700));
tester.view.viewPadding = const FakeViewPadding(bottom: 48);
tester.view.padding = FakeViewPadding(bottom: keyboard ? 0 : 48);
tester.view.viewInsets = FakeViewPadding(bottom: keyboard ? 300 : 0);
addTearDown(tester.view.resetViewPadding);
addTearDown(tester.view.resetPadding);
addTearDown(tester.view.resetViewInsets);
var continued = false;

await tester.pumpWidget(
MaterialApp(
home: MobileAuthLayout(
heading: 'Welcome back',
subtitle: 'Sign in to continue',
form: const SizedBox(height: 300),
footer: [TextButton(onPressed: () => continued = true, child: const Text('Continue offline'))],
),
),
);
await tester.drag(find.byType(CustomScrollView), const Offset(0, -700));
await tester.pumpAndSettle();

final button = find.widgetWithText(TextButton, 'Continue offline');
expect(tester.getRect(button).bottom, lessThanOrEqualTo(keyboard ? 400 : 652));
expect(tester.takeException(), isNull);
await tester.tap(button);
expect(continued, isTrue);
});
}

testWidgets('desktop swap button is focused after form controls', (tester) async {
setViewport(tester, const Size(1200, 800));

Expand Down
77 changes: 77 additions & 0 deletions app/test/widgets/goals/goal_details_sheets_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:papyrus/models/reading_goal.dart';
import 'package:papyrus/themes/app_theme.dart';
import 'package:papyrus/widgets/goals/active_goal_details_sheet.dart';
import 'package:papyrus/widgets/goals/completed_goal_chip.dart';
import 'package:papyrus/widgets/shared/app_bottom_sheet.dart';

void main() {
final goal = ReadingGoal(
id: 'goal',
type: GoalType.books,
targetValue: 12,
currentValue: 4,
period: GoalPeriod.yearly,
startDate: DateTime(2026),
endDate: DateTime(2026, 12, 31),
);

Future<void> openSheet(WidgetTester tester, {required bool completed, double width = 390}) async {
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = Size(width, 700);
tester.view.padding = const FakeViewPadding(bottom: 48);
tester.view.viewPadding = const FakeViewPadding(bottom: 48);
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.dark,
home: Scaffold(
body: Builder(
builder: (context) => TextButton(
onPressed: () {
if (completed) {
CompletedGoalChip.showDetailsSheet(context, goal: goal);
} else {
ActiveGoalDetailsSheet.show(context, goal: goal);
}
},
child: const Text('Open'),
),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
}

for (final completed in [false, true]) {
for (final width in [320.0, 390.0]) {
testWidgets('${completed ? 'completed' : 'active'} goal actions fit above system navigation at $width', (
tester,
) async {
await openSheet(tester, completed: completed, width: width);
final done = find.widgetWithText(FilledButton, 'Done');
final delete = find.widgetWithText(OutlinedButton, completed ? 'Delete goal' : 'Delete');
expect(done.hitTestable(), findsOneWidget);
expect(delete.hitTestable(), findsOneWidget);
expect(find.byTooltip('Close').hitTestable(), findsOneWidget);
expect(tester.getRect(done).bottom, lessThanOrEqualTo(652));
expect(tester.getRect(delete).top, tester.getRect(done).top);
expect(tester.getRect(delete).height, tester.getRect(done).height);
expect(tester.takeException(), isNull);
await tester.tap(done);
await tester.pumpAndSettle();
expect(find.byType(AppBottomSheet), findsNothing);
});
}

testWidgets('${completed ? 'completed' : 'active'} goal header closes the sheet', (tester) async {
await openSheet(tester, completed: completed);
await tester.tap(find.byTooltip('Close'));
await tester.pumpAndSettle();
expect(find.byType(AppBottomSheet), findsNothing);
});
}
}
Loading