@farcaster/snap 1.19.0 → 1.20.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 (34) hide show
  1. package/dist/react/index.d.ts +3 -1
  2. package/dist/react/index.js +3 -3
  3. package/dist/react/v1/snap-view.d.ts +2 -1
  4. package/dist/react/v1/snap-view.js +77 -2
  5. package/dist/react/v2/snap-view.d.ts +2 -1
  6. package/dist/react/v2/snap-view.js +11 -3
  7. package/dist/react-native/components/snap-action-button.js +1 -1
  8. package/dist/react-native/components/snap-bar-chart.js +1 -1
  9. package/dist/react-native/components/snap-image.js +0 -1
  10. package/dist/react-native/components/snap-progress.js +1 -1
  11. package/dist/react-native/components/snap-text.js +1 -1
  12. package/dist/react-native/index.d.ts +3 -1
  13. package/dist/react-native/index.js +3 -3
  14. package/dist/react-native/theme.js +6 -6
  15. package/dist/react-native/v1/snap-view.d.ts +2 -1
  16. package/dist/react-native/v1/snap-view.js +68 -11
  17. package/dist/react-native/v2/snap-view.d.ts +2 -1
  18. package/dist/react-native/v2/snap-view.js +25 -21
  19. package/dist/ui/catalog.d.ts +0 -1
  20. package/dist/ui/catalog.js +1 -2
  21. package/package.json +1 -1
  22. package/src/react/index.tsx +5 -0
  23. package/src/react/v1/snap-view.tsx +117 -7
  24. package/src/react/v2/snap-view.tsx +13 -1
  25. package/src/react-native/components/snap-action-button.tsx +1 -1
  26. package/src/react-native/components/snap-bar-chart.tsx +1 -1
  27. package/src/react-native/components/snap-image.tsx +0 -1
  28. package/src/react-native/components/snap-progress.tsx +1 -1
  29. package/src/react-native/components/snap-text.tsx +1 -1
  30. package/src/react-native/index.tsx +5 -0
  31. package/src/react-native/theme.tsx +6 -6
  32. package/src/react-native/v1/snap-view.tsx +102 -7
  33. package/src/react-native/v2/snap-view.tsx +52 -40
  34. package/src/ui/catalog.ts +1 -2
@@ -41,7 +41,7 @@ export type SnapActionHandlers = {
41
41
  buyToken?: string;
42
42
  }) => void;
43
43
  };
44
- export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, }: {
44
+ export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
45
45
  snap: SnapPage;
46
46
  handlers: SnapActionHandlers;
47
47
  loading?: boolean;
@@ -53,4 +53,6 @@ export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth
53
53
  validationErrorFallback?: ReactNode;
54
54
  /** Server-side action error message to display inline. */
55
55
  actionError?: string | null;
56
+ /** When true, renders without card frame (no border, background, or padding). */
57
+ plain?: boolean;
56
58
  }): 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,
@@ -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)",
@@ -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,6 @@ 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>;
422
421
  }, z.core.$strip>;
423
422
  };
424
423
  open_mini_app: {
@@ -99,10 +99,9 @@ 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.",
102
+ description: "Open URL in browser.",
103
103
  params: z.object({
104
104
  target: z.string(),
105
- isSnap: z.boolean().optional(),
106
105
  }),
107
106
  },
108
107
  open_mini_app: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
