@farcaster/snap 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react/components/cell-grid.d.ts +3 -1
- package/dist/react/components/cell-grid.js +8 -4
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.js +3 -3
- package/dist/react/snap-view-core.d.ts +12 -1
- package/dist/react/snap-view-core.js +10 -5
- package/dist/react/v1/snap-view.d.ts +7 -2
- package/dist/react/v1/snap-view.js +48 -40
- package/dist/react/v2/snap-view.d.ts +6 -2
- package/dist/react/v2/snap-view.js +98 -33
- package/dist/react-native/components/snap-cell-grid.d.ts +1 -1
- package/dist/react-native/components/snap-cell-grid.js +10 -4
- package/dist/react-native/confetti-overlay.js +33 -36
- package/dist/react-native/index.d.ts +3 -1
- package/dist/react-native/index.js +3 -3
- package/dist/react-native/snap-view-core.d.ts +11 -1
- package/dist/react-native/snap-view-core.js +25 -9
- package/dist/react-native/v1/snap-view.d.ts +9 -3
- package/dist/react-native/v1/snap-view.js +51 -52
- package/dist/react-native/v2/snap-view.d.ts +8 -3
- package/dist/react-native/v2/snap-view.js +92 -21
- package/dist/ui/catalog.js +2 -2
- package/dist/validator.js +8 -33
- package/llms.txt +26 -3
- package/package.json +1 -1
- package/src/react/components/cell-grid.tsx +11 -5
- package/src/react/index.tsx +5 -0
- package/src/react/snap-view-core.tsx +23 -8
- package/src/react/v1/snap-view.tsx +84 -55
- package/src/react/v2/snap-view.tsx +165 -52
- package/src/react-native/components/snap-cell-grid.tsx +11 -4
- package/src/react-native/confetti-overlay.tsx +40 -37
- package/src/react-native/index.tsx +5 -0
- package/src/react-native/snap-view-core.tsx +56 -14
- package/src/react-native/v1/snap-view.tsx +71 -47
- package/src/react-native/v2/snap-view.tsx +166 -28
- package/src/ui/catalog.ts +2 -2
- package/src/validator.ts +22 -46
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export declare function SnapCellGrid({ element: { props }, }: {
|
|
1
|
+
export declare function SnapCellGrid({ element: { props, on }, emit, }: {
|
|
2
2
|
element: {
|
|
3
3
|
props: Record<string, unknown>;
|
|
4
|
+
on?: Record<string, unknown>;
|
|
4
5
|
};
|
|
6
|
+
emit: (name: string) => void;
|
|
5
7
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,14 +4,16 @@ import { useStateStore } from "@json-render/react";
|
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
5
|
import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
6
6
|
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
7
|
-
export function SnapCellGrid({ element: { props }, }) {
|
|
7
|
+
export function SnapCellGrid({ element: { props, on }, emit, }) {
|
|
8
8
|
const { get, set } = useStateStore();
|
|
9
9
|
const colors = useSnapColors();
|
|
10
10
|
const cols = Number(props.cols ?? 2);
|
|
11
11
|
const rows = Number(props.rows ?? 2);
|
|
12
12
|
const select = String(props.select ?? "off");
|
|
13
|
-
const interactive = select !== "off";
|
|
14
13
|
const isMultiple = select === "multiple";
|
|
14
|
+
const isSelectable = select !== "off";
|
|
15
|
+
const hasPressAction = Boolean(on?.press);
|
|
16
|
+
const interactive = isSelectable || hasPressAction;
|
|
15
17
|
const cells = Array.isArray(props.cells) ? props.cells : [];
|
|
16
18
|
const gap = String(props.gap ?? "sm");
|
|
17
19
|
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
@@ -28,7 +30,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
28
30
|
selectedSet.add(part);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
|
-
const isSelected = (r, c) => selectedSet.has(`${r},${c}`);
|
|
33
|
+
const isSelected = (r, c) => isSelectable && selectedSet.has(`${r},${c}`);
|
|
32
34
|
const handleTap = (r, c) => {
|
|
33
35
|
const key = `${r},${c}`;
|
|
34
36
|
if (isMultiple) {
|
|
@@ -42,6 +44,8 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
42
44
|
else {
|
|
43
45
|
set(tapPath, key);
|
|
44
46
|
}
|
|
47
|
+
if (hasPressAction)
|
|
48
|
+
emit("press");
|
|
45
49
|
};
|
|
46
50
|
const cellMap = new Map();
|
|
47
51
|
for (const c of cells) {
|
|
@@ -72,7 +76,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
72
76
|
}, children: cell?.content ?? "" }, `${r}-${c}`));
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
|
-
const selectionLabel =
|
|
79
|
+
const selectionLabel = isSelectable && selectedSet.size > 0
|
|
76
80
|
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
77
81
|
: null;
|
|
78
82
|
return (_jsxs("div", { children: [_jsx("div", { style: {
|
package/dist/react/index.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export type SnapActionHandlers = {
|
|
|
42
42
|
buyToken?: string;
|
|
43
43
|
}) => void;
|
|
44
44
|
};
|
|
45
|
-
export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
|
|
45
|
+
export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
|
|
46
46
|
snap: SnapPage;
|
|
47
47
|
handlers: SnapActionHandlers;
|
|
48
48
|
loading?: boolean;
|
|
@@ -56,4 +56,6 @@ export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth
|
|
|
56
56
|
actionError?: string | null;
|
|
57
57
|
/** When true, renders without card frame (no border, background, or padding). */
|
|
58
58
|
plain?: boolean;
|
|
59
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
60
|
+
loadingOverlay?: ReactNode;
|
|
59
61
|
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/react/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import { SPEC_VERSION_2 } from "../constants.js";
|
|
|
4
4
|
import { SnapCardV1 } from "./v1/snap-view.js";
|
|
5
5
|
import { SnapCardV2 } from "./v2/snap-view.js";
|
|
6
6
|
// ─── SnapCard ────────────────────────────────────────
|
|
7
|
-
export function SnapCard({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
|
|
7
|
+
export function SnapCard({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
|
|
8
8
|
if (snap.version === SPEC_VERSION_2) {
|
|
9
|
-
return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain }));
|
|
9
|
+
return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay }));
|
|
10
10
|
}
|
|
11
|
-
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, actionError: actionError, plain: plain }));
|
|
11
|
+
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay }));
|
|
12
12
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import type { SnapActionHandlers, SnapPage } from "./index.js";
|
|
2
3
|
export declare function applyStatePaths(model: Record<string, unknown>, changes: {
|
|
3
4
|
path: string;
|
|
4
5
|
value: unknown;
|
|
5
6
|
}[] | Record<string, unknown>): void;
|
|
6
|
-
export declare function
|
|
7
|
+
export declare function SnapLoadingOverlay({ appearance, accentHex, active, }: {
|
|
8
|
+
appearance: "light" | "dark";
|
|
9
|
+
accentHex: string;
|
|
10
|
+
active: boolean;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare function SnapViewCore({ snap, handlers, loading, appearance, loadingOverlay, }: {
|
|
7
13
|
snap: SnapPage;
|
|
8
14
|
handlers: SnapActionHandlers;
|
|
9
15
|
loading?: boolean;
|
|
10
16
|
appearance?: "light" | "dark";
|
|
17
|
+
/**
|
|
18
|
+
* Custom content rendered while `loading` is true. When `undefined` (default)
|
|
19
|
+
* the built-in spinner + backdrop is used. Pass `null` to render nothing.
|
|
20
|
+
*/
|
|
21
|
+
loadingOverlay?: ReactNode;
|
|
11
22
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { snapJsonRenderCatalog } from "../ui/index.js";
|
|
4
4
|
import { SnapCatalogView } from "./catalog-renderer.js";
|
|
5
5
|
import { SnapPreviewAccentProvider } from "./accent-context.js";
|
|
@@ -75,7 +75,7 @@ function ConfettiOverlay() {
|
|
|
75
75
|
animation: `confettiFall ${duration}s ease-in ${delay}s forwards`,
|
|
76
76
|
} }, id))), _jsx("style", { children: `@keyframes confettiFall{0%{top:-20px;opacity:1;transform:rotate(0deg) translateX(0)}50%{opacity:1}100%{top:110%;opacity:0;transform:rotate(720deg) translateX(${Math.random() > 0.5 ? "" : "-"}40px)}}` })] }));
|
|
77
77
|
}
|
|
78
|
-
function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
78
|
+
export function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
79
79
|
const isDark = appearance === "dark";
|
|
80
80
|
const tint = isDark ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.2)";
|
|
81
81
|
const trackColor = isDark
|
|
@@ -130,7 +130,7 @@ const PALETTE = [
|
|
|
130
130
|
];
|
|
131
131
|
// ─── SnapViewCore ────────────────────────────────────
|
|
132
132
|
// Shared rendering logic used by both v1 and v2.
|
|
133
|
-
export function SnapViewCore({ snap, handlers, loading = false, appearance = "dark", }) {
|
|
133
|
+
export function SnapViewCore({ snap, handlers, loading = false, appearance = "dark", loadingOverlay, }) {
|
|
134
134
|
const spec = snap.ui;
|
|
135
135
|
const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
|
|
136
136
|
const stateRef = useRef(initialState);
|
|
@@ -155,7 +155,12 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
155
155
|
useEffect(() => {
|
|
156
156
|
setPageKey((k) => k + 1);
|
|
157
157
|
}, [spec]);
|
|
158
|
-
const showConfetti = snap.effects?.includes("confetti");
|
|
158
|
+
const showConfetti = snap.effects?.includes("confetti") ?? false;
|
|
159
|
+
const [confettiKey, setConfettiKey] = useState(0);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (showConfetti)
|
|
162
|
+
setConfettiKey((k) => k + 1);
|
|
163
|
+
}, [showConfetti, snap]);
|
|
159
164
|
const accentName = snap.theme?.accent ?? "purple";
|
|
160
165
|
const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
|
|
161
166
|
const previewSurfaceStyle = useMemo(() => {
|
|
@@ -221,7 +226,7 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
221
226
|
break;
|
|
222
227
|
}
|
|
223
228
|
}, [handlers]);
|
|
224
|
-
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}), _jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading }), _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
229
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
225
230
|
applyStatePaths(stateRef.current, changes);
|
|
226
231
|
}, onAction: handleAction }, pageKey) }) })] }));
|
|
227
232
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import type { SnapPage, SnapActionHandlers } from "../index.js";
|
|
2
|
-
export declare function SnapViewV1({ snap, handlers, loading, appearance, }: {
|
|
3
|
+
export declare function SnapViewV1({ snap, handlers, loading, appearance, loadingOverlay, }: {
|
|
3
4
|
snap: SnapPage;
|
|
4
5
|
handlers: SnapActionHandlers;
|
|
5
6
|
loading?: boolean;
|
|
6
7
|
appearance?: "light" | "dark";
|
|
8
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
9
|
+
loadingOverlay?: ReactNode;
|
|
7
10
|
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, actionError, plain, }: {
|
|
11
|
+
export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, actionError, plain, loadingOverlay, }: {
|
|
9
12
|
snap: SnapPage;
|
|
10
13
|
handlers: SnapActionHandlers;
|
|
11
14
|
loading?: boolean;
|
|
@@ -13,4 +16,6 @@ export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWid
|
|
|
13
16
|
maxWidth?: number;
|
|
14
17
|
actionError?: string | null;
|
|
15
18
|
plain?: boolean;
|
|
19
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
20
|
+
loadingOverlay?: ReactNode;
|
|
16
21
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { SnapViewCore } from "../snap-view-core.js";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core.js";
|
|
5
|
+
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
|
|
5
6
|
const SNAP_MAX_HEIGHT = 500;
|
|
6
|
-
export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", }) {
|
|
7
|
-
return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
|
|
7
|
+
export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", loadingOverlay, }) {
|
|
8
|
+
return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: loadingOverlay }));
|
|
8
9
|
}
|
|
9
|
-
export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, actionError, plain = false, }) {
|
|
10
|
+
export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, actionError, plain = false, loadingOverlay, }) {
|
|
10
11
|
const isDark = appearance === "dark";
|
|
11
12
|
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
12
13
|
const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
|
|
@@ -43,44 +44,51 @@ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark
|
|
|
43
44
|
}
|
|
44
45
|
}, [isExpandable]);
|
|
45
46
|
const isClipped = isExpandable && !isExpanded;
|
|
47
|
+
const accentHex = useMemo(() => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance), [snap.theme?.accent, appearance]);
|
|
46
48
|
return (_jsxs("div", { style: {
|
|
47
49
|
position: "relative",
|
|
48
50
|
width: "100%",
|
|
49
51
|
maxWidth,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
52
|
+
}, children: [_jsxs("div", { style: {
|
|
53
|
+
position: "relative",
|
|
54
|
+
overflow: "hidden",
|
|
55
|
+
...(plain ? {} : {
|
|
56
|
+
borderRadius: 16,
|
|
57
|
+
border: `1px solid ${borderColor}`,
|
|
58
|
+
backgroundColor: surfaceBg,
|
|
59
|
+
}),
|
|
60
|
+
}, children: [_jsx("div", { style: isClipped
|
|
61
|
+
? {
|
|
62
|
+
maxHeight: SNAP_MAX_HEIGHT,
|
|
63
|
+
overflow: "hidden",
|
|
64
|
+
}
|
|
65
|
+
: undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: null }) }) }), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null] }), isExpandable ? (_jsx("button", { type: "button", "aria-expanded": isExpanded, onClick: () => setIsExpanded((value) => !value), style: {
|
|
66
|
+
position: "absolute",
|
|
67
|
+
bottom: 0,
|
|
68
|
+
left: "50%",
|
|
69
|
+
transform: "translate(-50%, 50%)",
|
|
70
|
+
appearance: "none",
|
|
71
|
+
border: `1px solid ${borderColor}`,
|
|
72
|
+
borderRadius: 9999,
|
|
73
|
+
backgroundColor: isDark ? "rgba(30,30,30,0.6)" : "rgba(255,255,255,0.6)",
|
|
74
|
+
backdropFilter: "blur(12px) saturate(180%)",
|
|
75
|
+
WebkitBackdropFilter: "blur(12px) saturate(180%)",
|
|
76
|
+
color: toggleText,
|
|
77
|
+
padding: "2px 10px",
|
|
78
|
+
fontSize: 12,
|
|
79
|
+
lineHeight: "16px",
|
|
80
|
+
fontWeight: 600,
|
|
81
|
+
cursor: "pointer",
|
|
82
|
+
zIndex: 11,
|
|
83
|
+
}, onMouseEnter: (event) => {
|
|
84
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
85
|
+
? "rgba(50,50,50,0.7)"
|
|
86
|
+
: "rgba(245,245,245,0.75)";
|
|
87
|
+
}, onMouseLeave: (event) => {
|
|
88
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
89
|
+
? "rgba(30,30,30,0.6)"
|
|
90
|
+
: "rgba(255,255,255,0.6)";
|
|
91
|
+
}, children: isExpanded ? "Show less" : "Show more" })) : null, actionError && (_jsx("div", { style: {
|
|
84
92
|
padding: "8px 12px",
|
|
85
93
|
fontSize: 13,
|
|
86
94
|
color: appearance === "dark"
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { ValidationResult } from "../../validator.js";
|
|
3
3
|
import type { SnapPage, SnapActionHandlers } from "../index.js";
|
|
4
|
-
export declare function SnapViewV2({ snap, handlers, loading, appearance, onValidationError, validationErrorFallback, }: {
|
|
4
|
+
export declare function SnapViewV2({ snap, handlers, loading, appearance, onValidationError, validationErrorFallback, loadingOverlay, }: {
|
|
5
5
|
snap: SnapPage;
|
|
6
6
|
handlers: SnapActionHandlers;
|
|
7
7
|
loading?: boolean;
|
|
8
8
|
appearance?: "light" | "dark";
|
|
9
9
|
onValidationError?: (result: ValidationResult) => void;
|
|
10
10
|
validationErrorFallback?: ReactNode;
|
|
11
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
12
|
+
loadingOverlay?: ReactNode;
|
|
11
13
|
}): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
-
export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
|
|
14
|
+
export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
|
|
13
15
|
snap: SnapPage;
|
|
14
16
|
handlers: SnapActionHandlers;
|
|
15
17
|
loading?: boolean;
|
|
@@ -20,4 +22,6 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWid
|
|
|
20
22
|
validationErrorFallback?: ReactNode;
|
|
21
23
|
actionError?: string | null;
|
|
22
24
|
plain?: boolean;
|
|
25
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
26
|
+
loadingOverlay?: ReactNode;
|
|
23
27
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { validateSnapResponse } from "../../validator.js";
|
|
5
|
-
import { SnapViewCore } from "../snap-view-core.js";
|
|
5
|
+
import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core.js";
|
|
6
|
+
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
|
|
6
7
|
const SNAP_MAX_HEIGHT = 500;
|
|
7
8
|
const SNAP_WARNING_HEIGHT = 700;
|
|
8
9
|
// ─── Default validation error fallback ────────────────
|
|
@@ -19,7 +20,7 @@ function SnapValidationFallback({ appearance, message, }) {
|
|
|
19
20
|
}, children: _jsx("span", { children: message ? `Unable to render snap: ${message}` : "Unable to render snap" }) }));
|
|
20
21
|
}
|
|
21
22
|
// ─── SnapViewV2 ──────────────────────────────────────
|
|
22
|
-
export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", onValidationError, validationErrorFallback, }) {
|
|
23
|
+
export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", onValidationError, validationErrorFallback, loadingOverlay, }) {
|
|
23
24
|
const validation = useMemo(() => validateSnapResponse(snap), [snap]);
|
|
24
25
|
const valid = validation.valid;
|
|
25
26
|
const validationMessage = validation.issues[0]?.message;
|
|
@@ -39,48 +40,112 @@ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark
|
|
|
39
40
|
return null;
|
|
40
41
|
return _jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { appearance: appearance, message: validationMessage }) });
|
|
41
42
|
}
|
|
42
|
-
return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
|
|
43
|
+
return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: loadingOverlay }));
|
|
43
44
|
}
|
|
44
45
|
// ─── SnapCardV2 ──────────────────────────────────────
|
|
45
|
-
export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
|
|
46
|
-
const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
|
|
46
|
+
export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
|
|
47
47
|
const isDark = appearance === "dark";
|
|
48
48
|
const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
|
|
49
49
|
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
50
50
|
const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
|
|
51
|
+
const toggleBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
|
|
52
|
+
const toggleBgHover = isDark
|
|
53
|
+
? "rgba(255,255,255,0.1)"
|
|
54
|
+
: "rgba(0,0,0,0.08)";
|
|
55
|
+
const toggleText = isDark ? "rgba(255,255,255,0.82)" : "rgba(0,0,0,0.72)";
|
|
56
|
+
const accentHex = useMemo(() => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance), [snap.theme?.accent, appearance]);
|
|
57
|
+
const contentRef = useRef(null);
|
|
58
|
+
const [isExpandable, setIsExpandable] = useState(false);
|
|
59
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setIsExpanded(false);
|
|
62
|
+
}, [snap]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const node = contentRef.current;
|
|
65
|
+
if (!node)
|
|
66
|
+
return;
|
|
67
|
+
const measure = () => {
|
|
68
|
+
setIsExpandable(node.scrollHeight > SNAP_MAX_HEIGHT + 1);
|
|
69
|
+
};
|
|
70
|
+
measure();
|
|
71
|
+
if (typeof ResizeObserver === "undefined")
|
|
72
|
+
return;
|
|
73
|
+
const observer = new ResizeObserver(() => {
|
|
74
|
+
measure();
|
|
75
|
+
});
|
|
76
|
+
observer.observe(node);
|
|
77
|
+
return () => observer.disconnect();
|
|
78
|
+
}, [snap, plain, showOverflowWarning]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!isExpandable) {
|
|
81
|
+
setIsExpanded(false);
|
|
82
|
+
}
|
|
83
|
+
}, [isExpandable]);
|
|
84
|
+
const isClipped = !showOverflowWarning && isExpandable && !isExpanded;
|
|
85
|
+
const containerMaxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : undefined;
|
|
51
86
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
|
|
52
87
|
position: "relative",
|
|
53
88
|
width: "100%",
|
|
54
89
|
maxWidth,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
}, children: [_jsxs("div", { style: {
|
|
91
|
+
position: "relative",
|
|
92
|
+
maxHeight: containerMaxHeight,
|
|
93
|
+
overflow: "hidden",
|
|
94
|
+
...(plain ? {} : {
|
|
95
|
+
borderRadius: 16,
|
|
96
|
+
border: `1px solid ${borderColor}`,
|
|
97
|
+
backgroundColor: surfaceBg,
|
|
98
|
+
}),
|
|
99
|
+
}, children: [_jsx("div", { style: isClipped
|
|
100
|
+
? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" }
|
|
101
|
+
: undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null }) }) }), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, showOverflowWarning && (_jsxs("div", { style: {
|
|
102
|
+
position: "absolute",
|
|
103
|
+
top: SNAP_MAX_HEIGHT,
|
|
104
|
+
left: 0,
|
|
105
|
+
right: 0,
|
|
106
|
+
bottom: 0,
|
|
107
|
+
pointerEvents: "none",
|
|
108
|
+
zIndex: 10,
|
|
109
|
+
}, children: [_jsx("div", { style: { borderTop: "1px dashed rgba(255,100,100,0.6)", position: "relative" }, children: _jsxs("span", { style: {
|
|
110
|
+
position: "absolute",
|
|
111
|
+
top: -10,
|
|
112
|
+
right: 0,
|
|
113
|
+
fontSize: 10,
|
|
114
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
115
|
+
color: "rgba(255,100,100,0.7)",
|
|
116
|
+
background: bg,
|
|
117
|
+
padding: "1px 4px",
|
|
118
|
+
borderRadius: 3,
|
|
119
|
+
}, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx("div", { style: {
|
|
120
|
+
height: "100%",
|
|
121
|
+
background: "repeating-linear-gradient(-45deg, transparent, transparent 8px, rgba(255,100,100,0.06) 8px, rgba(255,100,100,0.06) 16px)",
|
|
122
|
+
} })] }))] }), !showOverflowWarning && isExpandable ? (_jsx("button", { type: "button", "aria-expanded": isExpanded, onClick: () => setIsExpanded((value) => !value), style: {
|
|
63
123
|
position: "absolute",
|
|
64
|
-
top: SNAP_MAX_HEIGHT,
|
|
65
|
-
left: 0,
|
|
66
|
-
right: 0,
|
|
67
124
|
bottom: 0,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
125
|
+
left: "50%",
|
|
126
|
+
transform: "translate(-50%, 50%)",
|
|
127
|
+
appearance: "none",
|
|
128
|
+
border: `1px solid ${borderColor}`,
|
|
129
|
+
borderRadius: 9999,
|
|
130
|
+
backgroundColor: isDark ? "rgba(30,30,30,0.6)" : "rgba(255,255,255,0.6)",
|
|
131
|
+
backdropFilter: "blur(12px) saturate(180%)",
|
|
132
|
+
WebkitBackdropFilter: "blur(12px) saturate(180%)",
|
|
133
|
+
color: toggleText,
|
|
134
|
+
padding: "2px 10px",
|
|
135
|
+
fontSize: 12,
|
|
136
|
+
lineHeight: "16px",
|
|
137
|
+
fontWeight: 600,
|
|
138
|
+
cursor: "pointer",
|
|
139
|
+
zIndex: 11,
|
|
140
|
+
}, onMouseEnter: (event) => {
|
|
141
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
142
|
+
? "rgba(50,50,50,0.7)"
|
|
143
|
+
: "rgba(245,245,245,0.75)";
|
|
144
|
+
}, onMouseLeave: (event) => {
|
|
145
|
+
event.currentTarget.style.backgroundColor = isDark
|
|
146
|
+
? "rgba(30,30,30,0.6)"
|
|
147
|
+
: "rgba(255,255,255,0.6)";
|
|
148
|
+
}, children: isExpanded ? "Show less" : "Show more" })) : null] }), actionError && (_jsx("div", { style: {
|
|
84
149
|
maxWidth,
|
|
85
150
|
padding: "8px 12px",
|
|
86
151
|
fontSize: 13,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
|
-
export declare function SnapCellGrid({ element
|
|
2
|
+
export declare function SnapCellGrid({ element, emit, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
|
|
@@ -4,7 +4,9 @@ import { useStateStore } from "@json-render/react-native";
|
|
|
4
4
|
import { useSnapPalette } from "../use-snap-palette.js";
|
|
5
5
|
import { useSnapTheme } from "../theme.js";
|
|
6
6
|
import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
7
|
-
export function SnapCellGrid({ element
|
|
7
|
+
export function SnapCellGrid({ element, emit, }) {
|
|
8
|
+
const { props } = element;
|
|
9
|
+
const on = element.on;
|
|
8
10
|
const { hex, appearance } = useSnapPalette();
|
|
9
11
|
const { colors } = useSnapTheme();
|
|
10
12
|
const { get, set } = useStateStore();
|
|
@@ -16,8 +18,10 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
16
18
|
const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
17
19
|
const gapPx = gapMap[gap] ?? 1;
|
|
18
20
|
const select = String(props.select ?? "off");
|
|
19
|
-
const interactive = select !== "off";
|
|
20
21
|
const isMultiple = select === "multiple";
|
|
22
|
+
const isSelectable = select !== "off";
|
|
23
|
+
const hasPressAction = Boolean(on?.press);
|
|
24
|
+
const interactive = isSelectable || hasPressAction;
|
|
21
25
|
const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
|
|
22
26
|
const tapPath = `/inputs/${name}`;
|
|
23
27
|
const tapRaw = get(tapPath);
|
|
@@ -28,7 +32,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
28
32
|
selectedSet.add(part);
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
|
-
const isSelected = (r, c) => selectedSet.has(`${r},${c}`);
|
|
35
|
+
const isSelected = (r, c) => isSelectable && selectedSet.has(`${r},${c}`);
|
|
32
36
|
const handleTap = (r, c) => {
|
|
33
37
|
const key = `${r},${c}`;
|
|
34
38
|
if (isMultiple) {
|
|
@@ -42,6 +46,8 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
42
46
|
else {
|
|
43
47
|
set(tapPath, key);
|
|
44
48
|
}
|
|
49
|
+
if (hasPressAction)
|
|
50
|
+
emit("press");
|
|
45
51
|
};
|
|
46
52
|
const cellMap = new Map();
|
|
47
53
|
for (const c of cells) {
|
|
@@ -69,7 +75,7 @@ export function SnapCellGrid({ element: { props }, }) {
|
|
|
69
75
|
}
|
|
70
76
|
rowEls.push(_jsx(View, { style: [styles.gridRow, { gap: gapPx }], children: rowCells }, r));
|
|
71
77
|
}
|
|
72
|
-
const selectionLabel =
|
|
78
|
+
const selectionLabel = isSelectable && selectedSet.size > 0
|
|
73
79
|
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
74
80
|
: null;
|
|
75
81
|
return (_jsxs(View, { style: [styles.wrap, { gap: gapPx, backgroundColor: colors.muted, padding: 4, borderRadius: 8 }], children: [rowEls, selectionLabel ? (_jsx(Text, { style: [styles.selectionText, { color: colors.textSecondary }], children: selectionLabel })) : null] }));
|