@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
|
@@ -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
|
},
|
|
@@ -7,7 +7,7 @@ import { hexToRgba } from "./use-snap-palette.js";
|
|
|
7
7
|
export type { JsonValue, SnapPage, SnapActionHandlers } from "./types.js";
|
|
8
8
|
export { useSnapTheme, hexToRgba };
|
|
9
9
|
export type { SnapNativeColors };
|
|
10
|
-
export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
|
|
10
|
+
export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
|
|
11
11
|
snap: SnapPage;
|
|
12
12
|
handlers: SnapActionHandlers;
|
|
13
13
|
loading?: boolean;
|
|
@@ -25,4 +25,6 @@ export declare function SnapCard({ snap, handlers, loading, appearance, colors,
|
|
|
25
25
|
actionError?: string | null;
|
|
26
26
|
/** When true, renders without card frame (no border, background, or padding). */
|
|
27
27
|
plain?: boolean;
|
|
28
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
29
|
+
loadingOverlay?: ReactNode;
|
|
28
30
|
}): import("react").JSX.Element;
|
|
@@ -7,9 +7,9 @@ import { SnapCardV2 } from "./v2/snap-view.js";
|
|
|
7
7
|
// ─── Re-exports ───────────────────────────────────────
|
|
8
8
|
export { useSnapTheme, hexToRgba };
|
|
9
9
|
// ─── SnapCard (version-switching) ─────────────────────
|
|
10
|
-
export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
|
|
10
|
+
export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
|
|
11
11
|
if (snap.version === SPEC_VERSION_2) {
|
|
12
|
-
return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain }));
|
|
12
|
+
return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay }));
|
|
13
13
|
}
|
|
14
|
-
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain }));
|
|
14
|
+
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay }));
|
|
15
15
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import type { SnapPage, SnapActionHandlers } from "./types.js";
|
|
2
3
|
export declare function applyStatePaths(model: Record<string, unknown>, changes: {
|
|
3
4
|
path: string;
|
|
4
5
|
value: unknown;
|
|
5
6
|
}[] | Record<string, unknown>): void;
|
|
6
7
|
export declare function resolveAccentHex(accent: string | undefined, appearance: "light" | "dark"): string;
|
|
7
|
-
export declare function SnapViewCoreInner({ snap, handlers, loading, }: {
|
|
8
|
+
export declare function SnapViewCoreInner({ snap, handlers, loading, loadingOverlay, }: {
|
|
8
9
|
snap: SnapPage;
|
|
9
10
|
handlers: SnapActionHandlers;
|
|
10
11
|
loading?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Custom content rendered while `loading` is true. When `undefined` (default)
|
|
14
|
+
* the built-in ActivityIndicator overlay is used. Pass `null` to render nothing.
|
|
15
|
+
*/
|
|
16
|
+
loadingOverlay?: ReactNode;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
export declare function SnapLoadingOverlay({ appearance, accentHex, }: {
|
|
19
|
+
appearance: "light" | "dark";
|
|
20
|
+
accentHex: string;
|
|
11
21
|
}): import("react").JSX.Element;
|
|
@@ -1,8 +1,9 @@
|
|
|
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
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
6
7
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
7
8
|
import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
|
|
8
9
|
// ─── Shared helpers ──────────────────────────────────
|
|
@@ -45,7 +46,7 @@ export function resolveAccentHex(accent, appearance) {
|
|
|
45
46
|
return map[name];
|
|
46
47
|
}
|
|
47
48
|
// ─── Core rendering component (no validation) ────────
|
|
48
|
-
export function SnapViewCoreInner({ snap, handlers, loading = false, }) {
|
|
49
|
+
export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOverlay, }) {
|
|
49
50
|
const { mode } = useSnapTheme();
|
|
50
51
|
const spec = snap.ui;
|
|
51
52
|
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
@@ -79,6 +80,12 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, }) {
|
|
|
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) => {
|
|
@@ -134,14 +141,23 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, }) {
|
|
|
134
141
|
break;
|
|
135
142
|
}
|
|
136
143
|
}, []);
|
|
137
|
-
return (_jsxs(View, { style: styles.container, children: [loading
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
], children: _jsx(ActivityIndicator, { size: "large", color: accentHex }) })) : null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
144
|
+
return (_jsxs(View, { style: styles.container, children: [loading
|
|
145
|
+
? loadingOverlay === undefined
|
|
146
|
+
? (_jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex }))
|
|
147
|
+
: loadingOverlay
|
|
148
|
+
: null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
143
149
|
applyStatePaths(stateRef.current, changes);
|
|
144
|
-
}, onAction: handleAction }, pageKey)] }));
|
|
150
|
+
}, onAction: handleAction }, pageKey), showConfetti && _jsx(ConfettiOverlay, {}, confettiKey)] }));
|
|
151
|
+
}
|
|
152
|
+
export function SnapLoadingOverlay({ appearance, accentHex, }) {
|
|
153
|
+
return (_jsx(View, { style: [
|
|
154
|
+
styles.overlay,
|
|
155
|
+
{
|
|
156
|
+
backgroundColor: appearance === "dark"
|
|
157
|
+
? "rgba(0,0,0,0.1)"
|
|
158
|
+
: "rgba(255,255,255,0.2)",
|
|
159
|
+
},
|
|
160
|
+
], children: _jsx(ActivityIndicator, { size: "large", color: accentHex }) }));
|
|
145
161
|
}
|
|
146
162
|
const styles = StyleSheet.create({
|
|
147
163
|
container: {
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import { type SnapNativeColors } from "../theme.js";
|
|
2
3
|
import type { SnapPage, SnapActionHandlers } from "../types.js";
|
|
3
|
-
export declare function SnapViewV1Inner({ snap, handlers, loading, }: {
|
|
4
|
+
export declare function SnapViewV1Inner({ snap, handlers, loading, loadingOverlay, }: {
|
|
4
5
|
snap: SnapPage;
|
|
5
6
|
handlers: SnapActionHandlers;
|
|
6
7
|
loading?: boolean;
|
|
8
|
+
loadingOverlay?: ReactNode;
|
|
7
9
|
}): import("react").JSX.Element;
|
|
8
|
-
export declare function SnapViewV1({ snap, handlers, loading, appearance, colors, }: {
|
|
10
|
+
export declare function SnapViewV1({ snap, handlers, loading, appearance, colors, loadingOverlay, }: {
|
|
9
11
|
snap: SnapPage;
|
|
10
12
|
handlers: SnapActionHandlers;
|
|
11
13
|
loading?: boolean;
|
|
12
14
|
appearance?: "light" | "dark";
|
|
13
15
|
colors?: Partial<SnapNativeColors>;
|
|
16
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
17
|
+
loadingOverlay?: ReactNode;
|
|
14
18
|
}): import("react").JSX.Element;
|
|
15
|
-
export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, }: {
|
|
19
|
+
export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, loadingOverlay, }: {
|
|
16
20
|
snap: SnapPage;
|
|
17
21
|
handlers: SnapActionHandlers;
|
|
18
22
|
loading?: boolean;
|
|
@@ -21,4 +25,6 @@ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors
|
|
|
21
25
|
borderRadius?: number;
|
|
22
26
|
actionError?: string | null;
|
|
23
27
|
plain?: boolean;
|
|
28
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
29
|
+
loadingOverlay?: ReactNode;
|
|
24
30
|
}): import("react").JSX.Element;
|
|
@@ -2,18 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { View, Text, StyleSheet, Pressable } from "react-native";
|
|
4
4
|
import { SnapThemeProvider, useSnapTheme } from "../theme.js";
|
|
5
|
-
import { SnapViewCoreInner } from "../snap-view-core.js";
|
|
5
|
+
import { SnapLoadingOverlay, SnapViewCoreInner, resolveAccentHex, } from "../snap-view-core.js";
|
|
6
6
|
const SNAP_MAX_HEIGHT = 500;
|
|
7
7
|
// ─── SnapViewV1 (no validation) ──────────────────────
|
|
8
|
-
export function SnapViewV1Inner({ snap, handlers, loading = false, }) {
|
|
9
|
-
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading }));
|
|
8
|
+
export function SnapViewV1Inner({ snap, handlers, loading = false, loadingOverlay, }) {
|
|
9
|
+
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }));
|
|
10
10
|
}
|
|
11
|
-
export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", colors, }) {
|
|
12
|
-
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }));
|
|
11
|
+
export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", colors, loadingOverlay, }) {
|
|
12
|
+
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }) }));
|
|
13
13
|
}
|
|
14
14
|
// ─── SnapCardV1 (card frame with expandable clipping) ──
|
|
15
|
-
function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, plain, }) {
|
|
16
|
-
const { colors } = useSnapTheme();
|
|
15
|
+
function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, plain, loadingOverlay, }) {
|
|
16
|
+
const { colors, mode } = useSnapTheme();
|
|
17
|
+
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
17
18
|
const [contentHeight, setContentHeight] = useState(0);
|
|
18
19
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
19
20
|
useEffect(() => {
|
|
@@ -22,35 +23,36 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
22
23
|
}, [snap]);
|
|
23
24
|
const isExpandable = contentHeight > SNAP_MAX_HEIGHT + 1;
|
|
24
25
|
const isClipped = isExpandable && !isExpanded;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? cardStyles.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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: [
|
|
54
56
|
cardStyles.actionError,
|
|
55
57
|
{
|
|
56
58
|
color: appearance === "dark"
|
|
@@ -59,37 +61,34 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
59
61
|
},
|
|
60
62
|
], children: actionError }))] }));
|
|
61
63
|
}
|
|
62
|
-
export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, plain = false, }) {
|
|
63
|
-
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV1Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, actionError: actionError, appearance: appearance, plain: plain }) }));
|
|
64
|
+
export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, plain = false, loadingOverlay, }) {
|
|
65
|
+
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV1Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, actionError: actionError, appearance: appearance, plain: plain, loadingOverlay: loadingOverlay }) }));
|
|
64
66
|
}
|
|
65
67
|
const cardStyles = StyleSheet.create({
|
|
66
68
|
frameRing: { alignSelf: "stretch" },
|
|
67
69
|
card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
|
|
68
70
|
body: { paddingHorizontal: 16, paddingVertical: 16 },
|
|
69
|
-
|
|
71
|
+
expandFloat: {
|
|
72
|
+
position: "absolute",
|
|
73
|
+
left: 0,
|
|
74
|
+
right: 0,
|
|
75
|
+
bottom: -14,
|
|
76
|
+
height: 28,
|
|
70
77
|
alignItems: "center",
|
|
71
|
-
|
|
72
|
-
paddingTop: 10,
|
|
73
|
-
paddingBottom: 12,
|
|
74
|
-
borderTopWidth: StyleSheet.hairlineWidth,
|
|
75
|
-
},
|
|
76
|
-
expandRowPlain: {
|
|
77
|
-
paddingHorizontal: 0,
|
|
78
|
-
paddingTop: 8,
|
|
79
|
-
paddingBottom: 0,
|
|
80
|
-
borderTopWidth: 0,
|
|
78
|
+
justifyContent: "center",
|
|
81
79
|
},
|
|
82
80
|
expandButton: {
|
|
83
81
|
minWidth: 92,
|
|
84
82
|
alignItems: "center",
|
|
85
83
|
justifyContent: "center",
|
|
86
84
|
borderRadius: 9999,
|
|
85
|
+
borderWidth: 1,
|
|
87
86
|
paddingHorizontal: 10,
|
|
88
|
-
paddingVertical:
|
|
87
|
+
paddingVertical: 4,
|
|
89
88
|
},
|
|
90
89
|
expandButtonText: {
|
|
91
|
-
fontSize:
|
|
92
|
-
lineHeight:
|
|
90
|
+
fontSize: 12,
|
|
91
|
+
lineHeight: 16,
|
|
93
92
|
fontWeight: "600",
|
|
94
93
|
},
|
|
95
94
|
actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
|
|
@@ -2,14 +2,15 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import { type SnapNativeColors } from "../theme.js";
|
|
3
3
|
import { type ValidationResult } from "@farcaster/snap";
|
|
4
4
|
import type { SnapPage, SnapActionHandlers } from "../types.js";
|
|
5
|
-
export declare function SnapViewV2Inner({ snap, handlers, loading, onValidationError, validationErrorFallback, }: {
|
|
5
|
+
export declare function SnapViewV2Inner({ snap, handlers, loading, onValidationError, validationErrorFallback, loadingOverlay, }: {
|
|
6
6
|
snap: SnapPage;
|
|
7
7
|
handlers: SnapActionHandlers;
|
|
8
8
|
loading?: boolean;
|
|
9
9
|
onValidationError?: (result: ValidationResult) => void;
|
|
10
10
|
validationErrorFallback?: ReactNode;
|
|
11
|
+
loadingOverlay?: ReactNode;
|
|
11
12
|
}): import("react").JSX.Element;
|
|
12
|
-
export declare function SnapViewV2({ snap, handlers, loading, appearance, colors, onValidationError, validationErrorFallback, }: {
|
|
13
|
+
export declare function SnapViewV2({ snap, handlers, loading, appearance, colors, onValidationError, validationErrorFallback, loadingOverlay, }: {
|
|
13
14
|
snap: SnapPage;
|
|
14
15
|
handlers: SnapActionHandlers;
|
|
15
16
|
loading?: boolean;
|
|
@@ -17,8 +18,10 @@ export declare function SnapViewV2({ snap, handlers, loading, appearance, colors
|
|
|
17
18
|
colors?: Partial<SnapNativeColors>;
|
|
18
19
|
onValidationError?: (result: ValidationResult) => void;
|
|
19
20
|
validationErrorFallback?: ReactNode;
|
|
21
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
22
|
+
loadingOverlay?: ReactNode;
|
|
20
23
|
}): import("react").JSX.Element;
|
|
21
|
-
export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
|
|
24
|
+
export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
|
|
22
25
|
snap: SnapPage;
|
|
23
26
|
handlers: SnapActionHandlers;
|
|
24
27
|
loading?: boolean;
|
|
@@ -30,4 +33,6 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors
|
|
|
30
33
|
validationErrorFallback?: ReactNode;
|
|
31
34
|
actionError?: string | null;
|
|
32
35
|
plain?: boolean;
|
|
36
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
37
|
+
loadingOverlay?: ReactNode;
|
|
33
38
|
}): import("react").JSX.Element;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { SnapThemeProvider, useSnapTheme } from "../theme.js";
|
|
5
|
-
import { SnapViewCoreInner } from "../snap-view-core.js";
|
|
5
|
+
import { SnapLoadingOverlay, SnapViewCoreInner, resolveAccentHex, } from "../snap-view-core.js";
|
|
6
6
|
import { validateSnapResponse, } from "@farcaster/snap";
|
|
7
7
|
// ─── Constants ───────────────────────────────────────
|
|
8
8
|
const SNAP_MAX_HEIGHT = 500;
|
|
@@ -24,7 +24,7 @@ const fallbackStyles = StyleSheet.create({
|
|
|
24
24
|
},
|
|
25
25
|
});
|
|
26
26
|
// ─── SnapViewV2 (with validation) ────────────────────
|
|
27
|
-
export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationError, validationErrorFallback, }) {
|
|
27
|
+
export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationError, validationErrorFallback, loadingOverlay, }) {
|
|
28
28
|
const validation = useMemo(() => validateSnapResponse(snap), [snap]);
|
|
29
29
|
const valid = validation.valid;
|
|
30
30
|
const validationMessage = validation.issues[0]?.message;
|
|
@@ -44,29 +44,73 @@ export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationE
|
|
|
44
44
|
return null;
|
|
45
45
|
return (_jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { message: validationMessage }) }));
|
|
46
46
|
}
|
|
47
|
-
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading }));
|
|
47
|
+
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }));
|
|
48
48
|
}
|
|
49
|
-
export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", colors, onValidationError, validationErrorFallback, }) {
|
|
50
|
-
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }));
|
|
49
|
+
export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", colors, onValidationError, validationErrorFallback, loadingOverlay, }) {
|
|
50
|
+
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: loadingOverlay }) }));
|
|
51
51
|
}
|
|
52
52
|
// ─── SnapCardV2 (card frame + height limits) ─────────
|
|
53
|
-
function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, }) {
|
|
54
|
-
const { colors } = useSnapTheme();
|
|
53
|
+
function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, loadingOverlay, }) {
|
|
54
|
+
const { colors, mode } = useSnapTheme();
|
|
55
|
+
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
55
56
|
const [contentHeight, setContentHeight] = useState(0);
|
|
56
|
-
const
|
|
57
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setIsExpanded(false);
|
|
60
|
+
setContentHeight(0);
|
|
61
|
+
}, [snap]);
|
|
62
|
+
const isExpandable = !showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT + 1;
|
|
63
|
+
const isClipped = isExpandable && !isExpanded;
|
|
64
|
+
const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null }));
|
|
57
65
|
if (plain) {
|
|
58
|
-
return
|
|
66
|
+
return (_jsxs(_Fragment, { children: [_jsx(View, { style: isClipped ? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" } : undefined, children: _jsx(View, { collapsable: false, onLayout: (e) => {
|
|
67
|
+
const nextHeight = Math.round(e.nativeEvent.layout.height);
|
|
68
|
+
setContentHeight((current) => isClipped
|
|
69
|
+
? Math.max(current, nextHeight)
|
|
70
|
+
: current === nextHeight
|
|
71
|
+
? current
|
|
72
|
+
: nextHeight);
|
|
73
|
+
}, children: content }) }), loading
|
|
74
|
+
? loadingOverlay === undefined
|
|
75
|
+
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
76
|
+
: loadingOverlay
|
|
77
|
+
: null, isExpandable ? (_jsx(View, { style: [cardStyles.expandRow, cardStyles.expandRowPlain], children: _jsx(Pressable, { style: ({ pressed }) => [
|
|
78
|
+
cardStyles.expandButton,
|
|
79
|
+
{
|
|
80
|
+
backgroundColor: pressed ? colors.mutedHover : colors.muted,
|
|
81
|
+
},
|
|
82
|
+
], onPress: () => setIsExpanded((value) => !value), children: _jsx(Text, { style: [cardStyles.expandButtonText, { color: colors.text }], children: isExpanded ? "Show less" : "Show more" }) }) })) : null] }));
|
|
59
83
|
}
|
|
60
84
|
const overflowAmount = showOverflowWarning ? contentHeight - SNAP_MAX_HEIGHT : 0;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
const isDark = mode === "dark";
|
|
86
|
+
const pillBg = isDark ? "rgba(40,40,40,0.92)" : "rgba(255,255,255,0.92)";
|
|
87
|
+
const pillBgPressed = isDark ? "rgba(60,60,60,0.95)" : "rgba(240,240,240,0.95)";
|
|
88
|
+
return (_jsxs(_Fragment, { children: [_jsxs(View, { style: { position: "relative" }, children: [_jsxs(View, { style: {
|
|
89
|
+
borderRadius,
|
|
90
|
+
borderWidth: 1,
|
|
91
|
+
borderColor: colors.border,
|
|
92
|
+
backgroundColor: colors.surface,
|
|
93
|
+
maxHeight: showOverflowWarning ? undefined : isClipped ? SNAP_MAX_HEIGHT : undefined,
|
|
94
|
+
overflow: "hidden",
|
|
95
|
+
minHeight: 120,
|
|
96
|
+
}, children: [_jsx(View, { collapsable: false, onLayout: (e) => {
|
|
97
|
+
const nextHeight = Math.round(e.nativeEvent.layout.height);
|
|
98
|
+
setContentHeight((current) => isClipped
|
|
99
|
+
? Math.max(current, nextHeight)
|
|
100
|
+
: current === nextHeight
|
|
101
|
+
? current
|
|
102
|
+
: nextHeight);
|
|
103
|
+
}, style: { paddingHorizontal: 16, paddingVertical: 16 }, children: content }), showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT && (_jsxs(View, { style: { position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, height: overflowAmount, zIndex: 10, pointerEvents: "none" }, children: [_jsx(View, { style: { height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" } }), _jsx(View, { style: { position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }, children: _jsxs(Text, { style: { fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx(View, { style: { flex: 1, backgroundColor: "rgba(255,50,50,0.15)" } })] })), loading
|
|
104
|
+
? loadingOverlay === undefined
|
|
105
|
+
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
106
|
+
: loadingOverlay
|
|
107
|
+
: null] }), isExpandable ? (_jsx(View, { pointerEvents: "box-none", style: cardStyles.expandFloat, children: _jsx(Pressable, { style: ({ pressed }) => [
|
|
108
|
+
cardStyles.expandButton,
|
|
109
|
+
{
|
|
110
|
+
backgroundColor: pressed ? pillBgPressed : pillBg,
|
|
111
|
+
borderColor: colors.border,
|
|
112
|
+
},
|
|
113
|
+
], onPress: () => setIsExpanded((value) => !value), children: _jsx(Text, { style: [cardStyles.expandButtonText, { color: colors.text }], children: isExpanded ? "Show less" : "Show more" }) }) })) : null] }), actionError && (_jsx(Text, { style: {
|
|
70
114
|
paddingHorizontal: 12,
|
|
71
115
|
paddingVertical: 8,
|
|
72
116
|
fontSize: 13,
|
|
@@ -75,14 +119,41 @@ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWa
|
|
|
75
119
|
: "rgba(200,0,0,0.8)",
|
|
76
120
|
}, children: actionError }))] }));
|
|
77
121
|
}
|
|
78
|
-
export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
|
|
79
|
-
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, appearance: appearance, plain: plain }) }));
|
|
122
|
+
export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
|
|
123
|
+
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, appearance: appearance, plain: plain, loadingOverlay: loadingOverlay }) }));
|
|
80
124
|
}
|
|
81
125
|
const cardStyles = StyleSheet.create({
|
|
82
126
|
frameRing: { alignSelf: "stretch" },
|
|
83
127
|
card: { borderWidth: 1, minHeight: 120, overflow: "hidden" },
|
|
84
128
|
body: { paddingHorizontal: 16, paddingVertical: 16 },
|
|
85
129
|
actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
|
|
130
|
+
expandFloat: {
|
|
131
|
+
position: "absolute",
|
|
132
|
+
left: 0,
|
|
133
|
+
right: 0,
|
|
134
|
+
bottom: -14,
|
|
135
|
+
height: 28,
|
|
136
|
+
alignItems: "center",
|
|
137
|
+
justifyContent: "center",
|
|
138
|
+
},
|
|
139
|
+
expandRowPlain: {
|
|
140
|
+
paddingTop: 8,
|
|
141
|
+
alignItems: "center",
|
|
142
|
+
},
|
|
143
|
+
expandButton: {
|
|
144
|
+
minWidth: 92,
|
|
145
|
+
alignItems: "center",
|
|
146
|
+
justifyContent: "center",
|
|
147
|
+
borderRadius: 9999,
|
|
148
|
+
borderWidth: 1,
|
|
149
|
+
paddingHorizontal: 10,
|
|
150
|
+
paddingVertical: 4,
|
|
151
|
+
},
|
|
152
|
+
expandButtonText: {
|
|
153
|
+
fontSize: 12,
|
|
154
|
+
lineHeight: 16,
|
|
155
|
+
fontWeight: "600",
|
|
156
|
+
},
|
|
86
157
|
warningOverlay: {
|
|
87
158
|
position: "absolute",
|
|
88
159
|
top: SNAP_MAX_HEIGHT,
|
package/dist/ui/catalog.js
CHANGED
|
@@ -50,7 +50,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
50
50
|
},
|
|
51
51
|
item: {
|
|
52
52
|
props: itemProps,
|
|
53
|
-
description: "Content row with title and optional description. Children render in the actions slot (right side) —
|
|
53
|
+
description: "Content row with title and optional description. Children render in the actions slot (right side) — badge, button, and icon elements are all valid. The item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.",
|
|
54
54
|
},
|
|
55
55
|
item_group: {
|
|
56
56
|
props: itemGroupProps,
|
|
@@ -90,7 +90,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
90
90
|
},
|
|
91
91
|
cell_grid: {
|
|
92
92
|
props: cellGridProps,
|
|
93
|
-
description: "Cell grid — sparse colored cells on a rows×cols grid.
|
|
93
|
+
description: "Cell grid — sparse colored cells on a rows×cols grid. Two interaction modes: leave select 'off' and bind on.press to fire an action per cell press (inputs[name] is the pressed 'row,col' before the action runs); or set select 'single'/'multiple' for press-to-select with a visual ring (no auto-fire — pair with a separate submit button). on.press is ignored when select is on.",
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
96
|
actions: {
|