@farcaster/snap 1.15.3 → 1.16.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 (60) hide show
  1. package/dist/constants.d.ts +8 -0
  2. package/dist/constants.js +9 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/react/components/action-button.d.ts +2 -1
  6. package/dist/react/components/action-button.js +16 -3
  7. package/dist/react/components/badge.js +2 -3
  8. package/dist/react/index.d.ts +8 -1
  9. package/dist/react/index.js +9 -228
  10. package/dist/react/snap-view-core.d.ts +11 -0
  11. package/dist/react/snap-view-core.js +224 -0
  12. package/dist/react/v1/snap-view.d.ts +14 -0
  13. package/dist/react/v1/snap-view.js +9 -0
  14. package/dist/react/v2/snap-view.d.ts +21 -0
  15. package/dist/react/v2/snap-view.js +76 -0
  16. package/dist/react-native/components/snap-action-button.d.ts +1 -1
  17. package/dist/react-native/components/snap-action-button.js +19 -2
  18. package/dist/react-native/components/snap-badge.js +3 -3
  19. package/dist/react-native/index.d.ts +15 -43
  20. package/dist/react-native/index.js +10 -164
  21. package/dist/react-native/snap-view-core.d.ts +11 -0
  22. package/dist/react-native/snap-view-core.js +153 -0
  23. package/dist/react-native/types.d.ts +41 -0
  24. package/dist/react-native/types.js +1 -0
  25. package/dist/react-native/v1/snap-view.d.ts +22 -0
  26. package/dist/react-native/v1/snap-view.js +31 -0
  27. package/dist/react-native/v2/snap-view.d.ts +31 -0
  28. package/dist/react-native/v2/snap-view.js +101 -0
  29. package/dist/schemas.d.ts +15 -9
  30. package/dist/schemas.js +7 -8
  31. package/dist/server/parseRequest.d.ts +7 -0
  32. package/dist/server/parseRequest.js +27 -0
  33. package/dist/ui/catalog.d.ts +1 -0
  34. package/dist/ui/catalog.js +5 -2
  35. package/dist/ui/schema.js +1 -1
  36. package/dist/validator.d.ts +3 -2
  37. package/dist/validator.js +193 -2
  38. package/llms.txt +9 -0
  39. package/package.json +1 -1
  40. package/src/constants.ts +11 -1
  41. package/src/index.ts +8 -0
  42. package/src/react/accent-context.tsx +1 -1
  43. package/src/react/components/action-button.tsx +25 -3
  44. package/src/react/components/badge.tsx +2 -3
  45. package/src/react/index.tsx +36 -327
  46. package/src/react/snap-view-core.tsx +340 -0
  47. package/src/react/v1/snap-view.tsx +50 -0
  48. package/src/react/v2/snap-view.tsx +168 -0
  49. package/src/react-native/components/snap-action-button.tsx +26 -4
  50. package/src/react-native/components/snap-badge.tsx +3 -3
  51. package/src/react-native/index.tsx +47 -263
  52. package/src/react-native/snap-view-core.tsx +209 -0
  53. package/src/react-native/types.ts +37 -0
  54. package/src/react-native/v1/snap-view.tsx +108 -0
  55. package/src/react-native/v2/snap-view.tsx +239 -0
  56. package/src/schemas.ts +9 -10
  57. package/src/server/parseRequest.ts +39 -0
  58. package/src/ui/catalog.ts +5 -2
  59. package/src/ui/schema.ts +1 -1
  60. package/src/validator.ts +240 -2
@@ -1,289 +1,73 @@
1
- import type { Spec } from "@json-render/core";
2
- import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
- import { SnapCatalogView } from "./catalog-renderer";
4
- import {
5
- SnapThemeProvider,
6
- useSnapTheme,
7
- type SnapNativeColors,
8
- } from "./theme";
1
+ import type { ReactNode } from "react";
2
+ import type { ValidationResult } from "@farcaster/snap";
3
+ import { SPEC_VERSION_2 } from "@farcaster/snap";
4
+ import type { SnapNativeColors } from "./theme";
5
+ import type { JsonValue, SnapPage, SnapActionHandlers } from "./types";
6
+ import { useSnapTheme } from "./theme";
9
7
  import { hexToRgba } from "./use-snap-palette";
