Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/application.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2477,6 +2477,7 @@ let timeline_page
~on_favorites_visible_range
~on_favorites_retry
~timeline_state
~current_day
~loading
~graph_error
~sync_error
Expand Down Expand Up @@ -2518,6 +2519,7 @@ let timeline_page
|> V.Body.static
| None ->
Journal_timeline.view
?current_day
?store:timeline_store
~on_region
~render_source
Expand Down Expand Up @@ -6425,7 +6427,8 @@ let start
let equal_calendar_projection left right =
Option.equal
(fun left right ->
Journal_calendar.equal_projection_fingerprint
Journal_calendar.local_day left = Journal_calendar.local_day right
&& Journal_calendar.equal_projection_fingerprint
(Journal_calendar.projection_fingerprint left)
(Journal_calendar.projection_fingerprint right))
left.calendar
Expand Down Expand Up @@ -6715,6 +6718,7 @@ let start
| _ -> ()))
~on_favorites_retry:(bind_action dispatch Favorites_retry)
~timeline_state:state.timeline
~current_day:(Option.map Journal_calendar.local_day state.calendar)
~loading:(not state.feed_loaded)
~graph_error:(Option.map graph_error_message state.graph_error)
~sync_error:
Expand Down Expand Up @@ -7403,6 +7407,7 @@ module For_testing = struct
~on_favorites_visible_range:handler
~on_favorites_retry:handler
~timeline_state:(Journal_timeline_state.empty ~today:20260908)
~current_day:None
~loading:false
~graph_error:None
~sync_error:None
Expand Down
10 changes: 10 additions & 0 deletions app/journal_calendar.ml
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,13 @@ let present_journal_day day =
; accessibility_label = date_text ^ ", " ^ full_weekdays.(index)
})
;;

let relative_day_label ~today day =
if not (Journal_validation.is_journal_day today && Journal_validation.is_journal_day day)
then None
else
match days_from_civil today - days_from_civil day with
| 0 -> Some "today"
| 1 -> Some "yesterday"
| _ -> None
;;
2 changes: 2 additions & 0 deletions app/journal_calendar.mli
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ type date_presentation =
}

val present_journal_day : int -> (date_presentation, string) result

val relative_day_label : today:int -> int -> string option
6 changes: 3 additions & 3 deletions app/journal_header.ml
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ let feedback ~key ~top ~visible ~compact ~expanded body =
feedback_with_visibility ~key ~top ~visible ~compact ~expanded body
;;

let date_header ~title =
let date_header ?relative_label ~title () =
Ui.Native_widget.widget
chrome
~props:(props ~mode:"header" ~title ())
~props:(props ~mode:"header" ~title ?text:relative_label ())
~on_event:(fun _ -> ())
~children:[]
()
Expand Down Expand Up @@ -132,7 +132,7 @@ let detail ~capture_enabled ~on_capture ~copy_enabled ~on_copy ~copy_feedback bo
Ui.Native_widget.widget
chrome
~key:(Ui.Key.string "journal-detail-header")
~props:(props ~mode:"detail" ~title:"Block" ())
~props:(props ~mode:"detail" ~title:"" ())
~on_event:(fun _ -> ())
~children:[ V.Body.Private.to_widget body; options ]
()
Expand Down
2 changes: 1 addition & 1 deletion app/journal_header.mli
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ val feedback
-> Journal_view.View.Body.t
-> Journal_view.View.Body.t

val date_header : title:string -> Journal_view.View.t
val date_header : ?relative_label:string -> title:string -> unit -> Journal_view.View.t

val detail_text : id:string -> depth:int -> string -> Journal_view.View.t
val detail_row : depth:int -> Journal_view.View.t -> Journal_view.View.t
Expand Down
6 changes: 5 additions & 1 deletion app/journal_timeline.ml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ end
let loading_view () = V.loading ~centered:true ~message:"Loading journal" ()

let view
?current_day
?store
?(on_region = fun _ -> ())
~render_source
Expand All @@ -198,6 +199,7 @@ let view
=
let slots = Timeline.fold_slots (fun acc slot -> slot :: acc) [] state |> List.rev in
let today = Timeline.today state in
let current_day = Option.value current_day ~default:today in
let metadata index slot : Journal_native_collection.row =
let section, header, block_id =
match slot with
Expand All @@ -217,7 +219,9 @@ let view
| Some value -> value.Journal_calendar.date_text
| None -> fallback
in
Journal_header.date_header ~title
Journal_header.date_header
?relative_label:(Journal_calendar.relative_day_label ~today:current_day day)
~title ()
|> V.with_test_id (Ui.Test_id.string ("journal-day-heading:" ^ string_of_int day))
in
let render previous_slot slot =
Expand Down
3 changes: 2 additions & 1 deletion app/journal_timeline.mli
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ end
val loading_view : unit -> Ui.View.t

