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
Original file line number Diff line number Diff line change
Expand Up @@ -390,18 +390,19 @@ public final class TimeGridView: NSView {
palette: (background: NSColor, surface: NSColor, surfaceRaised: NSColor, border: NSColor, text: NSColor, textMuted: NSColor),
currentHour: Int
) {
// Web `CalendarTimeColumn`: `top: calc(100% / TIMED_VISIBLE_HOURS - 5px)`.
let labelColumnTop = max(0, hourHeight - 5)
for hour in 0 ..< Int(ceil(state.civilDayHours)) {
let text = hour == 0 ? "" : (labels.indices.contains(hour - 1) ? labels[hour - 1] : "")
// Web `HourLabelColumn` (#4686): one hour slot per label from 1 AM,
// starting at the top of the track, the same 1 AM origin cards, hour
// lines and the now line use (`timedGridTopPx`). The old
// `calc(100% / TIMED_VISIBLE_HOURS - 5px)` offset plus an empty first
// slot drew every label two hours below its events.
for (index, text) in labels.enumerated() {
let label = NSTextField(labelWithString: text)
label.font = CompassFont.nsFont(size: 10)
let isCurrentHour = hour > 0 && hour == currentHour
label.textColor = isCurrentHour ? themeAccentColor : palette.textMuted
label.textColor = index + 1 == currentHour ? themeAccentColor : palette.textMuted
label.alignment = .center
label.frame = NSRect(
x: xOffset + 4,
y: labelColumnTop + Double(hour) * hourHeight + 4,
y: Double(index) * hourHeight + 1,
width: width - 8,
height: 14)
hourGutterView.addSubview(label)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ final class TimeGridCivilDayTests: XCTestCase {
}
}

/// Web #4686: the 5 PM label sits at the top of the 5 PM slot, where a
/// 5 PM event starts (1 AM track origin), not two hours lower.
func testHourLabelsShareTheEventTrackOrigin() throws {
let savedZone = EffectiveTimeZone.identifier
defer { EffectiveTimeZone.identifier = savedZone }
EffectiveTimeZone.identifier = "UTC"
let state = TimeGridState(layoutMode: .day,
scenario: GridLayoutScenario(layoutMode: .day,
referenceNow: GridLayoutSnapshotFixtures.referenceNow, visibleDateKeys: ["2026-05-20"]),
effectiveTimeZone: "UTC")
let view = TimeGridView(state: state, theme: .lightBeach)
view.frame = NSRect(x: 0, y: 0, width: 800, height: 600)
view.layoutSubtreeIfNeeded()
let document = try XCTUnwrap(descendants(view).compactMap { $0 as? NSScrollView }.first?.documentView)
let hourHeight = (document.frame.height - GridTimeConstants.gridPaddingBottom) / state.civilDayHours
let label = try XCTUnwrap(descendants(view).compactMap { $0 as? NSTextField }.first { $0.stringValue == "5 PM" })
let eventTop = GridTimeConstants.timedGridTopPx(minutesFromCivilMidnight: 17 * 60, hourHeight: hourHeight)
XCTAssertEqual(label.frame.minY, eventTop, accuracy: 3, "5 PM label at \(label.frame.minY), 5 PM event top at \(eventTop)")
}

private func descendants(_ view: NSView) -> [NSView] {
view.subviews.flatMap { [$0] + descendants($0) }
}
Expand Down
1 change: 1 addition & 0 deletions docs/features/desktop-web-parity-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ Each PR should link a child issue, include before/after screenshots (web Chromiu
| 2026-10-21 | Hold-Mod sidebar chips: month picker and Up next chips render, calendar list chip moved onto the list (compare run 39634795849) |
| 2026-10-21 | Per-account page-jump targets (web `calendar-account:` ids) |
| 2026-10-21 | Event jump chips use web day-prefix labels (W1, SU2) and digits after a day pick |
| 2026-10-22 | Week/day hour gutter labels use web's 1 AM track origin (#4686); they sat two hours below their events (a 5 PM event drew on the 3 PM row) |
| 2026-10-22 | Parity capture draws popover windows (form date picker, time list) over the window render, so those surfaces stop showing a closed field |
| 2026-10-22 | macOS capture: register `onboarding-welcome-account-choice` so the welcome account step exports instead of timing out |

Expand Down
Loading