diff --git a/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_08_Today_Timeline_State.md b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_08_Today_Timeline_State.md index 2159d78..d438ab5 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_08_Today_Timeline_State.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCKS_07_10_UPDATED/V1_BLOCK_08_Today_Timeline_State.md @@ -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 diff --git a/Codex Documentation/Current Software Plan/V1_BLOCK_08_Today_Timeline_State.md b/Codex Documentation/Current Software Plan/V1_BLOCK_08_Today_Timeline_State.md index 2159d78..d438ab5 100644 --- a/Codex Documentation/Current Software Plan/V1_BLOCK_08_Today_Timeline_State.md +++ b/Codex Documentation/Current Software Plan/V1_BLOCK_08_Today_Timeline_State.md @@ -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 diff --git a/lib/src/timeline_state.dart b/lib/src/timeline_state.dart index c01e177..500a514 100644 --- a/lib/src/timeline_state.dart +++ b/lib/src/timeline_state.dart @@ -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 || diff --git a/test/timeline_state_test.dart b/test/timeline_state_test.dart index 9768ad8..4c478c2 100644 --- a/test/timeline_state_test.dart +++ b/test/timeline_state_test.dart @@ -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); + }); }); }