@farcaster/snap 2.5.1 → 2.6.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 +1 -0
- package/dist/constants.js +1 -0
- package/dist/react/catalog-renderer.js +2 -0
- package/dist/react/components/action-button.js +9 -1
- package/dist/react/components/cell-grid.js +16 -2
- package/dist/react/components/image.js +5 -2
- package/dist/react/components/paginator.d.ts +7 -0
- package/dist/react/components/paginator.js +103 -0
- package/dist/react/components/stack.js +21 -18
- package/dist/react/components/text.js +19 -1
- package/dist/react/snap-version-context.d.ts +3 -0
- package/dist/react/snap-version-context.js +7 -0
- package/dist/react/snap-view-core.d.ts +1 -1
- package/dist/react/snap-view-core.js +27 -4
- package/dist/react-native/catalog-renderer.js +2 -0
- package/dist/react-native/components/snap-action-button.js +8 -2
- package/dist/react-native/components/snap-cell-grid.js +16 -2
- package/dist/react-native/components/snap-image.js +29 -4
- package/dist/react-native/components/snap-paginator.d.ts +5 -0
- package/dist/react-native/components/snap-paginator.js +194 -0
- package/dist/react-native/components/snap-text.js +10 -3
- package/dist/react-native/expand-state.d.ts +19 -0
- package/dist/react-native/expand-state.js +18 -0
- package/dist/react-native/index.d.ts +7 -1
- package/dist/react-native/index.js +3 -3
- package/dist/react-native/snap-version-context.d.ts +3 -0
- package/dist/react-native/snap-version-context.js +6 -0
- package/dist/react-native/snap-view-core.d.ts +1 -1
- package/dist/react-native/snap-view-core.js +27 -4
- package/dist/react-native/v1/snap-view.d.ts +7 -1
- package/dist/react-native/v1/snap-view.js +35 -11
- package/dist/react-native/v2/snap-view.d.ts +7 -1
- package/dist/react-native/v2/snap-view.js +60 -17
- package/dist/ui/catalog.d.ts +45 -0
- package/dist/ui/catalog.js +20 -3
- package/dist/ui/cell-grid.d.ts +5 -0
- package/dist/ui/cell-grid.js +2 -1
- package/dist/ui/image.d.ts +4 -1
- package/dist/ui/image.js +3 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/paginator-state.d.ts +18 -0
- package/dist/ui/paginator-state.js +47 -0
- package/dist/ui/paginator.d.ts +17 -0
- package/dist/ui/paginator.js +8 -0
- package/dist/ui/text.d.ts +1 -0
- package/dist/ui/text.js +1 -0
- package/dist/validator.js +16 -3
- package/llms.txt +19 -4
- package/package.json +1 -1
- package/src/constants.ts +1 -0
- package/src/react/catalog-renderer.tsx +2 -0
- package/src/react/components/action-button.tsx +13 -2
- package/src/react/components/cell-grid.tsx +22 -2
- package/src/react/components/image.tsx +17 -0
- package/src/react/components/paginator.tsx +208 -0
- package/src/react/components/stack.tsx +20 -18
- package/src/react/components/text.tsx +20 -1
- package/src/react/snap-version-context.tsx +12 -0
- package/src/react/snap-view-core.tsx +44 -12
- package/src/react-native/catalog-renderer.tsx +2 -0
- package/src/react-native/components/snap-action-button.tsx +10 -2
- package/src/react-native/components/snap-cell-grid.tsx +22 -2
- package/src/react-native/components/snap-image.tsx +40 -1
- package/src/react-native/components/snap-paginator.tsx +283 -0
- package/src/react-native/components/snap-text.tsx +11 -2
- package/src/react-native/expand-state.ts +48 -0
- package/src/react-native/index.tsx +15 -0
- package/src/react-native/snap-version-context.tsx +10 -0
- package/src/react-native/snap-view-core.tsx +47 -12
- package/src/react-native/v1/snap-view.tsx +57 -10
- package/src/react-native/v2/snap-view.tsx +88 -17
- package/src/ui/catalog.ts +25 -3
- package/src/ui/cell-grid.ts +2 -0
- package/src/ui/image.ts +3 -1
- package/src/ui/index.ts +3 -0
- package/src/ui/paginator-state.ts +67 -0
- package/src/ui/paginator.ts +11 -0
- package/src/ui/text.ts +1 -0
- package/src/validator.ts +19 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
2
|
import { Image } from "expo-image";
|
|
3
|
-
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { useSnapStackDirection } from "../stack-direction-context";
|
|
5
5
|
|
|
6
6
|
function aspectToRatio(aspect: string): number {
|
|
@@ -14,6 +14,9 @@ export function SnapImage({
|
|
|
14
14
|
}: ComponentRenderProps<Record<string, unknown>>) {
|
|
15
15
|
const url = String(props.url ?? "");
|
|
16
16
|
const alt = String(props.alt ?? "");
|
|
17
|
+
const title = props.title ? String(props.title) : "";
|
|
18
|
+
const subtitle = props.subtitle ? String(props.subtitle) : "";
|
|
19
|
+
const hasOverlay = title.length > 0 || subtitle.length > 0;
|
|
17
20
|
const ratio = aspectToRatio(String(props.aspect ?? "1:1"));
|
|
18
21
|
const stackDir = useSnapStackDirection();
|
|
19
22
|
const inHorizontalStack = stackDir === "horizontal";
|
|
@@ -32,6 +35,20 @@ export function SnapImage({
|
|
|
32
35
|
contentFit="cover"
|
|
33
36
|
accessibilityLabel={alt || undefined}
|
|
34
37
|
/>
|
|
38
|
+
{hasOverlay ? (
|
|
39
|
+
<View style={styles.overlay}>
|
|
40
|
+
{title ? (
|
|
41
|
+
<Text numberOfLines={1} style={styles.title}>
|
|
42
|
+
{title}
|
|
43
|
+
</Text>
|
|
44
|
+
) : null}
|
|
45
|
+
{subtitle ? (
|
|
46
|
+
<Text numberOfLines={1} style={styles.subtitle}>
|
|
47
|
+
{subtitle}
|
|
48
|
+
</Text>
|
|
49
|
+
) : null}
|
|
50
|
+
</View>
|
|
51
|
+
) : null}
|
|
35
52
|
</View>
|
|
36
53
|
);
|
|
37
54
|
}
|
|
@@ -49,4 +66,26 @@ const styles = StyleSheet.create({
|
|
|
49
66
|
flex: 1,
|
|
50
67
|
minWidth: 0,
|
|
51
68
|
},
|
|
69
|
+
overlay: {
|
|
70
|
+
position: "absolute",
|
|
71
|
+
left: 0,
|
|
72
|
+
right: 0,
|
|
73
|
+
bottom: 0,
|
|
74
|
+
paddingHorizontal: 12,
|
|
75
|
+
paddingTop: 24,
|
|
76
|
+
paddingBottom: 10,
|
|
77
|
+
backgroundColor: "rgba(0, 0, 0, 0.48)",
|
|
78
|
+
},
|
|
79
|
+
title: {
|
|
80
|
+
color: "#fff",
|
|
81
|
+
fontSize: 14,
|
|
82
|
+
lineHeight: 18,
|
|
83
|
+
fontWeight: "700",
|
|
84
|
+
},
|
|
85
|
+
subtitle: {
|
|
86
|
+
color: "rgba(255, 255, 255, 0.85)",
|
|
87
|
+
fontSize: 12,
|
|
88
|
+
lineHeight: 16,
|
|
89
|
+
fontWeight: "500",
|
|
90
|
+
},
|
|
52
91
|
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
|
+
import { useStateStore } from "@json-render/react-native";
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Animated, Pressable, StyleSheet, View } from "react-native";
|
|
12
|
+
import { ChevronLeft, ChevronRight } from "lucide-react-native";
|
|
13
|
+
import { useSnapPalette } from "../use-snap-palette";
|
|
14
|
+
import { useSnapTheme } from "../theme";
|
|
15
|
+
import {
|
|
16
|
+
clampPaginatorPage,
|
|
17
|
+
pageFromValue,
|
|
18
|
+
SNAP_PAGINATOR_PAGE_COUNT_PATH,
|
|
19
|
+
SNAP_PAGINATOR_PAGE_PATH,
|
|
20
|
+
} from "../../ui/paginator-state";
|
|
21
|
+
|
|
22
|
+
function clampInitialPage(value: unknown, pageCount: number): number {
|
|
23
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return 0;
|
|
24
|
+
return Math.min(Math.max(value, 0), Math.max(pageCount - 1, 0));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SnapPaginator({
|
|
28
|
+
element: { props },
|
|
29
|
+
children,
|
|
30
|
+
}: ComponentRenderProps<Record<string, unknown>> & { children?: ReactNode }) {
|
|
31
|
+
const pages = useMemo(
|
|
32
|
+
() => Children.toArray(children),
|
|
33
|
+
[children],
|
|
34
|
+
);
|
|
35
|
+
const { colors, mode } = useSnapTheme();
|
|
36
|
+
const { accentHex } = useSnapPalette();
|
|
37
|
+
const { get, set } = useStateStore();
|
|
38
|
+
const initialPage = clampInitialPage(props.initialPage, pages.length);
|
|
39
|
+
const page = clampPaginatorPage(
|
|
40
|
+
pageFromValue(get(SNAP_PAGINATOR_PAGE_PATH), initialPage),
|
|
41
|
+
pages.length,
|
|
42
|
+
);
|
|
43
|
+
const activePage = Math.min(page, Math.max(pages.length - 1, 0));
|
|
44
|
+
const showControls = props.showControls !== false && pages.length > 1;
|
|
45
|
+
const showIndicators = props.showIndicators !== false && pages.length > 1;
|
|
46
|
+
const controlsPosition = props.controlsPosition === "top" ? "top" : "bottom";
|
|
47
|
+
const transition =
|
|
48
|
+
props.transition === "fade" ||
|
|
49
|
+
props.transition === "scale" ||
|
|
50
|
+
props.transition === "none"
|
|
51
|
+
? props.transition
|
|
52
|
+
: "slide";
|
|
53
|
+
const showControlBar = showControls || showIndicators;
|
|
54
|
+
const [transitionDirection, setTransitionDirection] =
|
|
55
|
+
useState<"next" | "previous">("next");
|
|
56
|
+
const pageAnim = useRef(new Animated.Value(1)).current;
|
|
57
|
+
|
|
58
|
+
const canGoPrev = activePage > 0;
|
|
59
|
+
const canGoNext = activePage < pages.length - 1;
|
|
60
|
+
const goToPage = (targetPage: number) => {
|
|
61
|
+
const nextPage = clampPaginatorPage(targetPage, pages.length);
|
|
62
|
+
if (nextPage !== activePage) {
|
|
63
|
+
setTransitionDirection(nextPage > activePage ? "next" : "previous");
|
|
64
|
+
}
|
|
65
|
+
set(SNAP_PAGINATOR_PAGE_PATH, nextPage);
|
|
66
|
+
};
|
|
67
|
+
const goPrev = () => goToPage(activePage - 1);
|
|
68
|
+
const goNext = () => goToPage(activePage + 1);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (pages.length === 0) return;
|
|
72
|
+
const nextPage = clampPaginatorPage(
|
|
73
|
+
pageFromValue(get(SNAP_PAGINATOR_PAGE_PATH), initialPage),
|
|
74
|
+
pages.length,
|
|
75
|
+
);
|
|
76
|
+
if (get(SNAP_PAGINATOR_PAGE_PATH) !== nextPage) {
|
|
77
|
+
set(SNAP_PAGINATOR_PAGE_PATH, nextPage);
|
|
78
|
+
}
|
|
79
|
+
if (get(SNAP_PAGINATOR_PAGE_COUNT_PATH) !== pages.length) {
|
|
80
|
+
set(SNAP_PAGINATOR_PAGE_COUNT_PATH, pages.length);
|
|
81
|
+
}
|
|
82
|
+
}, [get, initialPage, pages.length, set]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (transition === "none") {
|
|
86
|
+
pageAnim.setValue(1);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
pageAnim.setValue(0);
|
|
90
|
+
Animated.timing(pageAnim, {
|
|
91
|
+
toValue: 1,
|
|
92
|
+
duration: transition === "scale" ? 240 : transition === "slide" ? 260 : 180,
|
|
93
|
+
useNativeDriver: true,
|
|
94
|
+
}).start();
|
|
95
|
+
}, [activePage, pageAnim, transition]);
|
|
96
|
+
|
|
97
|
+
if (pages.length === 0) return null;
|
|
98
|
+
|
|
99
|
+
const controlBar = showControlBar ? (
|
|
100
|
+
<View style={styles.footer}>
|
|
101
|
+
{showControls ? (
|
|
102
|
+
<Pressable
|
|
103
|
+
accessibilityRole="button"
|
|
104
|
+
accessibilityLabel="Previous page"
|
|
105
|
+
disabled={!canGoPrev}
|
|
106
|
+
onPress={goPrev}
|
|
107
|
+
style={[
|
|
108
|
+
styles.control,
|
|
109
|
+
{
|
|
110
|
+
borderColor: colors.border,
|
|
111
|
+
backgroundColor: colors.muted,
|
|
112
|
+
opacity: canGoPrev ? 1 : 0.35,
|
|
113
|
+
},
|
|
114
|
+
]}
|
|
115
|
+
>
|
|
116
|
+
<ChevronLeft size={15} color={colors.text} />
|
|
117
|
+
</Pressable>
|
|
118
|
+
) : (
|
|
119
|
+
<View style={styles.controlPlaceholder} />
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{showIndicators ? (
|
|
123
|
+
<View style={styles.indicators}>
|
|
124
|
+
{pages.map((_, index) => {
|
|
125
|
+
const current = index === activePage;
|
|
126
|
+
return (
|
|
127
|
+
<View
|
|
128
|
+
key={index}
|
|
129
|
+
accessibilityLabel={`Page ${index + 1}${current ? ", current" : ""}`}
|
|
130
|
+
style={[
|
|
131
|
+
styles.dot,
|
|
132
|
+
current ? styles.dotCurrent : styles.dotInactive,
|
|
133
|
+
{
|
|
134
|
+
backgroundColor: current
|
|
135
|
+
? accentHex
|
|
136
|
+
: mode === "dark"
|
|
137
|
+
? "rgba(255,255,255,0.5)"
|
|
138
|
+
: "rgba(0,0,0,0.28)",
|
|
139
|
+
borderColor: current
|
|
140
|
+
? mode === "dark"
|
|
141
|
+
? "rgba(255,255,255,0.18)"
|
|
142
|
+
: "rgba(0,0,0,0.12)"
|
|
143
|
+
: "transparent",
|
|
144
|
+
},
|
|
145
|
+
]}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</View>
|
|
150
|
+
) : (
|
|
151
|
+
<View style={styles.indicators} />
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{showControls ? (
|
|
155
|
+
<Pressable
|
|
156
|
+
accessibilityRole="button"
|
|
157
|
+
accessibilityLabel="Next page"
|
|
158
|
+
disabled={!canGoNext}
|
|
159
|
+
onPress={goNext}
|
|
160
|
+
style={[
|
|
161
|
+
styles.control,
|
|
162
|
+
{
|
|
163
|
+
borderColor: colors.border,
|
|
164
|
+
backgroundColor: colors.muted,
|
|
165
|
+
opacity: canGoNext ? 1 : 0.35,
|
|
166
|
+
},
|
|
167
|
+
]}
|
|
168
|
+
>
|
|
169
|
+
<ChevronRight size={15} color={colors.text} />
|
|
170
|
+
</Pressable>
|
|
171
|
+
) : (
|
|
172
|
+
<View style={styles.controlPlaceholder} />
|
|
173
|
+
)}
|
|
174
|
+
</View>
|
|
175
|
+
) : null;
|
|
176
|
+
|
|
177
|
+
const animatedPageStyle =
|
|
178
|
+
transition === "none"
|
|
179
|
+
? undefined
|
|
180
|
+
: {
|
|
181
|
+
opacity: pageAnim.interpolate({
|
|
182
|
+
inputRange: [0, 1],
|
|
183
|
+
outputRange: [transition === "fade" ? 0.2 : 0.35, 1],
|
|
184
|
+
}),
|
|
185
|
+
transform:
|
|
186
|
+
transition === "scale"
|
|
187
|
+
? [
|
|
188
|
+
{
|
|
189
|
+
scale: pageAnim.interpolate({
|
|
190
|
+
inputRange: [0, 1],
|
|
191
|
+
outputRange: [0.94, 1],
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
]
|
|
195
|
+
: transition === "slide"
|
|
196
|
+
? [
|
|
197
|
+
{
|
|
198
|
+
translateX: pageAnim.interpolate({
|
|
199
|
+
inputRange: [0, 1],
|
|
200
|
+
outputRange: [
|
|
201
|
+
transitionDirection === "previous" ? -22 : 22,
|
|
202
|
+
0,
|
|
203
|
+
],
|
|
204
|
+
}),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
scale: pageAnim.interpolate({
|
|
208
|
+
inputRange: [0, 1],
|
|
209
|
+
outputRange: [0.985, 1],
|
|
210
|
+
}),
|
|
211
|
+
},
|
|
212
|
+
]
|
|
213
|
+
: [],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<View style={styles.wrap}>
|
|
218
|
+
{controlsPosition === "top" ? controlBar : null}
|
|
219
|
+
<Animated.View
|
|
220
|
+
style={[
|
|
221
|
+
styles.page,
|
|
222
|
+
animatedPageStyle,
|
|
223
|
+
]}
|
|
224
|
+
>
|
|
225
|
+
{pages[activePage]}
|
|
226
|
+
</Animated.View>
|
|
227
|
+
{controlsPosition === "bottom" ? controlBar : null}
|
|
228
|
+
</View>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const styles = StyleSheet.create({
|
|
233
|
+
wrap: {
|
|
234
|
+
width: "100%",
|
|
235
|
+
minWidth: 0,
|
|
236
|
+
gap: 8,
|
|
237
|
+
},
|
|
238
|
+
page: {
|
|
239
|
+
width: "100%",
|
|
240
|
+
minWidth: 0,
|
|
241
|
+
},
|
|
242
|
+
footer: {
|
|
243
|
+
minHeight: 28,
|
|
244
|
+
flexDirection: "row",
|
|
245
|
+
alignItems: "center",
|
|
246
|
+
justifyContent: "space-between",
|
|
247
|
+
gap: 8,
|
|
248
|
+
},
|
|
249
|
+
control: {
|
|
250
|
+
width: 28,
|
|
251
|
+
height: 28,
|
|
252
|
+
borderWidth: 1,
|
|
253
|
+
borderRadius: 6,
|
|
254
|
+
alignItems: "center",
|
|
255
|
+
justifyContent: "center",
|
|
256
|
+
},
|
|
257
|
+
controlPlaceholder: {
|
|
258
|
+
width: 28,
|
|
259
|
+
height: 28,
|
|
260
|
+
},
|
|
261
|
+
indicators: {
|
|
262
|
+
flex: 1,
|
|
263
|
+
flexDirection: "row",
|
|
264
|
+
alignItems: "center",
|
|
265
|
+
justifyContent: "center",
|
|
266
|
+
gap: 6,
|
|
267
|
+
},
|
|
268
|
+
dot: {
|
|
269
|
+
borderWidth: 0,
|
|
270
|
+
overflow: "hidden",
|
|
271
|
+
},
|
|
272
|
+
dotInactive: {
|
|
273
|
+
width: 8,
|
|
274
|
+
height: 8,
|
|
275
|
+
borderRadius: 4,
|
|
276
|
+
},
|
|
277
|
+
dotCurrent: {
|
|
278
|
+
width: 10,
|
|
279
|
+
height: 10,
|
|
280
|
+
borderRadius: 5,
|
|
281
|
+
borderWidth: 2,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
2
|
import { StyleSheet, Text, View } from "react-native";
|
|
3
3
|
import { useSnapStackDirection } from "../stack-direction-context";
|
|
4
|
+
import { useSnapVersion } from "../snap-version-context";
|
|
4
5
|
import { useSnapTheme } from "../theme";
|
|
5
6
|
|
|
6
7
|
const SIZE_STYLES: Record<string, { fontSize: number; lineHeight?: number; fontWeight?: "400" | "500" | "600" | "700" }> = {
|
|
7
|
-
md: { fontSize: 16, lineHeight:
|
|
8
|
-
sm: { fontSize: 13, lineHeight:
|
|
8
|
+
md: { fontSize: 16, lineHeight: 22 },
|
|
9
|
+
sm: { fontSize: 13, lineHeight: 16 },
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
const WEIGHT_MAP: Record<string, "400" | "500" | "600" | "700"> = {
|
|
@@ -21,11 +22,18 @@ export function SnapText({
|
|
|
21
22
|
const size = String(props.size ?? "md");
|
|
22
23
|
const weight = props.weight ? String(props.weight) : undefined;
|
|
23
24
|
const align = (props.align as "left" | "center" | "right" | undefined) ?? undefined;
|
|
25
|
+
const snapVersion = useSnapVersion();
|
|
24
26
|
|
|
25
27
|
const sizeStyle = SIZE_STYLES[size] ?? SIZE_STYLES.md;
|
|
26
28
|
const resolvedWeight = weight ? WEIGHT_MAP[weight] : sizeStyle?.fontWeight;
|
|
27
29
|
const textAlign = align === "center" ? "center" : align === "right" ? "right" : "left";
|
|
28
30
|
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
31
|
+
const maxLines =
|
|
32
|
+
typeof props.maxLines === "number"
|
|
33
|
+
? props.maxLines
|
|
34
|
+
: snapVersion === "2.0"
|
|
35
|
+
? 1
|
|
36
|
+
: undefined;
|
|
29
37
|
|
|
30
38
|
return (
|
|
31
39
|
<View style={inHorizontalStack ? styles.wrapRow : styles.wrapCol}>
|
|
@@ -40,6 +48,7 @@ export function SnapText({
|
|
|
40
48
|
textAlign,
|
|
41
49
|
},
|
|
42
50
|
]}
|
|
51
|
+
numberOfLines={maxLines}
|
|
43
52
|
>
|
|
44
53
|
{content}
|
|
45
54
|
</Text>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const SNAP_MAX_HEIGHT = 500;
|
|
2
|
+
|
|
3
|
+
export type SnapExpansionOptions = {
|
|
4
|
+
contentHeight: number;
|
|
5
|
+
internalExpanded: boolean;
|
|
6
|
+
forceExpanded?: boolean;
|
|
7
|
+
onExpandPress?: (() => void) | undefined;
|
|
8
|
+
expandButtonLabel?: string | undefined;
|
|
9
|
+
showOverflowWarning?: boolean | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SnapExpansionState = {
|
|
13
|
+
expandable: boolean;
|
|
14
|
+
clipped: boolean;
|
|
15
|
+
showButton: boolean;
|
|
16
|
+
buttonLabel: string;
|
|
17
|
+
useInternalToggle: boolean;
|
|
18
|
+
showOverflowWarning: boolean;
|
|
19
|
+
maxHeight: number | undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function getSnapExpansionState({
|
|
23
|
+
contentHeight,
|
|
24
|
+
internalExpanded,
|
|
25
|
+
forceExpanded = false,
|
|
26
|
+
onExpandPress,
|
|
27
|
+
expandButtonLabel,
|
|
28
|
+
showOverflowWarning = false,
|
|
29
|
+
}: SnapExpansionOptions): SnapExpansionState {
|
|
30
|
+
const hostControlled = typeof onExpandPress === "function";
|
|
31
|
+
const overflowWarning = showOverflowWarning && !forceExpanded;
|
|
32
|
+
const expandable =
|
|
33
|
+
!forceExpanded && !overflowWarning && contentHeight > SNAP_MAX_HEIGHT + 1;
|
|
34
|
+
const clipped = expandable && !internalExpanded;
|
|
35
|
+
const showButton = expandable;
|
|
36
|
+
const useInternalToggle = !hostControlled;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
expandable,
|
|
40
|
+
clipped,
|
|
41
|
+
showButton,
|
|
42
|
+
buttonLabel:
|
|
43
|
+
clipped && expandButtonLabel ? expandButtonLabel : internalExpanded ? "Show less" : "Show more",
|
|
44
|
+
useInternalToggle,
|
|
45
|
+
showOverflowWarning: overflowWarning,
|
|
46
|
+
maxHeight: clipped ? SNAP_MAX_HEIGHT : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -32,6 +32,9 @@ export function SnapCard({
|
|
|
32
32
|
actionError,
|
|
33
33
|
plain = false,
|
|
34
34
|
loadingOverlay,
|
|
35
|
+
forceExpanded,
|
|
36
|
+
expandButtonLabel,
|
|
37
|
+
onExpandPress,
|
|
35
38
|
}: {
|
|
36
39
|
snap: SnapPage;
|
|
37
40
|
handlers: SnapActionHandlers;
|
|
@@ -52,6 +55,12 @@ export function SnapCard({
|
|
|
52
55
|
plain?: boolean;
|
|
53
56
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
54
57
|
loadingOverlay?: ReactNode;
|
|
58
|
+
/** When true, render full content height without 500px clipping or expand controls. */
|
|
59
|
+
forceExpanded?: boolean;
|
|
60
|
+
/** Custom label for the collapsed expand button. */
|
|
61
|
+
expandButtonLabel?: string;
|
|
62
|
+
/** Called from the collapsed expand button instead of toggling internal state. */
|
|
63
|
+
onExpandPress?: () => void;
|
|
55
64
|
}) {
|
|
56
65
|
if (snap.version === SPEC_VERSION_2) {
|
|
57
66
|
return (
|
|
@@ -68,6 +77,9 @@ export function SnapCard({
|
|
|
68
77
|
actionError={actionError}
|
|
69
78
|
plain={plain}
|
|
70
79
|
loadingOverlay={loadingOverlay}
|
|
80
|
+
forceExpanded={forceExpanded}
|
|
81
|
+
expandButtonLabel={expandButtonLabel}
|
|
82
|
+
onExpandPress={onExpandPress}
|
|
71
83
|
/>
|
|
72
84
|
);
|
|
73
85
|
}
|
|
@@ -83,6 +95,9 @@ export function SnapCard({
|
|
|
83
95
|
actionError={actionError}
|
|
84
96
|
plain={plain}
|
|
85
97
|
loadingOverlay={loadingOverlay}
|
|
98
|
+
forceExpanded={forceExpanded}
|
|
99
|
+
expandButtonLabel={expandButtonLabel}
|
|
100
|
+
onExpandPress={onExpandPress}
|
|
86
101
|
/>
|
|
87
102
|
);
|
|
88
103
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { SpecVersion } from "@farcaster/snap";
|
|
3
|
+
|
|
4
|
+
const SnapVersionContext = createContext<SpecVersion>("1.0");
|
|
5
|
+
|
|
6
|
+
export const SnapVersionProvider = SnapVersionContext.Provider;
|
|
7
|
+
|
|
8
|
+
export function useSnapVersion(): SpecVersion {
|
|
9
|
+
return useContext(SnapVersionContext);
|
|
10
|
+
}
|
|
@@ -4,6 +4,7 @@ import { SnapCatalogView } from "./catalog-renderer";
|
|
|
4
4
|
import { ConfettiOverlay } from "./confetti-overlay";
|
|
5
5
|
import { FireworksOverlay } from "./fireworks-overlay";
|
|
6
6
|
import { useSnapTheme } from "./theme";
|
|
7
|
+
import { SnapVersionProvider } from "./snap-version-context";
|
|
7
8
|
import {
|
|
8
9
|
type ReactNode,
|
|
9
10
|
useCallback,
|
|
@@ -25,8 +26,13 @@ import type { SnapPage, SnapActionHandlers, JsonValue } from "./types";
|
|
|
25
26
|
|
|
26
27
|
export function applyStatePaths(
|
|
27
28
|
model: Record<string, unknown>,
|
|
28
|
-
changes:
|
|
29
|
+
changes:
|
|
30
|
+
| { path: string; value: unknown }[]
|
|
31
|
+
| Record<string, unknown>
|
|
32
|
+
| null
|
|
33
|
+
| undefined,
|
|
29
34
|
): void {
|
|
35
|
+
if (!changes) return;
|
|
30
36
|
const entries = Array.isArray(changes)
|
|
31
37
|
? changes.map((c) => [c.path, c.value] as const)
|
|
32
38
|
: Object.entries(changes);
|
|
@@ -57,6 +63,30 @@ export function applyStatePaths(
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
function withDefaultElementProps(spec: Spec): Spec {
|
|
67
|
+
if (!spec || typeof spec !== "object" || !("elements" in spec)) return spec;
|
|
68
|
+
const elements = spec.elements as unknown as Record<
|
|
69
|
+
string,
|
|
70
|
+
Record<string, unknown>
|
|
71
|
+
>;
|
|
72
|
+
if (!elements || typeof elements !== "object") return spec;
|
|
73
|
+
|
|
74
|
+
let changed = false;
|
|
75
|
+
const nextElements: Record<string, Record<string, unknown>> = {};
|
|
76
|
+
for (const [id, element] of Object.entries(elements)) {
|
|
77
|
+
if (element.props !== undefined) {
|
|
78
|
+
nextElements[id] = element;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
changed = true;
|
|
82
|
+
nextElements[id] = { ...element, props: {} };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return changed
|
|
86
|
+
? ({ ...spec, elements: nextElements } as unknown as Spec)
|
|
87
|
+
: spec;
|
|
88
|
+
}
|
|
89
|
+
|
|
60
90
|
export function resolveAccentHex(
|
|
61
91
|
accent: string | undefined,
|
|
62
92
|
appearance: "light" | "dark",
|
|
@@ -87,7 +117,10 @@ export function SnapViewCoreInner({
|
|
|
87
117
|
loadingOverlay?: ReactNode;
|
|
88
118
|
}) {
|
|
89
119
|
const { mode } = useSnapTheme();
|
|
90
|
-
const spec =
|
|
120
|
+
const spec = useMemo(
|
|
121
|
+
() => withDefaultElementProps(snap.ui as unknown as Spec) as SnapPage["ui"],
|
|
122
|
+
[snap.ui],
|
|
123
|
+
);
|
|
91
124
|
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
92
125
|
|
|
93
126
|
const initialState = useMemo(
|
|
@@ -206,16 +239,18 @@ export function SnapViewCoreInner({
|
|
|
206
239
|
)
|
|
207
240
|
: loadingOverlay
|
|
208
241
|
: null}
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
242
|
+
<SnapVersionProvider value={snap.version === "2.0" ? "2.0" : "1.0"}>
|
|
243
|
+
<SnapCatalogView
|
|
244
|
+
key={pageKey}
|
|
245
|
+
spec={spec}
|
|
246
|
+
state={initialState}
|
|
247
|
+
loading={false}
|
|
248
|
+
onStateChange={(changes) => {
|
|
249
|
+
applyStatePaths(stateRef.current, changes);
|
|
250
|
+
}}
|
|
251
|
+
onAction={handleAction}
|
|
252
|
+
/>
|
|
253
|
+
</SnapVersionProvider>
|
|
219
254
|
{showConfetti && <ConfettiOverlay key={confettiKey} />}
|
|
220
255
|
{showFireworks && <FireworksOverlay key={fireworksKey} />}
|
|
221
256
|
</View>
|