@@ -57,6 +57,7 @@ export function SnapCard({
57
57
  onValidationError,
58
58
  validationErrorFallback,
59
59
  actionError,
60
+ plain = false,
60
61
  }: {
61
62
  snap: SnapPage;
62
63
  handlers: SnapActionHandlers;
@@ -69,6 +70,8 @@ export function SnapCard({
69
70
  validationErrorFallback?: ReactNode;
70
71
  /** Server-side action error message to display inline. */
71
72
  actionError?: string | null;
73
+ /** When true, renders without card frame (no border, background, or padding). */
74
+ plain?: boolean;
72
75
  }) {
73
76
  if (snap.version === SPEC_VERSION_2) {
74
77
  return (
@@ -82,6 +85,7 @@ export function SnapCard({
82
85
  onValidationError={onValidationError}
83
86
  validationErrorFallback={validationErrorFallback}
84
87
  actionError={actionError}
88
+ plain={plain}
85
89
  />
86
90
  );
87
91
  }
@@ -94,6 +98,7 @@ export function SnapCard({
94
98
  appearance={appearance}
95
99
  maxWidth={maxWidth}
96
100
  actionError={actionError}
101
+ plain={plain}
97
102
  />
98
103
  );
99
104
  }
@@ -1,8 +1,11 @@
1
1
  "use client";
2
2
 
3
+ import { useEffect, useRef, useState } from "react";
3
4
  import { SnapViewCore } from "../snap-view-core";
4
5
  import type { SnapPage, SnapActionHandlers } from "../index";
5
6
 
7
+ const SNAP_MAX_HEIGHT = 500;
8
+
6
9
  export function SnapViewV1({
7
10
  snap,
8
11
  handlers,
@@ -31,6 +34,7 @@ export function SnapCardV1({
31
34
  appearance = "dark",
32
35
  maxWidth = 480,
33
36
  actionError,
37
+ plain = false,
34
38
  }: {
35
39
  snap: SnapPage;
36
40
  handlers: SnapActionHandlers;
@@ -38,15 +42,121 @@ export function SnapCardV1({
38
42
  appearance?: "light" | "dark";
39
43
  maxWidth?: number;
40
44
  actionError?: string | null;
45
+ plain?: boolean;
41
46
  }) {
47
+ const isDark = appearance === "dark";
48
+ const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
49
+ const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
50
+ const toggleBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.05)";
51
+ const toggleBgHover = isDark
52
+ ? "rgba(255,255,255,0.1)"
53
+ : "rgba(0,0,0,0.08)";
54
+ const toggleText = isDark ? "rgba(255,255,255,0.82)" : "rgba(0,0,0,0.72)";
55
+ const contentRef = useRef<HTMLDivElement>(null);
56
+ const [isExpandable, setIsExpandable] = useState(false);
57
+ const [isExpanded, setIsExpanded] = useState(false);
58
+
59
+ useEffect(() => {
60
+ setIsExpanded(false);
61
+ }, [snap]);
62
+
63
+ useEffect(() => {
64
+ const node = contentRef.current;
65
+ if (!node) return;
66
+
67
+ const measure = () => {
68
+ setIsExpandable(node.scrollHeight > SNAP_MAX_HEIGHT + 1);
69
+ };
70
+
71
+ measure();
72
+
73
+ if (typeof ResizeObserver === "undefined") return;
74
+ const observer = new ResizeObserver(() => {
75
+ measure();
76
+ });
77
+ observer.observe(node);
78
+ return () => observer.disconnect();
79
+ }, [snap, plain]);
80
+
81
+ useEffect(() => {
82
+ if (!isExpandable) {
83
+ setIsExpanded(false);
84
+ }
85
+ }, [isExpandable]);
86
+
87
+ const isClipped = isExpandable && !isExpanded;
88
+
42
89
  return (
43
- <div style={{ position: "relative", width: "100%", maxWidth }}>
44
- <SnapViewV1
45
- snap={snap}
46
- handlers={handlers}
47
- loading={loading}
48
- appearance={appearance}
49
- />
90
+ <div
91
+ style={{
92
+ position: "relative",
93
+ width: "100%",
94
+ maxWidth,
95
+ overflow: "hidden",
96
+ ...(plain ? {} : {
97
+ borderRadius: 16,
98
+ border: `1px solid ${borderColor}`,
99
+ backgroundColor: surfaceBg,
100
+ }),
101
+ }}
102
+ >
103
+ <div
104
+ style={
105
+ isClipped
106
+ ? {
107
+ maxHeight: SNAP_MAX_HEIGHT,
108
+ overflow: "hidden",
109
+ }
110
+ : undefined
111
+ }
112
+ >
113
+ <div ref={contentRef} style={plain ? undefined : { padding: 16 }}>
114
+ <SnapViewV1
115
+ snap={snap}
116
+ handlers={handlers}
117
+ loading={loading}
118
+ appearance={appearance}
119
+ />
120
+ </div>
121
+ </div>
122
+ {isExpandable ? (
123
+ <div
124
+ style={{
125
+ display: "flex",
126
+ justifyContent: "center",
127
+ padding: plain ? "8px 0 0" : "10px 16px 12px",
128
+ ...(plain
129
+ ? {}
130
+ : { borderTop: `1px solid ${borderColor}` }),
131
+ }}
132
+ >
133
+ <button
134
+ type="button"
135
+ aria-expanded={isExpanded}
136
+ onClick={() => setIsExpanded((value) => !value)}
137
+ style={{
138
+ appearance: "none",
139
+ border: "none",
140
+ borderRadius: 9999,
141
+ backgroundColor: toggleBg,
142
+ color: toggleText,
143
+ padding: "6px 10px",
144
+ fontSize: 13,
145
+ lineHeight: "18px",
146
+ fontWeight: 600,
147
+ cursor: "pointer",
148
+ }}
149
+ onMouseEnter={(event) => {
150
+ event.currentTarget.style.backgroundColor = toggleBgHover;
151
+ }}
152
+ onMouseLeave={(event) => {
153
+ event.currentTarget.style.backgroundColor = toggleBg;
154
+ }}
155
+ >
156
+ {isExpanded ? "Show less" : "Show more"}
157
+ </button>
158
+ </div>
159
+ ) : null}
50
160
  {actionError && (
51
161
  <div
52
162
  style={{
@@ -95,6 +95,7 @@ export function SnapCardV2({
95
95
  onValidationError,
96
96
  validationErrorFallback,
97
97
  actionError,
98
+ plain = false,
98
99
  }: {
99
100
  snap: SnapPage;
100
101
  handlers: SnapActionHandlers;
@@ -105,9 +106,13 @@ export function SnapCardV2({
105
106
  onValidationError?: (result: ValidationResult) => void;
106
107
  validationErrorFallback?: ReactNode;
107
108
  actionError?: string | null;
109
+ plain?: boolean;
108
110
  }) {
109
111
  const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
110
- const bg = appearance === "dark" ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
112
+ const isDark = appearance === "dark";
113
+ const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
114
+ const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
115
+ const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
111
116
 
112
117
  return (
113
118
  <>
@@ -118,8 +123,14 @@ export function SnapCardV2({
118
123
  maxWidth,
119
124
  maxHeight,
120
125
  overflow: "hidden",
126
+ ...(plain ? {} : {
127
+ borderRadius: 16,
128
+ border: `1px solid ${borderColor}`,
129
+ backgroundColor: surfaceBg,
130
+ }),
121
131
  }}
122
132
  >
133
+ <div style={plain ? undefined : { padding: 16 }}>
123
134
  <SnapViewV2
124
135
  snap={snap}
125
136
  handlers={handlers}
@@ -128,6 +139,7 @@ export function SnapCardV2({
128
139
  onValidationError={onValidationError}
129
140
  validationErrorFallback={validationErrorFallback}
130
141
  />
142
+ </div>
131
143
  {showOverflowWarning && (
132
144
  <div
133
145
  style={{
@@ -77,7 +77,7 @@ export function SnapActionButton({
77
77
  }
78
78
 
79
79
  const styles = StyleSheet.create({
80
- outer: { flex: 1, minWidth: 0 },
80
+ outer: { minWidth: 0 },
81
81
  btn: {
82
82
  paddingHorizontal: 16,
83
83
  borderRadius: 10,
@@ -64,7 +64,7 @@ export function SnapBarChart({
64
64
  }
65
65
 
66
66
  const styles = StyleSheet.create({
67
- wrap: { flex: 1, width: "100%", gap: 8 },
67
+ wrap: { width: "100%", gap: 8 },
68
68
  row: { flexDirection: "row", alignItems: "center", gap: 8 },
69
69
  label: { width: 80, fontSize: 12, lineHeight: 16, textAlign: "right" },
70
70
  track: { flex: 1, height: 10, borderRadius: 9999, overflow: "hidden" },
@@ -29,7 +29,6 @@ export function SnapImage({
29
29
 
30
30
  const styles = StyleSheet.create({
31
31
  frame: {
32
- flex: 1,
33
32
  width: "100%",
34
33
  borderRadius: 8,
35
34
  overflow: "hidden",
@@ -26,7 +26,7 @@ export function SnapProgress({
26
26
  }
27
27
 
28
28
  const styles = StyleSheet.create({
29
- wrap: { flex: 1, width: "100%", gap: 4 },
29
+ wrap: { width: "100%", gap: 4 },
30
30
  label: { fontSize: 13, lineHeight: 18 },
31
31
  track: {
32
32
  height: 10,
@@ -46,6 +46,6 @@ export function SnapText({
46
46
  }
47
47
 
48
48
  const styles = StyleSheet.create({
49
- wrap: { flex: 1, width: "100%" },
49
+ wrap: { width: "100%" },
50
50
  base: {},
51
51
  });
@@ -30,6 +30,7 @@ export function SnapCard({
30
30
  onValidationError,
31
31
  validationErrorFallback,
32
32
  actionError,
33
+ plain = false,
33
34
  }: {
34
35
  snap: SnapPage;
35
36
  handlers: SnapActionHandlers;
@@ -46,6 +47,8 @@ export function SnapCard({
46
47
  validationErrorFallback?: ReactNode;
47
48
  /** Server-side action error message to display inline. */
48
49
  actionError?: string | null;
50
+ /** When true, renders without card frame (no border, background, or padding). */
51
+ plain?: boolean;
49
52
  }) {
50
53
  if (snap.version === SPEC_VERSION_2) {
51
54
  return (
@@ -60,6 +63,7 @@ export function SnapCard({
60
63
  onValidationError={onValidationError}
61
64
  validationErrorFallback={validationErrorFallback}
62
65
  actionError={actionError}
66
+ plain={plain}
63
67
  />
64
68
  );
65
69
  }
@@ -73,6 +77,7 @@ export function SnapCard({
73
77
  colors={colors}
74
78
  borderRadius={borderRadius}
75
79
  actionError={actionError}
80
+ plain={plain}
76
81
  />
77
82
  );
78
83
  }
@@ -21,9 +21,9 @@ export type SnapNativeColors = {
21
21
  const DEFAULT_LIGHT: SnapNativeColors = {
22
22
  bg: "#dfe3e8",
23
23
  surface: "#ffffff",
24
- text: "#111111",
25
- textSecondary: "#6b7280",
26
- border: "#E5E7EB",
24
+ text: "rgba(0,0,0,0.9)",
25
+ textSecondary: "rgba(0,0,0,0.5)",
26
+ border: "rgba(0,0,0,0.1)",
27
27
  inputBg: "rgba(0,0,0,0.06)",
28
28
  muted: "rgba(0,0,0,0.08)",
29
29
  mutedSubtle: "rgba(0,0,0,0.04)",
@@ -34,9 +34,9 @@ const DEFAULT_LIGHT: SnapNativeColors = {
34
34
  const DEFAULT_DARK: SnapNativeColors = {
35
35
  bg: "#111318",
36
36
  surface: "#1a1d24",
37
- text: "#fafafa",
38
- textSecondary: "#a1a1aa",
39
- border: "#2D2D44",
37
+ text: "rgba(255,255,255,0.9)",
38
+ textSecondary: "rgba(255,255,255,0.5)",
39
+ border: "rgba(255,255,255,0.1)",
40
40
  inputBg: "rgba(255,255,255,0.04)",
41
41
  muted: "rgba(255,255,255,0.06)",
42
42
  mutedSubtle: "rgba(255,255,255,0.03)",
@@ -1,9 +1,12 @@
1
- import { View, Text, StyleSheet } from "react-native";
1
+ import { useEffect, useState } from "react";
2
+ import { View, Text, StyleSheet, Pressable } from "react-native";
2
3
  import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
3
4
  import { SnapViewCoreInner } from "../snap-view-core";
4
5
  import type { SnapPage, SnapActionHandlers } from "../types";
5
6
 
6
- // ─── SnapViewV1 (no validation, no height limits) ────
7
+ const SNAP_MAX_HEIGHT = 500;
8
+
9
+ // ─── SnapViewV1 (no validation) ──────────────────────
7
10
 
8
11
  export function SnapViewV1Inner({
9
12
  snap,
@@ -39,7 +42,7 @@ export function SnapViewV1({
39
42
  );
40
43
  }
41
44
 
42
- // ─── SnapCardV1 (card frame, no height limits) ───────
45
+ // ─── SnapCardV1 (card frame with expandable clipping) ──
43
46
 
44
47
  function SnapCardV1Inner({
45
48
  snap,
@@ -48,6 +51,7 @@ function SnapCardV1Inner({
48
51
  borderRadius,
49
52
  actionError,
50
53
  appearance,
54
+ plain,
51
55
  }: {
52
56
  snap: SnapPage;
53
57
  handlers: SnapActionHandlers;
@@ -55,25 +59,87 @@ function SnapCardV1Inner({
55
59
  borderRadius: number;
56
60
  actionError?: string | null;
57
61
  appearance: "light" | "dark";
62
+ plain: boolean;
58
63
  }) {
59
64
  const { colors } = useSnapTheme();
65
+ const [contentHeight, setContentHeight] = useState(0);
66
+ const [isExpanded, setIsExpanded] = useState(false);
67
+
68
+ useEffect(() => {
69
+ setIsExpanded(false);
70
+ setContentHeight(0);
71
+ }, [snap]);
72
+
73
+ const isExpandable = contentHeight > SNAP_MAX_HEIGHT + 1;
74
+ const isClipped = isExpandable && !isExpanded;
60
75
 
61
76
  return (
62
77
  <>
63
78
  <View style={cardStyles.frameRing}>
64
79
  <View
65
80
  style={[
66
- cardStyles.card,
67
- {
81
+ plain ? undefined : cardStyles.card,
82
+ plain ? undefined : {
68
83
  borderRadius,
69
84
  borderColor: colors.border,
70
85
  backgroundColor: colors.surface,
71
86
  },
72
87
  ]}
73
88
  >
74
- <View style={cardStyles.body}>
75
- <SnapViewV1Inner snap={snap} handlers={handlers} loading={loading} />
89
+ <View
90
+ style={isClipped ? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" } : undefined}
91
+ >
92
+ <View
93
+ collapsable={false}
94
+ onLayout={(event) => {
95
+ const nextHeight = Math.round(event.nativeEvent.layout.height);
96
+ setContentHeight((currentHeight) =>
97
+ isClipped
98
+ ? Math.max(currentHeight, nextHeight)
99
+ : currentHeight === nextHeight
100
+ ? currentHeight
101
+ : nextHeight,
102
+ );
103
+ }}
104
+ style={plain ? undefined : cardStyles.body}
105
+ >
106
+ <SnapViewV1Inner
107
+ snap={snap}
108
+ handlers={handlers}
109
+ loading={loading}
110
+ />
111
+ </View>
76
112
  </View>
113
+ {isExpandable ? (
114
+ <View
115
+ style={[
116
+ cardStyles.expandRow,
117
+ plain
118
+ ? cardStyles.expandRowPlain
119
+ : { borderTopColor: colors.border },
120
+ ]}
121
+ >
122
+ <Pressable
123
+ style={({ pressed }) => [
124
+ cardStyles.expandButton,
125
+ {
126
+ backgroundColor: pressed
127
+ ? colors.mutedHover
128
+ : colors.muted,
129
+ },
130
+ ]}
131
+ onPress={() => {
132
+ setIsExpanded((value) => !value);
133
+ }}
134
+ >
135
+ <Text
136
+ style={[cardStyles.expandButtonText, { color: colors.text }]}
137
+ >
138
+ {isExpanded ? "Show less" : "Show more"}
139
+ </Text>
140
+ </Pressable>
141
+ </View>
142
+ ) : null}
77
143
  </View>
78
144
  </View>
79
145
  {actionError && (
@@ -103,6 +169,7 @@ export function SnapCardV1({
103
169
  colors,
104
170
  borderRadius = 16,
105
171
  actionError,
172
+ plain = false,
106
173
  }: {
107
174
  snap: SnapPage;
108
175
  handlers: SnapActionHandlers;
@@ -111,6 +178,7 @@ export function SnapCardV1({
111
178
  colors?: Partial<SnapNativeColors>;
112
179
  borderRadius?: number;
113
180
  actionError?: string | null;
181
+ plain?: boolean;
114
182
  }) {
115
183
  return (
116
184
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -121,6 +189,7 @@ export function SnapCardV1({
121
189
  borderRadius={borderRadius}
122
190
  actionError={actionError}
123
191
  appearance={appearance}
192
+ plain={plain}
124
193
  />
125
194
  </SnapThemeProvider>
126
195
  );
@@ -130,5 +199,31 @@ const cardStyles = StyleSheet.create({
130
199
  frameRing: { alignSelf: "stretch" },
131
200
  card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
132
201
  body: { paddingHorizontal: 16, paddingVertical: 16 },
202
+ expandRow: {
203
+ alignItems: "center",
204
+ paddingHorizontal: 16,
205
+ paddingTop: 10,
206
+ paddingBottom: 12,
207
+ borderTopWidth: StyleSheet.hairlineWidth,
208
+ },
209
+ expandRowPlain: {
210
+ paddingHorizontal: 0,
211
+ paddingTop: 8,
212
+ paddingBottom: 0,
213
+ borderTopWidth: 0,
214
+ },
215
+ expandButton: {
216
+ minWidth: 92,
217
+ alignItems: "center",
218
+ justifyContent: "center",
219
+ borderRadius: 9999,
220
+ paddingHorizontal: 10,
221
+ paddingVertical: 6,
222
+ },
223
+ expandButtonText: {
224
+ fontSize: 13,
225
+ lineHeight: 18,
226
+ fontWeight: "600",
227
+ },
133
228
  actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
134
229
  });
@@ -123,6 +123,7 @@ function SnapCardV2Inner({
123
123
  validationErrorFallback,
124
124
  actionError,
125
125
  appearance,
126
+ plain,
126
127
  }: {
127
128
  snap: SnapPage;
128
129
  handlers: SnapActionHandlers;
@@ -133,54 +134,62 @@ function SnapCardV2Inner({
133
134
  validationErrorFallback?: ReactNode;
134
135
  actionError?: string | null;
135
136
  appearance: "light" | "dark";
137
+ plain: boolean;
136
138
  }) {
137
139
  const { colors } = useSnapTheme();
138
- const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
140
+ const clipHeight = showOverflowWarning ? undefined : SNAP_MAX_HEIGHT;
141
+
142
+ const content = (
143
+ <SnapViewV2Inner
144
+ snap={snap}
145
+ handlers={handlers}
146
+ loading={loading}
147
+ onValidationError={onValidationError}
148
+ validationErrorFallback={validationErrorFallback}
149
+ />
150
+ );
151
+
152
+ if (plain) {
153
+ return content;
154
+ }
139
155
 
140
156
  return (
141
157
  <>
142
- <View style={cardStyles.frameRing}>
143
- <View
144
- style={[
145
- cardStyles.card,
146
- {
147
- borderRadius,
148
- ...(!showOverflowWarning && { maxHeight: SNAP_MAX_HEIGHT }),
149
- borderColor: colors.border,
150
- backgroundColor: colors.surface,
151
- },
152
- ]}
153
- >
154
- <View style={cardStyles.body}>
155
- <SnapViewV2Inner
156
- snap={snap}
157
- handlers={handlers}
158
- loading={loading}
159
- onValidationError={onValidationError}
160
- validationErrorFallback={validationErrorFallback}
161
- />
162
- </View>
163
- {showOverflowWarning && (
164
- <View style={cardStyles.warningOverlay}>
165
- <View style={cardStyles.warningLine} />
166
- <View style={cardStyles.warningLabel}>
167
- <Text style={cardStyles.warningLabelText}>{SNAP_MAX_HEIGHT}px</Text>
168
- </View>
169
- </View>
170
- )}
158
+ <View
159
+ style={{
160
+ borderRadius,
161
+ borderWidth: 1,
162
+ borderColor: colors.border,
163
+ backgroundColor: colors.surface,
164
+ maxHeight: clipHeight,
165
+ overflow: "hidden",
166
+ minHeight: 120,
167
+ }}
168
+ >
169
+ <View style={{ paddingHorizontal: 16, paddingVertical: 16 }}>
170
+ {content}
171
171
  </View>
172
+ {showOverflowWarning && (
173
+ <View style={{ position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, bottom: 0, zIndex: 10, pointerEvents: "none" }}>
174
+ <View style={{ height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" }} />
175
+ <View style={{ position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }}>
176
+ <Text style={{ fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }}>{SNAP_MAX_HEIGHT}px</Text>
177
+ </View>
178
+ <View style={{ flex: 1, backgroundColor: "rgba(255,50,50,0.15)" }} />
179
+ </View>
180
+ )}
172
181
  </View>
173
182
  {actionError && (
174
183
  <Text
175
- style={[
176
- cardStyles.actionError,
177
- {
178
- color:
179
- appearance === "dark"
180
- ? "rgba(255,100,100,0.9)"
181
- : "rgba(200,0,0,0.8)",
182
- },
183
- ]}
184
+ style={{
185
+ paddingHorizontal: 12,
186
+ paddingVertical: 8,
187
+ fontSize: 13,
188
+ color:
189
+ appearance === "dark"
190
+ ? "rgba(255,100,100,0.9)"
191
+ : "rgba(200,0,0,0.8)",
192
+ }}
184
193
  >
185
194
  {actionError}
186
195
  </Text>
@@ -200,6 +209,7 @@ export function SnapCardV2({
200
209
  onValidationError,
201
210
  validationErrorFallback,
202
211
  actionError,
212
+ plain = false,
203
213
  }: {
204
214
  snap: SnapPage;
205
215
  handlers: SnapActionHandlers;
@@ -211,6 +221,7 @@ export function SnapCardV2({
211
221
  onValidationError?: (result: ValidationResult) => void;
212
222
  validationErrorFallback?: ReactNode;
213
223
  actionError?: string | null;
224
+ plain?: boolean;
214
225
  }) {
215
226
  return (
216
227
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -224,6 +235,7 @@ export function SnapCardV2({
224
235
  validationErrorFallback={validationErrorFallback}
225
236
  actionError={actionError}
226
237
  appearance={appearance}
238
+ plain={plain}
227
239
  />
228
240
  </SnapThemeProvider>
229
241
  );
@@ -231,7 +243,7 @@ export function SnapCardV2({
231
243
 
232
244
  const cardStyles = StyleSheet.create({
233
245
  frameRing: { alignSelf: "stretch" },
234
- card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
246
+ card: { borderWidth: 1, minHeight: 120, overflow: "hidden" },
235
247
  body: { paddingHorizontal: 16, paddingVertical: 16 },
236
248
  actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
237
249
  warningOverlay: {
package/src/ui/catalog.ts CHANGED
@@ -117,10 +117,9 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
117
117
  params: z.object({ target: z.string() }),
118
118
  },
119
119
  open_url: {
120
- description: "Open target snap or external URL.",
120
+ description: "Open URL in browser.",
121
121
  params: z.object({
122
122
  target: z.string(),
123
- isSnap: z.boolean().optional(),
124
123
  }),
125
124
  },
126
125
  open_mini_app: {