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
|
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 latest tap, while Levé clamps it to the factual final-get-up timestamp.
|
||||||
The policy is revisioned, undoable, and independent of BPM/RR inference.
|
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.
|
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
|
## 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
|
intakes, and bounds. The agenda renders an estimated interval with distinct
|
||||||
hatching, never substitutes it for a factual interval, and retains the exact
|
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
|
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
|
sleep intervals or, in their absence, the displayed estimate minus overlapping
|
||||||
structured awakenings. The overlays remain available without an HR capture and
|
structured awakenings. The overlays remain available without an HR capture and
|
||||||
do not fabricate BPM.
|
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
|
and download receive the same explicit inclusive range over each entry's
|
||||||
`night_start_date`, and all observations are recomputed from that subset. The
|
`night_start_date`, and all observations are recomputed from that subset. The
|
||||||
range initially spans the loaded agenda period; later detail/HR selection does
|
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
|
not alter it. Each of the 24 hour labels is centered in the equal visual cell
|
||||||
columns used beneath every 18:00-to-18:00 agenda row.
|
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
|
## Automated validation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -721,26 +721,21 @@ describe("SleepDiaryWorkspace", () => {
|
||||||
);
|
);
|
||||||
expect(intake).toHaveStyle({ left: "18.75%" });
|
expect(intake).toHaveStyle({ left: "18.75%" });
|
||||||
const ticks = Array.from(document.querySelectorAll<HTMLElement>(".sleep-hour-axis > span"));
|
const ticks = Array.from(document.querySelectorAll<HTMLElement>(".sleep-hour-axis > span"));
|
||||||
expect(ticks).toHaveLength(25);
|
expect(ticks).toHaveLength(24);
|
||||||
expect(
|
expect(
|
||||||
ticks
|
ticks
|
||||||
.map((tick) => [tick.textContent, tick.style.left])
|
.filter((_, index) => index % 2 === 0)
|
||||||
.filter((_, index) => index % 2 === 0),
|
.map((tick) => tick.textContent),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
["18", "0%"],
|
"18", "20", "22", "00", "02", "04",
|
||||||
["20", `${(2 / 24) * 100}%`],
|
"06", "08", "10", "12", "14", "16",
|
||||||
["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%"],
|
|
||||||
]);
|
]);
|
||||||
|
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[1]).toHaveClass("sleep-hour-tick-odd");
|
||||||
expect(ticks[2]).toHaveClass("sleep-hour-tick-even");
|
expect(ticks[2]).toHaveClass("sleep-hour-tick-even");
|
||||||
expect(ticks[6]).toHaveClass("sleep-hour-tick-major");
|
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", () => {
|
it("derives exact one-hour and two-hour 18:00-to-18:00 ticks", () => {
|
||||||
const hourly = sleepAgendaTicks();
|
const hourly = sleepAgendaTicks();
|
||||||
expect(hourly).toHaveLength(25);
|
expect(hourly).toHaveLength(24);
|
||||||
expect(hourly[0]).toEqual({ hourOffset: 0, position: 0, label: "18" });
|
expect(hourly[0]).toEqual({ hourOffset: 0, position: 1 / 48, label: "18" });
|
||||||
expect(hourly[6]).toEqual({ hourOffset: 6, position: 0.25, label: "00" });
|
expect(hourly[6]).toEqual({ hourOffset: 6, position: 13 / 48, label: "00" });
|
||||||
expect(hourly[24]).toEqual({ hourOffset: 24, position: 1, label: "18" });
|
expect(hourly[23]).toEqual({ hourOffset: 23, position: 47 / 48, label: "17" });
|
||||||
expect(sleepAgendaTicks(2).map((tick) => tick.label)).toEqual([
|
expect(sleepAgendaTicks(2).map((tick) => tick.label)).toEqual([
|
||||||
"18",
|
"18",
|
||||||
"20",
|
"20",
|
||||||
|
|
@ -91,7 +91,6 @@ describe("sleep timeline geometry", () => {
|
||||||
"12",
|
"12",
|
||||||
"14",
|
"14",
|
||||||
"16",
|
"16",
|
||||||
"18",
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ export function sleepAgendaTicks(stepHours = 1): SleepAgendaTick[] {
|
||||||
if (!Number.isInteger(stepHours) || stepHours < 1 || 24 % stepHours !== 0) {
|
if (!Number.isInteger(stepHours) || stepHours < 1 || 24 % stepHours !== 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return Array.from({ length: 24 / stepHours + 1 }, (_, index) => {
|
return Array.from({ length: 24 / stepHours }, (_, index) => {
|
||||||
const hourOffset = index * stepHours;
|
const hourOffset = index * stepHours;
|
||||||
return {
|
return {
|
||||||
hourOffset,
|
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"),
|
label: String((18 + hourOffset) % 24).padStart(2, "0"),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -632,8 +632,32 @@ h2 {
|
||||||
color: var(--subtext-0);
|
color: var(--subtext-0);
|
||||||
font-size: 0.72rem;
|
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 {
|
.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 {
|
.sleep-pdf-range-error {
|
||||||
margin: 0.25rem 0 0.6rem;
|
margin: 0.25rem 0 0.6rem;
|
||||||
|
|
@ -731,12 +755,6 @@ h2 {
|
||||||
.sleep-hour-tick-odd {
|
.sleep-hour-tick-odd {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.sleep-hour-axis > span:first-child {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
.sleep-hour-axis > span:last-child {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
.sleep-row {
|
.sleep-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--sleep-label-column) var(--sleep-track-column) var(--sleep-facts-column);
|
grid-template-columns: var(--sleep-label-column) var(--sleep-track-column) var(--sleep-facts-column);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue