feat(todo): dialogs on the desktop, the tabs reordered, a task's path and menu (ADR 0033, D11 to D14)
- halkyon_ui: showAdaptiveSheet shows a dialog on Linux, macOS and Windows at every width (bottom sheets stay on phones and tablets below expanded windows), so Learn and Train follow; SheetScaffold takes a heading in the title's place and may leave out its close button. - Todo: the tabs are Inbox, Today, Upcoming, Browse, Today still opening; `serverpod start` launches the app on Linux, as it has no web build. - A task's sheet shows where the task is (its project, an icon once tasks are above it, and the tasks above it, each opening what it names) and a ⋮ menu in place of its close button: when it was added (its UUID v7), Duplicate with its sub-tasks, hiding its done sub-tasks (synced in its payload), Close in the pane, Delete after asking. The description's icon stays by its first line. - A row names its project and section at the end of its date's line; every menu entry has its icon. - Learn: a studyset's path starts at the Library tab's icon, named by its tooltip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
666e8dbcbb
commit
b908019969
2 changed files with 1050 additions and 125 deletions
|
|
@ -3,11 +3,14 @@ import 'dart:math' as math;
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_navigation.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Where a studyset sits, on one line: "Library › Japanese › Kana ›
|
||||
/// Hiragana", the steps as plain links in a muted colour, the studyset
|
||||
/// itself last, bold in the primary colour.
|
||||
/// Where a studyset sits, on one line: "📖 › Japanese › Kana › Hiragana",
|
||||
/// the library as the Library tab's icon, named by its tooltip (as Todo's
|
||||
/// path starts at its project's icon, Todo's ADR 0033, D13), the other
|
||||
/// steps as plain links in a muted colour, the studyset itself last, bold
|
||||
/// in the primary colour.
|
||||
///
|
||||
/// Every step before it goes back up there ([goUpTo]): the screens behind
|
||||
/// a studyset are its path, so going up closes the screens above, as the
|
||||
|
|
@ -40,6 +43,9 @@ class const StudysetBreadcrumb({
|
|||
/// How high every step is, to tap.
|
||||
static const minTarget = 48.0;
|
||||
|
||||
/// How wide a step shown as its icon is.
|
||||
static const _iconWidth = 32.0;
|
||||
|
||||
/// How far before the screen's end the line ends when [trailing] ends it.
|
||||
static const trailingEndPadding = 8.0;
|
||||
|
||||
|
|
@ -57,6 +63,7 @@ class const StudysetBreadcrumb({
|
|||
_Step(
|
||||
l10n.libraryTitle,
|
||||
enabled ? () => goUpTo(context, null, all) : null,
|
||||
icon: LucideIcons.book,
|
||||
),
|
||||
for (final ancestor in ancestors)
|
||||
_Step(
|
||||
|
|
@ -85,7 +92,10 @@ class const StudysetBreadcrumb({
|
|||
}
|
||||
|
||||
final separatorWidth = widthOf('›', muted) + 2 * _separatorPadding;
|
||||
final widths = [for (final step in steps) widthOf(step.label, muted)];
|
||||
final widths = [
|
||||
for (final step in steps)
|
||||
if (step.icon == null) widthOf(step.label, muted) else _iconWidth,
|
||||
];
|
||||
final currentWidth = widthOf(current.content.title, active);
|
||||
const foldWidth = _Fold.width;
|
||||
// Fold the first steps away until the rest fits; at the least, the
|
||||
|
|
@ -120,13 +130,23 @@ class const StudysetBreadcrumb({
|
|||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: step.onTap,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: minTarget),
|
||||
child: Center(
|
||||
widthFactor: 1,
|
||||
child: Text(step.label, maxLines: 1, style: muted),
|
||||
child: switch (step.icon) {
|
||||
final icon? => Tooltip(
|
||||
message: step.label,
|
||||
child: SizedBox(
|
||||
width: _iconWidth,
|
||||
height: minTarget,
|
||||
child: Icon(icon, size: 18, color: muted.color),
|
||||
),
|
||||
),
|
||||
),
|
||||
null => ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: minTarget),
|
||||
child: Center(
|
||||
widthFactor: 1,
|
||||
child: Text(step.label, maxLines: 1, style: muted),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
separator(),
|
||||
|
|
@ -160,8 +180,13 @@ class const StudysetBreadcrumb({
|
|||
}
|
||||
}
|
||||
|
||||
/// A step of the way, and where it leads; nowhere while null.
|
||||
class const _Step(final String label, final VoidCallback? onTap);
|
||||
/// A step of the way, and where it leads; nowhere while null. One with an
|
||||
/// [icon] shows it, its [label] the tooltip, and both in the fold's menu.
|
||||
class const _Step(
|
||||
final String label,
|
||||
final VoidCallback? onTap, {
|
||||
final IconData? icon,
|
||||
});
|
||||
|
||||
/// The steps that didn't fit, behind a "…" that turns primary under the
|
||||
/// pointer, nearest first in its menu; a square target as high as a step.
|
||||
|
|
@ -184,7 +209,11 @@ class _FoldState() extends State<_Fold> {
|
|||
Widget build(BuildContext context) => MenuAnchor(
|
||||
menuChildren: [
|
||||
for (final step in widget.steps)
|
||||
MenuItemButton(onPressed: step.onTap, child: Text(step.label)),
|
||||
MenuItemButton(
|
||||
onPressed: step.onTap,
|
||||
leadingIcon: step.icon == null ? null : Icon(step.icon),
|
||||
child: Text(step.label),
|
||||
),
|
||||
],
|
||||
builder: (context, menu, _) => MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
|
|
|
|||
|
|
@ -321,7 +321,22 @@ void main() {
|
|||
of: find.byType(StudysetBreadcrumb),
|
||||
matching: find.text(text),
|
||||
);
|
||||
expect(step('Library'), findsOneWidget);
|
||||
// The library is its icon, named by its tooltip (Todo's ADR 0033).
|
||||
expect(step('Library'), findsNothing);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(StudysetBreadcrumb),
|
||||
matching: find.byTooltip('Library'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(StudysetBreadcrumb),
|
||||
matching: find.byIcon(LucideIcons.book),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(step('Japanese'), findsOneWidget);
|
||||
expect(step('Kana'), findsOneWidget);
|
||||
expect(step('›'), findsNWidgets(2));
|
||||
|
|
@ -1412,7 +1427,7 @@ void main() {
|
|||
.getCenter(
|
||||
find.descendant(
|
||||
of: find.byType(StudysetBreadcrumb),
|
||||
matching: find.text('Library'),
|
||||
matching: find.byIcon(LucideIcons.book),
|
||||
),
|
||||
)
|
||||
.dy,
|
||||
|
|
@ -1576,7 +1591,9 @@ void main() {
|
|||
.ancestor(
|
||||
of: find.descendant(
|
||||
of: find.byType(StudysetBreadcrumb),
|
||||
matching: find.text(text),
|
||||
matching: text == 'Library'
|
||||
? find.byIcon(LucideIcons.book)
|
||||
: find.text(text),
|
||||
),
|
||||
matching: find.byType(GestureDetector),
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue