From 5475b9a80ef41091e3dc0a5c295724e54c0bf0eb Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Tue, 15 Sep 2026 20:57:26 +0200
Subject: [PATCH 1/5] fix(ui): stabilize timeline layout
---
static/css/timeline.css | 15 +++++++++--
static/js/timeline.js | 51 ++++++++++++++++++++++++++++----------
static/js/timeline_jump.js | 17 ++++++++++++-
3 files changed, 67 insertions(+), 16 deletions(-)
diff --git a/static/css/timeline.css b/static/css/timeline.css
index 9f57006..4f84d72 100644
--- a/static/css/timeline.css
+++ b/static/css/timeline.css
@@ -54,8 +54,9 @@
@media (max-width: 640px) {
.timeline {
+ /* Stack .info above ol; keep nowrap (below) so the axis
s stay on one
+ line — the ol scrolls horizontally at any width, it must never wrap. */
grid-template-columns: 1fr;
- white-space: normal;
}
}
@@ -103,9 +104,18 @@
.timeline ol {
font-size: 0;
- padding: 250px 0;
+ /* Fallback for the brief window before timeline.js measures real card heights
+ (initTimeline() overrides this per instance via inline style — see there for
+ why a fixed value can't fit every card). Covers a typical short card only. */
+ padding: 160px 0;
transition: all 1s;
overflow-x: scroll;
+ /* Cards are absolutely positioned above/below the axis (see nth-child rules
+ below), so setting overflow-x alone would leave overflow-y at its default
+ "auto", clipping any card the padding above doesn't cover with an
+ unstyled vertical scrollbar. initTimeline() sizes the padding to fit, so
+ vertical scrolling is never the intended way to reach a card. */
+ overflow-y: hidden;
scroll-snap-type: x mandatory;
scrollbar-color: var(--timeline-yellow) var(--timeline-midnight-green);
}
@@ -146,6 +156,7 @@
position: absolute;
left: calc(100% + 7px);
width: 280px;
+ max-width: min(280px, calc(100vw - 2.5rem));
padding: 15px;
font-size: 1rem;
white-space: normal;
diff --git a/static/js/timeline.js b/static/js/timeline.js
index ea10610..50ccc32 100644
--- a/static/js/timeline.js
+++ b/static/js/timeline.js
@@ -1,24 +1,49 @@
-// Timeline layout: equalise heights of list-item divs so the connector line aligns.
+// Timeline layout: equalise heights of list-item divs so the connector line aligns,
+// and size the axis's vertical padding to match. Each .timeline instance is measured
+// independently so unrelated timelines never force each other's card/axis size
+// (e.g. the Notes tab renders a history timeline and a biometrics timeline together).
// initTimeline() is called on window load and after htmx swaps.
function initTimeline() {
- const elements = document.querySelectorAll(".timeline li > div");
- if (elements.length > 0) {
- setEqualHeights(elements);
- }
+ document.querySelectorAll(".timeline").forEach(function (timeline) {
+ const ol = timeline.querySelector("ol");
+ const cards = timeline.querySelectorAll("li > div");
+ if (!ol || cards.length === 0) {
+ return;
+ }
+ const maxHeight = setEqualHeights(cards);
+ setAxisPadding(ol, maxHeight);
+ });
}
-function setEqualHeights(el) {
- let counter = 0;
- for (let i = 0; i < el.length; i++) {
- const singleHeight = el[i].offsetHeight;
- if (counter < singleHeight) {
- counter = singleHeight;
+function setEqualHeights(elements) {
+ // Clear any height a previous run set, otherwise a card can never shrink
+ // back down after "Load older" or an htmx swap removes its taller siblings.
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].style.height = "";
+ }
+ let maxHeight = 0;
+ for (let i = 0; i < elements.length; i++) {
+ const singleHeight = elements[i].offsetHeight;
+ if (maxHeight < singleHeight) {
+ maxHeight = singleHeight;
}
}
- for (let i = 0; i < el.length; i++) {
- el[i].style.height = counter + "px";
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].style.height = maxHeight + "px";
}
+ return maxHeight;
+}
+
+// Cards sit 16px above (odd) or below (even) the axis line via absolute
+// positioning (see .timeline ol li:nth-child(odd/even) div in timeline.css), so
+// the ol needs at least maxHeight + 16px of padding on each side, plus a little
+// breathing room, to avoid clipping the tallest card. A single fixed padding
+// can't fit every timeline's content, so it's computed per instance here.
+function setAxisPadding(ol, maxHeight) {
+ const padding = maxHeight + 32;
+ ol.style.paddingTop = padding + "px";
+ ol.style.paddingBottom = padding + "px";
}
window.addEventListener("load", initTimeline);
diff --git a/static/js/timeline_jump.js b/static/js/timeline_jump.js
index fd82568..13a0c0a 100644
--- a/static/js/timeline_jump.js
+++ b/static/js/timeline_jump.js
@@ -19,10 +19,25 @@ function initTimelineJump() {
var nodes = document.querySelectorAll("[id$='-" + month + "']");
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].id.indexOf("tlmonth-") === 0) {
- nodes[i].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
+ scrollAxisToNode(nodes[i]);
return;
}
}
}
+// Scroll the axis's own horizontal scroll container (the ) so the target month
+// comes into view. scrollIntoView() would also drag the whole page vertically to
+// satisfy the node's block-axis visibility, which is not wanted for a horizontal axis.
+function scrollAxisToNode(node) {
+ var container = node.closest("ol");
+ if (!container) {
+ node.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
+ return;
+ }
+ var containerRect = container.getBoundingClientRect();
+ var nodeRect = node.getBoundingClientRect();
+ var target = container.scrollLeft + (nodeRect.left - containerRect.left);
+ container.scrollTo({ left: target, behavior: "smooth" });
+}
+
window.addEventListener("load", initTimelineJump);
From 1fb083ee0d5fef6fa1d7018cf5114b1d38784a09 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Tue, 15 Sep 2026 22:56:21 +0200
Subject: [PATCH 2/5] refactor(ui): consolidate timeline markup
---
.../animals/templates/animals/tabs/_diet.html | 2 +-
.../templates/animals/tabs/_medications.html | 2 +-
.../templates/animals/tabs/_notes.html | 51 ++-----------------
.../animals/templates/animals/tabs/_vet.html | 40 +--------------
.../tabs/partials/_timeline_month_jump.html | 16 ++++++
static/css/timeline.css | 4 ++
6 files changed, 27 insertions(+), 88 deletions(-)
create mode 100644 src/ahc/apps/animals/templates/animals/tabs/partials/_timeline_month_jump.html
diff --git a/src/ahc/apps/animals/templates/animals/tabs/_diet.html b/src/ahc/apps/animals/templates/animals/tabs/_diet.html
index 80864a3..ded2383 100644
--- a/src/ahc/apps/animals/templates/animals/tabs/_diet.html
+++ b/src/ahc/apps/animals/templates/animals/tabs/_diet.html
@@ -29,7 +29,7 @@