@farcaster/snap 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/react/components/action-button.js +2 -2
  2. package/dist/react/index.d.ts +4 -1
  3. package/dist/react/index.js +3 -3
  4. package/dist/react/v1/snap-view.d.ts +2 -1
  5. package/dist/react/v1/snap-view.js +77 -2
  6. package/dist/react/v2/snap-view.d.ts +2 -1
  7. package/dist/react/v2/snap-view.js +11 -3
  8. package/dist/react-native/components/snap-action-button.js +3 -3
  9. package/dist/react-native/components/snap-bar-chart.js +1 -1
  10. package/dist/react-native/components/snap-image.js +0 -1
  11. package/dist/react-native/components/snap-progress.js +1 -1
  12. package/dist/react-native/components/snap-text.js +1 -1
  13. package/dist/react-native/index.d.ts +3 -1
  14. package/dist/react-native/index.js +3 -3
  15. package/dist/react-native/theme.js +6 -6
  16. package/dist/react-native/types.d.ts +1 -0
  17. package/dist/react-native/v1/snap-view.d.ts +2 -1
  18. package/dist/react-native/v1/snap-view.js +68 -11
  19. package/dist/react-native/v2/snap-view.d.ts +2 -1
  20. package/dist/react-native/v2/snap-view.js +25 -21
  21. package/dist/ui/catalog.d.ts +6 -1
  22. package/dist/ui/catalog.js +6 -5
  23. package/llms.txt +3 -2
  24. package/package.json +1 -1
  25. package/src/react/components/action-button.tsx +2 -2
  26. package/src/react/index.tsx +6 -0
  27. package/src/react/v1/snap-view.tsx +117 -7
  28. package/src/react/v2/snap-view.tsx +13 -1
  29. package/src/react-native/components/snap-action-button.tsx +3 -3
  30. package/src/react-native/components/snap-bar-chart.tsx +1 -1
  31. package/src/react-native/components/snap-image.tsx +0 -1
  32. package/src/react-native/components/snap-progress.tsx +1 -1
  33. package/src/react-native/components/snap-text.tsx +1 -1
  34. package/src/react-native/index.tsx +5 -0
  35. package/src/react-native/theme.tsx +6 -6
  36. package/src/react-native/types.ts +1 -0
  37. package/src/react-native/v1/snap-view.tsx +102 -7
  38. package/src/react-native/v2/snap-view.tsx +52 -40
  39. package/src/ui/catalog.ts +6 -5
@@ -10,9 +10,9 @@ function isExternalLinkAction(on) {
10
10
  if (!on)
11
11
  return false;
12
12
  const press = on.press;
13
- if (!press || press.action !== "open_url")
13
+ if (!press)
14
14
  return false;
15
- return press.params?.isSnap !== true;
15
+ return press.action === "open_url";
16
16
  }
17
17
  export function SnapActionButton({ element, emit, }) {
18
18
  const { props } = element;
@@ -15,6 +15,7 @@ export type SnapPage = {
15
15
  export type SnapActionHandlers = {
16
16
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
17
17
  open_url: (target: string) => void;
18
+ open_snap: (target: string) => void;
18
19
  open_mini_app: (target: string) => void;
19
20
  view_cast: (params: {
20
21
  hash: string;
@@ -41,7 +42,7 @@ export type SnapActionHandlers = {
41
42
  buyToken?: string;
42
43
  }) => void;
43
44
  };
44
- export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, }: {
45
+ export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
45
46
  snap: SnapPage;
46
47
  handlers: SnapActionHandlers;
47
48
  loading?: boolean;
@@ -53,4 +54,6 @@ export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth
53
54
  validationErrorFallback?: ReactNode;
54
55
  /** Server-side action error message to display inline. */
55
56
  actionError?: string | null;
57
+ /** When true, renders without card frame (no border, background, or padding). */
58
+ plain?: boolean;
56
59
  }): 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, }) {
7
+ export function SnapCard({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
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 }));
9
+ return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain }));
10
10
  }
11
- return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, actionError: actionError }));
11
+ return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, actionError: actionError, plain: plain }));
12
12
  }
@@ -5,11 +5,12 @@ export declare function SnapViewV1({ snap, handlers, loading, appearance, }: {
5
5
  loading?: boolean;
6
6
  appearance?: "light" | "dark";
7
7
  }): import("react/jsx-runtime").JSX.Element;
8
- export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, actionError, }: {
8
+ export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, actionError, plain, }: {
9
9
  snap: SnapPage;
10
10
  handlers: SnapActionHandlers;
11
11
  loading?: boolean;
12
12
  appearance?: "light" | "dark";
13
13
  maxWidth?: number;
14
14
  actionError?: string | null;
15
+ plain?: boolean;
15
16
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,86 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
3
4
  import { SnapViewCore } from "../snap-view-core.js";
5
+ const SNAP_MAX_HEIGHT = 500;
4
6
  export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", }) {
5
7
  return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
6
8
  }
7
- export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, actionError, }) {
8
- return (_jsxs("div", { style: { position: "relative", width: "100%", maxWidth }, children: [_jsx(SnapViewV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }), actionError && (_jsx("div", { style: {
9
+ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, actionError, plain = false, }) {
10
+ const isDark = appearance === "dark";
11
+ const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
12
+ const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
13
+ const toggleBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
14
+ const toggleBgHover = isDark
15
+ ? "rgba(255,255,255,0.1)"
16
+ : "rgba(0,0,0,0.08)";
17
+ const toggleText = isDark ? "rgba(255,255,255,0.82)" : "rgba(0,0,0,0.72)";
18
+ const contentRef = useRef(null);
19
+ const [isExpandable, setIsExpandable] = useState(false);
20
+ const [isExpanded, setIsExpanded] = useState(false);
21
+ useEffect(() => {
22
+ setIsExpanded(false);
23
+ }, [snap]);
24
+ useEffect(() => {
25
+ const node = contentRef.current;
26
+ if (!node)
27
+ return;
28
+ const measure = () => {
29
+ setIsExpandable(node.scrollHeight > SNAP_MAX_HEIGHT + 1);
30
+ };
31
+ measure();
32
+ if (typeof ResizeObserver === "undefined")
33
+ return;
34
+ const observer = new ResizeObserver(() => {
35
+ measure();
36
+ });
37
+ observer.observe(node);
38
+ return () => observer.disconnect();
39
+ }, [snap, plain]);
40
+ useEffect(() => {
41
+ if (!isExpandable) {
42
+ setIsExpanded(false);
43
+ }
44
+ }, [isExpandable]);
45
+ const isClipped = isExpandable && !isExpanded;
46
+ return (_jsxs("div", { style: {
47
+ position: "relative",
48
+ width: "100%",
49
+ maxWidth,
50
+ overflow: "hidden",
51
+ ...(plain ? {} : {
52
+ borderRadius: 16,
53
+ border: `1px solid ${borderColor}`,
54
+ backgroundColor: surfaceBg,
55
+ }),
56
+ }, children: [_jsx("div", { style: isClipped
57
+ ? {
58
+ maxHeight: SNAP_MAX_HEIGHT,
59
+ overflow: "hidden",
60
+ }
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: {
62
+ display: "flex",
63
+ justifyContent: "center",
64
+ padding: plain ? "8px 0 0" : "10px 16px 12px",
65
+ ...(plain
66
+ ? {}
67
+ : { borderTop: `1px solid ${borderColor}` }),
68
+ }, children: _jsx("button", { type: "button", "aria-expanded": isExpanded, onClick: () => setIsExpanded((value) => !value), style: {
69
+ appearance: "none",
70
+ border: "none",
71
+ borderRadius: 9999,
72
+ backgroundColor: toggleBg,
73
+ color: toggleText,
74
+ padding: "6px 10px",
75
+ fontSize: 13,
76
+ lineHeight: "18px",
77
+ fontWeight: 600,
78
+ cursor: "pointer",
79
+ }, onMouseEnter: (event) => {
80
+ event.currentTarget.style.backgroundColor = toggleBgHover;
81
+ }, onMouseLeave: (event) => {
82
+ event.currentTarget.style.backgroundColor = toggleBg;
83
+ }, children: isExpanded ? "Show less" : "Show more" }) })) : null, actionError && (_jsx("div", { style: {
9
84
  padding: "8px 12px",
10
85
  fontSize: 13,
11
86
  color: appearance === "dark"
@@ -9,7 +9,7 @@ export declare function SnapViewV2({ snap, handlers, loading, appearance, onVali
9
9
  onValidationError?: (result: ValidationResult) => void;
10
10
  validationErrorFallback?: ReactNode;
11
11
  }): import("react/jsx-runtime").JSX.Element | null;
12
- export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, }: {
12
+ export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
13
13
  snap: SnapPage;
14
14
  handlers: SnapActionHandlers;
15
15
  loading?: boolean;
@@ -19,4 +19,5 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWid
19
19
  onValidationError?: (result: ValidationResult) => void;
20
20
  validationErrorFallback?: ReactNode;
21
21
  actionError?: string | null;
22
+ plain?: boolean;
22
23
  }): import("react/jsx-runtime").JSX.Element;
@@ -42,16 +42,24 @@ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark
42
42
  return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
43
43
  }
44
44
  // ─── SnapCardV2 ──────────────────────────────────────
45
- export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, }) {
45
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
46
46
  const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
47
- const bg = appearance === "dark" ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
47
+ const isDark = appearance === "dark";
48
+ const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
49
+ const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
50
+ const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
48
51
  return (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
49
52
  position: "relative",
50
53
  width: "100%",
51
54
  maxWidth,
52
55
  maxHeight,
53
56
  overflow: "hidden",
54
- }, children: [_jsx(SnapViewV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }), showOverflowWarning && (_jsxs("div", { style: {
57
+ ...(plain ? {} : {
58
+ borderRadius: 16,
59
+ border: `1px solid ${borderColor}`,
60
+ backgroundColor: surfaceBg,
61
+ }),
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: {
55
63
  position: "absolute",
56
64
  top: SNAP_MAX_HEIGHT,
57
65
  left: 0,
@@ -8,9 +8,9 @@ function isExternalLinkAction(on) {
8
8
  if (!on)
9
9
  return false;
10
10
  const press = on.press;
11
- if (!press || press.action !== "open_url")
11
+ if (!press)
12
12
  return false;
13
- return press.params?.isSnap !== true;
13
+ return press.action === "open_url";
14
14
  }
15
15
  export function SnapActionButton({ element, emit, }) {
16
16
  const { accentHex } = useSnapPalette();
@@ -50,7 +50,7 @@ export function SnapActionButton({ element, emit, }) {
50
50
  : null, _jsx(Text, { style: { color: textColor, fontSize: 14, lineHeight: 18, fontWeight: "600" }, children: label }), showExternalIcon ? (_jsx(ExternalLink, { size: 14, color: iconColor, style: { opacity: 0.6 } })) : null] }) }));
51
51
  }
52
52
  const styles = StyleSheet.create({
53
- outer: { flex: 1, minWidth: 0 },
53
+ outer: { minWidth: 0 },
54
54
  btn: {
55
55
  paddingHorizontal: 16,
56
56
  borderRadius: 10,
@@ -30,7 +30,7 @@ export function SnapBarChart({ element: { props }, }) {
30
30
  }) }));
31
31
  }
32
32
  const styles = StyleSheet.create({
33
- wrap: { flex: 1, width: "100%", gap: 8 },
33
+ wrap: { width: "100%", gap: 8 },
34
34
  row: { flexDirection: "row", alignItems: "center", gap: 8 },
35
35
  label: { width: 80, fontSize: 12, lineHeight: 16, textAlign: "right" },
36
36
  track: { flex: 1, height: 10, borderRadius: 9999, overflow: "hidden" },
@@ -15,7 +15,6 @@ export function SnapImage({ element: { props }, }) {
15
15
  }
16
16
  const styles = StyleSheet.create({
17
17
  frame: {
18
- flex: 1,
19
18
  width: "100%",
20
19
  borderRadius: 8,
21
20
  overflow: "hidden",
@@ -12,7 +12,7 @@ export function SnapProgress({ element: { props }, }) {
12
12
  return (_jsxs(View, { style: styles.wrap, children: [label ? (_jsx(Text, { style: [styles.label, { color: colors.textSecondary }], children: label })) : null, _jsx(View, { style: [styles.track, { backgroundColor: colors.muted }], children: _jsx(View, { style: [styles.fill, { width: `${percent}%`, backgroundColor: accentHex }] }) })] }));
13
13
  }
14
14
  const styles = StyleSheet.create({
15
- wrap: { flex: 1, width: "100%", gap: 4 },
15
+ wrap: { width: "100%", gap: 4 },
16
16
  label: { fontSize: 13, lineHeight: 18 },
17
17
  track: {
18
18
  height: 10,
@@ -30,6 +30,6 @@ export function SnapText({ element: { props }, }) {
30
30
  ], children: content }) }));
31
31
  }
32
32
  const styles = StyleSheet.create({
33
- wrap: { flex: 1, width: "100%" },
33
+ wrap: { width: "100%" },
34
34
  base: {},
35
35
  });
@@ -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, }: {
10
+ export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
11
11
  snap: SnapPage;
12
12
  handlers: SnapActionHandlers;
13
13
  loading?: boolean;
@@ -23,4 +23,6 @@ export declare function SnapCard({ snap, handlers, loading, appearance, colors,
23
23
  validationErrorFallback?: ReactNode;
24
24
  /** Server-side action error message to display inline. */
25
25
  actionError?: string | null;
26
+ /** When true, renders without card frame (no border, background, or padding). */
27
+ plain?: boolean;
26
28
  }): 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, }) {
10
+ export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
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 }));
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 }));
13
13
  }
14
- return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError }));
14
+ return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain }));
15
15
  }
@@ -3,9 +3,9 @@ import { createContext, useContext, useMemo } from "react";
3
3
  const DEFAULT_LIGHT = {
4
4
  bg: "#dfe3e8",
5
5
  surface: "#ffffff",
6
- text: "#111111",
7
- textSecondary: "#6b7280",
8
- border: "#E5E7EB",
6
+ text: "rgba(0,0,0,0.9)",
7
+ textSecondary: "rgba(0,0,0,0.5)",
8
+ border: "rgba(0,0,0,0.1)",
9
9
  inputBg: "rgba(0,0,0,0.06)",
10
10
  muted: "rgba(0,0,0,0.08)",
11
11
  mutedSubtle: "rgba(0,0,0,0.04)",
@@ -15,9 +15,9 @@ const DEFAULT_LIGHT = {
15
15
  const DEFAULT_DARK = {
16
16
  bg: "#111318",
17
17
  surface: "#1a1d24",
18
- text: "#fafafa",
19
- textSecondary: "#a1a1aa",
20
- border: "#2D2D44",
18
+ text: "rgba(255,255,255,0.9)",
19
+ textSecondary: "rgba(255,255,255,0.5)",
20
+ border: "rgba(255,255,255,0.1)",
21
21
  inputBg: "rgba(255,255,255,0.04)",
22
22
  muted: "rgba(255,255,255,0.06)",
23
23
  mutedSubtle: "rgba(255,255,255,0.03)",
@@ -13,6 +13,7 @@ export type SnapPage = {
13
13
  export type SnapActionHandlers = {
14
14
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
15
15
  open_url: (target: string) => void;
16
+ open_snap: (target: string) => void;
16
17
  open_mini_app: (target: string) => void;
17
18
  view_cast: (params: {
18
19
  hash: string;
@@ -12,7 +12,7 @@ export declare function SnapViewV1({ snap, handlers, loading, appearance, colors
12
12
  appearance?: "light" | "dark";
13
13
  colors?: Partial<SnapNativeColors>;
14
14
  }): import("react").JSX.Element;
15
- export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, }: {
15
+ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, }: {
16
16
  snap: SnapPage;
17
17
  handlers: SnapActionHandlers;
18
18
  loading?: boolean;
@@ -20,4 +20,5 @@ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors
20
20
  colors?: Partial<SnapNativeColors>;
21
21
  borderRadius?: number;
22
22
  actionError?: string | null;
23
+ plain?: boolean;
23
24
  }): import("react").JSX.Element;
@@ -1,25 +1,56 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { View, Text, StyleSheet } from "react-native";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { View, Text, StyleSheet, Pressable } from "react-native";
3
4
  import { SnapThemeProvider, useSnapTheme } from "../theme.js";
4
5
  import { SnapViewCoreInner } from "../snap-view-core.js";
5
- // ─── SnapViewV1 (no validation, no height limits) ────
6
+ const SNAP_MAX_HEIGHT = 500;
7
+ // ─── SnapViewV1 (no validation) ──────────────────────
6
8
  export function SnapViewV1Inner({ snap, handlers, loading = false, }) {
7
9
  return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading }));
8
10
  }
9
11
  export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", colors, }) {
10
12
  return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }));
11
13
  }
12
- // ─── SnapCardV1 (card frame, no height limits) ───────
13
- function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, }) {
14
+ // ─── SnapCardV1 (card frame with expandable clipping) ──
15
+ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, plain, }) {
14
16
  const { colors } = useSnapTheme();
15
- return (_jsxs(_Fragment, { children: [_jsx(View, { style: cardStyles.frameRing, children: _jsx(View, { style: [
16
- cardStyles.card,
17
- {
17
+ const [contentHeight, setContentHeight] = useState(0);
18
+ const [isExpanded, setIsExpanded] = useState(false);
19
+ useEffect(() => {
20
+ setIsExpanded(false);
21
+ setContentHeight(0);
22
+ }, [snap]);
23
+ const isExpandable = contentHeight > SNAP_MAX_HEIGHT + 1;
24
+ const isClipped = isExpandable && !isExpanded;
25
+ return (_jsxs(_Fragment, { children: [_jsx(View, { style: cardStyles.frameRing, children: _jsxs(View, { style: [
26
+ plain ? undefined : cardStyles.card,
27
+ plain ? undefined : {
18
28
  borderRadius,
19
29
  borderColor: colors.border,
20
30
  backgroundColor: colors.surface,
21
31
  },
22
- ], children: _jsx(View, { style: cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }) }) }), actionError && (_jsx(Text, { style: [
32
+ ], children: [_jsx(View, { style: isClipped ? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" } : undefined, children: _jsx(View, { collapsable: false, onLayout: (event) => {
33
+ const nextHeight = Math.round(event.nativeEvent.layout.height);
34
+ setContentHeight((currentHeight) => isClipped
35
+ ? Math.max(currentHeight, nextHeight)
36
+ : currentHeight === nextHeight
37
+ ? currentHeight
38
+ : nextHeight);
39
+ }, style: plain ? undefined : cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }) }), isExpandable ? (_jsx(View, { style: [
40
+ cardStyles.expandRow,
41
+ plain
42
+ ? cardStyles.expandRowPlain
43
+ : { borderTopColor: colors.border },
44
+ ], children: _jsx(Pressable, { style: ({ pressed }) => [
45
+ cardStyles.expandButton,
46
+ {
47
+ backgroundColor: pressed
48
+ ? colors.mutedHover
49
+ : colors.muted,
50
+ },
51
+ ], onPress: () => {
52
+ setIsExpanded((value) => !value);
53
+ }, children: _jsx(Text, { style: [cardStyles.expandButtonText, { color: colors.text }], children: isExpanded ? "Show less" : "Show more" }) }) })) : null] }) }), actionError && (_jsx(Text, { style: [
23
54
  cardStyles.actionError,
24
55
  {
25
56
  color: appearance === "dark"
@@ -28,12 +59,38 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
28
59
  },
29
60
  ], children: actionError }))] }));
30
61
  }
31
- export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, }) {
32
- return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV1Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, actionError: actionError, appearance: appearance }) }));
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 }) }));
33
64
  }
34
65
  const cardStyles = StyleSheet.create({
35
66
  frameRing: { alignSelf: "stretch" },
36
67
  card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
37
68
  body: { paddingHorizontal: 16, paddingVertical: 16 },
69
+ expandRow: {
70
+ alignItems: "center",
71
+ paddingHorizontal: 16,
72
+ paddingTop: 10,
73
+ paddingBottom: 12,
74
+ borderTopWidth: StyleSheet.hairlineWidth,
75
+ },
76
+ expandRowPlain: {
77
+ paddingHorizontal: 0,
78
+ paddingTop: 8,
79
+ paddingBottom: 0,
80
+ borderTopWidth: 0,
81
+ },
82
+ expandButton: {
83
+ minWidth: 92,
84
+ alignItems: "center",
85
+ justifyContent: "center",
86
+ borderRadius: 9999,
87
+ paddingHorizontal: 10,
88
+ paddingVertical: 6,
89
+ },
90
+ expandButtonText: {
91
+ fontSize: 13,
92
+ lineHeight: 18,
93
+ fontWeight: "600",
94
+ },
38
95
  actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
39
96
  });
@@ -18,7 +18,7 @@ export declare function SnapViewV2({ snap, handlers, loading, appearance, colors
18
18
  onValidationError?: (result: ValidationResult) => void;
19
19
  validationErrorFallback?: ReactNode;
20
20
  }): import("react").JSX.Element;
21
- export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, }: {
21
+ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
22
22
  snap: SnapPage;
23
23
  handlers: SnapActionHandlers;
24
24
  loading?: boolean;
@@ -29,4 +29,5 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors
29
29
  onValidationError?: (result: ValidationResult) => void;
30
30
  validationErrorFallback?: ReactNode;
31
31
  actionError?: string | null;
32
+ plain?: boolean;
32
33
  }): import("react").JSX.Element;
@@ -50,32 +50,36 @@ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark
50
50
  return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }));
51
51
  }
52
52
  // ─── SnapCardV2 (card frame + height limits) ─────────
53
- function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, }) {
53
+ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, }) {
54
54
  const { colors } = useSnapTheme();
55
- const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
56
- return (_jsxs(_Fragment, { children: [_jsx(View, { style: cardStyles.frameRing, children: _jsxs(View, { style: [
57
- cardStyles.card,
58
- {
59
- borderRadius,
60
- ...(!showOverflowWarning && { maxHeight: SNAP_MAX_HEIGHT }),
61
- borderColor: colors.border,
62
- backgroundColor: colors.surface,
63
- },
64
- ], children: [_jsx(View, { style: cardStyles.body, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }), showOverflowWarning && (_jsxs(View, { style: cardStyles.warningOverlay, children: [_jsx(View, { style: cardStyles.warningLine }), _jsx(View, { style: cardStyles.warningLabel, children: _jsxs(Text, { style: cardStyles.warningLabelText, children: [SNAP_MAX_HEIGHT, "px"] }) })] }))] }) }), actionError && (_jsx(Text, { style: [
65
- cardStyles.actionError,
66
- {
67
- color: appearance === "dark"
68
- ? "rgba(255,100,100,0.9)"
69
- : "rgba(200,0,0,0.8)",
70
- },
71
- ], children: actionError }))] }));
55
+ const clipHeight = showOverflowWarning ? undefined : SNAP_MAX_HEIGHT;
56
+ const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }));
57
+ if (plain) {
58
+ return content;
59
+ }
60
+ return (_jsxs(_Fragment, { children: [_jsxs(View, { style: {
61
+ borderRadius,
62
+ borderWidth: 1,
63
+ borderColor: colors.border,
64
+ backgroundColor: colors.surface,
65
+ maxHeight: clipHeight,
66
+ overflow: "hidden",
67
+ minHeight: 120,
68
+ }, children: [_jsx(View, { style: { paddingHorizontal: 16, paddingVertical: 16 }, children: content }), showOverflowWarning && (_jsxs(View, { style: { position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, bottom: 0, 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: {
69
+ paddingHorizontal: 12,
70
+ paddingVertical: 8,
71
+ fontSize: 13,
72
+ color: appearance === "dark"
73
+ ? "rgba(255,100,100,0.9)"
74
+ : "rgba(200,0,0,0.8)",
75
+ }, children: actionError }))] }));
72
76
  }
73
- export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, }) {
74
- 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 }) }));
77
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
78
+ 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 }) }));
75
79
  }
76
80
  const cardStyles = StyleSheet.create({
77
81
  frameRing: { alignSelf: "stretch" },
78
- card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
82
+ card: { borderWidth: 1, minHeight: 120, overflow: "hidden" },
79
83
  body: { paddingHorizontal: 16, paddingVertical: 16 },
80
84
  actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
81
85
  warningOverlay: {
@@ -418,7 +418,12 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
418
418
  description: string;
419
419
  params: z.ZodObject<{
420
420
  target: z.ZodString;
421
- isSnap: z.ZodOptional<z.ZodBoolean>;
421
+ }, z.core.$strip>;
422
+ };
423
+ open_snap: {
424
+ description: string;
425
+ params: z.ZodObject<{
426
+ target: z.ZodString;
422
427
  }, z.core.$strip>;
423
428
  };
424
429
  open_mini_app: {
@@ -99,11 +99,12 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
99
99
  params: z.object({ target: z.string() }),
100
100
  },
101
101
  open_url: {
102
- description: "Open target snap or external URL.",
103
- params: z.object({
104
- target: z.string(),
105
- isSnap: z.boolean().optional(),
106
- }),
102
+ description: "Open external URL in browser.",
103
+ params: z.object({ target: z.string() }),
104
+ },
105
+ open_snap: {
106
+ description: "Open a snap URL inline. The client renders the target as a snap rather than opening a browser.",
107
+ params: z.object({ target: z.string() }),
107
108
  },
108
109
  open_mini_app: {
109
110
  description: "Open target URL as a Farcaster mini app.",
package/llms.txt CHANGED
@@ -153,14 +153,15 @@ Field values are sent in POST `inputs[name]` when a `submit` action fires.
153
153
  - `label` (string, optional, max 60)
154
154
  - POST value: string (single) or string[] (multiple)
155
155
 
156
- ## Actions (9 types)
156
+ ## Actions (10 types)
157
157
 
158
158
  Bound to buttons via `on.press`:
159
159
 
160
160
  | Action | Params | Description |
161
161
  |--------|--------|-------------|
162
162
  | `submit` | `target` (URL) | POST to server, get next page |
163
- | `open_url` | `target` (URL) | Open in system browser |
163
+ | `open_url` | `target` (URL) | Open external URL in browser |
164
+ | `open_snap` | `target` (URL) | Open a snap URL inline |
164
165
  | `open_mini_app` | `target` (URL) | Open as Farcaster mini app |
165
166
  | `view_cast` | `hash` (string) | Navigate to a cast |
166
167
  | `view_profile` | `fid` (number) | Navigate to a profile |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,8 +14,8 @@ function isExternalLinkAction(
14
14
  const press = on.press as
15
15
  | { action?: string; params?: Record<string, unknown> }
16
16
  | undefined;
17
- if (!press || press.action !== "open_url") return false;
18
- return press.params?.isSnap !== true;
17
+ if (!press) return false;
18
+ return press.action === "open_url";
19
19
  }
20
20
 
21
21
  export function SnapActionButton({