@farcaster/snap 2.0.0 → 2.0.2

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 (194) hide show
  1. package/dist/colors.d.ts +4 -4
  2. package/dist/colors.js +20 -20
  3. package/dist/constants.d.ts +17 -1
  4. package/dist/constants.js +19 -1
  5. package/dist/index.d.ts +4 -6
  6. package/dist/index.js +2 -4
  7. package/dist/react/accent-context.d.ts +3 -1
  8. package/dist/react/accent-context.js +7 -4
  9. package/dist/react/catalog-renderer.js +4 -0
  10. package/dist/react/components/action-button.d.ts +2 -1
  11. package/dist/react/components/action-button.js +35 -13
  12. package/dist/react/components/badge.js +8 -8
  13. package/dist/react/components/bar-chart.d.ts +5 -0
  14. package/dist/react/components/bar-chart.js +26 -0
  15. package/dist/react/components/cell-grid.d.ts +5 -0
  16. package/dist/react/components/cell-grid.js +87 -0
  17. package/dist/react/components/icon.js +4 -10
  18. package/dist/react/components/input.js +12 -6
  19. package/dist/react/components/item-group.js +3 -1
  20. package/dist/react/components/item.d.ts +3 -3
  21. package/dist/react/components/item.js +4 -3
  22. package/dist/react/components/progress.js +3 -3
  23. package/dist/react/components/separator.js +3 -1
  24. package/dist/react/components/slider.js +15 -10
  25. package/dist/react/components/switch.js +10 -12
  26. package/dist/react/components/text.js +6 -14
  27. package/dist/react/components/toggle-group.js +20 -6
  28. package/dist/react/hooks/use-snap-colors.d.ts +38 -0
  29. package/dist/react/hooks/use-snap-colors.js +81 -0
  30. package/dist/react/index.d.ts +13 -1
  31. package/dist/react/index.js +9 -188
  32. package/dist/react/snap-view-core.d.ts +11 -0
  33. package/dist/react/snap-view-core.js +227 -0
  34. package/dist/react/v1/snap-view.d.ts +16 -0
  35. package/dist/react/v1/snap-view.js +90 -0
  36. package/dist/react/v2/snap-view.d.ts +23 -0
  37. package/dist/react/v2/snap-view.js +91 -0
  38. package/dist/react-native/catalog-renderer.d.ts +5 -0
  39. package/dist/react-native/catalog-renderer.js +40 -0
  40. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  41. package/dist/react-native/components/snap-action-button.js +69 -0
  42. package/dist/react-native/components/snap-badge.d.ts +2 -0
  43. package/dist/react-native/components/snap-badge.js +41 -0
  44. package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
  45. package/dist/react-native/components/snap-bar-chart.js +39 -0
  46. package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
  47. package/dist/react-native/components/snap-cell-grid.js +94 -0
  48. package/dist/react-native/components/snap-icon.d.ts +5 -0
  49. package/dist/react-native/components/snap-icon.js +56 -0
  50. package/dist/react-native/components/snap-image.d.ts +2 -0
  51. package/dist/react-native/components/snap-image.js +23 -0
  52. package/dist/react-native/components/snap-input.d.ts +2 -0
  53. package/dist/react-native/components/snap-input.js +37 -0
  54. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  55. package/dist/react-native/components/snap-item-group.js +23 -0
  56. package/dist/react-native/components/snap-item.d.ts +5 -0
  57. package/dist/react-native/components/snap-item.js +42 -0
  58. package/dist/react-native/components/snap-progress.d.ts +2 -0
  59. package/dist/react-native/components/snap-progress.js +26 -0
  60. package/dist/react-native/components/snap-separator.d.ts +2 -0
  61. package/dist/react-native/components/snap-separator.js +23 -0
  62. package/dist/react-native/components/snap-slider.d.ts +2 -0
  63. package/dist/react-native/components/snap-slider.js +43 -0
  64. package/dist/react-native/components/snap-stack.d.ts +5 -0
  65. package/dist/react-native/components/snap-stack.js +49 -0
  66. package/dist/react-native/components/snap-switch.d.ts +2 -0
  67. package/dist/react-native/components/snap-switch.js +31 -0
  68. package/dist/react-native/components/snap-text.d.ts +2 -0
  69. package/dist/react-native/components/snap-text.js +35 -0
  70. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  71. package/dist/react-native/components/snap-toggle-group.js +99 -0
  72. package/dist/react-native/confetti-overlay.d.ts +1 -0
  73. package/dist/react-native/confetti-overlay.js +106 -0
  74. package/dist/react-native/index.d.ts +28 -0
  75. package/dist/react-native/index.js +15 -0
  76. package/dist/react-native/snap-view-core.d.ts +11 -0
  77. package/dist/react-native/snap-view-core.js +156 -0
  78. package/dist/react-native/theme.d.ts +27 -0
  79. package/dist/react-native/theme.js +43 -0
  80. package/dist/react-native/types.d.ts +42 -0
  81. package/dist/react-native/types.js +1 -0
  82. package/dist/react-native/use-snap-palette.d.ts +13 -0
  83. package/dist/react-native/use-snap-palette.js +48 -0
  84. package/dist/react-native/v1/snap-view.d.ts +24 -0
  85. package/dist/react-native/v1/snap-view.js +96 -0
  86. package/dist/react-native/v2/snap-view.d.ts +33 -0
  87. package/dist/react-native/v2/snap-view.js +114 -0
  88. package/dist/schemas.d.ts +100 -13
  89. package/dist/schemas.js +28 -10
  90. package/dist/server/parseRequest.d.ts +10 -0
  91. package/dist/server/parseRequest.js +48 -7
  92. package/dist/server/verify.d.ts +1 -0
  93. package/dist/server/verify.js +1 -0
  94. package/dist/ui/badge.d.ts +7 -2
  95. package/dist/ui/badge.js +2 -0
  96. package/dist/ui/bar-chart.d.ts +30 -0
  97. package/dist/ui/bar-chart.js +30 -0
  98. package/dist/ui/button.d.ts +4 -6
  99. package/dist/ui/button.js +1 -1
  100. package/dist/ui/catalog.d.ts +90 -16
  101. package/dist/ui/catalog.js +17 -3
  102. package/dist/ui/cell-grid.d.ts +34 -0
  103. package/dist/ui/cell-grid.js +39 -0
  104. package/dist/ui/icon.d.ts +2 -2
  105. package/dist/ui/image.d.ts +1 -2
  106. package/dist/ui/image.js +1 -1
  107. package/dist/ui/index.d.ts +4 -0
  108. package/dist/ui/index.js +2 -0
  109. package/dist/ui/item.d.ts +1 -3
  110. package/dist/ui/item.js +1 -1
  111. package/dist/ui/schema.d.ts +6 -2
  112. package/dist/ui/schema.js +2 -2
  113. package/dist/ui/slider.d.ts +1 -0
  114. package/dist/ui/slider.js +2 -0
  115. package/dist/ui/text.d.ts +2 -4
  116. package/dist/ui/text.js +2 -2
  117. package/dist/validator.d.ts +3 -2
  118. package/dist/validator.js +203 -2
  119. package/llms.txt +199 -0
  120. package/package.json +9 -3
  121. package/src/colors.ts +20 -20
  122. package/src/constants.ts +23 -1
  123. package/src/index.ts +16 -13
  124. package/src/react/accent-context.tsx +13 -6
  125. package/src/react/catalog-renderer.tsx +4 -0
  126. package/src/react/components/action-button.tsx +50 -20
  127. package/src/react/components/badge.tsx +14 -18
  128. package/src/react/components/bar-chart.tsx +69 -0
  129. package/src/react/components/cell-grid.tsx +128 -0
  130. package/src/react/components/icon.tsx +5 -18
  131. package/src/react/components/input.tsx +20 -9
  132. package/src/react/components/item-group.tsx +4 -1
  133. package/src/react/components/item.tsx +13 -10
  134. package/src/react/components/progress.tsx +12 -7
  135. package/src/react/components/separator.tsx +8 -1
  136. package/src/react/components/slider.tsx +28 -15
  137. package/src/react/components/switch.tsx +12 -16
  138. package/src/react/components/text.tsx +14 -23
  139. package/src/react/components/toggle-group.tsx +26 -9
  140. package/src/react/hooks/use-snap-colors.ts +128 -0
  141. package/src/react/index.tsx +49 -265
  142. package/src/react/snap-view-core.tsx +343 -0
  143. package/src/react/v1/snap-view.tsx +176 -0
  144. package/src/react/v2/snap-view.tsx +199 -0
  145. package/src/react-native/catalog-renderer.tsx +41 -0
  146. package/src/react-native/components/snap-action-button.tsx +96 -0
  147. package/src/react-native/components/snap-badge.tsx +60 -0
  148. package/src/react-native/components/snap-bar-chart.tsx +73 -0
  149. package/src/react-native/components/snap-cell-grid.tsx +150 -0
  150. package/src/react-native/components/snap-icon.tsx +102 -0
  151. package/src/react-native/components/snap-image.tsx +37 -0
  152. package/src/react-native/components/snap-input.tsx +58 -0
  153. package/src/react-native/components/snap-item-group.tsx +43 -0
  154. package/src/react-native/components/snap-item.tsx +66 -0
  155. package/src/react-native/components/snap-progress.tsx +40 -0
  156. package/src/react-native/components/snap-separator.tsx +32 -0
  157. package/src/react-native/components/snap-slider.tsx +85 -0
  158. package/src/react-native/components/snap-stack.tsx +66 -0
  159. package/src/react-native/components/snap-switch.tsx +46 -0
  160. package/src/react-native/components/snap-text.tsx +51 -0
  161. package/src/react-native/components/snap-toggle-group.tsx +127 -0
  162. package/src/react-native/confetti-overlay.tsx +134 -0
  163. package/src/react-native/index.tsx +83 -0
  164. package/src/react-native/snap-view-core.tsx +212 -0
  165. package/src/react-native/theme.tsx +85 -0
  166. package/src/react-native/types.ts +38 -0
  167. package/src/react-native/use-snap-palette.ts +64 -0
  168. package/src/react-native/v1/snap-view.tsx +229 -0
  169. package/src/react-native/v2/snap-view.tsx +283 -0
  170. package/src/schemas.ts +68 -17
  171. package/src/server/parseRequest.ts +68 -9
  172. package/src/server/verify.ts +2 -0
  173. package/src/ui/README.md +8 -8
  174. package/src/ui/badge.ts +2 -0
  175. package/src/ui/bar-chart.ts +38 -0
  176. package/src/ui/button.ts +1 -1
  177. package/src/ui/catalog.ts +19 -3
  178. package/src/ui/cell-grid.ts +49 -0
  179. package/src/ui/image.ts +1 -1
  180. package/src/ui/index.ts +6 -0
  181. package/src/ui/item.ts +1 -1
  182. package/src/ui/schema.ts +2 -2
  183. package/src/ui/slider.ts +2 -0
  184. package/src/ui/text.ts +2 -2
  185. package/src/validator.ts +251 -2
  186. package/dist/dataStore.d.ts +0 -12
  187. package/dist/dataStore.js +0 -35
  188. package/dist/middleware.d.ts +0 -3
  189. package/dist/middleware.js +0 -3
  190. package/dist/react/hooks/use-snap-accent.d.ts +0 -13
  191. package/dist/react/hooks/use-snap-accent.js +0 -32
  192. package/src/dataStore.ts +0 -62
  193. package/src/middleware.ts +0 -7
  194. package/src/react/hooks/use-snap-accent.ts +0 -45
