diff --git a/components/user/brain/UserPageBrainSidebarWaveItem.tsx b/components/user/brain/UserPageBrainSidebarWaveItem.tsx
index 8d914ce4e9..1a31e96018 100644
--- a/components/user/brain/UserPageBrainSidebarWaveItem.tsx
+++ b/components/user/brain/UserPageBrainSidebarWaveItem.tsx
@@ -71,19 +71,18 @@ export default function UserPageBrainSidebarWaveItem({
prefetch={false}
className="tw-group tw-flex tw-cursor-pointer tw-items-center tw-gap-3 tw-rounded-xl tw-border tw-border-solid tw-border-white/10 tw-bg-iron-950/80 tw-p-3 tw-no-underline tw-shadow-2xl tw-transition-all desktop-hover:hover:tw-border-white/15"
>
-
-
+
{imageSrc ? (
) : (
-
+
)}
diff --git a/components/user/brain/userPageBrainActivityHeatmap.helpers.ts b/components/user/brain/userPageBrainActivityHeatmap.helpers.ts
index 979c3ece3c..cc9a5799a0 100644
--- a/components/user/brain/userPageBrainActivityHeatmap.helpers.ts
+++ b/components/user/brain/userPageBrainActivityHeatmap.helpers.ts
@@ -46,8 +46,7 @@ export const LOADING_MONTH_HEADER_SEGMENTS = [
{ key: "feb", labelColumn: 44, widthPx: 18 },
{ key: "mar", labelColumn: 48, widthPx: 20 },
] as const;
-export const MONTH_LABEL_MIN_SPACING_PX = 34;
-const MONTH_LABEL_OVERFLOW_TOLERANCE_PX = 18;
+const MONTH_LABEL_MIN_SPACING_PX = 34;
export const TOOLTIP_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
@@ -62,8 +61,8 @@ export const TOOLTIP_STYLE = {
pointerEvents: "none",
} as const;
export const HEATMAP_VIEWPORT_CLASS_NAME =
- "tw-[scrollbar-gutter:stable] tw-flex-1 tw-overflow-x-auto tw-overflow-y-hidden tw-pb-3 tw-scrollbar-thin tw-scrollbar-track-transparent tw-scrollbar-thumb-iron-700/60 desktop-hover:hover:tw-scrollbar-thumb-iron-600/80";
-export const HEATMAP_CONTENT_CLASS_NAME = "tw-inline-flex tw-pr-2";
+ "horizontal-menu-scrollable-x tw-[scrollbar-gutter:stable] tw-flex-1 tw-overflow-x-auto tw-overflow-y-hidden tw-overscroll-x-contain tw-pb-3 tw-scrollbar-thin tw-scrollbar-track-transparent tw-scrollbar-thumb-iron-700/60 [touch-action:pan-x] desktop-hover:hover:tw-scrollbar-thumb-iron-600/80";
+export const HEATMAP_CONTENT_CLASS_NAME = "tw-inline-flex tw-flex-col tw-pr-2";
export const HEATMAP_GRID_STYLE = {
gridTemplateRows: `repeat(7, ${CELL_SIZE_PX}px)`,
gridAutoColumns: `${CELL_SIZE_PX}px`,
@@ -74,6 +73,18 @@ export const CELL_FRAME_STYLE = {
height: `${CELL_SIZE_PX}px`,
} as const;
+export function getHeatmapColumnLeftPx(column: number): number {
+ return column * COLUMN_STRIDE_PX;
+}
+
+export function getHeatmapGridWidthPx(weekCount: number): number {
+ if (weekCount <= 0) {
+ return 0;
+ }
+
+ return weekCount * COLUMN_STRIDE_PX - CELL_GAP_PX;
+}
+
// Cell presentation
const CELL_NODE_CLASS_NAMES: Readonly<
Record<
@@ -183,7 +194,7 @@ export function getCellFrameClassName(cell: UserPageBrainActivityCell): string {
}
return clsx(
- "tw-flex tw-items-center tw-justify-center tw-rounded tw-transition-colors tw-duration-200",
+ "tw-flex tw-items-center tw-justify-center tw-rounded desktop-hover:tw-transition-colors desktop-hover:tw-duration-200",
cell.ariaLabel &&
"tw-cursor-pointer desktop-hover:hover:tw-z-20 desktop-hover:hover:tw-bg-[#171b21]"
);
@@ -195,36 +206,21 @@ export function getCellNodeClassName(
): string {
return clsx(
variant === "grid" &&
- "tw-transform-gpu tw-transition-all tw-duration-200 tw-ease-out",
+ "desktop-hover:tw-transform-gpu desktop-hover:tw-transition-all desktop-hover:tw-duration-200 desktop-hover:tw-ease-out",
CELL_NODE_CLASS_NAMES[variant][cell.intensity]
);
}
// Month label placement
export function getPlacedMonthLabels(
- monthLabels: readonly HeatmapMonthLabel[],
- scrollLeft: number,
- clientWidth: number
+ monthLabels: readonly HeatmapMonthLabel[]
): readonly PlacedMonthLabel[] {
const placedMonthLabels: PlacedMonthLabel[] = [];
let nextMinimumLeftPx = 0;
for (const label of monthLabels) {
- const naturalLeftPx = label.labelColumn * COLUMN_STRIDE_PX - scrollLeft;
+ const leftPx = getHeatmapColumnLeftPx(label.firstVisibleColumn);
- if (
- clientWidth > 0 &&
- (naturalLeftPx < -MONTH_LABEL_OVERFLOW_TOLERANCE_PX ||
- naturalLeftPx > clientWidth)
- ) {
- continue;
- }
-
- const leftPx = Math.max(-MONTH_LABEL_OVERFLOW_TOLERANCE_PX, naturalLeftPx);
-
- if (clientWidth > 0 && leftPx >= clientWidth) {
- continue;
- }
if (leftPx < nextMinimumLeftPx) {
continue;
}
diff --git a/components/user/brain/userPageBrainActivityHeatmap.viewport.ts b/components/user/brain/userPageBrainActivityHeatmap.viewport.ts
index 3af3a60f79..b3eade6abc 100644
--- a/components/user/brain/userPageBrainActivityHeatmap.viewport.ts
+++ b/components/user/brain/userPageBrainActivityHeatmap.viewport.ts
@@ -1,25 +1,4 @@
-import {
- useCallback,
- useLayoutEffect,
- useRef,
- useSyncExternalStore,
-} from "react";
-
-type ViewportMetrics = Readonly<{
- scrollLeft: number;
- clientWidth: number;
- scrollWidth: number;
-}>;
-
-type MutableValueRef
= {
- current: T;
-};
-
-const EMPTY_VIEWPORT_METRICS: ViewportMetrics = {
- scrollLeft: 0,
- clientWidth: 0,
- scrollWidth: 0,
-};
+import { useCallback, useLayoutEffect, useRef } from "react";
function snapViewportToLatest(viewport: HTMLDivElement | null) {
if (!viewport) {
@@ -34,102 +13,16 @@ function snapViewportToLatest(viewport: HTMLDivElement | null) {
viewport.scrollLeft = maxScrollLeft;
}
-function readViewportMetrics(
- viewport: HTMLDivElement | null,
- cachedMetricsRef: MutableValueRef
-): ViewportMetrics {
- if (!viewport) {
- return EMPTY_VIEWPORT_METRICS;
- }
-
- const nextMetrics = {
- scrollLeft: viewport.scrollLeft,
- clientWidth: viewport.clientWidth,
- scrollWidth: viewport.scrollWidth,
- } as const;
- const previousMetrics = cachedMetricsRef.current;
-
- if (
- previousMetrics.scrollLeft === nextMetrics.scrollLeft &&
- previousMetrics.clientWidth === nextMetrics.clientWidth &&
- previousMetrics.scrollWidth === nextMetrics.scrollWidth
- ) {
- return previousMetrics;
- }
-
- cachedMetricsRef.current = nextMetrics;
- return nextMetrics;
-}
-
-function subscribeToViewportMetrics(
- viewport: HTMLDivElement | null,
- onStoreChange: () => void
-) {
- if (!viewport) {
- return () => {};
- }
-
- const content = viewport.firstElementChild;
- let frameId = 0;
-
- const notify = () => {
- cancelAnimationFrame(frameId);
- frameId = requestAnimationFrame(onStoreChange);
- };
-
- notify();
- viewport.addEventListener("scroll", notify, { passive: true });
-
- if (typeof ResizeObserver === "undefined") {
- window.addEventListener("resize", notify);
-
- return () => {
- cancelAnimationFrame(frameId);
- viewport.removeEventListener("scroll", notify);
- window.removeEventListener("resize", notify);
- };
- }
-
- const resizeObserver = new ResizeObserver(notify);
- resizeObserver.observe(viewport);
- if (content) {
- resizeObserver.observe(content);
- }
-
- return () => {
- cancelAnimationFrame(frameId);
- viewport.removeEventListener("scroll", notify);
- resizeObserver.disconnect();
- };
-}
-
export function useHeatmapViewport(resetKey?: string) {
const viewportRef = useRef(null);
- const cachedMetricsRef = useRef(EMPTY_VIEWPORT_METRICS);
const setViewportRef = useCallback((node: HTMLDivElement | null) => {
viewportRef.current = node;
- cachedMetricsRef.current = EMPTY_VIEWPORT_METRICS;
}, []);
useLayoutEffect(() => {
- cachedMetricsRef.current = EMPTY_VIEWPORT_METRICS;
snapViewportToLatest(viewportRef.current);
}, [resetKey]);
- const getViewportSnapshot = useCallback(() => {
- return readViewportMetrics(viewportRef.current, cachedMetricsRef);
- }, []);
-
- const subscribeToViewport = useCallback((onStoreChange: () => void) => {
- return subscribeToViewportMetrics(viewportRef.current, onStoreChange);
- }, []);
-
- const viewportMetrics = useSyncExternalStore(
- subscribeToViewport,
- getViewportSnapshot,
- () => EMPTY_VIEWPORT_METRICS
- );
-
- return { viewportRef: setViewportRef, viewportMetrics };
+ return { viewportRef: setViewportRef };
}