feat(timeline): add locked overlay reveal state

This commit is contained in:
Ashley Venn 2026-06-24 12:25:59 -07:00
parent 6f31290397
commit 9c19b85f03
4 changed files with 132 additions and 4 deletions

View file

@ -1,6 +1,6 @@
# V1 Block 08 — Today Timeline State # V1 Block 08 — Today Timeline State
Status: In progress — Chunk 8.1 complete Status: In progress — Chunks 8.1 and 8.2 complete
Purpose: Prepare domain/view-state data for the Today timeline without building full Flutter UI yet unless explicitly requested. Purpose: Prepare domain/view-state data for the Today timeline without building full Flutter UI yet unless explicitly requested.
@ -68,6 +68,14 @@ Acceptance criteria:
- Overlay item shows a name and explicit start/end times. - Overlay item shows a name and explicit start/end times.
- Locked overlay does not expose normal task quick actions. - Locked overlay does not expose normal task quick actions.
Completed:
- Added locked occurrence overlay mapping behind an explicit `revealHiddenLockedBlocks` flag.
- Hidden locked occurrences return no overlay unless reveal mode is active.
- Visible locked occurrences produce named overlay items with explicit times.
- Locked overlays remain distinct from task cards and expose no normal task quick actions.
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
BREAKPOINT: Stop here. Confirm `low` mode before implementing compact mode state. BREAKPOINT: Stop here. Confirm `low` mode before implementing compact mode state.
## Chunk 8.3 — Compact mode state ## Chunk 8.3 — Compact mode state

View file

@ -1,6 +1,6 @@
# V1 Block 08 — Today Timeline State # V1 Block 08 — Today Timeline State
Status: In progress — Chunk 8.1 complete Status: In progress — Chunks 8.1 and 8.2 complete
Purpose: Prepare domain/view-state data for the Today timeline without building full Flutter UI yet unless explicitly requested. Purpose: Prepare domain/view-state data for the Today timeline without building full Flutter UI yet unless explicitly requested.
@ -68,6 +68,14 @@ Acceptance criteria:
- Overlay item shows a name and explicit start/end times. - Overlay item shows a name and explicit start/end times.
- Locked overlay does not expose normal task quick actions. - Locked overlay does not expose normal task quick actions.
Completed:
- Added locked occurrence overlay mapping behind an explicit `revealHiddenLockedBlocks` flag.
- Hidden locked occurrences return no overlay unless reveal mode is active.
- Visible locked occurrences produce named overlay items with explicit times.
- Locked overlays remain distinct from task cards and expose no normal task quick actions.
- Verified with `dart format lib test`, `dart analyze`, and `dart test`.
BREAKPOINT: Stop here. Confirm `low` mode before implementing compact mode state. BREAKPOINT: Stop here. Confirm `low` mode before implementing compact mode state.
## Chunk 8.3 — Compact mode state ## Chunk 8.3 — Compact mode state

View file

