@farcaster/snap 2.5.1 → 2.6.1
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 +13 -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 +4 -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 +13 -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 +4 -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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStateStore } from "@json-render/react-native";
|
|
3
|
+
import { Children, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { Animated, Pressable, StyleSheet, View } from "react-native";
|
|
5
|
+
import { ChevronLeft, ChevronRight } from "lucide-react-native";
|
|
6
|
+
import { useSnapPalette } from "../use-snap-palette.js";
|
|
7
|
+
import { useSnapTheme } from "../theme.js";
|
|
8
|
+
import { clampPaginatorPage, pageFromValue, SNAP_PAGINATOR_PAGE_COUNT_PATH, SNAP_PAGINATOR_PAGE_PATH, } from "../../ui/paginator-state.js";
|
|
9
|
+
function clampInitialPage(value, pageCount) {
|
|
10
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
11
|
+
return 0;
|
|
12
|
+
return Math.min(Math.max(value, 0), Math.max(pageCount - 1, 0));
|
|
13
|
+
}
|
|
14
|
+
export function SnapPaginator({ element: { props }, children, }) {
|
|
15
|
+
const pages = useMemo(() => Children.toArray(children), [children]);
|
|
16
|
+
const { colors, mode } = useSnapTheme();
|
|
17
|
+
const { accentHex } = useSnapPalette();
|
|
18
|
+
const { get, set } = useStateStore();
|
|
19
|
+
const initialPage = clampInitialPage(props.initialPage, pages.length);
|
|
20
|
+
const page = clampPaginatorPage(pageFromValue(get(SNAP_PAGINATOR_PAGE_PATH), initialPage), pages.length);
|
|
21
|
+
const activePage = Math.min(page, Math.max(pages.length - 1, 0));
|
|
22
|
+
const showControls = props.showControls !== false && pages.length > 1;
|
|
23
|
+
const showIndicators = props.showIndicators !== false && pages.length > 1;
|
|
24
|
+
const controlsPosition = props.controlsPosition === "top" ? "top" : "bottom";
|
|
25
|
+
const transition = props.transition === "fade" ||
|
|
26
|
+
props.transition === "scale" ||
|
|
27
|
+
props.transition === "none"
|
|
28
|
+
? props.transition
|
|
29
|
+
: "slide";
|
|
30
|
+
const showControlBar = showControls || showIndicators;
|
|
31
|
+
const [transitionDirection, setTransitionDirection] = useState("next");
|
|
32
|
+
const pageAnim = useRef(new Animated.Value(1)).current;
|
|
33
|
+
const canGoPrev = activePage > 0;
|
|
34
|
+
const canGoNext = activePage < pages.length - 1;
|
|
35
|
+
const goToPage = (targetPage) => {
|
|
36
|
+
const nextPage = clampPaginatorPage(targetPage, pages.length);
|
|
37
|
+
if (nextPage !== activePage) {
|
|
38
|
+
setTransitionDirection(nextPage > activePage ? "next" : "previous");
|
|
39
|
+
}
|
|
40
|
+
set(SNAP_PAGINATOR_PAGE_PATH, nextPage);
|
|
41
|
+
};
|
|
42
|
+
const goPrev = () => goToPage(activePage - 1);
|
|
43
|
+
const goNext = () => goToPage(activePage + 1);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (pages.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
const nextPage = clampPaginatorPage(pageFromValue(get(SNAP_PAGINATOR_PAGE_PATH), initialPage), pages.length);
|
|
48
|
+
if (get(SNAP_PAGINATOR_PAGE_PATH) !== nextPage) {
|
|
49
|
+
set(SNAP_PAGINATOR_PAGE_PATH, nextPage);
|
|
50
|
+
}
|
|
51
|
+
if (get(SNAP_PAGINATOR_PAGE_COUNT_PATH) !== pages.length) {
|
|
52
|
+
set(SNAP_PAGINATOR_PAGE_COUNT_PATH, pages.length);
|
|
53
|
+
}
|
|
54
|
+
}, [get, initialPage, pages.length, set]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (transition === "none") {
|
|
57
|
+
pageAnim.setValue(1);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
pageAnim.setValue(0);
|
|
61
|
+
Animated.timing(pageAnim, {
|
|
62
|
+
toValue: 1,
|
|
63
|
+
duration: transition === "scale" ? 240 : transition === "slide" ? 260 : 180,
|
|
64
|
+
useNativeDriver: true,
|
|
65
|
+
}).start();
|
|
66
|
+
}, [activePage, pageAnim, transition]);
|
|
67
|
+
if (pages.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
const controlBar = showControlBar ? (_jsxs(View, { style: styles.footer, children: [showControls ? (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Previous page", disabled: !canGoPrev, onPress: goPrev, style: [
|
|
70
|
+
styles.control,
|
|
71
|
+
{
|
|
72
|
+
borderColor: colors.border,
|
|
73
|
+
backgroundColor: colors.muted,
|
|
74
|
+
opacity: canGoPrev ? 1 : 0.35,
|
|
75
|
+
},
|
|
76
|
+
], children: _jsx(ChevronLeft, { size: 15, color: colors.text }) })) : (_jsx(View, { style: styles.controlPlaceholder })), showIndicators ? (_jsx(View, { style: styles.indicators, children: pages.map((_, index) => {
|
|
77
|
+
const current = index === activePage;
|
|
78
|
+
return (_jsx(View, { accessibilityLabel: `Page ${index + 1}${current ? ", current" : ""}`, style: [
|
|
79
|
+
styles.dot,
|
|
80
|
+
current ? styles.dotCurrent : styles.dotInactive,
|
|
81
|
+
{
|
|
82
|
+
backgroundColor: current
|
|
83
|
+
? accentHex
|
|
84
|
+
: mode === "dark"
|
|
85
|
+
? "rgba(255,255,255,0.5)"
|
|
86
|
+
: "rgba(0,0,0,0.28)",
|
|
87
|
+
borderColor: current
|
|
88
|
+
? mode === "dark"
|
|
89
|
+
? "rgba(255,255,255,0.18)"
|
|
90
|
+
: "rgba(0,0,0,0.12)"
|
|
91
|
+
: "transparent",
|
|
92
|
+
},
|
|
93
|
+
] }, index));
|
|
94
|
+
}) })) : (_jsx(View, { style: styles.indicators })), showControls ? (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Next page", disabled: !canGoNext, onPress: goNext, style: [
|
|
95
|
+
styles.control,
|
|
96
|
+
{
|
|
97
|
+
borderColor: colors.border,
|
|
98
|
+
backgroundColor: colors.muted,
|
|
99
|
+
opacity: canGoNext ? 1 : 0.35,
|
|
100
|
+
},
|
|
101
|
+
], children: _jsx(ChevronRight, { size: 15, color: colors.text }) })) : (_jsx(View, { style: styles.controlPlaceholder }))] })) : null;
|
|
102
|
+
const animatedPageStyle = transition === "none"
|
|
103
|
+
? undefined
|
|
104
|
+
: {
|
|
105
|
+
opacity: pageAnim.interpolate({
|
|
106
|
+
inputRange: [0, 1],
|
|
107
|
+
outputRange: [transition === "fade" ? 0.2 : 0.35, 1],
|
|
108
|
+
}),
|
|
109
|
+
transform: transition === "scale"
|
|
110
|
+
? [
|
|
111
|
+
{
|
|
112
|
+
scale: pageAnim.interpolate({
|
|
113
|
+
inputRange: [0, 1],
|
|
114
|
+
outputRange: [0.94, 1],
|
|
115
|
+
}),
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
: transition === "slide"
|
|
119
|
+
? [
|
|
120
|
+
{
|
|
121
|
+
translateX: pageAnim.interpolate({
|
|
122
|
+
inputRange: [0, 1],
|
|
123
|
+
outputRange: [
|
|
124
|
+
transitionDirection === "previous" ? -22 : 22,
|
|
125
|
+
0,
|
|
126
|
+
],
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
scale: pageAnim.interpolate({
|
|
131
|
+
inputRange: [0, 1],
|
|
132
|
+
outputRange: [0.985, 1],
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
: [],
|
|
137
|
+
};
|
|
138
|
+
return (_jsxs(View, { style: styles.wrap, children: [controlsPosition === "top" ? controlBar : null, _jsx(Animated.View, { style: [
|
|
139
|
+
styles.page,
|
|
140
|
+
animatedPageStyle,
|
|
141
|
+
], children: pages[activePage] }), controlsPosition === "bottom" ? controlBar : null] }));
|
|
142
|
+
}
|
|
143
|
+
const styles = StyleSheet.create({
|
|
144
|
+
wrap: {
|
|
145
|
+
width: "100%",
|
|
146
|
+
minWidth: 0,
|
|
147
|
+
gap: 8,
|
|
148
|
+
},
|
|
149
|
+
page: {
|
|
150
|
+
width: "100%",
|
|
151
|
+
minWidth: 0,
|
|
152
|
+
},
|
|
153
|
+
footer: {
|
|
154
|
+
minHeight: 28,
|
|
155
|
+
flexDirection: "row",
|
|
156
|
+
alignItems: "center",
|
|
157
|
+
justifyContent: "space-between",
|
|
158
|
+
gap: 8,
|
|
159
|
+
},
|
|
160
|
+
control: {
|
|
161
|
+
width: 28,
|
|
162
|
+
height: 28,
|
|
163
|
+
borderWidth: 1,
|
|
164
|
+
borderRadius: 6,
|
|
165
|
+
alignItems: "center",
|
|
166
|
+
justifyContent: "center",
|
|
167
|
+
},
|
|
168
|
+
controlPlaceholder: {
|
|
169
|
+
width: 28,
|
|
170
|
+
height: 28,
|
|
171
|
+
},
|
|
172
|
+
indicators: {
|
|
173
|
+
flex: 1,
|
|
174
|
+
flexDirection: "row",
|
|
175
|
+
alignItems: "center",
|
|
176
|
+
justifyContent: "center",
|
|
177
|
+
gap: 6,
|
|
178
|
+
},
|
|
179
|
+
dot: {
|
|
180
|
+
borderWidth: 0,
|
|
181
|
+
overflow: "hidden",
|
|
182
|
+
},
|
|
183
|
+
dotInactive: {
|
|
184
|
+
width: 8,
|
|
185
|
+
height: 8,
|
|
186
|
+
borderRadius: 4,
|
|
187
|
+
},
|
|
188
|
+
dotCurrent: {
|
|
189
|
+
width: 10,
|
|
190
|
+
height: 10,
|
|
191
|
+
borderRadius: 5,
|
|
192
|
+
borderWidth: 2,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
@@ -3,8 +3,8 @@ import { StyleSheet, Text, View } from "react-native";
|
|
|
3
3
|
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
4
4
|
import { useSnapTheme } from "../theme.js";
|
|
5
5
|
const SIZE_STYLES = {
|
|
6
|
-
md: { fontSize: 16, lineHeight:
|
|
7
|
-
sm: { fontSize: 13, lineHeight:
|
|
6
|
+
md: { fontSize: 16, lineHeight: 22 },
|
|
7
|
+
sm: { fontSize: 13, lineHeight: 16 },
|
|
8
8
|
};
|
|
9
9
|
const WEIGHT_MAP = {
|
|
10
10
|
bold: "700",
|
|
@@ -20,6 +20,7 @@ export function SnapText({ element: { props }, }) {
|
|
|
20
20
|
const resolvedWeight = weight ? WEIGHT_MAP[weight] : sizeStyle?.fontWeight;
|
|
21
21
|
const textAlign = align === "center" ? "center" : align === "right" ? "right" : "left";
|
|
22
22
|
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
23
|
+
const maxLines = typeof props.maxLines === "number" ? props.maxLines : undefined;
|
|
23
24
|
return (_jsx(View, { style: inHorizontalStack ? styles.wrapRow : styles.wrapCol, children: _jsx(Text, { style: [
|
|
24
25
|
styles.base,
|
|
25
26
|
{
|
|
@@ -29,7 +30,7 @@ export function SnapText({ element: { props }, }) {
|
|
|
29
30
|
fontWeight: resolvedWeight,
|
|
30
31
|
textAlign,
|
|
31
32
|
},
|
|
32
|
-
], children: content }) }));
|
|
33
|
+
], numberOfLines: maxLines, children: content }) }));
|
|
33
34
|
}
|
|
34
35
|
const styles = StyleSheet.create({
|
|
35
36
|
/** Full width for vertical stacks (alignment / wrapping). */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const SNAP_MAX_HEIGHT = 500;
|
|
2
|
+
export type SnapExpansionOptions = {
|
|
3
|
+
contentHeight: number;
|
|
4
|
+
internalExpanded: boolean;
|
|
5
|
+
forceExpanded?: boolean;
|
|
6
|
+
onExpandPress?: (() => void) | undefined;
|
|
7
|
+
expandButtonLabel?: string | undefined;
|
|
8
|
+
showOverflowWarning?: boolean | undefined;
|
|
9
|
+
};
|
|
10
|
+
export type SnapExpansionState = {
|
|
11
|
+
expandable: boolean;
|
|
12
|
+
clipped: boolean;
|
|
13
|
+
showButton: boolean;
|
|
14
|
+
buttonLabel: string;
|
|
15
|
+
useInternalToggle: boolean;
|
|
16
|
+
showOverflowWarning: boolean;
|
|
17
|
+
maxHeight: number | undefined;
|
|
18
|
+
};
|
|
19
|
+
export declare function getSnapExpansionState({ contentHeight, internalExpanded, forceExpanded, onExpandPress, expandButtonLabel, showOverflowWarning, }: SnapExpansionOptions): SnapExpansionState;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const SNAP_MAX_HEIGHT = 500;
|
|
2
|
+
export function getSnapExpansionState({ contentHeight, internalExpanded, forceExpanded = false, onExpandPress, expandButtonLabel, showOverflowWarning = false, }) {
|
|
3
|
+
const hostControlled = typeof onExpandPress === "function";
|
|
4
|
+
const overflowWarning = showOverflowWarning && !forceExpanded;
|
|
5
|
+
const expandable = !forceExpanded && !overflowWarning && contentHeight > SNAP_MAX_HEIGHT + 1;
|
|
6
|
+
const clipped = expandable && !internalExpanded;
|
|
7
|
+
const showButton = expandable;
|
|
8
|
+
const useInternalToggle = !hostControlled;
|
|
9
|
+
return {
|
|
10
|
+
expandable,
|
|
11
|
+
clipped,
|
|
12
|
+
showButton,
|
|
13
|
+
buttonLabel: clipped && expandButtonLabel ? expandButtonLabel : internalExpanded ? "Show less" : "Show more",
|
|
14
|
+
useInternalToggle,
|
|
15
|
+
showOverflowWarning: overflowWarning,
|
|
16
|
+
maxHeight: clipped ? SNAP_MAX_HEIGHT : undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -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, loadingOverlay, }: {
|
|
10
|
+
export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }: {
|
|
11
11
|
snap: SnapPage;
|
|
12
12
|
handlers: SnapActionHandlers;
|
|
13
13
|
loading?: boolean;
|
|
@@ -27,4 +27,10 @@ export declare function SnapCard({ snap, handlers, loading, appearance, colors,
|
|
|
27
27
|
plain?: boolean;
|
|
28
28
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
29
29
|
loadingOverlay?: ReactNode;
|
|
30
|
+
/** When true, render full content height without 500px clipping or expand controls. */
|
|
31
|
+
forceExpanded?: boolean;
|
|
32
|
+
/** Custom label for the collapsed expand button. */
|
|
33
|
+
expandButtonLabel?: string;
|
|
34
|
+
/** Called from the collapsed expand button instead of toggling internal state. */
|
|
35
|
+
onExpandPress?: () => void;
|
|
30
36
|
}): 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, loadingOverlay, }) {
|
|
10
|
+
export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }) {
|
|
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, loadingOverlay: loadingOverlay }));
|
|
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, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress }));
|
|
13
13
|
}
|
|
14
|
-
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay }));
|
|
14
|
+
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress }));
|
|
15
15
|
}
|
|
@@ -3,7 +3,7 @@ import type { SnapPage, SnapActionHandlers } from "./types.js";
|
|
|
3
3
|
export declare function applyStatePaths(model: Record<string, unknown>, changes: {
|
|
4
4
|
path: string;
|
|
5
5
|
value: unknown;
|
|
6
|
-
}[] | Record<string, unknown>): void;
|
|
6
|
+
}[] | Record<string, unknown> | null | undefined): void;
|
|
7
7
|
export declare function resolveAccentHex(accent: string | undefined, appearance: "light" | "dark"): string;
|
|
8
8
|
export declare function SnapViewCoreInner({ snap, handlers, loading, loadingOverlay, }: {
|
|
9
9
|
snap: SnapPage;
|
|
@@ -4,11 +4,14 @@ import { SnapCatalogView } from "./catalog-renderer.js";
|
|
|
4
4
|
import { ConfettiOverlay } from "./confetti-overlay.js";
|
|
5
5
|
import { FireworksOverlay } from "./fireworks-overlay.js";
|
|
6
6
|
import { useSnapTheme } from "./theme.js";
|
|
7
|
+
import { SnapVersionProvider } from "./snap-version-context.js";
|
|
7
8
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
8
9
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
9
10
|
import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
|
|
10
11
|
// ─── Shared helpers ──────────────────────────────────
|
|
11
12
|
export function applyStatePaths(model, changes) {
|
|
13
|
+
if (!changes)
|
|
14
|
+
return;
|
|
12
15
|
const entries = Array.isArray(changes)
|
|
13
16
|
? changes.map((c) => [c.path, c.value])
|
|
14
17
|
: Object.entries(changes);
|
|
@@ -39,6 +42,26 @@ export function applyStatePaths(model, changes) {
|
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
}
|
|
45
|
+
function withDefaultElementProps(spec) {
|
|
46
|
+
if (!spec || typeof spec !== "object" || !("elements" in spec))
|
|
47
|
+
return spec;
|
|
48
|
+
const elements = spec.elements;
|
|
49
|
+
if (!elements || typeof elements !== "object")
|
|
50
|
+
return spec;
|
|
51
|
+
let changed = false;
|
|
52
|
+
const nextElements = {};
|
|
53
|
+
for (const [id, element] of Object.entries(elements)) {
|
|
54
|
+
if (element.props !== undefined) {
|
|
55
|
+
nextElements[id] = element;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
changed = true;
|
|
59
|
+
nextElements[id] = { ...element, props: {} };
|
|
60
|
+
}
|
|
61
|
+
return changed
|
|
62
|
+
? { ...spec, elements: nextElements }
|
|
63
|
+
: spec;
|
|
64
|
+
}
|
|
42
65
|
export function resolveAccentHex(accent, appearance) {
|
|
43
66
|
const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
|
|
44
67
|
const name = accent && Object.hasOwn(map, accent)
|
|
@@ -49,7 +72,7 @@ export function resolveAccentHex(accent, appearance) {
|
|
|
49
72
|
// ─── Core rendering component (no validation) ────────
|
|
50
73
|
export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOverlay, }) {
|
|
51
74
|
const { mode } = useSnapTheme();
|
|
52
|
-
const spec = snap.ui;
|
|
75
|
+
const spec = useMemo(() => withDefaultElementProps(snap.ui), [snap.ui]);
|
|
53
76
|
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
54
77
|
const initialState = useMemo(() => ({
|
|
55
78
|
...(spec.state ?? {}),
|
|
@@ -150,9 +173,9 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
150
173
|
? loadingOverlay === undefined
|
|
151
174
|
? (_jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex }))
|
|
152
175
|
: loadingOverlay
|
|
153
|
-
: null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
: null, _jsx(SnapVersionProvider, { value: snap.version === "2.0" ? "2.0" : "1.0", children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
177
|
+
applyStatePaths(stateRef.current, changes);
|
|
178
|
+
}, onAction: handleAction }, pageKey) }), showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), showFireworks && _jsx(FireworksOverlay, {}, fireworksKey)] }));
|
|
156
179
|
}
|
|
157
180
|
export function SnapLoadingOverlay({ appearance, accentHex, }) {
|
|
158
181
|
return (_jsx(View, { style: [
|
|
@@ -16,7 +16,7 @@ export declare function SnapViewV1({ snap, handlers, loading, appearance, colors
|
|
|
16
16
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
17
17
|
loadingOverlay?: ReactNode;
|
|
18
18
|
}): import("react").JSX.Element;
|
|
19
|
-
export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, loadingOverlay, }: {
|
|
19
|
+
export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }: {
|
|
20
20
|
snap: SnapPage;
|
|
21
21
|
handlers: SnapActionHandlers;
|
|
22
22
|
loading?: boolean;
|
|
@@ -27,4 +27,10 @@ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors
|
|
|
27
27
|
plain?: boolean;
|
|
28
28
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
29
29
|
loadingOverlay?: ReactNode;
|
|
30
|
+
/** When true, render full content height without 500px clipping or expand controls. */
|
|
31
|
+
forceExpanded?: boolean;
|
|
32
|
+
/** Custom label for the collapsed expand button. */
|
|
33
|
+
expandButtonLabel?: string;
|
|
34
|
+
/** Called from the collapsed expand button instead of toggling internal state. */
|
|
35
|
+
onExpandPress?: () => void;
|
|
30
36
|
}): import("react").JSX.Element;
|
|
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { View, Text, StyleSheet, Pressable } from "react-native";
|
|
4
4
|
import { SnapThemeProvider, useSnapTheme } from "../theme.js";
|
|
5
5
|
import { SnapLoadingOverlay, SnapViewCoreInner, resolveAccentHex, } from "../snap-view-core.js";
|
|
6
|
-
|
|
6
|
+
import { getSnapExpansionState } from "../expand-state.js";
|
|
7
7
|
// ─── SnapViewV1 (no validation) ──────────────────────
|
|
8
8
|
export function SnapViewV1Inner({ snap, handlers, loading = false, loadingOverlay, }) {
|
|
9
9
|
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }));
|
|
@@ -12,7 +12,7 @@ export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark
|
|
|
12
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, loadingOverlay, }) {
|
|
15
|
+
function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }) {
|
|
16
16
|
const { colors, mode } = useSnapTheme();
|
|
17
17
|
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
18
18
|
const [contentHeight, setContentHeight] = useState(0);
|
|
@@ -21,8 +21,14 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
21
21
|
setIsExpanded(false);
|
|
22
22
|
setContentHeight(0);
|
|
23
23
|
}, [snap]);
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const expansion = getSnapExpansionState({
|
|
25
|
+
contentHeight,
|
|
26
|
+
internalExpanded: isExpanded,
|
|
27
|
+
forceExpanded,
|
|
28
|
+
onExpandPress,
|
|
29
|
+
expandButtonLabel,
|
|
30
|
+
});
|
|
31
|
+
const expandButtonInsideCard = typeof onExpandPress === "function";
|
|
26
32
|
const isDark = mode === "dark";
|
|
27
33
|
const pillBg = isDark ? "rgba(40,40,40,0.92)" : "rgba(255,255,255,0.92)";
|
|
28
34
|
const pillBgPressed = isDark ? "rgba(60,60,60,0.95)" : "rgba(240,240,240,0.95)";
|
|
@@ -33,9 +39,11 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
33
39
|
borderColor: colors.border,
|
|
34
40
|
backgroundColor: colors.surface,
|
|
35
41
|
},
|
|
36
|
-
], children: [_jsx(View, { style:
|
|
42
|
+
], children: [_jsx(View, { style: expansion.clipped
|
|
43
|
+
? { maxHeight: expansion.maxHeight, overflow: "hidden" }
|
|
44
|
+
: undefined, children: _jsx(View, { collapsable: false, onLayout: (event) => {
|
|
37
45
|
const nextHeight = Math.round(event.nativeEvent.layout.height);
|
|
38
|
-
setContentHeight((currentHeight) =>
|
|
46
|
+
setContentHeight((currentHeight) => expansion.clipped
|
|
39
47
|
? Math.max(currentHeight, nextHeight)
|
|
40
48
|
: currentHeight === nextHeight
|
|
41
49
|
? currentHeight
|
|
@@ -44,15 +52,22 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
44
52
|
? loadingOverlay === undefined
|
|
45
53
|
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
46
54
|
: loadingOverlay
|
|
47
|
-
: null] }),
|
|
55
|
+
: null] }), expansion.showButton ? (_jsx(View, { pointerEvents: "box-none", style: expandButtonInsideCard
|
|
56
|
+
? cardStyles.expandFloatInset
|
|
57
|
+
: cardStyles.expandFloat, children: _jsx(Pressable, { style: ({ pressed }) => [
|
|
48
58
|
cardStyles.expandButton,
|
|
49
59
|
{
|
|
50
60
|
backgroundColor: pressed ? pillBgPressed : pillBg,
|
|
51
61
|
borderColor: colors.border,
|
|
52
62
|
},
|
|
53
63
|
], onPress: () => {
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
if (expansion.useInternalToggle) {
|
|
65
|
+
setIsExpanded((value) => !value);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
onExpandPress?.();
|
|
69
|
+
}
|
|
70
|
+
}, children: _jsx(Text, { style: [cardStyles.expandButtonText, { color: colors.text }], children: expansion.buttonLabel }) }) })) : null] }), actionError && (_jsx(Text, { style: [
|
|
56
71
|
cardStyles.actionError,
|
|
57
72
|
{
|
|
58
73
|
color: appearance === "dark"
|
|
@@ -61,8 +76,8 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
61
76
|
},
|
|
62
77
|
], children: actionError }))] }));
|
|
63
78
|
}
|
|
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 }) }));
|
|
79
|
+
export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, plain = false, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }) {
|
|
80
|
+
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, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress }) }));
|
|
66
81
|
}
|
|
67
82
|
const cardStyles = StyleSheet.create({
|
|
68
83
|
frameRing: { alignSelf: "stretch" },
|
|
@@ -77,6 +92,15 @@ const cardStyles = StyleSheet.create({
|
|
|
77
92
|
alignItems: "center",
|
|
78
93
|
justifyContent: "center",
|
|
79
94
|
},
|
|
95
|
+
expandFloatInset: {
|
|
96
|
+
position: "absolute",
|
|
97
|
+
left: 0,
|
|
98
|
+
right: 0,
|
|
99
|
+
bottom: 10,
|
|
100
|
+
height: 28,
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
justifyContent: "center",
|
|
103
|
+
},
|
|
80
104
|
expandButton: {
|
|
81
105
|
minWidth: 92,
|
|
82
106
|
alignItems: "center",
|
|
@@ -21,7 +21,7 @@ export declare function SnapViewV2({ snap, handlers, loading, appearance, colors
|
|
|
21
21
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
22
22
|
loadingOverlay?: ReactNode;
|
|
23
23
|
}): import("react").JSX.Element;
|
|
24
|
-
export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
|
|
24
|
+
export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }: {
|
|
25
25
|
snap: SnapPage;
|
|
26
26
|
handlers: SnapActionHandlers;
|
|
27
27
|
loading?: boolean;
|
|
@@ -35,4 +35,10 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors
|
|
|
35
35
|
plain?: boolean;
|
|
36
36
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
37
37
|
loadingOverlay?: ReactNode;
|
|
38
|
+
/** When true, render full content height without 500px clipping or expand controls. */
|
|
39
|
+
forceExpanded?: boolean;
|
|
40
|
+
/** Custom label for the collapsed expand button. */
|
|
41
|
+
expandButtonLabel?: string;
|
|
42
|
+
/** Called from the collapsed expand button instead of toggling internal state. */
|
|
43
|
+
onExpandPress?: () => void;
|
|
38
44
|
}): import("react").JSX.Element;
|