@farcaster/snap 2.0.2 → 2.0.3
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/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 +4 -4
- package/dist/react/v1/snap-view.d.ts +7 -2
- package/dist/react/v1/snap-view.js +9 -7
- package/dist/react/v2/snap-view.d.ts +6 -2
- package/dist/react/v2/snap-view.js +7 -5
- 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 +17 -8
- package/dist/react-native/v1/snap-view.d.ts +9 -3
- package/dist/react-native/v1/snap-view.js +15 -10
- package/dist/react-native/v2/snap-view.d.ts +8 -3
- package/dist/react-native/v2/snap-view.js +21 -12
- package/dist/ui/catalog.js +1 -1
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/src/react/index.tsx +5 -0
- package/src/react/snap-view-core.tsx +17 -6
- package/src/react/v1/snap-view.tsx +25 -2
- package/src/react/v2/snap-view.tsx +23 -1
- package/src/react-native/index.tsx +5 -0
- package/src/react-native/snap-view-core.tsx +48 -14
- package/src/react-native/v1/snap-view.tsx +37 -5
- package/src/react-native/v2/snap-view.tsx +41 -4
- package/src/ui/catalog.ts +1 -1
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);
|
|
@@ -221,7 +221,7 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
221
221
|
break;
|
|
222
222
|
}
|
|
223
223
|
}, [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) => {
|
|
224
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}), 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
225
|
applyStatePaths(stateRef.current, changes);
|
|
226
226
|
}, onAction: handleAction }, pageKey) }) })] }));
|
|
227
227
|
}
|
|
@@ -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,6 +44,7 @@ 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%",
|
|
@@ -58,7 +60,7 @@ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark
|
|
|
58
60
|
maxHeight: SNAP_MAX_HEIGHT,
|
|
59
61
|
overflow: "hidden",
|
|
60
62
|
}
|
|
61
|
-
: undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }) }) }), isExpandable ? (_jsx("div", { style: {
|
|
63
|
+
: 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("div", { style: {
|
|
62
64
|
display: "flex",
|
|
63
65
|
justifyContent: "center",
|
|
64
66
|
padding: plain ? "8px 0 0" : "10px 16px 12px",
|
|
@@ -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;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo } 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,15 +40,16 @@ 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
|
+
export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
|
|
46
47
|
const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
|
|
47
48
|
const isDark = appearance === "dark";
|
|
48
49
|
const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
|
|
49
50
|
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
50
51
|
const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
|
|
52
|
+
const accentHex = useMemo(() => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance), [snap.theme?.accent, appearance]);
|
|
51
53
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
|
|
52
54
|
position: "relative",
|
|
53
55
|
width: "100%",
|
|
@@ -59,7 +61,7 @@ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark
|
|
|
59
61
|
border: `1px solid ${borderColor}`,
|
|
60
62
|
backgroundColor: surfaceBg,
|
|
61
63
|
}),
|
|
62
|
-
}, children: [_jsx("div", { style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }), showOverflowWarning && (_jsxs("div", { style: {
|
|
64
|
+
}, children: [_jsx("div", { 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: {
|
|
63
65
|
position: "absolute",
|
|
64
66
|
top: SNAP_MAX_HEIGHT,
|
|
65
67
|
left: 0,
|
|
@@ -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, }: {
|
|
10
|
+
export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
|
|
11
11
|
snap: SnapPage;
|
|
12
12
|
handlers: SnapActionHandlers;
|
|
13
13
|
loading?: boolean;
|
|
@@ -25,4 +25,6 @@ export declare function SnapCard({ snap, handlers, loading, appearance, colors,
|
|
|
25
25
|
actionError?: string | null;
|
|
26
26
|
/** When true, renders without card frame (no border, background, or padding). */
|
|
27
27
|
plain?: boolean;
|
|
28
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
29
|
+
loadingOverlay?: ReactNode;
|
|
28
30
|
}): 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, }) {
|
|
10
|
+
export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
|
|
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 }));
|
|
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 }));
|
|
13
13
|
}
|
|
14
|
-
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain }));
|
|
14
|
+
return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay }));
|
|
15
15
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import type { SnapPage, SnapActionHandlers } from "./types.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
7
|
export declare function resolveAccentHex(accent: string | undefined, appearance: "light" | "dark"): string;
|
|
7
|
-
export declare function SnapViewCoreInner({ snap, handlers, loading, }: {
|
|
8
|
+
export declare function SnapViewCoreInner({ snap, handlers, loading, loadingOverlay, }: {
|
|
8
9
|
snap: SnapPage;
|
|
9
10
|
handlers: SnapActionHandlers;
|
|
10
11
|
loading?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Custom content rendered while `loading` is true. When `undefined` (default)
|
|
14
|
+
* the built-in ActivityIndicator overlay is used. Pass `null` to render nothing.
|
|
15
|
+
*/
|
|
16
|
+
loadingOverlay?: ReactNode;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
export declare function SnapLoadingOverlay({ appearance, accentHex, }: {
|
|
19
|
+
appearance: "light" | "dark";
|
|
20
|
+
accentHex: string;
|
|
11
21
|
}): import("react").JSX.Element;
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
3
|
import { SnapCatalogView } from "./catalog-renderer.js";
|
|
4
4
|
import { useSnapTheme } from "./theme.js";
|
|
5
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
6
6
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
7
7
|
import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
|
|
8
8
|
// ─── Shared helpers ──────────────────────────────────
|
|
@@ -45,7 +45,7 @@ export function resolveAccentHex(accent, appearance) {
|
|
|
45
45
|
return map[name];
|
|
46
46
|
}
|
|
47
47
|
// ─── Core rendering component (no validation) ────────
|
|
48
|
-
export function SnapViewCoreInner({ snap, handlers, loading = false, }) {
|
|
48
|
+
export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOverlay, }) {
|
|
49
49
|
const { mode } = useSnapTheme();
|
|
50
50
|
const spec = snap.ui;
|
|
51
51
|
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
@@ -134,15 +134,24 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, }) {
|
|
|
134
134
|
break;
|
|
135
135
|
}
|
|
136
136
|
}, []);
|
|
137
|
-
return (_jsxs(View, { style: styles.container, children: [loading
|
|
138
|
-
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
], children: _jsx(ActivityIndicator, { size: "large", color: accentHex }) })) : null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
137
|
+
return (_jsxs(View, { style: styles.container, children: [loading
|
|
138
|
+
? loadingOverlay === undefined
|
|
139
|
+
? (_jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex }))
|
|
140
|
+
: loadingOverlay
|
|
141
|
+
: null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
|
|
143
142
|
applyStatePaths(stateRef.current, changes);
|
|
144
143
|
}, onAction: handleAction }, pageKey)] }));
|
|
145
144
|
}
|
|
145
|
+
export function SnapLoadingOverlay({ appearance, accentHex, }) {
|
|
146
|
+
return (_jsx(View, { style: [
|
|
147
|
+
styles.overlay,
|
|
148
|
+
{
|
|
149
|
+
backgroundColor: appearance === "dark"
|
|
150
|
+
? "rgba(0,0,0,0.1)"
|
|
151
|
+
: "rgba(255,255,255,0.2)",
|
|
152
|
+
},
|
|
153
|
+
], children: _jsx(ActivityIndicator, { size: "large", color: accentHex }) }));
|
|
154
|
+
}
|
|
146
155
|
const styles = StyleSheet.create({
|
|
147
156
|
container: {
|
|
148
157
|
width: "100%",
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import { type SnapNativeColors } from "../theme.js";
|
|
2
3
|
import type { SnapPage, SnapActionHandlers } from "../types.js";
|
|
3
|
-
export declare function SnapViewV1Inner({ snap, handlers, loading, }: {
|
|
4
|
+
export declare function SnapViewV1Inner({ snap, handlers, loading, loadingOverlay, }: {
|
|
4
5
|
snap: SnapPage;
|
|
5
6
|
handlers: SnapActionHandlers;
|
|
6
7
|
loading?: boolean;
|
|
8
|
+
loadingOverlay?: ReactNode;
|
|
7
9
|
}): import("react").JSX.Element;
|
|
8
|
-
export declare function SnapViewV1({ snap, handlers, loading, appearance, colors, }: {
|
|
10
|
+
export declare function SnapViewV1({ snap, handlers, loading, appearance, colors, loadingOverlay, }: {
|
|
9
11
|
snap: SnapPage;
|
|
10
12
|
handlers: SnapActionHandlers;
|
|
11
13
|
loading?: boolean;
|
|
12
14
|
appearance?: "light" | "dark";
|
|
13
15
|
colors?: Partial<SnapNativeColors>;
|
|
16
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
17
|
+
loadingOverlay?: ReactNode;
|
|
14
18
|
}): import("react").JSX.Element;
|
|
15
|
-
export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, }: {
|
|
19
|
+
export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, loadingOverlay, }: {
|
|
16
20
|
snap: SnapPage;
|
|
17
21
|
handlers: SnapActionHandlers;
|
|
18
22
|
loading?: boolean;
|
|
@@ -21,4 +25,6 @@ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors
|
|
|
21
25
|
borderRadius?: number;
|
|
22
26
|
actionError?: string | null;
|
|
23
27
|
plain?: boolean;
|
|
28
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
29
|
+
loadingOverlay?: ReactNode;
|
|
24
30
|
}): import("react").JSX.Element;
|
|
@@ -2,18 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { View, Text, StyleSheet, Pressable } from "react-native";
|
|
4
4
|
import { SnapThemeProvider, useSnapTheme } from "../theme.js";
|
|
5
|
-
import { SnapViewCoreInner } from "../snap-view-core.js";
|
|
5
|
+
import { SnapLoadingOverlay, SnapViewCoreInner, resolveAccentHex, } from "../snap-view-core.js";
|
|
6
6
|
const SNAP_MAX_HEIGHT = 500;
|
|
7
7
|
// ─── SnapViewV1 (no validation) ──────────────────────
|
|
8
|
-
export function SnapViewV1Inner({ snap, handlers, loading = false, }) {
|
|
9
|
-
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading }));
|
|
8
|
+
export function SnapViewV1Inner({ snap, handlers, loading = false, loadingOverlay, }) {
|
|
9
|
+
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }));
|
|
10
10
|
}
|
|
11
|
-
export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", colors, }) {
|
|
12
|
-
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }));
|
|
11
|
+
export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", colors, loadingOverlay, }) {
|
|
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, }) {
|
|
16
|
-
const { colors } = useSnapTheme();
|
|
15
|
+
function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, plain, loadingOverlay, }) {
|
|
16
|
+
const { colors, mode } = useSnapTheme();
|
|
17
|
+
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
17
18
|
const [contentHeight, setContentHeight] = useState(0);
|
|
18
19
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
19
20
|
useEffect(() => {
|
|
@@ -36,7 +37,11 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
36
37
|
: currentHeight === nextHeight
|
|
37
38
|
? currentHeight
|
|
38
39
|
: nextHeight);
|
|
39
|
-
}, style: plain ? undefined : cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }) }),
|
|
40
|
+
}, style: plain ? undefined : cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: null }) }) }), loading
|
|
41
|
+
? loadingOverlay === undefined
|
|
42
|
+
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
43
|
+
: loadingOverlay
|
|
44
|
+
: null, isExpandable ? (_jsx(View, { style: [
|
|
40
45
|
cardStyles.expandRow,
|
|
41
46
|
plain
|
|
42
47
|
? cardStyles.expandRowPlain
|
|
@@ -59,8 +64,8 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
|
|
|
59
64
|
},
|
|
60
65
|
], children: actionError }))] }));
|
|
61
66
|
}
|
|
62
|
-
export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, plain = false, }) {
|
|
63
|
-
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV1Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, actionError: actionError, appearance: appearance, plain: plain }) }));
|
|
67
|
+
export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, plain = false, loadingOverlay, }) {
|
|
68
|
+
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 }) }));
|
|
64
69
|
}
|
|
65
70
|
const cardStyles = StyleSheet.create({
|
|
66
71
|
frameRing: { alignSelf: "stretch" },
|
|
@@ -2,14 +2,15 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import { type SnapNativeColors } from "../theme.js";
|
|
3
3
|
import { type ValidationResult } from "@farcaster/snap";
|
|
4
4
|
import type { SnapPage, SnapActionHandlers } from "../types.js";
|
|
5
|
-
export declare function SnapViewV2Inner({ snap, handlers, loading, onValidationError, validationErrorFallback, }: {
|
|
5
|
+
export declare function SnapViewV2Inner({ snap, handlers, loading, onValidationError, validationErrorFallback, loadingOverlay, }: {
|
|
6
6
|
snap: SnapPage;
|
|
7
7
|
handlers: SnapActionHandlers;
|
|
8
8
|
loading?: boolean;
|
|
9
9
|
onValidationError?: (result: ValidationResult) => void;
|
|
10
10
|
validationErrorFallback?: ReactNode;
|
|
11
|
+
loadingOverlay?: ReactNode;
|
|
11
12
|
}): import("react").JSX.Element;
|
|
12
|
-
export declare function SnapViewV2({ snap, handlers, loading, appearance, colors, onValidationError, validationErrorFallback, }: {
|
|
13
|
+
export declare function SnapViewV2({ snap, handlers, loading, appearance, colors, onValidationError, validationErrorFallback, loadingOverlay, }: {
|
|
13
14
|
snap: SnapPage;
|
|
14
15
|
handlers: SnapActionHandlers;
|
|
15
16
|
loading?: boolean;
|
|
@@ -17,8 +18,10 @@ export declare function SnapViewV2({ snap, handlers, loading, appearance, colors
|
|
|
17
18
|
colors?: Partial<SnapNativeColors>;
|
|
18
19
|
onValidationError?: (result: ValidationResult) => void;
|
|
19
20
|
validationErrorFallback?: ReactNode;
|
|
21
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
22
|
+
loadingOverlay?: ReactNode;
|
|
20
23
|
}): import("react").JSX.Element;
|
|
21
|
-
export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
|
|
24
|
+
export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
|
|
22
25
|
snap: SnapPage;
|
|
23
26
|
handlers: SnapActionHandlers;
|
|
24
27
|
loading?: boolean;
|
|
@@ -30,4 +33,6 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors
|
|
|
30
33
|
validationErrorFallback?: ReactNode;
|
|
31
34
|
actionError?: string | null;
|
|
32
35
|
plain?: boolean;
|
|
36
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
37
|
+
loadingOverlay?: ReactNode;
|
|
33
38
|
}): import("react").JSX.Element;
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { SnapThemeProvider, useSnapTheme } from "../theme.js";
|
|
5
|
-
import { SnapViewCoreInner } from "../snap-view-core.js";
|
|
5
|
+
import { SnapLoadingOverlay, SnapViewCoreInner, resolveAccentHex, } from "../snap-view-core.js";
|
|
6
6
|
import { validateSnapResponse, } from "@farcaster/snap";
|
|
7
7
|
// ─── Constants ───────────────────────────────────────
|
|
8
8
|
const SNAP_MAX_HEIGHT = 500;
|
|
@@ -24,7 +24,7 @@ const fallbackStyles = StyleSheet.create({
|
|
|
24
24
|
},
|
|
25
25
|
});
|
|
26
26
|
// ─── SnapViewV2 (with validation) ────────────────────
|
|
27
|
-
export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationError, validationErrorFallback, }) {
|
|
27
|
+
export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationError, validationErrorFallback, loadingOverlay, }) {
|
|
28
28
|
const validation = useMemo(() => validateSnapResponse(snap), [snap]);
|
|
29
29
|
const valid = validation.valid;
|
|
30
30
|
const validationMessage = validation.issues[0]?.message;
|
|
@@ -44,18 +44,23 @@ export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationE
|
|
|
44
44
|
return null;
|
|
45
45
|
return (_jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { message: validationMessage }) }));
|
|
46
46
|
}
|
|
47
|
-
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading }));
|
|
47
|
+
return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }));
|
|
48
48
|
}
|
|
49
|
-
export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", colors, onValidationError, validationErrorFallback, }) {
|
|
50
|
-
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }));
|
|
49
|
+
export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", colors, onValidationError, validationErrorFallback, loadingOverlay, }) {
|
|
50
|
+
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: loadingOverlay }) }));
|
|
51
51
|
}
|
|
52
52
|
// ─── SnapCardV2 (card frame + height limits) ─────────
|
|
53
|
-
function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, }) {
|
|
54
|
-
const { colors } = useSnapTheme();
|
|
53
|
+
function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, loadingOverlay, }) {
|
|
54
|
+
const { colors, mode } = useSnapTheme();
|
|
55
|
+
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
55
56
|
const [contentHeight, setContentHeight] = useState(0);
|
|
56
|
-
const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }));
|
|
57
|
+
const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null }));
|
|
57
58
|
if (plain) {
|
|
58
|
-
return content
|
|
59
|
+
return (_jsxs(_Fragment, { children: [content, loading
|
|
60
|
+
? loadingOverlay === undefined
|
|
61
|
+
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
62
|
+
: loadingOverlay
|
|
63
|
+
: null] }));
|
|
59
64
|
}
|
|
60
65
|
const overflowAmount = showOverflowWarning ? contentHeight - SNAP_MAX_HEIGHT : 0;
|
|
61
66
|
return (_jsxs(_Fragment, { children: [_jsxs(View, { style: {
|
|
@@ -66,7 +71,11 @@ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWa
|
|
|
66
71
|
maxHeight: showOverflowWarning ? undefined : SNAP_MAX_HEIGHT,
|
|
67
72
|
overflow: "hidden",
|
|
68
73
|
minHeight: 120,
|
|
69
|
-
}, children: [_jsx(View, { collapsable: false, onLayout: (e) => setContentHeight(Math.round(e.nativeEvent.layout.height)), style: { paddingHorizontal: 16, paddingVertical: 16 }, children: content }), showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT && (_jsxs(View, { style: { position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, height: overflowAmount, zIndex: 10, pointerEvents: "none" }, children: [_jsx(View, { style: { height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" } }), _jsx(View, { style: { position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }, children: _jsxs(Text, { style: { fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx(View, { style: { flex: 1, backgroundColor: "rgba(255,50,50,0.15)" } })] }))
|
|
74
|
+
}, children: [_jsx(View, { collapsable: false, onLayout: (e) => setContentHeight(Math.round(e.nativeEvent.layout.height)), style: { paddingHorizontal: 16, paddingVertical: 16 }, children: content }), showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT && (_jsxs(View, { style: { position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, height: overflowAmount, zIndex: 10, pointerEvents: "none" }, children: [_jsx(View, { style: { height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" } }), _jsx(View, { style: { position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }, children: _jsxs(Text, { style: { fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx(View, { style: { flex: 1, backgroundColor: "rgba(255,50,50,0.15)" } })] })), loading
|
|
75
|
+
? loadingOverlay === undefined
|
|
76
|
+
? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
|
|
77
|
+
: loadingOverlay
|
|
78
|
+
: null] }), actionError && (_jsx(Text, { style: {
|
|
70
79
|
paddingHorizontal: 12,
|
|
71
80
|
paddingVertical: 8,
|
|
72
81
|
fontSize: 13,
|
|
@@ -75,8 +84,8 @@ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWa
|
|
|
75
84
|
: "rgba(200,0,0,0.8)",
|
|
76
85
|
}, children: actionError }))] }));
|
|
77
86
|
}
|
|
78
|
-
export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
|
|
79
|
-
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, appearance: appearance, plain: plain }) }));
|
|
87
|
+
export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
|
|
88
|
+
return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, appearance: appearance, plain: plain, loadingOverlay: loadingOverlay }) }));
|
|
80
89
|
}
|
|
81
90
|
const cardStyles = StyleSheet.create({
|
|
82
91
|
frameRing: { alignSelf: "stretch" },
|
package/dist/ui/catalog.js
CHANGED
|
@@ -50,7 +50,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
50
50
|
},
|
|
51
51
|
item: {
|
|
52
52
|
props: itemProps,
|
|
53
|
-
description: "Content row with title and optional description. Children render in the actions slot (right side) —
|
|
53
|
+
description: "Content row with title and optional description. Children render in the actions slot (right side) — badge, button, and icon elements are all valid. The item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.",
|
|
54
54
|
},
|
|
55
55
|
item_group: {
|
|
56
56
|
props: itemGroupProps,
|
package/llms.txt
CHANGED
|
@@ -68,7 +68,7 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
|
|
|
68
68
|
- `title` (string, required, max 100)
|
|
69
69
|
- `description` (string, optional, max 160)
|
|
70
70
|
- `variant` (optional): `"default"`. Default: `"default"`
|
|
71
|
-
- Children render in the actions slot (right side)
|
|
71
|
+
- Children render in the actions slot (right side). Badges, buttons, and icons are all valid — but the item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.
|
|
72
72
|
|
|
73
73
|
**progress** — Horizontal progress bar.
|
|
74
74
|
- `value` (number, required, 0 to max)
|
|
@@ -165,7 +165,7 @@ Bound to buttons via `on.press`:
|
|
|
165
165
|
| `open_mini_app` | `target` (URL) | Open as Farcaster mini app |
|
|
166
166
|
| `view_cast` | `hash` (string) | Navigate to a cast |
|
|
167
167
|
| `view_profile` | `fid` (number) | Navigate to a profile |
|
|
168
|
-
| `compose_cast` | `text?`, `channelKey?`, `embeds?` | Open cast composer |
|
|
168
|
+
| `compose_cast` | `text?`, `channelKey?`, `embeds?` | Open cast composer. Put URLs in `embeds`, not `text` |
|
|
169
169
|
| `view_token` | `token` (CAIP-19) | View token in wallet |
|
|
170
170
|
| `send_token` | `token`, `amount?`, `recipientFid?`, `recipientAddress?` | Send token flow |
|
|
171
171
|
| `swap_token` | `sellToken?`, `buyToken?` | Swap token flow |
|
|
@@ -174,6 +174,8 @@ Bound to buttons via `on.press`:
|
|
|
174
174
|
|
|
175
175
|
`arrow-right`, `arrow-left`, `external-link`, `chevron-right`, `check`, `x`, `alert-triangle`, `info`, `clock`, `heart`, `message-circle`, `repeat`, `share`, `user`, `users`, `star`, `trophy`, `zap`, `flame`, `gift`, `image`, `play`, `pause`, `wallet`, `coins`, `plus`, `minus`, `refresh-cw`, `bookmark`, `thumbs-up`, `thumbs-down`, `trending-up`, `trending-down`
|
|
176
176
|
|
|
177
|
+
`chevron-right`, `arrow-right`, and `external-link` are navigation/disclosure affordances — only use them when the surrounding element actually navigates (e.g. a button bound to `open_url` or `open_snap`). Never place them inside an `item`'s actions slot; `item` is not interactive.
|
|
178
|
+
|
|
177
179
|
## Color Palette
|
|
178
180
|
|
|
179
181
|
`gray`, `blue`, `red`, `amber`, `green`, `teal`, `purple`, `pink`
|
package/package.json
CHANGED
package/src/react/index.tsx
CHANGED
|
@@ -59,6 +59,7 @@ export function SnapCard({
|
|
|
59
59
|
validationErrorFallback,
|
|
60
60
|
actionError,
|
|
61
61
|
plain = false,
|
|
62
|
+
loadingOverlay,
|
|
62
63
|
}: {
|
|
63
64
|
snap: SnapPage;
|
|
64
65
|
handlers: SnapActionHandlers;
|
|
@@ -73,6 +74,8 @@ export function SnapCard({
|
|
|
73
74
|
actionError?: string | null;
|
|
74
75
|
/** When true, renders without card frame (no border, background, or padding). */
|
|
75
76
|
plain?: boolean;
|
|
77
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
78
|
+
loadingOverlay?: ReactNode;
|
|
76
79
|
}) {
|
|
77
80
|
if (snap.version === SPEC_VERSION_2) {
|
|
78
81
|
return (
|
|
@@ -87,6 +90,7 @@ export function SnapCard({
|
|
|
87
90
|
validationErrorFallback={validationErrorFallback}
|
|
88
91
|
actionError={actionError}
|
|
89
92
|
plain={plain}
|
|
93
|
+
loadingOverlay={loadingOverlay}
|
|
90
94
|
/>
|
|
91
95
|
);
|
|
92
96
|
}
|
|
@@ -100,6 +104,7 @@ export function SnapCard({
|
|
|
100
104
|
maxWidth={maxWidth}
|
|
101
105
|
actionError={actionError}
|
|
102
106
|
plain={plain}
|
|
107
|
+
loadingOverlay={loadingOverlay}
|
|
103
108
|
/>
|
|
104
109
|
);
|
|
105
110
|
}
|
|
@@ -8,6 +8,7 @@ import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex";
|
|
|
8
8
|
import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css";
|
|
9
9
|
import {
|
|
10
10
|
type CSSProperties,
|
|
11
|
+
type ReactNode,
|
|
11
12
|
useCallback,
|
|
12
13
|
useEffect,
|
|
13
14
|
useMemo,
|
|
@@ -111,7 +112,7 @@ function ConfettiOverlay() {
|
|
|
111
112
|
);
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
function SnapLoadingOverlay({
|
|
115
|
+
export function SnapLoadingOverlay({
|
|
115
116
|
appearance,
|
|
116
117
|
accentHex,
|
|
117
118
|
active,
|
|
@@ -197,11 +198,17 @@ export function SnapViewCore({
|
|
|
197
198
|
handlers,
|
|
198
199
|
loading = false,
|
|
199
200
|
appearance = "dark",
|
|
201
|
+
loadingOverlay,
|
|
200
202
|
}: {
|
|
201
203
|
snap: SnapPage;
|
|
202
204
|
handlers: SnapActionHandlers;
|
|
203
205
|
loading?: boolean;
|
|
204
206
|
appearance?: "light" | "dark";
|
|
207
|
+
/**
|
|
208
|
+
* Custom content rendered while `loading` is true. When `undefined` (default)
|
|
209
|
+
* the built-in spinner + backdrop is used. Pass `null` to render nothing.
|
|
210
|
+
*/
|
|
211
|
+
loadingOverlay?: ReactNode;
|
|
205
212
|
}) {
|
|
206
213
|
const spec = snap.ui;
|
|
207
214
|
const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
|
|
@@ -315,11 +322,15 @@ export function SnapViewCore({
|
|
|
315
322
|
return (
|
|
316
323
|
<div style={{ position: "relative", width: "100%" }}>
|
|
317
324
|
{showConfetti && <ConfettiOverlay />}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
{loadingOverlay === undefined ? (
|
|
326
|
+
<SnapLoadingOverlay
|
|
327
|
+
appearance={appearance}
|
|
328
|
+
accentHex={accentHex}
|
|
329
|
+
active={loading}
|
|
330
|
+
/>
|
|
331
|
+
) : loading ? (
|
|
332
|
+
<>{loadingOverlay}</>
|
|
333
|
+
) : null}
|
|
323
334
|
|
|
324
335
|
<div style={previewSurfaceStyle}>
|
|
325
336
|
<SnapPreviewAccentProvider
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { SnapViewCore } from "../snap-view-core";
|
|
3
|
+
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
|
|
5
|
+
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
|
|
5
6
|
import type { SnapPage, SnapActionHandlers } from "../index";
|
|
6
7
|
|
|
7
8
|
const SNAP_MAX_HEIGHT = 500;
|
|
@@ -11,11 +12,14 @@ export function SnapViewV1({
|
|
|
11
12
|
handlers,
|
|
12
13
|
loading = false,
|
|
13
14
|
appearance = "dark",
|
|
15
|
+
loadingOverlay,
|
|
14
16
|
}: {
|
|
15
17
|
snap: SnapPage;
|
|
16
18
|
handlers: SnapActionHandlers;
|
|
17
19
|
loading?: boolean;
|
|
18
20
|
appearance?: "light" | "dark";
|
|
21
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
22
|
+
loadingOverlay?: ReactNode;
|
|
19
23
|
}) {
|
|
20
24
|
return (
|
|
21
25
|
<SnapViewCore
|
|
@@ -23,6 +27,7 @@ export function SnapViewV1({
|
|
|
23
27
|
handlers={handlers}
|
|
24
28
|
loading={loading}
|
|
25
29
|
appearance={appearance}
|
|
30
|
+
loadingOverlay={loadingOverlay}
|
|
26
31
|
/>
|
|
27
32
|
);
|
|
28
33
|
}
|
|
@@ -35,6 +40,7 @@ export function SnapCardV1({
|
|
|
35
40
|
maxWidth = 480,
|
|
36
41
|
actionError,
|
|
37
42
|
plain = false,
|
|
43
|
+
loadingOverlay,
|
|
38
44
|
}: {
|
|
39
45
|
snap: SnapPage;
|
|
40
46
|
handlers: SnapActionHandlers;
|
|
@@ -43,6 +49,8 @@ export function SnapCardV1({
|
|
|
43
49
|
maxWidth?: number;
|
|
44
50
|
actionError?: string | null;
|
|
45
51
|
plain?: boolean;
|
|
52
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
53
|
+
loadingOverlay?: ReactNode;
|
|
46
54
|
}) {
|
|
47
55
|
const isDark = appearance === "dark";
|
|
48
56
|
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
@@ -86,6 +94,11 @@ export function SnapCardV1({
|
|
|
86
94
|
|
|
87
95
|
const isClipped = isExpandable && !isExpanded;
|
|
88
96
|
|
|
97
|
+
const accentHex = useMemo(
|
|
98
|
+
() => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance),
|
|
99
|
+
[snap.theme?.accent, appearance],
|
|
100
|
+
);
|
|
101
|
+
|
|
89
102
|
return (
|
|
90
103
|
<div
|
|
91
104
|
style={{
|
|
@@ -116,9 +129,19 @@ export function SnapCardV1({
|
|
|
116
129
|
handlers={handlers}
|
|
117
130
|
loading={loading}
|
|
118
131
|
appearance={appearance}
|
|
132
|
+
loadingOverlay={null}
|
|
119
133
|
/>
|
|
120
134
|
</div>
|
|
121
135
|
</div>
|
|
136
|
+
{loadingOverlay === undefined ? (
|
|
137
|
+
<SnapLoadingOverlay
|
|
138
|
+
appearance={appearance}
|
|
139
|
+
accentHex={accentHex}
|
|
140
|
+
active={loading}
|
|
141
|
+
/>
|
|
142
|
+
) : loading ? (
|
|
143
|
+
<>{loadingOverlay}</>
|
|
144
|
+
) : null}
|
|
122
145
|
{isExpandable ? (
|
|
123
146
|
<div
|
|
124
147
|
style={{
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { type ReactNode, useEffect, useMemo } from "react";
|
|
4
4
|
import { validateSnapResponse } from "../../validator.js";
|
|
5
5
|
import type { ValidationResult } from "../../validator.js";
|
|
6
|
-
import { SnapViewCore } from "../snap-view-core";
|
|
6
|
+
import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
|
|
7
|
+
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
|
|
7
8
|
import type { SnapPage, SnapActionHandlers } from "../index";
|
|
8
9
|
|
|
9
10
|
const SNAP_MAX_HEIGHT = 500;
|
|
@@ -45,6 +46,7 @@ export function SnapViewV2({
|
|
|
45
46
|
appearance = "dark",
|
|
46
47
|
onValidationError,
|
|
47
48
|
validationErrorFallback,
|
|
49
|
+
loadingOverlay,
|
|
48
50
|
}: {
|
|
49
51
|
snap: SnapPage;
|
|
50
52
|
handlers: SnapActionHandlers;
|
|
@@ -52,6 +54,8 @@ export function SnapViewV2({
|
|
|
52
54
|
appearance?: "light" | "dark";
|
|
53
55
|
onValidationError?: (result: ValidationResult) => void;
|
|
54
56
|
validationErrorFallback?: ReactNode;
|
|
57
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
58
|
+
loadingOverlay?: ReactNode;
|
|
55
59
|
}) {
|
|
56
60
|
const validation = useMemo(() => validateSnapResponse(snap), [snap]);
|
|
57
61
|
const valid = validation.valid;
|
|
@@ -79,6 +83,7 @@ export function SnapViewV2({
|
|
|
79
83
|
handlers={handlers}
|
|
80
84
|
loading={loading}
|
|
81
85
|
appearance={appearance}
|
|
86
|
+
loadingOverlay={loadingOverlay}
|
|
82
87
|
/>
|
|
83
88
|
);
|
|
84
89
|
}
|
|
@@ -96,6 +101,7 @@ export function SnapCardV2({
|
|
|
96
101
|
validationErrorFallback,
|
|
97
102
|
actionError,
|
|
98
103
|
plain = false,
|
|
104
|
+
loadingOverlay,
|
|
99
105
|
}: {
|
|
100
106
|
snap: SnapPage;
|
|
101
107
|
handlers: SnapActionHandlers;
|
|
@@ -107,12 +113,18 @@ export function SnapCardV2({
|
|
|
107
113
|
validationErrorFallback?: ReactNode;
|
|
108
114
|
actionError?: string | null;
|
|
109
115
|
plain?: boolean;
|
|
116
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
117
|
+
loadingOverlay?: ReactNode;
|
|
110
118
|
}) {
|
|
111
119
|
const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
|
|
112
120
|
const isDark = appearance === "dark";
|
|
113
121
|
const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
|
|
114
122
|
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
115
123
|
const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
|
|
124
|
+
const accentHex = useMemo(
|
|
125
|
+
() => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance),
|
|
126
|
+
[snap.theme?.accent, appearance],
|
|
127
|
+
);
|
|
116
128
|
|
|
117
129
|
return (
|
|
118
130
|
<>
|
|
@@ -138,8 +150,18 @@ export function SnapCardV2({
|
|
|
138
150
|
appearance={appearance}
|
|
139
151
|
onValidationError={onValidationError}
|
|
140
152
|
validationErrorFallback={validationErrorFallback}
|
|
153
|
+
loadingOverlay={null}
|
|
141
154
|
/>
|
|
142
155
|
</div>
|
|
156
|
+
{loadingOverlay === undefined ? (
|
|
157
|
+
<SnapLoadingOverlay
|
|
158
|
+
appearance={appearance}
|
|
159
|
+
accentHex={accentHex}
|
|
160
|
+
active={loading}
|
|
161
|
+
/>
|
|
162
|
+
) : loading ? (
|
|
163
|
+
<>{loadingOverlay}</>
|
|
164
|
+
) : null}
|
|
143
165
|
{showOverflowWarning && (
|
|
144
166
|
<div
|
|
145
167
|
style={{
|
|
@@ -31,6 +31,7 @@ export function SnapCard({
|
|
|
31
31
|
validationErrorFallback,
|
|
32
32
|
actionError,
|
|
33
33
|
plain = false,
|
|
34
|
+
loadingOverlay,
|
|
34
35
|
}: {
|
|
35
36
|
snap: SnapPage;
|
|
36
37
|
handlers: SnapActionHandlers;
|
|
@@ -49,6 +50,8 @@ export function SnapCard({
|
|
|
49
50
|
actionError?: string | null;
|
|
50
51
|
/** When true, renders without card frame (no border, background, or padding). */
|
|
51
52
|
plain?: boolean;
|
|
53
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
54
|
+
loadingOverlay?: ReactNode;
|
|
52
55
|
}) {
|
|
53
56
|
if (snap.version === SPEC_VERSION_2) {
|
|
54
57
|
return (
|
|
@@ -64,6 +67,7 @@ export function SnapCard({
|
|
|
64
67
|
validationErrorFallback={validationErrorFallback}
|
|
65
68
|
actionError={actionError}
|
|
66
69
|
plain={plain}
|
|
70
|
+
loadingOverlay={loadingOverlay}
|
|
67
71
|
/>
|
|
68
72
|
);
|
|
69
73
|
}
|
|
@@ -78,6 +82,7 @@ export function SnapCard({
|
|
|
78
82
|
borderRadius={borderRadius}
|
|
79
83
|
actionError={actionError}
|
|
80
84
|
plain={plain}
|
|
85
|
+
loadingOverlay={loadingOverlay}
|
|
81
86
|
/>
|
|
82
87
|
);
|
|
83
88
|
}
|
|
@@ -2,7 +2,14 @@ import type { Spec } from "@json-render/core";
|
|
|
2
2
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
3
|
import { SnapCatalogView } from "./catalog-renderer";
|
|
4
4
|
import { useSnapTheme } from "./theme";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
6
13
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
7
14
|
import {
|
|
8
15
|
DEFAULT_THEME_ACCENT,
|
|
@@ -66,10 +73,16 @@ export function SnapViewCoreInner({
|
|
|
66
73
|
snap,
|
|
67
74
|
handlers,
|
|
68
75
|
loading = false,
|
|
76
|
+
loadingOverlay,
|
|
69
77
|
}: {
|
|
70
78
|
snap: SnapPage;
|
|
71
79
|
handlers: SnapActionHandlers;
|
|
72
80
|
loading?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Custom content rendered while `loading` is true. When `undefined` (default)
|
|
83
|
+
* the built-in ActivityIndicator overlay is used. Pass `null` to render nothing.
|
|
84
|
+
*/
|
|
85
|
+
loadingOverlay?: ReactNode;
|
|
73
86
|
}) {
|
|
74
87
|
const { mode } = useSnapTheme();
|
|
75
88
|
const spec = snap.ui;
|
|
@@ -172,19 +185,16 @@ export function SnapViewCoreInner({
|
|
|
172
185
|
|
|
173
186
|
return (
|
|
174
187
|
<View style={styles.container}>
|
|
175
|
-
{loading
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<ActivityIndicator size="large" color={accentHex} />
|
|
186
|
-
</View>
|
|
187
|
-
) : null}
|
|
188
|
+
{loading
|
|
189
|
+
? loadingOverlay === undefined
|
|
190
|
+
? (
|
|
191
|
+
<SnapLoadingOverlay
|
|
192
|
+
appearance={mode}
|
|
193
|
+
accentHex={accentHex}
|
|
194
|
+
/>
|
|
195
|
+
)
|
|
196
|
+
: loadingOverlay
|
|
197
|
+
: null}
|
|
188
198
|
<SnapCatalogView
|
|
189
199
|
key={pageKey}
|
|
190
200
|
spec={spec}
|
|
@@ -199,6 +209,30 @@ export function SnapViewCoreInner({
|
|
|
199
209
|
);
|
|
200
210
|
}
|
|
201
211
|
|
|
212
|
+
export function SnapLoadingOverlay({
|
|
213
|
+
appearance,
|
|
214
|
+
accentHex,
|
|
215
|
+
}: {
|
|
216
|
+
appearance: "light" | "dark";
|
|
217
|
+
accentHex: string;
|
|
218
|
+
}) {
|
|
219
|
+
return (
|
|
220
|
+
<View
|
|
221
|
+
style={[
|
|
222
|
+
styles.overlay,
|
|
223
|
+
{
|
|
224
|
+
backgroundColor:
|
|
225
|
+
appearance === "dark"
|
|
226
|
+
? "rgba(0,0,0,0.1)"
|
|
227
|
+
: "rgba(255,255,255,0.2)",
|
|
228
|
+
},
|
|
229
|
+
]}
|
|
230
|
+
>
|
|
231
|
+
<ActivityIndicator size="large" color={accentHex} />
|
|
232
|
+
</View>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
202
236
|
const styles = StyleSheet.create({
|
|
203
237
|
container: {
|
|
204
238
|
width: "100%",
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { type ReactNode, useEffect, useState } from "react";
|
|
2
2
|
import { View, Text, StyleSheet, Pressable } from "react-native";
|
|
3
3
|
import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
SnapLoadingOverlay,
|
|
6
|
+
SnapViewCoreInner,
|
|
7
|
+
resolveAccentHex,
|
|
8
|
+
} from "../snap-view-core";
|
|
5
9
|
import type { SnapPage, SnapActionHandlers } from "../types";
|
|
6
10
|
|
|
7
11
|
const SNAP_MAX_HEIGHT = 500;
|
|
@@ -12,13 +16,20 @@ export function SnapViewV1Inner({
|
|
|
12
16
|
snap,
|
|
13
17
|
handlers,
|
|
14
18
|
loading = false,
|
|
19
|
+
loadingOverlay,
|
|
15
20
|
}: {
|
|
16
21
|
snap: SnapPage;
|
|
17
22
|
handlers: SnapActionHandlers;
|
|
18
23
|
loading?: boolean;
|
|
24
|
+
loadingOverlay?: ReactNode;
|
|
19
25
|
}) {
|
|
20
26
|
return (
|
|
21
|
-
<SnapViewCoreInner
|
|
27
|
+
<SnapViewCoreInner
|
|
28
|
+
snap={snap}
|
|
29
|
+
handlers={handlers}
|
|
30
|
+
loading={loading}
|
|
31
|
+
loadingOverlay={loadingOverlay}
|
|
32
|
+
/>
|
|
22
33
|
);
|
|
23
34
|
}
|
|
24
35
|
|
|
@@ -28,16 +39,24 @@ export function SnapViewV1({
|
|
|
28
39
|
loading = false,
|
|
29
40
|
appearance = "dark",
|
|
30
41
|
colors,
|
|
42
|
+
loadingOverlay,
|
|
31
43
|
}: {
|
|
32
44
|
snap: SnapPage;
|
|
33
45
|
handlers: SnapActionHandlers;
|
|
34
46
|
loading?: boolean;
|
|
35
47
|
appearance?: "light" | "dark";
|
|
36
48
|
colors?: Partial<SnapNativeColors>;
|
|
49
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
50
|
+
loadingOverlay?: ReactNode;
|
|
37
51
|
}) {
|
|
38
52
|
return (
|
|
39
53
|
<SnapThemeProvider appearance={appearance} colors={colors}>
|
|
40
|
-
<SnapViewV1Inner
|
|
54
|
+
<SnapViewV1Inner
|
|
55
|
+
snap={snap}
|
|
56
|
+
handlers={handlers}
|
|
57
|
+
loading={loading}
|
|
58
|
+
loadingOverlay={loadingOverlay}
|
|
59
|
+
/>
|
|
41
60
|
</SnapThemeProvider>
|
|
42
61
|
);
|
|
43
62
|
}
|
|
@@ -52,6 +71,7 @@ function SnapCardV1Inner({
|
|
|
52
71
|
actionError,
|
|
53
72
|
appearance,
|
|
54
73
|
plain,
|
|
74
|
+
loadingOverlay,
|
|
55
75
|
}: {
|
|
56
76
|
snap: SnapPage;
|
|
57
77
|
handlers: SnapActionHandlers;
|
|
@@ -60,8 +80,10 @@ function SnapCardV1Inner({
|
|
|
60
80
|
actionError?: string | null;
|
|
61
81
|
appearance: "light" | "dark";
|
|
62
82
|
plain: boolean;
|
|
83
|
+
loadingOverlay?: ReactNode;
|
|
63
84
|
}) {
|
|
64
|
-
const { colors } = useSnapTheme();
|
|
85
|
+
const { colors, mode } = useSnapTheme();
|
|
86
|
+
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
65
87
|
const [contentHeight, setContentHeight] = useState(0);
|
|
66
88
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
67
89
|
|
|
@@ -107,9 +129,15 @@ function SnapCardV1Inner({
|
|
|
107
129
|
snap={snap}
|
|
108
130
|
handlers={handlers}
|
|
109
131
|
loading={loading}
|
|
132
|
+
loadingOverlay={null}
|
|
110
133
|
/>
|
|
111
134
|
</View>
|
|
112
135
|
</View>
|
|
136
|
+
{loading
|
|
137
|
+
? loadingOverlay === undefined
|
|
138
|
+
? <SnapLoadingOverlay appearance={mode} accentHex={accentHex} />
|
|
139
|
+
: loadingOverlay
|
|
140
|
+
: null}
|
|
113
141
|
{isExpandable ? (
|
|
114
142
|
<View
|
|
115
143
|
style={[
|
|
@@ -170,6 +198,7 @@ export function SnapCardV1({
|
|
|
170
198
|
borderRadius = 16,
|
|
171
199
|
actionError,
|
|
172
200
|
plain = false,
|
|
201
|
+
loadingOverlay,
|
|
173
202
|
}: {
|
|
174
203
|
snap: SnapPage;
|
|
175
204
|
handlers: SnapActionHandlers;
|
|
@@ -179,6 +208,8 @@ export function SnapCardV1({
|
|
|
179
208
|
borderRadius?: number;
|
|
180
209
|
actionError?: string | null;
|
|
181
210
|
plain?: boolean;
|
|
211
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
212
|
+
loadingOverlay?: ReactNode;
|
|
182
213
|
}) {
|
|
183
214
|
return (
|
|
184
215
|
<SnapThemeProvider appearance={appearance} colors={colors}>
|
|
@@ -190,6 +221,7 @@ export function SnapCardV1({
|
|
|
190
221
|
actionError={actionError}
|
|
191
222
|
appearance={appearance}
|
|
192
223
|
plain={plain}
|
|
224
|
+
loadingOverlay={loadingOverlay}
|
|
193
225
|
/>
|
|
194
226
|
</SnapThemeProvider>
|
|
195
227
|
);
|
|
@@ -2,7 +2,11 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
SnapLoadingOverlay,
|
|
7
|
+
SnapViewCoreInner,
|
|
8
|
+
resolveAccentHex,
|
|
9
|
+
} from "../snap-view-core";
|
|
6
10
|
import {
|
|
7
11
|
validateSnapResponse,
|
|
8
12
|
type ValidationResult,
|
|
@@ -47,12 +51,14 @@ export function SnapViewV2Inner({
|
|
|
47
51
|
loading = false,
|
|
48
52
|
onValidationError,
|
|
49
53
|
validationErrorFallback,
|
|
54
|
+
loadingOverlay,
|
|
50
55
|
}: {
|
|
51
56
|
snap: SnapPage;
|
|
52
57
|
handlers: SnapActionHandlers;
|
|
53
58
|
loading?: boolean;
|
|
54
59
|
onValidationError?: (result: ValidationResult) => void;
|
|
55
60
|
validationErrorFallback?: ReactNode;
|
|
61
|
+
loadingOverlay?: ReactNode;
|
|
56
62
|
}) {
|
|
57
63
|
const validation = useMemo(() => validateSnapResponse(snap), [snap]);
|
|
58
64
|
const valid = validation.valid;
|
|
@@ -77,7 +83,12 @@ export function SnapViewV2Inner({
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
return (
|
|
80
|
-
<SnapViewCoreInner
|
|
86
|
+
<SnapViewCoreInner
|
|
87
|
+
snap={snap}
|
|
88
|
+
handlers={handlers}
|
|
89
|
+
loading={loading}
|
|
90
|
+
loadingOverlay={loadingOverlay}
|
|
91
|
+
/>
|
|
81
92
|
);
|
|
82
93
|
}
|
|
83
94
|
|
|
@@ -89,6 +100,7 @@ export function SnapViewV2({
|
|
|
89
100
|
colors,
|
|
90
101
|
onValidationError,
|
|
91
102
|
validationErrorFallback,
|
|
103
|
+
loadingOverlay,
|
|
92
104
|
}: {
|
|
93
105
|
snap: SnapPage;
|
|
94
106
|
handlers: SnapActionHandlers;
|
|
@@ -97,6 +109,8 @@ export function SnapViewV2({
|
|
|
97
109
|
colors?: Partial<SnapNativeColors>;
|
|
98
110
|
onValidationError?: (result: ValidationResult) => void;
|
|
99
111
|
validationErrorFallback?: ReactNode;
|
|
112
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
113
|
+
loadingOverlay?: ReactNode;
|
|
100
114
|
}) {
|
|
101
115
|
return (
|
|
102
116
|
<SnapThemeProvider appearance={appearance} colors={colors}>
|
|
@@ -106,6 +120,7 @@ export function SnapViewV2({
|
|
|
106
120
|
loading={loading}
|
|
107
121
|
onValidationError={onValidationError}
|
|
108
122
|
validationErrorFallback={validationErrorFallback}
|
|
123
|
+
loadingOverlay={loadingOverlay}
|
|
109
124
|
/>
|
|
110
125
|
</SnapThemeProvider>
|
|
111
126
|
);
|
|
@@ -124,6 +139,7 @@ function SnapCardV2Inner({
|
|
|
124
139
|
actionError,
|
|
125
140
|
appearance,
|
|
126
141
|
plain,
|
|
142
|
+
loadingOverlay,
|
|
127
143
|
}: {
|
|
128
144
|
snap: SnapPage;
|
|
129
145
|
handlers: SnapActionHandlers;
|
|
@@ -135,8 +151,10 @@ function SnapCardV2Inner({
|
|
|
135
151
|
actionError?: string | null;
|
|
136
152
|
appearance: "light" | "dark";
|
|
137
153
|
plain: boolean;
|
|
154
|
+
loadingOverlay?: ReactNode;
|
|
138
155
|
}) {
|
|
139
|
-
const { colors } = useSnapTheme();
|
|
156
|
+
const { colors, mode } = useSnapTheme();
|
|
157
|
+
const accentHex = resolveAccentHex(snap.theme?.accent, mode);
|
|
140
158
|
const [contentHeight, setContentHeight] = useState(0);
|
|
141
159
|
|
|
142
160
|
const content = (
|
|
@@ -146,11 +164,21 @@ function SnapCardV2Inner({
|
|
|
146
164
|
loading={loading}
|
|
147
165
|
onValidationError={onValidationError}
|
|
148
166
|
validationErrorFallback={validationErrorFallback}
|
|
167
|
+
loadingOverlay={null}
|
|
149
168
|
/>
|
|
150
169
|
);
|
|
151
170
|
|
|
152
171
|
if (plain) {
|
|
153
|
-
return
|
|
172
|
+
return (
|
|
173
|
+
<>
|
|
174
|
+
{content}
|
|
175
|
+
{loading
|
|
176
|
+
? loadingOverlay === undefined
|
|
177
|
+
? <SnapLoadingOverlay appearance={mode} accentHex={accentHex} />
|
|
178
|
+
: loadingOverlay
|
|
179
|
+
: null}
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
154
182
|
}
|
|
155
183
|
|
|
156
184
|
const overflowAmount = showOverflowWarning ? contentHeight - SNAP_MAX_HEIGHT : 0;
|
|
@@ -184,6 +212,11 @@ function SnapCardV2Inner({
|
|
|
184
212
|
<View style={{ flex: 1, backgroundColor: "rgba(255,50,50,0.15)" }} />
|
|
185
213
|
</View>
|
|
186
214
|
)}
|
|
215
|
+
{loading
|
|
216
|
+
? loadingOverlay === undefined
|
|
217
|
+
? <SnapLoadingOverlay appearance={mode} accentHex={accentHex} />
|
|
218
|
+
: loadingOverlay
|
|
219
|
+
: null}
|
|
187
220
|
</View>
|
|
188
221
|
{actionError && (
|
|
189
222
|
<Text
|
|
@@ -216,6 +249,7 @@ export function SnapCardV2({
|
|
|
216
249
|
validationErrorFallback,
|
|
217
250
|
actionError,
|
|
218
251
|
plain = false,
|
|
252
|
+
loadingOverlay,
|
|
219
253
|
}: {
|
|
220
254
|
snap: SnapPage;
|
|
221
255
|
handlers: SnapActionHandlers;
|
|
@@ -228,6 +262,8 @@ export function SnapCardV2({
|
|
|
228
262
|
validationErrorFallback?: ReactNode;
|
|
229
263
|
actionError?: string | null;
|
|
230
264
|
plain?: boolean;
|
|
265
|
+
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
266
|
+
loadingOverlay?: ReactNode;
|
|
231
267
|
}) {
|
|
232
268
|
return (
|
|
233
269
|
<SnapThemeProvider appearance={appearance} colors={colors}>
|
|
@@ -242,6 +278,7 @@ export function SnapCardV2({
|
|
|
242
278
|
actionError={actionError}
|
|
243
279
|
appearance={appearance}
|
|
244
280
|
plain={plain}
|
|
281
|
+
loadingOverlay={loadingOverlay}
|
|
245
282
|
/>
|
|
246
283
|
</SnapThemeProvider>
|
|
247
284
|
);
|
package/src/ui/catalog.ts
CHANGED
|
@@ -58,7 +58,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
58
58
|
item: {
|
|
59
59
|
props: itemProps,
|
|
60
60
|
description:
|
|
61
|
-
"Content row with title and optional description. Children render in the actions slot (right side) —
|
|
61
|
+
"Content row with title and optional description. Children render in the actions slot (right side) — badge, button, and icon elements are all valid. The item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.",
|
|
62
62
|
},
|
|
63
63
|
item_group: {
|
|
64
64
|
props: itemGroupProps,
|