@farcaster/snap 1.15.3 → 1.16.0
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/constants.d.ts +8 -0
- package/dist/constants.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react/components/action-button.d.ts +2 -1
- package/dist/react/components/action-button.js +16 -3
- package/dist/react/components/badge.js +2 -3
- package/dist/react/index.d.ts +8 -1
- package/dist/react/index.js +9 -228
- package/dist/react/snap-view-core.d.ts +11 -0
- package/dist/react/snap-view-core.js +224 -0
- package/dist/react/v1/snap-view.d.ts +14 -0
- package/dist/react/v1/snap-view.js +9 -0
- package/dist/react/v2/snap-view.d.ts +21 -0
- package/dist/react/v2/snap-view.js +76 -0
- package/dist/react-native/components/snap-action-button.d.ts +1 -1
- package/dist/react-native/components/snap-action-button.js +19 -2
- package/dist/react-native/components/snap-badge.js +3 -3
- package/dist/react-native/index.d.ts +15 -43
- package/dist/react-native/index.js +10 -164
- package/dist/react-native/snap-view-core.d.ts +11 -0
- package/dist/react-native/snap-view-core.js +153 -0
- package/dist/react-native/types.d.ts +41 -0
- package/dist/react-native/types.js +1 -0
- package/dist/react-native/v1/snap-view.d.ts +22 -0
- package/dist/react-native/v1/snap-view.js +31 -0
- package/dist/react-native/v2/snap-view.d.ts +31 -0
- package/dist/react-native/v2/snap-view.js +101 -0
- package/dist/schemas.d.ts +15 -9
- package/dist/schemas.js +7 -8
- package/dist/server/parseRequest.d.ts +7 -0
- package/dist/server/parseRequest.js +27 -0
- package/dist/ui/catalog.d.ts +1 -0
- package/dist/ui/catalog.js +5 -2
- package/dist/ui/schema.js +1 -1
- package/dist/validator.d.ts +3 -2
- package/dist/validator.js +193 -2
- package/llms.txt +9 -0
- package/package.json +1 -1
- package/src/constants.ts +11 -1
- package/src/index.ts +8 -0
- package/src/react/accent-context.tsx +1 -1
- package/src/react/components/action-button.tsx +25 -3
- package/src/react/components/badge.tsx +2 -3
- package/src/react/index.tsx +36 -327
- package/src/react/snap-view-core.tsx +340 -0
- package/src/react/v1/snap-view.tsx +50 -0
- package/src/react/v2/snap-view.tsx +168 -0
- package/src/react-native/components/snap-action-button.tsx +26 -4
- package/src/react-native/components/snap-badge.tsx +3 -3
- package/src/react-native/index.tsx +47 -263
- package/src/react-native/snap-view-core.tsx +209 -0
- package/src/react-native/types.ts +37 -0
- package/src/react-native/v1/snap-view.tsx +108 -0
- package/src/react-native/v2/snap-view.tsx +239 -0
- package/src/schemas.ts +9 -10
- package/src/server/parseRequest.ts +39 -0
- package/src/ui/catalog.ts +5 -2
- package/src/ui/schema.ts +1 -1
- package/src/validator.ts +240 -2
package/dist/constants.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
export declare const SPEC_VERSION_1: "1.0";
|
|
2
|
+
export declare const SPEC_VERSION_2: "2.0";
|
|
1
3
|
export declare const SPEC_VERSION: "1.0";
|
|
4
|
+
export declare const SUPPORTED_SPEC_VERSIONS: readonly ["1.0", "2.0"];
|
|
5
|
+
export type SpecVersion = (typeof SUPPORTED_SPEC_VERSIONS)[number];
|
|
2
6
|
export declare const MEDIA_TYPE: "application/vnd.farcaster.snap+json";
|
|
3
7
|
export declare const EFFECT_VALUES: readonly ["confetti"];
|
|
4
8
|
export declare const POST_GRID_TAP_KEY: "grid_tap";
|
|
@@ -7,5 +11,9 @@ export declare const GRID_MAX_COLS = 32;
|
|
|
7
11
|
export declare const GRID_MIN_ROWS = 2;
|
|
8
12
|
export declare const GRID_MAX_ROWS = 16;
|
|
9
13
|
export declare const GRID_GAP_VALUES: readonly ["none", "sm", "md", "lg"];
|
|
14
|
+
export declare const MAX_ELEMENTS = 64;
|
|
15
|
+
export declare const MAX_ROOT_CHILDREN = 7;
|
|
16
|
+
export declare const MAX_CHILDREN = 6;
|
|
17
|
+
export declare const MAX_DEPTH = 4;
|
|
10
18
|
export declare const BAR_CHART_MAX_BARS = 6;
|
|
11
19
|
export declare const BAR_CHART_LABEL_MAX_CHARS = 40;
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const SPEC_VERSION_1 = "1.0";
|
|
2
|
+
export const SPEC_VERSION_2 = "2.0";
|
|
3
|
+
export const SPEC_VERSION = SPEC_VERSION_1;
|
|
4
|
+
export const SUPPORTED_SPEC_VERSIONS = [SPEC_VERSION_1, SPEC_VERSION_2];
|
|
2
5
|
export const MEDIA_TYPE = "application/vnd.farcaster.snap+json";
|
|
3
6
|
export const EFFECT_VALUES = ["confetti"];
|
|
4
7
|
// ─── Pixel grid ────────────────────────────────────────
|
|
@@ -8,6 +11,11 @@ export const GRID_MAX_COLS = 32;
|
|
|
8
11
|
export const GRID_MIN_ROWS = 2;
|
|
9
12
|
export const GRID_MAX_ROWS = 16;
|
|
10
13
|
export const GRID_GAP_VALUES = ["none", "sm", "md", "lg"];
|
|
14
|
+
// ─── Snap structural limits ───────────────────────────
|
|
15
|
+
export const MAX_ELEMENTS = 64;
|
|
16
|
+
export const MAX_ROOT_CHILDREN = 7;
|
|
17
|
+
export const MAX_CHILDREN = 6;
|
|
18
|
+
export const MAX_DEPTH = 4;
|
|
11
19
|
// ─── Bar chart ─────────────────────────────────────────
|
|
12
20
|
export const BAR_CHART_MAX_BARS = 6;
|
|
13
21
|
export const BAR_CHART_LABEL_MAX_CHARS = 40;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { Spec as SnapSpec, UIElement as SnapUIElement, } from "@json-render/core";
|
|
2
|
-
export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, } from "./constants.js";
|
|
2
|
+
export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS, type SpecVersion, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, MAX_ELEMENTS, MAX_ROOT_CHILDREN, MAX_CHILDREN, MAX_DEPTH, } from "./constants.js";
|
|
3
3
|
export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, type PaletteColor, } from "./colors.js";
|
|
4
4
|
export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, type SnapAction, type SnapContext, type SnapResponse, type SnapHandlerResult, type SnapElementInput, type SnapSpecInput, type SnapFunction, type SnapPayload, } from "./schemas.js";
|
|
5
5
|
export { validateSnapResponse, type ValidationResult } from "./validator.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, } from "./constants.js";
|
|
1
|
+
export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, MAX_ELEMENTS, MAX_ROOT_CHILDREN, MAX_CHILDREN, MAX_DEPTH, } from "./constants.js";
|
|
2
2
|
export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "./colors.js";
|
|
3
3
|
export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, } from "./schemas.js";
|
|
4
4
|
export { validateSnapResponse } from "./validator.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export declare function SnapActionButton({ element
|
|
1
|
+
export declare function SnapActionButton({ element, emit, }: {
|
|
2
2
|
element: {
|
|
3
3
|
props: Record<string, unknown>;
|
|
4
|
+
on?: Record<string, unknown>;
|
|
4
5
|
};
|
|
5
6
|
emit: (name: string) => void;
|
|
6
7
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { ExternalLink } from "lucide-react";
|
|
4
5
|
import { Button } from "@neynar/ui/button";
|
|
5
6
|
import { cn } from "@neynar/ui/utils";
|
|
6
7
|
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
7
8
|
import { ICON_MAP } from "./icon.js";
|
|
8
|
-
|
|
9
|
+
function isExternalLinkAction(on) {
|
|
10
|
+
if (!on)
|
|
11
|
+
return false;
|
|
12
|
+
const press = on.press;
|
|
13
|
+
if (!press || press.action !== "open_url")
|
|
14
|
+
return false;
|
|
15
|
+
return press.params?.isSnap !== true;
|
|
16
|
+
}
|
|
17
|
+
export function SnapActionButton({ element, emit, }) {
|
|
18
|
+
const { props } = element;
|
|
9
19
|
const label = String(props.label ?? "Action");
|
|
10
20
|
const variant = String(props.variant ?? "secondary");
|
|
11
21
|
const isPrimary = variant === "primary";
|
|
@@ -13,6 +23,7 @@ export function SnapActionButton({ element: { props }, emit, }) {
|
|
|
13
23
|
const colors = useSnapColors();
|
|
14
24
|
const [hovered, setHovered] = useState(false);
|
|
15
25
|
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
26
|
+
const showExternalIcon = isExternalLinkAction(element.on);
|
|
16
27
|
const style = isPrimary
|
|
17
28
|
? {
|
|
18
29
|
backgroundColor: hovered ? colors.accentHover : colors.accent,
|
|
@@ -20,9 +31,11 @@ export function SnapActionButton({ element: { props }, emit, }) {
|
|
|
20
31
|
borderColor: "transparent",
|
|
21
32
|
}
|
|
22
33
|
: {
|
|
23
|
-
backgroundColor: hovered
|
|
34
|
+
backgroundColor: hovered
|
|
35
|
+
? `color-mix(in srgb, ${colors.accent} 15%, transparent)`
|
|
36
|
+
: colors.muted,
|
|
24
37
|
color: colors.text,
|
|
25
38
|
borderColor: "transparent",
|
|
26
39
|
};
|
|
27
|
-
return (_jsx("div", { className: "w-full min-w-0 flex-1", children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("w-full gap-2"), style: style, onClick: () => emit("press"), onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label] }) }));
|
|
40
|
+
return (_jsx("div", { className: "w-full min-w-0 flex-1", children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("w-full gap-2"), style: style, onClick: () => emit("press"), onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label, showExternalIcon && (_jsx(ExternalLink, { size: 14, style: { opacity: 0.6 } }))] }) }));
|
|
28
41
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Badge } from "@neynar/ui/badge";
|
|
4
|
-
import { useSnapColors
|
|
4
|
+
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
5
5
|
import { ICON_MAP } from "./icon.js";
|
|
6
6
|
export function SnapBadge({ element: { props }, }) {
|
|
7
7
|
const content = String(props.label ?? "");
|
|
@@ -10,10 +10,9 @@ export function SnapBadge({ element: { props }, }) {
|
|
|
10
10
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
11
11
|
const colors = useSnapColors();
|
|
12
12
|
const badgeColor = colors.colorHex(color);
|
|
13
|
-
const badgeFg = variant === "default" ? pickForegroundForBg(badgeColor) : badgeColor;
|
|
14
13
|
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
15
14
|
const style = variant === "outline"
|
|
16
15
|
? { borderColor: badgeColor, color: badgeColor, backgroundColor: "transparent" }
|
|
17
|
-
: { backgroundColor: badgeColor
|
|
16
|
+
: { backgroundColor: `${badgeColor}20`, color: badgeColor, borderColor: "transparent" };
|
|
18
17
|
return (_jsxs(Badge, { variant: variant, className: "gap-1", style: style, children: [Icon && _jsx(Icon, { size: 12 }), content] }));
|
|
19
18
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Spec } from "@json-render/core";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { ValidationResult } from "../validator.js";
|
|
2
4
|
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
3
5
|
[key: string]: JsonValue;
|
|
4
6
|
};
|
|
@@ -39,9 +41,14 @@ export type SnapActionHandlers = {
|
|
|
39
41
|
buyToken?: string;
|
|
40
42
|
}) => void;
|
|
41
43
|
};
|
|
42
|
-
export declare function
|
|
44
|
+
export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, }: {
|
|
43
45
|
snap: SnapPage;
|
|
44
46
|
handlers: SnapActionHandlers;
|
|
45
47
|
loading?: boolean;
|
|
46
48
|
appearance?: "light" | "dark";
|
|
49
|
+
maxWidth?: number;
|
|
50
|
+
/** When true, extends to 700px and shows a warning overlay below 500px. When false, clips at 500px. Only applies to v2 snaps. */
|
|
51
|
+
showOverflowWarning?: boolean;
|
|
52
|
+
onValidationError?: (result: ValidationResult) => void;
|
|
53
|
+
validationErrorFallback?: ReactNode;
|
|
47
54
|
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/react/index.js
CHANGED
|
@@ -1,231 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function applyStatePaths(model, changes) {
|
|
11
|
-
const entries = Array.isArray(changes)
|
|
12
|
-
? changes.map((c) => [c.path, c.value])
|
|
13
|
-
: Object.entries(changes);
|
|
14
|
-
for (const [path, value] of entries) {
|
|
15
|
-
const trimmed = path.startsWith("/") ? path : `/${path}`;
|
|
16
|
-
const parts = trimmed.split("/").filter(Boolean);
|
|
17
|
-
if (parts.length < 2)
|
|
18
|
-
continue;
|
|
19
|
-
const [top, ...rest] = parts;
|
|
20
|
-
if (top === "inputs") {
|
|
21
|
-
if (typeof model.inputs !== "object" || model.inputs === null) {
|
|
22
|
-
model.inputs = {};
|
|
23
|
-
}
|
|
24
|
-
const inputs = model.inputs;
|
|
25
|
-
if (rest.length === 1) {
|
|
26
|
-
inputs[rest[0]] = value;
|
|
27
|
-
}
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (top === "theme") {
|
|
31
|
-
if (typeof model.theme !== "object" || model.theme === null) {
|
|
32
|
-
model.theme = {};
|
|
33
|
-
}
|
|
34
|
-
const theme = model.theme;
|
|
35
|
-
if (rest.length === 1) {
|
|
36
|
-
theme[rest[0]] = value;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { SPEC_VERSION_2 } from "../constants.js";
|
|
4
|
+
import { SnapCardV1 } from "./v1/snap-view.js";
|
|
5
|
+
import { SnapCardV2 } from "./v2/snap-view.js";
|
|
6
|
+
// ─── SnapCard ────────────────────────────────────────
|
|
7
|
+
export function SnapCard({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, }) {
|
|
8
|
+
if (snap.version === SPEC_VERSION_2) {
|
|
9
|
+
return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }));
|
|
39
10
|
}
|
|
40
|
-
}
|
|
41
|
-
const CONFETTI_COLORS = [
|
|
42
|
-
"#8B5CF6",
|
|
43
|
-
"#EC4899",
|
|
44
|
-
"#3B82F6",
|
|
45
|
-
"#10B981",
|
|
46
|
-
"#F59E0B",
|
|
47
|
-
"#EF4444",
|
|
48
|
-
"#06B6D4",
|
|
49
|
-
];
|
|
50
|
-
function ConfettiOverlay() {
|
|
51
|
-
const pieces = useMemo(() => Array.from({ length: 80 }, (_, i) => ({
|
|
52
|
-
id: i,
|
|
53
|
-
left: Math.random() * 100,
|
|
54
|
-
delay: Math.random() * 1.2,
|
|
55
|
-
duration: 2.5 + Math.random() * 2,
|
|
56
|
-
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
|
57
|
-
size: 6 + Math.random() * 8,
|
|
58
|
-
rotation: Math.random() * 360,
|
|
59
|
-
})), []);
|
|
60
|
-
return (_jsxs("div", { style: {
|
|
61
|
-
position: "absolute",
|
|
62
|
-
inset: 0,
|
|
63
|
-
overflow: "hidden",
|
|
64
|
-
pointerEvents: "none",
|
|
65
|
-
zIndex: 20,
|
|
66
|
-
}, children: [pieces.map(({ id, left, delay, duration, color, size, rotation }) => (_jsx("div", { style: {
|
|
67
|
-
position: "absolute",
|
|
68
|
-
left: `${left}%`,
|
|
69
|
-
top: -20,
|
|
70
|
-
width: size,
|
|
71
|
-
height: size * 0.6,
|
|
72
|
-
backgroundColor: color,
|
|
73
|
-
borderRadius: 2,
|
|
74
|
-
transform: `rotate(${rotation}deg)`,
|
|
75
|
-
animation: `confettiFall ${duration}s ease-in ${delay}s forwards`,
|
|
76
|
-
} }, id))), _jsx("style", { children: `@keyframes confettiFall{0%{top:-20px;opacity:1;transform:rotate(0deg) translateX(0)}50%{opacity:1}100%{top:110%;opacity:0;transform:rotate(720deg) translateX(${Math.random() > 0.5 ? "" : "-"}40px)}}` })] }));
|
|
77
|
-
}
|
|
78
|
-
function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
79
|
-
const isDark = appearance === "dark";
|
|
80
|
-
const tint = isDark ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.2)";
|
|
81
|
-
const trackColor = isDark
|
|
82
|
-
? "rgba(255, 255, 255, 0.12)"
|
|
83
|
-
: "rgba(15, 23, 42, 0.1)";
|
|
84
|
-
return (_jsxs("div", { style: {
|
|
85
|
-
position: "absolute",
|
|
86
|
-
inset: 0,
|
|
87
|
-
display: "flex",
|
|
88
|
-
alignItems: "center",
|
|
89
|
-
justifyContent: "center",
|
|
90
|
-
zIndex: 10,
|
|
91
|
-
background: tint,
|
|
92
|
-
backdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
|
|
93
|
-
WebkitBackdropFilter: active
|
|
94
|
-
? "blur(10px) saturate(1.05)"
|
|
95
|
-
: "none",
|
|
96
|
-
opacity: active ? 1 : 0,
|
|
97
|
-
pointerEvents: active ? "auto" : "none",
|
|
98
|
-
transition: "opacity 0.28s ease, backdrop-filter 0.28s ease",
|
|
99
|
-
}, "aria-hidden": !active, "aria-busy": active ? true : undefined, "aria-live": active ? "polite" : undefined, "aria-label": active ? "Loading" : undefined, children: [_jsx("div", { "data-snap-loading-spinner": true, style: {
|
|
100
|
-
width: 30,
|
|
101
|
-
height: 30,
|
|
102
|
-
borderRadius: "50%",
|
|
103
|
-
border: `2.5px solid ${trackColor}`,
|
|
104
|
-
borderTopColor: accentHex,
|
|
105
|
-
opacity: 0.88,
|
|
106
|
-
animation: "snapViewSpin 0.75s linear infinite",
|
|
107
|
-
flexShrink: 0,
|
|
108
|
-
} }), _jsx("style", { children: `
|
|
109
|
-
@keyframes snapViewSpin {
|
|
110
|
-
to { transform: rotate(360deg); }
|
|
111
|
-
}
|
|
112
|
-
@media (prefers-reduced-motion: reduce) {
|
|
113
|
-
[data-snap-loading-spinner] {
|
|
114
|
-
animation: none;
|
|
115
|
-
border-top-color: ${accentHex};
|
|
116
|
-
opacity: 0.75;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
` })] }));
|
|
120
|
-
}
|
|
121
|
-
const PALETTE = [
|
|
122
|
-
"gray",
|
|
123
|
-
"blue",
|
|
124
|
-
"red",
|
|
125
|
-
"amber",
|
|
126
|
-
"green",
|
|
127
|
-
"teal",
|
|
128
|
-
"purple",
|
|
129
|
-
"pink",
|
|
130
|
-
];
|
|
131
|
-
// ─── SnapView ──────────────────────────────────────────
|
|
132
|
-
export function SnapView({ snap, handlers, loading = false, appearance = "dark", }) {
|
|
133
|
-
const spec = snap.ui;
|
|
134
|
-
const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
|
|
135
|
-
const stateRef = useRef(initialState);
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
stateRef.current = {
|
|
138
|
-
inputs: {
|
|
139
|
-
...(initialState.inputs ?? {}),
|
|
140
|
-
},
|
|
141
|
-
theme: {
|
|
142
|
-
...(initialState.theme ?? {}),
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
}, [initialState]);
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
const result = snapJsonRenderCatalog.validate(spec);
|
|
148
|
-
if (!result.success) {
|
|
149
|
-
// eslint-disable-next-line no-console
|
|
150
|
-
console.warn("[SnapView] catalog validation issues:", result.error);
|
|
151
|
-
}
|
|
152
|
-
}, [spec]);
|
|
153
|
-
const [pageKey, setPageKey] = useState(0);
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
setPageKey((k) => k + 1);
|
|
156
|
-
}, [spec]);
|
|
157
|
-
const showConfetti = snap.effects?.includes("confetti");
|
|
158
|
-
// Increment key each time a new snap with confetti arrives so the overlay
|
|
159
|
-
// unmounts/remounts and restarts its animation on every trigger.
|
|
160
|
-
const confettiEpochRef = useRef(0);
|
|
161
|
-
const lastConfettiSnapRef = useRef(null);
|
|
162
|
-
if (showConfetti && snap !== lastConfettiSnapRef.current) {
|
|
163
|
-
confettiEpochRef.current++;
|
|
164
|
-
lastConfettiSnapRef.current = snap;
|
|
165
|
-
}
|
|
166
|
-
const accentName = snap.theme?.accent ?? "purple";
|
|
167
|
-
const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
|
|
168
|
-
const previewSurfaceStyle = useMemo(() => {
|
|
169
|
-
const vars = {};
|
|
170
|
-
for (const c of PALETTE)
|
|
171
|
-
vars[`--snap-color-${c}`] = resolveSnapPaletteHex(c, appearance);
|
|
172
|
-
return {
|
|
173
|
-
...snapPreviewPrimaryCssProperties(accentName, appearance),
|
|
174
|
-
...vars,
|
|
175
|
-
};
|
|
176
|
-
}, [accentName, appearance]);
|
|
177
|
-
const handleAction = useCallback((name, params) => {
|
|
178
|
-
const inputs = (stateRef.current.inputs ?? {});
|
|
179
|
-
const p = (params ?? {});
|
|
180
|
-
switch (name) {
|
|
181
|
-
case "submit":
|
|
182
|
-
handlers.submit(String(p.target ?? ""), inputs);
|
|
183
|
-
break;
|
|
184
|
-
case "open_url":
|
|
185
|
-
handlers.open_url(String(p.target ?? ""));
|
|
186
|
-
break;
|
|
187
|
-
case "open_mini_app":
|
|
188
|
-
handlers.open_mini_app(String(p.target ?? ""));
|
|
189
|
-
break;
|
|
190
|
-
case "view_cast":
|
|
191
|
-
handlers.view_cast({ hash: String(p.hash ?? "") });
|
|
192
|
-
break;
|
|
193
|
-
case "view_profile":
|
|
194
|
-
handlers.view_profile({ fid: Number(p.fid ?? 0) });
|
|
195
|
-
break;
|
|
196
|
-
case "compose_cast":
|
|
197
|
-
handlers.compose_cast({
|
|
198
|
-
text: p.text ? String(p.text) : undefined,
|
|
199
|
-
channelKey: p.channelKey ? String(p.channelKey) : undefined,
|
|
200
|
-
embeds: Array.isArray(p.embeds)
|
|
201
|
-
? p.embeds
|
|
202
|
-
: undefined,
|
|
203
|
-
});
|
|
204
|
-
break;
|
|
205
|
-
case "view_token":
|
|
206
|
-
handlers.view_token({ token: String(p.token ?? "") });
|
|
207
|
-
break;
|
|
208
|
-
case "send_token":
|
|
209
|
-
handlers.send_token({
|
|
210
|
-
token: String(p.token ?? ""),
|
|
211
|
-
amount: p.amount ? String(p.amount) : undefined,
|
|
212
|
-
recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
|
|
213
|
-
recipientAddress: p.recipientAddress
|
|
214
|
-
? String(p.recipientAddress)
|
|
215
|
-
: undefined,
|
|
216
|
-
});
|
|
217
|
-
break;
|
|
218
|
-
case "swap_token":
|
|
219
|
-
handlers.swap_token({
|
|
220
|
-
sellToken: p.sellToken ? String(p.sellToken) : undefined,
|
|
221
|
-
buyToken: p.buyToken ? String(p.buyToken) : undefined,
|
|
222
|
-
});
|
|
223
|
-
break;
|
|
224
|
-
default:
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}, [handlers]);
|
|
228
|
-
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, `confetti-${confettiEpochRef.current}`), _jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading }), _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
229
|
-
applyStatePaths(stateRef.current, changes);
|
|
230
|
-
}, onAction: handleAction }, pageKey) }) })] }));
|
|
11
|
+
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth }));
|
|
231
12
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SnapActionHandlers, SnapPage } from "./index.js";
|
|
2
|
+
export declare function applyStatePaths(model: Record<string, unknown>, changes: {
|
|
3
|
+
path: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
}[] | Record<string, unknown>): void;
|
|
6
|
+
export declare function SnapViewCore({ snap, handlers, loading, appearance, }: {
|
|
7
|
+
snap: SnapPage;
|
|
8
|
+
handlers: SnapActionHandlers;
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
appearance?: "light" | "dark";
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { snapJsonRenderCatalog } from "../ui/index.js";
|
|
4
|
+
import { SnapCatalogView } from "./catalog-renderer.js";
|
|
5
|
+
import { SnapPreviewAccentProvider } from "./accent-context.js";
|
|
6
|
+
import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex.js";
|
|
7
|
+
import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css.js";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
9
|
+
// ─── Internal helpers ──────────────────────────────────
|
|
10
|
+
export function applyStatePaths(model, changes) {
|
|
11
|
+
const entries = Array.isArray(changes)
|
|
12
|
+
? changes.map((c) => [c.path, c.value])
|
|
13
|
+
: Object.entries(changes);
|
|
14
|
+
for (const [path, value] of entries) {
|
|
15
|
+
const trimmed = path.startsWith("/") ? path : `/${path}`;
|
|
16
|
+
const parts = trimmed.split("/").filter(Boolean);
|
|
17
|
+
if (parts.length < 2)
|
|
18
|
+
continue;
|
|
19
|
+
const [top, ...rest] = parts;
|
|
20
|
+
if (top === "inputs") {
|
|
21
|
+
if (typeof model.inputs !== "object" || model.inputs === null) {
|
|
22
|
+
model.inputs = {};
|
|
23
|
+
}
|
|
24
|
+
const inputs = model.inputs;
|
|
25
|
+
if (rest.length === 1) {
|
|
26
|
+
inputs[rest[0]] = value;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (top === "theme") {
|
|
31
|
+
if (typeof model.theme !== "object" || model.theme === null) {
|
|
32
|
+
model.theme = {};
|
|
33
|
+
}
|
|
34
|
+
const theme = model.theme;
|
|
35
|
+
if (rest.length === 1) {
|
|
36
|
+
theme[rest[0]] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const CONFETTI_COLORS = [
|
|
42
|
+
"#8B5CF6",
|
|
43
|
+
"#EC4899",
|
|
44
|
+
"#3B82F6",
|
|
45
|
+
"#10B981",
|
|
46
|
+
"#F59E0B",
|
|
47
|
+
"#EF4444",
|
|
48
|
+
"#06B6D4",
|
|
49
|
+
];
|
|
50
|
+
function ConfettiOverlay() {
|
|
51
|
+
const pieces = useMemo(() => Array.from({ length: 80 }, (_, i) => ({
|
|
52
|
+
id: i,
|
|
53
|
+
left: Math.random() * 100,
|
|
54
|
+
delay: Math.random() * 1.2,
|
|
55
|
+
duration: 2.5 + Math.random() * 2,
|
|
56
|
+
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
|
57
|
+
size: 6 + Math.random() * 8,
|
|
58
|
+
rotation: Math.random() * 360,
|
|
59
|
+
})), []);
|
|
60
|
+
return (_jsxs("div", { style: {
|
|
61
|
+
position: "absolute",
|
|
62
|
+
inset: 0,
|
|
63
|
+
overflow: "hidden",
|
|
64
|
+
pointerEvents: "none",
|
|
65
|
+
zIndex: 20,
|
|
66
|
+
}, children: [pieces.map(({ id, left, delay, duration, color, size, rotation }) => (_jsx("div", { style: {
|
|
67
|
+
position: "absolute",
|
|
68
|
+
left: `${left}%`,
|
|
69
|
+
top: -20,
|
|
70
|
+
width: size,
|
|
71
|
+
height: size * 0.6,
|
|
72
|
+
backgroundColor: color,
|
|
73
|
+
borderRadius: 2,
|
|
74
|
+
transform: `rotate(${rotation}deg)`,
|
|
75
|
+
animation: `confettiFall ${duration}s ease-in ${delay}s forwards`,
|
|
76
|
+
} }, id))), _jsx("style", { children: `@keyframes confettiFall{0%{top:-20px;opacity:1;transform:rotate(0deg) translateX(0)}50%{opacity:1}100%{top:110%;opacity:0;transform:rotate(720deg) translateX(${Math.random() > 0.5 ? "" : "-"}40px)}}` })] }));
|
|
77
|
+
}
|
|
78
|
+
function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
79
|
+
const isDark = appearance === "dark";
|
|
80
|
+
const tint = isDark ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.2)";
|
|
81
|
+
const trackColor = isDark
|
|
82
|
+
? "rgba(255, 255, 255, 0.12)"
|
|
83
|
+
: "rgba(15, 23, 42, 0.1)";
|
|
84
|
+
return (_jsxs("div", { style: {
|
|
85
|
+
position: "absolute",
|
|
86
|
+
inset: 0,
|
|
87
|
+
display: "flex",
|
|
88
|
+
alignItems: "center",
|
|
89
|
+
justifyContent: "center",
|
|
90
|
+
zIndex: 10,
|
|
91
|
+
background: tint,
|
|
92
|
+
backdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
|
|
93
|
+
WebkitBackdropFilter: active
|
|
94
|
+
? "blur(10px) saturate(1.05)"
|
|
95
|
+
: "none",
|
|
96
|
+
opacity: active ? 1 : 0,
|
|
97
|
+
pointerEvents: active ? "auto" : "none",
|
|
98
|
+
transition: "opacity 0.28s ease, backdrop-filter 0.28s ease",
|
|
99
|
+
}, "aria-hidden": !active, "aria-busy": active ? true : undefined, "aria-live": active ? "polite" : undefined, "aria-label": active ? "Loading" : undefined, children: [_jsx("div", { "data-snap-loading-spinner": true, style: {
|
|
100
|
+
width: 30,
|
|
101
|
+
height: 30,
|
|
102
|
+
borderRadius: "50%",
|
|
103
|
+
border: `2.5px solid ${trackColor}`,
|
|
104
|
+
borderTopColor: accentHex,
|
|
105
|
+
opacity: 0.88,
|
|
106
|
+
animation: "snapViewSpin 0.75s linear infinite",
|
|
107
|
+
flexShrink: 0,
|
|
108
|
+
} }), _jsx("style", { children: `
|
|
109
|
+
@keyframes snapViewSpin {
|
|
110
|
+
to { transform: rotate(360deg); }
|
|
111
|
+
}
|
|
112
|
+
@media (prefers-reduced-motion: reduce) {
|
|
113
|
+
[data-snap-loading-spinner] {
|
|
114
|
+
animation: none;
|
|
115
|
+
border-top-color: ${accentHex};
|
|
116
|
+
opacity: 0.75;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
` })] }));
|
|
120
|
+
}
|
|
121
|
+
const PALETTE = [
|
|
122
|
+
"gray",
|
|
123
|
+
"blue",
|
|
124
|
+
"red",
|
|
125
|
+
"amber",
|
|
126
|
+
"green",
|
|
127
|
+
"teal",
|
|
128
|
+
"purple",
|
|
129
|
+
"pink",
|
|
130
|
+
];
|
|
131
|
+
// ─── SnapViewCore ────────────────────────────────────
|
|
132
|
+
// Shared rendering logic used by both v1 and v2.
|
|
133
|
+
export function SnapViewCore({ snap, handlers, loading = false, appearance = "dark", }) {
|
|
134
|
+
const spec = snap.ui;
|
|
135
|
+
const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
|
|
136
|
+
const stateRef = useRef(initialState);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
stateRef.current = {
|
|
139
|
+
inputs: {
|
|
140
|
+
...(initialState.inputs ?? {}),
|
|
141
|
+
},
|
|
142
|
+
theme: {
|
|
143
|
+
...(initialState.theme ?? {}),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}, [initialState]);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const catalogResult = snapJsonRenderCatalog.validate(spec);
|
|
149
|
+
if (!catalogResult.success) {
|
|
150
|
+
// eslint-disable-next-line no-console
|
|
151
|
+
console.warn("[Snap] catalog validation issues:", catalogResult.error);
|
|
152
|
+
}
|
|
153
|
+
}, [spec]);
|
|
154
|
+
const [pageKey, setPageKey] = useState(0);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
setPageKey((k) => k + 1);
|
|
157
|
+
}, [spec]);
|
|
158
|
+
const showConfetti = snap.effects?.includes("confetti");
|
|
159
|
+
const accentName = snap.theme?.accent ?? "purple";
|
|
160
|
+
const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
|
|
161
|
+
const previewSurfaceStyle = useMemo(() => {
|
|
162
|
+
const vars = {};
|
|
163
|
+
for (const c of PALETTE)
|
|
164
|
+
vars[`--snap-color-${c}`] = resolveSnapPaletteHex(c, appearance);
|
|
165
|
+
return {
|
|
166
|
+
...snapPreviewPrimaryCssProperties(accentName, appearance),
|
|
167
|
+
...vars,
|
|
168
|
+
};
|
|
169
|
+
}, [accentName, appearance]);
|
|
170
|
+
const handleAction = useCallback((name, params) => {
|
|
171
|
+
const inputs = (stateRef.current.inputs ?? {});
|
|
172
|
+
const p = (params ?? {});
|
|
173
|
+
switch (name) {
|
|
174
|
+
case "submit":
|
|
175
|
+
handlers.submit(String(p.target ?? ""), inputs);
|
|
176
|
+
break;
|
|
177
|
+
case "open_url":
|
|
178
|
+
handlers.open_url(String(p.target ?? ""));
|
|
179
|
+
break;
|
|
180
|
+
case "open_mini_app":
|
|
181
|
+
handlers.open_mini_app(String(p.target ?? ""));
|
|
182
|
+
break;
|
|
183
|
+
case "view_cast":
|
|
184
|
+
handlers.view_cast({ hash: String(p.hash ?? "") });
|
|
185
|
+
break;
|
|
186
|
+
case "view_profile":
|
|
187
|
+
handlers.view_profile({ fid: Number(p.fid ?? 0) });
|
|
188
|
+
break;
|
|
189
|
+
case "compose_cast":
|
|
190
|
+
handlers.compose_cast({
|
|
191
|
+
text: p.text ? String(p.text) : undefined,
|
|
192
|
+
channelKey: p.channelKey ? String(p.channelKey) : undefined,
|
|
193
|
+
embeds: Array.isArray(p.embeds)
|
|
194
|
+
? p.embeds
|
|
195
|
+
: undefined,
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
case "view_token":
|
|
199
|
+
handlers.view_token({ token: String(p.token ?? "") });
|
|
200
|
+
break;
|
|
201
|
+
case "send_token":
|
|
202
|
+
handlers.send_token({
|
|
203
|
+
token: String(p.token ?? ""),
|
|
204
|
+
amount: p.amount ? String(p.amount) : undefined,
|
|
205
|
+
recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
|
|
206
|
+
recipientAddress: p.recipientAddress
|
|
207
|
+
? String(p.recipientAddress)
|
|
208
|
+
: undefined,
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
case "swap_token":
|
|
212
|
+
handlers.swap_token({
|
|
213
|
+
sellToken: p.sellToken ? String(p.sellToken) : undefined,
|
|
214
|
+
buyToken: p.buyToken ? String(p.buyToken) : undefined,
|
|
215
|
+
});
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}, [handlers]);
|
|
221
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}), _jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading }), _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
222
|
+
applyStatePaths(stateRef.current, changes);
|
|
223
|
+
}, onAction: handleAction }, pageKey) }) })] }));
|
|
224
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SnapPage, SnapActionHandlers } from "../index.js";
|
|
2
|
+
export declare function SnapViewV1({ snap, handlers, loading, appearance, }: {
|
|
3
|
+
snap: SnapPage;
|
|
4
|
+
handlers: SnapActionHandlers;
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
appearance?: "light" | "dark";
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, }: {
|
|
9
|
+
snap: SnapPage;
|
|
10
|
+
handlers: SnapActionHandlers;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
appearance?: "light" | "dark";
|
|
13
|
+
maxWidth?: number;
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { SnapViewCore } from "../snap-view-core.js";
|
|
4
|
+
export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", }) {
|
|
5
|
+
return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
|
|
6
|
+
}
|
|
7
|
+
export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, }) {
|
|
8
|
+
return (_jsx("div", { style: { position: "relative", width: "100%", maxWidth }, children: _jsx(SnapViewV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }) }));
|
|
9
|
+
}
|