fix(web): center sleep hours and refine date controls

This commit is contained in:
fy59 2026-09-24 08:35:13 +02:00
parent ffcdc3efcf
commit 77b5bbca85
7 changed files with 53 additions and 35 deletions

View file

@ -72,9 +72,11 @@ also refined.
default. A repeated tap inside the window extends its end to 30 minutes after
the latest tap, while Levé clamps it to the factual final-get-up timestamp.
The policy is revisioned, undoable, and independent of BPM/RR inference.
- Aligned the Sleep agenda's hour labels with a shared 24-column visual grid
- Centered the Sleep agenda's hour labels inside a shared 24-column visual grid
and added an inclusive night-start date range for PDF preview and export.
Detail/HR night selection remains independent from the report range.
The duplicate terminal `18` is removed; detail/HR night selection remains
independent from the report range, whose date fields now use a grouped
Trainlog form treatment.
## 0.1.5 — 2026-09-23

View file

@ -205,7 +205,8 @@ sleep intervals, every structured long-awake interval, point events, medication
intakes, and bounds. The agenda renders an estimated interval with distinct
hatching, never substitutes it for a factual interval, and retains the exact
shared 18:00-to-18:00 geometry, reinforced by 24 equal hour columns shared by
the labels and every row. Displayed sleep duration is the sum of factual
every row, with each hour label centered inside the cell it names. Displayed
sleep duration is the sum of factual
sleep intervals or, in their absence, the displayed estimate minus overlapping
structured awakenings. The overlays remain available without an HR capture and
do not fabricate BPM.

View file

@ -15,8 +15,9 @@ editor snapshot, and HR context in memory without issuing a write. PDF preview
and download receive the same explicit inclusive range over each entry's
`night_start_date`, and all observations are recomputed from that subset. The
range initially spans the loaded agenda period; later detail/HR selection does
not alter it. The floating hour axis is reinforced by the same 24 equal visual
columns used beneath every 18:00-to-18:00 agenda row.
not alter it. Each of the 24 hour labels is centered in the equal visual cell
it names, using the same grid beneath every 18:00-to-18:00 agenda row; there is
no ambiguous duplicate `18` at the right edge.
## Automated validation

View file

@ -721,26 +721,21 @@ describe("SleepDiaryWorkspace", () => {
);
expect(intake).toHaveStyle({ left: "18.75%" });
const ticks = Array.from(document.querySelectorAll<HTMLElement>(".sleep-hour-axis > span"));
expect(ticks).toHaveLength(25);
expect(ticks).toHaveLength(24);
expect(
ticks
.map((tick) => [tick.textContent, tick.style.left])
.filter((_, index) => index % 2 === 0),
.filter((_, index) => index % 2 === 0)
.map((tick) => tick.textContent),
).toEqual([
["18", "0%"],
["20", `${(2 / 24) * 100}%`],
["22", `${(4 / 24) * 100}%`],
["00", "25%"],
["02", `${(8 / 24) * 100}%`],
["04", `${(10 / 24) * 100}%`],
["06", "50%"],
["08", `${(14 / 24) * 100}%`],
["10", `${(16 / 24) * 100}%`],
["12", "75%"],
["14", `${(20 / 24) * 100}%`],
["16", `${(22 / 24) * 100}%`],
["18", "100%"],
"18", "20", "22", "00", "02", "04",
"06", "08", "10", "12", "14", "16",
]);
ticks.forEach((tick, index) => {
expect(Number.parseFloat(tick.style.left)).toBeCloseTo(
((index + 0.5) / 24) * 100,
9,
);
});
expect(ticks[1]).toHaveClass("sleep-hour-tick-odd");
expect(ticks[2]).toHaveClass("sleep-hour-tick-even");
expect(ticks[6]).toHaveClass("sleep-hour-tick-major");

View file

@ -74,10 +74,10 @@ describe("sleep timeline geometry", () => {
it("derives exact one-hour and two-hour 18:00-to-18:00 ticks", () => {
const hourly = sleepAgendaTicks();
expect(hourly).toHaveLength(25);
expect(hourly[0]).toEqual({ hourOffset: 0, position: 0, label: "18" });
expect(hourly[6]).toEqual({ hourOffset: 6, position: 0.25, label: "00" });
expect(hourly[24]).toEqual({ hourOffset: 24, position: 1, label: "18" });
expect(hourly).toHaveLength(24);
expect(hourly[0]).toEqual({ hourOffset: 0, position: 1 / 48, label: "18" });
expect(hourly[6]).toEqual({ hourOffset: 6, position: 13 / 48, label: "00" });
expect(hourly[23]).toEqual({ hourOffset: 23, position: 47 / 48, label: "17" });
expect(sleepAgendaTicks(2).map((tick) => tick.label)).toEqual([
"18",
"20",
@ -91,7 +91,6 @@ describe("sleep timeline geometry", () => {
"12",
"14",
"16",
"18",
]);
});
});

View file

@ -15,11 +15,13 @@ export function sleepAgendaTicks(stepHours = 1): SleepAgendaTick[] {
if (!Number.isInteger(stepHours) || stepHours < 1 || 24 % stepHours !== 0) {
return [];
}
return Array.from({ length: 24 / stepHours + 1 }, (_, index) => {
return Array.from({ length: 24 / stepHours }, (_, index) => {
const hourOffset = index * stepHours;
return {
hourOffset,
position: hourOffset / 24,
// CONTRACT: labels name hour cells, so they sit at each cell's center;
// event positions and vertical grid boundaries remain exact instants.
position: (hourOffset + stepHours / 2) / 24,
label: String((18 + hourOffset) % 24).padStart(2, "0"),
};
});

View file

@ -632,8 +632,32 @@ h2 {
color: var(--subtext-0);
font-size: 0.72rem;
}
.sleep-pdf-controls {
padding: 0.55rem;
border: 1px solid var(--surface-1);
border-radius: 0.75rem;
background: var(--mantle);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 3%);
}
.sleep-pdf-controls input {
min-height: 2.25rem;
min-width: 10.5rem;
min-height: 2.6rem;
padding: 0.55rem 0.7rem;
color: var(--text);
color-scheme: dark;
background: var(--base);
border: 1px solid var(--surface-2);
border-radius: 0.5rem;
font: inherit;
font-variant-numeric: tabular-nums;
}
.sleep-pdf-controls input:hover {
border-color: var(--overlay-0);
}
.sleep-pdf-controls input:focus-visible {
outline: 3px solid var(--lavender);
outline-offset: 1px;
border-color: var(--lavender);
}
.sleep-pdf-range-error {
margin: 0.25rem 0 0.6rem;
@ -731,12 +755,6 @@ h2 {
.sleep-hour-tick-odd {
display: none;
}
.sleep-hour-axis > span:first-child {
transform: none;
}
.sleep-hour-axis > span:last-child {
transform: translateX(-100%);
}
.sleep-row {
display: grid;
grid-template-columns: var(--sleep-label-column) var(--sleep-track-column) var(--sleep-facts-column);