Skip to content

Commit d65bedf

Browse files
bloveclaude
andauthored
fix(website): drawer structure + PretableSurface measurement loop (#63)
Bug 1 — Drawer architecture: DrawerHandle and Drawer were siblings in page.tsx, so when the drawer-wrap was upgraded to fixed/peek-56px, the handle stayed in document flow and the peek was empty. Nest DrawerHandle inside Drawer's drawer-wrap so they share the upgrade and the handle becomes the visible peek. Bug 2 — Render loop: PretableSurface's row-height measurement useLayoutEffect had no deps, so after its own setMeasuredHeights triggered a re-render, the effect re-fired against the new DOM, and under high-churn streaming with wrap:true rows it never reached a fixed point — hitting React's "Maximum update depth exceeded" guard and crashing the page on first render. Gate the effect on [snapshot.visibleRows, columns, viewportWidth] so it only re-measures when something that could legitimately change row metrics has changed. Also: drop unused onTelemetryChange callback from HeroGrid (was a new-each-render arrow that compounded the loop), restore RATE_PER_SEC to 1000 (the marketing wedge claim), restore wrap:true on the message column, and seed 30 initial rows so the grid isn't blank before the first rAF flush. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e0bb4f3 commit d65bedf

6 files changed

Lines changed: 55 additions & 23 deletions

File tree

‎apps/website/app/components/Drawer.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type { ReactNode } from "react";
44

5+
import { DrawerHandle } from "./DrawerHandle";
56
import { useDrawer } from "./useDrawer";
67

78
interface DrawerProps {
@@ -12,6 +13,7 @@ export function Drawer({ children }: DrawerProps) {
1213
const { close } = useDrawer();
1314
return (
1415
<div className="drawer-wrap" data-testid="drawer">
16+
<DrawerHandle />
1517
<div
1618
aria-label="More about pretable"
1719
className="drawer-content overflow-y-auto bg-bg-page"

‎apps/website/app/components/HeroGrid.tsx‎

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
"use client";
22

3-
import {
4-
PretableSurface,
5-
type PretableTelemetry,
6-
} from "@pretable-internal/react-surface";
3+
import { PretableSurface } from "@pretable-internal/react-surface";
74
import { useEffect, useRef, useState } from "react";
85

96
import { type HeroEvent, heroEventLog } from "./heroGrid/eventLog";
@@ -12,6 +9,7 @@ import styles from "./heroGrid/heroGrid.module.css";
129

1310
const RATE_PER_SEC = 1000;
1411
const VISIBLE_BUFFER_ROWS = 200;
12+
const SEED_ROW_COUNT = 30;
1513

1614
const columns = [
1715
{ id: "timestamp", header: "Time", widthPx: 92, pinned: "left" as const },
@@ -32,10 +30,19 @@ interface DisplayRow {
3230
[key: string]: unknown;
3331
}
3432

33+
function buildSeedRows(count: number): DisplayRow[] {
34+
return heroEventLog.slice(0, count).map((entry, index) => ({
35+
...entry,
36+
__sequence: index,
37+
id: `seed-${index}`,
38+
}));
39+
}
40+
3541
export function HeroGrid() {
36-
const [rows, setRows] = useState<DisplayRow[]>([]);
42+
const [rows, setRows] = useState<DisplayRow[]>(() =>
43+
buildSeedRows(SEED_ROW_COUNT),
44+
);
3745
const [paused, setPaused] = useState(false);
38-
const telemetryRef = useRef<PretableTelemetry | null>(null);
3946
const containerRef = useRef<HTMLDivElement>(null);
4047

4148
useEffect(() => {
@@ -49,29 +56,37 @@ export function HeroGrid() {
4956
heroEventLog.slice(0, 50).map((entry, index) => ({
5057
...entry,
5158
__sequence: index,
59+
id: `seed-${index}`,
5260
})),
5361
);
5462
return;
5563
}
5664

65+
let pending: DisplayRow[] = [];
5766
const replay = createHeroReplay({
5867
ratePerSec: RATE_PER_SEC,
5968
onEmit: (event: HeroEvent, sequence: number) => {
60-
setRows((prev) => {
61-
const next = [
62-
{ ...event, __sequence: sequence, id: `seq-${sequence}` },
63-
...prev,
64-
];
65-
return next.length > VISIBLE_BUFFER_ROWS
66-
? next.slice(0, VISIBLE_BUFFER_ROWS)
67-
: next;
69+
pending.push({
70+
...event,
71+
__sequence: sequence,
72+
id: `seq-${sequence}`,
6873
});
6974
},
7075
});
7176

7277
let raf = 0;
7378
const loop = (timestampMs: number) => {
7479
replay.tickAtMs(timestampMs);
80+
if (pending.length > 0) {
81+
const batch = pending;
82+
pending = [];
83+
setRows((prev) => {
84+
const next = [...batch.reverse(), ...prev];
85+
return next.length > VISIBLE_BUFFER_ROWS
86+
? next.slice(0, VISIBLE_BUFFER_ROWS)
87+
: next;
88+
});
89+
}
7590
raf = requestAnimationFrame(loop);
7691
};
7792
raf = requestAnimationFrame(loop);
@@ -107,9 +122,6 @@ export function HeroGrid() {
107122
ariaLabel="Pretable streaming demo"
108123
columns={columns}
109124
getRowId={(row: DisplayRow) => row.id}
110-
onTelemetryChange={(t) => {
111-
telemetryRef.current = t;
112-
}}
113125
rows={rows}
114126
viewportHeight={520}
115127
/>

‎apps/website/app/components/__tests__/Drawer.test.tsx‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
1+
import {
2+
cleanup,
3+
fireEvent,
4+
render,
5+
screen,
6+
within,
7+
} from "@testing-library/react";
28
import { afterEach, describe, expect, it } from "vitest";
39

410
import { Drawer } from "../Drawer";
@@ -23,9 +29,17 @@ describe("Drawer", () => {
2329
expect(screen.getByText("Marketing content")).toBeInTheDocument();
2430
});
2531

26-
it("renders a close button labelled Close", () => {
32+
it("renders an internal close button", () => {
2733
render(<Drawer>x</Drawer>);
28-
expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument();
34+
expect(
35+
screen.getByRole("button", { name: /^close$/i }),
36+
).toBeInTheDocument();
37+
});
38+
39+
it("nests the DrawerHandle inside the drawer-wrap so the peek is the handle", () => {
40+
render(<Drawer>x</Drawer>);
41+
const wrap = screen.getByTestId("drawer");
42+
expect(within(wrap).getByTestId("drawer-handle")).toBeInTheDocument();
2943
});
3044
});
3145

‎apps/website/app/components/heroGrid/heroGrid.module.css‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
gap: 10px;
1616
padding: 10px 24px;
1717
background: rgba(255, 248, 241, 0.75);
18+
-webkit-backdrop-filter: blur(8px);
1819
backdrop-filter: blur(8px);
1920
border-bottom: 1px solid rgba(180, 83, 9, 0.15);
2021
font-family: var(--font-mono, ui-monospace, monospace);

‎apps/website/app/page.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { CodeExample } from "./components/CodeExample";
22
import { ComparisonTable } from "./components/ComparisonTable";
33
import { CtaSection } from "./components/CtaSection";
44
import { Drawer } from "./components/Drawer";
5-
import { DrawerHandle } from "./components/DrawerHandle";
65
import { FeatureGrid } from "./components/FeatureGrid";
76
import { HeroGrid } from "./components/HeroGrid";
87
import { HowItWorks } from "./components/HowItWorks";
@@ -15,7 +14,6 @@ export default function HomePage() {
1514
<>
1615
<main>
1716
<HeroGrid />
18-
<DrawerHandle />
1917
<Drawer>
2018
<ReceiptsBand />
2119
<ScrollReveal>

‎packages/react-surface/src/pretable-surface.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,12 @@ export function PretableSurface<TRow extends PretableRow = PretableRow>({
279279
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: measuring DOM in useLayoutEffect requires synchronous state update
280280
setMeasuredHeights(nextHeights);
281281
}
282-
});
282+
// Deps: only re-run when something that could legitimately change row
283+
// measurements has changed. Without these deps, the effect re-runs on
284+
// every render — including the re-render triggered by its own
285+
// setMeasuredHeights call — which under high-churn streaming with
286+
// wrap:true rows can hit React's "Maximum update depth" guard.
287+
}, [snapshot.visibleRows, columns, viewportWidth]);
283288

284289
return (
285290
<div

0 commit comments

Comments
 (0)