Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d7d9d3a53 | ||
|
|
c7924bb0ae |
@@ -23,3 +23,6 @@ vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
AGENTS.md
|
||||
|
||||
# Local agent/editor config
|
||||
.claude/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',
|
||||
|
||||
@@ -57,8 +57,10 @@
|
||||
// defer to after layout so the measured positions and scroll width are final
|
||||
requestAnimationFrame(() => {
|
||||
// scroll so the first day card sits exactly at the content edge (aligned
|
||||
// with the page hero), leaving the "past days" button fully off to the left
|
||||
el.scrollLeft += wrap.getBoundingClientRect().left - el.getBoundingClientRect().left;
|
||||
// with the page hero), leaving the "past days" button fully off to the
|
||||
// left. The scroll's pl-3 keeps the lifted/scaled card from being clipped,
|
||||
// so subtract it here to land the card on the content edge.
|
||||
el.scrollLeft += wrap.getBoundingClientRect().left - el.getBoundingClientRect().left - 12;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,7 +131,7 @@
|
||||
clipped, while the first card still lines up with the page content edge. -->
|
||||
<div
|
||||
bind:this={scrollEl}
|
||||
class="day-scroll flex min-w-0 flex-1 gap-2 overflow-x-auto pt-3 pb-3 md:pt-5 md:pb-11"
|
||||
class="day-scroll -ml-3 flex min-w-0 flex-1 gap-2 overflow-x-auto pt-3 pb-3 pl-3 md:pt-5 md:pb-11"
|
||||
class:scrolling
|
||||
onscroll={onScroll}
|
||||
>
|
||||
|
||||
@@ -281,32 +281,66 @@
|
||||
: null
|
||||
);
|
||||
|
||||
// ─── Start the scroll at 06:00 on day change (mobile) ───────────────────────
|
||||
// When the table overflows sideways, scroll so the 06:00 column is the leftmost
|
||||
// visible one — the early hours are rarely of interest and this keeps the
|
||||
// daytime in view on every day switch.
|
||||
// ─── Auto-scroll the (overflowing) table on day / interval change ────────────
|
||||
// • Day change → today lands on the current-hour cell, other days on 06:00.
|
||||
// • Interval change (3h ↔ 1h) → keep whatever cell is currently on the left.
|
||||
let tableScrollEl = $state<HTMLDivElement>();
|
||||
let autoScrolledDay = -1;
|
||||
let lastInterval = 0;
|
||||
let viewLeftHour = 6; // zoned hour at the left edge (plain, non-reactive)
|
||||
|
||||
function colWidthPx(): number {
|
||||
return (tableWidth - headerColWidth) / cellData.length;
|
||||
}
|
||||
|
||||
function scrollToIdx(idx: number) {
|
||||
const el = tableScrollEl;
|
||||
if (!el) return;
|
||||
if (el.scrollWidth <= el.clientWidth + 4) {
|
||||
el.scrollLeft = 0;
|
||||
return;
|
||||
}
|
||||
el.scrollLeft = idx <= 0 ? 0 : Math.max(0, idx * colWidthPx());
|
||||
}
|
||||
|
||||
// track the left-most visible cell's hour so an interval switch can restore it
|
||||
function onTableScroll() {
|
||||
const el = tableScrollEl;
|
||||
if (!el || cellData.length === 0 || tableWidth === 0) return;
|
||||
const idx = Math.min(
|
||||
cellData.length - 1,
|
||||
Math.max(0, Math.round(el.scrollLeft / colWidthPx()))
|
||||
);
|
||||
viewLeftHour = getZonedHour(cellData[idx].date, data.timezone);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const day = selectedDay.getTime(); // re-run on day switch
|
||||
const day = selectedDay.getTime();
|
||||
const interval = is3h ? 3 : 1;
|
||||
const el = tableScrollEl;
|
||||
if (!el || cellData.length === 0 || tableWidth === 0 || headerColWidth === 0) return;
|
||||
if (autoScrolledDay === day) return;
|
||||
const dayChanged = autoScrolledDay !== day;
|
||||
const intervalChanged = lastInterval !== interval;
|
||||
if (!dayChanged && !intervalChanged) return;
|
||||
autoScrolledDay = day;
|
||||
requestAnimationFrame(() => {
|
||||
// only when it actually overflows (i.e. mobile / narrow)
|
||||
if (el.scrollWidth <= el.clientWidth + 4) {
|
||||
el.scrollLeft = 0;
|
||||
return;
|
||||
}
|
||||
const sixIdx = cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= 6);
|
||||
if (sixIdx <= 0) {
|
||||
el.scrollLeft = 0;
|
||||
return;
|
||||
}
|
||||
const colWidth = (tableWidth - headerColWidth) / cellData.length;
|
||||
el.scrollLeft = Math.max(0, sixIdx * colWidth);
|
||||
});
|
||||
lastInterval = interval;
|
||||
|
||||
let targetIdx: number;
|
||||
if (dayChanged) {
|
||||
// today → the cell whose block contains "now"; otherwise → 06:00
|
||||
const nowMs = today.getTime();
|
||||
const stepMs = interval * 3600 * 1000;
|
||||
const nowIdx = cellData.findIndex(
|
||||
(c) => nowMs >= c.date.getTime() && nowMs < c.date.getTime() + stepMs
|
||||
);
|
||||
targetIdx =
|
||||
nowIdx >= 0 ? nowIdx : cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= 6);
|
||||
viewLeftHour = targetIdx >= 0 ? getZonedHour(cellData[targetIdx].date, data.timezone) : 6;
|
||||
} else {
|
||||
// interval change only: keep the same cell on the left
|
||||
targetIdx = cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= viewLeftHour);
|
||||
}
|
||||
requestAnimationFrame(() => scrollToIdx(targetIdx));
|
||||
});
|
||||
|
||||
// ─── Chart-hover mirror ─────────────────────────────────────────────────────
|
||||
@@ -415,7 +449,7 @@
|
||||
|
||||
<!-- Below the min-width the table scrolls sideways instead of squeezing;
|
||||
1h needs far more room than 3h (24 vs 8 columns) -->
|
||||
<div class="overflow-x-auto" bind:this={tableScrollEl}>
|
||||
<div class="overflow-x-auto" bind:this={tableScrollEl} onscroll={onTableScroll}>
|
||||
<div
|
||||
class="relative {is3h ? 'min-w-[560px]' : 'min-w-[1100px]'}"
|
||||
bind:clientWidth={tableWidth}
|
||||
|
||||
Reference in New Issue
Block a user