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