@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/renderSnapPage.ts
CHANGED
|
@@ -31,6 +31,10 @@ export function extractPageMeta(spec: SnapSpec): PageMeta {
|
|
|
31
31
|
let imageUrl: string | undefined;
|
|
32
32
|
let imageAlt: string | undefined;
|
|
33
33
|
|
|
34
|
+
// Fallbacks from text elements (lower priority than item)
|
|
35
|
+
let textTitle: string | undefined;
|
|
36
|
+
let textDescription: string | undefined;
|
|
37
|
+
|
|
34
38
|
for (const el of Object.values(spec.elements)) {
|
|
35
39
|
const e = el as SnapUIElement;
|
|
36
40
|
if (e.type === "item") {
|
|
@@ -41,12 +45,24 @@ export function extractPageMeta(spec: SnapSpec): PageMeta {
|
|
|
41
45
|
description = String(e.props.description);
|
|
42
46
|
}
|
|
43
47
|
}
|
|
48
|
+
if (e.type === "text" && e.props?.content) {
|
|
49
|
+
const content = String(e.props.content);
|
|
50
|
+
if (!textTitle && String(e.props.weight ?? "") === "bold") {
|
|
51
|
+
textTitle = content;
|
|
52
|
+
} else if (!textDescription && String(e.props.weight ?? "") !== "bold") {
|
|
53
|
+
textDescription = content;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
44
56
|
if (e.type === "image" && !imageUrl) {
|
|
45
57
|
imageUrl = e.props?.url ? String(e.props.url) : undefined;
|
|
46
58
|
imageAlt = e.props?.alt ? String(e.props.alt) : undefined;
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
// Use text fallbacks if no item-derived values
|
|
63
|
+
if (title === "Farcaster Snap" && textTitle) title = textTitle;
|
|
64
|
+
if (!description && textDescription) description = textDescription;
|
|
65
|
+
|
|
50
66
|
return {
|
|
51
67
|
title,
|
|
52
68
|
description: description || title,
|
|
@@ -118,8 +134,61 @@ function colorHex(
|
|
|
118
134
|
return (PALETTE_LIGHT_HEX as Record<string, string>)[color] ?? accent;
|
|
119
135
|
}
|
|
120
136
|
|
|
137
|
+
/** Readable foreground for a hex background (YIQ contrast check). */
|
|
138
|
+
function fgForBg(hex: string): string {
|
|
139
|
+
const h = hex.replace(/^#/, "");
|
|
140
|
+
if (h.length !== 6) return "#ffffff";
|
|
141
|
+
const r = Number.parseInt(h.slice(0, 2), 16);
|
|
142
|
+
const g = Number.parseInt(h.slice(2, 4), 16);
|
|
143
|
+
const b = Number.parseInt(h.slice(4, 6), 16);
|
|
144
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
145
|
+
return yiq >= 180 ? "#0a0a0a" : "#ffffff";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Lucide-style SVG paths for all snap icons. */
|
|
149
|
+
const ICON_SVGS: Record<string, string> = {
|
|
150
|
+
check: `<polyline points="20 6 9 17 4 12"/>`,
|
|
151
|
+
x: `<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>`,
|
|
152
|
+
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"/>`,
|
|
153
|
+
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"/>`,
|
|
154
|
+
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"/>`,
|
|
155
|
+
"arrow-right": `<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>`,
|
|
156
|
+
"arrow-left": `<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>`,
|
|
157
|
+
"chevron-right": `<polyline points="9 18 15 12 9 6"/>`,
|
|
158
|
+
"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"/>`,
|
|
159
|
+
"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"/>`,
|
|
160
|
+
clock: `<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>`,
|
|
161
|
+
"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"/>`,
|
|
162
|
+
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"/>`,
|
|
163
|
+
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"/>`,
|
|
164
|
+
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"/>`,
|
|
165
|
+
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"/>`,
|
|
166
|
+
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"/>`,
|
|
167
|
+
zap: `<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>`,
|
|
168
|
+
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"/>`,
|
|
169
|
+
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"/>`,
|
|
170
|
+
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"/>`,
|
|
171
|
+
play: `<polygon points="5 3 19 12 5 21 5 3"/>`,
|
|
172
|
+
pause: `<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>`,
|
|
173
|
+
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"/>`,
|
|
174
|
+
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"/>`,
|
|
175
|
+
plus: `<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>`,
|
|
176
|
+
minus: `<line x1="5" y1="12" x2="19" y2="12"/>`,
|
|
177
|
+
"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"/>`,
|
|
178
|
+
bookmark: `<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>`,
|
|
179
|
+
"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"/>`,
|
|
180
|
+
"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"/>`,
|
|
181
|
+
"trending-up": `<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>`,
|
|
182
|
+
"trending-down": `<polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/>`,
|
|
183
|
+
};
|
|
184
|
+
|
|
121
185
|
// ─── Element renderers ──────────────────────────────────
|
|
122
186
|
|
|
187
|
+
function renderIcon(name: string, size: number, color: string): string {
|
|
188
|
+
const inner = ICON_SVGS[name] ?? `<circle cx="12" cy="12" r="4"/>`;
|
|
189
|
+
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>`;
|
|
190
|
+
}
|
|
191
|
+
|
|
123
192
|
function renderElement(
|
|
124
193
|
key: string,
|
|
125
194
|
spec: SnapSpec,
|
|
@@ -133,48 +202,34 @@ function renderElement(
|
|
|
133
202
|
case "icon": {
|
|
134
203
|
const color = colorHex(p.color as string | undefined, accent);
|
|
135
204
|
const size = String(p.size ?? "md") === "sm" ? 16 : 20;
|
|
136
|
-
// Simplified inline SVG for common icons; falls back to a circle for unknown
|
|
137
205
|
const name = String(p.name ?? "info");
|
|
138
|
-
|
|
139
|
-
check: `<polyline points="20 6 9 17 4 12"/>`,
|
|
140
|
-
x: `<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>`,
|
|
141
|
-
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"/>`,
|
|
142
|
-
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"/>`,
|
|
143
|
-
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"/>`,
|
|
144
|
-
"arrow-right": `<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>`,
|
|
145
|
-
"chevron-right": `<polyline points="9 18 15 12 9 6"/>`,
|
|
146
|
-
"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"/>`,
|
|
147
|
-
zap: `<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>`,
|
|
148
|
-
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"/>`,
|
|
149
|
-
clock: `<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>`,
|
|
150
|
-
};
|
|
151
|
-
const inner = iconSvgs[name] ?? `<circle cx="12" cy="12" r="4"/>`;
|
|
152
|
-
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>`;
|
|
206
|
+
return `<span style="display:inline-flex;align-items:center">${renderIcon(name, size, color)}</span>`;
|
|
153
207
|
}
|
|
154
208
|
case "badge": {
|
|
155
209
|
const color = colorHex(p.color as string | undefined, accent);
|
|
156
|
-
|
|
210
|
+
const badgeVariant = String(p.variant ?? "default");
|
|
211
|
+
const isFilled = badgeVariant === "default";
|
|
212
|
+
const fg = isFilled ? fgForBg(color) : color;
|
|
213
|
+
const bgStyle = isFilled ? `background:${color};color:${fg}` : `border:1px solid ${color};color:${color}`;
|
|
214
|
+
const iconName = p.icon ? String(p.icon) : undefined;
|
|
215
|
+
const iconHtml = iconName ? renderIcon(iconName, 12, fg) : "";
|
|
216
|
+
const gap = iconHtml ? "gap:4px;" : "";
|
|
217
|
+
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>`;
|
|
157
218
|
}
|
|
158
219
|
case "image": {
|
|
159
220
|
const url = esc(String(p.url ?? ""));
|
|
160
|
-
const aspect = String(p.aspect ?? "
|
|
221
|
+
const aspect = String(p.aspect ?? "1:1");
|
|
161
222
|
const [w, h] = aspect.split(":").map(Number);
|
|
162
|
-
const ratio = w && h ? `${w}/${h}` : "
|
|
163
|
-
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>`;
|
|
223
|
+
const ratio = w && h ? `${w}/${h}` : "1/1";
|
|
224
|
+
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>`;
|
|
164
225
|
}
|
|
165
226
|
case "item": {
|
|
166
|
-
const variant = String(p.variant ?? "default");
|
|
167
|
-
const variantStyles: Record<string, string> = {
|
|
168
|
-
default: "",
|
|
169
|
-
outline: "border:1px solid #E5E7EB;border-radius:8px;padding:12px;",
|
|
170
|
-
muted: "background:#F9FAFB;border-radius:8px;padding:12px;",
|
|
171
|
-
};
|
|
172
227
|
const descHtml = p.description ? `<div style="font-size:13px;color:#6B7280;margin-top:2px">${esc(String(p.description))}</div>` : "";
|
|
173
228
|
const childIds = el.children ?? [];
|
|
174
229
|
const actionsHtml = childIds.length > 0
|
|
175
230
|
? `<div style="margin-left:auto;padding-left:12px;display:flex;align-items:center;gap:4px">${childIds.map((id) => renderElement(id, spec, accent)).join("")}</div>`
|
|
176
231
|
: "";
|
|
177
|
-
return `<div style="display:flex;align-items:flex-start
|
|
232
|
+
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>`;
|
|
178
233
|
}
|
|
179
234
|
case "item_group": {
|
|
180
235
|
const childIds = el.children ?? [];
|
|
@@ -195,10 +250,9 @@ function renderElement(
|
|
|
195
250
|
case "progress": {
|
|
196
251
|
const value = Number(p.value ?? 0);
|
|
197
252
|
const max = Number(p.max ?? 100);
|
|
198
|
-
const color = accent;
|
|
199
253
|
const pct = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
|
200
|
-
const labelHtml = p.label ? `<div style="font-size:
|
|
201
|
-
return `<div>${labelHtml}<div style="height:
|
|
254
|
+
const labelHtml = p.label ? `<div style="font-size:12px;color:#6B7280;margin-bottom:4px">${esc(String(p.label))}</div>` : "";
|
|
255
|
+
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>`;
|
|
202
256
|
}
|
|
203
257
|
case "separator": {
|
|
204
258
|
const orientation = String(p.orientation ?? "horizontal");
|
|
@@ -209,72 +263,125 @@ function renderElement(
|
|
|
209
263
|
const min = Number(p.min ?? 0);
|
|
210
264
|
const max = Number(p.max ?? 100);
|
|
211
265
|
const value = p.defaultValue !== undefined ? Number(p.defaultValue) : (min + max) / 2;
|
|
212
|
-
const labelHtml = p.label ? `<div style="font-size:13px;font-weight:500;color:#374151;margin-bottom:
|
|
213
|
-
return `<div>${labelHtml}<input type="range" min="${min}" max="${max}" value="${value}" disabled style="width:100%;accent-color:${accent};
|
|
266
|
+
const labelHtml = p.label ? `<div style="font-size:13px;font-weight:500;color:#374151;margin-bottom:6px">${esc(String(p.label))}</div>` : "";
|
|
267
|
+
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>`;
|
|
214
268
|
}
|
|
215
269
|
case "switch": {
|
|
216
270
|
const checked = Boolean(p.defaultChecked);
|
|
217
271
|
const bg = checked ? accent : "#D1D5DB";
|
|
218
272
|
const tx = checked ? "20px" : "2px";
|
|
219
|
-
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;
|
|
273
|
+
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>`;
|
|
220
274
|
}
|
|
221
275
|
case "input": {
|
|
222
|
-
const labelHtml = p.label ? `<label style="display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:
|
|
223
|
-
return `<div>${labelHtml}<input type="text" placeholder="${esc(String(p.placeholder ?? ""))}"
|
|
276
|
+
const labelHtml = p.label ? `<label style="display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:6px">${esc(String(p.label))}</label>` : "";
|
|
277
|
+
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>`;
|
|
224
278
|
}
|
|
225
279
|
case "toggle_group": {
|
|
226
|
-
const options = Array.isArray(p.options) ? p.options as string[] : [];
|
|
280
|
+
const options = Array.isArray(p.options) ? (p.options as string[]) : [];
|
|
227
281
|
const orientation = String(p.orientation ?? "horizontal");
|
|
228
282
|
const dir = orientation === "vertical" ? "column" : "row";
|
|
229
|
-
const
|
|
230
|
-
|
|
283
|
+
const defaultVal = p.defaultValue !== undefined ? String(p.defaultValue) : undefined;
|
|
284
|
+
const labelHtml = p.label ? `<div style="font-size:13px;font-weight:500;color:#374151;margin-bottom:6px">${esc(String(p.label))}</div>` : "";
|
|
285
|
+
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">`;
|
|
231
286
|
for (const opt of options) {
|
|
232
|
-
|
|
287
|
+
const selected = defaultVal === opt;
|
|
288
|
+
const optBg = selected ? accent : "transparent";
|
|
289
|
+
const optColor = selected ? fgForBg(accent) : "#374151";
|
|
290
|
+
const optWeight = selected ? "600" : "500";
|
|
291
|
+
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>`;
|
|
233
292
|
}
|
|
234
293
|
html += `</div></div>`;
|
|
235
294
|
return html;
|
|
236
295
|
}
|
|
237
296
|
case "button": {
|
|
238
|
-
const variant = String(p.variant ?? "
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
297
|
+
const variant = String(p.variant ?? "secondary");
|
|
298
|
+
const isPrimary = variant === "primary";
|
|
299
|
+
const fg = isPrimary ? fgForBg(accent) : accent;
|
|
300
|
+
const bg = isPrimary ? accent : "transparent";
|
|
301
|
+
const border = isPrimary ? "none" : `2px solid ${accent}`;
|
|
302
|
+
const pad = isPrimary ? "14px 16px" : "10px 16px";
|
|
303
|
+
const minH = isPrimary ? "min-height:44px;" : "";
|
|
304
|
+
const iconName = p.icon ? String(p.icon) : undefined;
|
|
305
|
+
const iconHtml = iconName ? renderIcon(iconName, 16, fg) : "";
|
|
306
|
+
const gap = iconHtml ? "gap:8px;" : "";
|
|
307
|
+
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>`;
|
|
245
308
|
}
|
|
246
309
|
case "text": {
|
|
247
310
|
const size = String(p.size ?? "md");
|
|
248
|
-
const weight = String(p.weight ??
|
|
311
|
+
const weight = String(p.weight ?? "normal");
|
|
249
312
|
const align = String(p.align ?? "left");
|
|
250
313
|
const styles: Record<string, string> = {
|
|
251
|
-
lg: "font-size:20px",
|
|
252
314
|
md: "font-size:15px;line-height:1.5",
|
|
253
|
-
sm: "font-size:13px",
|
|
315
|
+
sm: "font-size:13px;line-height:1.5",
|
|
254
316
|
};
|
|
255
317
|
const weights: Record<string, string> = {
|
|
256
318
|
bold: "font-weight:700",
|
|
257
|
-
medium: "font-weight:500",
|
|
258
319
|
normal: "font-weight:400",
|
|
259
320
|
};
|
|
260
|
-
return `<div style="
|
|
321
|
+
return `<div style="flex:1;${styles[size] ?? styles.md};${weights[weight] ?? weights.normal};color:#374151;text-align:${align}">${esc(String(p.content ?? ""))}</div>`;
|
|
261
322
|
}
|
|
262
323
|
case "stack": {
|
|
263
324
|
const direction = String(p.direction ?? "vertical");
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const
|
|
325
|
+
const isHorizontal = direction === "horizontal";
|
|
326
|
+
const vGap: Record<string, string> = { none: "0", sm: "8px", md: "16px", lg: "24px" };
|
|
327
|
+
const hGap: Record<string, string> = { none: "0", sm: "4px", md: "8px", lg: "12px" };
|
|
328
|
+
const gapMap = isHorizontal ? hGap : vGap;
|
|
329
|
+
const gapVal = gapMap[String(p.gap ?? "md")] ?? (isHorizontal ? "8px" : "16px");
|
|
330
|
+
const dir = isHorizontal ? "row" : "column";
|
|
331
|
+
const wrap = isHorizontal ? "flex-wrap:wrap;" : "";
|
|
332
|
+
const align = isHorizontal ? "align-items:center;" : "";
|
|
267
333
|
const justifyMap: Record<string, string> = { start: "flex-start", center: "center", end: "flex-end", between: "space-between", around: "space-around" };
|
|
268
334
|
const jc = p.justify ? justifyMap[String(p.justify)] : undefined;
|
|
269
335
|
const childIds = el.children ?? [];
|
|
270
|
-
let html = `<div style="display:flex;flex-direction:${dir};gap:${gapVal}${jc ?
|
|
336
|
+
let html = `<div style="display:flex;width:100%;flex-direction:${dir};gap:${gapVal};${wrap}${align}${jc ? `justify-content:${jc};` : ""}">`;
|
|
271
337
|
for (const childKey of childIds) {
|
|
272
|
-
const flex =
|
|
338
|
+
const flex = isHorizontal ? "flex:1;min-width:0;" : "";
|
|
273
339
|
html += `<div style="${flex}">${renderElement(childKey, spec, accent)}</div>`;
|
|
274
340
|
}
|
|
275
341
|
html += `</div>`;
|
|
276
342
|
return html;
|
|
277
343
|
}
|
|
344
|
+
case "bar_chart": {
|
|
345
|
+
const bars = Array.isArray(p.bars) ? (p.bars as Array<{ label?: string; value?: number; color?: string }>) : [];
|
|
346
|
+
const chartColor = colorHex(p.color as string | undefined, accent);
|
|
347
|
+
const maxVal = p.max != null ? Number(p.max) : Math.max(...bars.map((b) => Number(b.value ?? 0)), 1);
|
|
348
|
+
let html = `<div style="display:flex;flex-direction:column;gap:8px;width:100%">`;
|
|
349
|
+
for (const bar of bars) {
|
|
350
|
+
const value = Number(bar.value ?? 0);
|
|
351
|
+
const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
|
|
352
|
+
const fill = bar.color ? colorHex(bar.color, accent) : chartColor;
|
|
353
|
+
html += `<div style="display:flex;align-items:center;gap:8px">`;
|
|
354
|
+
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>`;
|
|
355
|
+
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>`;
|
|
356
|
+
html += `<span style="width:32px;flex-shrink:0;font-size:12px;color:#6B7280;font-variant-numeric:tabular-nums">${value}</span>`;
|
|
357
|
+
html += `</div>`;
|
|
358
|
+
}
|
|
359
|
+
html += `</div>`;
|
|
360
|
+
return html;
|
|
361
|
+
}
|
|
362
|
+
case "cell_grid": {
|
|
363
|
+
const cols = Number(p.cols ?? 2);
|
|
364
|
+
const rows = Number(p.rows ?? 2);
|
|
365
|
+
const cells = Array.isArray(p.cells) ? (p.cells as Array<{ row?: number; col?: number; color?: string; content?: string }>) : [];
|
|
366
|
+
const gap = String(p.gap ?? "sm");
|
|
367
|
+
const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
368
|
+
const gapPx = gapMap[gap] ?? 1;
|
|
369
|
+
const cellMap = new Map<string, { color?: string; content?: string }>();
|
|
370
|
+
for (const c of cells) {
|
|
371
|
+
cellMap.set(`${Number(c.row ?? 0)},${Number(c.col ?? 0)}`, { color: c.color, content: c.content });
|
|
372
|
+
}
|
|
373
|
+
let html = `<div style="display:grid;grid-template-columns:repeat(${cols},minmax(0,1fr));gap:${gapPx}px;width:100%">`;
|
|
374
|
+
for (let r = 0; r < rows; r++) {
|
|
375
|
+
for (let c = 0; c < cols; c++) {
|
|
376
|
+
const cell = cellMap.get(`${r},${c}`);
|
|
377
|
+
const bg = cell?.color ? colorHex(cell.color, accent) : "transparent";
|
|
378
|
+
const content = cell?.content ? esc(cell.content) : "";
|
|
379
|
+
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>`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
html += `</div>`;
|
|
383
|
+
return html;
|
|
384
|
+
}
|
|
278
385
|
default:
|
|
279
386
|
return "";
|
|
280
387
|
}
|
|
@@ -317,17 +424,18 @@ ${ogMeta}
|
|
|
317
424
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
318
425
|
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}
|
|
319
426
|
.card{background:#fff;border-radius:16px;max-width:420px;width:100%;padding:20px;box-shadow:0 4px 24px rgba(0,0,0,0.3)}
|
|
427
|
+
.card button:hover{filter:brightness(0.92)}
|
|
320
428
|
.foot{margin-top:16px;text-align:center}
|
|
321
|
-
.foot a{color
|
|
429
|
+
.foot a{color:${accent};text-decoration:none;font-size:13px;display:inline-flex;align-items:center;gap:6px}
|
|
322
430
|
.foot a:hover{opacity:.8}
|
|
323
431
|
.foot svg{width:14px;height:12px}
|
|
324
432
|
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);align-items:center;justify-content:center;z-index:99}
|
|
325
433
|
.modal-box{background:#1A1A2E;border-radius:16px;padding:32px;text-align:center;max-width:340px;width:90%}
|
|
326
|
-
.modal-box svg{width:40px;height:35px;color
|
|
434
|
+
.modal-box svg{width:40px;height:35px;color:${accent};margin-bottom:16px}
|
|
327
435
|
.modal-box h2{color:#FAFAFA;font-size:20px;margin-bottom:8px}
|
|
328
436
|
.modal-box p{color:#A1A1AA;font-size:14px;line-height:1.5;margin-bottom:24px}
|
|
329
437
|
.modal-box a{display:block;padding:12px;border-radius:10px;text-decoration:none;font-weight:600;font-size:15px;margin-bottom:12px}
|
|
330
|
-
.mb-primary{background
|
|
438
|
+
.mb-primary{background:${accent};color:${fgForBg(accent)}}
|
|
331
439
|
.mb-secondary{background:#1A1A2E;color:#FAFAFA;border:1px solid #2D2D44}
|
|
332
440
|
.modal-box a:hover{opacity:.85}
|
|
333
441
|
.modal-box button{background:none;border:none;color:#A1A1AA;cursor:pointer;font-size:13px;font-family:inherit}
|