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