@ -4,6 +4,7 @@
// without importing Flutter or choosing visual assets. UI layers can map these // without importing Flutter or choosing visual assets. UI layers can map these
// tokens to colors, icons, text, or widgets later. // tokens to colors, icons, text, or widgets later.
import 'locked_time.dart';
import 'models.dart'; import 'models.dart';
/// Broad display category for an item placed on the Today timeline. /// Broad display category for an item placed on the Today timeline.
@ -138,8 +139,7 @@ class TimelineItemMapper {
id: task.id, id: task.id,
displayTitle: task.title, displayTitle: task.title,
taskType: task.type, taskType: task.type,
projectColorToken: projectColorToken: _projectColorTokenFor(task.projectId),
projectColorTokensById[task.projectId] ?? task.projectId,
backgroundToken: _backgroundTokenFor(task.type), backgroundToken: _backgroundTokenFor(task.type),
rewardIconToken: _rewardIconTokenFor(task.reward), rewardIconToken: _rewardIconTokenFor(task.reward),
difficultyIconToken: _difficultyIconTokenFor(task.difficulty), difficultyIconToken: _difficultyIconTokenFor(task.difficulty),
@ -154,6 +154,55 @@ class TimelineItemMapper {
); );
} }
/// Convert a concrete locked-time occurrence into an overlay item.
///
/// Hidden locked time returns null unless [revealHiddenLockedBlocks] is true.
/// This keeps locked time available to scheduling while leaving UI visibility
/// controlled by an explicit temporary reveal state.
TimelineItem? fromLockedOccurrence(
LockedBlockOccurrence occurrence, {
required bool revealHiddenLockedBlocks,
}) {
if (occurrence.hiddenByDefault && !revealHiddenLockedBlocks) {
return null;
}
return TimelineItem(
id: _lockedOccurrenceId(occurrence),
displayTitle: occurrence.name,
taskType: TaskType.locked,
projectColorToken: _projectColorTokenFor(occurrence.projectId),
backgroundToken: TimelineBackgroundToken.locked,
rewardIconToken: TimelineRewardIconToken.notSet,
difficultyIconToken: TimelineDifficultyIconToken.notSet,
showsExplicitTime: true,
start: occurrence.interval.start,
end: occurrence.interval.end,
quickActions: const [],
category: TimelineItemCategory.overlay,
);
}
String _lockedOccurrenceId(LockedBlockOccurrence occurrence) {
final lockedBlockId = occurrence.lockedBlockId;
if (lockedBlockId != null) {
return 'locked-block:$lockedBlockId';
}
final overrideId = occurrence.overrideId;
if (overrideId != null) {
return 'locked-override:$overrideId';
}
return 'locked:${occurrence.name}:'
'${occurrence.interval.start.toIso8601String()}';
}
String _projectColorTokenFor(String? projectId) {
if (projectId == null) {
return 'locked';
}
return projectColorTokensById[projectId] ?? projectId;
}
bool _showsExplicitTime(TaskType type) { bool _showsExplicitTime(TaskType type) {
return type == TaskType.inflexible || return type == TaskType.inflexible ||
type == TaskType.critical || type == TaskType.critical ||

View file

@ -124,6 +124,69 @@ void main() {
expect(item.backgroundToken, TimelineBackgroundToken.locked); expect(item.backgroundToken, TimelineBackgroundToken.locked);
expect(item.quickActions, isEmpty); expect(item.quickActions, isEmpty);
}); });
test('hidden locked occurrences stay hidden until reveal mode is active',
() {
final occurrence = LockedBlockOccurrence(
name: 'Work',
interval: TimeInterval(
start: DateTime(2026, 6, 20, 9),
end: DateTime(2026, 6, 20, 17),
),
hiddenByDefault: true,
lockedBlockId: 'work-hours',
projectId: project.id,
);
final hiddenItem = mapper.fromLockedOccurrence(
occurrence,
revealHiddenLockedBlocks: false,
);
final revealedItem = mapper.fromLockedOccurrence(
occurrence,
revealHiddenLockedBlocks: true,
);
expect(hiddenItem, isNull);
expect(revealedItem, isNotNull);
expect(revealedItem!.id, 'locked-block:work-hours');
expect(revealedItem.displayTitle, 'Work');
expect(revealedItem.start, DateTime(2026, 6, 20, 9));
expect(revealedItem.end, DateTime(2026, 6, 20, 17));
expect(revealedItem.showsExplicitTime, isTrue);
expect(revealedItem.category, TimelineItemCategory.overlay);
expect(revealedItem.projectColorToken, 'project-home');
expect(revealedItem.quickActions, isEmpty);
});
test('visible locked occurrences can produce overlays without reveal mode',
() {
final occurrence = LockedBlockOccurrence(
name: 'Appointment',
interval: TimeInterval(
start: DateTime(2026, 6, 20, 11),
end: DateTime(2026, 6, 20, 12),
),
hiddenByDefault: false,
overrideId: 'appointment-override',
);
final item = mapper.fromLockedOccurrence(
occurrence,
revealHiddenLockedBlocks: false,
);
expect(item, isNotNull);
expect(item!.id, 'locked-override:appointment-override');
expect(item.displayTitle, 'Appointment');
expect(item.category, TimelineItemCategory.overlay);
expect(item.taskType, TaskType.locked);
expect(item.backgroundToken, TimelineBackgroundToken.locked);
expect(item.rewardIconToken, TimelineRewardIconToken.notSet);
expect(item.difficultyIconToken, TimelineDifficultyIconToken.notSet);
expect(item.showsExplicitTime, isTrue);
expect(item.quickActions, isEmpty);
});
}); });
} }