val view
: ?store:Store.t
: ?current_day:int
-> ?store:Store.t
-> ?on_region:(string -> unit)
-> render_source:(string -> string)
-> render_media:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Timeline relative dates and empty Detail title

## Problem

Timeline dates need immediate today/yesterday context. Block Detail has a redundant visible title.

## Decision

Timeline appends lowercase `today` and `yesterday` after the corresponding dates using native `.subheadline.weight(.regular)` text with secondary foreground alongside the existing `.title2.weight(.semibold)` date, separated by an en space on the shared native Text baseline. Other dates remain unchanged. Detail supplies an empty navigation title while retaining the native bar, back button and options menu.

## Authorization and ownership

The delegated user request explicitly supplies the labels, local calendar semantics, unchanged date format, soft header, accessibility, dynamic sizing, update reuse and isolated PR scope. The follow-up explicitly authorizes removing `Block`, creating a formal PR, independent review and CI tracking without merging. No design questions remain.

Application passes its sampled `Journal_calendar.local_day` as `Journal_timeline.view` current_day, so labels update immediately on the existing calendar refresh path, even while feed refresh is pending or has failed. Root presentation equality includes the local day as well as the time-zone projection fingerprint. `Journal_timeline_state.today` remains the existing feed/timestamp owner and the standalone component fallback. `Journal_calendar.relative_day_label` compares validated Gregorian civil day ordinals, without instant subtraction or extra timers. `Journal_header` carries only presentation props to the existing SwiftUI SectionDate.

## Alternatives considered

### Subtract 86400 seconds

Rejected because local days can contain 23 or 25 hours and time zone changes alter local dates.

### Add a SwiftUI date timer

Rejected because the OCaml calendar already owns local date updates.

## Consequences

- Today/yesterday labels are correct across month, leap day, year, local midnight, time zone and DST boundaries; older/future dates have no label.
- Existing date text, native soft section behavior and header accessibility remain; suffix uses dynamic smaller text.
- Detail has no visible `Block` title; back and options remain available.
- No protected Dune/spec interfaces/locks or mldoc changes enter the PR.
- Native presentation tests cover civil boundaries and updates; all 86 application-view tests, existing calendar/Timeline tests and source-boundary checks pass. The Simulator app builds with the pinned lui source. An isolated iPhone 13 / iOS 26.1 synthetic host verifies actual date labels, next-day/year changes, maximum Dynamic Type, empty Detail title, native Back and menu selection. The host mounts the production components; it does not exercise a personal graph or real device.

## Risks

- A longer header has less horizontal room; verify compact and accessibility dynamic sizes on Simulator.

## Regression ownership

Calendar installation belongs to Application's private effect closure. Its public pure `Root_navigation` events do not expose calendar installation or delayed feed completion; the new regression therefore uses the existing `For_testing.app_with_service` boundary, injected sampler and real foreground packets. It blocks the actual fixture feed request and then fails it, rather than injecting an already incorrect projection. Day and zone cases demonstrate suffix changes before completion, retention after failure, and no added feed query. The independent review found this gap in head `6f8f026`; the initial tests failed with `today` where `yesterday` was required before the fix.

## Relative-label typography

The native subheadline style (15pt at default Dynamic Type) keeps today/yesterday readable beside the semibold 22pt date. Regular weight and the semantic system `Color.secondary` retain the auxiliary hierarchy without shrinking to the 13pt footnote size the user found too small or adding another hierarchical opacity reduction in the List header. An en space separates the two runs; the single Text preserves native baseline alignment, proportional shrinking, Dynamic Type and combined heading semantics. Actual same-device before/after screenshots and maximum accessibility sizing verify the refinement.
6 changes: 3 additions & 3 deletions swift/JournalChrome.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,12 @@ struct JournalSheetDismissButton: SwiftUI.View {

private struct SectionDate: SwiftUI.View {
let title: String
let relativeLabel: String?
@Environment(\.journalControlsSize) private var controlsSize

var body: some SwiftUI.View {
HStack(spacing: 0) {
Text(title)
.font(.title2.weight(.semibold))
Text("\(Text(verbatim: title).font(.title2.weight(.semibold)))\(Text(verbatim: relativeLabel.map { "\u{2002}" + $0 } ?? "").font(.subheadline.weight(.regular)).foregroundStyle(Color.secondary))")
.foregroundStyle(.primary)
.monospacedDigit()
.textCase(nil)
Expand Down Expand Up @@ -225,7 +225,7 @@ struct JournalSheetDismissButton: SwiftUI.View {
}
case .header:
if let properties, context.childIDs.isEmpty, let title = properties.title {
SectionDate(title: title)
SectionDate(title: title, relativeLabel: properties.text)
}
case .detail:
if let title = properties?.title, context.childIDs.count == 2 {
Expand Down
Loading
Loading