fix(web): paginate sleep agenda newest first with page size selection
This commit is contained in:
parent
8995843e46
commit
f9bb96ae08
5 changed files with 284 additions and 33 deletions
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
## Unreleased — 0.1.7
|
||||
|
||||
- Show the Web Sleep agenda newest first with a 5, 10, 15, or All night
|
||||
selector (10 by default). Page changes leave the selected night, editor,
|
||||
statistics, and inclusive PDF range untouched. Removed only the redundant
|
||||
textual sport list below the Web agenda; factual blue overlays, the sport
|
||||
legend, and the complete PDF sport appendix remain.
|
||||
|
||||
- Corrected the Dashboard/Analysis active-Program next-session projection to
|
||||
select title, identity, and current date from one pending Program session.
|
||||
Recorded rescheduling dates replace imported dates, ceded slots are excluded,
|
||||
|
|
|
|||
|
|
@ -236,7 +236,11 @@ Desktop and Android schema numbers are independent. Neither changes the frozen
|
|||
Trainlog JSON V1 contract.
|
||||
|
||||
The active Sleep V2 delta keeps the Web agenda period-owned: every returned
|
||||
night has one row on the shared 18:00-to-18:00 geometry. Selecting a row is
|
||||
night remains available on the shared 18:00-to-18:00 geometry. The Web shows
|
||||
the newest nights first, ten per page by default, with 5, 10, 15, and All
|
||||
choices. The size choice is local to the browser and survives reloads. Paging
|
||||
changes only the visible rows; PDF selection, statistics, and
|
||||
the current editor remain based on the complete loaded period. Selecting a row is
|
||||
presentation-only and atomically drives the detail dates, editor content, and
|
||||
full-width heart-rate detail; it performs no database mutation. PDF preview
|
||||
and download instead share an explicit inclusive range over night start dates,
|
||||
|
|
@ -247,7 +251,8 @@ Each row uses its one Sleep-owned 18:00 offset and 24 elapsed-hour window for
|
|||
Sleep, medication, and sport in both Web and vector PDF. Valid sport intervals
|
||||
intersect `[start,end)` and are clipped only for display; missing or invalid
|
||||
ends are point markers without invented duration. The Web keeps a separate
|
||||
blue lane and accessible session list. Preview and download wait for the
|
||||
blue lane with accessible labels and tooltips; the redundant textual Web sport
|
||||
list is removed. Preview and download wait for the
|
||||
complete sport read and use the same selected sessions. The PDF uses a 180 pt treatment/remarks column with 7.2 pt text,
|
||||
width-aware wrapping, content-driven row heights, and dated continuation pages
|
||||
for exceptional notes. Each selected intake and remark is printed once without
|
||||
|
|
|
|||
|
|
@ -184,8 +184,25 @@ const dayAEntry = (): SleepEntry => ({
|
|||
],
|
||||
});
|
||||
|
||||
const datedEntries = (count: number): SleepEntry[] =>
|
||||
Array.from({ length: count }, (_, index) => {
|
||||
const date = `2026-09-${String(index + 1).padStart(2, "0")}`;
|
||||
const next = `2026-09-${String(index + 2).padStart(2, "0")}`;
|
||||
return {
|
||||
...dayAEntry(),
|
||||
entry_id: `sd_${String(index + 1).padStart(3, "0")}`,
|
||||
revision_id: `sdr_${String(index + 1).padStart(3, "0")}`,
|
||||
night_start_date: date,
|
||||
night_end_date: next,
|
||||
// A recently edited old night must retain its chronological position.
|
||||
updated_at: `2026-10-${String(count - index).padStart(2, "0")}T12:00:00+02:00`,
|
||||
treatment_and_notes: `Observation ${index + 1}`,
|
||||
};
|
||||
});
|
||||
|
||||
describe("SleepDiaryWorkspace", () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.removeItem("trainlog.web.sleep.page-size.v1");
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(new Date("2026-09-21T16:00:00+02:00"));
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -249,6 +266,116 @@ describe("SleepDiaryWorkspace", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it.each([0, 1, 5, 6, 10, 11, 15, 16, 23])(
|
||||
"paginates %i nights newest first without changing the source snapshot",
|
||||
async (count) => {
|
||||
const original = datedEntries(count);
|
||||
const source = original.length === 0 ? [] :
|
||||
[original[0], ...original.slice(1).reverse()];
|
||||
if (source.length > 0) source.push(source[0]);
|
||||
api.fetchSleepDiary.mockResolvedValue(snapshotWith(source));
|
||||
render(<SleepDiaryWorkspace period="all" language="fr" />);
|
||||
await waitFor(() => expect(screen.getByLabelText("Afficher")).toHaveValue("10"));
|
||||
await waitFor(() => expect(screen.queryAllByTestId(/^sleep-agenda-row-/)).toHaveLength(Math.min(count, 10)));
|
||||
const rows = () => screen.queryAllByTestId(/^sleep-agenda-row-/).map((row) => row.getAttribute("data-testid"));
|
||||
expect(rows()).toEqual(original.slice().reverse().slice(0, 10).map((entry) =>
|
||||
`sleep-agenda-row-${entry.entry_id}`));
|
||||
expect(source[0]).toBe(original[0]);
|
||||
if (count > 10) {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Suivant" }));
|
||||
expect(rows()).toEqual(original.slice().reverse().slice(10, 20).map((entry) =>
|
||||
`sleep-agenda-row-${entry.entry_id}`));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Précédent" }));
|
||||
}
|
||||
fireEvent.change(screen.getByLabelText("Afficher"), { target: { value: "all" } });
|
||||
expect(rows()).toHaveLength(count);
|
||||
expect(screen.queryByRole("button", { name: "Suivant" })).toBeNull();
|
||||
for (const size of [5, 15, 10]) {
|
||||
fireEvent.change(screen.getByLabelText("Afficher"), { target: { value: String(size) } });
|
||||
expect(rows()).toHaveLength(Math.min(count, size));
|
||||
}
|
||||
expect(screen.getByLabelText("Afficher").querySelectorAll("option")).toHaveLength(4);
|
||||
expect(api.saveSleepEntry).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps PDF range, selected detail and pending form input across pages and sizes", async () => {
|
||||
const entries = datedEntries(23);
|
||||
api.fetchSleepDiary.mockResolvedValue(snapshotWith(entries));
|
||||
sportApi.fetchSportSessions.mockResolvedValue([{
|
||||
identity: "se_old", label: "Old sport", session_type: "training",
|
||||
started_at: "2026-09-02T20:00:00+02:00",
|
||||
ended_at: "2026-09-02T21:00:00+02:00",
|
||||
}]);
|
||||
const build = vi.spyOn(pdfReport, "buildSleepDiaryPdf").mockReturnValue(
|
||||
new Blob(["all nights"], { type: "application/pdf" }),
|
||||
);
|
||||
vi.spyOn(pdfReport, "presentSleepDiaryPdf").mockImplementation(() => {});
|
||||
render(<SleepDiaryWorkspace period="all" language="fr" />);
|
||||
await screen.findByTestId("sleep-agenda-row-sd_023");
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Prévisualiser" })).toBeEnabled());
|
||||
expect(screen.queryByTestId("sleep-agenda-sport")).toBeNull();
|
||||
expect(screen.queryByText(/Aucune dans la période/)).toBeNull();
|
||||
fireEvent.change(screen.getByTestId("sleep-event-start"), { target: { value: "21:15" } });
|
||||
const start = screen.getByLabelText("PDF du") as HTMLInputElement;
|
||||
const end = screen.getByLabelText("au") as HTMLInputElement;
|
||||
expect([start.value, end.value]).toEqual(["2026-09-01", "2026-09-23"]);
|
||||
for (const size of ["10", "5", "15", "all"]) {
|
||||
fireEvent.change(screen.getByLabelText("Afficher"), { target: { value: size } });
|
||||
if (size !== "all" && size !== "15")
|
||||
fireEvent.click(screen.getByRole("button", { name: "Suivant" }));
|
||||
if (size === "all")
|
||||
expect(screen.getByTestId("sleep-agenda-sport")).toHaveAttribute("aria-label", expect.stringContaining("Old sport"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Prévisualiser" }));
|
||||
expect(screen.getByTestId("sleep-event-start")).toHaveValue("21:15");
|
||||
expect([start.value, end.value]).toEqual(["2026-09-01", "2026-09-23"]);
|
||||
}
|
||||
expect(build).toHaveBeenCalledTimes(4);
|
||||
for (const [selection, , sport, from, to] of build.mock.calls) {
|
||||
expect(selection.entries.map((entry) => entry.entry_id)).toEqual(entries.map((entry) => entry.entry_id));
|
||||
expect(selection.summary.nights).toBe(23);
|
||||
expect(selection.summary.intake_count).toBe(23);
|
||||
expect(sport).toHaveLength(1);
|
||||
expect([from, to]).toEqual(["2026-09-01", "2026-09-23"]);
|
||||
}
|
||||
expect(screen.queryByText(/Sport — horaires enregistrés/)).toBeNull();
|
||||
expect(screen.getByText("Bleu : séance de sport effectuée")).toBeInTheDocument();
|
||||
expect(api.saveSleepEntry).not.toHaveBeenCalled();
|
||||
expect(api.deleteSleepEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retains the page size but returns to newest on a deliberate period change", async () => {
|
||||
const many = datedEntries(16);
|
||||
const fewer = many.slice(0, 6);
|
||||
api.fetchSleepDiary
|
||||
.mockResolvedValueOnce(snapshotWith(many))
|
||||
.mockResolvedValue(snapshotWith(fewer));
|
||||
const view = render(<SleepDiaryWorkspace period="all" language="en" />);
|
||||
await screen.findByTestId("sleep-agenda-row-sd_016");
|
||||
fireEvent.change(screen.getByLabelText("Show"), { target: { value: "5" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect(screen.getByText("Nights 6–10 of 16 · Page 2/4")).toBeInTheDocument();
|
||||
view.rerender(<SleepDiaryWorkspace period="7d" language="en" />);
|
||||
await waitFor(() => expect(screen.queryAllByTestId(/^sleep-agenda-row-/)).toHaveLength(5));
|
||||
expect(screen.getByLabelText("Show")).toHaveValue("5");
|
||||
expect(screen.getByTestId("sleep-agenda-row-sd_006")).toBeInTheDocument();
|
||||
expect(screen.getByText("Nights 1–5 of 6 · Page 1/2")).toBeInTheDocument();
|
||||
expect(api.saveSleepEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restores the local page-size choice after remounting", async () => {
|
||||
api.fetchSleepDiary.mockResolvedValue(snapshotWith(datedEntries(11)));
|
||||
const first = render(<SleepDiaryWorkspace period="all" language="fr" />);
|
||||
await screen.findByTestId("sleep-agenda-row-sd_011");
|
||||
fireEvent.change(screen.getByLabelText("Afficher"), { target: { value: "5" } });
|
||||
expect(window.localStorage.getItem("trainlog.web.sleep.page-size.v1")).toBe("5");
|
||||
first.unmount();
|
||||
render(<SleepDiaryWorkspace period="all" language="fr" />);
|
||||
await screen.findByTestId("sleep-agenda-row-sd_011");
|
||||
expect(screen.getByLabelText("Afficher")).toHaveValue("5");
|
||||
expect(screen.queryAllByTestId(/^sleep-agenda-row-/)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("uses the same inclusive date-range subset for PDF preview and export", async () => {
|
||||
const first = dayAEntry();
|
||||
const second = {
|
||||
|
|
@ -310,13 +437,13 @@ describe("SleepDiaryWorkspace", () => {
|
|||
const view = render(<SleepDiaryWorkspace period="30d" language="fr" />);
|
||||
await waitFor(() => expect(sportApi.fetchSportSessions).toHaveBeenCalledTimes(1));
|
||||
view.rerender(<SleepDiaryWorkspace period="7d" language="fr" />);
|
||||
await screen.findByText(/Current session/);
|
||||
await screen.findByRole("img", { name: /Current session/ });
|
||||
finishOld([{
|
||||
identity: "se_stale", label: "Stale session", session_type: "training",
|
||||
started_at: "2026-09-21T07:00:00+02:00",
|
||||
ended_at: "2026-09-21T08:00:00+02:00",
|
||||
}]);
|
||||
await waitFor(() => expect(screen.queryByText(/Stale session/)).toBeNull());
|
||||
await waitFor(() => expect(screen.queryByRole("img", { name: /Stale session/ })).toBeNull());
|
||||
});
|
||||
|
||||
it("can export sport in a period with no recorded Sleep night", async () => {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,19 @@ import {
|
|||
} from "./sleepVisualProjection";
|
||||
|
||||
type Language = "fr" | "en";
|
||||
type AgendaPageSize = 5 | 10 | 15 | "all";
|
||||
const agendaPageSizeKey = "trainlog.web.sleep.page-size.v1";
|
||||
const initialAgendaPageSize = (): AgendaPageSize => {
|
||||
try {
|
||||
const stored = window.localStorage.getItem(agendaPageSizeKey);
|
||||
if (stored === "all") return "all";
|
||||
if (stored === "5" || stored === "10" || stored === "15")
|
||||
return Number(stored) as 5 | 10 | 15;
|
||||
} catch {
|
||||
// A disabled browser storage falls back to the documented first-use size.
|
||||
}
|
||||
return 10;
|
||||
};
|
||||
const q: readonly SleepQuality[] = ["TB", "B", "Moy", "M", "TM"];
|
||||
const pointTypes: readonly SleepEventType[] = [
|
||||
"bed_time",
|
||||
|
|
@ -71,6 +84,13 @@ const copy = {
|
|||
summary: "Synthèse factuelle",
|
||||
empty: "Aucune nuit enregistrée.",
|
||||
agendaEmpty: "Aucune donnée pour cette nuit.",
|
||||
show: "Afficher",
|
||||
all: "Tout",
|
||||
nightsCount: "Nuits",
|
||||
of: "sur",
|
||||
page: "Page",
|
||||
previous: "Précédent",
|
||||
next: "Suivant",
|
||||
preview: "Prévisualiser",
|
||||
export: "Exporter PDF",
|
||||
pdfFrom: "PDF du",
|
||||
|
|
@ -134,6 +154,13 @@ const copy = {
|
|||
summary: "Factual summary",
|
||||
empty: "No recorded nights.",
|
||||
agendaEmpty: "No data for this night.",
|
||||
show: "Show",
|
||||
all: "All",
|
||||
nightsCount: "Nights",
|
||||
of: "of",
|
||||
page: "Page",
|
||||
previous: "Previous",
|
||||
next: "Next",
|
||||
preview: "Preview",
|
||||
export: "Export PDF",
|
||||
pdfFrom: "PDF from",
|
||||
|
|
@ -318,6 +345,8 @@ export function SleepDiaryWorkspace({
|
|||
const [intakeUnit, setIntakeUnit] = useState("mg");
|
||||
const [intakeQuantity, setIntakeQuantity] = useState(1);
|
||||
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
|
||||
const [agendaPageSize, setAgendaPageSize] = useState<AgendaPageSize>(initialAgendaPageSize);
|
||||
const [agendaPage, setAgendaPage] = useState(0);
|
||||
const identity = useRef<{ entry_id: string; revision_id: string } | null>(
|
||||
null,
|
||||
);
|
||||
|
|
@ -392,6 +421,11 @@ export function SleepDiaryWorkspace({
|
|||
useEffect(() => {
|
||||
void reload();
|
||||
}, [period]);
|
||||
useEffect(() => {
|
||||
// CONTRACT: a deliberate period change starts at its newest night while
|
||||
// keeping the user's page-size choice and all editor/PDF state separate.
|
||||
setAgendaPage(0);
|
||||
}, [period]);
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setSportState("loading");
|
||||
|
|
@ -725,7 +759,22 @@ export function SleepDiaryWorkspace({
|
|||
const selectedEntry =
|
||||
agendaEntries.find((entry) => entry.entry_id === selectedEntryId) ??
|
||||
activeEntry ??
|
||||
agendaEntries[0];
|
||||
agendaEntries[agendaEntries.length - 1];
|
||||
// INVARIANT: presentation sorting and slicing never mutate or narrow the
|
||||
// ascending, deduplicated collection used by PDF and sport projections.
|
||||
const recentEntries = [...agendaEntries].sort((left, right) =>
|
||||
right.night_start_date.localeCompare(left.night_start_date) ||
|
||||
left.entry_id.localeCompare(right.entry_id));
|
||||
const pageCount = agendaPageSize === "all" ? 1 :
|
||||
Math.ceil(recentEntries.length / agendaPageSize);
|
||||
useEffect(() => {
|
||||
if (snapshot) setAgendaPage((current) => Math.min(current, Math.max(0, pageCount - 1)));
|
||||
}, [pageCount, snapshot]);
|
||||
const visiblePage = Math.min(agendaPage, Math.max(0, pageCount - 1));
|
||||
const firstVisible = agendaPageSize === "all" ? 0 :
|
||||
visiblePage * agendaPageSize;
|
||||
const visibleEntries = agendaPageSize === "all" ? recentEntries :
|
||||
recentEntries.slice(firstVisible, firstVisible + agendaPageSize);
|
||||
const [pdfStartDate, setPdfStartDate] = useState("");
|
||||
const [pdfEndDate, setPdfEndDate] = useState("");
|
||||
const pdfRangeInitialized = useRef(false);
|
||||
|
|
@ -753,8 +802,6 @@ export function SleepDiaryWorkspace({
|
|||
// selection. The range concerns the date on which each night begins; detail
|
||||
// selection remains independent and continues to drive the HR/editor view.
|
||||
const pdfWindows = pdfEntries.map(sleepRowWindow);
|
||||
const agendaSport = sport.filter((session) => agendaEntries.some((entry) =>
|
||||
projectSportSegments([session], sleepRowWindow(entry)).length > 0));
|
||||
const fallbackStart = Date.parse(`${pdfStartDate}T18:00:00`);
|
||||
const fallbackEnd = Date.parse(`${pdfEndDate}T18:00:00`) + 86400000;
|
||||
const pdfWindow = pdfWindows.length > 0 ? {
|
||||
|
|
@ -763,7 +810,6 @@ export function SleepDiaryWorkspace({
|
|||
} : { start: fallbackStart, end: fallbackEnd };
|
||||
const pdfSport = pdfRangeValid ? sport.filter((session) =>
|
||||
projectSportSegments([session], pdfWindow).length > 0) : [];
|
||||
const listedSport = agendaEntries.length === 0 ? pdfSport : agendaSport;
|
||||
const selectedSnapshot = snapshot && sportState === "ready" && pdfRangeValid &&
|
||||
(pdfEntries.length > 0 || pdfSport.length > 0)
|
||||
? sleepSnapshotSelection(snapshot, pdfEntries)
|
||||
|
|
@ -833,8 +879,46 @@ export function SleepDiaryWorkspace({
|
|||
)}
|
||||
{sportState === "ready" && <p className="sleep-sport-legend">
|
||||
{language === "fr" ? "Bleu : séance de sport effectuée" : "Blue: completed sport session"}
|
||||
{listedSport.length === 0 && (language === "fr" ? " · Aucune dans la période" : " · None in this period")}
|
||||
</p>}
|
||||
<div className="sleep-agenda-pagination" aria-label={t.nightsCount}>
|
||||
<label>
|
||||
<span>{t.show} :</span>
|
||||
<select
|
||||
aria-label={t.show}
|
||||
value={agendaPageSize}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
const next = value === "all" ? "all" : Number(value) as 5 | 10 | 15;
|
||||
setAgendaPageSize(next);
|
||||
try {
|
||||
window.localStorage.setItem(agendaPageSizeKey, String(next));
|
||||
} catch {
|
||||
// The current page still works when browser storage is unavailable.
|
||||
}
|
||||
setAgendaPage(0);
|
||||
}}
|
||||
>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="15">15</option>
|
||||
<option value="all">{t.all}</option>
|
||||
</select>
|
||||
</label>
|
||||
<span role="status" aria-live="polite">
|
||||
{t.nightsCount} {recentEntries.length === 0 ? "0" :
|
||||
`${firstVisible + 1}–${firstVisible + visibleEntries.length}`} {t.of} {recentEntries.length}
|
||||
{agendaPageSize !== "all" && pageCount > 1 &&
|
||||
` · ${t.page} ${visiblePage + 1}/${pageCount}`}
|
||||
</span>
|
||||
{agendaPageSize !== "all" && pageCount > 1 && (
|
||||
<div className="sleep-agenda-page-buttons">
|
||||
<button type="button" className="quiet-action" disabled={visiblePage === 0}
|
||||
onClick={() => setAgendaPage(Math.max(0, visiblePage - 1))}>{t.previous}</button>
|
||||
<button type="button" className="quiet-action" disabled={visiblePage >= pageCount - 1}
|
||||
onClick={() => setAgendaPage(Math.min(pageCount - 1, visiblePage + 1))}>{t.next}</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{agendaEntries.length === 0 ? (
|
||||
<p className="analysis-empty">{t.agendaEmpty}</p>
|
||||
) : (
|
||||
|
|
@ -864,7 +948,7 @@ export function SleepDiaryWorkspace({
|
|||
</span>
|
||||
<span />
|
||||
</div>
|
||||
{agendaEntries.map((entry) => {
|
||||
{visibleEntries.map((entry) => {
|
||||
const facts = sleepEntryFacts(entry);
|
||||
const projection = projectSleepTimeline(entry);
|
||||
// WHY: persistence metadata may be UTC while factual night
|
||||
|
|
@ -1080,21 +1164,6 @@ export function SleepDiaryWorkspace({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{sportState === "ready" && listedSport.length > 0 && (
|
||||
<div className="sleep-sport-list">
|
||||
<strong>{language === "fr" ? "Sport — horaires enregistrés" : "Sport — recorded times"}</strong>
|
||||
<ul>{listedSport.map((session) => (
|
||||
<li key={session.identity}>
|
||||
{session.label} ({session.session_type}) · {session.started_at.slice(0, 16)} → {
|
||||
session.ended_at && Date.parse(session.ended_at) > Date.parse(session.started_at)
|
||||
? session.ended_at.slice(0, 16)
|
||||
: (language === "fr" ? "fin non renseignée" : "end not recorded")}
|
||||
{agendaEntries.length === 0 && (language === "fr"
|
||||
? " · sommeil non renseigné" : " · sleep not recorded")}
|
||||
</li>
|
||||
))}</ul>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
|
||||
{selectedEntry && (
|
||||
|
|
|
|||
|
|
@ -609,8 +609,13 @@ h2 {
|
|||
|
||||
.sleep-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
.sleep-workspace .analysis-wide {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.sleep-heading,
|
||||
.sleep-actions,
|
||||
.sleep-entry-header {
|
||||
|
|
@ -633,6 +638,9 @@ h2 {
|
|||
font-size: 0.72rem;
|
||||
}
|
||||
.sleep-pdf-controls {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.55rem;
|
||||
border: 1px solid var(--surface-1);
|
||||
border-radius: 0.75rem;
|
||||
|
|
@ -659,6 +667,21 @@ h2 {
|
|||
outline-offset: 1px;
|
||||
border-color: var(--lavender);
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.sleep-heading > .sleep-pdf-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
}
|
||||
.sleep-pdf-controls label {
|
||||
min-width: 0;
|
||||
}
|
||||
.sleep-pdf-controls input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.sleep-pdf-range-error {
|
||||
margin: 0.25rem 0 0.6rem;
|
||||
color: var(--peach);
|
||||
|
|
@ -850,6 +873,35 @@ h2 {
|
|||
color: #629bff;
|
||||
font-weight: 700;
|
||||
}
|
||||
.sleep-agenda-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 0.8rem;
|
||||
margin: 0.5rem 0;
|
||||
color: var(--subtext-0);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
.sleep-agenda-pagination label,
|
||||
.sleep-agenda-page-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
.sleep-agenda-pagination select {
|
||||
min-height: 2.6rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
color: var(--text);
|
||||
background: var(--base);
|
||||
border: 1px solid var(--surface-2);
|
||||
border-radius: 0.5rem;
|
||||
font: inherit;
|
||||
}
|
||||
.sleep-agenda-pagination select:focus-visible,
|
||||
.sleep-agenda-page-buttons button:focus-visible {
|
||||
outline: 3px solid var(--lavender);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.sleep-sport {
|
||||
position: absolute;
|
||||
top: 0.1rem;
|
||||
|
|
@ -860,14 +912,6 @@ h2 {
|
|||
background: #3984eb;
|
||||
z-index: 3;
|
||||
}
|
||||
.sleep-sport-list {
|
||||
margin-top: 0.6rem;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
.sleep-sport-list ul {
|
||||
margin: 0.3rem 0 0;
|
||||
padding-left: 1.3rem;
|
||||
}
|
||||
.sleep-point,
|
||||
.sleep-medication {
|
||||
width: 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue