fix(web): center sleep hours and refine date controls
This commit is contained in:
parent
ffcdc3efcf
commit
77b5bbca85
7 changed files with 53 additions and 35 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue