feat(android): cluster close events on the activity day timeline #321

Merged
kfickel merged 3 commits from 314_stack-close-activity-dots into main 2026-10-22 18:24:28 +02:00
Collaborator

Closes #314.

What changed

On the day timeline of an activity command, events that sat minutes apart drew on top of each other and their labels collided. Now:

  • Clusters. Events whose 48 dp touch targets would overlap at the lane's current width are merged, chained so a run of close events is one cluster. A cluster is a larger dot (8 dp radius) with its count in it (black or white by the series colour's luminance), labelled with its span (06:40–06:52). The lane stays 64 dp tall. A wider screen keeps more events apart.
  • Labels. The fixed 150-minute rule is replaced: a label is above the line unless it would overlap the previous label above, and it stays inside the lane at 00:00 and 23:59.
  • Taps. Every dot has a 48 dp touch target. A single dot opens its note at its line. A cluster opens a menu of its shards (06:40 · title), and a row opens that note. Back or a tap outside closes the menu.
  • Accessibility. The description moved from the label to the touch target: <series> 06:45 <title>, or <series> 3 shards 06:40–06:52 for a cluster (new plural activity_cluster).

How

  • ui/ActivityChart.kt: timelineClusters(events, width) and TimelineCluster (pure, dp-based) replace timelineLabels.
  • ui/Activity.kt: ActivityTimeline draws the clusters and their touch targets. Like the ⋮ options overlay, ClusterMenu is a Surface drawn in the content, not a platform popup, so layoutlib renders it. It sits inside its lane, raised by zIndex and unbounded in height. Nothing can lie between the menu and the scrolling content, so dismissingOutside catches presses outside the menu in the initial pass and consumes them. The open cluster ("<series>@HH:mm") is state of ActivityContent, saved by ActivityScreen per range.
  • The TUI is untouched.

Tests

  • ActivityChartTest: far-apart events stay single, minutes-apart events merge with span and count, phone vs tablet width, chains, alternating labels, clamping at the ends of the day, empty input, the cluster key.
  • New screenshots: ActivityDayCloseEventsScreenshot (a cluster of three and a single dot) and ActivityDayClusterMenuScreenshot (the menu open). The three-series day scenes changed: Read's 06:45 and 08:15 are now one cluster at phone width. The plain day scene changed only slightly because labels are now centred on their dot.
  • nix build .#streamd-app (unit + screenshot validation) and nix flake check pass.

Docs

R36 in REQUIREMENTS.md, the activity paragraph in README.md and the activity screen bullet in AGENTS.md.

Closes #314. ## What changed On the day timeline of an activity command, events that sat minutes apart drew on top of each other and their labels collided. Now: - **Clusters.** Events whose 48 dp touch targets would overlap at the lane's current width are merged, chained so a run of close events is one cluster. A cluster is a larger dot (8 dp radius) with its count in it (black or white by the series colour's luminance), labelled with its span (`06:40–06:52`). The lane stays 64 dp tall. A wider screen keeps more events apart. - **Labels.** The fixed 150-minute rule is replaced: a label is above the line unless it would overlap the previous label above, and it stays inside the lane at 00:00 and 23:59. - **Taps.** Every dot has a 48 dp touch target. A single dot opens its note at its line. A cluster opens a menu of its shards (`06:40 · title`), and a row opens that note. Back or a tap outside closes the menu. - **Accessibility.** The description moved from the label to the touch target: `<series> 06:45 <title>`, or `<series> 3 shards 06:40–06:52` for a cluster (new plural `activity_cluster`). ## How - `ui/ActivityChart.kt`: `timelineClusters(events, width)` and `TimelineCluster` (pure, dp-based) replace `timelineLabels`. - `ui/Activity.kt`: `ActivityTimeline` draws the clusters and their touch targets. Like the ⋮ options overlay, `ClusterMenu` is a `Surface` drawn in the content, not a platform popup, so layoutlib renders it. It sits inside its lane, raised by `zIndex` and unbounded in height. Nothing can lie between the menu and the scrolling content, so `dismissingOutside` catches presses outside the menu in the initial pass and consumes them. The open cluster (`"<series>@HH:mm"`) is state of `ActivityContent`, saved by `ActivityScreen` per range. - The TUI is untouched. ## Tests - `ActivityChartTest`: far-apart events stay single, minutes-apart events merge with span and count, phone vs tablet width, chains, alternating labels, clamping at the ends of the day, empty input, the cluster key. - New screenshots: `ActivityDayCloseEventsScreenshot` (a cluster of three and a single dot) and `ActivityDayClusterMenuScreenshot` (the menu open). The three-series day scenes changed: Read's 06:45 and 08:15 are now one cluster at phone width. The plain day scene changed only slightly because labels are now centred on their dot. - `nix build .#streamd-app` (unit + screenshot validation) and `nix flake check` pass. ## Docs R36 in `REQUIREMENTS.md`, the activity paragraph in `README.md` and the activity screen bullet in `AGENTS.md`.
docs: describe the clusters of the activity day timeline
All checks were successful
Continuous Integration / Build Package (push) Successful in 15m31s
Continuous Integration / Conventional Commits (push) Successful in 1m51s
Continuous Integration / Lint, Check & Test (push) Successful in 12m26s
0bc6e769b6
kfickel deleted branch 314_stack-close-activity-dots 2026-10-22 18:24:28 +02:00
Sign in to join this conversation.
No reviewers
No labels
android
cli
epic
planned
tui
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
kfickel/streamd!321
No description provided.