Skip to content

Commit 511528e

Browse files
authored
Add square cell grid aspect ratio (#183)
1 parent efaf84c commit 511528e

22 files changed

Lines changed: 326 additions & 19 deletions

File tree

.changeset/square-grid-cells.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@farcaster/snap": minor
3+
"@farcaster/snap-hono": patch
4+
---
5+
6+
Add `cellAspectRatio` support to `cell_grid`.
7+
8+
`cell_grid` now accepts `cellAspectRatio: "square"` to keep each cell square as snap width changes, while preserving the existing `rowHeight` behavior by default. This makes board-style snaps such as Minesweeper render with stable cell geometry across client widths.
9+
10+
The React, React Native, static HTML, and OG image renderers now honor square grid cells. The 2.0 docs, agent-facing skill text, and `llms.txt` reference material document the new option, and a focused `cell-grid-square` example app exercises the behavior in the emulator.

apps/docs/public/2.0/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ Design guidance:
103103
`view_cast`, `view_profile`, `compose_cast`, `view_token`, `send_token`, `swap_token`.
104104
- Use `bar_chart` for ranked/comparative data (horizontal bars, 1-6 items).
105105
- Use `cell_grid` for game boards, pixel art, or color matrices (2-32 cols, 2-16 rows).
106-
Two mutually exclusive interaction modes: leave `select: "off"` (default) and bind
107-
`on.press` to a `submit` action so each press POSTs immediately (`inputs[name]` is
106+
Set `cellAspectRatio: "square"` when board cells must stay square across variable snap
107+
widths. Two mutually exclusive interaction modes: leave `select: "off"` (default) and
108+
bind `on.press` to a `submit` action so each press POSTs immediately (`inputs[name]` is
108109
`"row,col"`); OR set `select: "single"` / `"multiple"` for press-to-select with a
109110
visual ring and pair with a separate submit `button`. Don't combine `on.press` with a
110111
non-`off` `select``on.press` is ignored when `select` is on.

apps/docs/public/SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ Design guidance:
103103
`view_cast`, `view_profile`, `compose_cast`, `view_token`, `send_token`, `swap_token`.
104104
- Use `bar_chart` for ranked/comparative data (horizontal bars, 1-6 items).
105105
- Use `cell_grid` for game boards, pixel art, or color matrices (2-32 cols, 2-16 rows).
106-
Two mutually exclusive interaction modes: leave `select: "off"` (default) and bind
107-
`on.press` to a `submit` action so each press POSTs immediately (`inputs[name]` is
106+
Set `cellAspectRatio: "square"` when board cells must stay square across variable snap
107+
widths. Two mutually exclusive interaction modes: leave `select: "off"` (default) and
108+
bind `on.press` to a `submit` action so each press POSTs immediately (`inputs[name]` is
108109
`"row,col"`); OR set `select: "single"` / `"multiple"` for press-to-select with a
109110
visual ring and pair with a separate submit `button`. Don't combine `on.press` with a
110111
non-`off` `select``on.press` is ignored when `select` is on.

apps/docs/public/llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
9898
- `rows` (number, required, 2–16)
9999
- `cells` (array, required): sparse list of `{ row, col, color?: PaletteColor | #rrggbb, content?: string, value?: string (1–30 chars) }`. When `value` is set on a cell, that string is what's written to `inputs[name]` on press/select; otherwise `"row,col"` is used. Use `value` for grids with meaningful labels (calendar days, alphabet letters, region codes) so handlers don't have to reverse-lookup. Use the row/col fallback for true coordinate grids (minesweeper, tic-tac-toe, pixel art).
100100
- `gap` (optional): `"none"` (0px) | `"sm"` (1px) | `"md"` (2px) | `"lg"` (4px). Default: `"sm"`
101+
- `cellAspectRatio` (optional): `"auto"` | `"square"`. Default: `"auto"`. Use `"square"` for game boards whose cells must stay square as snap width changes.
101102
- `rowHeight` (number, optional, 8–64): pixel height per row. Default: 28. Grid height = rows × rowHeight
102103
- `select` (optional): `"off"` | `"single"` | `"multiple"`. Default: `"off"`. With `select: "off"`, bind `on.press` for press-to-act (each press writes the cell's `value` or `"row,col"` to `inputs[name]` and fires the action). With `"single"` / `"multiple"`, presses accumulate selection state and pair with a separate submit `button` (multi-select joins values with `|`); `on.press` is ignored.
103104
- Events: `press` — fires on cell press, only when `select: "off"`; `inputs[name]` is set to the pressed cell's `value` (or `"row,col"` fallback) before the bound action runs

apps/docs/src/app/(docs)/2.0/(spec)/constraints/page.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ validation to fail and the snap to not render.
3939
| `cell_grid` | `cells[].row` | 0 to `rows - 1` |
4040
| `cell_grid` | `cells[].col` | 0 to `cols - 1` |
4141
| `cell_grid` | `cells[].color`| Palette color name or `#rrggbb` hex |
42+
| `cell_grid` | `cellAspectRatio` | `"auto"` or `"square"` |
4243
| `cell_grid` | `rowHeight` | 8 to 64 |
4344

4445
## Structural Constraints

apps/docs/src/app/(docs)/2.0/(spec)/elements/page.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ the accumulating selection. The component ignores `on.press` whenever `select` i
446446
| `rows` | number | Yes | | Row count (2–16) |
447447
| `cells` | object[] | Yes | | Sparse cell definitions (see below) |
448448
| `gap` | string | No | `"sm"` | Cell spacing: `"none"` (0px) `"sm"` (1px) `"md"` (2px) `"lg"` (4px) |
449+
| `cellAspectRatio` | string | No | `"auto"` | Cell shape: `"auto"` uses `rowHeight`; `"square"` keeps each cell square based on column width |
449450
| `rowHeight` | number | No | `28` | Pixel height per row (8–64). Grid height = rows × rowHeight |
450451
| `select` | string | No | `"off"` | Selection mode: `"off"` (use with `on.press`) `"single"` `"multiple"` |
451452

@@ -500,6 +501,7 @@ without posting; pair with a `button` that submits when the user is done:
500501
{ "row": 1, "col": 1, "color": "green", "content": "X" },
501502
{ "row": 3, "col": 3, "color": "purple" }
502503
],
504+
"cellAspectRatio": "square",
503505
"select": "multiple"
504506
}
505507
}

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ Hono-based snap servers for local development and reference. Run from the repo r
1010
| `snap-expression` | 3013 | Dialogue-style snaps (CEO spectrum vote, fund explorer) |
1111
| `two-snaps` | 3016 / 3017 | Two Hono servers for testing cross-snap links in the emulator |
1212
| `item-media` | 3021 | Focused snap showing `item.props.media` with icon and image rows |
13+
| `cell-grid-square` | 3022 | Focused snap showing square `cell_grid` cells for board games |
1314

1415
Set `SNAP_PUBLIC_BASE_URL` when deploying so `post` / `link` targets resolve correctly.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "cell-grid-square",
3+
"private": true,
4+
"type": "module",
5+
"packageManager": "pnpm@9.15.4",
6+
"scripts": {
7+
"dev": "SKIP_JFS_VERIFICATION=true tsx watch src/server.ts",
8+
"build": "tsc --noEmit",
9+
"typecheck": "tsc --noEmit"
10+
},
11+
"dependencies": {
12+
"@farcaster/snap": "workspace:*",
13+
"@farcaster/snap-hono": "workspace:*",
14+
"@hono/node-server": "^1.19.13",
15+
"hono": "^4.0.0",
16+
"zod": "^4.0.0"
17+
},
18+
"devDependencies": {
19+
"@types/node": "^24.5.2",
20+
"tsx": "^4.0.0",
21+
"typescript": "^5.0.0"
22+
}
23+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { Hono } from "hono";
2+
import { registerSnapHandler } from "@farcaster/snap-hono";
3+
import type { SnapHandlerResult } from "@farcaster/snap";
4+
5+
const app = new Hono();
6+
7+
registerSnapHandler(app, async (ctx): Promise<SnapHandlerResult> => {
8+
const base = snapBaseUrlFromRequest(ctx.request);
9+
const pressed =
10+
ctx.action.type === "post" && typeof ctx.action.inputs.mine === "string"
11+
? ctx.action.inputs.mine
12+
: null;
13+
14+
return boardPage(base, pressed);
15+
});
16+
17+
export default app;
18+
19+
function boardPage(base: string, pressed: string | null): SnapHandlerResult {
20+
return {
21+
version: "2.0",
22+
theme: { accent: "green" },
23+
ui: {
24+
root: "page",
25+
elements: {
26+
page: {
27+
type: "stack",
28+
props: { gap: "sm" },
29+
children: ["title", "hint", "board", "pressed"],
30+
},
31+
title: {
32+
type: "item",
33+
props: {
34+
title: "Square cell grid",
35+
description: "A 9x9 board using cellAspectRatio: square.",
36+
media: { variant: "icon", name: "grid-3x3", color: "green" },
37+
},
38+
children: ["badge"],
39+
},
40+
badge: {
41+
type: "badge",
42+
props: { label: "2.0", color: "green" },
43+
},
44+
hint: {
45+
type: "text",
46+
props: {
47+
content: "Cells should remain square as the emulator width changes.",
48+
size: "sm",
49+
color: "muted",
50+
},
51+
},
52+
board: {
53+
type: "cell_grid",
54+
props: {
55+
name: "mine",
56+
cols: 9,
57+
rows: 9,
58+
gap: "sm",
59+
cellAspectRatio: "square",
60+
cells: mineCells(),
61+
},
62+
on: {
63+
press: {
64+
action: "submit",
65+
params: { target: `${base}/` },
66+
},
67+
},
68+
},
69+
pressed: {
70+
type: "text",
71+
props: {
72+
content: pressed ? `Last press: ${pressed}` : "Press a cell to submit coordinates.",
73+
size: "sm",
74+
align: "center",
75+
},
76+
},
77+
},
78+
},
79+
};
80+
}
81+
82+
function mineCells(): Array<{
83+
row: number;
84+
col: number;
85+
color?: string;
86+
content?: string;
87+
}> {
88+
const mines = new Set(["0,1", "1,3", "2,7", "4,4", "6,2", "7,6", "8,0"]);
89+
const numbers = new Map<string, string>([
90+
["0,0", "1"],
91+
["0,2", "1"],
92+
["1,1", "1"],
93+
["1,2", "1"],
94+
["2,3", "1"],
95+
["3,4", "1"],
96+
["4,3", "1"],
97+
["4,5", "1"],
98+
["5,2", "1"],
99+
["6,6", "1"],
100+
["7,0", "1"],
101+
]);
102+
103+
const cells = [];
104+
for (let row = 0; row < 9; row++) {
105+
for (let col = 0; col < 9; col++) {
106+
const key = `${row},${col}`;
107+
const isMine = mines.has(key);
108+
const content = isMine ? "*" : numbers.get(key);
109+
cells.push({
110+
row,
111+
col,
112+
color: isMine ? "#ef4444" : (row + col) % 2 === 0 ? "#d1fae5" : "#a7f3d0",
113+
...(content ? { content } : {}),
114+
});
115+
}
116+
}
117+
return cells;
118+
}
119+
120+
function snapBaseUrlFromRequest(request: Request): string {
121+
const fromEnv = process.env.SNAP_PUBLIC_BASE_URL?.trim();
122+
if (fromEnv) return fromEnv.replace(/\/$/, "");
123+
124+
const proto = request.headers.get("x-forwarded-proto")?.trim() || "https";
125+
const host =
126+
request.headers.get("x-forwarded-host")?.trim() ||
127+
request.headers.get("host")?.trim();
128+
if (host) return `${proto}://${host}`.replace(/\/$/, "");
129+
130+
return `http://localhost:${process.env.PORT ?? "3022"}`.replace(/\/$/, "");
131+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { serve } from "@hono/node-server";
2+
import app from "./index";
3+
4+
const port = Number(process.env.PORT ?? "3022");
5+
6+
serve({ fetch: app.fetch, port });
7+
8+
console.log(`running Cell Grid Square snap server on http://localhost:${port}`);

0 commit comments

Comments
 (0)