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
|
# 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ||
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue