Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f7cf8f5
canvas: put the panels on Geist, dark by default
Jing-yilin Sep 17, 2026
b73ca2a
canvas: let a board sit on the canvas, not on a white card
Jing-yilin Sep 17, 2026
3da8523
canvas: one Enter sends a command typed out in full
Jing-yilin Sep 18, 2026
ec62ed5
canvas: build on Tailwind and shadcn/ui, over the same scale
Jing-yilin Sep 18, 2026
c2a150b
canvas: draw from Vercel's icon set, not a transcription of it
Jing-yilin Sep 18, 2026
d70f0d8
canvas: one ground, one icon button, one scrollbar
Jing-yilin Sep 18, 2026
690482a
canvas: leave the top bar a switch and two destinations
Jing-yilin Sep 18, 2026
1bc0c7a
canvas: make the two asks one pair of buttons
Jing-yilin Sep 18, 2026
04e8c38
docs: write down what the Geist pass decided, and what changed for a …
Jing-yilin Sep 18, 2026
d685be9
docs: say it plainly across the Geist branch
Jing-yilin Sep 18, 2026
785548a
canvas: put pictures in the chat, both directions
Jing-yilin Sep 18, 2026
4f8e02e
docs: say what the chat panel does with pictures
Jing-yilin Sep 18, 2026
92f6ce3
canvas: make the slash palette open every time
Jing-yilin Sep 18, 2026
d2b413b
docs: correct what the note said about asking for commands
Jing-yilin Sep 18, 2026
7db61e6
canvas: what the review of #97 found, fixed
Jing-yilin Sep 18, 2026
624c8b5
canvas: hand a board or a picture to the chat from the canvas
Jing-yilin Sep 18, 2026
6c91422
canvas: a picture handed over from the canvas names itself too
Jing-yilin Sep 18, 2026
975b5ca
canvas: start the picture numbers over once nothing points at one
Jing-yilin Sep 18, 2026
cc3938b
canvas: one number per picture, and the sentence keeps the tray honest
Jing-yilin Sep 18, 2026
09f7446
canvas: two fingers over the attach buttons still pan the board
Jing-yilin Sep 18, 2026
e81d3bc
canvas: a picture is the whole of its box, not just its pixels
Jing-yilin Sep 18, 2026
3477eae
canvas: a mockup added to the chat looks like the mockup
Jing-yilin Sep 18, 2026
0075875
canvas: one button on a mockup, because there is one thing to do with it
Jing-yilin Sep 18, 2026
14b6e58
Merge origin/canvas-geist-ui into fix/canvas-geist-ui-review
Jing-yilin Sep 18, 2026
2b19654
canvas: the second review of the fix branch
Jing-yilin Sep 18, 2026
fc449a0
canvas: the server's image folder is made, not named
Jing-yilin Sep 18, 2026
e34d264
canvas: a picture already in the tray is not a twenty-first, and the …
Jing-yilin Sep 18, 2026
8f1bfe2
canvas: a pick that cannot fit is refused before it is read
Jing-yilin Sep 18, 2026
d38fd09
canvas: the tray is one tray, whoever is adding to it
Jing-yilin Sep 18, 2026
17adf38
canvas: a picture opened in a tab cannot be a program, and a run forg…
Jing-yilin Sep 18, 2026
ca167de
canvas: a preference that cannot be remembered is still a preference
Jing-yilin Sep 18, 2026
1be809c
canvas: the token block the inspector reads is the board's own
Jing-yilin Sep 18, 2026
2b3a511
skills: the refresh button is a menu item everywhere it is named
Jing-yilin Sep 18, 2026
036e94d
Merge #98: two fixes of one review, resolved to the better half of each
Jing-yilin Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,9 @@ status` and `sp-canvas stop` do what they say.

Deep-link a page with `?canvas=<slug>`, and one board of it with
`?canvas=<slug>#<file>`: it opens in the inspector with the camera on it, and
clicking any board writes that link into the address bar. The bottom toolbar carries a
styles-panel toggle alongside tldraw's own tools; the top bar carries a
force-relayout button. Press it after editing a `layout.json`. A board folder
added after boot appears on its own.
clicking any board writes that link into the address bar. Right-clicking the
canvas offers Force refresh; choose it after editing a `layout.json`. A board
folder added after boot appears on its own.

## The workflow

Expand Down
47 changes: 40 additions & 7 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,23 @@ This needs the toolkit reinstalled, not only the plugin updated: `sp-canvas`
the panel but every message answers 503. Re-run the README's `uv tool install`
line with `--force`.

The canvas toolbar drops its shape editing: undo, redo, delete, duplicate and
the overflow menu of aligns, distributes and reorders are gone from the top
bar, which leaves comments, Export to Figma, Brand kit, clone and refresh. The
actions themselves are unchanged — the keyboard and the right-click menu still
have them. The zoom readout and minimap toggle go from the bottom-left corner
too, where they sat under the chat panel's composer: zoom is the trackpad, ⌘+
and ⌘-, and ⇧1 to fit the page.
The canvas top bar is down to a switch, the page name and two destinations:
collapse the chat panel, Export to Figma, Brand kit. Everything else on it
moved. Shape editing, meaning undo, redo, delete, duplicate and the overflow
of aligns, distributes and reorders, is on the keyboard and the right-click
menu, where it already was. These boards are written from files by a
generator, so six buttons for nudging them crowded out the two the bar is
for. Comment, clone and force refresh join them on the right button. All
three act on what is under the cursor, or on the page it is on, which is what
a right-click has already picked out. The hamburger beside the page name goes
too, and so do the zoom readout and minimap toggle in the bottom-left corner,
where they sat under the chat panel's composer. Zoom is the trackpad, ⌘+ and
⌘-, and ⇧1 to fit the page.

The switch for the chat panel is in the canvas's own top-left corner, which is
the one place it can be whether the panel is open or shut. In the panel's
header it went away with the panel and needed a second control to bring it
back.

Under the message box: which model, which effort, and what the last message
cost. The model list is the agent's own — for Codex it is the list its own
Expand Down Expand Up @@ -96,6 +106,29 @@ A plus in the panel's header starts a new session: the log clears and the next
message has nothing above it. Anything still running keeps running and stays in
the history list — the button clears the view, it does not stop the agent.

The canvas is dark, and its panels are drawn in Vercel's Geist: its greys and
its accents, its icons, and Geist Sans and Geist Mono bundled so a board looks
the same offline as online. The chat panel, the inspector, the top bar and the
canvas they sit around all read as one app now, on one ground, separated by
hairlines rather than by shade. tldraw's own chrome comes with them. Its menus,
its toolbar and its context menu read the same tokens, so a page menu is the
same black card with the same hairline as a panel's menu, and an icon button
in the top bar is the same grey, and lights the same way, as one in the chat
panel's header beside it. The glyphs are Geist's own set rather than a
transcription of it, and the scrollbars are the app's rather than the
platform's: one thin grey thumb on whatever ground it is over, in the
transcript, the slash menu, the history, the notes and the inspector alike.
Your boards are untouched. A mockup is drawn by its own generator and keeps
whatever palette it was measured in, scrollbars included.

One thing about a board does change, and it is what the dark ground exposed. A
mockup used to sit on a white card. A frame paints an opaque white background
of its own underneath whatever the board draws, invisible while the canvas was
white and a card around every phone once it is not. The canvas releases it, so
the canvas shows through wherever a board paints nothing and every pixel the
board does paint stays as it was. A board that wants a ground, such as a token
sheet or an evidence sheet, still declares one and looks exactly as it did.

## v1.3.0

2026-09-15. The inspector moves onto the canvas: you click the board itself,
Expand Down
382 changes: 321 additions & 61 deletions canvas/bun.lock

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions canvas/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"utils": "@/lib/utils",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
2 changes: 1 addition & 1 deletion canvas/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#f0eeec" />
<meta name="theme-color" content="#000000" />
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
<link rel="icon" type="image/png" href="/favicon-32.png" sizes="32x32" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
Expand Down
8 changes: 8 additions & 0 deletions canvas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,30 @@
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/geist": "^5.3.0",
"@fontsource-variable/geist-mono": "^5.3.0",
"@tldraw/commenting": "^5.4.2",
"class-variance-authority": "^0.7.1",
"cn": "^0.3.0",
"dompurify": "^3.4.15",
"geist-icons": "^1.3.0",
"marked": "^18.0.13",
"radix-ui": "^1.6.7",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"remend": "^1.3.1",
"tldraw": "^5.4.2"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^26.5.1",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"jsdom": "^30.1.0",
"oxlint": "^1.82.0",
"sharp": "^0.35.4",
"tailwindcss": "^4.3.3",
"typescript": "~7.0.2",
"vite": "^8.3.0",
"vitest": "^5.0.0"
Expand Down
5 changes: 0 additions & 5 deletions canvas/public/clone.svg

This file was deleted.

4 changes: 0 additions & 4 deletions canvas/public/refresh.svg

This file was deleted.

80 changes: 56 additions & 24 deletions canvas/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
useState,
} from "react";
import {
Ellipse2d,
ImageShapeUtil,
Rectangle2d,
Tldraw,
commentSchemaRecords,
createShapeId,
Expand All @@ -21,12 +24,12 @@ import {
type TLPageId,
type TLAsset,
type TLAssetStore,
type TLImageShape,
type TLShapeId,
type TLDefaultColorStyle,
type TLTextShape,
useEditor,
useLocalStore,
useValue,
} from "tldraw";
import "tldraw/tldraw.css";
import "@tldraw/commenting/commenting.css";
Expand Down Expand Up @@ -83,14 +86,30 @@ import { canvasesDir, canvasesNamespace } from "virtual:canvases";
import { installCanvasComments, readCommentUser } from "./canvasComments";
import {
CanvasChromeContext,
canvasChromeAssetUrls,
canvasChromeComponents,
canvasCommentOverrides,
canvasUiOverrides,
canvasCommentTools,
} from "./canvasChrome";

/**
* A picture is the whole of its box. tldraw hit-tests an image that can carry transparency against
* its own pixels, so a mockup drawn on nothing answers nothing where it is nothing: the inspector
* stays shut and the attach buttons never appear except over the drawing itself. Here a picture is
* a tile in a row of tiles, and the empty part of a tile is still that tile.
*/
class CanvasImageShapeUtil extends ImageShapeUtil {
override getGeometry(shape: TLImageShape) {
const box = { width: shape.props.w, height: shape.props.h, isFilled: true };
// A circle crop is still a circle; it is only the alpha channel that stops counting.
return shape.props.crop?.isCircle
? new Ellipse2d(box)
: new Rectangle2d(box);
}
}

const shapeUtils = [
CanvasFileShapeUtil,
CanvasImageShapeUtil,
CanvasLinkShapeUtil,
CanvasStatusBannerShapeUtil,
];
Expand Down Expand Up @@ -137,8 +156,14 @@ const TLDRAW_LICENSE_KEY: string | undefined = import.meta.env
const storeOptions = {
persistenceKey: PERSISTENCE_KEY,
// The same set `<Tldraw>` merges for itself; the schema has to know every type the document
// can hold, hand-drawn annotations included.
shapeUtils: [...defaultShapeUtils, ...shapeUtils],
// can hold, hand-drawn annotations included. A default one of ours stands in for is dropped
// rather than listed beside it: the schema refuses the same shape type twice.
shapeUtils: [
...defaultShapeUtils.filter(
(fallback) => !shapeUtils.some((ours) => ours.type === fallback.type),
),
...shapeUtils,
],
bindingUtils: defaultBindingUtils,
assetUtils: defaultAssetUtils,
records: commentSchemaRecords,
Expand Down Expand Up @@ -247,22 +272,6 @@ function LockedLinkClicks() {
return null;
}

/** Blacks out the canvas under the welcome board, whose art runs to its own edges. */
function WelcomeGround() {
const editor = useEditor();
const isWelcome = useValue(
"on the welcome page",
() => editor.getCurrentPage().meta.canvasSlug === WELCOME_PAGE_SLUG,
[editor],
);
useEffect(() => {
const container = editor.getContainer();
container.classList.toggle("canvas-welcome-ground", isWelcome);
return () => container.classList.remove("canvas-welcome-ground");
}, [editor, isWelcome]);
return null;
}

/**
* A path as a single shell word. A boards directory is chosen by whoever ran `sp-canvas`, so it
* can hold a space, and the command below is meant to be copied and run as it stands.
Expand Down Expand Up @@ -1232,6 +1241,8 @@ function initializeCanvas(editor: Editor) {
/** Distance from the viewport's edge to the board the address named, in screen px. */
const BOARD_ZOOM_INSET = 80;

const CHAT_COLLAPSED_KEY = "sp-chat-collapsed";

export default function App() {
/** The board open in the inspector: click any board on the canvas to open it, Escape or × to close. */
const [inspecting, setInspecting] = useState<CanvasLibraryFile | null>(null);
Expand All @@ -1241,6 +1252,17 @@ export default function App() {
const [commentUser, setCommentUser] = useState(readCommentUser);
/** State rather than a ref: the inspector panel renders outside `<Tldraw>` and needs it. */
const [editor, setEditor] = useState<Editor | null>(null);
/** Whether the chat panel is shut. Here rather than in the panel, because the button that works
* it sits in the canvas's top bar, which is the panel's sibling, not its child. Remembered
* across reloads, because it is a preference about this window, not about any one board. */
const [chatCollapsed, setChatCollapsed] = useState(() => {
try {
return localStorage.getItem(CHAT_COLLAPSED_KEY) === "true";
} catch {
// Storage unavailable (private mode, blocked cookies), so the panel starts open.
return false;
}
});
const store = useLocalStore(storeOptions);
/** What the inspector has open, spelled the way the address spells it: a board by file name,
* a picture by its path inside the folder, each with the page it belongs to. For the address
Expand Down Expand Up @@ -1325,6 +1347,9 @@ export default function App() {

function handleMount(editor: Editor) {
setEditor(editor);
// tldraw's own dark theme, to match the panel's. A dark rail against tldraw's near-white ground
// looks like two apps in one window, and the ground is most of the window.
editor.user.updateUserPreferences({ colorScheme: "dark" });
initializeCanvas(editor);
// After the library, which is what creates the pages the comments are keyed to.
const disposeComments = installCanvasComments(editor);
Expand Down Expand Up @@ -1359,6 +1384,15 @@ export default function App() {
inspectBoard: onPick,
inspectingPath: inspecting?.path ?? null,
inspectorOpen: Boolean(inspecting || inspectingImage),
chatCollapsed,
toggleChat: () => {
try {
localStorage.setItem(CHAT_COLLAPSED_KEY, String(!chatCollapsed));
} catch {
// Storage unavailable (private mode, blocked cookies), so the choice lasts until a reload.
}
setChatCollapsed(!chatCollapsed);
},
setInspectorFrame: (frame: HTMLIFrameElement | null) => {
inspectorFrame.current = frame;
},
Expand All @@ -1369,12 +1403,11 @@ export default function App() {
{import.meta.env.DEV && <ChatPanel />}
<main className="tldraw__editor" aria-label="Prototype design canvas">
<Tldraw
assetUrls={canvasChromeAssetUrls}
components={canvasChromeComponents}
store={store}
shapeUtils={shapeUtils}
tools={canvasCommentTools}
overrides={canvasCommentOverrides}
overrides={canvasUiOverrides}
licenseKey={TLDRAW_LICENSE_KEY}
onMount={handleMount}
>
Expand All @@ -1386,7 +1419,6 @@ export default function App() {
inspectingPath={inspecting?.path ?? null}
frame={inspectorFrame}
/>
<WelcomeGround />
<EmptyLibraryNotice />
</Tldraw>
</main>
Expand Down
6 changes: 3 additions & 3 deletions canvas/src/BoardsSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FigmaMark } from "./FigmaMark";
import { LogoFigma } from "./geistIcons";
import { hasBrandMaterial, pageNameFor } from "./canvasLibrary";
import { sheetRows } from "./sheetLayout";
import { brandPageUrl, canvasPageUrl } from "./canvasUrl";
Expand Down Expand Up @@ -41,7 +41,7 @@ export function BoardsSheet({ slug }: { slug: string }) {
generated one. */}
<section className="howto">
<h2>
<FigmaMark height={17} />
<LogoFigma size={17} />
Into Figma, in three steps
</h2>
<div className="howto__body">
Expand Down Expand Up @@ -99,7 +99,7 @@ export function BoardsSheet({ slug }: { slug: string }) {
target="_blank"
rel="noopener noreferrer"
>
<FigmaMark height={19} />
<LogoFigma size={19} />
<span>
Get the Figma plugin
<small>Optional · html.to.design by ‹div›RIOTS</small>
Expand Down
13 changes: 12 additions & 1 deletion canvas/src/CanvasFileShapeUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ function CanvasFile({ shape }: { shape: CanvasFileShape }) {
border: 0,
display: "block",
pointerEvents: isEditing ? "auto" : "none",
// The board's own scheme, so the frame composites transparent rather than painting the
// white backdrop a frame gets under a document whose scheme differs from its element's.
// On the element, so nothing is written into the board. Measured in
// docs/2026-09-17-canvas-geist.md.
colorScheme: "light",
};

return (
Expand Down Expand Up @@ -118,7 +123,13 @@ function CanvasFile({ shape }: { shape: CanvasFileShape }) {
) : hasCanvasFile(shape.props.path) ? null : (
// A board that exists but is not in yet renders nothing, so the frame fills in when its
// chunk arrives rather than flashing an error first.
<div style={{ padding: 16, font: "13px sans-serif", color: "#a33" }}>
<div
style={{
padding: 16,
font: "13px var(--sp-sans)",
color: "var(--ds-red-900)",
}}
>
Missing source: {shape.props.path}
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion canvas/src/CanvasLinkShapeUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const MUTED = "#7C7C86";
// One step up from the welcome page's black ground, so a card reads as a raised panel.
const GROUND = "#111115";
const EDGE = "1px solid #26262C";
const FONT = "ui-sans-serif, -apple-system, system-ui, sans-serif";
const FONT = "var(--sp-sans)";

/**
* Places a board behind the shell's screen so the `[x, y, w, h]` box fills it and sits centred:
Expand Down
4 changes: 2 additions & 2 deletions canvas/src/CanvasStatusBannerShapeUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const STATUS_STYLE: Record<
Exclude<CanvasBoardStatus, "live">,
{ label: string; fill: string; ink: string }
> = {
exploring: { label: "EXPLORING", fill: "#FFB224", ink: "#171717" },
exploring: { label: "EXPLORING", fill: "var(--ds-amber-700)", ink: "#171717" },
outdated: { label: "OUTDATED", fill: "#4D4D4D", ink: "#FFFFFF" },
};

Expand Down Expand Up @@ -80,7 +80,7 @@ export class CanvasStatusBannerShapeUtil extends BaseBoxShapeUtil<CanvasStatusBa
height: shape.props.h,
background: style.fill,
color: style.ink,
font: `800 42px/${shape.props.h}px -apple-system, BlinkMacSystemFont, sans-serif`,
font: `800 42px/${shape.props.h}px var(--sp-sans)`,
letterSpacing: ".14em",
textAlign: "center",
}}
Expand Down
Loading
Loading