@farcaster/snap 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/colors.d.ts +4 -4
  2. package/dist/colors.js +20 -20
  3. package/dist/constants.d.ts +17 -1
  4. package/dist/constants.js +19 -1
  5. package/dist/index.d.ts +4 -6
  6. package/dist/index.js +2 -4
  7. package/dist/react/accent-context.d.ts +3 -1
  8. package/dist/react/accent-context.js +7 -4
  9. package/dist/react/catalog-renderer.js +4 -0
  10. package/dist/react/components/action-button.d.ts +2 -1
  11. package/dist/react/components/action-button.js +35 -13
  12. package/dist/react/components/badge.js +8 -8
  13. package/dist/react/components/bar-chart.d.ts +5 -0
  14. package/dist/react/components/bar-chart.js +26 -0
  15. package/dist/react/components/cell-grid.d.ts +5 -0
  16. package/dist/react/components/cell-grid.js +87 -0
  17. package/dist/react/components/icon.js +4 -10
  18. package/dist/react/components/input.js +12 -6
  19. package/dist/react/components/item-group.js +3 -1
  20. package/dist/react/components/item.d.ts +3 -3
  21. package/dist/react/components/item.js +4 -3
  22. package/dist/react/components/progress.js +3 -3
  23. package/dist/react/components/separator.js +3 -1
  24. package/dist/react/components/slider.js +15 -10
  25. package/dist/react/components/switch.js +10 -12
  26. package/dist/react/components/text.js +6 -14
  27. package/dist/react/components/toggle-group.js +20 -6
  28. package/dist/react/hooks/use-snap-colors.d.ts +38 -0
  29. package/dist/react/hooks/use-snap-colors.js +81 -0
  30. package/dist/react/index.d.ts +13 -1
  31. package/dist/react/index.js +9 -188
  32. package/dist/react/snap-view-core.d.ts +11 -0
  33. package/dist/react/snap-view-core.js +227 -0
  34. package/dist/react/v1/snap-view.d.ts +16 -0
  35. package/dist/react/v1/snap-view.js +90 -0
  36. package/dist/react/v2/snap-view.d.ts +23 -0
  37. package/dist/react/v2/snap-view.js +91 -0
  38. package/dist/react-native/catalog-renderer.d.ts +5 -0
  39. package/dist/react-native/catalog-renderer.js +40 -0
  40. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  41. package/dist/react-native/components/snap-action-button.js +69 -0
  42. package/dist/react-native/components/snap-badge.d.ts +2 -0
  43. package/dist/react-native/components/snap-badge.js +41 -0
  44. package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
  45. package/dist/react-native/components/snap-bar-chart.js +39 -0
  46. package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
  47. package/dist/react-native/components/snap-cell-grid.js +94 -0
  48. package/dist/react-native/components/snap-icon.d.ts +5 -0
  49. package/dist/react-native/components/snap-icon.js +56 -0
  50. package/dist/react-native/components/snap-image.d.ts +2 -0
  51. package/dist/react-native/components/snap-image.js +23 -0
  52. package/dist/react-native/components/snap-input.d.ts +2 -0
  53. package/dist/react-native/components/snap-input.js +37 -0
  54. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  55. package/dist/react-native/components/snap-item-group.js +23 -0
  56. package/dist/react-native/components/snap-item.d.ts +5 -0
  57. package/dist/react-native/components/snap-item.js +42 -0
  58. package/dist/react-native/components/snap-progress.d.ts +2 -0
  59. package/dist/react-native/components/snap-progress.js +26 -0
  60. package/dist/react-native/components/snap-separator.d.ts +2 -0
  61. package/dist/react-native/components/snap-separator.js +23 -0
  62. package/dist/react-native/components/snap-slider.d.ts +2 -0
  63. package/dist/react-native/components/snap-slider.js +43 -0
  64. package/dist/react-native/components/snap-stack.d.ts +5 -0
  65. package/dist/react-native/components/snap-stack.js +49 -0
  66. package/dist/react-native/components/snap-switch.d.ts +2 -0
  67. package/dist/react-native/components/snap-switch.js +31 -0
  68. package/dist/react-native/components/snap-text.d.ts +2 -0
  69. package/dist/react-native/components/snap-text.js +35 -0
  70. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  71. package/dist/react-native/components/snap-toggle-group.js +99 -0
  72. package/dist/react-native/confetti-overlay.d.ts +1 -0
  73. package/dist/react-native/confetti-overlay.js +106 -0
  74. package/dist/react-native/index.d.ts +28 -0
  75. package/dist/react-native/index.js +15 -0
  76. package/dist/react-native/snap-view-core.d.ts +11 -0
  77. package/dist/react-native/snap-view-core.js +156 -0
  78. package/dist/react-native/theme.d.ts +27 -0
  79. package/dist/react-native/theme.js +43 -0
  80. package/dist/react-native/types.d.ts +42 -0
  81. package/dist/react-native/types.js +1 -0
  82. package/dist/react-native/use-snap-palette.d.ts +13 -0
  83. package/dist/react-native/use-snap-palette.js +48 -0
  84. package/dist/react-native/v1/snap-view.d.ts +24 -0
  85. package/dist/react-native/v1/snap-view.js +96 -0
  86. package/dist/react-native/v2/snap-view.d.ts +33 -0
  87. package/dist/react-native/v2/snap-view.js +114 -0
  88. package/dist/schemas.d.ts +100 -13
  89. package/dist/schemas.js +28 -10
  90. package/dist/server/parseRequest.d.ts +10 -0
  91. package/dist/server/parseRequest.js +48 -7
  92. package/dist/server/verify.d.ts +1 -0
  93. package/dist/server/verify.js +1 -0
  94. package/dist/ui/badge.d.ts +7 -2
  95. package/dist/ui/badge.js +2 -0
  96. package/dist/ui/bar-chart.d.ts +30 -0
  97. package/dist/ui/bar-chart.js +30 -0
  98. package/dist/ui/button.d.ts +4 -6
  99. package/dist/ui/button.js +1 -1
  100. package/dist/ui/catalog.d.ts +90 -16
  101. package/dist/ui/catalog.js +17 -3
  102. package/dist/ui/cell-grid.d.ts +34 -0
  103. package/dist/ui/cell-grid.js +39 -0
  104. package/dist/ui/icon.d.ts +2 -2
  105. package/dist/ui/image.d.ts +1 -2
  106. package/dist/ui/image.js +1 -1
  107. package/dist/ui/index.d.ts +4 -0
  108. package/dist/ui/index.js +2 -0
  109. package/dist/ui/item.d.ts +1 -3
  110. package/dist/ui/item.js +1 -1
  111. package/dist/ui/schema.d.ts +6 -2
  112. package/dist/ui/schema.js +2 -2
  113. package/dist/ui/slider.d.ts +1 -0
  114. package/dist/ui/slider.js +2 -0
  115. package/dist/ui/text.d.ts +2 -4
  116. package/dist/ui/text.js +2 -2
  117. package/dist/validator.d.ts +3 -2
  118. package/dist/validator.js +203 -2
  119. package/llms.txt +199 -0
  120. package/package.json +9 -3
  121. package/src/colors.ts +20 -20
  122. package/src/constants.ts +23 -1
  123. package/src/index.ts +16 -13
  124. package/src/react/accent-context.tsx +13 -6
  125. package/src/react/catalog-renderer.tsx +4 -0
  126. package/src/react/components/action-button.tsx +50 -20
  127. package/src/react/components/badge.tsx +14 -18
  128. package/src/react/components/bar-chart.tsx +69 -0
  129. package/src/react/components/cell-grid.tsx +128 -0
  130. package/src/react/components/icon.tsx +5 -18
  131. package/src/react/components/input.tsx +20 -9
  132. package/src/react/components/item-group.tsx +4 -1
  133. package/src/react/components/item.tsx +13 -10
  134. package/src/react/components/progress.tsx +12 -7
  135. package/src/react/components/separator.tsx +8 -1
  136. package/src/react/components/slider.tsx +28 -15
  137. package/src/react/components/switch.tsx +12 -16
  138. package/src/react/components/text.tsx +14 -23
  139. package/src/react/components/toggle-group.tsx +26 -9
  140. package/src/react/hooks/use-snap-colors.ts +128 -0
  141. package/src/react/index.tsx +49 -265
  142. package/src/react/snap-view-core.tsx +343 -0
  143. package/src/react/v1/snap-view.tsx +176 -0
  144. package/src/react/v2/snap-view.tsx +199 -0
  145. package/src/react-native/catalog-renderer.tsx +41 -0
  146. package/src/react-native/components/snap-action-button.tsx +96 -0
  147. package/src/react-native/components/snap-badge.tsx +60 -0
  148. package/src/react-native/components/snap-bar-chart.tsx +73 -0
  149. package/src/react-native/components/snap-cell-grid.tsx +150 -0
  150. package/src/react-native/components/snap-icon.tsx +102 -0
  151. package/src/react-native/components/snap-image.tsx +37 -0
  152. package/src/react-native/components/snap-input.tsx +58 -0
  153. package/src/react-native/components/snap-item-group.tsx +43 -0
  154. package/src/react-native/components/snap-item.tsx +66 -0
  155. package/src/react-native/components/snap-progress.tsx +40 -0
  156. package/src/react-native/components/snap-separator.tsx +32 -0
  157. package/src/react-native/components/snap-slider.tsx +85 -0
  158. package/src/react-native/components/snap-stack.tsx +66 -0
  159. package/src/react-native/components/snap-switch.tsx +46 -0
  160. package/src/react-native/components/snap-text.tsx +51 -0
  161. package/src/react-native/components/snap-toggle-group.tsx +127 -0
  162. package/src/react-native/confetti-overlay.tsx +134 -0
  163. package/src/react-native/index.tsx +83 -0
  164. package/src/react-native/snap-view-core.tsx +212 -0
  165. package/src/react-native/theme.tsx +85 -0
  166. package/src/react-native/types.ts +38 -0
  167. package/src/react-native/use-snap-palette.ts +64 -0
  168. package/src/react-native/v1/snap-view.tsx +229 -0
  169. package/src/react-native/v2/snap-view.tsx +283 -0
  170. package/src/schemas.ts +68 -17
  171. package/src/server/parseRequest.ts +68 -9
  172. package/src/server/verify.ts +2 -0
  173. package/src/ui/README.md +8 -8
  174. package/src/ui/badge.ts +2 -0
  175. package/src/ui/bar-chart.ts +38 -0
  176. package/src/ui/button.ts +1 -1
  177. package/src/ui/catalog.ts +19 -3
  178. package/src/ui/cell-grid.ts +49 -0
  179. package/src/ui/image.ts +1 -1
  180. package/src/ui/index.ts +6 -0
  181. package/src/ui/item.ts +1 -1
  182. package/src/ui/schema.ts +2 -2
  183. package/src/ui/slider.ts +2 -0
  184. package/src/ui/text.ts +2 -2
  185. package/src/validator.ts +251 -2
  186. package/dist/dataStore.d.ts +0 -12
  187. package/dist/dataStore.js +0 -35
  188. package/dist/middleware.d.ts +0 -3
  189. package/dist/middleware.js +0 -3
  190. package/dist/react/hooks/use-snap-accent.d.ts +0 -13
  191. package/dist/react/hooks/use-snap-accent.js +0 -32
  192. package/src/dataStore.ts +0 -62
  193. package/src/middleware.ts +0 -7
  194. package/src/react/hooks/use-snap-accent.ts +0 -45
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
- * Validates a snap response against the schema.
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.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=#8F8F8F blue=#006BFF red=#FC0036 amber=#FFAE00
7
- * green=#28A948 teal=#00AC96 purple=#8B5CF6 pink=#F32782
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=#8F8F8F blue=#006FFE red=#F13342 amber=#FFAE00
11
- * green=#00AC3A teal=#00AA96 purple=#A78BFA pink=#F12B82
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: "#8F8F8F",
44
- blue: "#006BFF",
45
- red: "#FC0036",
46
- amber: "#FFAE00",
47
- green: "#28A948",
48
- teal: "#00AC96",
49
- purple: "#8B5CF6",
50
- pink: "#F32782",
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: "#8F8F8F",
56
- blue: "#006FFE",
57
- red: "#F13342",
58
- amber: "#FFAE00",
59
- green: "#00AC3A",
60
- teal: "#00AA96",
61
- purple: "#A78BFA",
62
- pink: "#F12B82",
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 SPEC_VERSION = "1.0" as 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 { Spec as SnapSpec, UIElement as SnapUIElement } from "@json-render/core";
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 SnapPreviewAccentContextValue = {
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 SnapPreviewAccentContext =
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
- <SnapPreviewAccentContext.Provider value={{ pageAccent }}>
24
+ <SnapPreviewContext.Provider value={{ pageAccent, appearance }}>
22
25
  {children}
23
- </SnapPreviewAccentContext.Provider>
26
+ </SnapPreviewContext.Provider>
24
27
  );
25
28
  }
26
29
 
27
30
  export function useSnapPreviewPageAccent(): string | undefined {
28
- return useContext(SnapPreviewAccentContext)?.pageAccent;
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
  });