feat(kalender): highlight + center-scroll selected feast in ring panel
CI / update (push) Successful in 3m49s
CI / update (push) Successful in 3m49s
Side list now tints the selected row (theme-aware color-mix on text-primary into surface; gold variant for today), caps at the ring's height via pure CSS (absolute-positioned aside in a relative slot so the ring alone drives row height), and auto-centers the selected item — falling back to the closest-dated feast when the selection is ferial.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.44.2",
|
||||
"version": "1.45.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+111
-2
@@ -199,6 +199,8 @@
|
||||
|
||||
let nextYearHovered = $state(false);
|
||||
let hoveredFeastIso = $state<string | null>(null);
|
||||
let feastListEl = $state<HTMLDivElement | null>(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<HTMLElement>('[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<HTMLElement>('.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 @@
|
||||
</div>
|
||||
|
||||
{#if active}
|
||||
<div class="aside-slot">
|
||||
<aside class="season-panel" style="border-top: 6px solid {litBg(active.color)}">
|
||||
<h3>
|
||||
{active.name}
|
||||
@@ -486,10 +526,16 @@
|
||||
|
||||
{#if activeFeasts.length}
|
||||
<h4 class="section-h">{T.feastsIn}</h4>
|
||||
<div class="feast-list">
|
||||
<div class="feast-list" bind:this={feastListEl}>
|
||||
{#each activeFeasts as f (f.iso + f.name)}
|
||||
{@const isSel = f.iso === selectedIso}
|
||||
{@const isToday = f.iso === todayIso}
|
||||
<a
|
||||
class="feast-item"
|
||||
class:selected={isSel}
|
||||
class:today={isSel && isToday}
|
||||
aria-current={isSel ? 'date' : undefined}
|
||||
data-iso={f.iso}
|
||||
href={dayHref(f.iso)}
|
||||
data-sveltekit-noscroll
|
||||
data-sveltekit-replacestate
|
||||
@@ -503,6 +549,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</aside>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user