feat(timeline): add locked overlay reveal state
This commit is contained in:
parent
6f31290397
commit
9c19b85f03
4 changed files with 132 additions and 4 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
|
|
@ -68,6 +68,14 @@ Acceptance criteria:
|
|||
- Overlay item shows a name and explicit start/end times.
|
||||
- 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.
|
||||
|
||||
## Chunk 8.3 — Compact mode state
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
|
|
@ -68,6 +68,14 @@ Acceptance criteria:
|
|||
- Overlay item shows a name and explicit start/end times.
|
||||
- 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.
|
||||
|
||||
## Chunk 8.3 — Compact mode state
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
// without importing Flutter or choosing visual assets. UI layers can map these
|
||||
// tokens to colors, icons, text, or widgets later.
|
||||
|
||||
import 'locked_time.dart';
|
||||
import 'models.dart';
|
||||
|
||||
/// Broad display category for an item placed on the Today timeline.
|
||||
|
|
@ -138,8 +139,7 @@ class TimelineItemMapper {
|
|||
id: task.id,
|
||||
displayTitle: task.title,
|
||||
taskType: task.type,
|
||||
projectColorToken:
|
||||
projectColorTokensById[task.projectId] ?? task.projectId,
|
||||
projectColorToken: _projectColorTokenFor(task.projectId),
|
||||
backgroundToken: _backgroundTokenFor(task.type),
|
||||
rewardIconToken: _rewardIconTokenFor(task.reward),
|
||||
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) {
|
||||
return type == TaskType.inflexible ||
|
||||
type == TaskType.critical ||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,69 @@ void main() {
|
|||
expect(item.backgroundToken, TimelineBackgroundToken.locked);
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue