@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.
- package/dist/colors.d.ts +4 -4
- package/dist/colors.js +20 -20
- package/dist/constants.d.ts +17 -1
- package/dist/constants.js +19 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.js +2 -4
- package/dist/react/accent-context.d.ts +3 -1
- package/dist/react/accent-context.js +7 -4
- package/dist/react/catalog-renderer.js +4 -0
- package/dist/react/components/action-button.d.ts +2 -1
- package/dist/react/components/action-button.js +35 -13
- package/dist/react/components/badge.js +8 -8
- package/dist/react/components/bar-chart.d.ts +5 -0
- package/dist/react/components/bar-chart.js +26 -0
- package/dist/react/components/cell-grid.d.ts +5 -0
- package/dist/react/components/cell-grid.js +87 -0
- package/dist/react/components/icon.js +4 -10
- package/dist/react/components/input.js +12 -6
- package/dist/react/components/item-group.js +3 -1
- package/dist/react/components/item.d.ts +3 -3
- package/dist/react/components/item.js +4 -3
- package/dist/react/components/progress.js +3 -3
- package/dist/react/components/separator.js +3 -1
- package/dist/react/components/slider.js +15 -10
- package/dist/react/components/switch.js +10 -12
- package/dist/react/components/text.js +6 -14
- package/dist/react/components/toggle-group.js +20 -6
- package/dist/react/hooks/use-snap-colors.d.ts +38 -0
- package/dist/react/hooks/use-snap-colors.js +81 -0
- package/dist/react/index.d.ts +13 -1
- package/dist/react/index.js +9 -188
- package/dist/react/snap-view-core.d.ts +11 -0
- package/dist/react/snap-view-core.js +227 -0
- package/dist/react/v1/snap-view.d.ts +16 -0
- package/dist/react/v1/snap-view.js +90 -0
- package/dist/react/v2/snap-view.d.ts +23 -0
- package/dist/react/v2/snap-view.js +91 -0
- package/dist/react-native/catalog-renderer.d.ts +5 -0
- package/dist/react-native/catalog-renderer.js +40 -0
- package/dist/react-native/components/snap-action-button.d.ts +2 -0
- package/dist/react-native/components/snap-action-button.js +69 -0
- package/dist/react-native/components/snap-badge.d.ts +2 -0
- package/dist/react-native/components/snap-badge.js +41 -0
- package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
- package/dist/react-native/components/snap-bar-chart.js +39 -0
- package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
- package/dist/react-native/components/snap-cell-grid.js +94 -0
- package/dist/react-native/components/snap-icon.d.ts +5 -0
- package/dist/react-native/components/snap-icon.js +56 -0
- package/dist/react-native/components/snap-image.d.ts +2 -0
- package/dist/react-native/components/snap-image.js +23 -0
- package/dist/react-native/components/snap-input.d.ts +2 -0
- package/dist/react-native/components/snap-input.js +37 -0
- package/dist/react-native/components/snap-item-group.d.ts +5 -0
- package/dist/react-native/components/snap-item-group.js +23 -0
- package/dist/react-native/components/snap-item.d.ts +5 -0
- package/dist/react-native/components/snap-item.js +42 -0
- package/dist/react-native/components/snap-progress.d.ts +2 -0
- package/dist/react-native/components/snap-progress.js +26 -0
- package/dist/react-native/components/snap-separator.d.ts +2 -0
- package/dist/react-native/components/snap-separator.js +23 -0
- package/dist/react-native/components/snap-slider.d.ts +2 -0
- package/dist/react-native/components/snap-slider.js +43 -0
- package/dist/react-native/components/snap-stack.d.ts +5 -0
- package/dist/react-native/components/snap-stack.js +49 -0
- package/dist/react-native/components/snap-switch.d.ts +2 -0
- package/dist/react-native/components/snap-switch.js +31 -0
- package/dist/react-native/components/snap-text.d.ts +2 -0
- package/dist/react-native/components/snap-text.js +35 -0
- package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
- package/dist/react-native/components/snap-toggle-group.js +99 -0
- package/dist/react-native/confetti-overlay.d.ts +1 -0
- package/dist/react-native/confetti-overlay.js +106 -0
- package/dist/react-native/index.d.ts +28 -0
- package/dist/react-native/index.js +15 -0
- package/dist/react-native/snap-view-core.d.ts +11 -0
- package/dist/react-native/snap-view-core.js +156 -0
- package/dist/react-native/theme.d.ts +27 -0
- package/dist/react-native/theme.js +43 -0
- package/dist/react-native/types.d.ts +42 -0
- package/dist/react-native/types.js +1 -0
- package/dist/react-native/use-snap-palette.d.ts +13 -0
- package/dist/react-native/use-snap-palette.js +48 -0
- package/dist/react-native/v1/snap-view.d.ts +24 -0
- package/dist/react-native/v1/snap-view.js +96 -0
- package/dist/react-native/v2/snap-view.d.ts +33 -0
- package/dist/react-native/v2/snap-view.js +114 -0
- package/dist/schemas.d.ts +100 -13
- package/dist/schemas.js +28 -10
- package/dist/server/parseRequest.d.ts +10 -0
- package/dist/server/parseRequest.js +48 -7
- package/dist/server/verify.d.ts +1 -0
- package/dist/server/verify.js +1 -0
- package/dist/ui/badge.d.ts +7 -2
- package/dist/ui/badge.js +2 -0
- package/dist/ui/bar-chart.d.ts +30 -0
- package/dist/ui/bar-chart.js +30 -0
- package/dist/ui/button.d.ts +4 -6
- package/dist/ui/button.js +1 -1
- package/dist/ui/catalog.d.ts +90 -16
- package/dist/ui/catalog.js +17 -3
- package/dist/ui/cell-grid.d.ts +34 -0
- package/dist/ui/cell-grid.js +39 -0
- package/dist/ui/icon.d.ts +2 -2
- package/dist/ui/image.d.ts +1 -2
- package/dist/ui/image.js +1 -1
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/item.d.ts +1 -3
- package/dist/ui/item.js +1 -1
- package/dist/ui/schema.d.ts +6 -2
- package/dist/ui/schema.js +2 -2
- package/dist/ui/slider.d.ts +1 -0
- package/dist/ui/slider.js +2 -0
- package/dist/ui/text.d.ts +2 -4
- package/dist/ui/text.js +2 -2
- package/dist/validator.d.ts +3 -2
- package/dist/validator.js +203 -2
- package/llms.txt +199 -0
- package/package.json +9 -3
- package/src/colors.ts +20 -20
- package/src/constants.ts +23 -1
- package/src/index.ts +16 -13
- package/src/react/accent-context.tsx +13 -6
- package/src/react/catalog-renderer.tsx +4 -0
- package/src/react/components/action-button.tsx +50 -20
- package/src/react/components/badge.tsx +14 -18
- package/src/react/components/bar-chart.tsx +69 -0
- package/src/react/components/cell-grid.tsx +128 -0
- package/src/react/components/icon.tsx +5 -18
- package/src/react/components/input.tsx +20 -9
- package/src/react/components/item-group.tsx +4 -1
- package/src/react/components/item.tsx +13 -10
- package/src/react/components/progress.tsx +12 -7
- package/src/react/components/separator.tsx +8 -1
- package/src/react/components/slider.tsx +28 -15
- package/src/react/components/switch.tsx +12 -16
- package/src/react/components/text.tsx +14 -23
- package/src/react/components/toggle-group.tsx +26 -9
- package/src/react/hooks/use-snap-colors.ts +128 -0
- package/src/react/index.tsx +49 -265
- package/src/react/snap-view-core.tsx +343 -0
- package/src/react/v1/snap-view.tsx +176 -0
- package/src/react/v2/snap-view.tsx +199 -0
- package/src/react-native/catalog-renderer.tsx +41 -0
- package/src/react-native/components/snap-action-button.tsx +96 -0
- package/src/react-native/components/snap-badge.tsx +60 -0
- package/src/react-native/components/snap-bar-chart.tsx +73 -0
- package/src/react-native/components/snap-cell-grid.tsx +150 -0
- package/src/react-native/components/snap-icon.tsx +102 -0
- package/src/react-native/components/snap-image.tsx +37 -0
- package/src/react-native/components/snap-input.tsx +58 -0
- package/src/react-native/components/snap-item-group.tsx +43 -0
- package/src/react-native/components/snap-item.tsx +66 -0
- package/src/react-native/components/snap-progress.tsx +40 -0
- package/src/react-native/components/snap-separator.tsx +32 -0
- package/src/react-native/components/snap-slider.tsx +85 -0
- package/src/react-native/components/snap-stack.tsx +66 -0
- package/src/react-native/components/snap-switch.tsx +46 -0
- package/src/react-native/components/snap-text.tsx +51 -0
- package/src/react-native/components/snap-toggle-group.tsx +127 -0
- package/src/react-native/confetti-overlay.tsx +134 -0
- package/src/react-native/index.tsx +83 -0
- package/src/react-native/snap-view-core.tsx +212 -0
- package/src/react-native/theme.tsx +85 -0
- package/src/react-native/types.ts +38 -0
- package/src/react-native/use-snap-palette.ts +64 -0
- package/src/react-native/v1/snap-view.tsx +229 -0
- package/src/react-native/v2/snap-view.tsx +283 -0
- package/src/schemas.ts +68 -17
- package/src/server/parseRequest.ts +68 -9
- package/src/server/verify.ts +2 -0
- package/src/ui/README.md +8 -8
- package/src/ui/badge.ts +2 -0
- package/src/ui/bar-chart.ts +38 -0
- package/src/ui/button.ts +1 -1
- package/src/ui/catalog.ts +19 -3
- package/src/ui/cell-grid.ts +49 -0
- package/src/ui/image.ts +1 -1
- package/src/ui/index.ts +6 -0
- package/src/ui/item.ts +1 -1
- package/src/ui/schema.ts +2 -2
- package/src/ui/slider.ts +2 -0
- package/src/ui/text.ts +2 -2
- package/src/validator.ts +251 -2
- package/dist/dataStore.d.ts +0 -12
- package/dist/dataStore.js +0 -35
- package/dist/middleware.d.ts +0 -3
- package/dist/middleware.js +0 -3
- package/dist/react/hooks/use-snap-accent.d.ts +0 -13
- package/dist/react/hooks/use-snap-accent.js +0 -32
- package/src/dataStore.ts +0 -62
- package/src/middleware.ts +0 -7
- package/src/react/hooks/use-snap-accent.ts +0 -45
package/dist/validator.js
CHANGED
|
@@ -1,8 +1,182 @@
|
|
|
1
1
|
import { snapResponseSchema } from "./schemas.js";
|
|
2
|
+
import { MAX_CHILDREN, MAX_DEPTH, MAX_ELEMENTS, MAX_ROOT_CHILDREN, SPEC_VERSION_1 } from "./constants.js";
|
|
3
|
+
import { snapJsonRenderCatalog } from "./ui/catalog.js";
|
|
4
|
+
// ─── Helpers ──────────────────────────────────────────
|
|
5
|
+
/** Actions whose `params.target` must be a valid URL. */
|
|
6
|
+
const URL_TARGET_ACTIONS = new Set([
|
|
7
|
+
"submit",
|
|
8
|
+
"open_url",
|
|
9
|
+
"open_snap",
|
|
10
|
+
"open_mini_app",
|
|
11
|
+
]);
|
|
12
|
+
/** Image file extensions allowed in image URLs. */
|
|
13
|
+
const ALLOWED_IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "gif", "webp"]);
|
|
2
14
|
/**
|
|
3
|
-
*
|
|
15
|
+
* Returns true if the URL is a loopback address (localhost dev exception).
|
|
16
|
+
*/
|
|
17
|
+
function isLoopback(url) {
|
|
18
|
+
const host = url.hostname;
|
|
19
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate a URL string: must be HTTPS (or HTTP on loopback for dev).
|
|
23
|
+
* Returns an error message or null if valid.
|
|
24
|
+
*/
|
|
25
|
+
function validateUrl(raw) {
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return `Invalid URL: "${raw}"`;
|
|
32
|
+
}
|
|
33
|
+
if (url.protocol === "https:")
|
|
34
|
+
return null;
|
|
35
|
+
if (url.protocol === "http:" && isLoopback(url))
|
|
36
|
+
return null;
|
|
37
|
+
if (url.protocol === "javascript:")
|
|
38
|
+
return `javascript: URIs are not allowed`;
|
|
39
|
+
return `URL must use HTTPS (got ${url.protocol.replace(":", "")}): "${raw}"`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate an image URL: must pass URL validation + have an allowed extension.
|
|
43
|
+
*/
|
|
44
|
+
function validateImageUrl(raw) {
|
|
45
|
+
const urlError = validateUrl(raw);
|
|
46
|
+
if (urlError)
|
|
47
|
+
return urlError;
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null; // already caught above
|
|
54
|
+
}
|
|
55
|
+
const pathname = url.pathname;
|
|
56
|
+
const lastDot = pathname.lastIndexOf(".");
|
|
57
|
+
if (lastDot === -1) {
|
|
58
|
+
return `Image URL must end with a supported extension (${[...ALLOWED_IMAGE_EXTENSIONS].join(", ")}): "${raw}"`;
|
|
59
|
+
}
|
|
60
|
+
const ext = pathname.slice(lastDot + 1).toLowerCase();
|
|
61
|
+
if (!ALLOWED_IMAGE_EXTENSIONS.has(ext)) {
|
|
62
|
+
return `Image URL has unsupported extension ".${ext}" (allowed: ${[...ALLOWED_IMAGE_EXTENSIONS].join(", ")}): "${raw}"`;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// ─── Depth measurement ────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Walk the element tree from `root` and return the max depth reached.
|
|
69
|
+
* Avoids infinite loops by tracking visited element ids.
|
|
70
|
+
*/
|
|
71
|
+
function measureDepth(elements, id, visited = new Set()) {
|
|
72
|
+
if (visited.has(id))
|
|
73
|
+
return 0;
|
|
74
|
+
visited.add(id);
|
|
75
|
+
const el = elements[id];
|
|
76
|
+
if (!el?.children?.length)
|
|
77
|
+
return 1;
|
|
78
|
+
let max = 0;
|
|
79
|
+
for (const childId of el.children) {
|
|
80
|
+
max = Math.max(max, measureDepth(elements, childId, visited));
|
|
81
|
+
}
|
|
82
|
+
return 1 + max;
|
|
83
|
+
}
|
|
84
|
+
// ─── Structural validation ────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Validate structural constraints on the snap UI tree:
|
|
87
|
+
* - root must reference an existing element
|
|
88
|
+
* - Total element count ≤ MAX_ELEMENTS
|
|
89
|
+
* - Children per element ≤ MAX_CHILDREN
|
|
90
|
+
* - Nesting depth ≤ MAX_DEPTH
|
|
91
|
+
*/
|
|
92
|
+
function validateStructure(ui) {
|
|
93
|
+
const issues = [];
|
|
94
|
+
const elements = ui.elements;
|
|
95
|
+
const elementCount = Object.keys(elements).length;
|
|
96
|
+
if (elementCount > MAX_ELEMENTS) {
|
|
97
|
+
issues.push({
|
|
98
|
+
code: "custom",
|
|
99
|
+
message: `Snap exceeds maximum of ${MAX_ELEMENTS} elements (found ${elementCount})`,
|
|
100
|
+
path: ["ui", "elements"],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Root element has a stricter children limit
|
|
104
|
+
const rootEl = elements[ui.root];
|
|
105
|
+
if (rootEl?.children && rootEl.children.length > MAX_ROOT_CHILDREN) {
|
|
106
|
+
issues.push({
|
|
107
|
+
code: "custom",
|
|
108
|
+
message: `Root element "${ui.root}" exceeds maximum of ${MAX_ROOT_CHILDREN} children (found ${rootEl.children.length})`,
|
|
109
|
+
path: ["ui", "elements", ui.root, "children"],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
for (const [id, el] of Object.entries(elements)) {
|
|
113
|
+
if (id === ui.root)
|
|
114
|
+
continue; // already checked above
|
|
115
|
+
if (el.children && el.children.length > MAX_CHILDREN) {
|
|
116
|
+
issues.push({
|
|
117
|
+
code: "custom",
|
|
118
|
+
message: `Element "${id}" exceeds maximum of ${MAX_CHILDREN} children (found ${el.children.length})`,
|
|
119
|
+
path: ["ui", "elements", id, "children"],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const depth = measureDepth(elements, ui.root);
|
|
124
|
+
if (depth > MAX_DEPTH) {
|
|
125
|
+
issues.push({
|
|
126
|
+
code: "custom",
|
|
127
|
+
message: `Snap exceeds maximum nesting depth of ${MAX_DEPTH} (found ${depth})`,
|
|
128
|
+
path: ["ui", "root"],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return issues;
|
|
132
|
+
}
|
|
133
|
+
// ─── URL validation ───────────────────────────────────
|
|
134
|
+
/**
|
|
135
|
+
* Validate all URLs in the snap:
|
|
136
|
+
* - image.url: must be HTTPS with allowed extension
|
|
137
|
+
* - action target URLs (submit, open_url, open_snap, open_mini_app): must be HTTPS
|
|
138
|
+
*/
|
|
139
|
+
function validateUrls(elements) {
|
|
140
|
+
const issues = [];
|
|
141
|
+
const els = elements;
|
|
142
|
+
for (const [id, el] of Object.entries(els)) {
|
|
143
|
+
// Validate image URLs
|
|
144
|
+
if (el.type === "image" && typeof el.props?.url === "string") {
|
|
145
|
+
const error = validateImageUrl(el.props.url);
|
|
146
|
+
if (error) {
|
|
147
|
+
issues.push({
|
|
148
|
+
code: "custom",
|
|
149
|
+
message: error,
|
|
150
|
+
path: ["ui", "elements", id, "props", "url"],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Validate action target URLs
|
|
155
|
+
if (el.on) {
|
|
156
|
+
for (const [event, binding] of Object.entries(el.on)) {
|
|
157
|
+
if (binding &&
|
|
158
|
+
URL_TARGET_ACTIONS.has(binding.action ?? "") &&
|
|
159
|
+
typeof binding.params?.target === "string") {
|
|
160
|
+
const error = validateUrl(binding.params.target);
|
|
161
|
+
if (error) {
|
|
162
|
+
issues.push({
|
|
163
|
+
code: "custom",
|
|
164
|
+
message: error,
|
|
165
|
+
path: ["ui", "elements", id, "on", event, "params", "target"],
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return issues;
|
|
173
|
+
}
|
|
174
|
+
// ─── Public API ───────────────────────────────────────
|
|
175
|
+
/**
|
|
176
|
+
* Validates a snap response against the schema, structural constraints, and URL rules.
|
|
4
177
|
* Element-level prop validation is handled by the json-render catalog.
|
|
5
|
-
* This validates the snap envelope (version, theme, effects, spec shape)
|
|
178
|
+
* This validates the snap envelope (version, theme, effects, spec shape)
|
|
179
|
+
* and enforces structural limits (element count, children, depth) and URL validation.
|
|
6
180
|
*/
|
|
7
181
|
export function validateSnapResponse(json) {
|
|
8
182
|
const parsed = snapResponseSchema.safeParse(json);
|
|
@@ -12,5 +186,32 @@ export function validateSnapResponse(json) {
|
|
|
12
186
|
issues: parsed.error.issues,
|
|
13
187
|
};
|
|
14
188
|
}
|
|
189
|
+
const ui = parsed.data.ui;
|
|
190
|
+
// Root reference check applies to all versions
|
|
191
|
+
if (!(ui.root in ui.elements)) {
|
|
192
|
+
return {
|
|
193
|
+
valid: false,
|
|
194
|
+
issues: [{
|
|
195
|
+
code: "custom",
|
|
196
|
+
message: `ui.root "${ui.root}" does not exist in ui.elements`,
|
|
197
|
+
path: ["ui", "root"],
|
|
198
|
+
}],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Structural limits and URL validation only apply to v2+ snaps
|
|
202
|
+
if (parsed.data.version !== SPEC_VERSION_1) {
|
|
203
|
+
const structuralIssues = validateStructure(ui);
|
|
204
|
+
if (structuralIssues.length > 0) {
|
|
205
|
+
return { valid: false, issues: structuralIssues };
|
|
206
|
+
}
|
|
207
|
+
const urlIssues = validateUrls(ui.elements);
|
|
208
|
+
if (urlIssues.length > 0) {
|
|
209
|
+
return { valid: false, issues: urlIssues };
|
|
210
|
+
}
|
|
211
|
+
const catalogResult = snapJsonRenderCatalog.validate(ui);
|
|
212
|
+
if (!catalogResult.success) {
|
|
213
|
+
return { valid: false, issues: catalogResult.error?.issues ?? [] };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
15
216
|
return { valid: true, issues: [] };
|
|
16
217
|
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# @farcaster/snap
|
|
2
|
+
|
|
3
|
+
> TypeScript SDK for building Farcaster Snaps — interactive feed cards driven by server-returned JSON. Provides schema validation, component catalog, React + React Native renderers, and server utilities.
|
|
4
|
+
|
|
5
|
+
## SnapResponse Format
|
|
6
|
+
|
|
7
|
+
Every snap handler returns a `SnapResponse`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"version": "2.0",
|
|
12
|
+
"theme": { "accent": "purple" },
|
|
13
|
+
"effects": ["confetti"],
|
|
14
|
+
"ui": {
|
|
15
|
+
"root": "page",
|
|
16
|
+
"elements": {
|
|
17
|
+
"page": { "type": "stack", "props": {}, "children": ["title", "btn"] },
|
|
18
|
+
"title": { "type": "text", "props": { "content": "Hello", "weight": "bold" } },
|
|
19
|
+
"btn": {
|
|
20
|
+
"type": "button",
|
|
21
|
+
"props": { "label": "Go", "variant": "primary" },
|
|
22
|
+
"on": { "press": { "action": "submit", "params": { "target": "https://example.com/" } } }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `{ accent: PaletteColor }`), `effects` (optional, `["confetti"]`), `ui` (required).
|
|
30
|
+
|
|
31
|
+
`ui.root` is the ID of the root element. `ui.elements` is a flat map of element ID to element definition.
|
|
32
|
+
|
|
33
|
+
## Structural Constraints
|
|
34
|
+
|
|
35
|
+
| Constraint | Limit |
|
|
36
|
+
|------------|-------|
|
|
37
|
+
| Total elements | Max **64** in `ui.elements` |
|
|
38
|
+
| Root children | Max **7** children on the root element |
|
|
39
|
+
| Children per element | Max **6** per non-root container (`stack`, `item_group`) |
|
|
40
|
+
| Nesting depth | Max **4** levels from root to deepest leaf |
|
|
41
|
+
|
|
42
|
+
## Components (16 total)
|
|
43
|
+
|
|
44
|
+
### Display Components
|
|
45
|
+
|
|
46
|
+
**badge** — Inline label with optional icon.
|
|
47
|
+
- `label` (string, required, max 30)
|
|
48
|
+
- `variant` (optional): `"default"` (filled) | `"outline"` (bordered). Default: `"default"`
|
|
49
|
+
- `color` (optional): PaletteColor. Default: `"accent"`
|
|
50
|
+
- `icon` (optional): IconName
|
|
51
|
+
|
|
52
|
+
**button** — Action trigger. Bind via `on.press`.
|
|
53
|
+
- `label` (string, required, max 30)
|
|
54
|
+
- `variant` (optional): `"primary"` (filled accent) | `"secondary"` (bordered). Default: `"secondary"`
|
|
55
|
+
- `icon` (optional): IconName
|
|
56
|
+
|
|
57
|
+
**icon** — Standalone Lucide icon.
|
|
58
|
+
- `name` (IconName, required)
|
|
59
|
+
- `color` (optional): PaletteColor. Default: `"accent"`
|
|
60
|
+
- `size` (optional): `"sm"` (16px) | `"md"` (20px). Default: `"md"`
|
|
61
|
+
|
|
62
|
+
**image** — HTTPS image with fixed aspect ratio.
|
|
63
|
+
- `url` (string, required)
|
|
64
|
+
- `aspect` (required): `"1:1"` | `"16:9"` | `"4:3"` | `"9:16"`
|
|
65
|
+
- `alt` (string, optional)
|
|
66
|
+
|
|
67
|
+
**item** — Content row with title and right-side actions slot.
|
|
68
|
+
- `title` (string, required, max 100)
|
|
69
|
+
- `description` (string, optional, max 160)
|
|
70
|
+
- `variant` (optional): `"default"`. Default: `"default"`
|
|
71
|
+
- Children render in the actions slot (right side)
|
|
72
|
+
|
|
73
|
+
**progress** — Horizontal progress bar.
|
|
74
|
+
- `value` (number, required, 0 to max)
|
|
75
|
+
- `max` (number, required, > 0)
|
|
76
|
+
- `label` (string, optional, max 60)
|
|
77
|
+
|
|
78
|
+
**separator** — Visual divider.
|
|
79
|
+
- `orientation` (optional): `"horizontal"` | `"vertical"`. Default: `"horizontal"`
|
|
80
|
+
|
|
81
|
+
**text** — Text block.
|
|
82
|
+
- `content` (string, required, max 320)
|
|
83
|
+
- `size` (optional): `"md"` (body) | `"sm"` (caption). Default: `"md"`
|
|
84
|
+
- `weight` (optional): `"bold"` | `"normal"`. Default: `"normal"`
|
|
85
|
+
- `align` (optional): `"left"` | `"center"` | `"right"`. Default: `"left"`
|
|
86
|
+
|
|
87
|
+
### Data Components
|
|
88
|
+
|
|
89
|
+
**bar_chart** — Horizontal bar chart with labeled bars.
|
|
90
|
+
- `bars` (array, required, 1–6 items): each `{ label: string (max 40), value: number (≥0), color?: PaletteColor }`
|
|
91
|
+
- `max` (number, optional, ≥0): ceiling value; defaults to max bar value
|
|
92
|
+
- `color` (optional): PaletteColor. Default bar color. Default: `"accent"`
|
|
93
|
+
|
|
94
|
+
**cell_grid** — Colored cell grid, optionally interactive.
|
|
95
|
+
- `name` (string, optional): POST inputs key. Default: `"grid_tap"`
|
|
96
|
+
- `cols` (number, required, 2–32)
|
|
97
|
+
- `rows` (number, required, 2–16)
|
|
98
|
+
- `cells` (array, required): sparse list of `{ row, col, color?: PaletteColor, content?: string }`
|
|
99
|
+
- `gap` (optional): `"none"` (0px) | `"sm"` (1px) | `"md"` (2px) | `"lg"` (4px). Default: `"sm"`
|
|
100
|
+
- `rowHeight` (number, optional, 8–64): pixel height per row. Default: 28. Grid height = rows × rowHeight
|
|
101
|
+
- `select` (optional): `"off"` | `"single"` | `"multiple"`. Default: `"off"`. Taps write to `inputs[name]`
|
|
102
|
+
|
|
103
|
+
### Container Components
|
|
104
|
+
|
|
105
|
+
**stack** — Layout container.
|
|
106
|
+
- `direction` (optional): `"vertical"` | `"horizontal"`. Default: `"vertical"`
|
|
107
|
+
- `gap` (optional): `"none"` | `"sm"` | `"md"` | `"lg"`. Default: `"md"`
|
|
108
|
+
- `justify` (optional): `"start"` | `"center"` | `"end"` | `"between"` | `"around"`
|
|
109
|
+
- Children are element IDs
|
|
110
|
+
|
|
111
|
+
**item_group** — Groups item children.
|
|
112
|
+
- `border` (boolean, optional)
|
|
113
|
+
- `separator` (boolean, optional)
|
|
114
|
+
- `gap` (optional): `"none"` | `"sm"` | `"md"` | `"lg"`
|
|
115
|
+
- Children must be item elements
|
|
116
|
+
|
|
117
|
+
### Field Components
|
|
118
|
+
|
|
119
|
+
Field values are sent in POST `inputs[name]` when a `submit` action fires.
|
|
120
|
+
|
|
121
|
+
**input** — Text or number input.
|
|
122
|
+
- `name` (string, required)
|
|
123
|
+
- `type` (optional): `"text"` | `"number"`. Default: `"text"`
|
|
124
|
+
- `label` (string, optional, max 60)
|
|
125
|
+
- `placeholder` (string, optional, max 60)
|
|
126
|
+
- `defaultValue` (string, optional)
|
|
127
|
+
- `maxLength` (number, optional, 1-280)
|
|
128
|
+
- POST value: string
|
|
129
|
+
|
|
130
|
+
**slider** — Numeric range.
|
|
131
|
+
- `name` (string, required)
|
|
132
|
+
- `min` (number, required)
|
|
133
|
+
- `max` (number, required, >= min)
|
|
134
|
+
- `step` (number, optional, > 0. Default: 1)
|
|
135
|
+
- `defaultValue` (number, optional, between min and max)
|
|
136
|
+
- `label` (string, optional, max 60)
|
|
137
|
+
- `showValue` (boolean, optional): display the current value next to the label
|
|
138
|
+
- POST value: number
|
|
139
|
+
|
|
140
|
+
**switch** — Boolean toggle.
|
|
141
|
+
- `name` (string, required)
|
|
142
|
+
- `label` (string, optional, max 60)
|
|
143
|
+
- `defaultChecked` (boolean, optional)
|
|
144
|
+
- POST value: boolean
|
|
145
|
+
|
|
146
|
+
**toggle_group** — Single or multi-select choice group.
|
|
147
|
+
- `name` (string, required)
|
|
148
|
+
- `options` (string[], required, 2-6 items, each max 30 chars)
|
|
149
|
+
- `multiple` (boolean, optional)
|
|
150
|
+
- `orientation` (optional): `"horizontal"` | `"vertical"`. Default: `"horizontal"`
|
|
151
|
+
- `defaultValue` (string | string[], optional)
|
|
152
|
+
- `variant` (optional): `"default"` | `"outline"`. Default: `"default"`
|
|
153
|
+
- `label` (string, optional, max 60)
|
|
154
|
+
- POST value: string (single) or string[] (multiple)
|
|
155
|
+
|
|
156
|
+
## Actions (10 types)
|
|
157
|
+
|
|
158
|
+
Bound to buttons via `on.press`:
|
|
159
|
+
|
|
160
|
+
| Action | Params | Description |
|
|
161
|
+
|--------|--------|-------------|
|
|
162
|
+
| `submit` | `target` (URL) | POST to server, get next page |
|
|
163
|
+
| `open_url` | `target` (URL) | Open external URL in browser |
|
|
164
|
+
| `open_snap` | `target` (URL) | Open a snap URL inline |
|
|
165
|
+
| `open_mini_app` | `target` (URL) | Open as Farcaster mini app |
|
|
166
|
+
| `view_cast` | `hash` (string) | Navigate to a cast |
|
|
167
|
+
| `view_profile` | `fid` (number) | Navigate to a profile |
|
|
168
|
+
| `compose_cast` | `text?`, `channelKey?`, `embeds?` | Open cast composer |
|
|
169
|
+
| `view_token` | `token` (CAIP-19) | View token in wallet |
|
|
170
|
+
| `send_token` | `token`, `amount?`, `recipientFid?`, `recipientAddress?` | Send token flow |
|
|
171
|
+
| `swap_token` | `sellToken?`, `buyToken?` | Swap token flow |
|
|
172
|
+
|
|
173
|
+
## Icon Names (34)
|
|
174
|
+
|
|
175
|
+
`arrow-right`, `arrow-left`, `external-link`, `chevron-right`, `check`, `x`, `alert-triangle`, `info`, `clock`, `heart`, `message-circle`, `repeat`, `share`, `user`, `users`, `star`, `trophy`, `zap`, `flame`, `gift`, `image`, `play`, `pause`, `wallet`, `coins`, `plus`, `minus`, `refresh-cw`, `bookmark`, `thumbs-up`, `thumbs-down`, `trending-up`, `trending-down`
|
|
176
|
+
|
|
177
|
+
## Color Palette
|
|
178
|
+
|
|
179
|
+
`gray`, `blue`, `red`, `amber`, `green`, `teal`, `purple`, `pink`
|
|
180
|
+
|
|
181
|
+
Plus the special value `"accent"` which references `theme.accent`.
|
|
182
|
+
|
|
183
|
+
## Package Exports
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
187
|
+
import { parseRequest, verifyJFSRequestBody } from "@farcaster/snap/server";
|
|
188
|
+
import { withTursoServerless, createInMemoryDataStore } from "@farcaster/snap-turso";
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- `@farcaster/snap` — schemas, types, validation
|
|
192
|
+
- `@farcaster/snap/ui` — json-render catalog, component schemas
|
|
193
|
+
- `@farcaster/snap/server` — request parsing, JFS verification
|
|
194
|
+
- `@farcaster/snap-hono` — Hono adapter (`registerSnapHandler`)
|
|
195
|
+
- `@farcaster/snap-turso` — `withTursoServerless`, `DataStore` / `DataStoreValue`, in-memory and Turso helpers
|
|
196
|
+
|
|
197
|
+
## Full Documentation
|
|
198
|
+
|
|
199
|
+
https://docs.farcaster.xyz/snap
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farcaster/snap",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Farcaster Snaps 🫰",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -100,11 +100,17 @@
|
|
|
100
100
|
"types": "./dist/react/index.d.ts",
|
|
101
101
|
"import": "./dist/react/index.js",
|
|
102
102
|
"default": "./dist/react/index.js"
|
|
103
|
+
},
|
|
104
|
+
"./react-native": {
|
|
105
|
+
"types": "./dist/react-native/index.d.ts",
|
|
106
|
+
"import": "./dist/react-native/index.js",
|
|
107
|
+
"default": "./dist/react-native/index.js"
|
|
103
108
|
}
|
|
104
109
|
},
|
|
105
110
|
"files": [
|
|
106
111
|
"dist",
|
|
107
|
-
"src"
|
|
112
|
+
"src",
|
|
113
|
+
"llms.txt"
|
|
108
114
|
],
|
|
109
115
|
"publishConfig": {
|
|
110
116
|
"access": "public"
|
|
@@ -147,7 +153,7 @@
|
|
|
147
153
|
"zod": "^4.0.0"
|
|
148
154
|
},
|
|
149
155
|
"scripts": {
|
|
150
|
-
"build": "tsc && tsc-alias --resolve-full-paths --resolve-full-extension .js",
|
|
156
|
+
"build": "tsc && (tsc -p tsconfig.react-native.json || true) && tsc-alias --resolve-full-paths --resolve-full-extension .js",
|
|
151
157
|
"clean": "rm -rf dist",
|
|
152
158
|
"test": "vitest run",
|
|
153
159
|
"typecheck": "tsc --noEmit"
|
package/src/colors.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* it to a hex value appropriate for its current light/dark mode.
|
|
4
4
|
*
|
|
5
5
|
* Light-mode hex values (used by emulator):
|
|
6
|
-
* gray=#
|
|
7
|
-
* green=#
|
|
6
|
+
* gray=#6E6A86 blue=#286983 red=#B4637A amber=#EA9D34
|
|
7
|
+
* green=#3E8F8F teal=#56949F purple=#907AA9 pink=#D7827E
|
|
8
8
|
*
|
|
9
9
|
* Dark-mode hex values (for reference; client-owned):
|
|
10
|
-
* gray=#
|
|
11
|
-
* green=#
|
|
10
|
+
* gray=#908CAA blue=#9CCFD8 red=#EB6F92 amber=#F6C177
|
|
11
|
+
* green=#56D4A4 teal=#3E8FB0 purple=#C4A7E7 pink=#EBBCBA
|
|
12
12
|
*/
|
|
13
13
|
export const PALETTE_COLOR = {
|
|
14
14
|
gray: "gray",
|
|
@@ -40,26 +40,26 @@ export type PaletteColor = (typeof PALETTE_COLOR_VALUES)[number];
|
|
|
40
40
|
|
|
41
41
|
/** Light-mode hex for each palette color (emulator / reference client). */
|
|
42
42
|
export const PALETTE_LIGHT_HEX: Record<PaletteColor, string> = {
|
|
43
|
-
gray: "#
|
|
44
|
-
blue: "#
|
|
45
|
-
red: "#
|
|
46
|
-
amber: "#
|
|
47
|
-
green: "#
|
|
48
|
-
teal: "#
|
|
49
|
-
purple: "#
|
|
50
|
-
pink: "#
|
|
43
|
+
gray: "#6E6A86",
|
|
44
|
+
blue: "#286983",
|
|
45
|
+
red: "#B4637A",
|
|
46
|
+
amber: "#EA9D34",
|
|
47
|
+
green: "#3E8F8F",
|
|
48
|
+
teal: "#56949F",
|
|
49
|
+
purple: "#907AA9",
|
|
50
|
+
pink: "#D7827E",
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
/** Dark-mode hex for each palette color (reference). */
|
|
54
54
|
export const PALETTE_DARK_HEX: Record<PaletteColor, string> = {
|
|
55
|
-
gray: "#
|
|
56
|
-
blue: "#
|
|
57
|
-
red: "#
|
|
58
|
-
amber: "#
|
|
59
|
-
green: "#
|
|
60
|
-
teal: "#
|
|
61
|
-
purple: "#
|
|
62
|
-
pink: "#
|
|
55
|
+
gray: "#908CAA",
|
|
56
|
+
blue: "#9CCFD8",
|
|
57
|
+
red: "#EB6F92",
|
|
58
|
+
amber: "#F6C177",
|
|
59
|
+
green: "#56D4A4",
|
|
60
|
+
teal: "#3E8FB0",
|
|
61
|
+
purple: "#C4A7E7",
|
|
62
|
+
pink: "#EBBCBA",
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
export const PROGRESS_COLOR_VALUES = [
|
package/src/constants.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const SPEC_VERSION_1 = "1.0" as const;
|
|
2
|
+
export const SPEC_VERSION_2 = "2.0" as const;
|
|
3
|
+
export const SPEC_VERSION = SPEC_VERSION_2;
|
|
4
|
+
export const SUPPORTED_SPEC_VERSIONS = [SPEC_VERSION_1, SPEC_VERSION_2] as const;
|
|
5
|
+
export type SpecVersion = (typeof SUPPORTED_SPEC_VERSIONS)[number];
|
|
2
6
|
|
|
3
7
|
export const MEDIA_TYPE = "application/vnd.farcaster.snap+json" as const;
|
|
4
8
|
|
|
5
9
|
export const EFFECT_VALUES = ["confetti"] as const;
|
|
10
|
+
|
|
11
|
+
// ─── Pixel grid ────────────────────────────────────────
|
|
12
|
+
export const POST_GRID_TAP_KEY = "grid_tap" as const;
|
|
13
|
+
export const GRID_MIN_COLS = 2;
|
|
14
|
+
export const GRID_MAX_COLS = 32;
|
|
15
|
+
export const GRID_MIN_ROWS = 2;
|
|
16
|
+
export const GRID_MAX_ROWS = 16;
|
|
17
|
+
export const GRID_GAP_VALUES = ["none", "sm", "md", "lg"] as const;
|
|
18
|
+
|
|
19
|
+
// ─── Snap structural limits ───────────────────────────
|
|
20
|
+
export const MAX_ELEMENTS = 64;
|
|
21
|
+
export const MAX_ROOT_CHILDREN = 7;
|
|
22
|
+
export const MAX_CHILDREN = 6;
|
|
23
|
+
export const MAX_DEPTH = 4;
|
|
24
|
+
|
|
25
|
+
// ─── Bar chart ─────────────────────────────────────────
|
|
26
|
+
export const BAR_CHART_MAX_BARS = 6;
|
|
27
|
+
export const BAR_CHART_LABEL_MAX_CHARS = 40;
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type {
|
|
2
|
+
Spec as SnapSpec,
|
|
3
|
+
UIElement as SnapUIElement,
|
|
4
|
+
} from "@json-render/core";
|
|
2
5
|
export {
|
|
3
6
|
SPEC_VERSION,
|
|
7
|
+
SPEC_VERSION_1,
|
|
8
|
+
SPEC_VERSION_2,
|
|
9
|
+
SUPPORTED_SPEC_VERSIONS,
|
|
10
|
+
type SpecVersion,
|
|
4
11
|
MEDIA_TYPE,
|
|
5
12
|
EFFECT_VALUES,
|
|
13
|
+
POST_GRID_TAP_KEY,
|
|
14
|
+
MAX_ELEMENTS,
|
|
15
|
+
MAX_ROOT_CHILDREN,
|
|
16
|
+
MAX_CHILDREN,
|
|
17
|
+
MAX_DEPTH,
|
|
6
18
|
} from "./constants";
|
|
7
19
|
export {
|
|
8
20
|
DEFAULT_THEME_ACCENT,
|
|
@@ -22,18 +34,9 @@ export {
|
|
|
22
34
|
type SnapContext,
|
|
23
35
|
type SnapResponse,
|
|
24
36
|
type SnapHandlerResult,
|
|
37
|
+
type SnapElementInput,
|
|
38
|
+
type SnapSpecInput,
|
|
25
39
|
type SnapFunction,
|
|
26
40
|
type SnapPayload,
|
|
27
41
|
} from "./schemas";
|
|
28
|
-
export {
|
|
29
|
-
validateSnapResponse,
|
|
30
|
-
type ValidationResult,
|
|
31
|
-
} from "./validator";
|
|
32
|
-
export {
|
|
33
|
-
type DataStoreValue,
|
|
34
|
-
type SnapDataStore,
|
|
35
|
-
type SnapDataStoreOperations,
|
|
36
|
-
createDefaultDataStore,
|
|
37
|
-
createInMemoryDataStore,
|
|
38
|
-
} from "./dataStore";
|
|
39
|
-
export { type Middleware, useMiddleware } from "./middleware";
|
|
42
|
+
export { validateSnapResponse, type ValidationResult } from "./validator";
|
|
@@ -2,28 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, type ReactNode } from "react";
|
|
4
4
|
|
|
5
|
-
type
|
|
5
|
+
type SnapPreviewContextValue = {
|
|
6
6
|
/** From loaded snap `page.theme.accent` (undefined if the snap omits it). */
|
|
7
7
|
pageAccent: string | undefined;
|
|
8
|
+
/** Light/dark appearance passed from SnapCard. */
|
|
9
|
+
appearance: "light" | "dark";
|
|
8
10
|
};
|
|
9
11
|
|
|
10
|
-
const
|
|
11
|
-
createContext<SnapPreviewAccentContextValue | null>(null);
|
|
12
|
+
const SnapPreviewContext = createContext<SnapPreviewContextValue | null>(null);
|
|
12
13
|
|
|
13
14
|
export function SnapPreviewAccentProvider({
|
|
14
15
|
pageAccent,
|
|
16
|
+
appearance = "dark",
|
|
15
17
|
children,
|
|
16
18
|
}: {
|
|
17
19
|
pageAccent: string | undefined;
|
|
20
|
+
appearance?: "light" | "dark";
|
|
18
21
|
children: ReactNode;
|
|
19
22
|
}) {
|
|
20
23
|
return (
|
|
21
|
-
<
|
|
24
|
+
<SnapPreviewContext.Provider value={{ pageAccent, appearance }}>
|
|
22
25
|
{children}
|
|
23
|
-
</
|
|
26
|
+
</SnapPreviewContext.Provider>
|
|
24
27
|
);
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export function useSnapPreviewPageAccent(): string | undefined {
|
|
28
|
-
return useContext(
|
|
31
|
+
return useContext(SnapPreviewContext)?.pageAccent;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useSnapAppearance(): "light" | "dark" {
|
|
35
|
+
return useContext(SnapPreviewContext)?.appearance ?? "dark";
|
|
29
36
|
}
|
|
@@ -16,6 +16,8 @@ import { SnapStack } from "./components/stack";
|
|
|
16
16
|
import { SnapSwitch } from "./components/switch";
|
|
17
17
|
import { SnapText } from "./components/text";
|
|
18
18
|
import { SnapToggleGroup } from "./components/toggle-group";
|
|
19
|
+
import { SnapBarChart } from "./components/bar-chart";
|
|
20
|
+
import { SnapCellGrid } from "./components/cell-grid";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Maps snap json-render catalog types to React components.
|
|
@@ -36,4 +38,6 @@ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
|
|
|
36
38
|
switch: SnapSwitch,
|
|
37
39
|
text: SnapText,
|
|
38
40
|
toggle_group: SnapToggleGroup,
|
|
41
|
+
bar_chart: SnapBarChart,
|
|
42
|
+
cell_grid: SnapCellGrid,
|
|
39
43
|
});
|