@farcaster/snap-hono 1.4.2 → 1.4.3
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 +123 -49
- package/dist/renderSnapPage.js +124 -58
- package/package.json +2 -2
- package/src/index.ts +1 -8
- package/src/og-image.ts +151 -48
- package/src/renderSnapPage.ts +126 -59
package/dist/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { cors } from "hono/cors";
|
|
2
|
-
import {
|
|
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://
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
...
|
|
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);
|
|
@@ -363,16 +392,41 @@ function mapElement(el, accent, imageMap) {
|
|
|
363
392
|
switch (type) {
|
|
364
393
|
case "text":
|
|
365
394
|
return mapText(el);
|
|
395
|
+
case "item":
|
|
396
|
+
return mapItem(el);
|
|
397
|
+
case "badge":
|
|
398
|
+
return mapBadge(el, accent);
|
|
366
399
|
case "image":
|
|
367
400
|
return mapImage(el, imageMap);
|
|
401
|
+
case "separator":
|
|
368
402
|
case "divider":
|
|
369
|
-
return
|
|
403
|
+
return mapSeparator();
|
|
370
404
|
case "progress":
|
|
371
405
|
return mapProgress(el, accent);
|
|
372
|
-
case "list":
|
|
373
|
-
return mapList(el);
|
|
374
406
|
case "toggle_group":
|
|
375
407
|
return mapButtonGroup(el, accent);
|
|
408
|
+
case "input": {
|
|
409
|
+
const label = el.label ? String(el.label) : "";
|
|
410
|
+
const placeholder = el.placeholder ? String(el.placeholder) : "";
|
|
411
|
+
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", {
|
|
412
|
+
display: "flex", padding: "10px 12px", borderRadius: 8,
|
|
413
|
+
border: "1px solid #E5E7EB", backgroundColor: "#fff",
|
|
414
|
+
fontSize: 14, color: "#9CA3AF",
|
|
415
|
+
}, placeholder || " "));
|
|
416
|
+
}
|
|
417
|
+
case "switch": {
|
|
418
|
+
const label = el.label ? String(el.label) : "";
|
|
419
|
+
const checked = Boolean(el.defaultChecked);
|
|
420
|
+
const bg = checked ? accent : "#D1D5DB";
|
|
421
|
+
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 })));
|
|
422
|
+
}
|
|
423
|
+
case "slider": {
|
|
424
|
+
const label = el.label ? String(el.label) : "";
|
|
425
|
+
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 })));
|
|
426
|
+
}
|
|
427
|
+
// Legacy types kept for backward compat with older specs
|
|
428
|
+
case "list":
|
|
429
|
+
return mapList(el);
|
|
376
430
|
case "bar_chart":
|
|
377
431
|
return mapBarChart(el, accent);
|
|
378
432
|
case "group": {
|
|
@@ -388,8 +442,8 @@ function mapElement(el, accent, imageMap) {
|
|
|
388
442
|
}
|
|
389
443
|
function mapButton(btn, accent, i) {
|
|
390
444
|
const label = String(btn.label ?? "");
|
|
391
|
-
const
|
|
392
|
-
const isPrimary =
|
|
445
|
+
const variant = btn.variant ?? btn.style ?? "secondary";
|
|
446
|
+
const isPrimary = variant === "primary";
|
|
393
447
|
// Primary CTA: generous vertical padding + minHeight so Satori/Yoga renders a tall tap target
|
|
394
448
|
// (small padding deltas are easy to miss; flexBasis:0 rows can also under-measure height).
|
|
395
449
|
const py = isPrimary ? 18 : 10;
|
|
@@ -425,21 +479,21 @@ function linesForWrappedText(charCount, innerWidthPx, avgCharPx) {
|
|
|
425
479
|
return Math.max(1, Math.ceil((charCount * 1.12) / cpl));
|
|
426
480
|
}
|
|
427
481
|
function estimateTextHeight(el) {
|
|
428
|
-
const
|
|
482
|
+
const size = String(el.size ?? "md");
|
|
429
483
|
const content = String(el.content ?? "");
|
|
430
484
|
const w = OG_CARD_INNER_WIDTH_PX;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
485
|
+
if (size === "sm")
|
|
486
|
+
return linesForWrappedText(content.length, w, 7) * 20;
|
|
487
|
+
return linesForWrappedText(content.length, w, 7.5) * 23;
|
|
488
|
+
}
|
|
489
|
+
function estimateItemHeight(el) {
|
|
490
|
+
const title = String(el.title ?? "");
|
|
491
|
+
const desc = el.description ? String(el.description) : "";
|
|
492
|
+
const w = OG_CARD_INNER_WIDTH_PX;
|
|
493
|
+
let total = linesForWrappedText(title.length, w, 7.5) * 23 + 12;
|
|
494
|
+
if (desc)
|
|
495
|
+
total += linesForWrappedText(desc.length, w, 7) * 20;
|
|
496
|
+
return total;
|
|
443
497
|
}
|
|
444
498
|
function estimateImageHeight(el, imageMap) {
|
|
445
499
|
const url = el.url;
|
|
@@ -499,12 +553,23 @@ function estimateElementHeight(el, imageMap) {
|
|
|
499
553
|
switch (type) {
|
|
500
554
|
case "text":
|
|
501
555
|
return estimateTextHeight(el);
|
|
556
|
+
case "item":
|
|
557
|
+
return estimateItemHeight(el);
|
|
558
|
+
case "badge":
|
|
559
|
+
return 24;
|
|
502
560
|
case "image":
|
|
503
561
|
return estimateImageHeight(el, imageMap);
|
|
562
|
+
case "separator":
|
|
504
563
|
case "divider":
|
|
505
564
|
return 1;
|
|
506
565
|
case "progress":
|
|
507
566
|
return estimateProgressHeight(el);
|
|
567
|
+
case "input":
|
|
568
|
+
return (el.label ? 20 : 0) + 42;
|
|
569
|
+
case "switch":
|
|
570
|
+
return 28;
|
|
571
|
+
case "slider":
|
|
572
|
+
return (el.label ? 20 : 0) + 16;
|
|
508
573
|
case "list":
|
|
509
574
|
return estimateListHeight(el);
|
|
510
575
|
case "toggle_group":
|
|
@@ -557,19 +622,28 @@ function estimateDefaultOgHeight(elements, imageMap, buttons, buttonLayout) {
|
|
|
557
622
|
return Math.min(OG_MAX_HEIGHT_PX, Math.max(OG_MIN_HEIGHT_PX, Math.ceil(outerH)));
|
|
558
623
|
}
|
|
559
624
|
// ─── Spec helpers ─────────────────────────────────────
|
|
560
|
-
/** Walk the flat spec from root and collect
|
|
625
|
+
/** Walk the flat spec from root, recursing into stack containers, and collect leaf elements as El objects. */
|
|
561
626
|
function specToElementList(spec) {
|
|
627
|
+
function collect(keys) {
|
|
628
|
+
const result = [];
|
|
629
|
+
for (const key of keys) {
|
|
630
|
+
const el = spec.elements[key];
|
|
631
|
+
if (!el)
|
|
632
|
+
continue;
|
|
633
|
+
// Recurse into stack and item_group containers
|
|
634
|
+
if ((el.type === "stack" || el.type === "item_group") && el.children?.length) {
|
|
635
|
+
result.push(...collect(el.children));
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
result.push({ type: el.type, ...el.props });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
562
643
|
const rootEl = spec.elements[spec.root];
|
|
563
644
|
if (!rootEl?.children)
|
|
564
645
|
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);
|
|
646
|
+
return collect(rootEl.children);
|
|
573
647
|
}
|
|
574
648
|
/** Extract button elements (type: "button") from the spec. */
|
|
575
649
|
function specToButtons(spec) {
|
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,67 +209,79 @@ 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>`;
|
|
@@ -255,17 +320,18 @@ ${ogMeta}
|
|
|
255
320
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
256
321
|
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
322
|
.card{background:#fff;border-radius:16px;max-width:420px;width:100%;padding:20px;box-shadow:0 4px 24px rgba(0,0,0,0.3)}
|
|
323
|
+
.card button:hover{filter:brightness(0.92)}
|
|
258
324
|
.foot{margin-top:16px;text-align:center}
|
|
259
|
-
.foot a{color
|
|
325
|
+
.foot a{color:${accent};text-decoration:none;font-size:13px;display:inline-flex;align-items:center;gap:6px}
|
|
260
326
|
.foot a:hover{opacity:.8}
|
|
261
327
|
.foot svg{width:14px;height:12px}
|
|
262
328
|
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);align-items:center;justify-content:center;z-index:99}
|
|
263
329
|
.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
|
|
330
|
+
.modal-box svg{width:40px;height:35px;color:${accent};margin-bottom:16px}
|
|
265
331
|
.modal-box h2{color:#FAFAFA;font-size:20px;margin-bottom:8px}
|
|
266
332
|
.modal-box p{color:#A1A1AA;font-size:14px;line-height:1.5;margin-bottom:24px}
|
|
267
333
|
.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
|
|
334
|
+
.mb-primary{background:${accent};color:${fgForBg(accent)}}
|
|
269
335
|
.mb-secondary{background:#1A1A2E;color:#FAFAFA;border:1px solid #2D2D44}
|
|
270
336
|
.modal-box a:hover{opacity:.85}
|
|
271
337
|
.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.3",
|
|
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.10.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 {
|
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);
|
|
@@ -534,16 +575,60 @@ function mapElement(
|
|
|
534
575
|
switch (type) {
|
|
535
576
|
case "text":
|
|
536
577
|
return mapText(el);
|
|
578
|
+
case "item":
|
|
579
|
+
return mapItem(el);
|
|
580
|
+
case "badge":
|
|
581
|
+
return mapBadge(el, accent);
|
|
537
582
|
case "image":
|
|
538
583
|
return mapImage(el, imageMap);
|
|
584
|
+
case "separator":
|
|
539
585
|
case "divider":
|
|
540
|
-
return
|
|
586
|
+
return mapSeparator();
|
|
541
587
|
case "progress":
|
|
542
588
|
return mapProgress(el, accent);
|
|
543
|
-
case "list":
|
|
544
|
-
return mapList(el);
|
|
545
589
|
case "toggle_group":
|
|
546
590
|
return mapButtonGroup(el, accent);
|
|
591
|
+
case "input": {
|
|
592
|
+
const label = el.label ? String(el.label) : "";
|
|
593
|
+
const placeholder = el.placeholder ? String(el.placeholder) : "";
|
|
594
|
+
return h(
|
|
595
|
+
"div",
|
|
596
|
+
{ display: "flex", flexDirection: "column", gap: 6, width: OG_CARD_INNER_WIDTH_PX },
|
|
597
|
+
label ? h("div", { display: "flex", fontSize: 13, fontWeight: 500, color: "#374151" }, label) : null,
|
|
598
|
+
h("div", {
|
|
599
|
+
display: "flex", padding: "10px 12px", borderRadius: 8,
|
|
600
|
+
border: "1px solid #E5E7EB", backgroundColor: "#fff",
|
|
601
|
+
fontSize: 14, color: "#9CA3AF",
|
|
602
|
+
}, placeholder || " "),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
case "switch": {
|
|
606
|
+
const label = el.label ? String(el.label) : "";
|
|
607
|
+
const checked = Boolean(el.defaultChecked);
|
|
608
|
+
const bg = checked ? accent : "#D1D5DB";
|
|
609
|
+
return h(
|
|
610
|
+
"div",
|
|
611
|
+
{ display: "flex", alignItems: "center", justifyContent: "space-between", width: OG_CARD_INNER_WIDTH_PX },
|
|
612
|
+
h("div", { display: "flex", fontSize: 14, color: "#374151" }, label),
|
|
613
|
+
h("div", { display: "flex", width: 44, height: 24, borderRadius: 12, backgroundColor: bg, position: "relative" },
|
|
614
|
+
h("div", { display: "flex", width: 20, height: 20, borderRadius: 10, backgroundColor: "#fff", position: "absolute", top: 2, left: checked ? 20 : 2 }),
|
|
615
|
+
),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
case "slider": {
|
|
619
|
+
const label = el.label ? String(el.label) : "";
|
|
620
|
+
return h(
|
|
621
|
+
"div",
|
|
622
|
+
{ display: "flex", flexDirection: "column", gap: 6, width: OG_CARD_INNER_WIDTH_PX },
|
|
623
|
+
label ? h("div", { display: "flex", fontSize: 13, fontWeight: 500, color: "#374151" }, label) : null,
|
|
624
|
+
h("div", { display: "flex", height: 10, backgroundColor: "#E5E7EB", borderRadius: 9999, width: "100%" },
|
|
625
|
+
h("div", { display: "flex", height: 10, width: "50%", backgroundColor: accent, borderRadius: 9999 }),
|
|
626
|
+
),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
// Legacy types kept for backward compat with older specs
|
|
630
|
+
case "list":
|
|
631
|
+
return mapList(el);
|
|
547
632
|
case "bar_chart":
|
|
548
633
|
return mapBarChart(el, accent);
|
|
549
634
|
case "group": {
|
|
@@ -564,8 +649,8 @@ function mapElement(
|
|
|
564
649
|
|
|
565
650
|
function mapButton(btn: El, accent: string, i: number): VNode {
|
|
566
651
|
const label = String(btn.label ?? "");
|
|
567
|
-
const
|
|
568
|
-
const isPrimary =
|
|
652
|
+
const variant = (btn.variant as string) ?? (btn.style as string) ?? "secondary";
|
|
653
|
+
const isPrimary = variant === "primary";
|
|
569
654
|
// Primary CTA: generous vertical padding + minHeight so Satori/Yoga renders a tall tap target
|
|
570
655
|
// (small padding deltas are easy to miss; flexBasis:0 rows can also under-measure height).
|
|
571
656
|
const py = isPrimary ? 18 : 10;
|
|
@@ -606,21 +691,20 @@ function linesForWrappedText(
|
|
|
606
691
|
}
|
|
607
692
|
|
|
608
693
|
function estimateTextHeight(el: El): number {
|
|
609
|
-
const
|
|
694
|
+
const size = String(el.size ?? "md");
|
|
610
695
|
const content = String(el.content ?? "");
|
|
611
696
|
const w = OG_CARD_INNER_WIDTH_PX;
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
697
|
+
if (size === "sm") return linesForWrappedText(content.length, w, 7) * 20;
|
|
698
|
+
return linesForWrappedText(content.length, w, 7.5) * 23;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function estimateItemHeight(el: El): number {
|
|
702
|
+
const title = String(el.title ?? "");
|
|
703
|
+
const desc = el.description ? String(el.description) : "";
|
|
704
|
+
const w = OG_CARD_INNER_WIDTH_PX;
|
|
705
|
+
let total = linesForWrappedText(title.length, w, 7.5) * 23 + 12;
|
|
706
|
+
if (desc) total += linesForWrappedText(desc.length, w, 7) * 20;
|
|
707
|
+
return total;
|
|
624
708
|
}
|
|
625
709
|
|
|
626
710
|
function estimateImageHeight(el: El, imageMap: Map<string, string>): number {
|
|
@@ -688,12 +772,23 @@ function estimateElementHeight(el: El, imageMap: Map<string, string>): number {
|
|
|
688
772
|
switch (type) {
|
|
689
773
|
case "text":
|
|
690
774
|
return estimateTextHeight(el);
|
|
775
|
+
case "item":
|
|
776
|
+
return estimateItemHeight(el);
|
|
777
|
+
case "badge":
|
|
778
|
+
return 24;
|
|
691
779
|
case "image":
|
|
692
780
|
return estimateImageHeight(el, imageMap);
|
|
781
|
+
case "separator":
|
|
693
782
|
case "divider":
|
|
694
783
|
return 1;
|
|
695
784
|
case "progress":
|
|
696
785
|
return estimateProgressHeight(el);
|
|
786
|
+
case "input":
|
|
787
|
+
return (el.label ? 20 : 0) + 42;
|
|
788
|
+
case "switch":
|
|
789
|
+
return 28;
|
|
790
|
+
case "slider":
|
|
791
|
+
return (el.label ? 20 : 0) + 16;
|
|
697
792
|
case "list":
|
|
698
793
|
return estimateListHeight(el);
|
|
699
794
|
case "toggle_group":
|
|
@@ -767,17 +862,25 @@ function estimateDefaultOgHeight(
|
|
|
767
862
|
|
|
768
863
|
// ─── Spec helpers ─────────────────────────────────────
|
|
769
864
|
|
|
770
|
-
/** Walk the flat spec from root and collect
|
|
865
|
+
/** Walk the flat spec from root, recursing into stack containers, and collect leaf elements as El objects. */
|
|
771
866
|
function specToElementList(spec: SnapSpec): El[] {
|
|
867
|
+
function collect(keys: string[]): El[] {
|
|
868
|
+
const result: El[] = [];
|
|
869
|
+
for (const key of keys) {
|
|
870
|
+
const el = spec.elements[key];
|
|
871
|
+
if (!el) continue;
|
|
872
|
+
// Recurse into stack and item_group containers
|
|
873
|
+
if ((el.type === "stack" || el.type === "item_group") && el.children?.length) {
|
|
874
|
+
result.push(...collect(el.children));
|
|
875
|
+
} else {
|
|
876
|
+
result.push({ type: el.type, ...el.props } as El);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return result;
|
|
880
|
+
}
|
|
772
881
|
const rootEl = spec.elements[spec.root];
|
|
773
882
|
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);
|
|
883
|
+
return collect(rootEl.children);
|
|
781
884
|
}
|
|
782
885
|
|
|
783
886
|
/** Extract button elements (type: "button") from the spec. */
|
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,67 +263,79 @@ 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>`;
|
|
@@ -317,17 +383,18 @@ ${ogMeta}
|
|
|
317
383
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
318
384
|
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
385
|
.card{background:#fff;border-radius:16px;max-width:420px;width:100%;padding:20px;box-shadow:0 4px 24px rgba(0,0,0,0.3)}
|
|
386
|
+
.card button:hover{filter:brightness(0.92)}
|
|
320
387
|
.foot{margin-top:16px;text-align:center}
|
|
321
|
-
.foot a{color
|
|
388
|
+
.foot a{color:${accent};text-decoration:none;font-size:13px;display:inline-flex;align-items:center;gap:6px}
|
|
322
389
|
.foot a:hover{opacity:.8}
|
|
323
390
|
.foot svg{width:14px;height:12px}
|
|
324
391
|
.modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);align-items:center;justify-content:center;z-index:99}
|
|
325
392
|
.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
|
|
393
|
+
.modal-box svg{width:40px;height:35px;color:${accent};margin-bottom:16px}
|
|
327
394
|
.modal-box h2{color:#FAFAFA;font-size:20px;margin-bottom:8px}
|
|
328
395
|
.modal-box p{color:#A1A1AA;font-size:14px;line-height:1.5;margin-bottom:24px}
|
|
329
396
|
.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
|
|
397
|
+
.mb-primary{background:${accent};color:${fgForBg(accent)}}
|
|
331
398
|
.mb-secondary{background:#1A1A2E;color:#FAFAFA;border:1px solid #2D2D44}
|
|
332
399
|
.modal-box a:hover{opacity:.85}
|
|
333
400
|
.modal-box button{background:none;border:none;color:#A1A1AA;cursor:pointer;font-size:13px;font-family:inherit}
|