@farcaster/snap 1.5.2 → 1.6.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 (153) 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/schemas.d.ts +14 -1629
  48. package/dist/schemas.js +14 -526
  49. package/dist/ui/badge.d.ts +52 -0
  50. package/dist/ui/badge.js +9 -0
  51. package/dist/ui/button.d.ts +42 -28
  52. package/dist/ui/button.js +7 -9
  53. package/dist/ui/catalog.d.ts +280 -155
  54. package/dist/ui/catalog.js +102 -83
  55. package/dist/ui/icon.d.ts +56 -0
  56. package/dist/ui/icon.js +51 -0
  57. package/dist/ui/image.d.ts +1 -0
  58. package/dist/ui/image.js +2 -2
  59. package/dist/ui/index.d.ts +20 -22
  60. package/dist/ui/index.js +10 -11
  61. package/dist/ui/input.d.ts +17 -0
  62. package/dist/ui/input.js +13 -0
  63. package/dist/ui/item-group.d.ts +12 -0
  64. package/dist/ui/item-group.js +7 -0
  65. package/dist/ui/item.d.ts +14 -0
  66. package/dist/ui/item.js +9 -0
  67. package/dist/ui/progress.d.ts +1 -11
  68. package/dist/ui/progress.js +21 -4
  69. package/dist/ui/schema.js +3 -3
  70. package/dist/ui/separator.d.ts +9 -0
  71. package/dist/ui/separator.js +5 -0
  72. package/dist/ui/slider.d.ts +4 -3
  73. package/dist/ui/slider.js +34 -5
  74. package/dist/ui/stack.d.ts +22 -1
  75. package/dist/ui/stack.js +8 -1
  76. package/dist/ui/switch.d.ts +8 -0
  77. package/dist/ui/switch.js +7 -0
  78. package/dist/ui/text.d.ts +15 -7
  79. package/dist/ui/text.js +8 -4
  80. package/dist/ui/toggle-group.d.ts +23 -0
  81. package/dist/ui/toggle-group.js +19 -0
  82. package/dist/validator.d.ts +5 -1
  83. package/dist/validator.js +6 -136
  84. package/package.json +72 -52
  85. package/src/constants.ts +0 -179
  86. package/src/dataStore.ts +62 -0
  87. package/src/index.ts +10 -20
  88. package/src/react/accent-context.tsx +29 -0
  89. package/src/react/catalog-renderer.tsx +39 -0
  90. package/src/react/components/action-button.tsx +48 -0
  91. package/src/react/components/badge.tsx +37 -0
  92. package/src/react/components/icon.tsx +115 -0
  93. package/src/react/components/image.tsx +33 -0
  94. package/src/react/components/input.tsx +36 -0
  95. package/src/react/components/item-group.tsx +43 -0
  96. package/src/react/components/item.tsx +33 -0
  97. package/src/react/components/progress.tsx +29 -0
  98. package/src/react/components/separator.tsx +14 -0
  99. package/src/react/components/slider.tsx +43 -0
  100. package/src/react/components/stack.tsx +55 -0
  101. package/src/react/components/switch.tsx +46 -0
  102. package/src/react/components/text.tsx +43 -0
  103. package/src/react/components/toggle-group.tsx +85 -0
  104. package/src/react/hooks/use-snap-accent.ts +45 -0
  105. package/src/react/index.tsx +321 -0
  106. package/src/react/lib/preview-primary-css.ts +57 -0
  107. package/src/react/lib/resolve-palette-hex.ts +20 -0
  108. package/src/schemas.ts +18 -644
  109. package/src/ui/badge.ts +13 -0
  110. package/src/ui/button.ts +9 -12
  111. package/src/ui/catalog.ts +106 -86
  112. package/src/ui/icon.ts +56 -0
  113. package/src/ui/image.ts +3 -2
  114. package/src/ui/index.ts +26 -29
  115. package/src/ui/input.ts +17 -0
  116. package/src/ui/item-group.ts +11 -0
  117. package/src/ui/item.ts +13 -0
  118. package/src/ui/progress.ts +25 -7
  119. package/src/ui/schema.ts +3 -3
  120. package/src/ui/separator.ts +9 -0
  121. package/src/ui/slider.ts +40 -10
  122. package/src/ui/stack.ts +9 -1
  123. package/src/ui/switch.ts +11 -0
  124. package/src/ui/text.ts +9 -4
  125. package/src/ui/toggle-group.ts +23 -0
  126. package/src/validator.ts +6 -176
  127. package/dist/ui/bar-chart.d.ts +0 -30
  128. package/dist/ui/bar-chart.js +0 -15
  129. package/dist/ui/button-group.d.ts +0 -19
  130. package/dist/ui/button-group.js +0 -18
  131. package/dist/ui/divider.d.ts +0 -3
  132. package/dist/ui/divider.js +0 -2
  133. package/dist/ui/grid.d.ts +0 -22
  134. package/dist/ui/grid.js +0 -16
  135. package/dist/ui/group.d.ts +0 -7
  136. package/dist/ui/group.js +0 -5
  137. package/dist/ui/list.d.ts +0 -13
  138. package/dist/ui/list.js +0 -13
  139. package/dist/ui/spacer.d.ts +0 -9
  140. package/dist/ui/spacer.js +0 -5
  141. package/dist/ui/text-input.d.ts +0 -7
  142. package/dist/ui/text-input.js +0 -12
  143. package/dist/ui/toggle.d.ts +0 -7
  144. package/dist/ui/toggle.js +0 -6
  145. package/src/ui/bar-chart.ts +0 -20
  146. package/src/ui/button-group.ts +0 -26
  147. package/src/ui/divider.ts +0 -5
  148. package/src/ui/grid.ts +0 -25
  149. package/src/ui/group.ts +0 -8
  150. package/src/ui/list.ts +0 -17
  151. package/src/ui/spacer.ts +0 -8
  152. package/src/ui/text-input.ts +0 -15
  153. 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
+ }