10
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
- import { ActivityIndicator, StyleSheet, View } from "react-native";
12
- import { ConfettiOverlay } from "./confetti-overlay";
13
- import {
14
- DEFAULT_THEME_ACCENT,
15
- PALETTE_LIGHT_HEX,
16
- PALETTE_DARK_HEX,
17
- type PaletteColor,
18
- } from "@farcaster/snap";
8
+ import { SnapCardV1 } from "./v1/snap-view";
9
+ import { SnapCardV2 } from "./v2/snap-view";
19
10
 
20
11
  // ─── Public types ──────────────────────────────────────
21
12
 
22
- export type JsonValue =
23
- | string
24
- | number
25
- | boolean
26
- | null
27
- | JsonValue[]
28
- | { [key: string]: JsonValue };
29
-
30
- export type SnapPage = {
31
- version: string;
32
- theme?: { accent?: string };
33
- effects?: string[];
34
- ui: Spec;
35
- };
36
-
37
- export type SnapActionHandlers = {
38
- submit: (target: string, inputs: Record<string, JsonValue>) => void;
39
- open_url: (target: string) => void;
40
- open_mini_app: (target: string) => void;
41
- view_cast: (params: { hash: string }) => void;
42
- view_profile: (params: { fid: number }) => void;
43
- compose_cast: (params: {
44
- text?: string;
45
- channelKey?: string;
46
- embeds?: string[];
47
- }) => void;
48
- view_token: (params: { token: string }) => void;
49
- send_token: (params: {
50
- token: string;
51
- amount?: string;
52
- recipientFid?: number;
53
- recipientAddress?: string;
54
- }) => void;
55
- swap_token: (params: { sellToken?: string; buyToken?: string }) => void;
56
- };
13
+ export type { JsonValue, SnapPage, SnapActionHandlers } from "./types";
57
14
 
58
15
  // ─── Re-exports ───────────────────────────────────────
59
16
 
60
17
  export { useSnapTheme, hexToRgba };
61
18
  export type { SnapNativeColors };
62
19
 
63
- // ─── Internal helpers ─────────────────────────────────
64
-
65
- function applyStatePaths(
66
- model: Record<string, unknown>,
67
- changes: { path: string; value: unknown }[] | Record<string, unknown>,
68
- ): void {
69
- const entries = Array.isArray(changes)
70
- ? changes.map((c) => [c.path, c.value] as const)
71
- : Object.entries(changes);
72
- for (const [path, value] of entries) {
73
- const trimmed = path.startsWith("/") ? path : `/${path}`;
74
- const parts = trimmed.split("/").filter(Boolean);
75
- if (parts.length < 2) continue;
76
- const [top, ...rest] = parts;
77
- if (top === "inputs") {
78
- if (typeof model.inputs !== "object" || model.inputs === null) {
79
- model.inputs = {};
80
- }
81
- const inputs = model.inputs as Record<string, unknown>;
82
- if (rest.length === 1) {
83
- inputs[rest[0]!] = value;
84
- }
85
- continue;
86
- }
87
- if (top === "theme") {
88
- if (typeof model.theme !== "object" || model.theme === null) {
89
- model.theme = {};
90
- }
91
- const theme = model.theme as Record<string, unknown>;
92
- if (rest.length === 1) {
93
- theme[rest[0]!] = value;
94
- }
95
- }
96
- }
97
- }
98
-
99
- function resolveAccentHex(
100
- accent: string | undefined,
101
- appearance: "light" | "dark",
102
- ): string {
103
- const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
104
- const name =
105
- accent && Object.hasOwn(map, accent)
106
- ? (accent as PaletteColor)
107
- : DEFAULT_THEME_ACCENT;
108
- return map[name];
109
- }
110
-
111
- // ─── SnapView ─────────────────────────────────────────
20
+ // ─── SnapCard (version-switching) ─────────────────────
112
21
 
