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

### Changed

- **Every module on Path opens and shuts.** The one you are working through
was pinned open with no caret, because the design ties *cannot collapse* to
*always open*. It now carries a caret like a finished module and folds away
on a tap, while still opening itself on arrival, so your current lesson is in
front of you without being asked for. A module you have not reached keeps no
caret — there is nothing behind one to open.

- **Every section that opens, opens the same way.** The practice groups,
Path's modules, Reference, the parked brews and the Help answers were five
hand-rolled expanders with four different timings and two different marks.
They are now one: a caret turning 180° for a list, a plus turning 45° into a
cross for an answer, both over the design's 240ms, and a panel that grows
over the same beat. The parked brews collapse for the first time, and start
shut with their count beside the header. A shut panel is not drawn at all,
so a screen reader never finds what is not on screen and a swipe never lands
in it. Reduced motion cuts the move to nothing rather than dropping it.

- **Today's date is shorter.** The Learn header reads *Fri, May 8* rather
than *Friday, May 8*, as the design now sets it, so the title takes less
of the header. Term of the Day keeps the long form.
Expand Down
37 changes: 0 additions & 37 deletions lib/core/icons/caret_mark.dart

This file was deleted.

124 changes: 104 additions & 20 deletions lib/core/icons/disclosure_mark.dart
Original file line number Diff line number Diff line change
@@ -1,37 +1,121 @@
import 'package:brew_path/core/icons/app_icon.dart';
import 'package:brew_path/core/icons/icon_mark.dart';
import 'package:brew_path/shared/theme/app_motion.dart';
import 'package:brew_path/shared/theme/mood_colors.dart';
import 'package:flutter/material.dart';

/// The plus that turns into a cross as its answer opens.
/// The mark a disclosure header carries, and what it promises.
///
/// The design's own mark, and its `transition: transform 200ms ease` — unlike
/// `CaretMark`, which the design states without one. The glyph is `close`
/// rotated: that mark is already the symmetric cross this ends on, so the two
/// share one stroke weight rather than two drawings of the same lines.
/// Two on purpose: a caret means *more of the same, below* — things that were
/// already countable while shut — and a plus means *there is an answer here*,
/// prose that did not exist until it was asked for.
enum DisclosureGlyph {
/// Lists: practice groups, Path's modules, a shelf of guides.
caret,

/// Prose: an FAQ answer.
plus,

/// No mark, for a header that is a heading rather than a toggle.
none,
}

/// The glyph on a disclosure header, turning as its panel opens.
///
/// The caret turns 180°, the plus 45° into a cross — both over the design's
/// `240ms cubic-bezier(.4,0,.2,1)`, cut to nothing when motion is reduced.
class DisclosureMark extends StatelessWidget {
/// Creates a mark for a row that is [open] or closed.
const DisclosureMark({required this.open, this.size, this.color, super.key});
/// Draws [glyph] for a panel that is [open] or shut.
const DisclosureMark({
required this.glyph,
required this.open,
this.size,
this.color,
super.key,
});

/// The caret's drawn size — the design's `size || 18`.
static const double _caretSize = 18;

/// The plus's drawn size — the design's `size || 12`.
static const double _plusSize = 12;

/// The caret where a section sets it smaller: the design draws it at 18, or
/// 16 where a site says so, which Path's modules and Reference both do.
static const double sectionCaretSize = 16;

/// Half a turn: the design's 180° on the caret.
static const double _caretOpenTurns = 0.5;

/// An eighth: the design's 45°, which makes a plus into a cross.
static const double _plusOpenTurns = 0.125;

/// Which mark to draw.
final DisclosureGlyph glyph;

/// Whether the row it belongs to is open, which is what it states.
/// Whether the panel it belongs to is open, which is what it points at.
final bool open;

/// The box to fit the mark into. Null draws it at the mark's own size.
/// The box to fit the mark into. Null draws it at the size the design did.
final double? size;

/// The mark's ink. Null takes the ambient icon colour, as any mark does.
/// The mark's ink. Null takes the muted ink the design gives every glyph.
final Color? color;

/// An eighth of a turn — the design's 45°, which makes a cross a plus.
static const double _closedTurns = 0.125;
@override
Widget build(BuildContext context) {
if (glyph == DisclosureGlyph.none) return const SizedBox.shrink();

final ink = color ?? context.mood.inkMute;
final isCaret = glyph == DisclosureGlyph.caret;
final openTurns = isCaret ? _caretOpenTurns : _plusOpenTurns;

return AnimatedRotation(
turns: open ? openTurns : 0,
duration: MediaQuery.disableAnimationsOf(context)
? Duration.zero
: AppMotion.disclosure,
curve: AppMotion.disclosureGlyph,
child: isCaret
? IconMark(AppIcon.caret, size: size ?? _caretSize, color: ink)
: CustomPaint(
size: Size.square(size ?? _plusSize),
painter: _PlusPainter(ink),
),
);
}
}

/// The design's own plus, which no icon in the mark family draws: `M6 1v10M1
/// 6h10` at `strokeWidth: 1.4` and `strokeOpacity: 0.7` in a 12-unit box.
class _PlusPainter extends CustomPainter {
const _PlusPainter(this.color);

static const double _box = 12;
static const double _armInset = 1;
static const double _strokeWidth = 1.4;
static const double _strokeOpacity = 0.7;

final Color color;

@override
void paint(Canvas canvas, Size size) {
final scale = size.width / _box;
final paint = Paint()
..color = color.withValues(alpha: color.a * _strokeOpacity)
..strokeWidth = _strokeWidth * scale
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;

final middle = size.width / 2;
final start = _armInset * scale;
final end = size.width - start;

canvas
..drawLine(Offset(middle, start), Offset(middle, end), paint)
..drawLine(Offset(start, middle), Offset(end, middle), paint);
}

@override
Widget build(BuildContext context) => AnimatedRotation(
turns: open ? 0 : _closedTurns,
duration: MediaQuery.disableAnimationsOf(context)
? Duration.zero
: AppMotion.markTurn,
curve: Curves.ease,
child: IconMark(AppIcon.close, size: size, color: color),
);
bool shouldRepaint(_PlusPainter oldDelegate) => oldDelegate.color != color;
}
Loading
Loading