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
7 changes: 7 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ You can always edit this file by hand instead — the helpers just save effort.

### Fixed

- **Six more number lines are set at the width the design gives them.** The
lesson and mini-game position counter, the Coffee Challenges count, a
dictionary category's term count, the core-lessons count, the tree hero's
count and the stage-still-to-come line each took their rung's letter spacing
instead of their own — the widths run 0.06em to 0.12em, not one shared step.
The challenge row also drew its kicker in mono and its count in the wrong
face and rung; both now follow the design.
- **The Tour's first card no longer opens under the clock.** Its card takes the
side of the target the design names whenever it fits there, and the other
side when it does not — so the tall Today card, which the design's rule sends
Expand Down
6 changes: 5 additions & 1 deletion lib/core/widgets/roast_meter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ class RoastMeter extends StatelessWidget {
const SizedBox(width: AppSpacing.xs),
Text(
'${zeroPadded(position)} / ${zeroPadded(total)}',
style: AppText.label(mood: mood, face: AppFace.mono),
style: AppText.label(
mood: mood,
face: AppFace.mono,
tracking: AppTracking.hint,
),
),
],
),
Expand Down
16 changes: 8 additions & 8 deletions lib/features/challenges/presentation/challenge_stat_row.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:brew_path/core/widgets/smallcaps_label.dart';
import 'package:brew_path/features/challenges/domain/challenge_providers.dart';
import 'package:brew_path/shared/theme/app_radii.dart';
import 'package:brew_path/shared/theme/app_spacing.dart';
import 'package:brew_path/shared/theme/app_text.dart';
import 'package:brew_path/shared/theme/mood_colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand Down Expand Up @@ -41,7 +43,6 @@ class _StatRow extends StatelessWidget {

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final mood = context.mood;
// A floor, so an untouched bar still reads as a bar rather than as a rule.
final fraction = total == 0
Expand All @@ -65,15 +66,14 @@ class _StatRow extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'COFFEE CHALLENGES',
style: theme.textTheme.labelSmall?.copyWith(
color: mood.inkMute,
),
),
const SmallcapsLabel('Coffee challenges'),
Text(
'$brewed / $total',
style: theme.textTheme.labelLarge,
style: AppText.label(
mood: mood,
face: AppFace.mono,
tracking: AppTracking.count,
),
),
],
),
Expand Down
6 changes: 2 additions & 4 deletions lib/features/dictionary/presentation/category_index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ const double _glyphSize = 22;
const double _rowPadding = AppSpacing.md;

/// The way into a category: its mark, its name, what it covers, and how many
/// terms sit behind it.
///
/// The index is how the design opens the dictionary — a learner arrives
/// wanting a subject, not a scroll of every term. Tapping a row filters to it.
/// terms sit behind it. Tapping a row filters to it.
///
/// The count is the row's point as much as the name: it says whether a subject
/// is worth opening before the learner opens it.
Expand Down Expand Up @@ -124,6 +121,7 @@ class _CategoryRow extends StatelessWidget {
mood: mood,
face: AppFace.mono,
color: mood.inkMute,
tracking: AppTracking.count,
),
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ class TreeHeroCard extends StatelessWidget {
const SizedBox(height: AppSpacing.xs + 1),
Text(
_countLine.toUpperCase(),
style: AppText.label(mood: mood, face: AppFace.mono),
style: AppText.label(
mood: mood,
face: AppFace.mono,
tracking: AppTracking.tag,
),
),
],
),
Expand Down
35 changes: 33 additions & 2 deletions lib/features/progress/presentation/tree_progress_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ class TreeProgressBar extends StatelessWidget {
const SmallcapsLabel(_label),
Text(
'$completed / $total',
style: AppText.label(mood: mood, face: AppFace.mono),
style: AppText.label(
mood: mood,
face: AppFace.mono,
tracking: AppTracking.meta,
),
),
],
),
Expand All @@ -78,13 +82,40 @@ class TreeProgressBar extends StatelessWidget {
),
if (nextStageName case final next?) ...[
const SizedBox(height: AppSpacing.xs),
SmallcapsLabel('Next · $next'),
_NextStageLine(next),
],
],
);
}
}

/// What the tree grows into next — mono at `0.1em`, not the smallcaps rule the
/// kicker above it follows, so the two lines do not read as a pair.
class _NextStageLine extends StatelessWidget {
const _NextStageLine(this.stageName);

final String stageName;

@override
Widget build(BuildContext context) {
final line = 'Next · $stageName';

// Announced as written: uppercase is the type rule, not what it is called.
return Semantics(
label: line,
excludeSemantics: true,
child: Text(
line.toUpperCase(),
style: AppText.label(
mood: context.mood,
face: AppFace.mono,
tracking: AppTracking.tag,
),
),
);
}
}