113
- function SnapViewInner({
114
- snap,
115
- handlers,
116
- loading = false,
117
- }: {
118
- snap: SnapPage;
119
- handlers: SnapActionHandlers;
120
- loading?: boolean;
121
- }) {
122
- const { mode } = useSnapTheme();
123
- const spec = snap.ui;
124
- const accentHex = resolveAccentHex(snap.theme?.accent, mode);
125
-
126
- const showConfetti = snap.effects?.includes("confetti");
127
-
128
- // Increment key each time a new snap with confetti arrives so the overlay
129
- // unmounts/remounts and restarts its animation on every trigger.
130
- const confettiEpochRef = useRef(0);
131
- const lastConfettiSnapRef = useRef<typeof snap | null>(null);
132
- if (showConfetti && snap !== lastConfettiSnapRef.current) {
133
- confettiEpochRef.current++;
134
- lastConfettiSnapRef.current = snap;
135
- }
136
-
137
- const initialState = useMemo(
138
- () => ({
139
- ...(spec.state ?? {}),
140
- inputs: { ...((spec.state?.inputs ?? {}) as Record<string, unknown>) },
141
- theme: {
142
- ...((spec.state?.theme ?? {}) as Record<string, unknown>),
143
- ...(snap.theme ? { accent: snap.theme.accent } : {}),
144
- },
145
- }),
146
- [spec, snap.theme],
147
- );
148
-
149
- const stateRef = useRef<Record<string, unknown>>(initialState);
150
-
151
- useEffect(() => {
152
- stateRef.current = {
153
- inputs: {
154
- ...((initialState.inputs ?? {}) as Record<string, unknown>),
155
- },
156
- theme: {
157
- ...((initialState.theme ?? {}) as Record<string, unknown>),
158
- },
159
- };
160
- }, [initialState]);
161
-
162
- useEffect(() => {
163
- const result = snapJsonRenderCatalog.validate(spec);
164
- if (!result.success) {
165
- // eslint-disable-next-line no-console
166
- console.warn("[SnapView] catalog validation issues:", result.error);
167
- }
168
- }, [spec]);
169
-
170
- const [pageKey, setPageKey] = useState(0);
171
- useEffect(() => {
172
- setPageKey((k) => k + 1);
173
- }, [spec]);
174
-
175
- const handlersRef = useRef(handlers);
176
- handlersRef.current = handlers;
177
-
178
- const handleAction = useCallback((name: unknown, params: unknown) => {
179
- const inputs = (stateRef.current.inputs ?? {}) as Record<string, JsonValue>;
180
- const p = (params ?? {}) as Record<string, unknown>;
181
- const h = handlersRef.current;
182
- switch (name) {
183
- case "submit":
184
- h.submit(String(p.target ?? ""), inputs);
185
- break;
186
- case "open_url":
187
- h.open_url(String(p.target ?? ""));
188
- break;
189
- case "open_mini_app":
190
- h.open_mini_app(String(p.target ?? ""));
191
- break;
192
- case "view_cast":
193
- h.view_cast({ hash: String(p.hash ?? "") });
194
- break;
195
- case "view_profile":
196
- h.view_profile({ fid: Number(p.fid ?? 0) });
197
- break;
198
- case "compose_cast":
199
- h.compose_cast({
200
- text: p.text ? String(p.text) : undefined,
201
- channelKey: p.channelKey ? String(p.channelKey) : undefined,
202
- embeds: Array.isArray(p.embeds) ? (p.embeds as string[]) : undefined,
203
- });
204
- break;
205
- case "view_token":
206
- h.view_token({ token: String(p.token ?? "") });
207
- break;
208
- case "send_token":
209
- h.send_token({
210
- token: String(p.token ?? ""),
211
- amount: p.amount ? String(p.amount) : undefined,
212
- recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
213
- recipientAddress: p.recipientAddress
214
- ? String(p.recipientAddress)
215
- : undefined,
216
- });
217
- break;
218
- case "swap_token":
219
- h.swap_token({
220
- sellToken: p.sellToken ? String(p.sellToken) : undefined,
221
- buyToken: p.buyToken ? String(p.buyToken) : undefined,
222
- });
223
- break;
224
- default:
225
- break;
226
- }
227
- }, []);
228
-
229
- return (
230
- <View style={styles.container}>
231
- {loading ? (
232
- <View
233
- style={[
234
- styles.overlay,
235
- {
236
- backgroundColor:
237
- mode === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
238
- },
239
- ]}
240
- >
241
- <ActivityIndicator size="large" color={accentHex} />
242
- </View>
243
- ) : null}
244
- {showConfetti ? <ConfettiOverlay key={`confetti-${confettiEpochRef.current}`} /> : null}
245
- <SnapCatalogView
246
- key={pageKey}
247
- spec={spec}
248
- state={initialState}
249
- loading={false}
250
- onStateChange={(changes) => {
251
- applyStatePaths(stateRef.current, changes);
252
- }}
253
- onAction={handleAction}
254
- />
255
- </View>
256
- );
257
- }
258
-
259
- export function SnapView({
22
+ export function SnapCard({
260
23
  snap,
261
24
  handlers,
262
25
  loading = false,
263
26
  appearance = "dark",
264
27
  colors,
28
+ borderRadius = 16,
29
+ showOverflowWarning = false,
30
+ onValidationError,
31
+ validationErrorFallback,
265
32
  }: {
266
33
  snap: SnapPage;
267
34
  handlers: SnapActionHandlers;
268
35
  loading?: boolean;
269
36
  appearance?: "light" | "dark";
270
37
  colors?: Partial<SnapNativeColors>;
38
+ /** Border radius of the card (default 16). */
39
+ borderRadius?: number;
40
+ /** When true (v2 only), extends to 700px and shows a warning overlay below 500px. When false, clips at 500px. */
41
+ showOverflowWarning?: boolean;
42
+ /** Called when snap validation fails (v2 only). */
43
+ onValidationError?: (result: ValidationResult) => void;
44
+ /** Custom fallback rendered when validation fails (v2 only). */
45
+ validationErrorFallback?: ReactNode;
271
46
  }) {
47
+ if (snap.version === SPEC_VERSION_2) {
48
+ return (
49
+ <SnapCardV2
50
+ snap={snap}
51
+ handlers={handlers}
52
+ loading={loading}
53
+ appearance={appearance}
54
+ colors={colors}
55
+ borderRadius={borderRadius}
56
+ showOverflowWarning={showOverflowWarning}
57
+ onValidationError={onValidationError}
58
+ validationErrorFallback={validationErrorFallback}
59
+ />
60
+ );
61
+ }
62
+
272
63
  return (
273
- <SnapThemeProvider appearance={appearance} colors={colors}>
274
- <SnapViewInner snap={snap} handlers={handlers} loading={loading} />
275
- </SnapThemeProvider>
64
+ <SnapCardV1
65
+ snap={snap}
66
+ handlers={handlers}
67
+ loading={loading}
68
+ appearance={appearance}
69
+ colors={colors}
70
+ borderRadius={borderRadius}
71
+ />
276
72
  );
277
73
  }
278
-
279
- const styles = StyleSheet.create({
280
- container: {
281
- width: "100%",
282
- },
283
- overlay: {
284
- ...StyleSheet.absoluteFillObject,
285
- alignItems: "center",
286
- justifyContent: "center",
287
- zIndex: 10,
288
- },
289
- });
@@ -0,0 +1,209 @@
1
+ import type { Spec } from "@json-render/core";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapCatalogView } from "./catalog-renderer";
4
+ import { useSnapTheme } from "./theme";
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { ActivityIndicator, StyleSheet, View } from "react-native";
7
+ import {
8
+ DEFAULT_THEME_ACCENT,
9
+ PALETTE_LIGHT_HEX,
10
+ PALETTE_DARK_HEX,
11
+ type PaletteColor,
12
+ } from "@farcaster/snap";
13
+ import type { SnapPage, SnapActionHandlers, JsonValue } from "./types";
14
+
15
+ // ─── Shared helpers ──────────────────────────────────
16
+
17
+ export function applyStatePaths(
18
+ model: Record<string, unknown>,
19
+ changes: { path: string; value: unknown }[] | Record<string, unknown>,
20
+ ): void {
21
+ const entries = Array.isArray(changes)
22
+ ? changes.map((c) => [c.path, c.value] as const)
23
+ : Object.entries(changes);
24
+ for (const [path, value] of entries) {
25
+ const trimmed = path.startsWith("/") ? path : `/${path}`;
26
+ const parts = trimmed.split("/").filter(Boolean);
27
+ if (parts.length < 2) continue;
28
+ const [top, ...rest] = parts;
29
+ if (top === "inputs") {
30
+ if (typeof model.inputs !== "object" || model.inputs === null) {
31
+ model.inputs = {};
32
+ }
33
+ const inputs = model.inputs as Record<string, unknown>;
34
+ if (rest.length === 1) {
35
+ inputs[rest[0]!] = value;
36
+ }
37
+ continue;
38
+ }
39
+ if (top === "theme") {
40
+ if (typeof model.theme !== "object" || model.theme === null) {
41
+ model.theme = {};
42
+ }
43
+ const theme = model.theme as Record<string, unknown>;
44
+ if (rest.length === 1) {
45
+ theme[rest[0]!] = value;
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ export function resolveAccentHex(
52
+ accent: string | undefined,
53
+ appearance: "light" | "dark",
54
+ ): string {
55
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
56
+ const name =
57
+ accent && Object.hasOwn(map, accent)
58
+ ? (accent as PaletteColor)
59
+ : DEFAULT_THEME_ACCENT;
60
+ return map[name];
61
+ }
62
+
63
+ // ─── Core rendering component (no validation) ────────
64
+
65
+ export function SnapViewCoreInner({
66
+ snap,
67
+ handlers,
68
+ loading = false,
69
+ }: {
70
+ snap: SnapPage;
71
+ handlers: SnapActionHandlers;
72
+ loading?: boolean;
73
+ }) {
74
+ const { mode } = useSnapTheme();
75
+ const spec = snap.ui;
76
+ const accentHex = resolveAccentHex(snap.theme?.accent, mode);
77
+
78
+ const initialState = useMemo(
79
+ () => ({
80
+ ...(spec.state ?? {}),
81
+ inputs: { ...((spec.state?.inputs ?? {}) as Record<string, unknown>) },
82
+ theme: {
83
+ ...((spec.state?.theme ?? {}) as Record<string, unknown>),
84
+ ...(snap.theme ? { accent: snap.theme.accent } : {}),
85
+ },
86
+ }),
87
+ [spec, snap.theme],
88
+ );
89
+
90
+ const stateRef = useRef<Record<string, unknown>>(initialState);
91
+
92
+ useEffect(() => {
93
+ stateRef.current = {
94
+ inputs: {
95
+ ...((initialState.inputs ?? {}) as Record<string, unknown>),
96
+ },
97
+ theme: {
98
+ ...((initialState.theme ?? {}) as Record<string, unknown>),
99
+ },
100
+ };
101
+ }, [initialState]);
102
+
103
+ useEffect(() => {
104
+ const catalogResult = snapJsonRenderCatalog.validate(spec);
105
+ if (!catalogResult.success) {
106
+ // eslint-disable-next-line no-console
107
+ console.warn("[Snap] catalog validation issues:", catalogResult.error);
108
+ }
109
+ }, [spec]);
110
+
111
+ const [pageKey, setPageKey] = useState(0);
112
+ useEffect(() => {
113
+ setPageKey((k) => k + 1);
114
+ }, [spec]);
115
+
116
+ const handlersRef = useRef(handlers);
117
+ handlersRef.current = handlers;
118
+
119
+ const handleAction = useCallback((name: unknown, params: unknown) => {
120
+ const inputs = (stateRef.current.inputs ?? {}) as Record<string, JsonValue>;
121
+ const p = (params ?? {}) as Record<string, unknown>;
122
+ const h = handlersRef.current;
123
+ switch (name) {
124
+ case "submit":
125
+ h.submit(String(p.target ?? ""), inputs);
126
+ break;
127
+ case "open_url":
128
+ h.open_url(String(p.target ?? ""));
129
+ break;
130
+ case "open_mini_app":
131
+ h.open_mini_app(String(p.target ?? ""));
132
+ break;
133
+ case "view_cast":
134
+ h.view_cast({ hash: String(p.hash ?? "") });
135
+ break;
136
+ case "view_profile":
137
+ h.view_profile({ fid: Number(p.fid ?? 0) });
138
+ break;
139
+ case "compose_cast":
140
+ h.compose_cast({
141
+ text: p.text ? String(p.text) : undefined,
142
+ channelKey: p.channelKey ? String(p.channelKey) : undefined,
143
+ embeds: Array.isArray(p.embeds) ? (p.embeds as string[]) : undefined,
144
+ });
145
+ break;
146
+ case "view_token":
147
+ h.view_token({ token: String(p.token ?? "") });
148
+ break;
149
+ case "send_token":
150
+ h.send_token({
151
+ token: String(p.token ?? ""),
152
+ amount: p.amount ? String(p.amount) : undefined,
153
+ recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
154
+ recipientAddress: p.recipientAddress
155
+ ? String(p.recipientAddress)
156
+ : undefined,
157
+ });
158
+ break;
159
+ case "swap_token":
160
+ h.swap_token({
161
+ sellToken: p.sellToken ? String(p.sellToken) : undefined,
162
+ buyToken: p.buyToken ? String(p.buyToken) : undefined,
163
+ });
164
+ break;
165
+ default:
166
+ break;
167
+ }
168
+ }, []);
169
+
170
+ return (
171
+ <View style={styles.container}>
172
+ {loading ? (
173
+ <View
174
+ style={[
175
+ styles.overlay,
176
+ {
177
+ backgroundColor:
178
+ mode === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
179
+ },
180
+ ]}
181
+ >
182
+ <ActivityIndicator size="large" color={accentHex} />
183
+ </View>
184
+ ) : null}
185
+ <SnapCatalogView
186
+ key={pageKey}
187
+ spec={spec}
188
+ state={initialState}
189
+ loading={false}
190
+ onStateChange={(changes) => {
191
+ applyStatePaths(stateRef.current, changes);
192
+ }}
193
+ onAction={handleAction}
194
+ />
195
+ </View>
196
+ );
197
+ }
198
+
199
+ const styles = StyleSheet.create({
200
+ container: {
201
+ width: "100%",
202
+ },
203
+ overlay: {
204
+ ...StyleSheet.absoluteFillObject,
205
+ alignItems: "center",
206
+ justifyContent: "center",
207
+ zIndex: 10,
208
+ },
209
+ });
@@ -0,0 +1,37 @@
1
+ import type { Spec } from "@json-render/core";
2
+
3
+ export type JsonValue =
4
+ | string
5
+ | number
6
+ | boolean
7
+ | null
8
+ | JsonValue[]
9
+ | { [key: string]: JsonValue };
10
+
11
+ export type SnapPage = {
12
+ version: string;
13
+ theme?: { accent?: string };
14
+ effects?: string[];
15
+ ui: Spec;
16
+ };
17
+
18
+ export type SnapActionHandlers = {
19
+ submit: (target: string, inputs: Record<string, JsonValue>) => void;
20
+ open_url: (target: string) => void;
21
+ open_mini_app: (target: string) => void;
22
+ view_cast: (params: { hash: string }) => void;
23
+ view_profile: (params: { fid: number }) => void;
24
+ compose_cast: (params: {
25
+ text?: string;
26
+ channelKey?: string;
27
+ embeds?: string[];
28
+ }) => void;
29
+ view_token: (params: { token: string }) => void;
30
+ send_token: (params: {
31
+ token: string;
32
+ amount?: string;
33
+ recipientFid?: number;
34
+ recipientAddress?: string;
35
+ }) => void;
36
+ swap_token: (params: { sellToken?: string; buyToken?: string }) => void;
37
+ };
@@ -0,0 +1,108 @@
1
+ import { View, StyleSheet } from "react-native";
2
+ import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
3
+ import { SnapViewCoreInner } from "../snap-view-core";
4
+ import type { SnapPage, SnapActionHandlers } from "../types";
5
+
6
+ // ─── SnapViewV1 (no validation, no height limits) ────
7
+
8
+ export function SnapViewV1Inner({
9
+ snap,
10
+ handlers,
11
+ loading = false,
12
+ }: {
13
+ snap: SnapPage;
14
+ handlers: SnapActionHandlers;
15
+ loading?: boolean;
16
+ }) {
17
+ return (
18
+ <SnapViewCoreInner snap={snap} handlers={handlers} loading={loading} />
19
+ );
20
+ }
21
+
22
+ export function SnapViewV1({
23
+ snap,
24
+ handlers,
25
+ loading = false,
26
+ appearance = "dark",
27
+ colors,
28
+ }: {
29
+ snap: SnapPage;
30
+ handlers: SnapActionHandlers;
31
+ loading?: boolean;
32
+ appearance?: "light" | "dark";
33
+ colors?: Partial<SnapNativeColors>;
34
+ }) {
35
+ return (
36
+ <SnapThemeProvider appearance={appearance} colors={colors}>
37
+ <SnapViewV1Inner snap={snap} handlers={handlers} loading={loading} />
38
+ </SnapThemeProvider>
39
+ );
40
+ }
41
+
42
+ // ─── SnapCardV1 (card frame, no height limits) ───────
43
+
44
+ function SnapCardV1Inner({
45
+ snap,
46
+ handlers,
47
+ loading = false,
48
+ borderRadius,
49
+ }: {
50
+ snap: SnapPage;
51
+ handlers: SnapActionHandlers;
52
+ loading?: boolean;
53
+ borderRadius: number;
54
+ }) {
55
+ const { colors } = useSnapTheme();
56
+
57
+ return (
58
+ <View style={cardStyles.frameRing}>
59
+ <View
60
+ style={[
61
+ cardStyles.card,
62
+ {
63
+ borderRadius,
64
+ borderColor: colors.border,
65
+ backgroundColor: colors.surface,
66
+ },
67
+ ]}
68
+ >
69
+ <View style={cardStyles.body}>
70
+ <SnapViewV1Inner snap={snap} handlers={handlers} loading={loading} />
71
+ </View>
72
+ </View>
73
+ </View>
74
+ );
75
+ }
76
+
77
+ export function SnapCardV1({
78
+ snap,
79
+ handlers,
80
+ loading = false,
81
+ appearance = "dark",
82
+ colors,
83
+ borderRadius = 16,
84
+ }: {
85
+ snap: SnapPage;
86
+ handlers: SnapActionHandlers;
87
+ loading?: boolean;
88
+ appearance?: "light" | "dark";
89
+ colors?: Partial<SnapNativeColors>;
90
+ borderRadius?: number;
91
+ }) {
92
+ return (
93
+ <SnapThemeProvider appearance={appearance} colors={colors}>
94
+ <SnapCardV1Inner
95
+ snap={snap}
96
+ handlers={handlers}
97
+ loading={loading}
98
+ borderRadius={borderRadius}
99
+ />
100
+ </SnapThemeProvider>
101
+ );
102
+ }
103
+
104
+ const cardStyles = StyleSheet.create({
105
+ frameRing: { alignSelf: "stretch" },
106
+ card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
107
+ body: { paddingHorizontal: 16, paddingVertical: 16 },
108
+ });