diff --git a/package.json b/package.json index e25a72e6..562eb2b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.44.2", + "version": "1.45.0", "private": true, "type": "module", "scripts": { diff --git a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/RingView.svelte b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/RingView.svelte index 835c06e6..da1ac5ac 100644 --- a/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/RingView.svelte +++ b/src/routes/[faithLang=faithLang]/[calendar=calendarLang]/[rite=calendarRite]/[[yyyy=calendarYear]]/[[mm=calendarMonth]]/[[dd=calendarDay]]/RingView.svelte @@ -199,6 +199,8 @@ let nextYearHovered = $state(false); let hoveredFeastIso = $state(null); + let feastListEl = $state(null); + let didInitialScroll = false; const hoveredFeast = $derived( hoveredFeastIso ? feastDots.find((f) => f.iso === hoveredFeastIso) ?? null : null ); @@ -211,6 +213,43 @@ ); }); + $effect(() => { + activeFeasts; + selectedIso; + const list = feastListEl; + if (!list || list.clientHeight === 0) return; + let el = list.querySelector('[aria-current="date"]'); + if (!el && selectedIso) { + // Selected day isn't a listed feast (e.g. ferial) — center the + // closest feast by date so the user still lands near "today". + const items = list.querySelectorAll('.feast-item[data-iso]'); + let best: HTMLElement | null = null; + let bestDelta = Infinity; + const selTime = Date.parse(selectedIso); + for (const item of items) { + const iso = item.dataset.iso; + if (!iso) continue; + const delta = Math.abs(Date.parse(iso) - selTime); + if (delta < bestDelta) { + bestDelta = delta; + best = item; + } + } + el = best; + } + if (!el) return; + const listRect = list.getBoundingClientRect(); + const elRect = el.getBoundingClientRect(); + const relTop = elRect.top - listRect.top + list.scrollTop; + const target = relTop - (list.clientHeight - elRect.height) / 2; + const max = list.scrollHeight - list.clientHeight; + list.scrollTo({ + top: Math.max(0, Math.min(max, target)), + behavior: didInitialScroll && !prefersReducedMotion.current ? 'smooth' : 'auto' + }); + didInitialScroll = true; + }); + function fmtShort(iso: string): string { const [y, m, d] = iso.split('-').map(Number); return new Date(y, m - 1, d).toLocaleDateString( @@ -468,6 +507,7 @@ {#if active} +
+
{/if} @@ -513,10 +560,23 @@ gap: 32px; align-items: start; } + /* Ring column's intrinsic height drives the row height. The aside is + positioned absolutely inside `.aside-slot`, so it contributes nothing to + row sizing — the slot stretches to the ring's height, and the aside then + fills the slot. All pure CSS, no ResizeObserver. */ + .aside-slot { + position: relative; + align-self: stretch; + min-width: 0; + } @media (max-width: 900px) { .ring-wrap { grid-template-columns: 1fr; } + .aside-slot { + position: static; + align-self: auto; + } } .ring-svg-wrap { min-width: 0; @@ -629,6 +689,16 @@ border-radius: var(--radius-card); padding: 22px; box-shadow: var(--shadow-sm); + display: flex; + flex-direction: column; + min-height: 0; + } + @media (min-width: 901px) { + .season-panel { + position: absolute; + inset: 0; + overflow: hidden; + } } .season-panel h3 { margin: 0 0 8px; @@ -660,9 +730,33 @@ font-weight: 700; } .feast-list { + position: relative; display: flex; flex-direction: column; gap: 2px; + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: var(--color-border) transparent; + padding-right: 4px; + } + .feast-list::-webkit-scrollbar { + width: 6px; + } + .feast-list::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 100px; + } + .feast-list::-webkit-scrollbar-track { + background: transparent; + } + @media (max-width: 900px) { + .feast-list { + flex: 0 1 auto; + max-height: 300px; + } } .feast-item { display: grid; @@ -673,12 +767,27 @@ border-radius: var(--radius-md); text-decoration: none; color: inherit; - transition: background var(--transition-fast); + transition: background var(--transition-fast), box-shadow var(--transition-fast); font-size: 0.9rem; } .feast-item:hover { background: var(--color-surface-hover); } + .feast-item.selected { + /* Mix text color into surface: darkens in light mode, lightens in dark. */ + background: color-mix(in srgb, var(--color-text-primary) 16%, var(--color-surface)); + } + .feast-item.selected .n { + font-weight: 700; + color: var(--color-text-primary); + } + .feast-item.selected .d, + .feast-item.selected .r { + color: var(--color-text-primary); + } + .feast-item.selected.today { + background: color-mix(in srgb, var(--lit-gold) 38%, var(--color-surface)); + } .feast-item .d { color: var(--color-text-tertiary); font-variant-numeric: tabular-nums;