@farcaster/snap 2.0.2 → 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/index.d.ts +3 -1
- package/dist/react/index.js +3 -3
- package/dist/react/snap-view-core.d.ts +12 -1
- package/dist/react/snap-view-core.js +10 -5
- package/dist/react/v1/snap-view.d.ts +7 -2
- package/dist/react/v1/snap-view.js +48 -40
- package/dist/react/v2/snap-view.d.ts +6 -2
- package/dist/react/v2/snap-view.js +98 -33
- 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/index.d.ts +3 -1
- package/dist/react-native/index.js +3 -3
- package/dist/react-native/snap-view-core.d.ts +11 -1
- package/dist/react-native/snap-view-core.js +25 -9
- package/dist/react-native/v1/snap-view.d.ts +9 -3
- package/dist/react-native/v1/snap-view.js +51 -52
- package/dist/react-native/v2/snap-view.d.ts +8 -3
- package/dist/react-native/v2/snap-view.js +92 -21
- package/dist/ui/catalog.js +2 -2
- package/dist/validator.js +8 -33
- package/llms.txt +26 -3
- package/package.json +1 -1
- package/src/react/components/cell-grid.tsx +11 -5
- package/src/react/index.tsx +5 -0
- package/src/react/snap-view-core.tsx +23 -8
- package/src/react/v1/snap-view.tsx +84 -55
- package/src/react/v2/snap-view.tsx +165 -52
- package/src/react-native/components/snap-cell-grid.tsx +11 -4
- package/src/react-native/confetti-overlay.tsx +40 -37
- package/src/react-native/index.tsx +5 -0
- package/src/react-native/snap-view-core.tsx +56 -14
- package/src/react-native/v1/snap-view.tsx +71 -47
- package/src/react-native/v2/snap-view.tsx +166 -28
- package/src/ui/catalog.ts +2 -2
- package/src/validator.ts +22 -46
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { type ReactNode, useEffect, useMemo } from "react";
|
|
3
|
+
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { validateSnapResponse } from "../../validator.js";
|
|
5
5
|
import type { ValidationResult } from "../../validator.js";
|
|
6
|
-
import { SnapViewCore } from "../snap-view-core";
|
|
6
|
+
import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
|
|
7
|
+
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
|
|
7
8
|
import type { SnapPage, SnapActionHandlers } from "../index";
|
|
8
9
|
|
|
9
10
|
const SNAP_MAX_HEIGHT = 500;
|
|
@@ -45,6 +46,7 @@ export function SnapViewV2({
|
|
|
45
46
|
appearance = "dark",
|
|
46
47
|
onValidationError,
|
|
47
48
|
validationErrorFallback,
|
|
49
|
+
loadingOverlay,
|
|
48
50
|
}: {
|
|
49
51
|
snap: SnapPage;
|
|
50
52
|
handlers: SnapActionHandlers;
|
|
@@ -52,6 +54,8 @@ export function SnapViewV2({
|
|
|
52
54
|
appearance?: "light" | "dark";
|
|
53
55
|
onValidationError?: (result: ValidationResult) => void;
|
|
54
56
|
validationErrorFallback?: ReactNode;
|
|
57
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
58
|
+
loadingOverlay?: ReactNode;
|
|
55
59
|
}) {
|
|
56
60
|
const validation = useMemo(() => validateSnapResponse(snap), [snap]);
|
|
57
61
|
const valid = validation.valid;
|
|
@@ -79,6 +83,7 @@ export function SnapViewV2({
|
|
|
79
83
|
handlers={handlers}
|
|
80
84
|
loading={loading}
|
|
81
85
|
appearance={appearance}
|
|
86
|
+
loadingOverlay={loadingOverlay}
|
|
82
87
|
/>
|
|
83
88
|
);
|
|
84
89
|
}
|
|
@@ -96,6 +101,7 @@ export function SnapCardV2({
|
|
|
96
101
|
validationErrorFallback,
|
|
97
102
|
actionError,
|
|
98
103
|
plain = false,
|
|
104
|
+
loadingOverlay,
|
|
99
105
|
}: {
|
|
100
106
|
snap: SnapPage;
|
|
101
107
|
handlers: SnapActionHandlers;
|
|
@@ -107,12 +113,57 @@ export function SnapCardV2({
|
|
|
107
113
|
validationErrorFallback?: ReactNode;
|
|
108
114
|
actionError?: string | null;
|
|
109
115
|
plain?: boolean;
|
|
116
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
117
|
+
loadingOverlay?: ReactNode;
|
|
110
118
|
}) {
|
|
111
|
-
const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
|
|
112
119
|
const isDark = appearance === "dark";
|
|
113
120
|
const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
|
|
114
121
|
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
115
122
|
const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
|
|
123
|
+
const toggleBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
|
|
124
|
+
const toggleBgHover = isDark
|
|
125
|
+
? "rgba(255,255,255,0.1)"
|
|
126
|
+
: "rgba(0,0,0,0.08)";
|
|
127
|
+
const toggleText = isDark ? "rgba(255,255,255,0.82)" : "rgba(0,0,0,0.72)";
|
|
128
|
+
const accentHex = useMemo(
|
|
129
|
+
() => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance),
|
|
130
|
+
[snap.theme?.accent, appearance],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
134
|
+
const [isExpandable, setIsExpandable] = useState(false);
|
|
135
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
setIsExpanded(false);
|
|
139
|
+
}, [snap]);
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const node = contentRef.current;
|
|
143
|
+
if (!node) return;
|
|
144
|
+
|
|
145
|
+
const measure = () => {
|
|
146
|
+
setIsExpandable(node.scrollHeight > SNAP_MAX_HEIGHT + 1);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
measure();
|
|
150
|
+
|
|
151
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
152
|
+
const observer = new ResizeObserver(() => {
|
|
153
|
+
measure();
|
|
154
|
+
});
|
|
155
|
+
observer.observe(node);
|
|
156
|
+
return () => observer.disconnect();
|
|
157
|
+
}, [snap, plain, showOverflowWarning]);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!isExpandable) {
|
|
161
|
+
setIsExpanded(false);
|
|
162
|
+
}
|
|
163
|
+
}, [isExpandable]);
|
|
164
|
+
|
|
165
|
+
const isClipped = !showOverflowWarning && isExpandable && !isExpanded;
|
|
166
|
+
const containerMaxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : undefined;
|
|
116
167
|
|
|
117
168
|
return (
|
|
118
169
|
<>
|
|
@@ -121,63 +172,125 @@ export function SnapCardV2({
|
|
|
121
172
|
position: "relative",
|
|
122
173
|
width: "100%",
|
|
123
174
|
maxWidth,
|
|
124
|
-
maxHeight,
|
|
125
|
-
overflow: "hidden",
|
|
126
|
-
...(plain ? {} : {
|
|
127
|
-
borderRadius: 16,
|
|
128
|
-
border: `1px solid ${borderColor}`,
|
|
129
|
-
backgroundColor: surfaceBg,
|
|
130
|
-
}),
|
|
131
175
|
}}
|
|
132
176
|
>
|
|
133
|
-
<div
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
position: "relative",
|
|
180
|
+
maxHeight: containerMaxHeight,
|
|
181
|
+
overflow: "hidden",
|
|
182
|
+
...(plain ? {} : {
|
|
183
|
+
borderRadius: 16,
|
|
184
|
+
border: `1px solid ${borderColor}`,
|
|
185
|
+
backgroundColor: surfaceBg,
|
|
186
|
+
}),
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
144
189
|
<div
|
|
145
|
-
style={
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
bottom: 0,
|
|
151
|
-
pointerEvents: "none",
|
|
152
|
-
zIndex: 10,
|
|
153
|
-
}}
|
|
190
|
+
style={
|
|
191
|
+
isClipped
|
|
192
|
+
? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" }
|
|
193
|
+
: undefined
|
|
194
|
+
}
|
|
154
195
|
>
|
|
155
|
-
<div style={
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
padding: "1px 4px",
|
|
166
|
-
borderRadius: 3,
|
|
167
|
-
}}
|
|
168
|
-
>
|
|
169
|
-
{SNAP_MAX_HEIGHT}px
|
|
170
|
-
</span>
|
|
196
|
+
<div ref={contentRef} style={plain ? undefined : { padding: 16 }}>
|
|
197
|
+
<SnapViewV2
|
|
198
|
+
snap={snap}
|
|
199
|
+
handlers={handlers}
|
|
200
|
+
loading={loading}
|
|
201
|
+
appearance={appearance}
|
|
202
|
+
onValidationError={onValidationError}
|
|
203
|
+
validationErrorFallback={validationErrorFallback}
|
|
204
|
+
loadingOverlay={null}
|
|
205
|
+
/>
|
|
171
206
|
</div>
|
|
207
|
+
</div>
|
|
208
|
+
{loadingOverlay === undefined ? (
|
|
209
|
+
<SnapLoadingOverlay
|
|
210
|
+
appearance={appearance}
|
|
211
|
+
accentHex={accentHex}
|
|
212
|
+
active={loading}
|
|
213
|
+
/>
|
|
214
|
+
) : loading ? (
|
|
215
|
+
<>{loadingOverlay}</>
|
|
216
|
+
) : null}
|
|
217
|
+
{showOverflowWarning && (
|
|
172
218
|
<div
|
|
173
219
|
style={{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
220
|
+
position: "absolute",
|
|
221
|
+
top: SNAP_MAX_HEIGHT,
|
|
222
|
+
left: 0,
|
|
223
|
+
right: 0,
|
|
224
|
+
bottom: 0,
|
|
225
|
+
pointerEvents: "none",
|
|
226
|
+
zIndex: 10,
|
|
177
227
|
}}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
228
|
+
>
|
|
229
|
+
<div style={{ borderTop: "1px dashed rgba(255,100,100,0.6)", position: "relative" }}>
|
|
230
|
+
<span
|
|
231
|
+
style={{
|
|
232
|
+
position: "absolute",
|
|
233
|
+
top: -10,
|
|
234
|
+
right: 0,
|
|
235
|
+
fontSize: 10,
|
|
236
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
237
|
+
color: "rgba(255,100,100,0.7)",
|
|
238
|
+
background: bg,
|
|
239
|
+
padding: "1px 4px",
|
|
240
|
+
borderRadius: 3,
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{SNAP_MAX_HEIGHT}px
|
|
244
|
+
</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div
|
|
247
|
+
style={{
|
|
248
|
+
height: "100%",
|
|
249
|
+
background:
|
|
250
|
+
"repeating-linear-gradient(-45deg, transparent, transparent 8px, rgba(255,100,100,0.06) 8px, rgba(255,100,100,0.06) 16px)",
|
|
251
|
+
}}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
{!showOverflowWarning && isExpandable ? (
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
aria-expanded={isExpanded}
|
|
260
|
+
onClick={() => setIsExpanded((value) => !value)}
|
|
261
|
+
style={{
|
|
262
|
+
position: "absolute",
|
|
263
|
+
bottom: 0,
|
|
264
|
+
left: "50%",
|
|
265
|
+
transform: "translate(-50%, 50%)",
|
|
266
|
+
appearance: "none",
|
|
267
|
+
border: `1px solid ${borderColor}`,
|
|
268
|
+
borderRadius: 9999,
|
|
269
|
+
backgroundColor: isDark ? "rgba(30,30,30,0.6)" : "rgba(255,255,255,0.6)",
|
|
270
|
+
backdropFilter: "blur(12px) saturate(180%)",
|
|
271
|
+
WebkitBackdropFilter: "blur(12px) saturate(180%)",
|
|
272
|
+
color: toggleText,
|
|
273
|
+
padding: "2px 10px",
|
|
274
|
+
fontSize: 12,
|
|
275
|
+
lineHeight: "16px",
|
|
276
|
+
fontWeight: 600,
|
|
277
|
+
cursor: "pointer",
|
|
278
|
+
zIndex: 11,
|
|
279
|
+
}}
|
|
280
|
+
onMouseEnter={(event) => {
|
|
281
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
282
|
+
? "rgba(50,50,50,0.7)"
|
|
283
|
+
: "rgba(245,245,245,0.75)";
|
|
284
|
+
}}
|
|
285
|
+
onMouseLeave={(event) => {
|
|
286
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
287
|
+
? "rgba(30,30,30,0.6)"
|
|
288
|
+
: "rgba(255,255,255,0.6)";
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
{isExpanded ? "Show less" : "Show more"}
|
|
292
|
+
</button>
|
|
293
|
+
) : null}
|
|
181
294
|
</div>
|
|
182
295
|
{actionError && (
|
|
183
296
|
<div
|
|
@@ -6,8 +6,11 @@ import { useSnapTheme } from "../theme";
|
|
|
6
6
|
import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
7
7
|
|
|
8
8
|
export function SnapCellGrid({
|
|
9
|
-
element
|
|
9
|
+
element,
|
|
10
|
+
emit,
|
|
10
11
|
}: ComponentRenderProps<Record<string, unknown>>) {
|
|
12
|
+
const { props } = element;
|
|
13
|
+
const on = (element as unknown as { on?: Record<string, unknown> }).on;
|
|
11
14
|
const { hex, appearance } = useSnapPalette();
|
|
12
15
|
const { colors } = useSnapTheme();
|
|
13
16
|
const { get, set } = useStateStore();
|
|
@@ -20,8 +23,10 @@ export function SnapCellGrid({
|
|
|
20
23
|
const gapPx = gapMap[gap] ?? 1;
|
|
21
24
|
|
|
22
25
|
const select = String(props.select ?? "off");
|
|
23
|
-
const interactive = select !== "off";
|
|
24
26
|
const isMultiple = select === "multiple";
|
|
27
|
+
const isSelectable = select !== "off";
|
|
28
|
+
const hasPressAction = Boolean(on?.press);
|
|
29
|
+
const interactive = isSelectable || hasPressAction;
|
|
25
30
|
|
|
26
31
|
const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
|
|
27
32
|
const tapPath = `/inputs/${name}`;
|
|
@@ -34,7 +39,8 @@ export function SnapCellGrid({
|
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
const isSelected = (r: number, c: number) =>
|
|
42
|
+
const isSelected = (r: number, c: number) =>
|
|
43
|
+
isSelectable && selectedSet.has(`${r},${c}`);
|
|
38
44
|
|
|
39
45
|
const handleTap = (r: number, c: number) => {
|
|
40
46
|
const key = `${r},${c}`;
|
|
@@ -46,6 +52,7 @@ export function SnapCellGrid({
|
|
|
46
52
|
} else {
|
|
47
53
|
set(tapPath, key);
|
|
48
54
|
}
|
|
55
|
+
if (hasPressAction) emit("press");
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
const cellMap = new Map<string, { color?: string; content?: string }>();
|
|
@@ -114,7 +121,7 @@ export function SnapCellGrid({
|
|
|
114
121
|
);
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
const selectionLabel =
|
|
124
|
+
const selectionLabel = isSelectable && selectedSet.size > 0
|
|
118
125
|
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
119
126
|
: null;
|
|
120
127
|
|
|
@@ -30,7 +30,10 @@ export function ConfettiOverlay() {
|
|
|
30
30
|
CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)]!,
|
|
31
31
|
size: 6 + Math.random() * 8,
|
|
32
32
|
startRotation: Math.random() * 360,
|
|
33
|
-
|
|
33
|
+
// Per-piece swirl: amplitude, frequency (full oscillations), phase.
|
|
34
|
+
swirlAmp: 20 + Math.random() * 40,
|
|
35
|
+
swirlFreq: 1 + Math.random() * 1.5,
|
|
36
|
+
swirlPhase: Math.random() * Math.PI * 2,
|
|
34
37
|
})),
|
|
35
38
|
// width captured once on mount; intentional stable dep
|
|
36
39
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -39,45 +42,21 @@ export function ConfettiOverlay() {
|
|
|
39
42
|
|
|
40
43
|
const anims = useRef(
|
|
41
44
|
pieces.map(() => ({
|
|
42
|
-
|
|
43
|
-
opacity: new Animated.Value(1),
|
|
44
|
-
rotate: new Animated.Value(0),
|
|
45
|
-
translateX: new Animated.Value(0),
|
|
45
|
+
progress: new Animated.Value(0),
|
|
46
46
|
})),
|
|
47
47
|
).current;
|
|
48
48
|
|
|
49
49
|
useEffect(() => {
|
|
50
50
|
const animations = pieces.map((piece, i) => {
|
|
51
51
|
const anim = anims[i]!;
|
|
52
|
-
anim.
|
|
53
|
-
anim.opacity.setValue(1);
|
|
54
|
-
anim.rotate.setValue(0);
|
|
55
|
-
anim.translateX.setValue(0);
|
|
56
|
-
|
|
52
|
+
anim.progress.setValue(0);
|
|
57
53
|
return Animated.sequence([
|
|
58
54
|
Animated.delay(piece.delay),
|
|
59
|
-
Animated.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}),
|
|
65
|
-
Animated.timing(anim.opacity, {
|
|
66
|
-
toValue: 0,
|
|
67
|
-
duration: piece.duration,
|
|
68
|
-
useNativeDriver: true,
|
|
69
|
-
}),
|
|
70
|
-
Animated.timing(anim.rotate, {
|
|
71
|
-
toValue: 720,
|
|
72
|
-
duration: piece.duration,
|
|
73
|
-
useNativeDriver: true,
|
|
74
|
-
}),
|
|
75
|
-
Animated.timing(anim.translateX, {
|
|
76
|
-
toValue: piece.driftX,
|
|
77
|
-
duration: piece.duration,
|
|
78
|
-
useNativeDriver: true,
|
|
79
|
-
}),
|
|
80
|
-
]),
|
|
55
|
+
Animated.timing(anim.progress, {
|
|
56
|
+
toValue: 1,
|
|
57
|
+
duration: piece.duration,
|
|
58
|
+
useNativeDriver: true,
|
|
59
|
+
}),
|
|
81
60
|
]);
|
|
82
61
|
});
|
|
83
62
|
|
|
@@ -86,17 +65,41 @@ export function ConfettiOverlay() {
|
|
|
86
65
|
return () => composite.stop();
|
|
87
66
|
}, [pieces, anims, height]);
|
|
88
67
|
|
|
68
|
+
// Sample the sine curve at fixed progress points to build an interpolation
|
|
69
|
+
// that drives horizontal swirl on the native driver.
|
|
70
|
+
const SAMPLE_COUNT = 21;
|
|
71
|
+
const samplePoints = Array.from(
|
|
72
|
+
{ length: SAMPLE_COUNT },
|
|
73
|
+
(_, k) => k / (SAMPLE_COUNT - 1),
|
|
74
|
+
);
|
|
75
|
+
|
|
89
76
|
return (
|
|
90
77
|
<View style={[StyleSheet.absoluteFill, styles.container]} pointerEvents="none">
|
|
91
78
|
{pieces.map((piece, i) => {
|
|
92
79
|
const anim = anims[i]!;
|
|
93
|
-
const
|
|
94
|
-
inputRange: [0,
|
|
80
|
+
const translateY = anim.progress.interpolate({
|
|
81
|
+
inputRange: [0, 1],
|
|
82
|
+
outputRange: [-20, height + 20],
|
|
83
|
+
});
|
|
84
|
+
const rotate = anim.progress.interpolate({
|
|
85
|
+
inputRange: [0, 1],
|
|
95
86
|
outputRange: [
|
|
96
87
|
`${piece.startRotation}deg`,
|
|
97
88
|
`${piece.startRotation + 720}deg`,
|
|
98
89
|
],
|
|
99
90
|
});
|
|
91
|
+
const opacity = anim.progress.interpolate({
|
|
92
|
+
inputRange: [0, 0.5, 1],
|
|
93
|
+
outputRange: [1, 1, 0],
|
|
94
|
+
});
|
|
95
|
+
const translateX = anim.progress.interpolate({
|
|
96
|
+
inputRange: samplePoints,
|
|
97
|
+
outputRange: samplePoints.map(
|
|
98
|
+
(t) =>
|
|
99
|
+
Math.sin(t * piece.swirlFreq * Math.PI * 2 + piece.swirlPhase) *
|
|
100
|
+
piece.swirlAmp,
|
|
101
|
+
),
|
|
102
|
+
});
|
|
100
103
|
return (
|
|
101
104
|
<Animated.View
|
|
102
105
|
key={piece.id}
|
|
@@ -107,10 +110,10 @@ export function ConfettiOverlay() {
|
|
|
107
110
|
width: piece.size,
|
|
108
111
|
height: piece.size * 0.6,
|
|
109
112
|
backgroundColor: piece.color,
|
|
110
|
-
opacity
|
|
113
|
+
opacity,
|
|
111
114
|
transform: [
|
|
112
|
-
{ translateY
|
|
113
|
-
{ translateX
|
|
115
|
+
{ translateY },
|
|
116
|
+
{ translateX },
|
|
114
117
|
{ rotate },
|
|
115
118
|
],
|
|
116
119
|
},
|
|
@@ -31,6 +31,7 @@ export function SnapCard({
|
|
|
31
31
|
validationErrorFallback,
|
|
32
32
|
actionError,
|
|
33
33
|
plain = false,
|
|
34
|
+
loadingOverlay,
|
|
34
35
|
}: {
|
|
35
36
|
snap: SnapPage;
|
|
36
37
|
handlers: SnapActionHandlers;
|
|
@@ -49,6 +50,8 @@ export function SnapCard({
|
|
|
49
50
|
actionError?: string | null;
|
|
50
51
|
/** When true, renders without card frame (no border, background, or padding). */
|
|
51
52
|
plain?: boolean;
|
|
53
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
54
|
+
loadingOverlay?: ReactNode;
|
|
52
55
|
}) {
|
|
53
56
|
if (snap.version === SPEC_VERSION_2) {
|
|
54
57
|
return (
|
|
@@ -64,6 +67,7 @@ export function SnapCard({
|
|
|
64
67
|
validationErrorFallback={validationErrorFallback}
|
|
65
68
|
actionError={actionError}
|
|
66
69
|
plain={plain}
|
|
70
|
+
loadingOverlay={loadingOverlay}
|
|
67
71
|
/>
|
|
68
72
|
);
|
|
69
73
|
}
|
|
@@ -78,6 +82,7 @@ export function SnapCard({
|
|
|
78
82
|
borderRadius={borderRadius}
|
|
79
83
|
actionError={actionError}
|
|
80
84
|
plain={plain}
|
|
85
|
+
loadingOverlay={loadingOverlay}
|
|
81
86
|
/>
|
|
82
87
|
);
|
|
83
88
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { Spec } from "@json-render/core";
|
|
2
2
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
3
|
import { SnapCatalogView } from "./catalog-renderer";
|
|
4
|
+
import { ConfettiOverlay } from "./confetti-overlay";
|
|
4
5
|
import { useSnapTheme } from "./theme";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type ReactNode,
|
|
8
|
+
useCallback,
|
|
9
|
+
useEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
6
14
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
7
15
|
import {
|
|
8
16
|
DEFAULT_THEME_ACCENT,
|
|
@@ -66,10 +74,16 @@ export function SnapViewCoreInner({
|
|
|
66
74
|
snap,
|
|
67
75
|
handlers,
|
|
68
76
|
loading = false,
|
|
77
|
+
loadingOverlay,
|
|
69
78
|
}: {
|
|
70
79
|
snap: SnapPage;
|
|
71
80
|
handlers: SnapActionHandlers;
|
|
72
81
|
loading?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Custom content rendered while `loading` is true. When `undefined` (default)
|
|
84
|
+
* the built-in ActivityIndicator overlay is used. Pass `null` to render nothing.
|
|
85
|
+
*/
|
|
86
|
+
loadingOverlay?: ReactNode;
|
|
73
87
|
}) {
|
|
74
88
|
const { mode } = useSnapTheme();
|
|
75
89
|
const spec = snap.ui;
|
|
@@ -113,6 +127,12 @@ export function SnapViewCoreInner({
|
|
|
113
127
|
setPageKey((k) => k + 1);
|
|
114
128
|
}, [spec]);
|
|
115
129
|
|
|
130
|
+
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
131
|
+
const [confettiKey, setConfettiKey] = useState(0);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (showConfetti) setConfettiKey((k) => k + 1);
|
|
134
|
+
}, [showConfetti, snap]);
|
|
135
|
+
|
|
116
136
|
const handlersRef = useRef(handlers);
|
|
117
137
|
handlersRef.current = handlers;
|
|
118
138
|
|
|
@@ -172,19 +192,16 @@ export function SnapViewCoreInner({
|
|
|
172
192
|
|
|
173
193
|
return (
|
|
174
194
|
<View style={styles.container}>
|
|
175
|
-
{loading
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<ActivityIndicator size="large" color={accentHex} />
|
|
186
|
-
</View>
|
|
187
|
-
) : null}
|
|
195
|
+
{loading
|
|
196
|
+
? loadingOverlay === undefined
|
|
197
|
+
? (
|
|
198
|
+
<SnapLoadingOverlay
|
|
199
|
+
appearance={mode}
|
|
200
|
+
accentHex={accentHex}
|
|
201
|
+
/>
|
|
202
|
+
)
|
|
203
|
+
: loadingOverlay
|
|
204
|
+
: null}
|
|
188
205
|
<SnapCatalogView
|
|
189
206
|
key={pageKey}
|
|
190
207
|
spec={spec}
|
|
@@ -195,6 +212,31 @@ export function SnapViewCoreInner({
|
|
|
195
212
|
}}
|
|
196
213
|
onAction={handleAction}
|
|
197
214
|
/>
|
|
215
|
+
{showConfetti && <ConfettiOverlay key={confettiKey} />}
|
|
216
|
+
</View>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function SnapLoadingOverlay({
|
|
221
|
+
appearance,
|
|
222
|
+
accentHex,
|
|
223
|
+
}: {
|
|
224
|
+
appearance: "light" | "dark";
|
|
225
|
+
accentHex: string;
|
|
226
|
+
}) {
|
|
227
|
+
return (
|
|
228
|
+
<View
|
|
229
|
+
style={[
|
|
230
|
+
styles.overlay,
|
|
231
|
+
{
|
|
232
|
+
backgroundColor:
|
|
233
|
+
appearance === "dark"
|
|
234
|
+
? "rgba(0,0,0,0.1)"
|
|
235
|
+
: "rgba(255,255,255,0.2)",
|
|
236
|
+
},
|
|
237
|
+
]}
|
|
238
|
+
>
|
|
239
|
+
<ActivityIndicator size="large" color={accentHex} />
|
|
198
240
|
</View>
|
|
199
241
|
);
|
|
200
242
|
}
|