/// The track and the accent fill that grows across it.
class _Track extends StatelessWidget {
const _Track({
Expand Down
115 changes: 109 additions & 6 deletions test/unit/shared/theme/count_lines_lettered_test.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
import 'package:brew_path/app/app_theme.dart';
import 'package:brew_path/core/widgets/roast_meter.dart';
import 'package:brew_path/features/dictionary/presentation/category_index.dart';
import 'package:brew_path/features/lessons/presentation/lesson_completion_header.dart';
import 'package:brew_path/features/lessons/presentation/reward_points_line.dart';
import 'package:brew_path/features/profile/presentation/widgets/lesson_progress_rollup.dart';
import 'package:brew_path/features/profile/presentation/widgets/profile_progress_line.dart';
import 'package:brew_path/features/profile/presentation/widgets/tree_hero_card.dart';
import 'package:brew_path/features/progress/domain/grove_treatment.dart';
import 'package:brew_path/features/progress/domain/mastery.dart';
import 'package:brew_path/features/progress/domain/mastery_rollup.dart';
import 'package:brew_path/features/progress/presentation/tree_progress_bar.dart';
import 'package:brew_path/shared/models/content/dictionary_category.dart';
import 'package:brew_path/shared/models/content/dictionary_term.dart';
import 'package:brew_path/shared/theme/app_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

// The four lines the design letters below the smallcaps rule (#551). Each is a
// number that is the subject of its line, and each sat at whatever its rung set
// until it was told otherwise — nothing at all on the support and body rungs,
// the 0.14em kicker rule on the label rung. Asserted on what is rendered, not
// on the source: a token named in the file says nothing about which line
// took it.
// The count lines the design letters below the smallcaps rule, each at its own
// width rather than at one shared step — 0.06em through 0.12em. Every one sat
// at whatever its rung set until it was told otherwise. Asserted on what is
// rendered, not on the source: a token named in a file says nothing about
// which line took it. The challenge row and the dictionary count are asserted
// the same way in their own suites, where their harnesses already live.
const _beans = DictionaryCategory(
id: 'beans',
label: 'Beans and Botany',
glyph: 'cherry',
summary: 'The plant, the seed, where it grows.',
);
const _arabica = DictionaryTerm(
id: 'arabica',
term: 'Arabica',
categoryId: 'beans',
shortExplanation: 'The species behind most specialty coffee.',
lessonId: 'm1l2',
);
const _robusta = DictionaryTerm(
id: 'robusta',
term: 'Robusta',
categoryId: 'beans',
shortExplanation: 'The backbone of instant coffee.',
lessonId: 'm1l2',
);

void main() {
Future<void> pump(WidgetTester tester, Widget child) => tester.pumpWidget(
MaterialApp(
Expand Down Expand Up @@ -94,6 +122,81 @@ void main() {
);
});

testWidgets('a run’s position counter is lettered as a hint', (tester) async {
await pump(
tester,
const RoastMeter(position: 3, total: 8, semanticsLabel: 'Card 3 of 8'),
);

expect(
letteringOf(tester, '03 / 08'),
lettered(AppTracking.hint, labelSize),
);
});

testWidgets('the core-lessons count is lettered as a meta line', (
tester,
) async {
await pump(
tester,
const TreeProgressBar(completed: 5, total: 32, nextStageName: 'Sapling'),
);

expect(
letteringOf(tester, '5 / 32'),
lettered(AppTracking.meta, labelSize),
);
});

testWidgets('the stage still to come is lettered as a tag, in mono', (
tester,
) async {
await pump(
tester,
const TreeProgressBar(completed: 5, total: 32, nextStageName: 'Sapling'),
);

final line = tester.widget<Text>(find.text('NEXT · SAPLING'));
expect(line.style!.letterSpacing, lettered(AppTracking.tag, labelSize));
expect(
line.style!.fontFamily,
AppText.label(face: AppFace.mono).fontFamily,
);
});

testWidgets('the tree hero’s count is lettered as a tag', (tester) async {
await pump(
tester,
TreeHeroCard(
stage: 1,
treatment: GroveTreatment.identity,
completed: 5,
total: 32,
onTap: () {},
),
);

expect(
letteringOf(tester, '5 / 32 CORE LESSONS'),
lettered(AppTracking.tag, labelSize),
);
});

testWidgets('a dictionary category’s term count is lettered as one', (
tester,
) async {
await pump(
tester,
CategoryIndex(
categories: const [_beans],
terms: const [_arabica, _robusta],
onOpen: (_) {},
),
);

expect(letteringOf(tester, '2'), lettered(AppTracking.count, labelSize));
});

test('a count sits between a bare figure and a meta line', () {
expect(AppTracking.count.em, 0.06);
expect(AppTracking.count.em, greaterThan(AppTracking.figure.em));
Expand Down
17 changes: 17 additions & 0 deletions test/widget/features/challenges/challenge_stat_row_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import 'package:brew_path/app/app_theme.dart';
import 'package:brew_path/features/challenges/domain/challenge_providers.dart';
import 'package:brew_path/features/challenges/presentation/challenge_stat_row.dart';
import 'package:brew_path/shared/models/content/brew_challenge.dart';
import 'package:brew_path/shared/theme/app_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../../support/content_fixtures.dart';
import '../../../support/widget_harness.dart';

const _labelSize = 11.0;

void main() {
setUp(useInMemoryDatabase);

Expand Down Expand Up @@ -76,6 +79,20 @@ void main() {
expect(find.text('1 / 1'), findsOneWidget);
});

testWidgets('sets the count in mono, lettered as a count', (tester) async {
await pump(tester, bank: [testChallenge()], done: const {'bc-m1'});

final count = tester.widget<Text>(find.text('1 / 1'));
expect(
count.style!.letterSpacing,
closeTo(AppTracking.count.em * _labelSize, 0.0001),
);
expect(
count.style!.fontFamily,
AppText.label(face: AppFace.mono).fontFamily,
);
});

testWidgets('summarises the whole block into one label', (tester) async {
final semantics = tester.ensureSemantics();
await pump(tester, bank: [testChallenge()], done: const {'bc-m1'});
Expand Down
Loading