@farcaster/snap-hono 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,10 @@
1
1
  import { cors } from "hono/cors";
2
- import { createDefaultDataStore, MEDIA_TYPE, ACTION_TYPE_GET, } from "@farcaster/snap";
2
+ import { MEDIA_TYPE, ACTION_TYPE_GET, } from "@farcaster/snap";
3
3
  import { parseRequest } from "@farcaster/snap/server";
4
4
  import { brandedFallbackHtml } from "./fallback.js";
5
5
  import { payloadToResponse, snapHeaders } from "./payloadToResponse.js";
6
6
  import { renderSnapPage } from "./renderSnapPage.js";
7
7
  import { renderSnapPageToPng, renderWithDedup, etagForPage, } from "./og-image.js";
8
- const defaultData = createDefaultDataStore();
9
8
  /**
10
9
  * Register GET and POST snap handlers on `app` at `options.path` (default `/`).
11
10
  *
@@ -32,7 +31,6 @@ export function registerSnapHandler(app, snapFn, options = {}) {
32
31
  const snap = await snapFn({
33
32
  action: { type: ACTION_TYPE_GET },
34
33
  request: stripAuthHeaders(c.req.raw),
35
- data: defaultData,
36
34
  });
37
35
  const snapJson = JSON.stringify(snap);
38
36
  const etag = etagForPage(snapJson);
@@ -107,7 +105,6 @@ export function registerSnapHandler(app, snapFn, options = {}) {
107
105
  const response = await snapFn({
108
106
  action: { type: ACTION_TYPE_GET },
109
107
  request: c.req.raw,
110
- data: defaultData,
111
108
  });
112
109
  return payloadToResponse(response, {
113
110
  resourcePath,
@@ -142,7 +139,6 @@ export function registerSnapHandler(app, snapFn, options = {}) {
142
139
  const response = await snapFn({
143
140
  action: parsed.action,
144
141
  request: raw,
145
- data: defaultData,
146
142
  });
147
143
  return payloadToResponse(response, {
148
144
  resourcePath: resourcePathFromRequest(raw.url),
@@ -186,7 +182,6 @@ async function getFallbackHtml(request, snapFn, ogImageUrl) {
186
182
  const snap = await snapFn({
187
183
  action: { type: ACTION_TYPE_GET },
188
184
  request: stripAuthHeaders(request),
189
- data: defaultData,
190
185
  });
191
186
  return renderSnapPage(snap, origin, { ogImageUrl, resourcePath, siteName });
192
187
  }
@@ -203,7 +198,7 @@ function snapOriginFromRequest(request) {
203
198
  request.headers.get("host")?.trim();
204
199
  if (host)
205
200
  return `${proto}://${host}`.replace(/\/$/, "");
206
- return "https://snap.farcaster.xyz";
201
+ return "https://docs.farcaster.xyz/snap";
207
202
  }
208
203
  function clientWantsSnapResponse(accept) {
209
204
  if (!accept || accept.trim() === "")
package/dist/og-image.js CHANGED
@@ -169,37 +169,66 @@ function colorHex(color, accent) {
169
169
  return PALETTE_LIGHT_HEX[color] ?? accent;
170
170
  }
171
171
  function mapText(el) {
172
- const style = el.style;
172
+ const size = String(el.size ?? "md");
173
+ const weight = String(el.weight ?? "normal");
173
174
  const align = el.align ?? "left";
174
175
  let content = String(el.content ?? "");
175
- // Inter WOFF subset: normalize arrows / punctuation so glyphs don't substitute badly in Satori.
176
- if (style === "caption" || style === "body") {
177
- content = content
178
- .replace(/\u2192/g, "->")
179
- .replace(/\u2190/g, "<-")
180
- .replace(/\u27a1/gi, "->");
181
- }
182
- // Match `renderSnapPage` `renderText` (card HTML): title 20px #111, body/caption/list tones.
183
- const styleMap = {
184
- title: { fontSize: 20, fontWeight: 700, color: "#111111", lineHeight: 1.3 },
185
- body: { fontSize: 15, color: "#374151", lineHeight: 1.5 },
186
- caption: { fontSize: 13, color: "#9CA3AF", lineHeight: 1.4 },
187
- label: {
188
- fontSize: 13,
189
- fontWeight: 600,
190
- color: "#6B7280",
191
- textTransform: "uppercase",
192
- letterSpacing: "0.5px",
193
- },
176
+ content = content
177
+ .replace(/\u2192/g, "->")
178
+ .replace(/\u2190/g, "<-")
179
+ .replace(/\u27a1/gi, "->");
180
+ const sizeStyles = {
181
+ md: { fontSize: 15, lineHeight: 1.5 },
182
+ sm: { fontSize: 13, lineHeight: 1.5 },
183
+ };
184
+ const weightStyles = {
185
+ bold: { fontWeight: 700 },
186
+ normal: { fontWeight: 400 },
194
187
  };
195
- const ts = styleMap[style] ?? styleMap["body"];
196
188
  return h("div", {
197
189
  display: "flex",
198
190
  width: OG_CARD_INNER_WIDTH_PX,
191
+ color: "#374151",
199
192
  textAlign: align,
200
- ...ts,
193
+ ...(sizeStyles[size] ?? sizeStyles.md),
194
+ ...(weightStyles[weight] ?? weightStyles.normal),
201
195
  }, content);
202
196
  }
197
+ function mapItem(el) {
198
+ const title = String(el.title ?? "");
199
+ const description = el.description ? String(el.description) : undefined;
200
+ return h("div", { display: "flex", flexDirection: "column", gap: 2, padding: "6px 10px" }, h("div", { display: "flex", fontSize: 15, fontWeight: 500, color: "#111" }, title), description
201
+ ? h("div", { display: "flex", fontSize: 13, color: "#6B7280", lineHeight: 1.4 }, description)
202
+ : null);
203
+ }
204
+ function mapBadge(el, accent) {
205
+ const label = String(el.label ?? "");
206
+ const color = colorHex(el.color, accent);
207
+ const variant = String(el.variant ?? "default");
208
+ const isFilled = variant === "default";
209
+ const bg = isFilled ? color : "transparent";
210
+ const fg = isFilled ? "#fff" : color;
211
+ const border = isFilled ? undefined : `1px solid ${color}`;
212
+ return h("div", {
213
+ display: "flex",
214
+ alignItems: "center",
215
+ paddingTop: 2, paddingBottom: 2, paddingLeft: 10, paddingRight: 10,
216
+ borderRadius: 9999,
217
+ fontSize: 12,
218
+ fontWeight: 500,
219
+ backgroundColor: bg,
220
+ color: fg,
221
+ ...(border ? { border } : {}),
222
+ }, label);
223
+ }
224
+ function mapSeparator() {
225
+ return h("div", {
226
+ display: "flex",
227
+ height: 1,
228
+ backgroundColor: "#E5E7EB",
229
+ width: "100%",
230
+ });
231
+ }
203
232
  function mapImage(el, imageMap) {
204
233
  const url = el.url;
205
234
  const dataUri = imageMap.get(url);
@@ -332,49 +361,84 @@ function mapBarChart(el, accent) {
332
361
  const color = bar.color !== undefined && bar.color !== ""
333
362
  ? colorHex(bar.color, accent)
334
363
  : chartDefault;
335
- const pct = maxVal > 0 ? (bar.value / maxVal) * 100 : 0;
336
- return h("div", {
337
- display: "flex",
338
- flex: 1,
339
- flexDirection: "column",
340
- alignItems: "center",
341
- height: "100%",
342
- justifyContent: "flex-end",
343
- }, h("div", { display: "flex", fontSize: 11, color: "#6B7280", marginBottom: 4 }, String(bar.value)), h("div", {
344
- display: "flex",
345
- width: "100%",
346
- height: `${pct}%`,
347
- backgroundColor: color,
348
- borderRadius: "4px 4px 0 0",
349
- minHeight: 4,
350
- }), h("div", { display: "flex", fontSize: 11, color: "#9CA3AF", marginTop: 4 }, bar.label.slice(0, 12)));
364
+ const pct = maxVal > 0 ? Math.min(100, (bar.value / maxVal) * 100) : 0;
365
+ return h("div", { display: "flex", flexDirection: "row", alignItems: "center", gap: 8, width: OG_CARD_INNER_WIDTH_PX }, h("div", { display: "flex", width: 80, fontSize: 12, color: "#6B7280", justifyContent: "flex-end" }, bar.label.slice(0, 20)), h("div", { display: "flex", flex: 1, height: 10, backgroundColor: "#E5E7EB", borderRadius: 9999, overflow: "hidden" }, h("div", { display: "flex", height: 10, width: `${pct}%`, backgroundColor: color, borderRadius: 9999 })), h("div", { display: "flex", width: 32, fontSize: 12, color: "#6B7280" }, String(bar.value)));
351
366
  });
352
- return h("div", {
353
- display: "flex",
354
- flexDirection: "row",
355
- alignItems: "flex-end",
356
- gap: 12,
357
- height: 100,
358
- width: "100%",
359
- }, ...barNodes);
367
+ return h("div", { display: "flex", flexDirection: "column", gap: 8, width: OG_CARD_INNER_WIDTH_PX }, ...barNodes);
368
+ }
369
+ function mapCellGrid(el, accent) {
370
+ const cols = Number(el.cols ?? 2);
371
+ const rows = Number(el.rows ?? 2);
372
+ const cells = Array.isArray(el.cells) ? el.cells : [];
373
+ const gap = String(el.gap ?? "sm");
374
+ const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
375
+ const gapPx = gapMap[gap] ?? 1;
376
+ const cellW = Math.floor((OG_CARD_INNER_WIDTH_PX - (cols - 1) * gapPx) / cols);
377
+ const cellMap = new Map();
378
+ for (const c of cells) {
379
+ cellMap.set(`${Number(c.row ?? 0)},${Number(c.col ?? 0)}`, { color: c.color, content: c.content });
380
+ }
381
+ const rowNodes = [];
382
+ for (let r = 0; r < rows; r++) {
383
+ const cellNodes = [];
384
+ for (let c = 0; c < cols; c++) {
385
+ const cell = cellMap.get(`${r},${c}`);
386
+ const bg = cell?.color ? colorHex(cell.color, accent) : "#F3F4F6";
387
+ cellNodes.push(h("div", {
388
+ display: "flex", alignItems: "center", justifyContent: "center",
389
+ width: cellW, height: cellW > 28 ? 28 : cellW, borderRadius: 4,
390
+ backgroundColor: bg, border: "1px solid #E5E7EB",
391
+ fontSize: 10, fontWeight: 600, color: "#374151",
392
+ }, cell?.content ?? ""));
393
+ }
394
+ rowNodes.push(h("div", { display: "flex", flexDirection: "row", gap: gapPx }, ...cellNodes));
395
+ }
396
+ return h("div", { display: "flex", flexDirection: "column", gap: gapPx, width: OG_CARD_INNER_WIDTH_PX }, ...rowNodes);
360
397
  }
361
398
  function mapElement(el, accent, imageMap) {
362
399
  const type = el.type;
363
400
  switch (type) {
364
401
  case "text":
365
402
  return mapText(el);
403
+ case "item":
404
+ return mapItem(el);
405
+ case "badge":
406
+ return mapBadge(el, accent);
366
407
  case "image":
367
408
  return mapImage(el, imageMap);
409
+ case "separator":
368
410
  case "divider":
369
- return mapDivider();
411
+ return mapSeparator();
370
412
  case "progress":
371
413
  return mapProgress(el, accent);
372
- case "list":
373
- return mapList(el);
374
414
  case "toggle_group":
375
415
  return mapButtonGroup(el, accent);
416
+ case "input": {
417
+ const label = el.label ? String(el.label) : "";
418
+ const placeholder = el.placeholder ? String(el.placeholder) : "";
419
+ return h("div", { display: "flex", flexDirection: "column", gap: 6, width: OG_CARD_INNER_WIDTH_PX }, label ? h("div", { display: "flex", fontSize: 13, fontWeight: 500, color: "#374151" }, label) : null, h("div", {
420
+ display: "flex", padding: "10px 12px", borderRadius: 8,
421
+ border: "1px solid #E5E7EB", backgroundColor: "#fff",
422
+ fontSize: 14, color: "#9CA3AF",
423
+ }, placeholder || " "));
424
+ }
425
+ case "switch": {
426
+ const label = el.label ? String(el.label) : "";
427
+ const checked = Boolean(el.defaultChecked);
428
+ const bg = checked ? accent : "#D1D5DB";
429
+ return h("div", { display: "flex", alignItems: "center", justifyContent: "space-between", width: OG_CARD_INNER_WIDTH_PX }, h("div", { display: "flex", fontSize: 14, color: "#374151" }, label), h("div", { display: "flex", width: 44, height: 24, borderRadius: 12, backgroundColor: bg, position: "relative" }, h("div", { display: "flex", width: 20, height: 20, borderRadius: 10, backgroundColor: "#fff", position: "absolute", top: 2, left: checked ? 20 : 2 })));
430
+ }
431
+ case "slider": {
432
+ const label = el.label ? String(el.label) : "";
433
+ return h("div", { display: "flex", flexDirection: "column", gap: 6, width: OG_CARD_INNER_WIDTH_PX }, label ? h("div", { display: "flex", fontSize: 13, fontWeight: 500, color: "#374151" }, label) : null, h("div", { display: "flex", height: 10, backgroundColor: "#E5E7EB", borderRadius: 9999, width: "100%" }, h("div", { display: "flex", height: 10, width: "50%", backgroundColor: accent, borderRadius: 9999 })));
434
+ }
435
+ // Legacy types kept for backward compat with older specs
436
+ case "list":
437
+ return mapList(el);
376
438
  case "bar_chart":
377
439
  return mapBarChart(el, accent);
440
+ case "cell_grid":
441
+ return mapCellGrid(el, accent);
378
442
  case "group": {
379
443
  const children = el.children ?? [];
380
444
  const childNodes = children
@@ -388,8 +452,8 @@ function mapElement(el, accent, imageMap) {
388
452
  }
389
453
  function mapButton(btn, accent, i) {
390
454
  const label = String(btn.label ?? "");
391
- const style = btn.style ?? (i === 0 ? "primary" : "secondary");
392
- const isPrimary = style === "primary";
455
+ const variant = btn.variant ?? btn.style ?? "secondary";
456
+ const isPrimary = variant === "primary";
393
457
  // Primary CTA: generous vertical padding + minHeight so Satori/Yoga renders a tall tap target
394
458
  // (small padding deltas are easy to miss; flexBasis:0 rows can also under-measure height).
395
459
  const py = isPrimary ? 18 : 10;
@@ -425,21 +489,21 @@ function linesForWrappedText(charCount, innerWidthPx, avgCharPx) {
425
489
  return Math.max(1, Math.ceil((charCount * 1.12) / cpl));
426
490
  }
427
491
  function estimateTextHeight(el) {
428
- const style = el.style ?? "body";
492
+ const size = String(el.size ?? "md");
429
493
  const content = String(el.content ?? "");
430
494
  const w = OG_CARD_INNER_WIDTH_PX;
431
- switch (style) {
432
- case "title":
433
- return linesForWrappedText(content.length, w, 11) * 26;
434
- case "body":
435
- return linesForWrappedText(content.length, w, 7.5) * 23;
436
- case "caption":
437
- return linesForWrappedText(content.length, w, 7) * 18;
438
- case "label":
439
- return linesForWrappedText(content.length, w, 7) * 18;
440
- default:
441
- return linesForWrappedText(content.length, w, 7.5) * 23;
442
- }
495
+ if (size === "sm")
496
+ return linesForWrappedText(content.length, w, 7) * 20;
497
+ return linesForWrappedText(content.length, w, 7.5) * 23;
498
+ }
499
+ function estimateItemHeight(el) {
500
+ const title = String(el.title ?? "");
501
+ const desc = el.description ? String(el.description) : "";
502
+ const w = OG_CARD_INNER_WIDTH_PX;
503
+ let total = linesForWrappedText(title.length, w, 7.5) * 23 + 12;
504
+ if (desc)
505
+ total += linesForWrappedText(desc.length, w, 7) * 20;
506
+ return total;
443
507
  }
444
508
  function estimateImageHeight(el, imageMap) {
445
509
  const url = el.url;
@@ -499,18 +563,38 @@ function estimateElementHeight(el, imageMap) {
499
563
  switch (type) {
500
564
  case "text":
501
565
  return estimateTextHeight(el);
566
+ case "item":
567
+ return estimateItemHeight(el);
568
+ case "badge":
569
+ return 24;
502
570
  case "image":
503
571
  return estimateImageHeight(el, imageMap);
572
+ case "separator":
504
573
  case "divider":
505
574
  return 1;
506
575
  case "progress":
507
576
  return estimateProgressHeight(el);
577
+ case "input":
578
+ return (el.label ? 20 : 0) + 42;
579
+ case "switch":
580
+ return 28;
581
+ case "slider":
582
+ return (el.label ? 20 : 0) + 16;
508
583
  case "list":
509
584
  return estimateListHeight(el);
510
585
  case "toggle_group":
511
586
  return estimateButtonGroupHeight(el);
512
- case "bar_chart":
513
- return 100;
587
+ case "bar_chart": {
588
+ const bars = Array.isArray(el.bars) ? el.bars : [];
589
+ return Math.max(1, bars.length) * 26;
590
+ }
591
+ case "cell_grid": {
592
+ const rows = Number(el.rows ?? 2);
593
+ const gap = String(el.gap ?? "sm");
594
+ const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
595
+ const gapPx = gapMap[gap] ?? 1;
596
+ return rows * 28 + (rows - 1) * gapPx;
597
+ }
514
598
  case "group": {
515
599
  const children = el.children ?? [];
516
600
  if (children.length === 0)
@@ -557,19 +641,28 @@ function estimateDefaultOgHeight(elements, imageMap, buttons, buttonLayout) {
557
641
  return Math.min(OG_MAX_HEIGHT_PX, Math.max(OG_MIN_HEIGHT_PX, Math.ceil(outerH)));
558
642
  }
559
643
  // ─── Spec helpers ─────────────────────────────────────
560
- /** Walk the flat spec from root and collect top-level children as El objects for the OG renderer. */
644
+ /** Walk the flat spec from root, recursing into stack containers, and collect leaf elements as El objects. */
561
645
  function specToElementList(spec) {
646
+ function collect(keys) {
647
+ const result = [];
648
+ for (const key of keys) {
649
+ const el = spec.elements[key];
650
+ if (!el)
651
+ continue;
652
+ // Recurse into stack and item_group containers
653
+ if ((el.type === "stack" || el.type === "item_group") && el.children?.length) {
654
+ result.push(...collect(el.children));
655
+ }
656
+ else {
657
+ result.push({ type: el.type, ...el.props });
658
+ }
659
+ }
660
+ return result;
661
+ }
562
662
  const rootEl = spec.elements[spec.root];
563
663
  if (!rootEl?.children)
564
664
  return [];
565
- return rootEl.children
566
- .map((key) => {
567
- const el = spec.elements[key];
568
- if (!el)
569
- return null;
570
- return { type: el.type, ...el.props };
571
- })
572
- .filter((el) => el != null);
665
+ return collect(rootEl.children);
573
666
  }
574
667
  /** Extract button elements (type: "button") from the spec. */
575
668
  function specToButtons(spec) {