The Display's new Layout picks List or Board. The board has a column for the tasks in no section, then one a section with its menu, each task a card and each column ending with Add task, a last column adding a section; grouped otherwise, a column a group. Narrower than a medium width it pages one column at a time with dots below; from it on the columns stand side by side. Cards drag within and between columns as rows do (D16), every scrollable they are in scrolling at its edges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
298 lines
23 KiB
Markdown
298 lines
23 KiB
Markdown
# ADR 0033: Halkyon Todo, what it does and how it looks
|
||
|
||
- **Status:** decided 1 Nov 2026, by the developer (D1–D10 below, the agent's
|
||
recommendations taken as a whole). Built the same day: steps 1 to 3, Today and
|
||
Upcoming as lists, recurrence, reminders and the week strip (step 4), the task
|
||
pane on wide windows and search (D2).
|
||
- **Numbering:** 0025–0029 are held (ADR 0030's note), 0032 is Jellyfin's; this is the
|
||
next free number.
|
||
- **Builds on:** ADR 0030, D7, D12, D14, D15 and D60 (Train's sealed rows, its shell, its
|
||
frame and settings, which Todo takes); ADR 0017 (the account's keys); ADR 0031 (Todo's
|
||
icon); Train's D61 (What's new).
|
||
|
||
## Context
|
||
|
||
On 1 Nov 2026 the developer asked for work on Todo to start from 68 screenshots of
|
||
Todoist's Android app (31 Oct 2026, `~/Downloads/todoist-screenshots-mobile/`), "following
|
||
the same foundational reasoning of Halkyon Learn and Halkyon Train": when the bar and when
|
||
the rail, a wash of strawberry, settings grouped as theirs. What the screenshots show:
|
||
|
||
- **Five tabs** (Inbox, Today, Search, Upcoming, Browse), a floating add button above the
|
||
bar, the bar's items and the button's side set by the person; Browse holds the
|
||
projects, Reporting, templates and the cog to Settings.
|
||
- **A task** has a title, a description, a **date** (optional time and **duration**,
|
||
"06:30–07:00") and, apart from it, a **deadline** (date only, its own icon, neutral
|
||
colour; a task may have a deadline and no date), a priority (P1–P4), labels, reminders
|
||
(relative to the time, automatic 30 minutes before a timed task; by location in the paid
|
||
plans), a recurrence (completing it moves it on; its sub-tasks reset), sub-tasks
|
||
(counted "0/5" on its row), comments with attachments. Rows colour the date by meaning
|
||
(overdue red, tomorrow orange, this week purple).
|
||
- **Projects** have sections (name, description, count), seen as a list, as a **board** (a
|
||
column a section, paged) or as a **calendar** (a week strip opening into the month, an
|
||
agenda below). **Today** puts Overdue first, with Reschedule for all of them;
|
||
**Upcoming** is an agenda from today on, or a **3-day** grid of hours where a long press
|
||
picks 30 minutes. Every view has its own **Display** sheet: layout, completed tasks,
|
||
grouping, sorting, filters by date, deadline, priority and label.
|
||
- **The task sheet** shows a row for each field set and a chip for each one not, the
|
||
sub-tasks, and a comment field at its foot; **quick add** is a card above the keyboard
|
||
with chips (date, deadline, priority, reminders, project), dates recognised in what is
|
||
typed.
|
||
- **Productivity**: a daily and weekly goal, streaks, karma, days off and a vacation mode;
|
||
**Reporting**, the completed tasks by day.
|
||
- **Settings**: Account, General (home view, smart dates, reset sub-tasks, the week's
|
||
first day, how "next week" and "weekend" are read, swipe actions), Theme, Navigation
|
||
bar, Quick Add, Productivity, Reminders, Notifications (a morning overview and an evening
|
||
review), About, What's new.
|
||
|
||
## Decisions (1 Nov 2026, the developer's)
|
||
|
||
- **D1 Todo's frame is Train's (ADR 0030, D60)**: go_router with a `StatefulShellRoute`,
|
||
every tab keeping its own stack; halkyon_ui's `NavigationFrame`, a bar floating above the
|
||
foot below medium windows and a rail with Todo's icon on top from medium on, a tick on
|
||
every choice; Todo's account sheet goes. Over a frame of Todo's own, which would make the
|
||
three apps behave differently on the same window.
|
||
- **D2 Four tabs: Today, Upcoming, Inbox, Browse** (recommended, over Todoist's five and over
|
||
a bar the person arranges). Today opens. Browse holds the projects, the labels and,
|
||
later, saved filters, with the cog to Settings atop it as Train's Home and Profile carry
|
||
theirs. **Search** is an icon in every tab's app bar, not a tab, as neither Learn nor
|
||
Train gives search a tab. **Adding a task** is a floating button above the bar
|
||
(Train's `TabScreen` places it clear of the bar), on the rail's screen from medium
|
||
windows on. From expanded windows on, a task opens in a pane beside its list, as Train's
|
||
Maps opens a route's map beside the list (Material's list–detail layout), and in a
|
||
bottom sheet below. Arranging the bar may come later; Search may become a tab if it
|
||
proves to be used as one. As built (1 Nov): the pane is the frame's, beside whichever tab
|
||
is shown (`TaskPane` around the shell's tabs), 420 wide, the list keeping the rest; a
|
||
task tapped there opens in it, another replaces it, what was typed into the last
|
||
kept. Narrowed below expanded, the pane hides and tasks open in sheets again. Search is
|
||
a screen pushed onto the tab's own stack (back returns to the tab, a task found opens in
|
||
the pane where there is one): as typed, the projects whose name holds it and the tasks
|
||
every word is found in, in their title, description or labels' names, whatever the
|
||
case and the accents, open ones first by their date, done ones last; on the device
|
||
alone, which the screen says, as the instance reads no task (D5).
|
||
- **D3 Every screen washes down from the top in strawberry** (the developer's):
|
||
`TopGradient` under the app's `ScreenColor`, `PaletteColor.strawberry`, as Learn's in
|
||
cobalt and Train's in carrot; buttons and links stay cobalt.
|
||
- **D4 Settings as Learn's and Train's** (the developer's): a screen over the tabs,
|
||
grouped in `SectionCard`s. Sync's card first (as Learn's), the operator's card for
|
||
whoever operates the instance, then Todo's own groups (*Tasks*: the week's first day
|
||
from the language unless set, resetting sub-tasks; *Reminders*: the automatic reminder
|
||
before a timed task, snoozing), the account near the foot (halkyon_app's
|
||
`AccountTiles`, `SimpleLoginSection`, `AccountSecuritySection`), What's new and About
|
||
Halkyon Todo last, the version small below them. Groups come as their settings are
|
||
built; none is shown empty.
|
||
- **D5 Todo's rows are sealed, always, as Train's (ADR 0030, D7, D15)**, synced with
|
||
`serverpod_offline_sync`: every payload ADR 0017's sealed content under the person's
|
||
data key (`content_key`, halkyon_app's `PayloadEnvelope`), nothing readable in a column,
|
||
not a title nor a date. Recommended over readable rows (ADR 0002's default), which would
|
||
let the server send reminders by email and a morning overview, and would make shared
|
||
projects simpler: what a person plans to do is theirs, the server needs to read none of
|
||
it to sync it, and Todo's rows can't be unsealed later without the person's devices. So
|
||
**reminders are scheduled on the device**, as local notifications on each device that
|
||
holds the account's keys; there is no email reminder and no overview the server writes.
|
||
A project shared later is sealed under a key its members share, which is that step's to
|
||
decide.
|
||
As built (1 Nov, the developer's choice of `flutter_local_notifications` over
|
||
`awesome_notifications` and platform code of Halkyon's own): a task's reminders are
|
||
minutes before the time on its date, in its sealed payload, so every device of the
|
||
person's reminds; a task with a day alone has none until the automatic reminder (D4)
|
||
is a setting. Each device plans its reminders whenever the tasks change, comes to the
|
||
front, or every six hours: the next 48 within 30 days (iOS keeps 64 pending), a
|
||
recurring task's next time alone, each under an ID hashed from its task and offset so
|
||
that planning again replaces it. They are scheduled with the system on Android, iOS,
|
||
macOS and Windows, so they come while the app is closed, at their instant: a device
|
||
moved to another zone has them moved once the app is opened. Linux schedules none, so
|
||
there the running app shows them. Android brings them at the minute only where the
|
||
person allows exact alarms (`SCHEDULE_EXACT_ALARM`, off by default since Android 14),
|
||
else within a few minutes; the app doesn't send the person to that setting. The
|
||
permission to notify is asked when a task first gets a reminder; a notification's
|
||
text, the task's title, is hidden on a locked Android screen. Signing out, or
|
||
switching person, cancels the person's reminders on the device. Tapping one opens the
|
||
app, not yet its task.
|
||
- **D6 Four tables: `project`, `section`, `task`, `label`** (recommended, as Train keeps a
|
||
workout and its series apart, D14): one row each, changed when edited, the last writer
|
||
winning, every payload JSON with a `schemaVersion`, UUID v7 ids. Inbox is a project the
|
||
app makes once per person, under a row ID keyed by the account (Train's D57,
|
||
`keyedRowId`), not marked in its payload (as built, 1 Nov): every device makes the
|
||
same row, so two making it offline write one, and the instance can tell neither
|
||
which row is the Inbox nor whose it is; a payload's mark would let a moved row
|
||
claim to be it. A **sub-task** is a task naming its
|
||
parent. A task's payload: title, description, its project and section, its parent, its
|
||
place among its siblings, a **date** (a day, or a day and a time with an optional
|
||
duration, the time as the person's wall clock with its time zone), a **deadline** (a
|
||
day), a priority (1–4), its labels' ids, a recurrence (D7), its reminders, when it was
|
||
completed. Completing a recurring task moves its date on and writes the occurrence done
|
||
into its payload's history, so Reporting can count it. Comments and attachments are a
|
||
later step (attachments need sealed files, which no Halkyon app has yet).
|
||
- **D7 A recurrence is a structured rule, set through a picker** (recommended, over
|
||
parsing what is typed): every n days, weeks (on which weekdays), months (on which day,
|
||
or the nth weekday), years, from the date or from completion, until a day or for n
|
||
times. Dates and tokens typed into quick add ("tomorrow 6am", "every Monday", `#project`,
|
||
`p1`), in English and German, are a later step, built on the same rule.
|
||
As built (1 Nov): the rule is part of the task's date (`Repeat` in `TaskDate`, its
|
||
payload's `date.repeat`), so a task without a date doesn't recur and clearing the date
|
||
ends it; schema 1 holds it, as no task was written before. Done, a task counted from its
|
||
date moves to the rule's first day after both its date and today (an overdue one isn't
|
||
moved onto a day already past), counted from completion it moves the rule's units on
|
||
from today, on no particular weekday; its deadline moves as far, its time is kept, its
|
||
done sub-tasks open again, and when it was done is added to its `completions`. Undo
|
||
writes back every task that changed. The picker offers the rules that fit its day
|
||
(every day, every weekday, its weekday, its day or nth weekday of the month, its day of
|
||
the year) and a rule of the person's own; weeks start on Monday for every n weeks, until
|
||
the week's first day is a setting (D4).
|
||
- **D8 The first version** (recommended): Inbox and projects with sections, shown as a
|
||
list; sub-tasks; Today (Overdue first, rescheduling them together, then today's); Upcoming
|
||
as an agenda with the week strip opening into the month; the task sheet (rows for the
|
||
fields set, chips for the others, the sub-tasks); quick add; labels; completed tasks shown
|
||
or hidden per view; reminders on the device. **Later**, each put to the developer when
|
||
its turn comes: the board and calendar layouts, Upcoming's 3-day grid, each view's
|
||
Display sheet (grouping, sorting, filters), saved filters, Reporting and goals (karma
|
||
not before the developer asks for it), location reminders, comments and attachments,
|
||
typed dates, sharing.
|
||
- **D9 Colours that mean something come from halkyon_ui's palette** (recommended): a
|
||
project's colour is picked from `PaletteColor`, with its localised name, as Learn's
|
||
colours are; priorities and dates take palette colours chosen not to blur against the
|
||
strawberry wash, so overdue and P1 are **cerise**, not strawberry, P2 carrot, P3
|
||
cobalt, P4 none; tomorrow and this week as Todoist's meanings go, in palette colours.
|
||
Each is checked against the wash in light and dark before it is kept.
|
||
- **D10 The icon stays as ADR 0031 drew it** (cobalt through pacific into fern), though
|
||
the wash is strawberry: Learn's icon ends in strawberry and washes in cobalt, so an
|
||
app's wash and its icon already differ, and Todo's and Meditate's icons are kept
|
||
related (ADR 0031, D1). The agent made no recommendation; this is the default the
|
||
decision of 1 Nov left in place, to be asked again if the screens look wrong beside it.
|
||
|
||
## Decisions after trying it (1 Nov 2026, the developer's)
|
||
|
||
The developer used the first build beside Todoist's and asked for these; where an
|
||
alternative was put to them, the choice is theirs.
|
||
|
||
- **D11 Sheets are dialogs on the desktop, at every width** (over the window's size
|
||
alone, and over Todo alone): halkyon_ui's `showAdaptiveSheet` shows a centred dialog
|
||
(Material's term) on Linux, macOS and Windows however narrow the window, and on phones
|
||
and tablets a bottom sheet below expanded windows as before, so Learn and Train follow.
|
||
A bottom sheet is a touch surface; rising from the foot of a desktop window it reads
|
||
as a phone's screen. In a narrow window the dialog keeps a smaller margin. A sheet
|
||
that can't go through it asks `sheetShowsAsDialog` and follows the same rule: Learn's
|
||
conversation is a dialog as tall as the window there, nothing to drag. Train's route
|
||
import still opens its bottom sheet everywhere, until Train 0.1.1 is out.
|
||
- **D12 The tabs are Inbox, Today, Upcoming, Browse; Today still opens** (amends D2's
|
||
order, not what opens).
|
||
- **D13 A task's sheet** names where the task is on top (its project, an icon once tasks
|
||
are above it, then each task above it; the project goes to its list, a task opens in
|
||
the sheet's place), as Learn's path through a studyset, and has a ⋮ menu in place of
|
||
its close button (`SheetScaffold`'s `heading` and `closeButton`): when it was added
|
||
(from its UUID v7, so the payload needn't say it), Duplicate (with its sub-tasks, right
|
||
after it), hiding or showing its done sub-tasks (`hideDoneSubTasks` in its payload,
|
||
so every device hides them, schema 1 as nothing is released), Close in the pane, and
|
||
Delete after asking. Copy link, View activity, Email to the task, Print and Open in a
|
||
new window wait for what they need (deep links, an activity log; mailing in is ruled
|
||
out by D5). The description's icon stays by its first line.
|
||
- **D14 Rows and menus**: a row shows its project and section ("Inbox / Errands" with the
|
||
project's icon) at the end of the line of its date, not beside the whole row; every
|
||
menu entry has its icon, a destructive one in the error colour (`MenuEntry`). A
|
||
section's heading stays atop the list while its tasks scroll beneath it, until the
|
||
next pushes it away (`PinnedHeaderSliver`). Upcoming's month unfolds in place beneath
|
||
its strip, from its name or its handle dragged down, the arrows then going by month,
|
||
the handle dragged up folding it; a button beside the month goes back to today once
|
||
another week shows. Tasks are chosen to act on together, as Todoist's: a row swiped
|
||
right or held, or *Select tasks* in Today's, Upcoming's and a project's ⋮, a tap then
|
||
choosing or letting go (a row held is dragged since D16); the app bar counts them, with an arrow (or back) ending it,
|
||
and a bar takes the navigation bar's place: date, move, labels (those all have
|
||
ticked; a tick added to each, one unticked taken off each), priority, and ⋮ with
|
||
complete (undone together), duplicate, deadline and delete (asked first). *Assign*
|
||
waits for shared projects and *View activity* for a history, neither of which Todo
|
||
has. A row swiped left asks for its date. Neither swipe removes the row; a sheet's
|
||
sub-tasks and search's results aren't chosen (`TaskSelection`, one for the frame).
|
||
- **D15 Typed tokens in quick add** (amends D7's later step; the developer's, 1 Nov, after
|
||
asking whether the keyboard's language could choose): `#project`, `@label`,
|
||
`{deadline}`, `!!1` and `p1` to `p4`, `!30m`, dates and times (`9am`, `Mon 18:00`,
|
||
`ev Tue 7pm`) and recurrences (`every day`, `every 2 days`, `every second day`), each
|
||
highlighted as it is recognised and undone with a tap. The words are data, a table per
|
||
language (weekdays, months, *tomorrow*, *every*, *next*, times), read in the app's
|
||
language and English, so a new app language adds a table, not a parser. Not the
|
||
keyboard's language: Flutter tells nothing of it, Android doesn't reliably, and it
|
||
changes as the person switches keyboards while typing. Symbols hold in every language.
|
||
As built (`parseQuickAdd`, `QuickAddWords.en` and `.de`): a `#` naming no project
|
||
stays text, an `@` naming no label makes one as the task is added; only the first of
|
||
each kind counts, the rest stays in the title; a time alone is its next to come
|
||
(tomorrow once it has passed today), a weekday alone the coming one (today too),
|
||
`next` one after today; a month needs its day (`may` alone is a word), a number alone
|
||
is none; a recurrence starts on its first day from today (`every weekday` on a Sunday:
|
||
Monday). `!30m` makes a task without a time due 30 minutes from now, reminded then,
|
||
and one with a time reminded 30 minutes before it; with a day but no time it is
|
||
text. German's weekdays only by their whole names, its short forms (`so`, `do`,
|
||
`die`, `mit`) being everyday words. Quick add shows its title alone at first, its
|
||
chips once typing starts, a chip picked taking its token out of the title, and the
|
||
description and the deadline behind its + (the description also from the down
|
||
arrow). As `#` or `@` is typed, up to five projects or labels whose name, or a word
|
||
of it, starts with what follows are offered below the title, one tapped completing
|
||
it; none for a symbol inside a word (`C#`), nor once a name is typed whole.
|
||
- **D16 Dragging** (the developer's, 1 Nov, as Todoist's): a row of a project's list
|
||
(the Inbox's too), held, is dragged into another place, a line showing where: the
|
||
upper half of a row puts it before that row, the lower half after it, and the lower
|
||
half reached 48 px to the right of where the drag began makes it that row's last
|
||
sub-task; a section's heading takes it first in the section, or last in the group
|
||
before from its upper half. It goes with its sub-tasks, and *Order changed* offers
|
||
to undo it (`TaskRepository.place`, never under itself or its own sub-task). The
|
||
list scrolls at its edges. Holding a row therefore drags it everywhere it can be
|
||
dragged and no longer selects it (D14): a swipe to the right does. The add button
|
||
is dragged straight away, without a hold, its place meanwhile a larger × where
|
||
dropping it gives up: dropped between a project's rows, quick add adds there (each
|
||
next task below the one before); on a day in Upcoming, for that day; on Today, for
|
||
today. Upcoming's rows, overdue ones too, held and dropped on another day move there,
|
||
their time, duration and recurrence kept, *Moved to …* offering to undo it; their
|
||
order within a day stays their times' and priorities', and Today's rows aren't
|
||
dragged.
|
||
- **D17 Each project's Display, synced** (the developer's, 1 Nov, as Todoist's; D8's
|
||
later step): a project's screen, the Inbox's too, has a Display button beside its ⋮, a
|
||
dot on it while anything differs from how a project shows at first. Its sheet sets
|
||
completed tasks (moved out of the ⋮), grouping (none, which keeps the sections; date,
|
||
deadline, added date, priority, label), sorting (manual, name, date, deadline, added
|
||
date, priority; a tie, or a task without what is sorted by, last and by hand), and
|
||
filters by date and deadline (all, today, this week from the language's first
|
||
weekday, next 7 days, this month, next 30 days, none) and by priority and label (any
|
||
ticked). Each change is written at once; Reset all puts the project back. Grouped,
|
||
the groups stand in the sections' place under headings that stay atop the list
|
||
(overdue first, then each day, none last; P1 first; a task under each of its labels);
|
||
rows are dragged (D16) only while sorted by hand and grouped by sections. The choices
|
||
are kept **in the project's sealed row**, so every device of the person's shows the
|
||
project alike (`ProjectView`, the payload's `view`, written only once set; values a
|
||
version doesn't know read as at first). The schema stays 1: an older version reads
|
||
such a project, and renaming it there drops its Display, which beats a newer schema
|
||
it couldn't read at all. Today's and Upcoming's Display come with Upcoming's 3-day
|
||
grid, in a sealed person-settings row (`todo_settings`, one migration), which also
|
||
holds D4's settings. **The Board** (as built, 1 Nov), the Display's Layout (List,
|
||
Board): a column for the tasks in no section, *(No section)*, always first, then one
|
||
a section with its menu, each heading counting its open tasks, each task a card,
|
||
each column ending with Add task, a last column adding a section; grouped otherwise,
|
||
a column a group, nothing dragged. Below a medium width (the board's own, not the
|
||
window's, so the pane beside the tabs counts) one column at a time, paged, the next
|
||
peeking in, settling on the nearest when let go, dots below saying where; from it on,
|
||
columns 320 wide side by side, scrolled across. Cards are dragged as rows (D16),
|
||
within and between columns, every scrollable they are in scrolling at its edges; a
|
||
column takes a card or the add button dropped below its cards, last in it. **To come,
|
||
in this order**: Calendar for projects (the week strip and agenda; the Layout's third
|
||
choice, until then shown as a list), Upcoming's 3-day grid.
|
||
|
||
## Steps
|
||
|
||
1. **The frame**: go_router and `NavigationFrame` with the four tabs (empty screens that
|
||
say what comes), the strawberry wash, the cog, Settings as D4 with what exists today
|
||
(the account, What's new, About, the version); the account sheet removed.
|
||
2. **The first synced model**: `content_key` and `project` (Inbox made once), its gateway
|
||
and repository on Learn's and Train's pattern, the server's access rules each with a
|
||
test asserting denial; walked through with the developer as the first of Todo's.
|
||
3. **Tasks**: `section`, `task`, `label`; Inbox and a project's list; the task sheet; quick
|
||
add.
|
||
4. **Today and Upcoming**, recurrence (D7), reminders on the device.
|
||
5. Todo's first release (0.1.0), its *What's new* drafted by `tool/release.dart`.
|
||
|
||
## Consequences
|
||
|
||
- No email reminder, morning overview or evening review: the server can't read what is
|
||
due. A device without the account's keys neither reads nor writes Todo's rows, as in
|
||
Train.
|
||
- Todo's server holds no logic over tasks; what Today, Upcoming and Reporting show is
|
||
worked out on the device, over every task the person has. That is fine for thousands of
|
||
tasks; a person with far more would need the device to keep an index of its own.
|
||
- Sharing a project is harder than with readable rows (a key per shared project, its
|
||
members' keys wrapping it); D5 accepts that for now.
|