@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 +2 -7
- package/dist/og-image.js +168 -75
- package/dist/renderSnapPage.js +165 -58
- package/package.json +2 -2
- package/src/index.ts +1 -8
- package/src/og-image.ts +205 -82
- package/src/renderSnapPage.ts +167 -59
package/src/og-image.ts
CHANGED
|
@@ -255,42 +255,83 @@ function colorHex(color: string | undefined, accent: string): string {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
function mapText(el: El): VNode {
|
|
258
|
-
const
|
|
258
|
+
const size = String(el.size ?? "md");
|
|
259
|
+
const weight = String(el.weight ?? "normal");
|
|
259
260
|
const align = (el.align as string) ?? "left";
|
|
260
261
|
let content = String(el.content ?? "");
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
caption: { fontSize: 13, color: "#9CA3AF", lineHeight: 1.4 },
|
|
273
|
-
label: {
|
|
274
|
-
fontSize: 13,
|
|
275
|
-
fontWeight: 600,
|
|
276
|
-
color: "#6B7280",
|
|
277
|
-
textTransform: "uppercase",
|
|
278
|
-
letterSpacing: "0.5px",
|
|
279
|
-
},
|
|
262
|
+
content = content
|
|
263
|
+
.replace(/\u2192/g, "->")
|
|
264
|
+
.replace(/\u2190/g, "<-")
|
|
265
|
+
.replace(/\u27a1/gi, "->");
|
|
266
|
+
const sizeStyles: Record<string, Record<string, unknown>> = {
|
|
267
|
+
md: { fontSize: 15, lineHeight: 1.5 },
|
|
268
|
+
sm: { fontSize: 13, lineHeight: 1.5 },
|
|
269
|
+
};
|
|
270
|
+
const weightStyles: Record<string, Record<string, unknown>> = {
|
|
271
|
+
bold: { fontWeight: 700 },
|
|
272
|
+
normal: { fontWeight: 400 },
|
|
280
273
|
};
|
|
281
|
-
const ts = styleMap[style] ?? styleMap["body"]!;
|
|
282
274
|
return h(
|
|
283
275
|
"div",
|
|
284
276
|
{
|
|
285
277
|
display: "flex",
|
|
286
278
|
width: OG_CARD_INNER_WIDTH_PX,
|
|
279
|
+
color: "#374151",
|
|
287
280
|
textAlign: align,
|
|
288
|
-
...
|
|
281
|
+
...(sizeStyles[size] ?? sizeStyles.md),
|
|
282
|
+
...(weightStyles[weight] ?? weightStyles.normal),
|
|
289
283
|
},
|
|
290
284
|
content,
|
|
291
285
|
);
|
|
292
286
|
}
|
|
293
287
|
|
|
288
|
+
function mapItem(el: El): VNode {
|
|
289
|
+
const title = String(el.title ?? "");
|
|
290
|
+
const description = el.description ? String(el.description) : undefined;
|
|
291
|
+
return h(
|
|
292
|
+
"div",
|
|
293
|
+
{ display: "flex", flexDirection: "column", gap: 2, padding: "6px 10px" },
|
|
294
|
+
h("div", { display: "flex", fontSize: 15, fontWeight: 500, color: "#111" }, title),
|
|
295
|
+
description
|
|
296
|
+
? h("div", { display: "flex", fontSize: 13, color: "#6B7280", lineHeight: 1.4 }, description)
|
|
297
|
+
: null,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function mapBadge(el: El, accent: string): VNode {
|
|
302
|
+
const label = String(el.label ?? "");
|
|
303
|
+
const color = colorHex(el.color as string | undefined, accent);
|
|
304
|
+
const variant = String(el.variant ?? "default");
|
|
305
|
+
const isFilled = variant === "default";
|
|
306
|
+
const bg = isFilled ? color : "transparent";
|
|
307
|
+
const fg = isFilled ? "#fff" : color;
|
|
308
|
+
const border = isFilled ? undefined : `1px solid ${color}`;
|
|
309
|
+
return h(
|
|
310
|
+
"div",
|
|
311
|
+
{
|
|
312
|
+
display: "flex",
|
|
313
|
+
alignItems: "center",
|
|
314
|
+
paddingTop: 2, paddingBottom: 2, paddingLeft: 10, paddingRight: 10,
|
|
315
|
+
borderRadius: 9999,
|
|
316
|
+
fontSize: 12,
|
|
317
|
+
fontWeight: 500,
|
|
318
|
+
backgroundColor: bg,
|
|
319
|
+
color: fg,
|
|
320
|
+
...(border ? { border } : {}),
|
|
321
|
+
},
|
|
322
|
+
label,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function mapSeparator(): VNode {
|
|
327
|
+
return h("div", {
|
|
328
|
+
display: "flex",
|
|
329
|
+
height: 1,
|
|
330
|
+
backgroundColor: "#E5E7EB",
|
|
331
|
+
width: "100%",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
294
335
|
function mapImage(el: El, imageMap: Map<string, string>): VNode {
|
|
295
336
|
const url = el.url as string;
|
|
296
337
|
const dataUri = imageMap.get(url);
|
|
@@ -480,51 +521,60 @@ function mapBarChart(el: El, accent: string): VNode {
|
|
|
480
521
|
bar.color !== undefined && bar.color !== ""
|
|
481
522
|
? colorHex(bar.color as string, accent)
|
|
482
523
|
: chartDefault;
|
|
483
|
-
const pct = maxVal > 0 ? (bar.value / maxVal) * 100 : 0;
|
|
524
|
+
const pct = maxVal > 0 ? Math.min(100, (bar.value / maxVal) * 100) : 0;
|
|
484
525
|
return h(
|
|
485
526
|
"div",
|
|
486
|
-
{
|
|
487
|
-
|
|
488
|
-
flex: 1,
|
|
489
|
-
flexDirection: "column",
|
|
490
|
-
alignItems: "center",
|
|
491
|
-
height: "100%",
|
|
492
|
-
justifyContent: "flex-end",
|
|
493
|
-
},
|
|
494
|
-
h(
|
|
495
|
-
"div",
|
|
496
|
-
{ display: "flex", fontSize: 11, color: "#6B7280", marginBottom: 4 },
|
|
497
|
-
String(bar.value),
|
|
498
|
-
),
|
|
499
|
-
h("div", {
|
|
500
|
-
display: "flex",
|
|
501
|
-
width: "100%",
|
|
502
|
-
height: `${pct}%`,
|
|
503
|
-
backgroundColor: color,
|
|
504
|
-
borderRadius: "4px 4px 0 0",
|
|
505
|
-
minHeight: 4,
|
|
506
|
-
}),
|
|
527
|
+
{ display: "flex", flexDirection: "row", alignItems: "center", gap: 8, width: OG_CARD_INNER_WIDTH_PX },
|
|
528
|
+
h("div", { display: "flex", width: 80, fontSize: 12, color: "#6B7280", justifyContent: "flex-end" }, bar.label.slice(0, 20)),
|
|
507
529
|
h(
|
|
508
530
|
"div",
|
|
509
|
-
{ display: "flex",
|
|
510
|
-
|
|
531
|
+
{ display: "flex", flex: 1, height: 10, backgroundColor: "#E5E7EB", borderRadius: 9999, overflow: "hidden" },
|
|
532
|
+
h("div", { display: "flex", height: 10, width: `${pct}%`, backgroundColor: color, borderRadius: 9999 }),
|
|
511
533
|
),
|
|
534
|
+
h("div", { display: "flex", width: 32, fontSize: 12, color: "#6B7280" }, String(bar.value)),
|
|
512
535
|
);
|
|
513
536
|
});
|
|
514
537
|
return h(
|
|
515
538
|
"div",
|
|
516
|
-
{
|
|
517
|
-
display: "flex",
|
|
518
|
-
flexDirection: "row",
|
|
519
|
-
alignItems: "flex-end",
|
|
520
|
-
gap: 12,
|
|
521
|
-
height: 100,
|
|
522
|
-
width: "100%",
|
|
523
|
-
},
|
|
539
|
+
{ display: "flex", flexDirection: "column", gap: 8, width: OG_CARD_INNER_WIDTH_PX },
|
|
524
540
|
...barNodes,
|
|
525
541
|
);
|
|
526
542
|
}
|
|
527
543
|
|
|
544
|
+
function mapCellGrid(el: El, accent: string): VNode {
|
|
545
|
+
const cols = Number(el.cols ?? 2);
|
|
546
|
+
const rows = Number(el.rows ?? 2);
|
|
547
|
+
const cells = Array.isArray(el.cells) ? (el.cells as Array<{ row?: number; col?: number; color?: string; content?: string }>) : [];
|
|
548
|
+
const gap = String(el.gap ?? "sm");
|
|
549
|
+
const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
550
|
+
const gapPx = gapMap[gap] ?? 1;
|
|
551
|
+
const cellW = Math.floor((OG_CARD_INNER_WIDTH_PX - (cols - 1) * gapPx) / cols);
|
|
552
|
+
|
|
553
|
+
const cellMap = new Map<string, { color?: string; content?: string }>();
|
|
554
|
+
for (const c of cells) {
|
|
555
|
+
cellMap.set(`${Number(c.row ?? 0)},${Number(c.col ?? 0)}`, { color: c.color, content: c.content });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const rowNodes = [];
|
|
559
|
+
for (let r = 0; r < rows; r++) {
|
|
560
|
+
const cellNodes = [];
|
|
561
|
+
for (let c = 0; c < cols; c++) {
|
|
562
|
+
const cell = cellMap.get(`${r},${c}`);
|
|
563
|
+
const bg = cell?.color ? colorHex(cell.color, accent) : "#F3F4F6";
|
|
564
|
+
cellNodes.push(
|
|
565
|
+
h("div", {
|
|
566
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
567
|
+
width: cellW, height: cellW > 28 ? 28 : cellW, borderRadius: 4,
|
|
568
|
+
backgroundColor: bg, border: "1px solid #E5E7EB",
|
|
569
|
+
fontSize: 10, fontWeight: 600, color: "#374151",
|
|
570
|
+
}, cell?.content ?? ""),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
rowNodes.push(h("div", { display: "flex", flexDirection: "row", gap: gapPx }, ...cellNodes));
|
|
574
|
+
}
|
|
575
|
+
return h("div", { display: "flex", flexDirection: "column", gap: gapPx, width: OG_CARD_INNER_WIDTH_PX }, ...rowNodes);
|
|
576
|
+
}
|
|
577
|
+
|
|
528
578
|
function mapElement(
|
|
529
579
|
el: El,
|
|
530
580
|
accent: string,
|
|
@@ -534,18 +584,64 @@ function mapElement(
|
|
|
534
584
|
switch (type) {
|
|
535
585
|
case "text":
|
|
536
586
|
return mapText(el);
|
|
587
|
+
case "item":
|
|
588
|
+
return mapItem(el);
|
|
589
|
+
case "badge":
|
|
590
|
+
return mapBadge(el, accent);
|
|
537
591
|
case "image":
|
|
538
592
|
return mapImage(el, imageMap);
|
|
593
|
+
case "separator":
|
|
539
594
|
case "divider":
|
|
540
|
-
return
|
|
595
|
+
return mapSeparator();
|
|
541
596
|
case "progress":
|
|
542
597
|
return mapProgress(el, accent);
|
|
543
|
-
case "list":
|
|
544
|
-
return mapList(el);
|
|
545
598
|
case "toggle_group":
|
|
546
599
|
return mapButtonGroup(el, accent);
|
|
600
|
+
case "input": {
|
|
601
|
+
const label = el.label ? String(el.label) : "";
|
|
602
|
+
const placeholder = el.placeholder ? String(el.placeholder) : "";
|
|
603
|
+
return h(
|
|
604
|
+
"div",
|
|
605
|
+
{ display: "flex", flexDirection: "column", gap: 6, width: OG_CARD_INNER_WIDTH_PX },
|
|
606
|
+
label ? h("div", { display: "flex", fontSize: 13, fontWeight: 500, color: "#374151" }, label) : null,
|
|
607
|
+
h("div", {
|
|
608
|
+
display: "flex", padding: "10px 12px", borderRadius: 8,
|
|
609
|
+
border: "1px solid #E5E7EB", backgroundColor: "#fff",
|
|
610
|
+
fontSize: 14, color: "#9CA3AF",
|
|
611
|
+
}, placeholder || " "),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
case "switch": {
|
|
615
|
+
const label = el.label ? String(el.label) : "";
|
|
616
|
+
const checked = Boolean(el.defaultChecked);
|
|
617
|
+
const bg = checked ? accent : "#D1D5DB";
|
|
618
|
+
return h(
|
|
619
|
+
"div",
|
|
620
|
+
{ display: "flex", alignItems: "center", justifyContent: "space-between", width: OG_CARD_INNER_WIDTH_PX },
|
|
621
|
+
h("div", { display: "flex", fontSize: 14, color: "#374151" }, label),
|
|
622
|
+
h("div", { display: "flex", width: 44, height: 24, borderRadius: 12, backgroundColor: bg, position: "relative" },
|
|
623
|
+
h("div", { display: "flex", width: 20, height: 20, borderRadius: 10, backgroundColor: "#fff", position: "absolute", top: 2, left: checked ? 20 : 2 }),
|
|
624
|
+
),
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
case "slider": {
|
|
628
|
+
const label = el.label ? String(el.label) : "";
|
|
629
|
+
return h(
|
|
630
|
+
"div",
|
|
631
|
+
{ display: "flex", flexDirection: "column", gap: 6, width: OG_CARD_INNER_WIDTH_PX },
|
|
632
|
+
label ? h("div", { display: "flex", fontSize: 13, fontWeight: 500, color: "#374151" }, label) : null,
|
|
633
|
+
h("div", { display: "flex", height: 10, backgroundColor: "#E5E7EB", borderRadius: 9999, width: "100%" },
|
|
634
|
+
h("div", { display: "flex", height: 10, width: "50%", backgroundColor: accent, borderRadius: 9999 }),
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
// Legacy types kept for backward compat with older specs
|
|
639
|
+
case "list":
|
|
640
|
+
return mapList(el);
|
|
547
641
|
case "bar_chart":
|
|
548
642
|
return mapBarChart(el, accent);
|
|
643
|
+
case "cell_grid":
|
|
644
|
+
return mapCellGrid(el, accent);
|
|
549
645
|
case "group": {
|
|
550
646
|
const children = (el.children as El[]) ?? [];
|
|
551
647
|
const childNodes = children
|
|
@@ -564,8 +660,8 @@ function mapElement(
|
|
|
564
660
|
|
|
565
661
|
function mapButton(btn: El, accent: string, i: number): VNode {
|
|
566
662
|
const label = String(btn.label ?? "");
|
|
567
|
-
const
|
|
568
|
-
const isPrimary =
|
|
663
|
+
const variant = (btn.variant as string) ?? (btn.style as string) ?? "secondary";
|
|
664
|
+
const isPrimary = variant === "primary";
|
|
569
665
|
// Primary CTA: generous vertical padding + minHeight so Satori/Yoga renders a tall tap target
|
|
570
666
|
// (small padding deltas are easy to miss; flexBasis:0 rows can also under-measure height).
|
|
571
667
|
const py = isPrimary ? 18 : 10;
|
|
@@ -606,21 +702,20 @@ function linesForWrappedText(
|
|
|
606
702
|
}
|
|
607
703
|
|
|
608
704
|
function estimateTextHeight(el: El): number {
|
|
609
|
-
const
|
|
705
|
+
const size = String(el.size ?? "md");
|
|
610
706
|
const content = String(el.content ?? "");
|
|
611
707
|
const w = OG_CARD_INNER_WIDTH_PX;
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
708
|
+
if (size === "sm") return linesForWrappedText(content.length, w, 7) * 20;
|
|
709
|
+
return linesForWrappedText(content.length, w, 7.5) * 23;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function estimateItemHeight(el: El): number {
|
|
713
|
+
const title = String(el.title ?? "");
|
|
714
|
+
const desc = el.description ? String(el.description) : "";
|
|
715
|
+
const w = OG_CARD_INNER_WIDTH_PX;
|
|
716
|
+
let total = linesForWrappedText(title.length, w, 7.5) * 23 + 12;
|
|
717
|
+
if (desc) total += linesForWrappedText(desc.length, w, 7) * 20;
|
|
718
|
+
return total;
|
|
624
719
|
}
|
|
625
720
|
|
|
626
721
|
function estimateImageHeight(el: El, imageMap: Map<string, string>): number {
|
|
@@ -688,18 +783,38 @@ function estimateElementHeight(el: El, imageMap: Map<string, string>): number {
|
|
|
688
783
|
switch (type) {
|
|
689
784
|
case "text":
|
|
690
785
|
return estimateTextHeight(el);
|
|
786
|
+
case "item":
|
|
787
|
+
return estimateItemHeight(el);
|
|
788
|
+
case "badge":
|
|
789
|
+
return 24;
|
|
691
790
|
case "image":
|
|
692
791
|
return estimateImageHeight(el, imageMap);
|
|
792
|
+
case "separator":
|
|
693
793
|
case "divider":
|
|
694
794
|
return 1;
|
|
695
795
|
case "progress":
|
|
696
796
|
return estimateProgressHeight(el);
|
|
797
|
+
case "input":
|
|
798
|
+
return (el.label ? 20 : 0) + 42;
|
|
799
|
+
case "switch":
|
|
800
|
+
return 28;
|
|
801
|
+
case "slider":
|
|
802
|
+
return (el.label ? 20 : 0) + 16;
|
|
697
803
|
case "list":
|
|
698
804
|
return estimateListHeight(el);
|
|
699
805
|
case "toggle_group":
|
|
700
806
|
return estimateButtonGroupHeight(el);
|
|
701
|
-
case "bar_chart":
|
|
702
|
-
|
|
807
|
+
case "bar_chart": {
|
|
808
|
+
const bars = Array.isArray(el.bars) ? el.bars : [];
|
|
809
|
+
return Math.max(1, bars.length) * 26;
|
|
810
|
+
}
|
|
811
|
+
case "cell_grid": {
|
|
812
|
+
const rows = Number(el.rows ?? 2);
|
|
813
|
+
const gap = String(el.gap ?? "sm");
|
|
814
|
+
const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
815
|
+
const gapPx = gapMap[gap] ?? 1;
|
|
816
|
+
return rows * 28 + (rows - 1) * gapPx;
|
|
817
|
+
}
|
|
703
818
|
case "group": {
|
|
704
819
|
const children = (el.children as El[]) ?? [];
|
|
705
820
|
if (children.length === 0) return 0;
|
|
@@ -767,17 +882,25 @@ function estimateDefaultOgHeight(
|
|
|
767
882
|
|
|
768
883
|
// ─── Spec helpers ─────────────────────────────────────
|
|
769
884
|
|
|
770
|
-
/** Walk the flat spec from root and collect
|
|
885
|
+
/** Walk the flat spec from root, recursing into stack containers, and collect leaf elements as El objects. */
|
|
771
886
|
function specToElementList(spec: SnapSpec): El[] {
|
|
887
|
+
function collect(keys: string[]): El[] {
|
|
888
|
+
const result: El[] = [];
|
|
889
|
+
for (const key of keys) {
|
|
890
|
+
const el = spec.elements[key];
|
|
891
|
+
if (!el) continue;
|
|
892
|
+
// Recurse into stack and item_group containers
|
|
893
|
+
if ((el.type === "stack" || el.type === "item_group") && el.children?.length) {
|
|
894
|
+
result.push(...collect(el.children));
|
|
895
|
+
} else {
|
|
896
|
+
result.push({ type: el.type, ...el.props } as El);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return result;
|
|
900
|
+
}
|
|
772
901
|
const rootEl = spec.elements[spec.root];
|
|
773
902
|
if (!rootEl?.children) return [];
|
|
774
|
-
return rootEl.children
|
|
775
|
-
.map((key) => {
|
|
776
|
-
const el = spec.elements[key];
|
|
777
|
-
if (!el) return null;
|
|
778
|
-
return { type: el.type, ...el.props } as El;
|
|
779
|
-
})
|
|
780
|
-
.filter((el): el is El => el != null);
|
|
903
|
+
return collect(rootEl.children);
|
|
781
904
|
}
|
|
782
905
|
|
|
783
906
|
/** Extract button elements (type: "button") from the spec. */
|