@@ -0,0 +1,227 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { snapJsonRenderCatalog } from "../ui/index.js";
4
+ import { SnapCatalogView } from "./catalog-renderer.js";
5
+ import { SnapPreviewAccentProvider } from "./accent-context.js";
6
+ import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex.js";
7
+ import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css.js";
8
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
9
+ // ─── Internal helpers ──────────────────────────────────
10
+ export function applyStatePaths(model, changes) {
11
+ const entries = Array.isArray(changes)
12
+ ? changes.map((c) => [c.path, c.value])
13
+ : Object.entries(changes);
14
+ for (const [path, value] of entries) {
15
+ const trimmed = path.startsWith("/") ? path : `/${path}`;
16
+ const parts = trimmed.split("/").filter(Boolean);
17
+ if (parts.length < 2)
18
+ continue;
19
+ const [top, ...rest] = parts;
20
+ if (top === "inputs") {
21
+ if (typeof model.inputs !== "object" || model.inputs === null) {
22
+ model.inputs = {};
23
+ }
24
+ const inputs = model.inputs;
25
+ if (rest.length === 1) {
26
+ inputs[rest[0]] = value;
27
+ }
28
+ continue;
29
+ }
30
+ if (top === "theme") {
31
+ if (typeof model.theme !== "object" || model.theme === null) {
32
+ model.theme = {};
33
+ }
34
+ const theme = model.theme;
35
+ if (rest.length === 1) {
36
+ theme[rest[0]] = value;
37
+ }
38
+ }
39
+ }
40
+ }
41
+ const CONFETTI_COLORS = [
42
+ "#907AA9",
43
+ "#EC4899",
44
+ "#3B82F6",
45
+ "#10B981",
46
+ "#F59E0B",
47
+ "#EF4444",
48
+ "#06B6D4",
49
+ ];
50
+ function ConfettiOverlay() {
51
+ const pieces = useMemo(() => Array.from({ length: 80 }, (_, i) => ({
52
+ id: i,
53
+ left: Math.random() * 100,
54
+ delay: Math.random() * 1.2,
55
+ duration: 2.5 + Math.random() * 2,
56
+ color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
57
+ size: 6 + Math.random() * 8,
58
+ rotation: Math.random() * 360,
59
+ })), []);
60
+ return (_jsxs("div", { style: {
61
+ position: "absolute",
62
+ inset: 0,
63
+ overflow: "hidden",
64
+ pointerEvents: "none",
65
+ zIndex: 20,
66
+ }, children: [pieces.map(({ id, left, delay, duration, color, size, rotation }) => (_jsx("div", { style: {
67
+ position: "absolute",
68
+ left: `${left}%`,
69
+ top: -20,
70
+ width: size,
71
+ height: size * 0.6,
72
+ backgroundColor: color,
73
+ borderRadius: 2,
74
+ transform: `rotate(${rotation}deg)`,
75
+ animation: `confettiFall ${duration}s ease-in ${delay}s forwards`,
76
+ } }, id))), _jsx("style", { children: `@keyframes confettiFall{0%{top:-20px;opacity:1;transform:rotate(0deg) translateX(0)}50%{opacity:1}100%{top:110%;opacity:0;transform:rotate(720deg) translateX(${Math.random() > 0.5 ? "" : "-"}40px)}}` })] }));
77
+ }
78
+ function SnapLoadingOverlay({ appearance, accentHex, active, }) {
79
+ const isDark = appearance === "dark";
80
+ const tint = isDark ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.2)";
81
+ const trackColor = isDark
82
+ ? "rgba(255, 255, 255, 0.12)"
83
+ : "rgba(15, 23, 42, 0.1)";
84
+ return (_jsxs("div", { style: {
85
+ position: "absolute",
86
+ inset: 0,
87
+ display: "flex",
88
+ alignItems: "center",
89
+ justifyContent: "center",
90
+ zIndex: 10,
91
+ background: tint,
92
+ backdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
93
+ WebkitBackdropFilter: active
94
+ ? "blur(10px) saturate(1.05)"
95
+ : "none",
96
+ opacity: active ? 1 : 0,
97
+ pointerEvents: active ? "auto" : "none",
98
+ transition: "opacity 0.28s ease, backdrop-filter 0.28s ease",
99
+ }, "aria-hidden": !active, "aria-busy": active ? true : undefined, "aria-live": active ? "polite" : undefined, "aria-label": active ? "Loading" : undefined, children: [_jsx("div", { "data-snap-loading-spinner": true, style: {
100
+ width: 30,
101
+ height: 30,
102
+ borderRadius: "50%",
103
+ border: `2.5px solid ${trackColor}`,
104
+ borderTopColor: accentHex,
105
+ opacity: 0.88,
106
+ animation: "snapViewSpin 0.75s linear infinite",
107
+ flexShrink: 0,
108
+ } }), _jsx("style", { children: `
109
+ @keyframes snapViewSpin {
110
+ to { transform: rotate(360deg); }
111
+ }
112
+ @media (prefers-reduced-motion: reduce) {
113
+ [data-snap-loading-spinner] {
114
+ animation: none;
115
+ border-top-color: ${accentHex};
116
+ opacity: 0.75;
117
+ }
118
+ }
119
+ ` })] }));
120
+ }
121
+ const PALETTE = [
122
+ "gray",
123
+ "blue",
124
+ "red",
125
+ "amber",
126
+ "green",
127
+ "teal",
128
+ "purple",
129
+ "pink",
130
+ ];
131
+ // ─── SnapViewCore ────────────────────────────────────
132
+ // Shared rendering logic used by both v1 and v2.
133
+ export function SnapViewCore({ snap, handlers, loading = false, appearance = "dark", }) {
134
+ const spec = snap.ui;
135
+ const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
136
+ const stateRef = useRef(initialState);
137
+ useEffect(() => {
138
+ stateRef.current = {
139
+ inputs: {
140
+ ...(initialState.inputs ?? {}),
141
+ },
142
+ theme: {
143
+ ...(initialState.theme ?? {}),
144
+ },
145
+ };
146
+ }, [initialState]);
147
+ useEffect(() => {
148
+ const catalogResult = snapJsonRenderCatalog.validate(spec);
149
+ if (!catalogResult.success) {
150
+ // eslint-disable-next-line no-console
151
+ console.warn("[Snap] catalog validation issues:", catalogResult.error);
152
+ }
153
+ }, [spec]);
154
+ const [pageKey, setPageKey] = useState(0);
155
+ useEffect(() => {
156
+ setPageKey((k) => k + 1);
157
+ }, [spec]);
158
+ const showConfetti = snap.effects?.includes("confetti");
159
+ const accentName = snap.theme?.accent ?? "purple";
160
+ const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
161
+ const previewSurfaceStyle = useMemo(() => {
162
+ const vars = {};
163
+ for (const c of PALETTE)
164
+ vars[`--snap-color-${c}`] = resolveSnapPaletteHex(c, appearance);
165
+ return {
166
+ ...snapPreviewPrimaryCssProperties(accentName, appearance),
167
+ ...vars,
168
+ };
169
+ }, [accentName, appearance]);
170
+ const handleAction = useCallback((name, params) => {
171
+ const inputs = (stateRef.current.inputs ?? {});
172
+ const p = (params ?? {});
173
+ switch (name) {
174
+ case "submit":
175
+ handlers.submit(String(p.target ?? ""), inputs);
176
+ break;
177
+ case "open_url":
178
+ handlers.open_url(String(p.target ?? ""));
179
+ break;
180
+ case "open_snap":
181
+ handlers.open_snap(String(p.target ?? ""));
182
+ break;
183
+ case "open_mini_app":
184
+ handlers.open_mini_app(String(p.target ?? ""));
185
+ break;
186
+ case "view_cast":
187
+ handlers.view_cast({ hash: String(p.hash ?? "") });
188
+ break;
189
+ case "view_profile":
190
+ handlers.view_profile({ fid: Number(p.fid ?? 0) });
191
+ break;
192
+ case "compose_cast":
193
+ handlers.compose_cast({
194
+ text: p.text ? String(p.text) : undefined,
195
+ channelKey: p.channelKey ? String(p.channelKey) : undefined,
196
+ embeds: Array.isArray(p.embeds)
197
+ ? p.embeds
198
+ : undefined,
199
+ });
200
+ break;
201
+ case "view_token":
202
+ handlers.view_token({ token: String(p.token ?? "") });
203
+ break;
204
+ case "send_token":
205
+ handlers.send_token({
206
+ token: String(p.token ?? ""),
207
+ amount: p.amount ? String(p.amount) : undefined,
208
+ recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
209
+ recipientAddress: p.recipientAddress
210
+ ? String(p.recipientAddress)
211
+ : undefined,
212
+ });
213
+ break;
214
+ case "swap_token":
215
+ handlers.swap_token({
216
+ sellToken: p.sellToken ? String(p.sellToken) : undefined,
217
+ buyToken: p.buyToken ? String(p.buyToken) : undefined,
218
+ });
219
+ break;
220
+ default:
221
+ break;
222
+ }
223
+ }, [handlers]);
224
+ return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}), _jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading }), _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
225
+ applyStatePaths(stateRef.current, changes);
226
+ }, onAction: handleAction }, pageKey) }) })] }));
227
+ }
@@ -0,0 +1,16 @@
1
+ import type { SnapPage, SnapActionHandlers } from "../index.js";
2
+ export declare function SnapViewV1({ snap, handlers, loading, appearance, }: {
3
+ snap: SnapPage;
4
+ handlers: SnapActionHandlers;
5
+ loading?: boolean;
6
+ appearance?: "light" | "dark";
7
+ }): import("react/jsx-runtime").JSX.Element;
8
+ export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, actionError, plain, }: {
9
+ snap: SnapPage;
10
+ handlers: SnapActionHandlers;
11
+ loading?: boolean;
12
+ appearance?: "light" | "dark";
13
+ maxWidth?: number;
14
+ actionError?: string | null;
15
+ plain?: boolean;
16
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,90 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { SnapViewCore } from "../snap-view-core.js";
5
+ const SNAP_MAX_HEIGHT = 500;
6
+ export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", }) {
7
+ return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
8
+ }
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: {
84
+ padding: "8px 12px",
85
+ fontSize: 13,
86
+ color: appearance === "dark"
87
+ ? "rgba(255,100,100,0.9)"
88
+ : "rgba(200,0,0,0.8)",
89
+ }, children: actionError }))] }));
90
+ }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ValidationResult } from "../../validator.js";
3
+ import type { SnapPage, SnapActionHandlers } from "../index.js";
4
+ export declare function SnapViewV2({ snap, handlers, loading, appearance, onValidationError, validationErrorFallback, }: {
5
+ snap: SnapPage;
6
+ handlers: SnapActionHandlers;
7
+ loading?: boolean;
8
+ appearance?: "light" | "dark";
9
+ onValidationError?: (result: ValidationResult) => void;
10
+ validationErrorFallback?: ReactNode;
11
+ }): import("react/jsx-runtime").JSX.Element | null;
12
+ export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, }: {
13
+ snap: SnapPage;
14
+ handlers: SnapActionHandlers;
15
+ loading?: boolean;
16
+ appearance?: "light" | "dark";
17
+ maxWidth?: number;
18
+ showOverflowWarning?: boolean;
19
+ onValidationError?: (result: ValidationResult) => void;
20
+ validationErrorFallback?: ReactNode;
21
+ actionError?: string | null;
22
+ plain?: boolean;
23
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,91 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo } from "react";
4
+ import { validateSnapResponse } from "../../validator.js";
5
+ import { SnapViewCore } from "../snap-view-core.js";
6
+ const SNAP_MAX_HEIGHT = 500;
7
+ const SNAP_WARNING_HEIGHT = 700;
8
+ // ─── Default validation error fallback ────────────────
9
+ function SnapValidationFallback({ appearance, message, }) {
10
+ const isDark = appearance === "dark";
11
+ return (_jsx("div", { style: {
12
+ width: "100%",
13
+ padding: 16,
14
+ display: "flex",
15
+ alignItems: "center",
16
+ justifyContent: "center",
17
+ color: isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.4)",
18
+ fontSize: 14,
19
+ }, children: _jsx("span", { children: message ? `Unable to render snap: ${message}` : "Unable to render snap" }) }));
20
+ }
21
+ // ─── SnapViewV2 ──────────────────────────────────────
22
+ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", onValidationError, validationErrorFallback, }) {
23
+ const validation = useMemo(() => validateSnapResponse(snap), [snap]);
24
+ const valid = validation.valid;
25
+ const validationMessage = validation.issues[0]?.message;
26
+ useEffect(() => {
27
+ if (!valid) {
28
+ if (onValidationError) {
29
+ onValidationError(validation);
30
+ }
31
+ else {
32
+ // eslint-disable-next-line no-console
33
+ console.warn("[Snap] validation issues:", validation.issues);
34
+ }
35
+ }
36
+ }, [valid, validation, onValidationError]);
37
+ if (!valid) {
38
+ if (validationErrorFallback === null)
39
+ return null;
40
+ return _jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { appearance: appearance, message: validationMessage }) });
41
+ }
42
+ return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
43
+ }
44
+ // ─── SnapCardV2 ──────────────────────────────────────
45
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, }) {
46
+ const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
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)";
51
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
52
+ position: "relative",
53
+ width: "100%",
54
+ maxWidth,
55
+ maxHeight,
56
+ overflow: "hidden",
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: {
63
+ position: "absolute",
64
+ top: SNAP_MAX_HEIGHT,
65
+ left: 0,
66
+ right: 0,
67
+ bottom: 0,
68
+ pointerEvents: "none",
69
+ zIndex: 10,
70
+ }, children: [_jsx("div", { style: { borderTop: "1px dashed rgba(255,100,100,0.6)", position: "relative" }, children: _jsxs("span", { style: {
71
+ position: "absolute",
72
+ top: -10,
73
+ right: 0,
74
+ fontSize: 10,
75
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
76
+ color: "rgba(255,100,100,0.7)",
77
+ background: bg,
78
+ padding: "1px 4px",
79
+ borderRadius: 3,
80
+ }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx("div", { style: {
81
+ height: "100%",
82
+ background: "repeating-linear-gradient(-45deg, transparent, transparent 8px, rgba(255,100,100,0.06) 8px, rgba(255,100,100,0.06) 16px)",
83
+ } })] }))] }), actionError && (_jsx("div", { style: {
84
+ maxWidth,
85
+ padding: "8px 12px",
86
+ fontSize: 13,
87
+ color: appearance === "dark"
88
+ ? "rgba(255,100,100,0.9)"
89
+ : "rgba(200,0,0,0.8)",
90
+ }, children: actionError }))] }));
91
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Maps snap json-render catalog types to React Native primitives.
3
+ * Keys match the snap wire-format `type` strings exactly (snake_case).
4
+ */
5
+ export declare const SnapCatalogView: ComponentType<import("@json-render/react-native").CreateRendererProps>;
@@ -0,0 +1,40 @@
1
+ import { createRenderer } from "@json-render/react-native";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapActionButton } from "./components/snap-action-button.js";
4
+ import { SnapBadge } from "./components/snap-badge.js";
5
+ import { SnapIcon } from "./components/snap-icon.js";
6
+ import { SnapImage } from "./components/snap-image.js";
7
+ import { SnapInput } from "./components/snap-input.js";
8
+ import { SnapItem } from "./components/snap-item.js";
9
+ import { SnapItemGroup } from "./components/snap-item-group.js";
10
+ import { SnapProgress } from "./components/snap-progress.js";
11
+ import { SnapSeparator } from "./components/snap-separator.js";
12
+ import { SnapSlider } from "./components/snap-slider.js";
13
+ import { SnapStack } from "./components/snap-stack.js";
14
+ import { SnapSwitch } from "./components/snap-switch.js";
15
+ import { SnapText } from "./components/snap-text.js";
16
+ import { SnapToggleGroup } from "./components/snap-toggle-group.js";
17
+ import { SnapBarChart } from "./components/snap-bar-chart.js";
18
+ import { SnapCellGrid } from "./components/snap-cell-grid.js";
19
+ /**
20
+ * Maps snap json-render catalog types to React Native primitives.
21
+ * Keys match the snap wire-format `type` strings exactly (snake_case).
22
+ */
23
+ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
24
+ badge: SnapBadge,
25
+ button: SnapActionButton,
26
+ icon: SnapIcon,
27
+ image: SnapImage,
28
+ input: SnapInput,
29
+ item: SnapItem,
30
+ item_group: SnapItemGroup,
31
+ progress: SnapProgress,
32
+ separator: SnapSeparator,
33
+ slider: SnapSlider,
34
+ stack: SnapStack,
35
+ switch: SnapSwitch,
36
+ text: SnapText,
37
+ toggle_group: SnapToggleGroup,
38
+ bar_chart: SnapBarChart,
39
+ cell_grid: SnapCellGrid,
40
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapActionButton({ element, emit, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { ExternalLink } from "lucide-react-native";
4
+ import { useSnapPalette } from "../use-snap-palette.js";
5
+ import { useSnapTheme } from "../theme.js";
6
+ import { ICON_MAP } from "./snap-icon.js";
7
+ function isExternalLinkAction(on) {
8
+ if (!on)
9
+ return false;
10
+ const press = on.press;
11
+ if (!press)
12
+ return false;
13
+ return press.action === "open_url";
14
+ }
15
+ export function SnapActionButton({ element, emit, }) {
16
+ const { accentHex } = useSnapPalette();
17
+ const { colors } = useSnapTheme();
18
+ const { props } = element;
19
+ const label = String(props.label ?? "Action");
20
+ const variant = String(props.variant ?? "secondary");
21
+ const isPrimary = variant === "primary";
22
+ const iconName = props.icon ? String(props.icon) : undefined;
23
+ const textColor = isPrimary ? "#fff" : colors.text;
24
+ const iconColor = isPrimary ? "#fff" : colors.text;
25
+ const on = element.on;
26
+ const showExternalIcon = isExternalLinkAction(on);
27
+ return (_jsx(View, { style: styles.outer, children: _jsxs(Pressable, { style: ({ pressed }) => [
28
+ styles.btn,
29
+ isPrimary ? styles.btnDefault : styles.btnOther,
30
+ isPrimary
31
+ ? { backgroundColor: pressed ? accentHex + "DD" : accentHex }
32
+ : { backgroundColor: pressed ? colors.mutedHover : colors.muted },
33
+ pressed && styles.pressed,
34
+ ], onPress: () => {
35
+ void (async () => {
36
+ try {
37
+ await emit("press");
38
+ }
39
+ catch (err) {
40
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
41
+ console.error("[snap] action failed", err);
42
+ }
43
+ }
44
+ })();
45
+ }, children: [iconName && ICON_MAP[iconName]
46
+ ? (() => {
47
+ const I = ICON_MAP[iconName];
48
+ return _jsx(I, { size: 16, color: iconColor });
49
+ })()
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
+ }
52
+ const styles = StyleSheet.create({
53
+ outer: { minWidth: 0 },
54
+ btn: {
55
+ paddingHorizontal: 16,
56
+ borderRadius: 10,
57
+ alignItems: "center",
58
+ justifyContent: "center",
59
+ flexDirection: "row",
60
+ gap: 8,
61
+ },
62
+ btnDefault: {
63
+ paddingVertical: 10,
64
+ },
65
+ btnOther: {
66
+ paddingVertical: 8,
67
+ },
68
+ pressed: { opacity: 0.88 },
69
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapBadge({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette.js";
4
+ import { ICON_MAP } from "./snap-icon.js";
5
+ export function SnapBadge({ element: { props }, }) {
6
+ const { accentHex, hex } = useSnapPalette();
7
+ const label = String(props.label ?? "");
8
+ const variant = String(props.variant ?? "default");
9
+ const color = props.color ? String(props.color) : undefined;
10
+ const iconName = props.icon ? String(props.icon) : undefined;
11
+ const isAccent = !color || color === "accent";
12
+ const resolvedColor = isAccent ? accentHex : hex(color);
13
+ const isFilled = variant !== "outline";
14
+ const Icon = iconName ? ICON_MAP[iconName] : undefined;
15
+ return (_jsxs(View, { style: [
16
+ styles.badge,
17
+ isFilled
18
+ ? { backgroundColor: resolvedColor + "20", borderColor: "transparent" }
19
+ : { borderColor: resolvedColor },
20
+ ], children: [Icon && (_jsx(Icon, { size: 12, color: resolvedColor })), _jsx(Text, { style: [
21
+ styles.label,
22
+ { color: resolvedColor },
23
+ ], children: label })] }));
24
+ }
25
+ const styles = StyleSheet.create({
26
+ badge: {
27
+ alignSelf: "flex-start",
28
+ flexDirection: "row",
29
+ alignItems: "center",
30
+ gap: 4,
31
+ paddingHorizontal: 8,
32
+ paddingVertical: 2,
33
+ borderRadius: 9999,
34
+ borderWidth: 1,
35
+ },
36
+ label: {
37
+ fontSize: 12,
38
+ lineHeight: 16,
39
+ fontWeight: "500",
40
+ },
41
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapBarChart({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette.js";
4
+ import { useSnapTheme } from "../theme.js";
5
+ export function SnapBarChart({ element: { props }, }) {
6
+ const { accentHex, hex } = useSnapPalette();
7
+ const { colors } = useSnapTheme();
8
+ const bars = Array.isArray(props.bars) ? props.bars : [];
9
+ const chartColor = String(props.color ?? "accent");
10
+ const maxVal = props.max != null
11
+ ? Number(props.max)
12
+ : Math.max(...bars.map((b) => Number(b.value ?? 0)), 1);
13
+ function barFill(bar) {
14
+ if (bar.color)
15
+ return hex(bar.color);
16
+ if (chartColor !== "accent")
17
+ return hex(chartColor);
18
+ return accentHex;
19
+ }
20
+ return (_jsx(View, { style: styles.wrap, children: bars.map((bar, i) => {
21
+ const value = Number(bar.value ?? 0);
22
+ const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
23
+ return (_jsxs(View, { style: styles.row, children: [_jsx(Text, { style: [styles.label, { color: colors.textSecondary }], numberOfLines: 1, children: String(bar.label ?? "") }), _jsx(View, { style: [styles.track, { backgroundColor: colors.muted }], children: _jsx(View, { style: [
24
+ styles.fill,
25
+ {
26
+ width: `${pct}%`,
27
+ backgroundColor: barFill(bar),
28
+ },
29
+ ] }) }), _jsx(Text, { style: [styles.value, { color: colors.textSecondary }], children: value })] }, i));
30
+ }) }));
31
+ }
32
+ const styles = StyleSheet.create({
33
+ wrap: { width: "100%", gap: 8 },
34
+ row: { flexDirection: "row", alignItems: "center", gap: 8 },
35
+ label: { width: 80, fontSize: 12, lineHeight: 16, textAlign: "right" },
36
+ track: { flex: 1, height: 10, borderRadius: 9999, overflow: "hidden" },
37
+ fill: { height: "100%", borderRadius: 9999 },
38
+ value: { width: 32, fontSize: 12, lineHeight: 16, fontVariant: ["tabular-nums"] },
39
+ });
@@ -0,0 +1,2 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ export declare function SnapCellGrid({ element: { props }, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;