@farcaster/snap 2.0.3 → 2.1.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/react/components/cell-grid.d.ts +3 -1
- package/dist/react/components/cell-grid.js +8 -4
- package/dist/react/snap-view-core.js +7 -2
- package/dist/react/v1/snap-view.js +40 -34
- package/dist/react/v2/snap-view.js +92 -29
- package/dist/react-native/components/snap-cell-grid.d.ts +1 -1
- package/dist/react-native/components/snap-cell-grid.js +10 -4
- package/dist/react-native/confetti-overlay.js +33 -36
- package/dist/react-native/snap-view-core.js +8 -1
- package/dist/react-native/v1/snap-view.js +41 -47
- package/dist/react-native/v2/snap-view.js +78 -16
- package/dist/ui/catalog.js +1 -1
- package/dist/validator.js +8 -33
- package/llms.txt +22 -1
- package/package.json +1 -1
- package/src/react/components/cell-grid.tsx +11 -5
- package/src/react/snap-view-core.tsx +6 -2
- package/src/react/v1/snap-view.tsx +69 -63
- package/src/react/v2/snap-view.tsx +152 -61
- package/src/react-native/components/snap-cell-grid.tsx +11 -4
- package/src/react-native/confetti-overlay.tsx +40 -37
- package/src/react-native/snap-view-core.tsx +8 -0
- package/src/react-native/v1/snap-view.tsx +34 -42
- package/src/react-native/v2/snap-view.tsx +131 -30
- package/src/ui/catalog.ts +1 -1
- package/src/validator.ts +22 -46
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export declare function SnapCellGrid({ element: { props }, }: {
|
|
1
|
+
export declare function SnapCellGrid({ element: { props, on }, emit, }: {
|
|
2
2
|
element: {
|
|
3
3
|
props: Record<string, unknown>;
|
|
4
|
+
on?: Record<string, unknown>;
|
|
4
5
|
};
|
|
6
|
+
emit: (name: string) => void;
|
|
5
7
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,14 +4,16 @@ import { useStateStore } from "@json-render/react";
|
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
5
|
import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
6
6
|
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
7
|
-
export function SnapCellGrid({ element: { props }, }) {
|
|
7
|
+
export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
8
8
|
const { get, set } = useStateStore();
|
|
9
9
|
const colors = useSnapColors();
|
|
10
10
|
const cols = Number(props.cols ?? 2);
|
|
11
11
|
const rows = Number(props.rows ?? 2);
|
|
12
12
|
const select = String(props.select ?? "off");
|
|
13
|
-
const interactive = select !== "off";
|
|
14
13
|
const isMultiple = select === "multiple";
|
|
14
|
+
const isSelectable = select !== "off";
|
|
15
|
+
const hasPressAction = Boolean(on?.press);
|
|
16
|
+
const interactive = isSelectable || hasPressAction;
|
|
15
17
|
const cells = Array.isArray(props.cells) ? props.cells : [];
|
|
16
18
|
const gap = String(props.gap ?? "sm");
|
|
17
19
|
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
@@ -28,7 +30,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
28
30
|
selectedSet.add(part);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
|
-
const isSelected = (r, c) => selectedSet.has(`${r},${c}`);
|
|
33
|
+
const isSelected = (r, c) => isSelectable && selectedSet.has(`${r},${c}`);
|
|
32
34
|
const handleTap = (r, c) => {
|
|
33
35
|
const key = `${r},${c}`;
|
|
34
36
|
if (isMultiple) {
|
|
@@ -42,6 +44,8 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
42
44
|
else {
|
|
43
45
|
set(tapPath, key);
|
|
44
46
|
}
|
|
47
|
+
if (hasPressAction)
|
|
48
|
+
emit("press");
|
|
45
49
|
};
|
|
46
50
|
const cellMap = new Map();
|
|
47
51
|
for (const c of cells) {
|
|
@@ -72,7 +76,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
72
76
|
}, children: cell?.content ?? "" }, `${r}-${c}`));
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
|
-
const selectionLabel =
|
|
79
|
+
const selectionLabel = isSelectable && selectedSet.size > 0
|
|
76
80
|
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
77
81
|
: null;
|
|
78
82
|
return (_jsxs("div", { children: [_jsx("div", { style: {
|
|
@@ -155,7 +155,12 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
155
155
|
useEffect(() => {
|
|
156
156
|
setPageKey((k) => k + 1);
|
|
157
157
|
}, [spec]);
|
|
158
|
-
const showConfetti = snap.effects?.includes("confetti");
|
|
158
|
+
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
159
|
+
const [confettiKey, setConfettiKey] = useState(0);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (showConfetti)
|
|
162
|
+
setConfettiKey((k) => k + 1);
|
|
163
|
+
}, [showConfetti, snap]);
|
|
159
164
|
const accentName = snap.theme?.accent ?? "purple";
|
|
160
165
|
const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
|
|
161
166
|
const previewSurfaceStyle = useMemo(() => {
|
|
@@ -221,7 +226,7 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
221
226
|
break;
|
|
222
227
|
}
|
|
223
228
|
}, [handlers]);
|
|
224
|
-
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _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
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
225
230
|
applyStatePaths(stateRef.current, changes);
|
|
226
231
|
}, onAction: handleAction }, pageKey) }) })] }));
|
|
227
232
|
}
|
|
@@ -49,40 +49,46 @@ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark
|
|
|
49
49
|
position: "relative",
|
|
50
50
|
width: "100%",
|
|
51
51
|
maxWidth,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
52
|
+
}, children: [_jsxs("div", { style: {
|
|
53
|
+
position: "relative",
|
|
54
|
+
overflow: "hidden",
|
|
55
|
+
...(plain ? {} : {
|
|
56
|
+
borderRadius: 16,
|
|
57
|
+
border: `1px solid ${borderColor}`,
|
|
58
|
+
backgroundColor: surfaceBg,
|
|
59
|
+
}),
|
|
60
|
+
}, children: [_jsx("div", { style: isClipped
|
|
61
|
+
? {
|
|
62
|
+
maxHeight: SNAP_MAX_HEIGHT,
|
|
63
|
+
overflow: "hidden",
|
|
64
|
+
}
|
|
65
|
+
: undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: null }) }) }), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null] }), isExpandable ? (_jsx("button", { type: "button", "aria-expanded": isExpanded, onClick: () => setIsExpanded((value) => !value), style: {
|
|
66
|
+
position: "absolute",
|
|
67
|
+
bottom: 0,
|
|
68
|
+
left: "50%",
|
|
69
|
+
transform: "translate(-50%, 50%)",
|
|
70
|
+
appearance: "none",
|
|
71
|
+
border: `1px solid ${borderColor}`,
|
|
72
|
+
borderRadius: 9999,
|
|
73
|
+
backgroundColor: isDark ? "rgba(30,30,30,0.6)" : "rgba(255,255,255,0.6)",
|
|
74
|
+
backdropFilter: "blur(12px) saturate(180%)",
|
|
75
|
+
WebkitBackdropFilter: "blur(12px) saturate(180%)",
|
|
76
|
+
color: toggleText,
|
|
77
|
+
padding: "2px 10px",
|
|
78
|
+
fontSize: 12,
|
|
79
|
+
lineHeight: "16px",
|
|
80
|
+
fontWeight: 600,
|
|
81
|
+
cursor: "pointer",
|
|
82
|
+
zIndex: 11,
|
|
83
|
+
}, onMouseEnter: (event) => {
|
|
84
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
85
|
+
? "rgba(50,50,50,0.7)"
|
|
86
|
+
: "rgba(245,245,245,0.75)";
|
|
87
|
+
}, onMouseLeave: (event) => {
|
|
88
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
89
|
+
? "rgba(30,30,30,0.6)"
|
|
90
|
+
: "rgba(255,255,255,0.6)";
|
|
91
|
+
}, children: isExpanded ? "Show less" : "Show more" })) : null, actionError && (_jsx("div", { style: {
|
|
86
92
|
padding: "8px 12px",
|
|
87
93
|
fontSize: 13,
|
|
88
94
|
color: appearance === "dark"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { validateSnapResponse } from "../../validator.js";
|
|
5
5
|
import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core.js";
|
|
6
6
|
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
|
|
@@ -44,45 +44,108 @@ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark
|
|
|
44
44
|
}
|
|
45
45
|
// ─── SnapCardV2 ──────────────────────────────────────
|
|
46
46
|
export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
|
|
47
|
-
const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
|
|
48
47
|
const isDark = appearance === "dark";
|
|
49
48
|
const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
|
|
50
49
|
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
51
50
|
const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
|
|
51
|
+
const toggleBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
|
|
52
|
+
const toggleBgHover = isDark
|
|
53
|
+
? "rgba(255,255,255,0.1)"
|
|
54
|
+
: "rgba(0,0,0,0.08)";
|
|
55
|
+
const toggleText = isDark ? "rgba(255,255,255,0.82)" : "rgba(0,0,0,0.72)";
|
|
52
56
|
const accentHex = useMemo(() => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance), [snap.theme?.accent, appearance]);
|
|
57
|
+
const contentRef = useRef(null);
|
|
58
|
+
const [isExpandable, setIsExpandable] = useState(false);
|
|
59
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setIsExpanded(false);
|
|
62
|
+
}, [snap]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const node = contentRef.current;
|
|
65
|
+
if (!node)
|
|
66
|
+
return;
|
|
67
|
+
const measure = () => {
|
|
68
|
+
setIsExpandable(node.scrollHeight > SNAP_MAX_HEIGHT + 1);
|
|
69
|
+
};
|
|
70
|
+
measure();
|
|
71
|
+
if (typeof ResizeObserver === "undefined")
|
|
72
|
+
return;
|
|
73
|
+
const observer = new ResizeObserver(() => {
|
|
74
|
+
measure();
|
|
75
|
+
});
|
|
76
|
+
observer.observe(node);
|
|
77
|
+
return () => observer.disconnect();
|
|
78
|
+
}, [snap, plain, showOverflowWarning]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!isExpandable) {
|
|
81
|
+
setIsExpanded(false);
|
|
82
|
+
}
|
|
83
|
+
}, [isExpandable]);
|
|
84
|
+
const isClipped = !showOverflowWarning && isExpandable && !isExpanded;
|
|
85
|
+
const containerMaxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : undefined;
|
|
53
86
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
|
|
54
87
|
position: "relative",
|
|
55
88
|
width: "100%",
|
|
56
89
|
maxWidth,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
}, children: [_jsxs("div", { style: {
|
|
91
|
+
position: "relative",
|
|
92
|
+
maxHeight: containerMaxHeight,
|
|
93
|
+
overflow: "hidden",
|
|
94
|
+
...(plain ? {} : {
|
|
95
|
+
borderRadius: 16,
|
|
96
|
+
border: `1px solid ${borderColor}`,
|
|
97
|
+
backgroundColor: surfaceBg,
|
|
98
|
+
}),
|
|
99
|
+
}, children: [_jsx("div", { style: isClipped
|
|
100
|
+
? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" }
|
|
101
|
+
: undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null }) }) }), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, showOverflowWarning && (_jsxs("div", { style: {
|
|
102
|
+
position: "absolute",
|
|
103
|
+
top: SNAP_MAX_HEIGHT,
|
|
104
|
+
left: 0,
|
|
105
|
+
right: 0,
|
|
106
|
+
bottom: 0,
|
|
107
|
+
pointerEvents: "none",
|
|
108
|
+
zIndex: 10,
|
|
109
|
+
}, children: [_jsx("div", { style: { borderTop: "1px dashed rgba(255,100,100,0.6)", position: "relative" }, children: _jsxs("span", { style: {
|
|
110
|
+
position: "absolute",
|
|
111
|
+
top: -10,
|
|
112
|
+
right: 0,
|
|
113
|
+
fontSize: 10,
|
|
114
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
115
|
+
color: "rgba(255,100,100,0.7)",
|
|
116
|
+
background: bg,
|
|
117
|
+
padding: "1px 4px",
|
|
118
|
+
borderRadius: 3,
|
|
119
|
+
}, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx("div", { style: {
|
|
120
|
+
height: "100%",
|
|
121
|
+
background: "repeating-linear-gradient(-45deg, transparent, transparent 8px, rgba(255,100,100,0.06) 8px, rgba(255,100,100,0.06) 16px)",
|
|
122
|
+
} })] }))] }), !showOverflowWarning && isExpandable ? (_jsx("button", { type: "button", "aria-expanded": isExpanded, onClick: () => setIsExpanded((value) => !value), style: {
|
|
65
123
|
position: "absolute",
|
|
66
|
-
top: SNAP_MAX_HEIGHT,
|
|
67
|
-
left: 0,
|
|
68
|
-
right: 0,
|
|
69
124
|
bottom: 0,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
125
|
+
left: "50%",
|
|
126
|
+
transform: "translate(-50%, 50%)",
|
|
127
|
+
appearance: "none",
|
|
128
|
+
border: `1px solid ${borderColor}`,
|
|
129
|
+
borderRadius: 9999,
|
|
130
|
+
backgroundColor: isDark ? "rgba(30,30,30,0.6)" : "rgba(255,255,255,0.6)",
|
|
131
|
+
backdropFilter: "blur(12px) saturate(180%)",
|
|
132
|
+
WebkitBackdropFilter: "blur(12px) saturate(180%)",
|
|
133
|
+
color: toggleText,
|
|
134
|
+
padding: "2px 10px",
|
|
135
|
+
fontSize: 12,
|
|
136
|
+
lineHeight: "16px",
|
|
137
|
+
fontWeight: 600,
|
|
138
|
+
cursor: "pointer",
|
|
139
|
+
zIndex: 11,
|
|
140
|
+
}, onMouseEnter: (event) => {
|
|
141
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
142
|
+
? "rgba(50,50,50,0.7)"
|
|
143
|
+
: "rgba(245,245,245,0.75)";
|
|
144
|
+
}, onMouseLeave: (event) => {
|
|
145
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
146
|
+
? "rgba(30,30,30,0.6)"
|
|
147
|
+
: "rgba(255,255,255,0.6)";
|
|
148
|
+
}, children: isExpanded ? "Show less" : "Show more" })) : null] }), actionError && (_jsx("div", { style: {
|
|
86
149
|
maxWidth,
|
|
87
150
|
padding: "8px 12px",
|
|
88
151
|
fontSize: 13,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
|
-
export declare function SnapCellGrid({ element
|
|
2
|
+
export declare function SnapCellGrid({ element, emit, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
|
|
@@ -4,7 +4,9 @@ import { useStateStore } from "@json-render/react-native";
|
|
|
4
4
|
import { useSnapPalette } from "../use-snap-palette.js";
|
|
5
5
|
import { useSnapTheme } from "../theme.js";
|
|
6
6
|
import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
7
|
-
export function SnapCellGrid({ element
|
|
7
|
+
export function SnapCellGrid({ element, emit, }) {
|
|
8
|
+
const { props } = element;
|
|
9
|
+
const on = element.on;
|
|
8
10
|
const { hex, appearance } = useSnapPalette();
|
|
9
11
|
const { colors } = useSnapTheme();
|
|
10
12
|
const { get, set } = useStateStore();
|
|
@@ -16,8 +18,10 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
16
18
|
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
17
19
|
const gapPx = gapMap[gap] ?? 1;
|
|
18
20
|
const select = String(props.select ?? "off");
|
|
19
|
-
const interactive = select !== "off";
|
|
20
21
|
const isMultiple = select === "multiple";
|
|
22
|
+
const isSelectable = select !== "off";
|
|
23
|
+
const hasPressAction = Boolean(on?.press);
|
|
24
|
+
const interactive = isSelectable || hasPressAction;
|
|
21
25
|
const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
|
|
22
26
|
const tapPath = `/inputs/${name}`;
|
|
23
27
|
const tapRaw = get(tapPath);
|
|
@@ -28,7 +32,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
28
32
|
selectedSet.add(part);
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
|
-
const isSelected = (r, c) => selectedSet.has(`${r},${c}`);
|
|
35
|
+
const isSelected = (r, c) => isSelectable && selectedSet.has(`${r},${c}`);
|
|
32
36
|
const handleTap = (r, c) => {
|
|
33
37
|
const key = `${r},${c}`;
|
|
34
38
|
if (isMultiple) {
|
|
@@ -42,6 +46,8 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
42
46
|
else {
|
|
43
47
|
set(tapPath, key);
|
|
44
48
|
}
|
|
49
|
+
if (hasPressAction)
|
|
50
|
+
emit("press");
|
|
45
51
|
};
|
|
46
52
|
const cellMap = new Map();
|
|
47
53
|
for (const c of cells) {
|
|
@@ -69,7 +75,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
69
75
|
}
|
|
70
76
|
rowEls.push(_jsx(View, { style: [styles.gridRow, { gap: gapPx }], children: rowCells }, r));
|
|
71
77
|
}
|
|
72
|
-
const selectionLabel =
|
|
78
|
+
const selectionLabel = isSelectable && selectedSet.size > 0
|
|
73
79
|
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
74
80
|
: null;
|
|
75
81
|
return (_jsxs(View, { style: [styles.wrap, { gap: gapPx, backgroundColor: colors.muted, padding: 4, borderRadius: 8 }], children: [rowEls, selectionLabel ? (_jsx(Text, { style: [styles.selectionText, { color: colors.textSecondary }], children: selectionLabel })) : null] }));
|
|
@@ -20,63 +20,60 @@ export function ConfettiOverlay() {
|
|
|
20
20
|
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
|
21
21
|
size: 6 + Math.random() * 8,
|
|
22
22
|
startRotation: Math.random() * 360,
|
|
23
|
-
|
|
23
|
+
// Per-piece swirl: amplitude, frequency (full oscillations), phase.
|
|
24
|
+
swirlAmp: 20 + Math.random() * 40,
|
|
25
|
+
swirlFreq: 1 + Math.random() * 1.5,
|
|
26
|
+
swirlPhase: Math.random() * Math.PI * 2,
|
|
24
27
|
})),
|
|
25
28
|
// width captured once on mount; intentional stable dep
|
|
26
29
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
30
|
[]);
|
|
28
31
|
const anims = useRef(pieces.map(() => ({
|
|
29
|
-
|
|
30
|
-
opacity: new Animated.Value(1),
|
|
31
|
-
rotate: new Animated.Value(0),
|
|
32
|
-
translateX: new Animated.Value(0),
|
|
32
|
+
progress: new Animated.Value(0),
|
|
33
33
|
}))).current;
|
|
34
34
|
useEffect(() => {
|
|
35
35
|
const animations = pieces.map((piece, i) => {
|
|
36
36
|
const anim = anims[i];
|
|
37
|
-
anim.
|
|
38
|
-
anim.opacity.setValue(1);
|
|
39
|
-
anim.rotate.setValue(0);
|
|
40
|
-
anim.translateX.setValue(0);
|
|
37
|
+
anim.progress.setValue(0);
|
|
41
38
|
return Animated.sequence([
|
|
42
39
|
Animated.delay(piece.delay),
|
|
43
|
-
Animated.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}),
|
|
49
|
-
Animated.timing(anim.opacity, {
|
|
50
|
-
toValue: 0,
|
|
51
|
-
duration: piece.duration,
|
|
52
|
-
useNativeDriver: true,
|
|
53
|
-
}),
|
|
54
|
-
Animated.timing(anim.rotate, {
|
|
55
|
-
toValue: 720,
|
|
56
|
-
duration: piece.duration,
|
|
57
|
-
useNativeDriver: true,
|
|
58
|
-
}),
|
|
59
|
-
Animated.timing(anim.translateX, {
|
|
60
|
-
toValue: piece.driftX,
|
|
61
|
-
duration: piece.duration,
|
|
62
|
-
useNativeDriver: true,
|
|
63
|
-
}),
|
|
64
|
-
]),
|
|
40
|
+
Animated.timing(anim.progress, {
|
|
41
|
+
toValue: 1,
|
|
42
|
+
duration: piece.duration,
|
|
43
|
+
useNativeDriver: true,
|
|
44
|
+
}),
|
|
65
45
|
]);
|
|
66
46
|
});
|
|
67
47
|
const composite = Animated.parallel(animations);
|
|
68
48
|
composite.start();
|
|
69
49
|
return () => composite.stop();
|
|
70
50
|
}, [pieces, anims, height]);
|
|
51
|
+
// Sample the sine curve at fixed progress points to build an interpolation
|
|
52
|
+
// that drives horizontal swirl on the native driver.
|
|
53
|
+
const SAMPLE_COUNT = 21;
|
|
54
|
+
const samplePoints = Array.from({ length: SAMPLE_COUNT }, (_, k) => k / (SAMPLE_COUNT - 1));
|
|
71
55
|
return (_jsx(View, { style: [StyleSheet.absoluteFill, styles.container], pointerEvents: "none", children: pieces.map((piece, i) => {
|
|
72
56
|
const anim = anims[i];
|
|
73
|
-
const
|
|
74
|
-
inputRange: [0,
|
|
57
|
+
const translateY = anim.progress.interpolate({
|
|
58
|
+
inputRange: [0, 1],
|
|
59
|
+
outputRange: [-20, height + 20],
|
|
60
|
+
});
|
|
61
|
+
const rotate = anim.progress.interpolate({
|
|
62
|
+
inputRange: [0, 1],
|
|
75
63
|
outputRange: [
|
|
76
64
|
`${piece.startRotation}deg`,
|
|
77
65
|
`${piece.startRotation + 720}deg`,
|
|
78
66
|
],
|
|
79
67
|
});
|
|
68
|
+
const opacity = anim.progress.interpolate({
|
|
69
|
+
inputRange: [0, 0.5, 1],
|
|
70
|
+
outputRange: [1, 1, 0],
|
|
71
|
+
});
|
|
72
|
+
const translateX = anim.progress.interpolate({
|
|
73
|
+
inputRange: samplePoints,
|
|
74
|
+
outputRange: samplePoints.map((t) => Math.sin(t * piece.swirlFreq * Math.PI * 2 + piece.swirlPhase) *
|
|
75
|
+
piece.swirlAmp),
|
|
76
|
+
});
|
|
80
77
|
return (_jsx(Animated.View, { style: [
|
|
81
78
|
styles.piece,
|
|
82
79
|
{
|
|
@@ -84,10 +81,10 @@ export function ConfettiOverlay() {
|
|
|
84
81
|
width: piece.size,
|
|
85
82
|
height: piece.size * 0.6,
|
|
86
83
|
backgroundColor: piece.color,
|
|
87
|
-
opacity
|
|
84
|
+
opacity,
|
|
88
85
|
transform: [
|
|
89
|
-
{ translateY
|
|
90
|
-
{ translateX
|
|
86
|
+
{ translateY },
|
|
87
|
+
{ translateX },
|
|
91
88
|
{ rotate },
|
|
92
89
|
],
|
|
93
90
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
3
|
import { SnapCatalogView } from "./catalog-renderer.js";
|
|
4
|
+
import { ConfettiOverlay } from "./confetti-overlay.js";
|
|
4
5
|
import { useSnapTheme } from "./theme.js";
|
|
5
6
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
6
7
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
@@ -79,6 +80,12 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
79
80
|
useEffect(() => {
|
|
80
81
|
setPageKey((k) => k + 1);
|
|
81
82
|
}, [spec]);
|
|
83
|
+
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
84
|
+
const [confettiKey, setConfettiKey] = useState(0);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (showConfetti)
|
|
87
|
+
setConfettiKey((k) => k + 1);
|
|
88
|
+
}, [showConfetti, snap]);
|
|
82
89
|
const handlersRef = useRef(handlers);
|
|
83
90
|
handlersRef.current = handlers;
|
|
84
91
|
const handleAction = useCallback((name, params) => {
|
|
@@ -140,7 +147,7 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
140
147
|
: loadingOverlay
|
|
141
148
|
: null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
142
149
|
applyStatePaths(stateRef.current, changes);
|
|
143
|
-
}, onAction: handleAction }, pageKey)] }));
|
|
150
|
+
}, onAction: handleAction }, pageKey), showConfetti && _jsx(ConfettiOverlay, {}, confettiKey)] }));
|
|
144
151
|
}
|
|
145
152
|
export function SnapLoadingOverlay({ appearance, accentHex, }) {
|
|
146
153
|
return (_jsx(View, { style: [
|
|
@@ -23,39 +23,36 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
23
23
|
}, [snap]);
|
|
24
24
|
const isExpandable = contentHeight > SNAP_MAX_HEIGHT + 1;
|
|
25
25
|
const isClipped = isExpandable && !isExpanded;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
], onPress: () => {
|
|
57
|
-
setIsExpanded((value) => !value);
|
|
58
|
-
}, children: _jsx(Text, { style: [cardStyles.expandButtonText, { color: colors.text }], children: isExpanded ? "Show less" : "Show more" }) }) })) : null] }) }), actionError && (_jsx(Text, { style: [
|
|
26
|
+
const isDark = mode === "dark";
|
|
27
|
+
const pillBg = isDark ? "rgba(40,40,40,0.92)" : "rgba(255,255,255,0.92)";
|
|
28
|
+
const pillBgPressed = isDark ? "rgba(60,60,60,0.95)" : "rgba(240,240,240,0.95)";
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsxs(View, { style: cardStyles.frameRing, children: [_jsxs(View, { style: [
|
|
30
|
+
plain ? undefined : cardStyles.card,
|
|
31
|
+
plain ? undefined : {
|
|
32
|
+
borderRadius,
|
|
33
|
+
borderColor: colors.border,
|
|
34
|
+
backgroundColor: colors.surface,
|
|
35
|
+
},
|
|
36
|
+
], children: [_jsx(View, { style: isClipped ? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" } : undefined, children: _jsx(View, { collapsable: false, onLayout: (event) => {
|
|
37
|
+
const nextHeight = Math.round(event.nativeEvent.layout.height);
|
|
38
|
+
setContentHeight((currentHeight) => isClipped
|
|
39
|
+
? Math.max(currentHeight, nextHeight)
|
|
40
|
+
: currentHeight === nextHeight
|
|
41
|
+
? currentHeight
|
|
42
|
+
: nextHeight);
|
|
43
|
+
}, style: plain ? undefined : cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: null }) }) }), loading
|
|
44
|
+
? loadingOverlay === undefined
|
|
45
|
+
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
46
|
+
: loadingOverlay
|
|
47
|
+
: null] }), isExpandable ? (_jsx(View, { pointerEvents: "box-none", style: cardStyles.expandFloat, children: _jsx(Pressable, { style: ({ pressed }) => [
|
|
48
|
+
cardStyles.expandButton,
|
|
49
|
+
{
|
|
50
|
+
backgroundColor: pressed ? pillBgPressed : pillBg,
|
|
51
|
+
borderColor: colors.border,
|
|
52
|
+
},
|
|
53
|
+
], onPress: () => {
|
|
54
|
+
setIsExpanded((value) => !value);
|
|
55
|
+
}, children: _jsx(Text, { style: [cardStyles.expandButtonText, { color: colors.text }], children: isExpanded ? "Show less" : "Show more" }) }) })) : null] }), actionError && (_jsx(Text, { style: [
|
|
59
56
|
cardStyles.actionError,
|
|
60
57
|
{
|
|
61
58
|
color: appearance === "dark"
|
|
@@ -71,30 +68,27 @@ const cardStyles = StyleSheet.create({
|
|
|
71
68
|
frameRing: { alignSelf: "stretch" },
|
|
72
69
|
card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
|
|
73
70
|
body: { paddingHorizontal: 16, paddingVertical: 16 },
|
|
74
|
-
|
|
71
|
+
expandFloat: {
|
|
72
|
+
position: "absolute",
|
|
73
|
+
left: 0,
|
|
74
|
+
right: 0,
|
|
75
|
+
bottom: -14,
|
|
76
|
+
height: 28,
|
|
75
77
|
alignItems: "center",
|
|
76
|
-
|
|
77
|
-
paddingTop: 10,
|
|
78
|
-
paddingBottom: 12,
|
|
79
|
-
borderTopWidth: StyleSheet.hairlineWidth,
|
|
80
|
-
},
|
|
81
|
-
expandRowPlain: {
|
|
82
|
-
paddingHorizontal: 0,
|
|
83
|
-
paddingTop: 8,
|
|
84
|
-
paddingBottom: 0,
|
|
85
|
-
borderTopWidth: 0,
|
|
78
|
+
justifyContent: "center",
|
|
86
79
|
},
|
|
87
80
|
expandButton: {
|
|
88
81
|
minWidth: 92,
|
|
89
82
|
alignItems: "center",
|
|
90
83
|
justifyContent: "center",
|
|
91
84
|
borderRadius: 9999,
|
|
85
|
+
borderWidth: 1,
|
|
92
86
|
paddingHorizontal: 10,
|
|
93
|
-
paddingVertical:
|
|
87
|
+
paddingVertical: 4,
|
|
94
88
|
},
|
|
95
89
|
expandButtonText: {
|
|
96
|
-
fontSize:
|
|
97
|
-
lineHeight:
|
|
90
|
+
fontSize: 12,
|
|
91
|
+
lineHeight: 16,
|
|
98
92
|
fontWeight: "600",
|
|
99
93
|
},
|
|
100
94
|
actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
|