@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.
@@ -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;
@@ -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 SnapViewCore({ snap, handlers, loading, appearance, }: {
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 ? (_jsx(View, { style: [
138
- styles.overlay,
139
- {
140
- backgroundColor: mode === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
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 }) }) }), isExpandable ? (_jsx(View, { style: [
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)" } })] }))] }), actionError && (_jsx(Text, { style: {
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" },
@@ -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) — use badge, button, or text elements.",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- <SnapLoadingOverlay
319
- appearance={appearance}
320
- accentHex={accentHex}
321
- active={loading}
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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
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
- <View
177
- style={[
178
- styles.overlay,
179
- {
180
- backgroundColor:
181
- mode === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
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 { SnapViewCoreInner } from "../snap-view-core";
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 snap={snap} handlers={handlers} loading={loading} />
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 snap={snap} handlers={handlers} loading={loading} />
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 { SnapViewCoreInner } from "../snap-view-core";
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 snap={snap} handlers={handlers} loading={loading} />
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 content;
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) — use badge, button, or text elements.",
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,