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:
Tizian 「ティツィアーン」 2026-11-01 16:53:38 +01:00
commit b908019969
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 1050 additions and 125 deletions

View file

@ -45,6 +45,7 @@ void main() {
reminders: const [30, 0],
completedAt: DateTime(2026, 11, 2, 7),
completions: [DateTime(2026, 10, 19, 7, 5), DateTime(2026, 10, 22, 6)],
hideDoneSubTasks: true,
);
group('a task', () {
@ -84,6 +85,7 @@ void main() {
DateTime(2026, 10, 19, 7, 5).toUtc().toIso8601String(),
DateTime(2026, 10, 22, 6).toUtc().toIso8601String(),
],
'hideDoneSubTasks': true,
});
expect(
written(encodeTask(const TaskContent(title: 'Milk', projectId: 'p'))),

View file

@ -177,6 +177,45 @@ void main() {
},
);
test('duplicating a task copies it with its sub-tasks, theirs too, right '
'after it among its siblings', () async {
final tasks = await device(alice);
final first = await tasks.add(milk.copyWith(position: 1));
await tasks.add(milk.copyWith(title: 'Next', position: 2));
final sub = await tasks.add(
const TaskContent(
title: 'Oat',
projectId: 'inbox',
position: 1,
).copyWith(parentId: () => first),
);
await tasks.add(
TaskContent(
title: 'Bottles',
projectId: 'inbox',
parentId: sub,
completedAt: DateTime(2026, 11, 2),
),
);
final copy = await tasks.duplicate(first);
final all = {for (final t in await tasks.all()) t.id: t.content!};
expect(all, hasLength(7));
expect(all[copy]!.title, 'Buy milk');
expect(all[copy]!.position, 1.5, reason: 'between it and the next');
final copiedSub = all.entries.singleWhere(
(e) => e.value.parentId == copy,
);
expect(copiedSub.value.title, 'Oat');
expect(copiedSub.key, isNot(sub));
final copiedBottles = all.values.singleWhere(
(t) => t.parentId == copiedSub.key,
);
expect(copiedBottles.title, 'Bottles');
expect(copiedBottles.completed, isTrue, reason: 'copied as it was');
});
test("deleting a project's or a section's tasks deletes only theirs, "
'with their sub-tasks', () async {
final phone = await device(alice);

View file

@ -106,4 +106,14 @@ void main() {
],
);
});
test('a task was added when its UUID v7 says, its first 48 bits the '
'milliseconds since 1970; another ID says nothing', () {
expect(
addedAt('019a3e2f-8c00-7000-8000-000000000000'),
DateTime.fromMillisecondsSinceEpoch(0x019a3e2f8c00),
);
expect(addedAt('not-an-id'), isNull);
expect(addedAt('019a3e2f-8c00-4000-8000-000000000000'), isNull);
});
}