Skip to content

Commit 541a44c

Browse files
Jing-yilinclaude
andcommitted
A picture clicked in the chat shows over the window as a board does
The overlay becomes Present, shared by the canvas and the chat. A click beside what it shows closes it as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0beacda commit 541a44c

2 files changed

Lines changed: 72 additions & 27 deletions

File tree

‎canvas/src/CanvasPresent.tsx‎

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from "react";
1+
import { type ReactNode, useEffect, useState } from "react";
22
import { createPortal } from "react-dom";
33
import type { Editor } from "tldraw";
44
import { asCanvasTarget, type CanvasTarget } from "./canvasClicks";
@@ -9,33 +9,18 @@ import { useCanvasFileHtml } from "./canvasLibrary";
99
const INSET = 32;
1010

1111
/**
12-
* Space on a selected board or picture shows it alone over the whole window, the window dimmed
13-
* round the board, which is scaled to fit it. Space again, or Esc, comes back to the canvas
14-
* as it was. Inside the window rather than the browser's full screen, which on a Mac moves the
15-
* window to a display of its own and back, slowly.
16-
*
17-
* In the window's document, not the canvas frame's, so it covers the tab bar and the chat as well
18-
* (the window loads the same stylesheet, shell.tsx). Taken from tldraw on the key going down, so
19-
* with one of them selected Space is this and not the hand. Heard in both documents: clicking the
20-
* shown board moves the focus out of the frame.
12+
* Space on a selected board or picture shows it alone over the whole window (`Present`), scaled
13+
* to fit it. Taken from tldraw on the key going down, so with one of them selected Space is this
14+
* and not the hand.
2115
*/
2216
export function CanvasPresent({ editor }: { editor: Editor | null }) {
2317
const [shown, setShown] = useState<CanvasTarget>();
2418
const top = window.top!;
2519
const [view, setView] = useState({ w: top.innerWidth, h: top.innerHeight });
2620

2721
useEffect(() => {
28-
if (!editor) return;
22+
if (!editor || shown) return;
2923
const onDown = (e: KeyboardEvent) => {
30-
if (shown) {
31-
if (e.code !== "Space" && e.key !== "Escape") return;
32-
e.preventDefault();
33-
e.stopPropagation();
34-
if (e.repeat) return;
35-
setShown(undefined);
36-
editor.focus();
37-
return;
38-
}
3924
if (e.code !== "Space" || e.repeat) return;
4025
const typing =
4126
e.target instanceof HTMLElement &&
@@ -50,20 +35,66 @@ export function CanvasPresent({ editor }: { editor: Editor | null }) {
5035
e.stopPropagation();
5136
setShown(target);
5237
};
38+
addEventListener("keydown", onDown, true);
39+
return () => removeEventListener("keydown", onDown, true);
40+
}, [editor, shown]);
41+
42+
useEffect(() => {
5343
const resized = () => setView({ w: top.innerWidth, h: top.innerHeight });
44+
top.addEventListener("resize", resized);
45+
return () => top.removeEventListener("resize", resized);
46+
}, [top]);
47+
48+
if (!editor || !shown) return null;
49+
return (
50+
<Present
51+
close={() => {
52+
setShown(undefined);
53+
editor.focus();
54+
}}
55+
>
56+
<Presented editor={editor} shape={shown} view={view} />
57+
</Present>
58+
);
59+
}
60+
61+
/**
62+
* Something shown alone over the whole window, the window dimmed round it: a board or picture
63+
* from the canvas, or a picture in the chat. Space, Esc or a click beside it comes back to where
64+
* it was. Inside the window rather than the browser's full screen, which on a Mac moves the window
65+
* to a display of its own and back, slowly.
66+
*
67+
* In the window's document, not the canvas frame's, so it covers the tab bar and the chat as well
68+
* (the window loads the same stylesheet, shell.tsx). The keys are heard in both documents: the
69+
* focus can be in either.
70+
*/
71+
export function Present({
72+
close,
73+
children,
74+
}: {
75+
close: () => void;
76+
children: ReactNode;
77+
}) {
78+
const top = window.top!;
79+
useEffect(() => {
80+
const onDown = (e: KeyboardEvent) => {
81+
if (e.code !== "Space" && e.key !== "Escape") return;
82+
e.preventDefault();
83+
e.stopPropagation();
84+
if (!e.repeat) close();
85+
};
5486
const windows = top === window ? [window] : [window, top];
5587
for (const w of windows) w.addEventListener("keydown", onDown, true);
56-
top.addEventListener("resize", resized);
5788
return () => {
5889
for (const w of windows) w.removeEventListener("keydown", onDown, true);
59-
top.removeEventListener("resize", resized);
6090
};
61-
}, [editor, shown, top]);
62-
63-
if (!editor || !shown) return null;
91+
}, [close, top]);
6492
return createPortal(
65-
<div className="sp-present">
66-
<Presented editor={editor} shape={shown} view={view} />
93+
<div
94+
className="sp-present"
95+
onClick={(e) => e.target === e.currentTarget && close()}
96+
>
97+
{children}
6798
</div>,
6899
top.document.body,
69100
);

‎canvas/src/ChatPanel.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import {
6666
import type { Session } from "./agentRun";
6767
import { namedPictures, readDraft, slashWord } from "./chatDraft";
6868
import { applyFrame, followRun, writingTo, type Turn } from "./chatTransport";
69+
import { Present } from "./CanvasPresent";
6970
import { ClaudeMark } from "./ClaudeMark";
7071
import { CodexMark } from "./CodexMark";
7172
import { Check, ClockRewind, Image, Plus } from "./geistIcons";
@@ -331,6 +332,12 @@ export function ChatPanel(props: {
331332
/** Whether the composer is ringing, to say a half-written message is waiting in it. Off again
332333
* when the ring has faded, so the next one rings too. */
333334
const [cued, setCued] = useState(false);
335+
/** The picture shown over the window, clicked in the conversation. */
336+
const [shown, setShown] = useState<string>();
337+
const show = (e: React.MouseEvent<HTMLAnchorElement>) => {
338+
e.preventDefault();
339+
setShown(e.currentTarget.href);
340+
};
334341
const abort = useRef(new AbortController());
335342
const log = useRef<HTMLDivElement>(null);
336343
/** Whether the log follows what arrives. Scrolling up to read stops it, so new output no longer
@@ -1084,6 +1091,11 @@ export function ChatPanel(props: {
10841091

10851092
return (
10861093
<>
1094+
{shown && (
1095+
<Present close={() => setShown(undefined)}>
1096+
<img src={shown} alt="" />
1097+
</Present>
1098+
)}
10871099
<aside
10881100
className={
10891101
open ? "sp-panel sp-chat" : "sp-panel sp-chat sp-chat-collapsed"
@@ -1267,6 +1279,7 @@ export function ChatPanel(props: {
12671279
href={`/__sp/agent/run/${t.runId}/image/${g.n}`}
12681280
target="_blank"
12691281
rel="noreferrer"
1282+
onClick={show}
12701283
title={g.name}
12711284
>
12721285
<img
@@ -1324,6 +1337,7 @@ export function ChatPanel(props: {
13241337
href={`/__sp/agent/run/${t.runId}/shot/${s.k}`}
13251338
target="_blank"
13261339
rel="noreferrer"
1340+
onClick={show}
13271341
title={b.detail}
13281342
>
13291343
<img

0 commit comments

Comments
 (0)