diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ff63fe37..1155bdf4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/lib/core/widgets/roast_meter.dart b/lib/core/widgets/roast_meter.dart index 4788f508..c9197c80 100644 --- a/lib/core/widgets/roast_meter.dart +++ b/lib/core/widgets/roast_meter.dart @@ -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, + ), ), ], ), diff --git a/lib/features/challenges/presentation/challenge_stat_row.dart b/lib/features/challenges/presentation/challenge_stat_row.dart index 6c91da1c..4dcc9a25 100644 --- a/lib/features/challenges/presentation/challenge_stat_row.dart +++ b/lib/features/challenges/presentation/challenge_stat_row.dart @@ -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'; @@ -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 @@ -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, + ), ), ], ), diff --git a/lib/features/dictionary/presentation/category_index.dart b/lib/features/dictionary/presentation/category_index.dart index c6d794dd..6cea4d3a 100644 --- a/lib/features/dictionary/presentation/category_index.dart +++ b/lib/features/dictionary/presentation/category_index.dart @@ -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. @@ -124,6 +121,7 @@ class _CategoryRow extends StatelessWidget { mood: mood, face: AppFace.mono, color: mood.inkMute, + tracking: AppTracking.count, ), ), ], diff --git a/lib/features/profile/presentation/widgets/tree_hero_card.dart b/lib/features/profile/presentation/widgets/tree_hero_card.dart index ef47ed5c..a6676b6b 100644 --- a/lib/features/profile/presentation/widgets/tree_hero_card.dart +++ b/lib/features/profile/presentation/widgets/tree_hero_card.dart @@ -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, + ), ), ], ), diff --git a/lib/features/progress/presentation/tree_progress_bar.dart b/lib/features/progress/presentation/tree_progress_bar.dart index 6e4ab14b..bca2610e 100644 --- a/lib/features/progress/presentation/tree_progress_bar.dart +++ b/lib/features/progress/presentation/tree_progress_bar.dart @@ -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, + ), ), ], ), @@ -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({ diff --git a/test/unit/shared/theme/count_lines_lettered_test.dart b/test/unit/shared/theme/count_lines_lettered_test.dart index d36a67c6..b7ad4923 100644 --- a/test/unit/shared/theme/count_lines_lettered_test.dart +++ b/test/unit/shared/theme/count_lines_lettered_test.dart @@ -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 pump(WidgetTester tester, Widget child) => tester.pumpWidget( MaterialApp( @@ -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(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)); diff --git a/test/widget/features/challenges/challenge_stat_row_test.dart b/test/widget/features/challenges/challenge_stat_row_test.dart index caa5d73a..7186892d 100644 --- a/test/widget/features/challenges/challenge_stat_row_test.dart +++ b/test/widget/features/challenges/challenge_stat_row_test.dart @@ -2,6 +2,7 @@ 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'; @@ -9,6 +10,8 @@ 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); @@ -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(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'});