@farcaster/snap 1.5.2 → 1.7.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 (208) hide show
  1. package/dist/constants.d.ts +0 -107
  2. package/dist/constants.js +0 -148
  3. package/dist/dataStore.d.ts +12 -0
  4. package/dist/dataStore.js +35 -0
  5. package/dist/index.d.ts +5 -3
  6. package/dist/index.js +4 -3
  7. package/dist/react/accent-context.d.ts +6 -0
  8. package/dist/react/accent-context.js +10 -0
  9. package/dist/react/catalog-renderer.d.ts +5 -0
  10. package/dist/react/catalog-renderer.js +37 -0
  11. package/dist/react/components/action-button.d.ts +6 -0
  12. package/dist/react/components/action-button.js +22 -0
  13. package/dist/react/components/badge.d.ts +5 -0
  14. package/dist/react/components/badge.js +18 -0
  15. package/dist/react/components/icon.d.ts +7 -0
  16. package/dist/react/components/icon.js +60 -0
  17. package/dist/react/components/image.d.ts +5 -0
  18. package/dist/react/components/image.js +15 -0
  19. package/dist/react/components/input.d.ts +5 -0
  20. package/dist/react/components/input.js +18 -0
  21. package/dist/react/components/item-group.d.ts +7 -0
  22. package/dist/react/components/item-group.js +17 -0
  23. package/dist/react/components/item.d.ts +7 -0
  24. package/dist/react/components/item.js +9 -0
  25. package/dist/react/components/progress.d.ts +5 -0
  26. package/dist/react/components/progress.js +11 -0
  27. package/dist/react/components/separator.d.ts +5 -0
  28. package/dist/react/components/separator.js +7 -0
  29. package/dist/react/components/slider.d.ts +5 -0
  30. package/dist/react/components/slider.js +21 -0
  31. package/dist/react/components/stack.d.ts +7 -0
  32. package/dist/react/components/stack.js +32 -0
  33. package/dist/react/components/switch.d.ts +5 -0
  34. package/dist/react/components/switch.js +23 -0
  35. package/dist/react/components/text.d.ts +5 -0
  36. package/dist/react/components/text.js +25 -0
  37. package/dist/react/components/toggle-group.d.ts +5 -0
  38. package/dist/react/components/toggle-group.js +52 -0
  39. package/dist/react/hooks/use-snap-accent.d.ts +13 -0
  40. package/dist/react/hooks/use-snap-accent.js +32 -0
  41. package/dist/react/index.d.ts +47 -0
  42. package/dist/react/index.js +191 -0
  43. package/dist/react/lib/preview-primary-css.d.ts +6 -0
  44. package/dist/react/lib/preview-primary-css.js +43 -0
  45. package/dist/react/lib/resolve-palette-hex.d.ts +2 -0
  46. package/dist/react/lib/resolve-palette-hex.js +10 -0
  47. package/dist/react-native/catalog-renderer.d.ts +5 -0
  48. package/dist/react-native/catalog-renderer.js +36 -0
  49. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  50. package/dist/react-native/components/snap-action-button.js +68 -0
  51. package/dist/react-native/components/snap-badge.d.ts +2 -0
  52. package/dist/react-native/components/snap-badge.js +38 -0
  53. package/dist/react-native/components/snap-icon.d.ts +5 -0
  54. package/dist/react-native/components/snap-icon.js +56 -0
  55. package/dist/react-native/components/snap-image.d.ts +2 -0
  56. package/dist/react-native/components/snap-image.js +24 -0
  57. package/dist/react-native/components/snap-input.d.ts +2 -0
  58. package/dist/react-native/components/snap-input.js +36 -0
  59. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  60. package/dist/react-native/components/snap-item-group.js +23 -0
  61. package/dist/react-native/components/snap-item.d.ts +5 -0
  62. package/dist/react-native/components/snap-item.js +45 -0
  63. package/dist/react-native/components/snap-progress.d.ts +2 -0
  64. package/dist/react-native/components/snap-progress.js +26 -0
  65. package/dist/react-native/components/snap-separator.d.ts +2 -0
  66. package/dist/react-native/components/snap-separator.js +23 -0
  67. package/dist/react-native/components/snap-slider.d.ts +2 -0
  68. package/dist/react-native/components/snap-slider.js +42 -0
  69. package/dist/react-native/components/snap-stack.d.ts +5 -0
  70. package/dist/react-native/components/snap-stack.js +49 -0
  71. package/dist/react-native/components/snap-switch.d.ts +2 -0
  72. package/dist/react-native/components/snap-switch.js +30 -0
  73. package/dist/react-native/components/snap-text.d.ts +2 -0
  74. package/dist/react-native/components/snap-text.js +37 -0
  75. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  76. package/dist/react-native/components/snap-toggle-group.js +100 -0
  77. package/dist/react-native/index.d.ts +52 -0
  78. package/dist/react-native/index.js +155 -0
  79. package/dist/react-native/theme.d.ts +21 -0
  80. package/dist/react-native/theme.js +37 -0
  81. package/dist/react-native/use-snap-palette.d.ts +13 -0
  82. package/dist/react-native/use-snap-palette.js +48 -0
  83. package/dist/schemas.d.ts +14 -1629
  84. package/dist/schemas.js +14 -526
  85. package/dist/ui/badge.d.ts +52 -0
  86. package/dist/ui/badge.js +9 -0
  87. package/dist/ui/button.d.ts +42 -28
  88. package/dist/ui/button.js +7 -9
  89. package/dist/ui/catalog.d.ts +281 -156
  90. package/dist/ui/catalog.js +102 -83
  91. package/dist/ui/icon.d.ts +56 -0
  92. package/dist/ui/icon.js +51 -0
  93. package/dist/ui/image.d.ts +1 -0
  94. package/dist/ui/image.js +2 -2
  95. package/dist/ui/index.d.ts +20 -22
  96. package/dist/ui/index.js +10 -11
  97. package/dist/ui/input.d.ts +17 -0
  98. package/dist/ui/input.js +13 -0
  99. package/dist/ui/item-group.d.ts +12 -0
  100. package/dist/ui/item-group.js +7 -0
  101. package/dist/ui/item.d.ts +14 -0
  102. package/dist/ui/item.js +9 -0
  103. package/dist/ui/progress.d.ts +1 -11
  104. package/dist/ui/progress.js +21 -4
  105. package/dist/ui/schema.d.ts +1 -1
  106. package/dist/ui/schema.js +3 -3
  107. package/dist/ui/separator.d.ts +9 -0
  108. package/dist/ui/separator.js +5 -0
  109. package/dist/ui/slider.d.ts +4 -3
  110. package/dist/ui/slider.js +34 -5
  111. package/dist/ui/stack.d.ts +22 -1
  112. package/dist/ui/stack.js +8 -1
  113. package/dist/ui/switch.d.ts +8 -0
  114. package/dist/ui/switch.js +7 -0
  115. package/dist/ui/text.d.ts +15 -7
  116. package/dist/ui/text.js +8 -4
  117. package/dist/ui/toggle-group.d.ts +23 -0
  118. package/dist/ui/toggle-group.js +19 -0
  119. package/dist/validator.d.ts +5 -1
  120. package/dist/validator.js +6 -136
  121. package/package.json +78 -53
  122. package/src/constants.ts +0 -179
  123. package/src/dataStore.ts +62 -0
  124. package/src/index.ts +10 -20
  125. package/src/react/accent-context.tsx +29 -0
  126. package/src/react/catalog-renderer.tsx +39 -0
  127. package/src/react/components/action-button.tsx +48 -0
  128. package/src/react/components/badge.tsx +37 -0
  129. package/src/react/components/icon.tsx +115 -0
  130. package/src/react/components/image.tsx +33 -0
  131. package/src/react/components/input.tsx +36 -0
  132. package/src/react/components/item-group.tsx +43 -0
  133. package/src/react/components/item.tsx +33 -0
  134. package/src/react/components/progress.tsx +29 -0
  135. package/src/react/components/separator.tsx +14 -0
  136. package/src/react/components/slider.tsx +43 -0
  137. package/src/react/components/stack.tsx +55 -0
  138. package/src/react/components/switch.tsx +46 -0
  139. package/src/react/components/text.tsx +43 -0
  140. package/src/react/components/toggle-group.tsx +85 -0
  141. package/src/react/hooks/use-snap-accent.ts +45 -0
  142. package/src/react/index.tsx +321 -0
  143. package/src/react/lib/preview-primary-css.ts +57 -0
  144. package/src/react/lib/resolve-palette-hex.ts +20 -0
  145. package/src/react-native/catalog-renderer.tsx +37 -0
  146. package/src/react-native/components/snap-action-button.tsx +92 -0
  147. package/src/react-native/components/snap-badge.tsx +57 -0
  148. package/src/react-native/components/snap-icon.tsx +102 -0
  149. package/src/react-native/components/snap-image.tsx +38 -0
  150. package/src/react-native/components/snap-input.tsx +57 -0
  151. package/src/react-native/components/snap-item-group.tsx +43 -0
  152. package/src/react-native/components/snap-item.tsx +70 -0
  153. package/src/react-native/components/snap-progress.tsx +40 -0
  154. package/src/react-native/components/snap-separator.tsx +32 -0
  155. package/src/react-native/components/snap-slider.tsx +82 -0
  156. package/src/react-native/components/snap-stack.tsx +66 -0
  157. package/src/react-native/components/snap-switch.tsx +45 -0
  158. package/src/react-native/components/snap-text.tsx +53 -0
  159. package/src/react-native/components/snap-toggle-group.tsx +128 -0
  160. package/src/react-native/index.tsx +267 -0
  161. package/src/react-native/theme.tsx +73 -0
  162. package/src/react-native/use-snap-palette.ts +64 -0
  163. package/src/schemas.ts +18 -644
  164. package/src/ui/badge.ts +13 -0
  165. package/src/ui/button.ts +9 -12
  166. package/src/ui/catalog.ts +106 -86
  167. package/src/ui/icon.ts +56 -0
  168. package/src/ui/image.ts +3 -2
  169. package/src/ui/index.ts +26 -29
  170. package/src/ui/input.ts +17 -0
  171. package/src/ui/item-group.ts +11 -0
  172. package/src/ui/item.ts +13 -0
  173. package/src/ui/progress.ts +25 -7
  174. package/src/ui/schema.ts +3 -3
  175. package/src/ui/separator.ts +9 -0
  176. package/src/ui/slider.ts +40 -10
  177. package/src/ui/stack.ts +9 -1
  178. package/src/ui/switch.ts +11 -0
  179. package/src/ui/text.ts +9 -4
  180. package/src/ui/toggle-group.ts +23 -0
  181. package/src/validator.ts +6 -176
  182. package/dist/ui/bar-chart.d.ts +0 -30
  183. package/dist/ui/bar-chart.js +0 -15
  184. package/dist/ui/button-group.d.ts +0 -19
  185. package/dist/ui/button-group.js +0 -18
  186. package/dist/ui/divider.d.ts +0 -3
  187. package/dist/ui/divider.js +0 -2
  188. package/dist/ui/grid.d.ts +0 -22
  189. package/dist/ui/grid.js +0 -16
  190. package/dist/ui/group.d.ts +0 -7
  191. package/dist/ui/group.js +0 -5
  192. package/dist/ui/list.d.ts +0 -13
  193. package/dist/ui/list.js +0 -13
  194. package/dist/ui/spacer.d.ts +0 -9
  195. package/dist/ui/spacer.js +0 -5
  196. package/dist/ui/text-input.d.ts +0 -7
  197. package/dist/ui/text-input.js +0 -12
  198. package/dist/ui/toggle.d.ts +0 -7
  199. package/dist/ui/toggle.js +0 -6
  200. package/src/ui/bar-chart.ts +0 -20
  201. package/src/ui/button-group.ts +0 -26
  202. package/src/ui/divider.ts +0 -5
  203. package/src/ui/grid.ts +0 -25
  204. package/src/ui/group.ts +0 -8
  205. package/src/ui/list.ts +0 -17
  206. package/src/ui/spacer.ts +0 -8
  207. package/src/ui/text-input.ts +0 -15
  208. package/src/ui/toggle.ts +0 -9
