@farcaster/snap 1.18.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 (54) hide show
  1. package/dist/react/components/slider.js +2 -1
  2. package/dist/react/index.d.ts +3 -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 +2 -2
  9. package/dist/react-native/components/snap-badge.js +2 -1
  10. package/dist/react-native/components/snap-bar-chart.js +3 -3
  11. package/dist/react-native/components/snap-cell-grid.js +1 -1
  12. package/dist/react-native/components/snap-image.js +0 -1
  13. package/dist/react-native/components/snap-input.js +2 -1
  14. package/dist/react-native/components/snap-item.js +4 -3
  15. package/dist/react-native/components/snap-progress.js +2 -2
  16. package/dist/react-native/components/snap-slider.js +6 -5
  17. package/dist/react-native/components/snap-switch.js +1 -0
  18. package/dist/react-native/components/snap-text.js +2 -2
  19. package/dist/react-native/components/snap-toggle-group.js +2 -1
  20. package/dist/react-native/index.d.ts +3 -1
  21. package/dist/react-native/index.js +3 -3
  22. package/dist/react-native/theme.js +16 -16
  23. package/dist/react-native/v1/snap-view.d.ts +2 -1
  24. package/dist/react-native/v1/snap-view.js +68 -11
  25. package/dist/react-native/v2/snap-view.d.ts +2 -1
  26. package/dist/react-native/v2/snap-view.js +25 -21
  27. package/dist/ui/catalog.d.ts +1 -1
  28. package/dist/ui/catalog.js +1 -2
  29. package/dist/ui/slider.d.ts +1 -0
  30. package/dist/ui/slider.js +2 -0
  31. package/llms.txt +1 -0
  32. package/package.json +1 -1
  33. package/src/react/components/slider.tsx +11 -1
  34. package/src/react/index.tsx +5 -0
  35. package/src/react/v1/snap-view.tsx +117 -7
  36. package/src/react/v2/snap-view.tsx +13 -1
  37. package/src/react-native/components/snap-action-button.tsx +2 -2
  38. package/src/react-native/components/snap-badge.tsx +2 -1
  39. package/src/react-native/components/snap-bar-chart.tsx +3 -3
  40. package/src/react-native/components/snap-cell-grid.tsx +1 -1
  41. package/src/react-native/components/snap-image.tsx +0 -1
  42. package/src/react-native/components/snap-input.tsx +2 -1
  43. package/src/react-native/components/snap-item.tsx +4 -3
  44. package/src/react-native/components/snap-progress.tsx +2 -2
  45. package/src/react-native/components/snap-slider.tsx +10 -7
  46. package/src/react-native/components/snap-switch.tsx +1 -0
  47. package/src/react-native/components/snap-text.tsx +2 -2
  48. package/src/react-native/components/snap-toggle-group.tsx +2 -1
  49. package/src/react-native/index.tsx +5 -0
  50. package/src/react-native/theme.tsx +16 -16
  51. package/src/react-native/v1/snap-view.tsx +102 -7
  52. package/src/react-native/v2/snap-view.tsx +52 -40
  53. package/src/ui/catalog.ts +1 -2
  54. package/src/ui/slider.ts +2 -0
@@ -11,6 +11,7 @@ export function SnapSlider({ element: { props }, }) {
11
11
  const max = Number(props.max ?? 100);
12
12
  const step = Number(props.step ?? 1);
13
13
  const label = props.label ? String(props.label) : undefined;
14
+ const showValue = props.showValue === true;
14
15
  const path = `/inputs/${name}`;
15
16
  const raw = get(path);
16
17
  const value = raw !== undefined
@@ -18,7 +19,7 @@ export function SnapSlider({ element: { props }, }) {
18
19
  : props.defaultValue !== undefined
19
20
  ? Number(props.defaultValue)
20
21
  : (min + max) / 2;
21
- return (_jsxs("div", { className: "flex w-full flex-col gap-1.5", children: [label && _jsx(Label, { style: { color: colors.text }, children: label }), _jsx("input", { type: "range", min: min, max: max, step: step, value: value, onChange: (e) => set(path, Number(e.target.value)), className: "w-full h-2.5 rounded-full appearance-none cursor-pointer", style: {
22
+ return (_jsxs("div", { className: "flex w-full flex-col gap-1.5", children: [label && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { style: { color: colors.text }, children: label }), showValue && (_jsx("span", { style: { color: colors.textMuted, fontSize: 13, lineHeight: "18px" }, children: Math.round(value) }))] })), _jsx("input", { type: "range", min: min, max: max, step: step, value: value, onChange: (e) => set(path, Number(e.target.value)), className: "w-full h-2.5 rounded-full appearance-none cursor-pointer", style: {
22
23
  backgroundColor: colors.muted,
23
24
  accentColor: colors.accent,
24
25
  } })] }));
@@ -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,
@@ -47,10 +47,10 @@ export function SnapActionButton({ element, emit, }) {
47
47
  const I = ICON_MAP[iconName];
48
48
  return _jsx(I, { size: 16, color: iconColor });
49
49
  })()
50
- : null, _jsx(Text, { style: { color: textColor, fontSize: 14, fontWeight: "600" }, children: label }), showExternalIcon ? (_jsx(ExternalLink, { size: 14, color: iconColor, style: { opacity: 0.6 } })) : null] }) }));
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,
@@ -10,7 +10,7 @@ export function SnapBadge({ element: { props }, }) {
10
10
  const iconName = props.icon ? String(props.icon) : undefined;
11
11
  const isAccent = !color || color === "accent";
12
12
  const resolvedColor = isAccent ? accentHex : hex(color);
13
- const isFilled = variant === "default";
13
+ const isFilled = variant !== "outline";
14
14
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
15
15
  return (_jsxs(View, { style: [
16
16
  styles.badge,
@@ -35,6 +35,7 @@ const styles = StyleSheet.create({
35
35
  },
36
36
  label: {
37
37
  fontSize: 12,
38
+ lineHeight: 16,
38
39
  fontWeight: "500",
39
40
  },
40
41
  });
@@ -30,10 +30,10 @@ 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
- label: { width: 80, fontSize: 12, textAlign: "right" },
35
+ label: { width: 80, fontSize: 12, lineHeight: 16, textAlign: "right" },
36
36
  track: { flex: 1, height: 10, borderRadius: 9999, overflow: "hidden" },
37
37
  fill: { height: "100%", borderRadius: 9999 },
38
- value: { width: 32, fontSize: 12, fontVariant: ["tabular-nums"] },
38
+ value: { width: 32, fontSize: 12, lineHeight: 16, fontVariant: ["tabular-nums"] },
39
39
  });
@@ -91,6 +91,6 @@ const styles = StyleSheet.create({
91
91
  alignItems: "center",
92
92
  justifyContent: "center",
93
93
  },
94
- cellText: { fontSize: 12, fontWeight: "600" },
94
+ cellText: { fontSize: 12, lineHeight: 16, fontWeight: "600" },
95
95
  selectionText: { fontSize: 11, fontFamily: "monospace", marginTop: 6 },
96
96
  });
@@ -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",
@@ -25,12 +25,13 @@ export function SnapInput({ element: { props }, }) {
25
25
  }
26
26
  const styles = StyleSheet.create({
27
27
  wrap: { width: "100%", gap: 4 },
28
- label: { fontSize: 13, fontWeight: "500" },
28
+ label: { fontSize: 13, lineHeight: 18, fontWeight: "500" },
29
29
  input: {
30
30
  borderWidth: 1,
31
31
  borderRadius: 8,
32
32
  paddingHorizontal: 12,
33
33
  paddingVertical: 10,
34
34
  fontSize: 14,
35
+ lineHeight: 18,
35
36
  },
36
37
  });
@@ -8,12 +8,11 @@ export function SnapItem({ element: { props }, children, }) {
8
8
  ? String(props.description)
9
9
  : undefined;
10
10
  const variant = String(props.variant ?? "default");
11
- const containerVariant = { paddingVertical: 8, paddingHorizontal: 10 };
12
- return (_jsxs(View, { style: [styles.container, containerVariant], children: [_jsxs(View, { style: styles.content, children: [_jsx(Text, { style: [styles.title, { color: colors.text }], children: title }), description ? (_jsx(Text, { style: [styles.description, { color: colors.textSecondary }], children: description })) : null] }), children ? (_jsx(View, { style: styles.actions, children: _jsx(View, { style: { flex: 0 }, children: children }) })) : null] }));
11
+ const containerVariant = { paddingVertical: 6, paddingHorizontal: 10 };
12
+ return (_jsxs(View, { style: [styles.container, containerVariant], children: [_jsxs(View, { style: styles.content, children: [title ? _jsx(Text, { style: [styles.title, { color: colors.text }], children: title }) : null, description ? (_jsx(Text, { style: [styles.description, { color: colors.textSecondary }], children: description })) : null] }), children ? (_jsx(View, { style: styles.actions, children: _jsx(View, { style: { flex: 0 }, children: children }) })) : null] }));
13
13
  }
14
14
  const styles = StyleSheet.create({
15
15
  container: {
16
- flex: 1,
17
16
  flexDirection: "row",
18
17
  alignItems: "center",
19
18
  },
@@ -22,10 +21,12 @@ const styles = StyleSheet.create({
22
21
  },
23
22
  title: {
24
23
  fontSize: 15,
24
+ lineHeight: 20,
25
25
  fontWeight: "500",
26
26
  },
27
27
  description: {
28
28
  fontSize: 13,
29
+ lineHeight: 18,
29
30
  marginTop: 1,
30
31
  },
31
32
  actions: {
@@ -12,8 +12,8 @@ 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 },
16
- label: { fontSize: 13 },
15
+ wrap: { width: "100%", gap: 4 },
16
+ label: { fontSize: 13, lineHeight: 18 },
17
17
  track: {
18
18
  height: 10,
19
19
  borderRadius: 9999,
@@ -20,23 +20,24 @@ export function SnapSlider({ element: { props }, }) {
20
20
  ? Math.min(max, Math.max(min, value))
21
21
  : fallback;
22
22
  const label = props.label != null ? String(props.label) : null;
23
+ const showValue = props.showValue === true;
23
24
  const minLabel = props.minLabel != null ? String(props.minLabel) : null;
24
25
  const maxLabel = props.maxLabel != null ? String(props.maxLabel) : null;
25
- return (_jsxs(View, { style: styles.wrap, children: [label ? (_jsxs(View, { style: styles.labelRow, children: [_jsx(Text, { style: [styles.label, { color: colors.text }], children: label }), _jsx(Text, { style: [styles.valueText, { color: colors.textSecondary }], children: String(Math.round(clamped)) })] })) : null, _jsx(Slider, { style: styles.slider, minimumValue: min, maximumValue: max, step: step > 0 ? step : 1, value: clamped, onValueChange: (v) => set(path, v), minimumTrackTintColor: accentHex, maximumTrackTintColor: colors.muted, thumbTintColor: accentHex }), minLabel != null || maxLabel != null ? (_jsxs(View, { style: styles.minMaxRow, children: [_jsx(Text, { style: [styles.minMax, { color: colors.textSecondary }], children: minLabel ?? String(min) }), _jsx(Text, { style: [styles.minMax, { color: colors.textSecondary }], children: maxLabel ?? String(max) })] })) : null] }));
26
+ return (_jsxs(View, { style: styles.wrap, children: [label ? (_jsxs(View, { style: styles.labelRow, children: [_jsx(Text, { style: [styles.label, { color: colors.text }], children: label }), showValue && (_jsx(Text, { style: [styles.valueText, { color: colors.textSecondary }], children: String(Math.round(clamped)) }))] })) : null, _jsx(Slider, { style: styles.slider, minimumValue: min, maximumValue: max, step: step > 0 ? step : 1, value: clamped, onValueChange: (v) => set(path, v), minimumTrackTintColor: accentHex, maximumTrackTintColor: colors.muted, thumbTintColor: accentHex }), minLabel != null || maxLabel != null ? (_jsxs(View, { style: styles.minMaxRow, children: [_jsx(Text, { style: [styles.minMax, { color: colors.textSecondary }], children: minLabel ?? String(min) }), _jsx(Text, { style: [styles.minMax, { color: colors.textSecondary }], children: maxLabel ?? String(max) })] })) : null] }));
26
27
  }
27
28
  const styles = StyleSheet.create({
28
- wrap: { width: "100%", gap: 6 },
29
+ wrap: { width: "100%", gap: 2 },
29
30
  labelRow: {
30
31
  flexDirection: "row",
31
32
  justifyContent: "space-between",
32
33
  alignItems: "center",
33
34
  },
34
- label: { fontSize: 13, fontWeight: "500", flex: 1 },
35
- valueText: { fontSize: 13 },
35
+ label: { fontSize: 13, lineHeight: 18, fontWeight: "500", flex: 1 },
36
+ valueText: { fontSize: 13, lineHeight: 18 },
36
37
  slider: { width: "100%", height: 40 },
37
38
  minMaxRow: {
38
39
  flexDirection: "row",
39
40
  justifyContent: "space-between",
40
41
  },
41
- minMax: { fontSize: 12 },
42
+ minMax: { fontSize: 12, lineHeight: 16 },
42
43
  });
@@ -24,6 +24,7 @@ const styles = StyleSheet.create({
24
24
  },
25
25
  label: {
26
26
  fontSize: 14,
27
+ lineHeight: 18,
27
28
  fontWeight: "400",
28
29
  flex: 1,
29
30
  },
@@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from "react-native";
3
3
  import { useSnapTheme } from "../theme.js";
4
4
  const SIZE_STYLES = {
5
5
  md: { fontSize: 16, lineHeight: 24 },
6
- sm: { fontSize: 13 },
6
+ sm: { fontSize: 13, lineHeight: 18 },
7
7
  };
8
8
  const WEIGHT_MAP = {
9
9
  bold: "700",
@@ -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
  });
@@ -69,7 +69,7 @@ export function SnapToggleGroup({ element: { props }, }) {
69
69
  }
70
70
  const styles = StyleSheet.create({
71
71
  wrap: { width: "100%", gap: 6 },
72
- label: { fontSize: 13, fontWeight: "500" },
72
+ label: { fontSize: 13, lineHeight: 18, fontWeight: "500" },
73
73
  group: {
74
74
  padding: 4,
75
75
  borderRadius: 8,
@@ -93,6 +93,7 @@ const styles = StyleSheet.create({
93
93
  },
94
94
  optionText: {
95
95
  fontSize: 13,
96
+ lineHeight: 18,
96
97
  fontWeight: "500",
97
98
  },
98
99
  });
@@ -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,26 +3,26 @@ 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",
9
- inputBg: "rgba(0,0,0,0.12)",
10
- muted: "rgba(0,0,0,0.12)",
11
- mutedSubtle: "rgba(0,0,0,0.06)",
12
- mutedHover: "rgba(0,0,0,0.10)",
13
- mutedSelected: "rgba(0,0,0,0.18)",
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
+ inputBg: "rgba(0,0,0,0.06)",
10
+ muted: "rgba(0,0,0,0.08)",
11
+ mutedSubtle: "rgba(0,0,0,0.04)",
12
+ mutedHover: "rgba(0,0,0,0.12)",
13
+ mutedSelected: "rgba(0,0,0,0.16)",
14
14
  };
15
15
  const DEFAULT_DARK = {
16
16
  bg: "#111318",
17
17
  surface: "#1a1d24",
18
- text: "#fafafa",
19
- textSecondary: "#a1a1aa",
20
- border: "#2D2D44",
21
- inputBg: "rgba(255,255,255,0.03)",
22
- muted: "rgba(255,255,255,0.03)",
23
- mutedSubtle: "rgba(255,255,255,0.02)",
24
- mutedHover: "rgba(255,255,255,0.04)",
25
- mutedSelected: "rgba(255,255,255,0.10)",
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
+ inputBg: "rgba(255,255,255,0.04)",
22
+ muted: "rgba(255,255,255,0.06)",
23
+ mutedSubtle: "rgba(255,255,255,0.03)",
24
+ mutedHover: "rgba(255,255,255,0.08)",
25
+ mutedSelected: "rgba(255,255,255,0.12)",
26
26
  };
27
27
  const SnapThemeContext = createContext({
28
28
  mode: "dark",
@@ -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;