Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d7d9d3a53 | ||
|
|
c7924bb0ae |
@@ -23,3 +23,6 @@ vite.config.js.timestamp-*
|
|||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
|
||||||
|
# Local agent/editor config
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
export const buttonVariants = tv({
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',
|
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
|
// defer to after layout so the measured positions and scroll width are final
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// scroll so the first day card sits exactly at the content edge (aligned
|
// 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
|
// with the page hero), leaving the "past days" button fully off to the
|
||||||
el.scrollLeft += wrap.getBoundingClientRect().left - el.getBoundingClientRect().left;
|
// 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. -->
|
clipped, while the first card still lines up with the page content edge. -->
|
||||||
<div
|
<div
|
||||||
bind:this={scrollEl}
|
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
|
class:scrolling
|
||||||
onscroll={onScroll}
|
onscroll={onScroll}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -281,32 +281,66 @@
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─── Start the scroll at 06:00 on day change (mobile) ───────────────────────
|
// ─── Auto-scroll the (overflowing) table on day / interval change ────────────
|
||||||
// When the table overflows sideways, scroll so the 06:00 column is the leftmost
|
// • Day change → today lands on the current-hour cell, other days on 06:00.
|
||||||
// visible one — the early hours are rarely of interest and this keeps the
|
// • Interval change (3h ↔ 1h) → keep whatever cell is currently on the left.
|
||||||
// daytime in view on every day switch.
|
|
||||||
let tableScrollEl = $state<HTMLDivElement>();
|
let tableScrollEl = $state<HTMLDivElement>();
|
||||||
let autoScrolledDay = -1;
|
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(() => {
|
$effect(() => {
|
||||||
const day = selectedDay.getTime(); // re-run on day switch
|
const day = selectedDay.getTime();
|
||||||
|
const interval = is3h ? 3 : 1;
|
||||||
const el = tableScrollEl;
|
const el = tableScrollEl;
|
||||||
if (!el || cellData.length === 0 || tableWidth === 0 || headerColWidth === 0) return;
|
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;
|
autoScrolledDay = day;
|
||||||
requestAnimationFrame(() => {
|
lastInterval = interval;
|
||||||
// only when it actually overflows (i.e. mobile / narrow)
|
|
||||||
if (el.scrollWidth <= el.clientWidth + 4) {
|
let targetIdx: number;
|
||||||
el.scrollLeft = 0;
|
if (dayChanged) {
|
||||||
return;
|
// today → the cell whose block contains "now"; otherwise → 06:00
|
||||||
}
|
const nowMs = today.getTime();
|
||||||
const sixIdx = cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= 6);
|
const stepMs = interval * 3600 * 1000;
|
||||||
if (sixIdx <= 0) {
|
const nowIdx = cellData.findIndex(
|
||||||
el.scrollLeft = 0;
|
(c) => nowMs >= c.date.getTime() && nowMs < c.date.getTime() + stepMs
|
||||||
return;
|
);
|
||||||
}
|
targetIdx =
|
||||||
const colWidth = (tableWidth - headerColWidth) / cellData.length;
|
nowIdx >= 0 ? nowIdx : cellData.findIndex((c) => getZonedHour(c.date, data.timezone) >= 6);
|
||||||
el.scrollLeft = Math.max(0, sixIdx * colWidth);
|
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 ─────────────────────────────────────────────────────
|
// ─── Chart-hover mirror ─────────────────────────────────────────────────────
|
||||||
@@ -415,7 +449,7 @@
|
|||||||
|
|
||||||
<!-- Below the min-width the table scrolls sideways instead of squeezing;
|
<!-- Below the min-width the table scrolls sideways instead of squeezing;
|
||||||
1h needs far more room than 3h (24 vs 8 columns) -->
|
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
|
<div
|
||||||
class="relative {is3h ? 'min-w-[560px]' : 'min-w-[1100px]'}"
|
class="relative {is3h ? 'min-w-[560px]' : 'min-w-[1100px]'}"
|
||||||
bind:clientWidth={tableWidth}
|
bind:clientWidth={tableWidth}
|
||||||
|
|||||||
Reference in New Issue
Block a user