Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cebe923d68 | |||
| 8dc4fca4e2 | |||
| b030cfb72a | |||
| 7cb59a7a23 | |||
| 131346a7c7 | |||
| b608dfc342 |
+198
-8
@@ -989,20 +989,27 @@
|
||||
parts.push(`<text x="${ML - 4}" y="${y + 3}" text-anchor="end">${vText}h</text>`);
|
||||
}
|
||||
|
||||
const selYmd = ymd(selectedDay());
|
||||
days.forEach((d, i) => {
|
||||
const isToday = i === days.length - 1;
|
||||
const isSel = d.ymd === selYmd;
|
||||
const x = ML + i * (barW + gap);
|
||||
const h = (d.sleepHours / yMax) * innerH;
|
||||
const y = MT + innerH - h;
|
||||
const title = `${d.date.toLocaleDateString(undefined, { weekday: "long", month: "short", day: "numeric" })} — ${d.sleepHours.toFixed(1)}h`;
|
||||
if (isSel) {
|
||||
parts.push(`<rect class="day-highlight" x="${(x - gap / 2).toFixed(1)}" y="${MT}" width="${(barW + gap).toFixed(1)}" height="${innerH}" rx="3"/>`);
|
||||
}
|
||||
parts.push(
|
||||
`<rect class="bar bar-sleep ${isToday ? "" : "bar-faded"}" data-day="${d.ymd}" ` +
|
||||
`x="${x}" y="${y}" width="${barW}" height="${Math.max(0, h)}" rx="3">` +
|
||||
`<title>${escapeText(title)}</title></rect>`
|
||||
);
|
||||
if (showDayLabel(i, days.length)) {
|
||||
// The selected day always gets a label (accent-colored), even on wide
|
||||
// windows that would otherwise skip it.
|
||||
if (showDayLabel(i, days.length) || isSel) {
|
||||
parts.push(
|
||||
`<text x="${x + barW / 2}" y="${H - MB + 14}" text-anchor="middle">` +
|
||||
`<text class="${isSel ? "day-label-sel" : ""}" x="${x + barW / 2}" y="${H - MB + 14}" text-anchor="middle">` +
|
||||
`${escapeText(dayLabel(d.date, isToday))}</text>`
|
||||
);
|
||||
}
|
||||
@@ -1040,10 +1047,15 @@
|
||||
{ key: "meals", label: "Meals", cls: "bar-eat" },
|
||||
];
|
||||
|
||||
const selYmd = ymd(selectedDay());
|
||||
days.forEach((d, i) => {
|
||||
const isToday = i === days.length - 1;
|
||||
const isSel = d.ymd === selYmd;
|
||||
const groupX = ML + i * (groupW + groupGap);
|
||||
|
||||
if (isSel) {
|
||||
parts.push(`<rect class="day-highlight" x="${(groupX - groupGap / 2).toFixed(1)}" y="${MT}" width="${(groupW + groupGap).toFixed(1)}" height="${innerH}" rx="3"/>`);
|
||||
}
|
||||
series.forEach((s, j) => {
|
||||
const val = d[s.key];
|
||||
const x = groupX + j * (barW + innerBarGap);
|
||||
@@ -1057,9 +1069,9 @@
|
||||
);
|
||||
});
|
||||
|
||||
if (showDayLabel(i, days.length)) {
|
||||
if (showDayLabel(i, days.length) || isSel) {
|
||||
parts.push(
|
||||
`<text x="${groupX + groupW / 2}" y="${H - MB + 14}" text-anchor="middle">` +
|
||||
`<text class="${isSel ? "day-label-sel" : ""}" x="${groupX + groupW / 2}" y="${H - MB + 14}" text-anchor="middle">` +
|
||||
`${escapeText(dayLabel(d.date, isToday))}</text>`
|
||||
);
|
||||
}
|
||||
@@ -1095,20 +1107,25 @@
|
||||
parts.push(`<text x="${ML - 4}" y="${y + 3}" text-anchor="end">${vText}</text>`);
|
||||
}
|
||||
|
||||
const selYmd = ymd(selectedDay());
|
||||
days.forEach((d, i) => {
|
||||
const isToday = i === days.length - 1;
|
||||
const isSel = d.ymd === selYmd;
|
||||
const x = ML + i * (barW + gap);
|
||||
const h = (d.grams / yMax) * innerH;
|
||||
const y = MT + innerH - h;
|
||||
const title = `${d.date.toLocaleDateString(undefined, { weekday: "long", month: "short", day: "numeric" })} — ${Math.round(d.grams)} g`;
|
||||
if (isSel) {
|
||||
parts.push(`<rect class="day-highlight" x="${(x - gap / 2).toFixed(1)}" y="${MT}" width="${(barW + gap).toFixed(1)}" height="${innerH}" rx="3"/>`);
|
||||
}
|
||||
parts.push(
|
||||
`<rect class="bar bar-eat ${isToday ? "" : "bar-faded"}" data-day="${d.ymd}" ` +
|
||||
`x="${x}" y="${y}" width="${barW}" height="${Math.max(0, h)}" rx="3">` +
|
||||
`<title>${escapeText(title)}</title></rect>`
|
||||
);
|
||||
if (showDayLabel(i, days.length)) {
|
||||
if (showDayLabel(i, days.length) || isSel) {
|
||||
parts.push(
|
||||
`<text x="${x + barW / 2}" y="${H - MB + 14}" text-anchor="middle">` +
|
||||
`<text class="${isSel ? "day-label-sel" : ""}" x="${x + barW / 2}" y="${H - MB + 14}" text-anchor="middle">` +
|
||||
`${escapeText(dayLabel(d.date, isToday))}</text>`
|
||||
);
|
||||
}
|
||||
@@ -1157,15 +1174,17 @@
|
||||
parts.push(`<text x="${x.toFixed(1)}" y="${MT - 5}" text-anchor="${anchor}">${hr}h</text>`);
|
||||
}
|
||||
|
||||
const selYmd = ymd(selectedDay());
|
||||
for (let i = 0; i < N; i++) {
|
||||
const day = new Date(today);
|
||||
day.setDate(day.getDate() - (N - 1 - i)); // oldest at top, today at bottom
|
||||
const dayStart = startOfDay(day).getTime();
|
||||
const dayEnd = dayStart + dayMs;
|
||||
const isToday = ymd(day) === ymd(new Date());
|
||||
const isSel = ymd(day) === selYmd;
|
||||
const y = MT + i * (rowH + rowGap);
|
||||
|
||||
parts.push(`<rect class="stl-track" x="${ML}" y="${y.toFixed(1)}" width="${innerW}" height="${rowH.toFixed(1)}" rx="2"/>`);
|
||||
parts.push(`<rect class="stl-track${isSel ? " stl-selected" : ""}" x="${ML}" y="${y.toFixed(1)}" width="${innerW}" height="${rowH.toFixed(1)}" rx="2"/>`);
|
||||
|
||||
for (const wdw of windows) {
|
||||
const s = Math.max(wdw.start, dayStart);
|
||||
@@ -1177,7 +1196,7 @@
|
||||
}
|
||||
|
||||
const label = isToday ? "Today" : `${day.toLocaleDateString(undefined, { weekday: "short" })} ${day.getDate()}`;
|
||||
parts.push(`<text class="stl-day ${isToday ? "stl-today" : ""}" x="${ML - 6}" y="${(y + rowH / 2 + 3).toFixed(1)}" text-anchor="end">${escapeText(label)}</text>`);
|
||||
parts.push(`<text class="stl-day ${isToday ? "stl-today" : ""}${isSel ? " stl-sel" : ""}" x="${ML - 6}" y="${(y + rowH / 2 + 3).toFixed(1)}" text-anchor="end">${escapeText(label)}</text>`);
|
||||
|
||||
const title = day.toLocaleDateString(undefined, { weekday: "long", month: "short", day: "numeric" });
|
||||
parts.push(`<rect class="bar stl-hit" data-day="${ymd(day)}" x="${ML}" y="${y.toFixed(1)}" width="${innerW}" height="${rowH.toFixed(1)}"><title>${escapeText(title)}</title></rect>`);
|
||||
@@ -1241,6 +1260,128 @@
|
||||
svg.innerHTML = parts.join("");
|
||||
}
|
||||
|
||||
// ---------- sleep trend ----------
|
||||
// Cumulative hours slept as the day progresses: today's running total (up to
|
||||
// now) against yesterday's full curve and the mean of the last N full days,
|
||||
// where N follows the 7/14/30 chart-days picker.
|
||||
// Each curve is a list of { x: hour-of-day 0..24, y: cumulative hours }.
|
||||
function sleepTrendCurves(events) {
|
||||
const HOUR = 3_600_000;
|
||||
const windows = sleepWindows(events); // ongoing sleep already clipped to now
|
||||
const sleptMs = (from, to) => {
|
||||
let total = 0;
|
||||
for (const w of windows) {
|
||||
const s = Math.max(w.start, from);
|
||||
const e = Math.min(w.end, to);
|
||||
if (e > s) total += e - s;
|
||||
}
|
||||
return total;
|
||||
};
|
||||
const dayStartTs = (daysAgo) => {
|
||||
const d = startOfDay(new Date());
|
||||
d.setDate(d.getDate() - daysAgo);
|
||||
return d.getTime();
|
||||
};
|
||||
// capTs (today only) truncates the curve at "now" with a final fractional
|
||||
// point, so the line visibly ends where the day currently stands.
|
||||
const curveFor = (start, capTs) => {
|
||||
const pts = [];
|
||||
for (let h = 0; h <= 24; h++) {
|
||||
const to = start + h * HOUR;
|
||||
if (capTs != null && to >= capTs) {
|
||||
pts.push({ x: (capTs - start) / HOUR, y: sleptMs(start, capTs) / HOUR });
|
||||
break;
|
||||
}
|
||||
pts.push({ x: h, y: sleptMs(start, to) / HOUR });
|
||||
}
|
||||
return pts;
|
||||
};
|
||||
|
||||
const today = curveFor(dayStartTs(0), Date.now());
|
||||
|
||||
const yesterdayCurve = curveFor(dayStartTs(1));
|
||||
const yesterday = yesterdayCurve[24].y > 0 ? yesterdayCurve : null;
|
||||
|
||||
// Mean of the last N full days, skipping days with no sleep at all so a
|
||||
// young log (or a tracking gap) doesn't drag the average toward zero.
|
||||
const avgDays = chartDays();
|
||||
const dayCurves = [];
|
||||
for (let i = 1; i <= avgDays; i++) {
|
||||
const c = curveFor(dayStartTs(i));
|
||||
if (c[24].y > 0) dayCurves.push(c);
|
||||
}
|
||||
let avg = null;
|
||||
if (dayCurves.length > 0) {
|
||||
avg = [];
|
||||
for (let h = 0; h <= 24; h++) {
|
||||
avg.push({ x: h, y: dayCurves.reduce((s, c) => s + c[h].y, 0) / dayCurves.length });
|
||||
}
|
||||
}
|
||||
|
||||
return { today, yesterday, avg, avgDays };
|
||||
}
|
||||
|
||||
function drawSleepTrendChart(curves) {
|
||||
const svg = document.getElementById("chart-sleep-trend");
|
||||
if (!svg) return;
|
||||
const W = 320, H = 160;
|
||||
const ML = 26, MR = 8, MT = 10, MB = 22;
|
||||
const innerW = W - ML - MR;
|
||||
const innerH = H - MT - MB;
|
||||
|
||||
const series = [
|
||||
// Reference lines first so today draws on top of them.
|
||||
{ pts: curves.avg, cls: "trend-avg", label: `${curves.avgDays}-day average` },
|
||||
{ pts: curves.yesterday, cls: "trend-yesterday", label: "Yesterday" },
|
||||
{ pts: curves.today, cls: "trend-today", label: "Today" },
|
||||
].filter(s => s.pts && s.pts.length > 1);
|
||||
|
||||
const rawMax = Math.max(...series.flatMap(s => s.pts.map(p => p.y)));
|
||||
const { yMax, steps: ySteps } = niceAxisSleepHours(rawMax);
|
||||
|
||||
const xOf = (hour) => ML + (hour / 24) * innerW;
|
||||
const yOf = (v) => MT + innerH * (1 - v / yMax);
|
||||
|
||||
const parts = [];
|
||||
for (let i = 0; i <= ySteps; i++) {
|
||||
const y = MT + innerH * (1 - i / ySteps);
|
||||
const v = Math.round(yMax * i / ySteps * 10) / 10;
|
||||
const vText = v % 1 === 0 ? v : v.toFixed(1);
|
||||
parts.push(`<line class="grid" x1="${ML}" y1="${y}" x2="${W - MR}" y2="${y}"/>`);
|
||||
parts.push(`<text x="${ML - 4}" y="${y + 3}" text-anchor="end">${vText}h</text>`);
|
||||
}
|
||||
for (const hr of [0, 6, 12, 18, 24]) {
|
||||
const x = xOf(hr);
|
||||
parts.push(`<line class="grid" x1="${x.toFixed(1)}" y1="${MT}" x2="${x.toFixed(1)}" y2="${MT + innerH}"/>`);
|
||||
const anchor = hr === 0 ? "start" : hr === 24 ? "end" : "middle";
|
||||
parts.push(`<text x="${x.toFixed(1)}" y="${H - MB + 14}" text-anchor="${anchor}">${pad2(hr)}</text>`);
|
||||
}
|
||||
|
||||
for (const s of series) {
|
||||
const d = s.pts
|
||||
.map((p, i) => `${i === 0 ? "M" : "L"}${xOf(p.x).toFixed(1)} ${yOf(p.y).toFixed(1)}`)
|
||||
.join(" ");
|
||||
const last = s.pts[s.pts.length - 1];
|
||||
const title = `${s.label} — ${last.y.toFixed(1)}h slept`;
|
||||
parts.push(`<path class="${s.cls}" d="${d}"><title>${escapeText(title)}</title></path>`);
|
||||
}
|
||||
|
||||
svg.innerHTML = parts.join("");
|
||||
}
|
||||
|
||||
function renderSleepTrend(events) {
|
||||
const curves = sleepTrendCurves(events);
|
||||
drawSleepTrendChart(curves);
|
||||
// Drop legend chips for missing reference lines (no data for them yet),
|
||||
// and keep the average chip's label in step with the chart-days picker.
|
||||
const yLegend = document.getElementById("legend-trend-yesterday");
|
||||
const aLegend = document.getElementById("legend-trend-avg");
|
||||
const aText = document.getElementById("legend-trend-avg-text");
|
||||
if (yLegend) yLegend.hidden = !curves.yesterday;
|
||||
if (aLegend) aLegend.hidden = !curves.avg;
|
||||
if (aText) aText.textContent = `${curves.avgDays}-day avg`;
|
||||
}
|
||||
|
||||
// ---------- weight ----------
|
||||
// Pick a "nice" kg axis that frames the data with a little headroom rather
|
||||
// than forcing 0-based (a puppy going 5→8 kg would otherwise look flat).
|
||||
@@ -1462,6 +1603,13 @@
|
||||
const cellW = innerW / N;
|
||||
|
||||
const parts = [];
|
||||
const selCol = dayIndex.get(ymd(selectedDay()));
|
||||
if (selCol !== undefined) {
|
||||
parts.push(
|
||||
`<rect class="day-highlight" x="${(ML + selCol * cellW - 0.75).toFixed(1)}" y="${MT - 2}" ` +
|
||||
`width="${cellW.toFixed(1)}" height="${(H - MT - MB + 4).toFixed(1)}" rx="3"/>`
|
||||
);
|
||||
}
|
||||
exercises.forEach((x, r) => {
|
||||
const y = MT + r * (rowH + rowGap);
|
||||
const max = Math.max(1, ...counts[r]);
|
||||
@@ -1563,6 +1711,10 @@
|
||||
const isToday = ymd(day) === ymd(new Date());
|
||||
document.getElementById("day-next").disabled = isToday;
|
||||
document.getElementById("day-today").disabled = isToday;
|
||||
// Year-less date on the picker's face — the year is implicit and the
|
||||
// saved width keeps the bar on one row on small phones.
|
||||
document.getElementById("day-date-face").textContent =
|
||||
day.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||
const title = document.getElementById("overview-title");
|
||||
if (title) {
|
||||
title.textContent = isToday
|
||||
@@ -1586,12 +1738,28 @@
|
||||
if (sleepTitle) sleepTitle.textContent = cfg.name ? `When ${cfg.name} sleeps` : "When sleeping";
|
||||
}
|
||||
|
||||
// Dim the quick actions that don't fit the current sleep state — a nudge
|
||||
// toward the likely next tap, never a block: everything stays clickable so
|
||||
// corrections (a missed sleep-end, a mid-nap pee) are always possible.
|
||||
function renderActionHints(events) {
|
||||
const { state } = currentSleepState(events);
|
||||
document.querySelectorAll("button.action").forEach(btn => {
|
||||
const type = btn.dataset.type;
|
||||
const unlikely =
|
||||
state === "asleep" ? type !== "sleep-end"
|
||||
: state === "awake" ? type === "sleep-end"
|
||||
: false; // no sleep history yet — no hints to give
|
||||
btn.classList.toggle("unlikely", unlikely);
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
const events = live();
|
||||
renderHeader();
|
||||
renderDayBar();
|
||||
renderChartWindow();
|
||||
renderBigClock(events);
|
||||
renderActionHints(events);
|
||||
renderStats(events);
|
||||
renderLasts(events);
|
||||
renderTiming(events);
|
||||
@@ -1599,6 +1767,7 @@
|
||||
renderWakeWindows(events);
|
||||
renderWeekly(events);
|
||||
renderSleepTimeline(events);
|
||||
renderSleepTrend(events);
|
||||
renderHourHeatmap(events);
|
||||
renderTraining(events);
|
||||
renderWeight(events);
|
||||
@@ -1970,6 +2139,14 @@
|
||||
}
|
||||
renderEditPhotos();
|
||||
editDialog.showModal();
|
||||
// Without an explicit focus the browser autofocuses the first control —
|
||||
// the date input — and iOS immediately pops the calendar over the form.
|
||||
// Land on the first relevant text field instead (same trick and delay as
|
||||
// openNoteDialog: synchronous focus can be lost while the dialog settles).
|
||||
const focusTarget = !editWeightField.hidden ? editWeight
|
||||
: !editGramsField.hidden ? editGrams
|
||||
: editNote;
|
||||
setTimeout(() => focusTarget.focus(), 50);
|
||||
}
|
||||
|
||||
editPhotoBtn.addEventListener("click", () => editPhotoInput.click());
|
||||
@@ -2355,6 +2532,17 @@
|
||||
dayPicker.value = ymd(new Date());
|
||||
dayPicker.addEventListener("change", render);
|
||||
|
||||
// The face opens the hidden input's native picker (iOS 16+ has showPicker;
|
||||
// focus() is the fallback and is what pops the picker on older iOS anyway).
|
||||
document.getElementById("day-date-face").addEventListener("click", () => {
|
||||
try {
|
||||
if (typeof dayPicker.showPicker === "function") dayPicker.showPicker();
|
||||
else dayPicker.focus();
|
||||
} catch {
|
||||
dayPicker.focus();
|
||||
}
|
||||
});
|
||||
|
||||
function shiftSelectedDay(days) {
|
||||
const d = selectedDay();
|
||||
d.setDate(d.getDate() + days);
|
||||
@@ -2526,6 +2714,7 @@
|
||||
const evs = live();
|
||||
renderHeader();
|
||||
renderBigClock(evs);
|
||||
renderActionHints(evs);
|
||||
renderStats(evs);
|
||||
renderLasts(evs);
|
||||
renderTiming(evs);
|
||||
@@ -2533,6 +2722,7 @@
|
||||
renderWakeWindows(evs);
|
||||
renderWeekly(evs);
|
||||
renderSleepTimeline(evs);
|
||||
renderSleepTrend(evs);
|
||||
renderHourHeatmap(evs);
|
||||
renderTraining(evs);
|
||||
if (navigator.onLine && !syncing) setStatus();
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
[
|
||||
{ "date": "2026-07-15", "text": "The charts highlight the selected day, so it's easy to spot which bars, rows and cells you're looking at" },
|
||||
{ "date": "2026-07-15", "text": "New Sleep trend chart: today's running sleep total through the day, against yesterday and the average over your chart window (7/14/30 days)" },
|
||||
{ "date": "2026-07-15", "text": "Quick actions that don't fit right now are dimmed (asleep → everything but Sleep end; awake → Sleep end) — still tappable for corrections" },
|
||||
{ "date": "2026-07-15", "text": "Editing an event no longer pops the date picker over the whole screen on iPhone" },
|
||||
{ "date": "2026-07-15", "text": "Weight and amount fields no longer show up on events that don't use them (e.g. editing a pee)" },
|
||||
{ "date": "2026-07-15", "text": "The date in the top bar is shorter (no year), so the whole bar fits on smaller iPhones" },
|
||||
{ "date": "2026-07-13", "text": "The big timer is back at the top — scroll past it and it hops into the frozen bar instead" },
|
||||
{ "date": "2026-07-13", "text": "The top bar fits on one row: timer, day arrows, date picker and Today" },
|
||||
{ "date": "2026-07-13", "text": "Pick how many days the charts cover — 7, 14 or 30 (default 7)" },
|
||||
|
||||
+18
-1
@@ -86,7 +86,13 @@
|
||||
<span id="bar-clock-time"></span>
|
||||
</div>
|
||||
<button type="button" id="day-prev" class="ghost" aria-label="Previous day">←</button>
|
||||
<input type="date" id="day-picker" />
|
||||
<!-- A browser won't let us shorten the text a native date input shows,
|
||||
so the face button carries a compact year-less date and the real
|
||||
input stays (visually hidden) as the value + native picker. -->
|
||||
<span class="day-date">
|
||||
<button type="button" id="day-date-face" class="ghost"></button>
|
||||
<input type="date" id="day-picker" tabindex="-1" aria-hidden="true" />
|
||||
</span>
|
||||
<button type="button" id="day-next" class="ghost" aria-label="Next day">→</button>
|
||||
<button type="button" id="day-today" class="ghost">Today</button>
|
||||
</section>
|
||||
@@ -214,6 +220,17 @@
|
||||
<p class="muted-note">Each row is a day, midnight to midnight; shaded = asleep. Tap a row to open that day.</p>
|
||||
</section>
|
||||
|
||||
<section class="patterns" data-panel="sleep-trend">
|
||||
<h2>Sleep trend</h2>
|
||||
<svg id="chart-sleep-trend" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Cumulative sleep hours through the day: today, yesterday and the recent average"></svg>
|
||||
<div class="legend">
|
||||
<span class="lg trend-today"><span class="sw"></span>Today</span>
|
||||
<span class="lg trend-yesterday" id="legend-trend-yesterday"><span class="sw"></span>Yesterday</span>
|
||||
<span class="lg trend-avg" id="legend-trend-avg"><span class="sw"></span><span id="legend-trend-avg-text">7-day avg</span></span>
|
||||
</div>
|
||||
<p class="muted-note">Hours slept so far at each point of the day, against yesterday and the average over the picked chart window.</p>
|
||||
</section>
|
||||
|
||||
<section class="patterns" data-panel="hour-heatmap">
|
||||
<h2>By hour of day <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<svg id="chart-hour-heatmap" class="chart-svg" viewBox="0 0 320 120" role="img" aria-label="Pee, poo and meal frequency by hour of day"></svg>
|
||||
|
||||
+66
-3
@@ -147,10 +147,23 @@ body::before {
|
||||
flex-wrap: nowrap;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.day-bar input[type="date"] {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
/* The face button shows the short date; the real input sits invisibly behind
|
||||
it so its native picker still anchors here (and .showPicker() has a target). */
|
||||
.day-date {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
}
|
||||
.day-date input[type="date"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
#day-date-face {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.day-bar button {
|
||||
padding: 8px 10px;
|
||||
@@ -242,6 +255,9 @@ button.action.eat { background: var(--eat); }
|
||||
button.action.pee { background: var(--pee); color: #2b240a; }
|
||||
button.action.poo { background: var(--poo); }
|
||||
button.action.weight { background: var(--weight); }
|
||||
/* Unlikely given the current sleep state (see renderActionHints) — dimmed
|
||||
but fully tappable, so corrections are never blocked. */
|
||||
button.action.unlikely { opacity: 0.4; }
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
@@ -453,6 +469,9 @@ dialog::backdrop { background: rgba(0,0,0,0.4); }
|
||||
dialog h3 { margin: 0 0 12px; }
|
||||
|
||||
dialog label { display: block; font-size: 0.85rem; color: var(--muted); margin-bottom: 10px; }
|
||||
/* The display rule above beats the UA [hidden] rule, so hide explicitly —
|
||||
otherwise the weight/grams fields show on event types that don't use them. */
|
||||
dialog label[hidden] { display: none; }
|
||||
dialog label input, dialog label textarea { margin-top: 4px; }
|
||||
|
||||
dialog menu {
|
||||
@@ -584,6 +603,16 @@ dialog menu {
|
||||
}
|
||||
.chart-svg .bar { cursor: pointer; }
|
||||
.chart-svg .bar-faded { opacity: 0.7; }
|
||||
/* The selected day's slice in every per-day chart: a soft accent band behind
|
||||
the bars/cells (visible even when the day's values are all zero) plus an
|
||||
accent-colored day label. */
|
||||
.chart-svg .day-highlight {
|
||||
fill: var(--accent);
|
||||
fill-opacity: 0.08;
|
||||
stroke: var(--accent);
|
||||
stroke-opacity: 0.45;
|
||||
}
|
||||
.chart-svg text.day-label-sel { fill: var(--accent); font-weight: 600; }
|
||||
.chart-svg .bar-sleep { fill: var(--sleep); }
|
||||
.chart-svg .bar-pee { fill: var(--pee); }
|
||||
.chart-svg .bar-poo { fill: var(--poo); }
|
||||
@@ -878,9 +907,43 @@ input.switch:checked::after { transform: translateX(18px); }
|
||||
}
|
||||
.chart-svg .stl-sleep { fill: var(--sleep); }
|
||||
.chart-svg .stl-today { fill: var(--accent); font-weight: 600; }
|
||||
/* Selected day: accent ring on the row's track + accent label. */
|
||||
.chart-svg .stl-track.stl-selected {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 1.2;
|
||||
stroke-opacity: 0.8;
|
||||
}
|
||||
.chart-svg .stl-sel { fill: var(--accent); }
|
||||
.chart-svg .stl-hit { fill: transparent; cursor: pointer; }
|
||||
.chart-svg .stl-hit:hover { fill: var(--accent); fill-opacity: 0.08; }
|
||||
|
||||
/* Sleep trend lines: today strongest, the reference curves lighter/dashed. */
|
||||
.chart-svg .trend-today {
|
||||
stroke: var(--sleep);
|
||||
stroke-width: 2.5;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
.chart-svg .trend-yesterday {
|
||||
stroke: var(--muted);
|
||||
stroke-width: 1.5;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.chart-svg .trend-avg {
|
||||
stroke: var(--sleep);
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 4 3;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.lg.trend-today .sw { background: var(--sleep); }
|
||||
.lg.trend-yesterday .sw { background: var(--muted); }
|
||||
.lg.trend-avg .sw { background: color-mix(in srgb, var(--sleep) 55%, var(--surface)); }
|
||||
.lg[hidden] { display: none; }
|
||||
|
||||
.chart-svg .hm-cell { stroke: none; }
|
||||
.chart-svg .hm-pee { fill: var(--pee); }
|
||||
.chart-svg .hm-poo { fill: var(--poo); }
|
||||
|
||||
Reference in New Issue
Block a user