@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/dist/renderSnapPage.js
CHANGED
|
@@ -4,6 +4,9 @@ export function extractPageMeta(spec) {
|
|
|
4
4
|
let description = "";
|
|
5
5
|
let imageUrl;
|
|
6
6
|
let imageAlt;
|
|
7
|
+
// Fallbacks from text elements (lower priority than item)
|
|
8
|
+
let textTitle;
|
|
9
|
+
let textDescription;
|
|
7
10
|
for (const el of Object.values(spec.elements)) {
|
|
8
11
|
const e = el;
|
|
9
12
|
if (e.type === "item") {
|
|
@@ -14,11 +17,25 @@ export function extractPageMeta(spec) {
|
|
|
14
17
|
description = String(e.props.description);
|
|
15
18
|
}
|
|
16
19
|
}
|
|
20
|
+
if (e.type === "text" && e.props?.content) {
|
|
21
|
+
const content = String(e.props.content);
|
|
22
|
+
if (!textTitle && String(e.props.weight ?? "") === "bold") {
|
|
23
|
+
textTitle = content;
|
|
24
|
+
}
|
|
25
|
+
else if (!textDescription && String(e.props.weight ?? "") !== "bold") {
|
|
26
|
+
textDescription = content;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
17
29
|
if (e.type === "image" && !imageUrl) {
|
|
18
30
|
imageUrl = e.props?.url ? String(e.props.url) : undefined;
|
|
19
31
|
imageAlt = e.props?.alt ? String(e.props.alt) : undefined;
|
|
20
32
|
}
|
|
21
33
|
}
|
|
34
|
+
// Use text fallbacks if no item-derived values
|
|
35
|
+
if (title === "Farcaster Snap" && textTitle)
|
|
36
|
+
title = textTitle;
|
|
37
|
+
if (!description && textDescription)
|
|
38
|
+
description = textDescription;
|
|
22
39
|
return {
|
|
23
40
|
title,
|
|
24
41
|
description: description || title,
|
|
@@ -69,7 +86,58 @@ function colorHex(color, accent) {
|
|
|
69
86
|
return accent;
|
|
70
87
|
return PALETTE_LIGHT_HEX[color] ?? accent;
|
|
71
88
|
}
|
|
89
|
+
/** Readable foreground for a hex background (YIQ contrast check). */
|
|
90
|
+
function fgForBg(hex) {
|
|
91
|
+
const h = hex.replace(/^#/, "");
|
|
92
|
+
if (h.length !== 6)
|
|
93
|
+
return "#ffffff";
|
|
94
|
+
const r = Number.parseInt(h.slice(0, 2), 16);
|
|
95
|
+
const g = Number.parseInt(h.slice(2, 4), 16);
|
|
96
|
+
const b = Number.parseInt(h.slice(4, 6), 16);
|
|
97
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
98
|
+
return yiq >= 180 ? "#0a0a0a" : "#ffffff";
|
|
99
|
+
}
|
|
100
|
+
/** Lucide-style SVG paths for all snap icons. */
|
|
101
|
+
const ICON_SVGS = {
|
|
102
|
+
check: `<polyline points="20 6 9 17 4 12"/>`,
|
|
103
|
+
x: `<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>`,
|
|
104
|
+
heart: `<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>`,
|
|
105
|
+
star: `<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>`,
|
|
106
|
+
info: `<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>`,
|
|
107
|
+
"arrow-right": `<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>`,
|
|
108
|
+
"arrow-left": `<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>`,
|
|
109
|
+
"chevron-right": `<polyline points="9 18 15 12 9 6"/>`,
|
|
110
|
+
"external-link": `<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>`,
|
|
111
|
+
"alert-triangle": `<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>`,
|
|
112
|
+
clock: `<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>`,
|
|
113
|
+
"message-circle": `<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>`,
|
|
114
|
+
repeat: `<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>`,
|
|
115
|
+
share: `<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>`,
|
|
116
|
+
user: `<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>`,
|
|
117
|
+
users: `<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>`,
|
|
118
|
+
trophy: `<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/>`,
|
|
119
|
+
zap: `<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>`,
|
|
120
|
+
flame: `<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>`,
|
|
121
|
+
gift: `<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>`,
|
|
122
|
+
image: `<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>`,
|
|
123
|
+
play: `<polygon points="5 3 19 12 5 21 5 3"/>`,
|
|
124
|
+
pause: `<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>`,
|
|
125
|
+
wallet: `<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/>`,
|
|
126
|
+
coins: `<circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/>`,
|
|
127
|
+
plus: `<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>`,
|
|
128
|
+
minus: `<line x1="5" y1="12" x2="19" y2="12"/>`,
|
|
129
|
+
"refresh-cw": `<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>`,
|
|
130
|
+
bookmark: `<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>`,
|
|
131
|
+
"thumbs-up": `<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>`,
|
|
132
|
+
"thumbs-down": `<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/>`,
|
|
133
|
+
"trending-up": `<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>`,
|
|
134
|
+
"trending-down": `<polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/>`,
|
|
135
|
+
};
|
|
72
136
|
// ─── Element renderers ──────────────────────────────────
|
|
137
|
+
function renderIcon(name, size, color) {
|
|
138
|
+
const inner = ICON_SVGS[name] ?? `<circle cx="12" cy="12" r="4"/>`;
|
|
139
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:middle;flex-shrink:0">${inner}</svg>`;
|
|
140
|
+
}
|
|
73
141
|
function renderElement(key, spec, accent) {
|
|
74
142
|
const el = spec.elements[key];
|
|
75
143
|
if (!el)
|
|
@@ -79,48 +147,34 @@ function renderElement(key, spec, accent) {
|
|
|
79
147
|
case "icon": {
|
|
80
148
|
const color = colorHex(p.color, accent);
|
|
81
149
|
const size = String(p.size ?? "md") === "sm" ? 16 : 20;
|
|
82
|
-
// Simplified inline SVG for common icons; falls back to a circle for unknown
|
|
83
150
|
const name = String(p.name ?? "info");
|
|
84
|
-
|
|
85
|
-
check: `<polyline points="20 6 9 17 4 12"/>`,
|
|
86
|
-
x: `<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>`,
|
|
87
|
-
heart: `<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>`,
|
|
88
|
-
star: `<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>`,
|
|
89
|
-
info: `<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>`,
|
|
90
|
-
"arrow-right": `<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>`,
|
|
91
|
-
"chevron-right": `<polyline points="9 18 15 12 9 6"/>`,
|
|
92
|
-
"external-link": `<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>`,
|
|
93
|
-
zap: `<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>`,
|
|
94
|
-
user: `<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>`,
|
|
95
|
-
clock: `<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>`,
|
|
96
|
-
};
|
|
97
|
-
const inner = iconSvgs[name] ?? `<circle cx="12" cy="12" r="4"/>`;
|
|
98
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:middle">${inner}</svg>`;
|
|
151
|
+
return `<span style="display:inline-flex;align-items:center">${renderIcon(name, size, color)}</span>`;
|
|
99
152
|
}
|
|
100
153
|
case "badge": {
|
|
101
154
|
const color = colorHex(p.color, accent);
|
|
102
|
-
|
|
155
|
+
const badgeVariant = String(p.variant ?? "default");
|
|
156
|
+
const isFilled = badgeVariant === "default";
|
|
157
|
+
const fg = isFilled ? fgForBg(color) : color;
|
|
158
|
+
const bgStyle = isFilled ? `background:${color};color:${fg}` : `border:1px solid ${color};color:${color}`;
|
|
159
|
+
const iconName = p.icon ? String(p.icon) : undefined;
|
|
160
|
+
const iconHtml = iconName ? renderIcon(iconName, 12, fg) : "";
|
|
161
|
+
const gap = iconHtml ? "gap:4px;" : "";
|
|
162
|
+
return `<span style="display:inline-flex;align-items:center;${gap}padding:2px 10px;border-radius:9999px;font-size:12px;font-weight:500;line-height:1.5;${bgStyle}">${iconHtml}${esc(String(p.label ?? ""))}</span>`;
|
|
103
163
|
}
|
|
104
164
|
case "image": {
|
|
105
165
|
const url = esc(String(p.url ?? ""));
|
|
106
|
-
const aspect = String(p.aspect ?? "
|
|
166
|
+
const aspect = String(p.aspect ?? "1:1");
|
|
107
167
|
const [w, h] = aspect.split(":").map(Number);
|
|
108
|
-
const ratio = w && h ? `${w}/${h}` : "
|
|
109
|
-
return `<div style="aspect-ratio:${ratio};border-radius:8px;overflow:hidden;background:#F3F4F6"><img src="${url}" alt="${esc(String(p.alt ?? ""))}" style="width:100%;height:100%;object-fit:cover"></div>`;
|
|
168
|
+
const ratio = w && h ? `${w}/${h}` : "1/1";
|
|
169
|
+
return `<div style="flex:1;aspect-ratio:${ratio};border-radius:8px;overflow:hidden;background:#F3F4F6"><img src="${url}" alt="${esc(String(p.alt ?? ""))}" style="width:100%;height:100%;object-fit:cover"></div>`;
|
|
110
170
|
}
|
|
111
171
|
case "item": {
|
|
112
|
-
const variant = String(p.variant ?? "default");
|
|
113
|
-
const variantStyles = {
|
|
114
|
-
default: "",
|
|
115
|
-
outline: "border:1px solid #E5E7EB;border-radius:8px;padding:12px;",
|
|
116
|
-
muted: "background:#F9FAFB;border-radius:8px;padding:12px;",
|
|
117
|
-
};
|
|
118
172
|
const descHtml = p.description ? `<div style="font-size:13px;color:#6B7280;margin-top:2px">${esc(String(p.description))}</div>` : "";
|
|
119
173
|
const childIds = el.children ?? [];
|
|
120
174
|
const actionsHtml = childIds.length > 0
|
|
121
175
|
? `<div style="margin-left:auto;padding-left:12px;display:flex;align-items:center;gap:4px">${childIds.map((id) => renderElement(id, spec, accent)).join("")}</div>`
|
|
122
176
|
: "";
|
|
123
|
-
return `<div style="display:flex;align-items:flex-start
|
|
177
|
+
return `<div style="display:flex;align-items:flex-start;padding:6px 10px"><div style="flex:1;min-width:0"><div style="font-size:15px;font-weight:500;color:#111">${esc(String(p.title ?? ""))}</div>${descHtml}</div>${actionsHtml}</div>`;
|
|
124
178
|
}
|
|
125
179
|
case "item_group": {
|
|
126
180
|
const childIds = el.children ?? [];
|
|
@@ -141,10 +195,9 @@ function renderElement(key, spec, accent) {
|
|
|
141
195
|
case "progress": {
|
|
142
196
|
const value = Number(p.value ?? 0);
|
|
143
197
|
const max = Number(p.max ?? 100);
|
|
144
|
-
const color = accent;
|
|
145
198
|
const pct = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
|
146
|
-
const labelHtml = p.label ? `<div style="font-size:
|
|
147
|
-
return `<div>${labelHtml}<div style="height:
|
|
199
|
+
const labelHtml = p.label ? `<div style="font-size:12px;color:#6B7280;margin-bottom:4px">${esc(String(p.label))}</div>` : "";
|
|
200
|
+
return `<div style="display:flex;flex:1;flex-direction:column;gap:4px">${labelHtml}<div style="height:10px;background:#E5E7EB;border-radius:9999px;overflow:hidden"><div style="height:100%;width:${pct}%;background:${accent};border-radius:9999px;transition:width 0.3s"></div></div></div>`;
|
|
148
201
|
}
|
|
149
202
|
case "separator": {
|
|
150
203
|
const orientation = String(p.orientation ?? "horizontal");
|
|
@@ -156,72 +209,125 @@ function renderElement(key, spec, accent) {
|
|
|
156
209
|
const min = Number(p.min ?? 0);
|
|
157
210
|
const max = Number(p.max ?? 100);
|
|
158
211
|
const value = p.defaultValue !== undefined ? Number(p.defaultValue) : (min + max) / 2;
|
|
159
|
-
const labelHtml = p.label ? `<div style="font-size:13px;font-weight:500;color:#374151;margin-bottom:
|
|
160
|
-
return `<div>${labelHtml}<input type="range" min="${min}" max="${max}" value="${value}" disabled style="width:100%;accent-color:${accent};
|
|
212
|
+
const labelHtml = p.label ? `<div style="font-size:13px;font-weight:500;color:#374151;margin-bottom:6px">${esc(String(p.label))}</div>` : "";
|
|
213
|
+
return `<div style="display:flex;flex-direction:column;gap:6px">${labelHtml}<input type="range" min="${min}" max="${max}" value="${value}" disabled style="width:100%;height:10px;border-radius:9999px;accent-color:${accent};background:#E5E7EB;-webkit-appearance:none;appearance:none"></div>`;
|
|
161
214
|
}
|
|
162
215
|
case "switch": {
|
|
163
216
|
const checked = Boolean(p.defaultChecked);
|
|
164
217
|
const bg = checked ? accent : "#D1D5DB";
|
|
165
218
|
const tx = checked ? "20px" : "2px";
|
|
166
|
-
return `<div style="display:flex;align-items:center;justify-content:space-between"><span style="font-size:14px;color:#374151">${esc(String(p.label ?? ""))}</span><div style="width:44px;height:24px;background:${bg};border-radius:12px;position:relative;
|
|
219
|
+
return `<div style="display:flex;align-items:center;justify-content:space-between;gap:12px"><span style="font-size:14px;color:#374151">${esc(String(p.label ?? ""))}</span><div style="width:44px;height:24px;background:${bg};border-radius:12px;position:relative;transition:background 0.2s"><div style="width:20px;height:20px;background:#fff;border-radius:50%;position:absolute;top:2px;left:${tx};transition:left 0.2s;box-shadow:0 1px 3px rgba(0,0,0,0.2)"></div></div></div>`;
|
|
167
220
|
}
|
|
168
221
|
case "input": {
|
|
169
|
-
const labelHtml = p.label ? `<label style="display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:
|
|
170
|
-
return `<div>${labelHtml}<input type="text" placeholder="${esc(String(p.placeholder ?? ""))}"
|
|
222
|
+
const labelHtml = p.label ? `<label style="display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:6px">${esc(String(p.label))}</label>` : "";
|
|
223
|
+
return `<div style="display:flex;flex-direction:column;gap:6px">${labelHtml}<input type="text" placeholder="${esc(String(p.placeholder ?? ""))}" readonly style="width:100%;padding:10px 12px;border-radius:8px;border:1px solid #E5E7EB;background:#fff;font-size:14px;color:#374151;font-family:inherit;box-sizing:border-box"></div>`;
|
|
171
224
|
}
|
|
172
225
|
case "toggle_group": {
|
|
173
226
|
const options = Array.isArray(p.options) ? p.options : [];
|
|
174
227
|
const orientation = String(p.orientation ?? "horizontal");
|
|
175
228
|
const dir = orientation === "vertical" ? "column" : "row";
|
|
176
|
-
const
|
|
177
|
-
|
|
229
|
+
const defaultVal = p.defaultValue !== undefined ? String(p.defaultValue) : undefined;
|
|
230
|
+
const labelHtml = p.label ? `<div style="font-size:13px;font-weight:500;color:#374151;margin-bottom:6px">${esc(String(p.label))}</div>` : "";
|
|
231
|
+
let html = `<div>${labelHtml}<div style="display:flex;flex-direction:${dir};gap:4px;padding:4px;background:rgba(229,231,235,0.2);border-radius:8px">`;
|
|
178
232
|
for (const opt of options) {
|
|
179
|
-
|
|
233
|
+
const selected = defaultVal === opt;
|
|
234
|
+
const optBg = selected ? accent : "transparent";
|
|
235
|
+
const optColor = selected ? fgForBg(accent) : "#374151";
|
|
236
|
+
const optWeight = selected ? "600" : "500";
|
|
237
|
+
html += `<button onclick="showModal()" style="flex:1;padding:8px 12px;border-radius:6px;border:none;background:${optBg};font-size:13px;font-weight:${optWeight};color:${optColor};cursor:pointer;font-family:inherit;transition:background 0.15s,color 0.15s">${esc(opt)}</button>`;
|
|
180
238
|
}
|
|
181
239
|
html += `</div></div>`;
|
|
182
240
|
return html;
|
|
183
241
|
}
|
|
184
242
|
case "button": {
|
|
185
|
-
const variant = String(p.variant ?? "
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
243
|
+
const variant = String(p.variant ?? "secondary");
|
|
244
|
+
const isPrimary = variant === "primary";
|
|
245
|
+
const fg = isPrimary ? fgForBg(accent) : accent;
|
|
246
|
+
const bg = isPrimary ? accent : "transparent";
|
|
247
|
+
const border = isPrimary ? "none" : `2px solid ${accent}`;
|
|
248
|
+
const pad = isPrimary ? "14px 16px" : "10px 16px";
|
|
249
|
+
const minH = isPrimary ? "min-height:44px;" : "";
|
|
250
|
+
const iconName = p.icon ? String(p.icon) : undefined;
|
|
251
|
+
const iconHtml = iconName ? renderIcon(iconName, 16, fg) : "";
|
|
252
|
+
const gap = iconHtml ? "gap:8px;" : "";
|
|
253
|
+
return `<button onclick="showModal()" style="display:inline-flex;align-items:center;justify-content:center;${gap}width:100%;${minH}padding:${pad};border-radius:10px;background:${bg};color:${fg};border:${border};font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;box-sizing:border-box">${iconHtml}${esc(String(p.label ?? ""))}</button>`;
|
|
192
254
|
}
|
|
193
255
|
case "text": {
|
|
194
256
|
const size = String(p.size ?? "md");
|
|
195
|
-
const weight = String(p.weight ??
|
|
257
|
+
const weight = String(p.weight ?? "normal");
|
|
196
258
|
const align = String(p.align ?? "left");
|
|
197
259
|
const styles = {
|
|
198
|
-
lg: "font-size:20px",
|
|
199
260
|
md: "font-size:15px;line-height:1.5",
|
|
200
|
-
sm: "font-size:13px",
|
|
261
|
+
sm: "font-size:13px;line-height:1.5",
|
|
201
262
|
};
|
|
202
263
|
const weights = {
|
|
203
264
|
bold: "font-weight:700",
|
|
204
|
-
medium: "font-weight:500",
|
|
205
265
|
normal: "font-weight:400",
|
|
206
266
|
};
|
|
207
|
-
return `<div style="
|
|
267
|
+
return `<div style="flex:1;${styles[size] ?? styles.md};${weights[weight] ?? weights.normal};color:#374151;text-align:${align}">${esc(String(p.content ?? ""))}</div>`;
|
|
208
268
|
}
|
|
209
269
|
case "stack": {
|
|
210
270
|
const direction = String(p.direction ?? "vertical");
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const
|
|
271
|
+
const isHorizontal = direction === "horizontal";
|
|
272
|
+
const vGap = { none: "0", sm: "8px", md: "16px", lg: "24px" };
|
|
273
|
+
const hGap = { none: "0", sm: "4px", md: "8px", lg: "12px" };
|
|
274
|
+
const gapMap = isHorizontal ? hGap : vGap;
|
|
275
|
+
const gapVal = gapMap[String(p.gap ?? "md")] ?? (isHorizontal ? "8px" : "16px");
|
|
276
|
+
const dir = isHorizontal ? "row" : "column";
|
|
277
|
+
const wrap = isHorizontal ? "flex-wrap:wrap;" : "";
|
|
278
|
+
const align = isHorizontal ? "align-items:center;" : "";
|
|
214
279
|
const justifyMap = { start: "flex-start", center: "center", end: "flex-end", between: "space-between", around: "space-around" };
|
|
215
280
|
const jc = p.justify ? justifyMap[String(p.justify)] : undefined;
|
|
216
281
|
const childIds = el.children ?? [];
|
|
217
|
-
let html = `<div style="display:flex;flex-direction:${dir};gap:${gapVal}${jc ?
|
|
282
|
+
let html = `<div style="display:flex;width:100%;flex-direction:${dir};gap:${gapVal};${wrap}${align}${jc ? `justify-content:${jc};` : ""}">`;
|
|
218
283
|
for (const childKey of childIds) {
|
|
219
|
-
const flex =
|
|
284
|
+
const flex = isHorizontal ? "flex:1;min-width:0;" : "";
|
|
220
285
|
html += `<div style="${flex}">${renderElement(childKey, spec, accent)}</div>`;
|
|
221
286
|
}
|
|
222
287
|
html += `</div>`;
|
|
223
288
|
return html;
|
|
224
289
|
}
|
|
290
|
+
case "bar_chart": {
|
|
291
|
+
const bars = Array.isArray(p.bars) ? p.bars : [];
|
|
292
|
+
const chartColor = colorHex(p.color, accent);
|
|
293
|
+
const maxVal = p.max != null ? Number(p.max) : Math.max(...bars.map((b) => Number(b.value ?? 0)), 1);
|
|
294
|
+
let html = `<div style="display:flex;flex-direction:column;gap:8px;width:100%">`;
|
|
295
|
+
for (const bar of bars) {
|
|
296
|
+
const value = Number(bar.value ?? 0);
|
|
297
|
+
const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
|
|
298
|
+
const fill = bar.color ? colorHex(bar.color, accent) : chartColor;
|
|
299
|
+
html += `<div style="display:flex;align-items:center;gap:8px">`;
|
|
300
|
+
html += `<span style="width:80px;flex-shrink:0;text-align:right;font-size:12px;color:#6B7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(String(bar.label ?? ""))}</span>`;
|
|
301
|
+
html += `<div style="flex:1;height:10px;background:#E5E7EB;border-radius:9999px;overflow:hidden"><div style="height:100%;width:${pct}%;background:${fill};border-radius:9999px;transition:width 0.3s"></div></div>`;
|
|
302
|
+
html += `<span style="width:32px;flex-shrink:0;font-size:12px;color:#6B7280;font-variant-numeric:tabular-nums">${value}</span>`;
|
|
303
|
+
html += `</div>`;
|
|
304
|
+
}
|
|
305
|
+
html += `</div>`;
|
|
306
|
+
return html;
|
|
307
|
+
}
|
|
308
|
+
case "cell_grid": {
|
|
309
|
+
const cols = Number(p.cols ?? 2);
|
|
310
|
+
const rows = Number(p.rows ?? 2);
|
|
311
|
+
const cells = Array.isArray(p.cells) ? p.cells : [];
|
|
312
|
+
const gap = String(p.gap ?? "sm");
|
|
313
|
+
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
314
|
+
const gapPx = gapMap[gap] ?? 1;
|
|
315
|
+
const cellMap = new Map();
|
|
316
|
+
for (const c of cells) {
|
|
317
|
+
cellMap.set(`${Number(c.row ?? 0)},${Number(c.col ?? 0)}`, { color: c.color, content: c.content });
|
|
318
|
+
}
|
|
319
|
+
let html = `<div style="display:grid;grid-template-columns:repeat(${cols},minmax(0,1fr));gap:${gapPx}px;width:100%">`;
|
|
320
|
+
for (let r = 0; r < rows; r++) {
|
|
321
|
+
for (let c = 0; c < cols; c++) {
|
|
322
|
+
const cell = cellMap.get(`${r},${c}`);
|
|
323
|
+
const bg = cell?.color ? colorHex(cell.color, accent) : "transparent";
|
|
324
|
+
const content = cell?.content ? esc(cell.content) : "";
|
|
325
|
+
html += `<div style="min-height:28px;border:1px solid #E5E7EB;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:#374151;background:${bg}">${content}</div>`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
html += `</div>`;
|
|
329
|
+
return html;
|
|
330
|
+
}
|
|
225
331
|
default:
|
|
226
332
|
return "";
|
|
227
333
|
}
|
|
@@ -255,17 +361,18 @@ ${ogMeta}
|
|
|
255
361
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
256
362
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0A0A0A;min-height:100vh;display:flex;align-items:center;justify-content:center;flex-direction:column;padding:24px}
|
|
257
363
|
.card{background:#fff;border-radius:16px;max-width:420px;width:100%;padding:20px;box-shadow:0 4px 24px rgba(0,0,0,0.3)}
|
|
364
|
+
.card button:hover{filter:brightness(0.92)}
|
|
258
365
|
.foot{margin-top:16px;text-align:center}
|
|
259
|
-
.foot a{color
|
|
366
|
+
.foot a{color:${accent};text-decoration:none;font-size:13px;display:inline-flex;align-items:center;gap:6px}
|
|
260
367
|
.foot a:hover{opacity:.8}
|
|
261
368
|
.foot svg{width:14px;height:12px}
|
|
262
369
|
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);align-items:center;justify-content:center;z-index:99}
|
|
263
370
|
.modal-box{background:#1A1A2E;border-radius:16px;padding:32px;text-align:center;max-width:340px;width:90%}
|
|
264
|
-
.modal-box svg{width:40px;height:35px;color
|
|
371
|
+
.modal-box svg{width:40px;height:35px;color:${accent};margin-bottom:16px}
|
|
265
372
|
.modal-box h2{color:#FAFAFA;font-size:20px;margin-bottom:8px}
|
|
266
373
|
.modal-box p{color:#A1A1AA;font-size:14px;line-height:1.5;margin-bottom:24px}
|
|
267
374
|
.modal-box a{display:block;padding:12px;border-radius:10px;text-decoration:none;font-weight:600;font-size:15px;margin-bottom:12px}
|
|
268
|
-
.mb-primary{background
|
|
375
|
+
.mb-primary{background:${accent};color:${fgForBg(accent)}}
|
|
269
376
|
.mb-secondary{background:#1A1A2E;color:#FAFAFA;border:1px solid #2D2D44}
|
|
270
377
|
.modal-box a:hover{opacity:.85}
|
|
271
378
|
.modal-box button{background:none;border:none;color:#A1A1AA;cursor:pointer;font-size:13px;font-family:inherit}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farcaster/snap-hono",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Hono integration for Farcaster Snap servers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
30
30
|
"satori": "^0.10.0",
|
|
31
|
-
"@farcaster/snap": "1.
|
|
31
|
+
"@farcaster/snap": "1.13.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"hono": ">=4.0.0"
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Hono } from "hono";
|
|
2
2
|
import { cors } from "hono/cors";
|
|
3
3
|
import {
|
|
4
|
-
createDefaultDataStore,
|
|
5
4
|
MEDIA_TYPE,
|
|
6
5
|
type SnapFunction,
|
|
7
6
|
ACTION_TYPE_GET,
|
|
@@ -17,8 +16,6 @@ import {
|
|
|
17
16
|
type OgOptions,
|
|
18
17
|
} from "./og-image";
|
|
19
18
|
|
|
20
|
-
const defaultData = createDefaultDataStore();
|
|
21
|
-
|
|
22
19
|
export type SnapHandlerOptions = {
|
|
23
20
|
/**
|
|
24
21
|
* Route path to register GET and POST handlers on.
|
|
@@ -81,7 +78,6 @@ export function registerSnapHandler(
|
|
|
81
78
|
const snap = await snapFn({
|
|
82
79
|
action: { type: ACTION_TYPE_GET },
|
|
83
80
|
request: stripAuthHeaders(c.req.raw),
|
|
84
|
-
data: defaultData,
|
|
85
81
|
});
|
|
86
82
|
const snapJson = JSON.stringify(snap);
|
|
87
83
|
const etag = etagForPage(snapJson);
|
|
@@ -169,7 +165,6 @@ export function registerSnapHandler(
|
|
|
169
165
|
const response = await snapFn({
|
|
170
166
|
action: { type: ACTION_TYPE_GET },
|
|
171
167
|
request: c.req.raw,
|
|
172
|
-
data: defaultData,
|
|
173
168
|
});
|
|
174
169
|
|
|
175
170
|
return payloadToResponse(response, {
|
|
@@ -213,7 +208,6 @@ export function registerSnapHandler(
|
|
|
213
208
|
const response = await snapFn({
|
|
214
209
|
action: parsed.action,
|
|
215
210
|
request: raw,
|
|
216
|
-
data: defaultData,
|
|
217
211
|
});
|
|
218
212
|
|
|
219
213
|
return payloadToResponse(response, {
|
|
@@ -273,7 +267,6 @@ async function getFallbackHtml(
|
|
|
273
267
|
const snap = await snapFn({
|
|
274
268
|
action: { type: ACTION_TYPE_GET },
|
|
275
269
|
request: stripAuthHeaders(request),
|
|
276
|
-
data: defaultData,
|
|
277
270
|
});
|
|
278
271
|
return renderSnapPage(snap, origin, { ogImageUrl, resourcePath, siteName });
|
|
279
272
|
} catch {
|
|
@@ -291,7 +284,7 @@ function snapOriginFromRequest(request: Request): string {
|
|
|
291
284
|
request.headers.get("host")?.trim();
|
|
292
285
|
if (host) return `${proto}://${host}`.replace(/\/$/, "");
|
|
293
286
|
|
|
294
|
-
return "https://
|
|
287
|
+
return "https://docs.farcaster.xyz/snap";
|
|
295
288
|
}
|
|
296
289
|
|
|
297
290
|
function clientWantsSnapResponse(accept: string | undefined): boolean {
|