authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-10-08 19:22:52-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 00:22:52+00:00
log29980a31409234b676f97bd477c4e46fdb61a929
tree32ab45fbea8ced3d6b7fab4fe39c1473a207566e
parentdbd83435347d13d5f456271f4f1f5efe294a111d
signature Signed by PGP key B5690EEEBB952194

fix(web): add room for thread timeline markers (#17372)


5 files changed, 46 insertions(+), 28 deletions(-)

apps/web/src/components/chat/ChatCanvas.tsx+2-2
...@@ -41,7 +41,7 @@ export function ChatCanvas({...@@ -41,7 +41,7 @@ export function ChatCanvas({
41 const [measurements, setMeasurements] = useState({41 const [measurements, setMeasurements] = useState({
42 width: 0,42 width: 0,
43 height: 0,43 height: 0,
44 padding: 20,44 padding: 48,
45 maxChatWidth: 768,45 maxChatWidth: 768,
46 minChatWidth: 640,46 minChatWidth: 640,
47 composerHeight: 0,47 composerHeight: 0,
...@@ -139,7 +139,7 @@ export function ChatCanvas({...@@ -139,7 +139,7 @@ export function ChatCanvas({
139 <div139 <div
140 ref={widthProbeRef}140 ref={widthProbeRef}
141 aria-hidden141 aria-hidden
142 className="pointer-events-none invisible absolute h-0 w-(--chat-content-max-width) min-w-[40rem] box-content ps-3 sm:ps-5"142 className="pointer-events-none invisible absolute h-0 w-(--chat-content-max-width) min-w-[40rem] box-content ps-3 sm:ps-12"
143 />143 />
144 {children}144 {children}
145 </div>145 </div>
apps/web/src/components/chat/chatCanvasLayout.test.ts+36-16
...@@ -51,7 +51,13 @@ describe("chat canvas layout", () => {...@@ -51,7 +51,13 @@ describe("chat canvas layout", () => {
5151
52 it("centers chat in the whole container without a preview", () => {52 it("centers chat in the whole container without a preview", () => {
53 expect(resolve(1344, null).chat).toEqual({ left: 288, width: 768, insetStart: 0, insetEnd: 0 });53 expect(resolve(1344, null).chat).toEqual({ left: 288, width: 768, insetStart: 0, insetEnd: 0 });
54 expect(resolve(390, null).chat).toEqual({ left: 20, width: 350, insetStart: 0, insetEnd: 0 });54 expect(
55 resolveChatCanvasLayout({
56 container: { width: 390, height: 900 },
57 preview: null,
58 padding: 12,
59 }).chat,
60 ).toEqual({ left: 12, width: 366, insetStart: 0, insetEnd: 0 });
55 });61 });
56 it("does not move chat when a bottom-right preview fits in its margin", () => {62 it("does not move chat when a bottom-right preview fits in its margin", () => {
57 const result = resolve(1600);63 const result = resolve(1600);
...@@ -66,21 +72,21 @@ describe("chat canvas layout", () => {...@@ -66,21 +72,21 @@ describe("chat canvas layout", () => {
66 });72 });
67 it("shrinks chat modestly after using the left margin", () => {73 it("shrinks chat modestly after using the left margin", () => {
68 const result = resolve(1200, { ...preview, width: 480 });74 const result = resolve(1200, { ...preview, width: 480 });
69 expect(result.chat.width).toBe(676);75 expect(result.chat.width).toBe(648);
70 expect(result.chat.left).toBe(20);76 expect(result.chat.left).toBe(48);
71 expectClear(result);77 expectClear(result);
72 });78 });
73 it("stops leftward dragging before the player can push chat to the right", () => {79 it("stops leftward dragging before the player can push chat to the right", () => {
74 const centered = resolve(1344, null).chat;80 const centered = resolve(1344, null).chat;
75 for (const x of [1100, 800, 672, 600, 400, 12, -100]) {81 for (const x of [1100, 800, 700, 600, 400, 12, -100]) {
76 const result = resolve(1344, { ...preview, position: { x, y: 500 } });82 const result = resolve(1344, { ...preview, position: { x, y: 500 } });
77 expect(result.frame!.x).toBeGreaterThanOrEqual(672);83 expect(result.frame!.x).toBeGreaterThanOrEqual(700);
78 expect(result.chat.left).toBeLessThanOrEqual(centered.left);84 expect(result.chat.left).toBeLessThanOrEqual(centered.left);
79 expect(result.chat.insetStart).toBe(0);85 expect(result.chat.insetStart).toBe(0);
80 expect(result.chat.width).toBeGreaterThanOrEqual(640);86 expect(result.chat.width).toBeGreaterThanOrEqual(640);
81 expectClear(result);87 expectClear(result);
82 }88 }
83 expect(resolve(1344, { ...preview, position: { x: 12, y: 500 } }).frame!.x).toBe(672);89 expect(resolve(1344, { ...preview, position: { x: 12, y: 500 } }).frame!.x).toBe(700);
84 });90 });
85 it("keeps a dragged player above the composer when no readable lane fits beside it", () => {91 it("keeps a dragged player above the composer when no readable lane fits beside it", () => {
86 const result = resolve(1000, { ...preview, position: { x: 12, y: 700 } });92 const result = resolve(1000, { ...preview, position: { x: 12, y: 700 } });
...@@ -171,8 +177,8 @@ describe("chat canvas layout", () => {...@@ -171,8 +177,8 @@ describe("chat canvas layout", () => {
171 expect(shifted.overlapsDetailsCard).toBe(false);177 expect(shifted.overlapsDetailsCard).toBe(false);
172 expectClear(shifted);178 expectClear(shifted);
173179
174 const narrowed = layout(560);180 const narrowed = layout(532);
175 expect(narrowed.frame!.width).toBe(560);181 expect(narrowed.frame!.width).toBe(532);
176 expect(narrowed.chat.width).toBe(656);182 expect(narrowed.chat.width).toBe(656);
177 expect(narrowed.overlapsDetailsCard).toBe(false);183 expect(narrowed.overlapsDetailsCard).toBe(false);
178 expectClear(narrowed);184 expectClear(narrowed);
...@@ -182,7 +188,7 @@ describe("chat canvas layout", () => {...@@ -182,7 +188,7 @@ describe("chat canvas layout", () => {
182 expect(grown.overlapsDetailsCard).toBe(true);188 expect(grown.overlapsDetailsCard).toBe(true);
183 expect(grown.overlapsChat).toBe(false);189 expect(grown.overlapsChat).toBe(false);
184 expectClear(grown);190 expectClear(grown);
185 expect(layout(560)).toEqual(narrowed);191 expect(layout(532)).toEqual(narrowed);
186 });192 });
187 it.each(["west", "north"] as const)(193 it.each(["west", "north"] as const)(
188 "folds the card before moving a %s resize beside it",194 "folds the card before moving a %s resize beside it",
...@@ -214,7 +220,7 @@ describe("chat canvas layout", () => {...@@ -214,7 +220,7 @@ describe("chat canvas layout", () => {
214 const density = (result: ReturnType<typeof layout>) => {220 const density = (result: ReturnType<typeof layout>) => {
215 const card = resolveThreadDetailsCardLayout({221 const card = resolveThreadDetailsCardLayout({
216 container,222 container,
217 lane: { padding: 20, minChatWidth: 640 },223 lane: { padding: 48, minChatWidth: 640 },
218 frame: result.frame,224 frame: result.frame,
219 overlapsDetailsCard: result.overlapsDetailsCard,225 overlapsDetailsCard: result.overlapsDetailsCard,
220 });226 });
...@@ -274,16 +280,30 @@ describe("workspace card beside chat", () => {...@@ -274,16 +280,30 @@ describe("workspace card beside chat", () => {
274 expect(withCard(1147).chat).toEqual({ left: 87, width: 736, insetStart: 0, insetEnd: 237 });280 expect(withCard(1147).chat).toEqual({ left: 87, width: 736, insetStart: 0, insetEnd: 237 });
275 });281 });
276 it("narrows chat only after it reaches the left padding", () => {282 it("narrows chat only after it reaches the left padding", () => {
277 expect(withCard(1080).chat).toMatchObject({ left: 20, width: 736 });283 expect(withCard(1108).chat).toMatchObject({ left: 48, width: 736 });
278 expect(withCard(1000).chat).toEqual({ left: 20, width: 656, insetStart: 0, insetEnd: 304 });284 expect(withCard(1028).chat).toEqual({ left: 48, width: 656, insetStart: 0, insetEnd: 276 });
279 expect(withCard(984).chat).toMatchObject({ left: 20, width: 640 });285 expect(withCard(1012).chat).toMatchObject({ left: 48, width: 640 });
286 });
287 it("reserves the marker gutter across widths where the details card fits", () => {
288 for (let width = 1012; width <= 1440; width++) {
289 const container = { width, height: 900 };
290 const card = resolveThreadDetailsCardLayout({
291 container,
292 lane: { padding: 48, minChatWidth: 640 },
293 frame: null,
294 })!;
295 const chat = withCard(width).chat;
296 expect(chat.left).toBeGreaterThanOrEqual(48);
297 expect(chat.width).toBeGreaterThanOrEqual(640);
298 expect(card.x - chat.left - chat.width).toBeGreaterThanOrEqual(32);
299 }
280 });300 });
281 it("keeps a full-width chat clear of the card", () => {301 it("keeps a full-width chat clear of the card", () => {
282 expect(withCard(1147, null, 10_000).chat).toEqual({302 expect(withCard(1147, null, 10_000).chat).toEqual({
283 left: 20,303 left: 48,
284 width: 803,304 width: 775,
285 insetStart: 0,305 insetStart: 0,
286 insetEnd: 304,306 insetEnd: 276,
287 });307 });
288 });308 });
289 it("keeps centered chat in place when a new preview opens below the card", () => {309 it("keeps centered chat in place when a new preview opens below the card", () => {
apps/web/src/components/chat/chatCanvasLayout.ts+1-1
...@@ -27,7 +27,7 @@ export const DETAILS_CARD_CLEARANCE = 32;...@@ -27,7 +27,7 @@ export const DETAILS_CARD_CLEARANCE = 32;
27export function resolveChatCanvasLayout({27export function resolveChatCanvasLayout({
28 container,28 container,
29 preview,29 preview,
30 padding = 20,30 padding = 48,
31 maxChatWidth = 768,31 maxChatWidth = 768,
32 minChatWidth = 640,32 minChatWidth = 640,
33 composerHeight = 0,33 composerHeight = 0,
apps/web/src/components/chat/threadDetailsCardLayout.test.ts+3-3
...@@ -4,7 +4,7 @@ import {...@@ -4,7 +4,7 @@ import {
4 resolveThreadDetailsCardLayout,4 resolveThreadDetailsCardLayout,
5} from "./threadDetailsCardLayout";5} from "./threadDetailsCardLayout";
66
7const lane = { padding: 20, minChatWidth: 640 };7const lane = { padding: 48, minChatWidth: 640 };
8const resolve = (width: number, height: number, previewY: number | null = null) =>8const resolve = (width: number, height: number, previewY: number | null = null) =>
9 resolveThreadDetailsCardLayout({9 resolveThreadDetailsCardLayout({
10 container: { width, height },10 container: { width, height },
...@@ -33,8 +33,8 @@ describe("workspace card", () => {...@@ -33,8 +33,8 @@ describe("workspace card", () => {
33 ).toEqual({ x: 1308, y: 60, width: 280, height: 828 });33 ).toEqual({ x: 1308, y: 60, width: 280, height: 828 });
34 });34 });
35 it("hides when a readable chat lane cannot fit beside it", () => {35 it("hides when a readable chat lane cannot fit beside it", () => {
36 expect(resolve(984, 900)).toMatchObject({ x: 692 });36 expect(resolve(1012, 900)).toMatchObject({ x: 720 });
37 expect(resolve(983, 900)).toBeNull();37 expect(resolve(1011, 900)).toBeNull();
38 });38 });
39 it("keeps the card at the top right while the preview is freely dragged vertically", () => {39 it("keeps the card at the top right while the preview is freely dragged vertically", () => {
40 for (const y of [12, 170, 250, 400, 648]) {40 for (const y of [12, 170, 250, 400, 648]) {
apps/web/src/index.css+4-6
...@@ -2344,18 +2344,16 @@ code {...@@ -2344,18 +2344,16 @@ code {
2344 @media (min-width: 640px) {2344 @media (min-width: 640px) {
2345 .chat-composer-lane {2345 .chat-composer-lane {
2346 padding-inline-start: calc(2346 padding-inline-start: calc(
2347 env(safe-area-inset-left) + 1.25rem + var(--chat-lane-inset-start, 0px)2347 env(safe-area-inset-left) + 3rem + var(--chat-lane-inset-start, 0px)
2348 );
2349 padding-inline-end: calc(
2350 env(safe-area-inset-right) + 1.25rem + var(--chat-lane-inset-end, 0px)
2351 );2348 );
2349 padding-inline-end: calc(env(safe-area-inset-right) + 3rem + var(--chat-lane-inset-end, 0px));
2352 }2350 }
2353 [data-chat-canvas] .messages-timeline-scroll {2351 [data-chat-canvas] .messages-timeline-scroll {
2354 padding-inline-start: calc(2352 padding-inline-start: calc(
2355 1.25rem + var(--chat-lane-inset-start, 0px) - var(--chat-timeline-gutter, 0px)2353 3rem + var(--chat-lane-inset-start, 0px) - var(--chat-timeline-gutter, 0px)
2356 );2354 );
2357 padding-inline-end: calc(2355 padding-inline-end: calc(
2358 1.25rem + var(--chat-lane-inset-end, 0px) - var(--chat-timeline-gutter, 0px)2356 3rem + var(--chat-lane-inset-end, 0px) - var(--chat-timeline-gutter, 0px)
2359 );2357 );
2360 }2358 }
2361 }2359 }