@@ -0,0 +1,321 @@
1
+ "use client";
2
+
3
+ import type { Spec } from "@json-render/core";
4
+ import { snapJsonRenderCatalog } from "../ui/index.js";
5
+ import { SnapCatalogView } from "./catalog-renderer";
6
+ import { SnapPreviewAccentProvider } from "./accent-context";
7
+ import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex";
8
+ import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css";
9
+ import {
10
+ type CSSProperties,
11
+ useCallback,
12
+ useEffect,
13
+ useMemo,
14
+ useRef,
15
+ useState,
16
+ } from "react";
17
+
18
+ // ─── Public types ──────────────────────────────────────
19
+
20
+ export type JsonValue =
21
+ | string
22
+ | number
23
+ | boolean
24
+ | null
25
+ | JsonValue[]
26
+ | { [key: string]: JsonValue };
27
+
28
+ export type SnapPage = {
29
+ version: string;
30
+ theme?: { accent?: string };
31
+ effects?: string[];
32
+ ui: Spec;
33
+ };
34
+
35
+ export type SnapActionHandlers = {
36
+ submit: (target: string, inputs: Record<string, JsonValue>) => void;
37
+ open_url: (target: string) => void;
38
+ open_mini_app: (target: string) => void;
39
+ view_cast: (params: { hash: string }) => void;
40
+ view_profile: (params: { fid: number }) => void;
41
+ compose_cast: (params: {
42
+ text?: string;
43
+ channelKey?: string;
44
+ embeds?: string[];
45
+ }) => void;
46
+ view_token: (params: { token: string }) => void;
47
+ send_token: (params: {
48
+ token: string;
49
+ amount?: string;
50
+ recipientFid?: number;
51
+ recipientAddress?: string;
52
+ }) => void;
53
+ swap_token: (params: {
54
+ sellToken?: string;
55
+ buyToken?: string;
56
+ }) => void;
57
+ };
58
+
59
+ // ─── Internal helpers ──────────────────────────────────
60
+
61
+ function applyStatePaths(
62
+ model: Record<string, unknown>,
63
+ changes: { path: string; value: unknown }[] | Record<string, unknown>,
64
+ ): void {
65
+ const entries = Array.isArray(changes)
66
+ ? changes.map((c) => [c.path, c.value] as const)
67
+ : Object.entries(changes);
68
+ for (const [path, value] of entries) {
69
+ const trimmed = path.startsWith("/") ? path : `/${path}`;
70
+ const parts = trimmed.split("/").filter(Boolean);
71
+ if (parts.length < 2) continue;
72
+ const [top, ...rest] = parts;
73
+ if (top === "inputs") {
74
+ if (typeof model.inputs !== "object" || model.inputs === null) {
75
+ model.inputs = {};
76
+ }
77
+ const inputs = model.inputs as Record<string, unknown>;
78
+ if (rest.length === 1) {
79
+ inputs[rest[0]!] = value;
80
+ }
81
+ continue;
82
+ }
83
+ if (top === "theme") {
84
+ if (typeof model.theme !== "object" || model.theme === null) {
85
+ model.theme = {};
86
+ }
87
+ const theme = model.theme as Record<string, unknown>;
88
+ if (rest.length === 1) {
89
+ theme[rest[0]!] = value;
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ const CONFETTI_COLORS = [
96
+ "#8B5CF6",
97
+ "#EC4899",
98
+ "#3B82F6",
99
+ "#10B981",
100
+ "#F59E0B",
101
+ "#EF4444",
102
+ "#06B6D4",
103
+ ];
104
+
105
+ function ConfettiOverlay() {
106
+ const pieces = useMemo(
107
+ () =>
108
+ Array.from({ length: 80 }, (_, i) => ({
109
+ id: i,
110
+ left: Math.random() * 100,
111
+ delay: Math.random() * 1.2,
112
+ duration: 2.5 + Math.random() * 2,
113
+ color:
114
+ CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
115
+ size: 6 + Math.random() * 8,
116
+ rotation: Math.random() * 360,
117
+ })),
118
+ [],
119
+ );
120
+
121
+ return (
122
+ <div
123
+ style={{
124
+ position: "absolute",
125
+ inset: 0,
126
+ overflow: "hidden",
127
+ pointerEvents: "none",
128
+ zIndex: 20,
129
+ }}
130
+ >
131
+ {pieces.map(({ id, left, delay, duration, color, size, rotation }) => (
132
+ <div
133
+ key={id}
134
+ style={{
135
+ position: "absolute",
136
+ left: `${left}%`,
137
+ top: -20,
138
+ width: size,
139
+ height: size * 0.6,
140
+ backgroundColor: color,
141
+ borderRadius: 2,
142
+ transform: `rotate(${rotation}deg)`,
143
+ animation: `confettiFall ${duration}s ease-in ${delay}s forwards`,
144
+ }}
145
+ />
146
+ ))}
147
+ <style>{`@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)}}`}</style>
148
+ </div>
149
+ );
150
+ }
151
+
152
+ const PALETTE = [
153
+ "gray",
154
+ "blue",
155
+ "red",
156
+ "amber",
157
+ "green",
158
+ "teal",
159
+ "purple",
160
+ "pink",
161
+ ] as const;
162
+
163
+ // ─── SnapView ──────────────────────────────────────────
164
+
165
+ export function SnapView({
166
+ snap,
167
+ handlers,
168
+ loading = false,
169
+ appearance = "dark",
170
+ }: {
171
+ snap: SnapPage;
172
+ handlers: SnapActionHandlers;
173
+ loading?: boolean;
174
+ appearance?: "light" | "dark";
175
+ }) {
176
+ const spec = snap.ui;
177
+ const initialState = useMemo(
178
+ () => spec.state ?? { inputs: {} },
179
+ [spec],
180
+ );
181
+
182
+ const stateRef = useRef<Record<string, unknown>>(initialState);
183
+
184
+ useEffect(() => {
185
+ stateRef.current = {
186
+ inputs: {
187
+ ...((initialState.inputs ?? {}) as Record<string, unknown>),
188
+ },
189
+ theme: {
190
+ ...((initialState.theme ?? {}) as Record<string, unknown>),
191
+ },
192
+ };
193
+ }, [initialState]);
194
+
195
+ useEffect(() => {
196
+ const result = snapJsonRenderCatalog.validate(spec);
197
+ if (!result.success) {
198
+ // eslint-disable-next-line no-console
199
+ console.warn("[SnapView] catalog validation issues:", result.error);
200
+ }
201
+ }, [spec]);
202
+
203
+ const [pageKey, setPageKey] = useState(0);
204
+ useEffect(() => {
205
+ setPageKey((k) => k + 1);
206
+ }, [spec]);
207
+
208
+ const showConfetti = snap.effects?.includes("confetti");
209
+
210
+ const previewSurfaceStyle = useMemo(() => {
211
+ const vars: Record<string, string> = {};
212
+ for (const c of PALETTE)
213
+ vars[`--snap-color-${c}`] = resolveSnapPaletteHex(c, appearance);
214
+ return {
215
+ ...snapPreviewPrimaryCssProperties(
216
+ snap.theme?.accent ?? "purple",
217
+ appearance,
218
+ ),
219
+ ...vars,
220
+ } as CSSProperties;
221
+ }, [snap.theme?.accent, appearance]);
222
+
223
+ const handleAction = useCallback(
224
+ (name: unknown, params: unknown) => {
225
+ const inputs = (stateRef.current.inputs ?? {}) as Record<
226
+ string,
227
+ JsonValue
228
+ >;
229
+ const p = (params ?? {}) as Record<string, unknown>;
230
+ switch (name) {
231
+ case "submit":
232
+ handlers.submit(String(p.target ?? ""), inputs);
233
+ break;
234
+ case "open_url":
235
+ handlers.open_url(String(p.target ?? ""));
236
+ break;
237
+ case "open_mini_app":
238
+ handlers.open_mini_app(String(p.target ?? ""));
239
+ break;
240
+ case "view_cast":
241
+ handlers.view_cast({ hash: String(p.hash ?? "") });
242
+ break;
243
+ case "view_profile":
244
+ handlers.view_profile({ fid: Number(p.fid ?? 0) });
245
+ break;
246
+ case "compose_cast":
247
+ handlers.compose_cast({
248
+ text: p.text ? String(p.text) : undefined,
249
+ channelKey: p.channelKey ? String(p.channelKey) : undefined,
250
+ embeds: Array.isArray(p.embeds)
251
+ ? (p.embeds as string[])
252
+ : undefined,
253
+ });
254
+ break;
255
+ case "view_token":
256
+ handlers.view_token({ token: String(p.token ?? "") });
257
+ break;
258
+ case "send_token":
259
+ handlers.send_token({
260
+ token: String(p.token ?? ""),
261
+ amount: p.amount ? String(p.amount) : undefined,
262
+ recipientFid: p.recipientFid
263
+ ? Number(p.recipientFid)
264
+ : undefined,
265
+ recipientAddress: p.recipientAddress
266
+ ? String(p.recipientAddress)
267
+ : undefined,
268
+ });
269
+ break;
270
+ case "swap_token":
271
+ handlers.swap_token({
272
+ sellToken: p.sellToken ? String(p.sellToken) : undefined,
273
+ buyToken: p.buyToken ? String(p.buyToken) : undefined,
274
+ });
275
+ break;
276
+ default:
277
+ break;
278
+ }
279
+ },
280
+ [handlers],
281
+ );
282
+
283
+ return (
284
+ <div style={{ position: "relative", width: "100%" }}>
285
+ {showConfetti && <ConfettiOverlay />}
286
+ {loading && (
287
+ <div
288
+ style={{
289
+ position: "absolute",
290
+ inset: 0,
291
+ display: "flex",
292
+ alignItems: "center",
293
+ justifyContent: "center",
294
+ zIndex: 10,
295
+ fontSize: 14,
296
+ color: "var(--text-muted)",
297
+ background: "var(--bg-primary, rgba(0,0,0,0.6))",
298
+ backdropFilter: "blur(4px)",
299
+ }}
300
+ >
301
+ Loading...
302
+ </div>
303
+ )}
304
+
305
+ <div style={previewSurfaceStyle}>
306
+ <SnapPreviewAccentProvider pageAccent={snap.theme?.accent}>
307
+ <SnapCatalogView
308
+ key={pageKey}
309
+ spec={spec}
310
+ state={initialState}
311
+ loading={false}
312
+ onStateChange={(changes) => {
313
+ applyStatePaths(stateRef.current, changes);
314
+ }}
315
+ onAction={handleAction}
316
+ />
317
+ </SnapPreviewAccentProvider>
318
+ </div>
319
+ </div>
320
+ );
321
+ }
@@ -0,0 +1,57 @@
1
+ import type { CSSProperties } from "react";
2
+ import { resolveSnapPaletteHex } from "./resolve-palette-hex";
3
+
4
+ /** Readable on-primary text for hex backgrounds (e.g. amber vs purple). */
5
+ function pickForegroundForBg(hex: string): string {
6
+ const h = hex.replace(/^#/, "");
7
+ if (h.length !== 6) return "#ffffff";
8
+ const r = Number.parseInt(h.slice(0, 2), 16);
9
+ const g = Number.parseInt(h.slice(2, 4), 16);
10
+ const b = Number.parseInt(h.slice(4, 6), 16);
11
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
12
+ return yiq >= 180 ? "#0a0a0a" : "#ffffff";
13
+ }
14
+
15
+ /** Match `globals.css` `--snap-card-bg` so hover tints sit on the preview card. */
16
+ const SNAP_CARD_BG: Record<"light" | "dark", string> = {
17
+ light: "#ffffff",
18
+ dark: "#23262f",
19
+ };
20
+
21
+ function snapActionPrimaryHover(
22
+ hex: string,
23
+ appearance: "light" | "dark",
24
+ ): string {
25
+ return appearance === "light"
26
+ ? `color-mix(in srgb, ${hex} 82%, #000000)`
27
+ : `color-mix(in srgb, ${hex} 78%, #ffffff)`;
28
+ }
29
+
30
+ function snapActionOutlineHover(
31
+ hex: string,
32
+ appearance: "light" | "dark",
33
+ ): string {
34
+ const card = SNAP_CARD_BG[appearance];
35
+ return `color-mix(in srgb, ${hex} 14%, ${card})`;
36
+ }
37
+
38
+ /**
39
+ * Overrides Neynar / Tailwind theme tokens so `bg-primary`, `border-primary`, etc.
40
+ * use the snap spec accent inside the preview subtree.
41
+ */
42
+ export function snapPreviewPrimaryCssProperties(
43
+ accentName: string,
44
+ appearance: "light" | "dark",
45
+ ): CSSProperties {
46
+ const hex = resolveSnapPaletteHex(accentName, appearance);
47
+ const fg = pickForegroundForBg(hex);
48
+ return {
49
+ "--primary": hex,
50
+ "--primary-foreground": fg,
51
+ "--ring": hex,
52
+ "--color-primary": hex,
53
+ "--color-primary-foreground": fg,
54
+ "--snap-action-primary-hover": snapActionPrimaryHover(hex, appearance),
55
+ "--snap-action-outline-hover": snapActionOutlineHover(hex, appearance),
56
+ } as CSSProperties;
57
+ }
@@ -0,0 +1,20 @@
1
+ import {
2
+ PALETTE_DARK_HEX,
3
+ PALETTE_LIGHT_HEX,
4
+ type PaletteColor,
5
+ } from "@farcaster/snap";
6
+
7
+ /** Resolve a snap palette color name to hex for the current shell appearance. */
8
+ export function resolveSnapPaletteHex(
9
+ name: string,
10
+ appearance: "light" | "dark",
11
+ ): string {
12
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
13
+ if (
14
+ Object.hasOwn(map, name) &&
15
+ typeof map[name as PaletteColor] === "string"
16
+ ) {
17
+ return map[name as PaletteColor];
18
+ }
19
+ return map.purple;
20
+ }
@@ -0,0 +1,37 @@
1
+ import { createRenderer } from "@json-render/react-native";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapActionButton } from "./components/snap-action-button";
4
+ import { SnapBadge } from "./components/snap-badge";
5
+ import { SnapIcon } from "./components/snap-icon";
6
+ import { SnapImage } from "./components/snap-image";
7
+ import { SnapInput } from "./components/snap-input";
8
+ import { SnapItem } from "./components/snap-item";
9
+ import { SnapItemGroup } from "./components/snap-item-group";
10
+ import { SnapProgress } from "./components/snap-progress";
11
+ import { SnapSeparator } from "./components/snap-separator";
12
+ import { SnapSlider } from "./components/snap-slider";
13
+ import { SnapStack } from "./components/snap-stack";
14
+ import { SnapSwitch } from "./components/snap-switch";
15
+ import { SnapText } from "./components/snap-text";
16
+ import { SnapToggleGroup } from "./components/snap-toggle-group";
17
+
18
+ /**
19
+ * Maps snap json-render catalog types to React Native primitives.
20
+ * Keys match the snap wire-format `type` strings exactly (snake_case).
21
+ */
22
+ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
23
+ badge: SnapBadge,
24
+ button: SnapActionButton,
25
+ icon: SnapIcon,
26
+ image: SnapImage,
27
+ input: SnapInput,
28
+ item: SnapItem,
29
+ item_group: SnapItemGroup,
30
+ progress: SnapProgress,
31
+ separator: SnapSeparator,
32
+ slider: SnapSlider,
33
+ stack: SnapStack,
34
+ switch: SnapSwitch,
35
+ text: SnapText,
36
+ toggle_group: SnapToggleGroup,
37
+ });
@@ -0,0 +1,92 @@
1
+ declare const __DEV__: boolean;
2
+
3
+ import type { ComponentRenderProps } from "@json-render/react-native";
4
+ import { Pressable, StyleSheet, Text, View } from "react-native";
5
+ import { useSnapPalette } from "../use-snap-palette";
6
+ import { useSnapTheme } from "../theme";
7
+ import { ICON_MAP } from "./snap-icon";
8
+
9
+ const VARIANT_MAP: Record<string, "default" | "secondary" | "outline" | "ghost"> = {
10
+ default: "default",
11
+ secondary: "secondary",
12
+ outline: "outline",
13
+ ghost: "ghost",
14
+ };
15
+
16
+ export function SnapActionButton({
17
+ element: { props },
18
+ emit,
19
+ }: ComponentRenderProps<Record<string, unknown>>) {
20
+ const { accentHex } = useSnapPalette();
21
+ const { colors } = useSnapTheme();
22
+ const label = String(props.label ?? "Action");
23
+ const variant = VARIANT_MAP[String(props.variant ?? "default")] ?? "default";
24
+ const iconName = props.icon ? String(props.icon) : undefined;
25
+
26
+ const variantStyle = (() => {
27
+ switch (variant) {
28
+ case "default":
29
+ return { backgroundColor: accentHex };
30
+ case "secondary":
31
+ return { backgroundColor: "transparent", borderWidth: 1.5, borderColor: accentHex };
32
+ case "outline":
33
+ return { backgroundColor: "rgba(255,255,255,0.04)", borderWidth: 1, borderColor: colors.border };
34
+ case "ghost":
35
+ return { backgroundColor: "transparent" };
36
+ }
37
+ })();
38
+
39
+ const textColor = variant === "default" ? "#fff" : variant === "secondary" ? accentHex : colors.text;
40
+ const iconColor = variant === "default" ? "#fff" : variant === "secondary" ? accentHex : colors.text;
41
+
42
+ return (
43
+ <View style={styles.outer}>
44
+ <Pressable
45
+ style={({ pressed }) => [
46
+ styles.btn,
47
+ variant === "default" ? styles.btnDefault : styles.btnOther,
48
+ variantStyle,
49
+ pressed && styles.pressed,
50
+ ]}
51
+ onPress={() => {
52
+ void (async () => {
53
+ try {
54
+ await emit("press");
55
+ } catch (err: unknown) {
56
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
57
+ // eslint-disable-next-line no-console
58
+ console.error("[snap] action failed", err);
59
+ }
60
+ }
61
+ })();
62
+ }}
63
+ >
64
+ {iconName && ICON_MAP[iconName] ? (
65
+ (() => { const I = ICON_MAP[iconName]!; return <I size={16} color={iconColor} />; })()
66
+ ) : null}
67
+ <Text style={{ color: textColor, fontSize: 14, fontWeight: "600" }}>
68
+ {label}
69
+ </Text>
70
+ </Pressable>
71
+ </View>
72
+ );
73
+ }
74
+
75
+ const styles = StyleSheet.create({
76
+ outer: { flex: 1, minWidth: 0 },
77
+ btn: {
78
+ paddingHorizontal: 16,
79
+ borderRadius: 10,
80
+ alignItems: "center",
81
+ justifyContent: "center",
82
+ flexDirection: "row",
83
+ gap: 8,
84
+ },
85
+ btnDefault: {
86
+ paddingVertical: 10,
87
+ },
88
+ btnOther: {
89
+ paddingVertical: 8,
90
+ },
91
+ pressed: { opacity: 0.88 },
92
+ });
@@ -0,0 +1,57 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette";
4
+ import { ICON_MAP } from "./snap-icon";
5
+
6
+ export function SnapBadge({
7
+ element: { props },
8
+ }: ComponentRenderProps<Record<string, unknown>>) {
9
+ const { accentHex, hex } = useSnapPalette();
10
+ const label = String(props.label ?? "");
11
+ const color = props.color ? String(props.color) : undefined;
12
+ const iconName = props.icon ? String(props.icon) : undefined;
13
+ const isAccent = !color || color === "accent";
14
+ const resolvedColor = isAccent ? accentHex : hex(color);
15
+
16
+ const Icon = iconName ? ICON_MAP[iconName] : undefined;
17
+
18
+ return (
19
+ <View
20
+ style={[
21
+ styles.badge,
22
+ isAccent
23
+ ? { backgroundColor: resolvedColor, borderColor: resolvedColor }
24
+ : { borderColor: resolvedColor },
25
+ ]}
26
+ >
27
+ {Icon && (
28
+ <Icon size={12} color={isAccent ? "#fff" : resolvedColor} />
29
+ )}
30
+ <Text
31
+ style={[
32
+ styles.label,
33
+ { color: isAccent ? "#fff" : resolvedColor },
34
+ ]}
35
+ >
36
+ {label}
37
+ </Text>
38
+ </View>
39
+ );
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ badge: {
44
+ alignSelf: "flex-start",
45
+ flexDirection: "row",
46
+ alignItems: "center",
47
+ gap: 4,
48
+ paddingHorizontal: 8,
49
+ paddingVertical: 2,
50
+ borderRadius: 9999,
51
+ borderWidth: 1,
52
+ },
53
+ label: {
54
+ fontSize: 12,
55
+ fontWeight: "500",
56
+ },
57
+ });
@@ -0,0 +1,102 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { View } from "react-native";
3
+ import { useSnapPalette } from "../use-snap-palette";
4
+ import {
5
+ ArrowRight,
6
+ ArrowLeft,
7
+ ExternalLink,
8
+ ChevronRight,
9
+ Check,
10
+ X,
11
+ AlertTriangle,
12
+ Info,
13
+ Clock,
14
+ Heart,
15
+ MessageCircle,
16
+ Repeat,
17
+ Share,
18
+ User,
19
+ Users,
20
+ Star,
21
+ Trophy,
22
+ Zap,
23
+ Flame,
24
+ Gift,
25
+ ImageIcon,
26
+ Play,
27
+ Pause,
28
+ Wallet,
29
+ Coins,
30
+ Plus,
31
+ Minus,
32
+ RefreshCw,
33
+ Bookmark,
34
+ ThumbsUp,
35
+ ThumbsDown,
36
+ TrendingUp,
37
+ TrendingDown,
38
+ type LucideIcon,
39
+ } from "lucide-react-native";
40
+
41
+ const ICON_MAP: Record<string, LucideIcon> = {
42
+ "arrow-right": ArrowRight,
43
+ "arrow-left": ArrowLeft,
44
+ "external-link": ExternalLink,
45
+ "chevron-right": ChevronRight,
46
+ check: Check,
47
+ x: X,
48
+ "alert-triangle": AlertTriangle,
49
+ info: Info,
50
+ clock: Clock,
51
+ heart: Heart,
52
+ "message-circle": MessageCircle,
53
+ repeat: Repeat,
54
+ share: Share,
55
+ user: User,
56
+ users: Users,
57
+ star: Star,
58
+ trophy: Trophy,
59
+ zap: Zap,
60
+ flame: Flame,
61
+ gift: Gift,
62
+ image: ImageIcon,
63
+ play: Play,
64
+ pause: Pause,
65
+ wallet: Wallet,
66
+ coins: Coins,
67
+ plus: Plus,
68
+ minus: Minus,
69
+ "refresh-cw": RefreshCw,
70
+ bookmark: Bookmark,
71
+ "thumbs-up": ThumbsUp,
72
+ "thumbs-down": ThumbsDown,
73
+ "trending-up": TrendingUp,
74
+ "trending-down": TrendingDown,
75
+ };
76
+
77
+ const SIZE_PX: Record<string, number> = {
78
+ sm: 16,
79
+ md: 20,
80
+ };
81
+
82
+ export function SnapIcon({
83
+ element: { props },
84
+ }: ComponentRenderProps<Record<string, unknown>>) {
85
+ const { accentHex, hex } = useSnapPalette();
86
+ const name = String(props.name ?? "info");
87
+ const size = SIZE_PX[String(props.size ?? "md")] ?? 20;
88
+ const color = props.color ? String(props.color) : undefined;
89
+ const isAccent = !color || color === "accent";
90
+ const resolvedColor = isAccent ? accentHex : hex(color);
91
+
92
+ const Icon = ICON_MAP[name];
93
+ if (!Icon) return null;
94
+
95
+ return (
96
+ <View style={{ alignItems: "center", justifyContent: "center" }}>
97
+ <Icon size={size} color={resolvedColor} />
98
+ </View>
99
+ );
100
+ }
101
+
102
+ export { ICON_MAP };