fix(timeline): ensure cards always alternate sides across month boundaries
Per-month <ol> containers caused :nth-of-type to reset at each month, putting the first card of consecutive months on the same side. Flatten all items into a single container and use :nth-child(odd of .timeline-item) for continuous left/right alternation across months and infinite scroll.
This commit is contained in:
parent
80849f2bf5
commit
4ed1544d66
3 changed files with 149 additions and 147 deletions
|
|
@ -7,11 +7,9 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<ol class="timeline-list relative">
|
</div>
|
||||||
{# Central timeline line #}
|
{% for event in month.events %}
|
||||||
<div class="timeline-line absolute top-0 bottom-0 w-0.5 bg-amber-200" aria-hidden="true"></div>
|
<div class="timeline-item relative mb-8" data-timeline-event>
|
||||||
{% for event in month.events %}
|
|
||||||
<li class="timeline-item relative mb-8 last:mb-0" data-timeline-event>
|
|
||||||
{# Timeline node/bullet #}
|
{# Timeline node/bullet #}
|
||||||
<span
|
<span
|
||||||
class="timeline-node absolute h-5 w-5 rounded-full border-4 border-amber-50 bg-amber-600"
|
class="timeline-node absolute h-5 w-5 rounded-full border-4 border-amber-50 bg-amber-600"
|
||||||
|
|
@ -109,7 +107,5 @@
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %} {% endif %}
|
{% endif %} {% endif %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -118,24 +118,24 @@ a {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Left side items (odd) - uses nth-of-type to skip the timeline-line div */
|
/* Left side items (odd) - counts only .timeline-item children, skipping month headings */
|
||||||
.timeline-item:nth-of-type(odd) {
|
.timeline-item:nth-child(odd of .timeline-item) {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item:nth-of-type(odd) .timeline-node {
|
.timeline-item:nth-child(odd of .timeline-item) .timeline-node {
|
||||||
right: -10px;
|
right: -10px;
|
||||||
left: auto;
|
left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right side items (even) */
|
/* Right side items (even) */
|
||||||
.timeline-item:nth-of-type(even) {
|
.timeline-item:nth-child(even of .timeline-item) {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item:nth-of-type(even) .timeline-node {
|
.timeline-item:nth-child(even of .timeline-item) .timeline-node {
|
||||||
left: -10px;
|
left: -10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<section
|
<section
|
||||||
class="space-y-12"
|
|
||||||
id="timeline-events"
|
id="timeline-events"
|
||||||
data-role="timeline-months"
|
|
||||||
data-empty="{{ months.is_empty() }}"
|
data-empty="{{ months.is_empty() }}"
|
||||||
>
|
>
|
||||||
{% if months.is_empty() %}
|
{% if months.is_empty() %}
|
||||||
|
|
@ -20,7 +18,12 @@
|
||||||
>
|
>
|
||||||
No events yet.
|
No events yet.
|
||||||
</p>
|
</p>
|
||||||
{% else %} {% for month in months %} {% include "partials/timeline_month.html" %} {% endfor %}
|
{% else %}
|
||||||
|
<div class="timeline-list relative" id="timeline-items">
|
||||||
|
{# Single central timeline line for all months #}
|
||||||
|
<div class="timeline-line absolute top-0 bottom-0 w-0.5 bg-amber-200" aria-hidden="true"></div>
|
||||||
|
{% for month in months %} {% include "partials/timeline_month.html" %} {% endfor %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -49,15 +52,16 @@
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const loader = document.getElementById("timeline-loader")
|
const loader = document.getElementById("timeline-loader")
|
||||||
const monthsContainer = document.getElementById("timeline-events")
|
const itemsContainer = document.getElementById("timeline-items")
|
||||||
const loadMoreButton = document.getElementById("timeline-load-more")
|
const loadMoreButton = document.getElementById("timeline-load-more")
|
||||||
const statusLine = document.getElementById("timeline-status")
|
const statusLine = document.getElementById("timeline-status")
|
||||||
const endLine = document.getElementById("timeline-end")
|
const endLine = document.getElementById("timeline-end")
|
||||||
const errorLine = document.getElementById("timeline-error")
|
const errorLine = document.getElementById("timeline-error")
|
||||||
const sentinel = document.getElementById("timeline-sentinel")
|
const sentinel = document.getElementById("timeline-sentinel")
|
||||||
const emptyState = document.querySelector('[data-role="timeline-empty-state"]')
|
const emptyState = document.querySelector('[data-role="timeline-empty-state"]')
|
||||||
|
const section = document.getElementById("timeline-events")
|
||||||
|
|
||||||
if (loader && monthsContainer && sentinel) {
|
if (loader && sentinel) {
|
||||||
let nextUrl = loader.dataset.nextUrl || ""
|
let nextUrl = loader.dataset.nextUrl || ""
|
||||||
let hasMorePages = loader.dataset.hasMore === "true"
|
let hasMorePages = loader.dataset.hasMore === "true"
|
||||||
let loading = false
|
let loading = false
|
||||||
|
|
@ -84,29 +88,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ensureItemsContainer = () => {
|
||||||
|
if (itemsContainer) return itemsContainer
|
||||||
|
// First load on a previously empty timeline: create the container
|
||||||
|
const container = document.createElement("div")
|
||||||
|
container.className = "timeline-list relative"
|
||||||
|
container.id = "timeline-items"
|
||||||
|
const line = document.createElement("div")
|
||||||
|
line.className = "timeline-line absolute top-0 bottom-0 w-0.5 bg-amber-200"
|
||||||
|
line.setAttribute("aria-hidden", "true")
|
||||||
|
container.appendChild(line)
|
||||||
|
section.insertBefore(container, loader)
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
const appendMonths = (chunk) => {
|
const appendMonths = (chunk) => {
|
||||||
const monthsFragment = chunk.querySelector("[data-chunk-months]")
|
const monthsFragment = chunk.querySelector("[data-chunk-months]")
|
||||||
if (!monthsFragment) {
|
if (!monthsFragment) return
|
||||||
return
|
|
||||||
}
|
const container = ensureItemsContainer()
|
||||||
for (const monthNode of Array.from(monthsFragment.children)) {
|
|
||||||
const anchor = monthNode.id
|
for (const node of Array.from(monthsFragment.children)) {
|
||||||
if (!anchor) {
|
// Skip duplicate month headings (same month spanning a page boundary)
|
||||||
|
if (node.hasAttribute("data-timeline-month") && document.getElementById(node.id)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const existing = document.getElementById(anchor)
|
container.appendChild(node)
|
||||||
if (existing) {
|
|
||||||
// Merge new events into existing month
|
|
||||||
const existingList = existing.querySelector("ol")
|
|
||||||
const newList = monthNode.querySelector("ol")
|
|
||||||
if (existingList && newList) {
|
|
||||||
// CSS :nth-of-type handles alternating pattern automatically
|
|
||||||
const newItems = Array.from(newList.querySelectorAll(".timeline-item"))
|
|
||||||
existingList.append(...newItems)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
monthsContainer.insertBefore(monthNode, loader)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,6 +195,8 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use MutationObserver to watch for new month headings being added
|
// Use MutationObserver to watch for new month headings being added
|
||||||
|
const timelineItems = document.getElementById("timeline-items")
|
||||||
|
if (timelineItems) {
|
||||||
const mutationObserver = new MutationObserver((mutations) => {
|
const mutationObserver = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
mutation.addedNodes.forEach((node) => {
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
|
@ -200,10 +209,7 @@
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
mutationObserver.observe(timelineItems, { childList: true })
|
||||||
const timelineContainer = document.getElementById("timeline-events")
|
|
||||||
if (timelineContainer) {
|
|
||||||
mutationObserver.observe(timelineContainer, { childList: true })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe existing headings
|
// Observe existing headings
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue