@farcaster/snap 1.8.0 → 1.9.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.
@@ -5,14 +5,12 @@ import { cn } from "@neynar/ui/utils";
5
5
  import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
6
6
  import { ICON_MAP } from "./icon.js";
7
7
  const VARIANT_MAP = {
8
- default: "default",
8
+ primary: "default",
9
9
  secondary: "secondary",
10
- outline: "outline",
11
- ghost: "ghost",
12
10
  };
13
11
  export function SnapActionButton({ element: { props }, emit, }) {
14
12
  const label = String(props.label ?? "Action");
15
- const variant = VARIANT_MAP[String(props.variant ?? "default")] ?? "default";
13
+ const variant = VARIANT_MAP[String(props.variant ?? "secondary")] ?? "secondary";
16
14
  const iconName = props.icon ? String(props.icon) : undefined;
17
15
  const accentStyle = useSnapAccentScopeStyle();
18
16
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
@@ -5,14 +5,13 @@ import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent.js";
5
5
  import { ICON_MAP } from "./icon.js";
6
6
  export function SnapBadge({ element: { props }, }) {
7
7
  const content = String(props.label ?? "");
8
+ const variant = String(props.variant ?? "default");
8
9
  const color = props.color ? String(props.color) : undefined;
9
10
  const iconName = props.icon ? String(props.icon) : undefined;
10
11
  const accentStyle = useSnapAccentScopeStyle();
11
12
  const isAccent = !color || color === "accent";
12
13
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
13
- return (_jsx("span", { style: isAccent ? accentStyle : undefined, children: _jsxs(Badge, { variant: isAccent ? "default" : "outline", className: "gap-1",
14
- // TODO: fix outline badge border color in @neynar/ui — too bright in dark mode
15
- style: !isAccent
14
+ return (_jsx("span", { style: isAccent ? accentStyle : undefined, children: _jsxs(Badge, { variant: variant, className: "gap-1", style: variant === "outline" && !isAccent
16
15
  ? { borderColor: `var(--snap-color-${color})`, color: `var(--snap-color-${color})` }
17
16
  : undefined, children: [Icon && _jsx(Icon, { size: 12 }), content] }) }));
18
17
  }
@@ -5,5 +5,5 @@ export function SnapItem({ element: { props }, children, }) {
5
5
  const title = String(props.title ?? "");
6
6
  const description = props.description ? String(props.description) : undefined;
7
7
  const variant = props.variant ?? "default";
8
- return (_jsxs(Item, { variant: variant, className: `flex-1 py-1.5 px-2.5 ${variant === "muted" ? "!bg-border/20" : ""}`, children: [_jsxs(ItemContent, { className: "gap-0.5", children: [_jsx(ItemTitle, { children: title }), description && _jsx(ItemDescription, { className: "mt-0", children: description })] }), children && _jsx(ItemActions, { children: children })] }));
8
+ return (_jsxs(Item, { variant: variant, className: "flex-1 py-1.5 px-2.5", children: [_jsxs(ItemContent, { className: "gap-0.5", children: [_jsx(ItemTitle, { children: title }), description && _jsx(ItemDescription, { className: "mt-0", children: description })] }), children && _jsx(ItemActions, { children: children })] }));
9
9
  }
@@ -1,14 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { Text, Title } from "@neynar/ui/typography";
3
+ import { Text } from "@neynar/ui/typography";
4
4
  const SIZE_MAP = {
5
- lg: { component: "title", textSize: undefined, order: 3 },
6
- md: { component: "text", textSize: "base", order: undefined },
7
- sm: { component: "text", textSize: "sm", order: undefined },
5
+ md: { component: "text", textSize: "base" },
6
+ sm: { component: "text", textSize: "sm" },
8
7
  };
9
8
  const WEIGHT_MAP = {
10
9
  bold: "bold",
11
- medium: "medium",
12
10
  normal: "normal",
13
11
  };
14
12
  export function SnapText({ element: { props }, }) {
@@ -17,9 +15,5 @@ export function SnapText({ element: { props }, }) {
17
15
  const weight = props.weight ? String(props.weight) : undefined;
18
16
  const align = props.align ?? undefined;
19
17
  const config = SIZE_MAP[size] ?? SIZE_MAP.md;
20
- const alignClass = align === "center" ? "text-center" : align === "right" ? "text-right" : "";
21
- if (config.component === "title") {
22
- return (_jsx(Title, { order: config.order, weight: weight ?? "bold", className: `flex-1 ${alignClass}`, children: content }));
23
- }
24
18
  return (_jsx(Text, { size: config.textSize, weight: weight, align: align, className: "flex-1", children: content }));
25
19
  }
@@ -4,34 +4,28 @@ import { useSnapPalette } from "../use-snap-palette.js";
4
4
  import { useSnapTheme } from "../theme.js";
5
5
  import { ICON_MAP } from "./snap-icon.js";
6
6
  const VARIANT_MAP = {
7
- default: "default",
7
+ primary: "primary",
8
8
  secondary: "secondary",
9
- outline: "outline",
10
- ghost: "ghost",
11
9
  };
12
10
  export function SnapActionButton({ element: { props }, emit, }) {
13
11
  const { accentHex } = useSnapPalette();
14
12
  const { colors } = useSnapTheme();
15
13
  const label = String(props.label ?? "Action");
16
- const variant = VARIANT_MAP[String(props.variant ?? "default")] ?? "default";
14
+ const variant = VARIANT_MAP[String(props.variant ?? "secondary")] ?? "secondary";
17
15
  const iconName = props.icon ? String(props.icon) : undefined;
18
16
  const variantStyle = (() => {
19
17
  switch (variant) {
20
- case "default":
18
+ case "primary":
21
19
  return { backgroundColor: accentHex };
22
20
  case "secondary":
23
21
  return { backgroundColor: "transparent", borderWidth: 1.5, borderColor: accentHex };
24
- case "outline":
25
- return { backgroundColor: "rgba(255,255,255,0.04)", borderWidth: 1, borderColor: colors.border };
26
- case "ghost":
27
- return { backgroundColor: "transparent" };
28
22
  }
29
23
  })();
30
- const textColor = variant === "default" ? "#fff" : variant === "secondary" ? accentHex : colors.text;
31
- const iconColor = variant === "default" ? "#fff" : variant === "secondary" ? accentHex : colors.text;
24
+ const textColor = variant === "primary" ? "#fff" : accentHex;
25
+ const iconColor = variant === "primary" ? "#fff" : accentHex;
32
26
  return (_jsx(View, { style: styles.outer, children: _jsxs(Pressable, { style: ({ pressed }) => [
33
27
  styles.btn,
34
- variant === "default" ? styles.btnDefault : styles.btnOther,
28
+ variant === "primary" ? styles.btnDefault : styles.btnOther,
35
29
  variantStyle,
36
30
  pressed && styles.pressed,
37
31
  ], onPress: () => {
@@ -5,19 +5,21 @@ import { ICON_MAP } from "./snap-icon.js";
5
5
  export function SnapBadge({ element: { props }, }) {
6
6
  const { accentHex, hex } = useSnapPalette();
7
7
  const label = String(props.label ?? "");
8
+ const variant = String(props.variant ?? "default");
8
9
  const color = props.color ? String(props.color) : undefined;
9
10
  const iconName = props.icon ? String(props.icon) : undefined;
10
11
  const isAccent = !color || color === "accent";
11
12
  const resolvedColor = isAccent ? accentHex : hex(color);
13
+ const isFilled = variant === "default";
12
14
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
13
15
  return (_jsxs(View, { style: [
14
16
  styles.badge,
15
- isAccent
17
+ isFilled
16
18
  ? { backgroundColor: resolvedColor, borderColor: resolvedColor }
17
19
  : { borderColor: resolvedColor },
18
- ], children: [Icon && (_jsx(Icon, { size: 12, color: isAccent ? "#fff" : resolvedColor })), _jsx(Text, { style: [
20
+ ], children: [Icon && (_jsx(Icon, { size: 12, color: isFilled ? "#fff" : resolvedColor })), _jsx(Text, { style: [
19
21
  styles.label,
20
- { color: isAccent ? "#fff" : resolvedColor },
22
+ { color: isFilled ? "#fff" : resolvedColor },
21
23
  ], children: label })] }));
22
24
  }
23
25
  const styles = StyleSheet.create({
@@ -8,11 +8,7 @@ export function SnapItem({ element: { props }, children, }) {
8
8
  ? String(props.description)
9
9
  : undefined;
10
10
  const variant = String(props.variant ?? "default");
11
- const containerVariant = variant === "outline"
12
- ? { borderWidth: 1, borderColor: colors.border + "80", borderRadius: 8, padding: 10 }
13
- : variant === "muted"
14
- ? { backgroundColor: "rgba(255,255,255,0.04)", borderRadius: 8, padding: 10 }
15
- : { paddingVertical: 8, paddingHorizontal: 10 };
11
+ const containerVariant = { paddingVertical: 8, paddingHorizontal: 10 };
16
12
  return (_jsxs(View, { style: [styles.container, containerVariant], children: [_jsxs(View, { style: styles.content, children: [_jsx(Text, { style: [styles.title, { color: colors.text }], children: title }), description ? (_jsx(Text, { style: [styles.description, { color: colors.textSecondary }], children: description })) : null] }), children ? (_jsx(View, { style: styles.actions, children: _jsx(View, { style: { flex: 0 }, children: children }) })) : null] }));
17
13
  }
18
14
  const styles = StyleSheet.create({
@@ -2,13 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { StyleSheet, Text, View } from "react-native";
3
3
  import { useSnapTheme } from "../theme.js";
4
4
  const SIZE_STYLES = {
5
- lg: { fontSize: 20, fontWeight: "700" },
6
5
  md: { fontSize: 16, lineHeight: 24 },
7
6
  sm: { fontSize: 13 },
8
7
  };
9
8
  const WEIGHT_MAP = {
10
9
  bold: "700",
11
- medium: "500",
12
10
  normal: "400",
13
11
  };
14
12
  export function SnapText({ element: { props }, }) {
@@ -1,7 +1,12 @@
1
1
  import { z } from "zod";
2
+ export declare const BADGE_VARIANTS: readonly ["default", "outline"];
2
3
  export declare const BADGE_MAX_LABEL_CHARS = 30;
3
4
  export declare const badgeProps: z.ZodObject<{
4
5
  label: z.ZodString;
6
+ variant: z.ZodOptional<z.ZodEnum<{
7
+ default: "default";
8
+ outline: "outline";
9
+ }>>;
5
10
  color: z.ZodOptional<z.ZodEnum<{
6
11
  gray: "gray";
7
12
  blue: "blue";
package/dist/ui/badge.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { PROGRESS_COLOR_VALUES } from "../colors.js";
3
3
  import { ICON_NAMES } from "./icon.js";
4
+ export const BADGE_VARIANTS = ["default", "outline"];
4
5
  export const BADGE_MAX_LABEL_CHARS = 30;
5
6
  export const badgeProps = z.object({
6
7
  label: z.string().min(1).max(BADGE_MAX_LABEL_CHARS),
8
+ variant: z.enum(BADGE_VARIANTS).optional(),
7
9
  color: z.enum(PROGRESS_COLOR_VALUES).optional(),
8
10
  icon: z.enum(ICON_NAMES).optional(),
9
11
  });
@@ -1,13 +1,11 @@
1
1
  import { z } from "zod";
2
- export declare const BUTTON_VARIANTS: readonly ["default", "secondary", "outline", "ghost"];
2
+ export declare const BUTTON_VARIANTS: readonly ["secondary", "primary"];
3
3
  export declare const BUTTON_MAX_LABEL_CHARS = 30;
4
4
  export declare const buttonProps: z.ZodObject<{
5
5
  label: z.ZodString;
6
6
  variant: z.ZodOptional<z.ZodEnum<{
7
- default: "default";
8
7
  secondary: "secondary";
9
- outline: "outline";
10
- ghost: "ghost";
8
+ primary: "primary";
11
9
  }>>;
12
10
  icon: z.ZodOptional<z.ZodEnum<{
13
11
  "arrow-right": "arrow-right";
package/dist/ui/button.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { ICON_NAMES } from "./icon.js";
3
- export const BUTTON_VARIANTS = ["default", "secondary", "outline", "ghost"];
3
+ export const BUTTON_VARIANTS = ["secondary", "primary"];
4
4
  export const BUTTON_MAX_LABEL_CHARS = 30;
5
5
  export const buttonProps = z.object({
6
6
  label: z.string().min(1).max(BUTTON_MAX_LABEL_CHARS),
@@ -37,6 +37,10 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
37
37
  badge: {
38
38
  props: z.ZodObject<{
39
39
  label: z.ZodString;
40
+ variant: z.ZodOptional<z.ZodEnum<{
41
+ default: "default";
42
+ outline: "outline";
43
+ }>>;
40
44
  color: z.ZodOptional<z.ZodEnum<{
41
45
  gray: "gray";
42
46
  blue: "blue";
@@ -90,10 +94,8 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
90
94
  props: z.ZodObject<{
91
95
  label: z.ZodString;
92
96
  variant: z.ZodOptional<z.ZodEnum<{
93
- default: "default";
94
97
  secondary: "secondary";
95
- outline: "outline";
96
- ghost: "ghost";
98
+ primary: "primary";
97
99
  }>>;
98
100
  icon: z.ZodOptional<z.ZodEnum<{
99
101
  "arrow-right": "arrow-right";
@@ -179,8 +181,6 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
179
181
  description: z.ZodOptional<z.ZodString>;
180
182
  variant: z.ZodOptional<z.ZodEnum<{
181
183
  default: "default";
182
- outline: "outline";
183
- muted: "muted";
184
184
  }>>;
185
185
  }, z.core.$strip>;
186
186
  description: string;
@@ -260,7 +260,6 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
260
260
  "1:1": "1:1";
261
261
  "16:9": "16:9";
262
262
  "4:3": "4:3";
263
- "3:4": "3:4";
264
263
  "9:16": "9:16";
265
264
  }>;
266
265
  alt: z.ZodOptional<z.ZodString>;
@@ -323,11 +322,9 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
323
322
  size: z.ZodOptional<z.ZodEnum<{
324
323
  sm: "sm";
325
324
  md: "md";
326
- lg: "lg";
327
325
  }>>;
328
326
  weight: z.ZodOptional<z.ZodEnum<{
329
327
  bold: "bold";
330
- medium: "medium";
331
328
  normal: "normal";
332
329
  }>>;
333
330
  align: z.ZodOptional<z.ZodEnum<{
@@ -28,7 +28,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
28
28
  components: {
29
29
  badge: {
30
30
  props: badgeProps,
31
- description: "Inline label — variant: default | secondary | destructive | outline.",
31
+ description: "Inline label — variant: default (filled) or outline (bordered). Optional color and icon.",
32
32
  },
33
33
  button: {
34
34
  props: buttonProps,
@@ -80,7 +80,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
80
80
  },
81
81
  text: {
82
82
  props: textProps,
83
- description: "Text block — size: lg (heading), md (body, default), sm (caption). Optional weight and align.",
83
+ description: "Text block — size: md (body, default), sm (caption). Optional weight and align.",
84
84
  },
85
85
  },
86
86
  actions: {
@@ -1,12 +1,11 @@
1
1
  import { z } from "zod";
2
- export declare const IMAGE_ASPECTS: readonly ["1:1", "16:9", "4:3", "3:4", "9:16"];
2
+ export declare const IMAGE_ASPECTS: readonly ["1:1", "16:9", "4:3", "9:16"];
3
3
  export declare const imageProps: z.ZodObject<{
4
4
  url: z.ZodString;
5
5
  aspect: z.ZodEnum<{
6
6
  "1:1": "1:1";
7
7
  "16:9": "16:9";
8
8
  "4:3": "4:3";
9
- "3:4": "3:4";
10
9
  "9:16": "9:16";
11
10
  }>;
12
11
  alt: z.ZodOptional<z.ZodString>;
package/dist/ui/image.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export const IMAGE_ASPECTS = ["1:1", "16:9", "4:3", "3:4", "9:16"];
2
+ export const IMAGE_ASPECTS = ["1:1", "16:9", "4:3", "9:16"];
3
3
  export const imageProps = z.object({
4
4
  url: z.string(),
5
5
  aspect: z.enum(IMAGE_ASPECTS),
package/dist/ui/item.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export declare const ITEM_VARIANTS: readonly ["default", "outline", "muted"];
2
+ export declare const ITEM_VARIANTS: readonly ["default"];
3
3
  export declare const ITEM_MAX_TITLE_CHARS = 100;
4
4
  export declare const ITEM_MAX_DESCRIPTION_CHARS = 160;
5
5
  export declare const itemProps: z.ZodObject<{
@@ -7,8 +7,6 @@ export declare const itemProps: z.ZodObject<{
7
7
  description: z.ZodOptional<z.ZodString>;
8
8
  variant: z.ZodOptional<z.ZodEnum<{
9
9
  default: "default";
10
- outline: "outline";
11
- muted: "muted";
12
10
  }>>;
13
11
  }, z.core.$strip>;
14
12
  export type ItemProps = z.infer<typeof itemProps>;
package/dist/ui/item.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export const ITEM_VARIANTS = ["default", "outline", "muted"];
2
+ export const ITEM_VARIANTS = ["default"];
3
3
  export const ITEM_MAX_TITLE_CHARS = 100;
4
4
  export const ITEM_MAX_DESCRIPTION_CHARS = 160;
5
5
  export const itemProps = z.object({
package/dist/ui/text.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
- export declare const TEXT_SIZES: readonly ["lg", "md", "sm"];
3
- export declare const TEXT_WEIGHTS: readonly ["bold", "medium", "normal"];
2
+ export declare const TEXT_SIZES: readonly ["md", "sm"];
3
+ export declare const TEXT_WEIGHTS: readonly ["bold", "normal"];
4
4
  export declare const TEXT_ALIGNS: readonly ["left", "center", "right"];
5
5
  export declare const TEXT_MAX_CONTENT_CHARS = 320;
6
6
  export declare const textProps: z.ZodObject<{
@@ -8,11 +8,9 @@ export declare const textProps: z.ZodObject<{
8
8
  size: z.ZodOptional<z.ZodEnum<{
9
9
  sm: "sm";
10
10
  md: "md";
11
- lg: "lg";
12
11
  }>>;
13
12
  weight: z.ZodOptional<z.ZodEnum<{
14
13
  bold: "bold";
15
- medium: "medium";
16
14
  normal: "normal";
17
15
  }>>;
18
16
  align: z.ZodOptional<z.ZodEnum<{
package/dist/ui/text.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
- export const TEXT_SIZES = ["lg", "md", "sm"];
3
- export const TEXT_WEIGHTS = ["bold", "medium", "normal"];
2
+ export const TEXT_SIZES = ["md", "sm"];
3
+ export const TEXT_WEIGHTS = ["bold", "normal"];
4
4
  export const TEXT_ALIGNS = ["left", "center", "right"];
5
5
  export const TEXT_MAX_CONTENT_CHARS = 320;
6
6
  export const textProps = z.object({
package/llms.txt ADDED
@@ -0,0 +1,172 @@
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": "1.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"`), `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
+ ## Components (14 total)
34
+
35
+ ### Display Components
36
+
37
+ **badge** — Inline label with optional icon.
38
+ - `label` (string, required, max 30)
39
+ - `variant` (optional): `"default"` (filled) | `"outline"` (bordered). Default: `"default"`
40
+ - `color` (optional): PaletteColor. Default: `"accent"`
41
+ - `icon` (optional): IconName
42
+
43
+ **button** — Action trigger. Bind via `on.press`.
44
+ - `label` (string, required, max 30)
45
+ - `variant` (optional): `"primary"` (filled accent) | `"secondary"` (bordered). Default: `"secondary"`
46
+ - `icon` (optional): IconName
47
+
48
+ **icon** — Standalone Lucide icon.
49
+ - `name` (IconName, required)
50
+ - `color` (optional): PaletteColor. Default: `"accent"`
51
+ - `size` (optional): `"sm"` (16px) | `"md"` (20px). Default: `"md"`
52
+
53
+ **image** — HTTPS image with fixed aspect ratio.
54
+ - `url` (string, required)
55
+ - `aspect` (required): `"1:1"` | `"16:9"` | `"4:3"` | `"9:16"`
56
+ - `alt` (string, optional)
57
+
58
+ **item** — Content row with title and right-side actions slot.
59
+ - `title` (string, required, max 100)
60
+ - `description` (string, optional, max 160)
61
+ - `variant` (optional): `"default"`. Default: `"default"`
62
+ - Children render in the actions slot (right side)
63
+
64
+ **progress** — Horizontal progress bar.
65
+ - `value` (number, required, 0 to max)
66
+ - `max` (number, required, > 0)
67
+ - `label` (string, optional, max 60)
68
+
69
+ **separator** — Visual divider.
70
+ - `orientation` (optional): `"horizontal"` | `"vertical"`. Default: `"horizontal"`
71
+
72
+ **text** — Text block.
73
+ - `content` (string, required, max 320)
74
+ - `size` (optional): `"md"` (body) | `"sm"` (caption). Default: `"md"`
75
+ - `weight` (optional): `"bold"` | `"normal"`. Default: `"normal"`
76
+ - `align` (optional): `"left"` | `"center"` | `"right"`. Default: `"left"`
77
+
78
+ ### Container Components
79
+
80
+ **stack** — Layout container.
81
+ - `direction` (optional): `"vertical"` | `"horizontal"`. Default: `"vertical"`
82
+ - `gap` (optional): `"none"` | `"sm"` | `"md"` | `"lg"`. Default: `"md"`
83
+ - `justify` (optional): `"start"` | `"center"` | `"end"` | `"between"` | `"around"`
84
+ - Children are element IDs
85
+
86
+ **item_group** — Groups item children.
87
+ - `border` (boolean, optional)
88
+ - `separator` (boolean, optional)
89
+ - `gap` (optional): `"none"` | `"sm"` | `"md"` | `"lg"`
90
+ - Children must be item elements
91
+
92
+ ### Field Components
93
+
94
+ Field values are sent in POST `inputs[name]` when a `submit` action fires.
95
+
96
+ **input** — Text or number input.
97
+ - `name` (string, required)
98
+ - `type` (optional): `"text"` | `"number"`. Default: `"text"`
99
+ - `label` (string, optional, max 60)
100
+ - `placeholder` (string, optional, max 60)
101
+ - `defaultValue` (string, optional)
102
+ - `maxLength` (number, optional, 1-280)
103
+ - POST value: string
104
+
105
+ **slider** — Numeric range.
106
+ - `name` (string, required)
107
+ - `min` (number, required)
108
+ - `max` (number, required, >= min)
109
+ - `step` (number, optional, > 0. Default: 1)
110
+ - `defaultValue` (number, optional, between min and max)
111
+ - `label` (string, optional, max 60)
112
+ - POST value: number
113
+
114
+ **switch** — Boolean toggle.
115
+ - `name` (string, required)
116
+ - `label` (string, optional, max 60)
117
+ - `defaultChecked` (boolean, optional)
118
+ - POST value: boolean
119
+
120
+ **toggle_group** — Single or multi-select choice group.
121
+ - `name` (string, required)
122
+ - `options` (string[], required, 2-6 items, each max 30 chars)
123
+ - `multiple` (boolean, optional)
124
+ - `orientation` (optional): `"horizontal"` | `"vertical"`. Default: `"horizontal"`
125
+ - `defaultValue` (string | string[], optional)
126
+ - `variant` (optional): `"default"` | `"outline"`. Default: `"default"`
127
+ - `label` (string, optional, max 60)
128
+ - POST value: string (single) or string[] (multiple)
129
+
130
+ ## Actions (9 types)
131
+
132
+ Bound to buttons via `on.press`:
133
+
134
+ | Action | Params | Description |
135
+ |--------|--------|-------------|
136
+ | `submit` | `target` (URL) | POST to server, get next page |
137
+ | `open_url` | `target` (URL) | Open in system browser |
138
+ | `open_mini_app` | `target` (URL) | Open as Farcaster mini app |
139
+ | `view_cast` | `hash` (string) | Navigate to a cast |
140
+ | `view_profile` | `fid` (number) | Navigate to a profile |
141
+ | `compose_cast` | `text?`, `channelKey?`, `embeds?` | Open cast composer |
142
+ | `view_token` | `token` (CAIP-19) | View token in wallet |
143
+ | `send_token` | `token`, `amount?`, `recipientFid?`, `recipientAddress?` | Send token flow |
144
+ | `swap_token` | `sellToken?`, `buyToken?` | Swap token flow |
145
+
146
+ ## Icon Names (34)
147
+
148
+ `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`
149
+
150
+ ## Color Palette
151
+
152
+ `gray`, `blue`, `red`, `amber`, `green`, `teal`, `purple`, `pink`
153
+
154
+ Plus the special value `"accent"` which references `theme.accent`.
155
+
156
+ ## Package Exports
157
+
158
+ ```ts
159
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
160
+ import { parseRequest, verifyJFSRequestBody } from "@farcaster/snap/server";
161
+ import { createInMemoryDataStore } from "@farcaster/snap";
162
+ ```
163
+
164
+ - `@farcaster/snap` — schemas, types, validation, data store
165
+ - `@farcaster/snap/ui` — json-render catalog, component schemas
166
+ - `@farcaster/snap/server` — request parsing, JFS verification
167
+ - `@farcaster/snap-hono` — Hono adapter (`registerSnapHandler`)
168
+ - `@farcaster/snap-turso` — Turso data store middleware
169
+
170
+ ## Full Documentation
171
+
172
+ https://docs.farcaster.xyz/snap
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
@@ -109,7 +109,8 @@
109
109
  },
110
110
  "files": [
111
111
  "dist",
112
- "src"
112
+ "src",
113
+ "llms.txt"
113
114
  ],
114
115
  "publishConfig": {
115
116
  "access": "public"
@@ -5,11 +5,9 @@ import { cn } from "@neynar/ui/utils";
5
5
  import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
6
6
  import { ICON_MAP } from "./icon";
7
7
 
8
- const VARIANT_MAP: Record<string, "default" | "outline" | "ghost" | "secondary"> = {
9
- default: "default",
8
+ const VARIANT_MAP: Record<string, "default" | "secondary"> = {
9
+ primary: "default",
10
10
  secondary: "secondary",
11
- outline: "outline",
12
- ghost: "ghost",
13
11
  };
14
12
 
15
13
  export function SnapActionButton({
@@ -20,7 +18,7 @@ export function SnapActionButton({
20
18
  emit: (name: string) => void;
21
19
  }) {
22
20
  const label = String(props.label ?? "Action");
23
- const variant = VARIANT_MAP[String(props.variant ?? "default")] ?? "default";
21
+ const variant = VARIANT_MAP[String(props.variant ?? "secondary")] ?? "secondary";
24
22
  const iconName = props.icon ? String(props.icon) : undefined;
25
23
  const accentStyle = useSnapAccentScopeStyle();
26
24
 
@@ -10,6 +10,7 @@ export function SnapBadge({
10
10
  element: { props: Record<string, unknown> };
11
11
  }) {
12
12
  const content = String(props.label ?? "");
13
+ const variant = String(props.variant ?? "default") as "default" | "outline";
13
14
  const color = props.color ? String(props.color) : undefined;
14
15
  const iconName = props.icon ? String(props.icon) : undefined;
15
16
  const accentStyle = useSnapAccentScopeStyle();
@@ -20,11 +21,10 @@ export function SnapBadge({
20
21
  return (
21
22
  <span style={isAccent ? accentStyle : undefined}>
22
23
  <Badge
23
- variant={isAccent ? "default" : "outline"}
24
+ variant={variant}
24
25
  className="gap-1"
25
- // TODO: fix outline badge border color in @neynar/ui — too bright in dark mode
26
26
  style={
27
- !isAccent
27
+ variant === "outline" && !isAccent
28
28
  ? { borderColor: `var(--snap-color-${color})`, color: `var(--snap-color-${color})` }
29
29
  : undefined
30
30
  }
@@ -19,10 +19,10 @@ export function SnapItem({
19
19
  const title = String(props.title ?? "");
20
20
  const description = props.description ? String(props.description) : undefined;
21
21
  const variant =
22
- (props.variant as "default" | "outline" | "muted") ?? "default";
22
+ (props.variant as "default") ?? "default";
23
23
 
24
24
  return (
25
- <Item variant={variant} className={`flex-1 py-1.5 px-2.5 ${variant === "muted" ? "!bg-border/20" : ""}`}>
25
+ <Item variant={variant} className="flex-1 py-1.5 px-2.5">
26
26
  <ItemContent className="gap-0.5">
27
27
  <ItemTitle>{title}</ItemTitle>
28
28
  {description && <ItemDescription className="mt-0">{description}</ItemDescription>}
@@ -1,16 +1,14 @@
1
1
  "use client";
2
2
 
3
- import { Text, Title } from "@neynar/ui/typography";
3
+ import { Text } from "@neynar/ui/typography";
4
4
 
5
5
  const SIZE_MAP = {
6
- lg: { component: "title", textSize: undefined, order: 3 },
7
- md: { component: "text", textSize: "base" as const, order: undefined },
8
- sm: { component: "text", textSize: "sm" as const, order: undefined },
6
+ md: { component: "text", textSize: "base" as const },
7
+ sm: { component: "text", textSize: "sm" as const },
9
8
  } as const;
10
9
 
11
10
  const WEIGHT_MAP = {
12
11
  bold: "bold",
13
- medium: "medium",
14
12
  normal: "normal",
15
13
  } as const;
16
14
 
@@ -20,21 +18,11 @@ export function SnapText({
20
18
  element: { props: Record<string, unknown> };
21
19
  }) {
22
20
  const content = String(props.content ?? "");
23
- const size = String(props.size ?? "md") as "lg" | "md" | "sm";
24
- const weight = props.weight ? String(props.weight) as "bold" | "medium" | "normal" : undefined;
21
+ const size = String(props.size ?? "md") as "md" | "sm";
22
+ const weight = props.weight ? String(props.weight) as "bold" | "normal" : undefined;
25
23
  const align = (props.align as "left" | "center" | "right") ?? undefined;
26
24
  const config = SIZE_MAP[size] ?? SIZE_MAP.md;
27
25
 
28
- const alignClass = align === "center" ? "text-center" : align === "right" ? "text-right" : "";
29
-
30
- if (config.component === "title") {
31
- return (
32
- <Title order={config.order} weight={weight ?? "bold"} className={`flex-1 ${alignClass}`}>
33
- {content}
34
- </Title>
35
- );
36
- }
37
-
38
26
  return (
39
27
  <Text size={config.textSize} weight={weight} align={align} className="flex-1">
40
28
  {content}
@@ -6,11 +6,9 @@ import { useSnapPalette } from "../use-snap-palette";
6
6
  import { useSnapTheme } from "../theme";
7
7
  import { ICON_MAP } from "./snap-icon";
8
8
 
9
- const VARIANT_MAP: Record<string, "default" | "secondary" | "outline" | "ghost"> = {
10
- default: "default",
9
+ const VARIANT_MAP: Record<string, "primary" | "secondary"> = {
10
+ primary: "primary",
11
11
  secondary: "secondary",
12
- outline: "outline",
13
- ghost: "ghost",
14
12
  };
15
13
 
16
14
  export function SnapActionButton({
@@ -20,31 +18,27 @@ export function SnapActionButton({
20
18
  const { accentHex } = useSnapPalette();
21
19
  const { colors } = useSnapTheme();
22
20
  const label = String(props.label ?? "Action");
23
- const variant = VARIANT_MAP[String(props.variant ?? "default")] ?? "default";
21
+ const variant = VARIANT_MAP[String(props.variant ?? "secondary")] ?? "secondary";
24
22
  const iconName = props.icon ? String(props.icon) : undefined;
25
23
 
26
24
  const variantStyle = (() => {
27
25
  switch (variant) {
28
- case "default":
26
+ case "primary":
29
27
  return { backgroundColor: accentHex };
30
28
  case "secondary":
31
29
  return { backgroundColor: "transparent", borderWidth: 1.5, borderColor: accentHex };
32
- case "outline":
33
- return { backgroundColor: "rgba(255,255,255,0.04)", borderWidth: 1, borderColor: colors.border };
34
- case "ghost":
35
- return { backgroundColor: "transparent" };
36
30
  }
37
31
  })();
38
32
 
39
- const textColor = variant === "default" ? "#fff" : variant === "secondary" ? accentHex : colors.text;
40
- const iconColor = variant === "default" ? "#fff" : variant === "secondary" ? accentHex : colors.text;
33
+ const textColor = variant === "primary" ? "#fff" : accentHex;
34
+ const iconColor = variant === "primary" ? "#fff" : accentHex;
41
35
 
42
36
  return (
43
37
  <View style={styles.outer}>
44
38
  <Pressable
45
39
  style={({ pressed }) => [
46
40
  styles.btn,
47
- variant === "default" ? styles.btnDefault : styles.btnOther,
41
+ variant === "primary" ? styles.btnDefault : styles.btnOther,
48
42
  variantStyle,
49
43
  pressed && styles.pressed,
50
44
  ]}
@@ -8,10 +8,12 @@ export function SnapBadge({
8
8
  }: ComponentRenderProps<Record<string, unknown>>) {
9
9
  const { accentHex, hex } = useSnapPalette();
10
10
  const label = String(props.label ?? "");
11
+ const variant = String(props.variant ?? "default");
11
12
  const color = props.color ? String(props.color) : undefined;
12
13
  const iconName = props.icon ? String(props.icon) : undefined;
13
14
  const isAccent = !color || color === "accent";
14
15
  const resolvedColor = isAccent ? accentHex : hex(color);
16
+ const isFilled = variant === "default";
15
17
 
16
18
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
17
19
 
@@ -19,18 +21,18 @@ export function SnapBadge({
19
21
  <View
20
22
  style={[
21
23
  styles.badge,
22
- isAccent
24
+ isFilled
23
25
  ? { backgroundColor: resolvedColor, borderColor: resolvedColor }
24
26
  : { borderColor: resolvedColor },
25
27
  ]}
26
28
  >
27
29
  {Icon && (
28
- <Icon size={12} color={isAccent ? "#fff" : resolvedColor} />
30
+ <Icon size={12} color={isFilled ? "#fff" : resolvedColor} />
29
31
  )}
30
32
  <Text
31
33
  style={[
32
34
  styles.label,
33
- { color: isAccent ? "#fff" : resolvedColor },
35
+ { color: isFilled ? "#fff" : resolvedColor },
34
36
  ]}
35
37
  >
36
38
  {label}
@@ -14,12 +14,7 @@ export function SnapItem({
14
14
  : undefined;
15
15
  const variant = String(props.variant ?? "default");
16
16
 
17
- const containerVariant =
18
- variant === "outline"
19
- ? { borderWidth: 1, borderColor: colors.border + "80", borderRadius: 8, padding: 10 }
20
- : variant === "muted"
21
- ? { backgroundColor: "rgba(255,255,255,0.04)", borderRadius: 8, padding: 10 }
22
- : { paddingVertical: 8, paddingHorizontal: 10 };
17
+ const containerVariant = { paddingVertical: 8, paddingHorizontal: 10 };
23
18
 
24
19
  return (
25
20
  <View style={[styles.container, containerVariant]}>
@@ -3,14 +3,12 @@ import { StyleSheet, Text, View } from "react-native";
3
3
  import { useSnapTheme } from "../theme";
4
4
 
5
5
  const SIZE_STYLES: Record<string, { fontSize: number; lineHeight?: number; fontWeight?: "400" | "500" | "600" | "700" }> = {
6
- lg: { fontSize: 20, fontWeight: "700" },
7
6
  md: { fontSize: 16, lineHeight: 24 },
8
7
  sm: { fontSize: 13 },
9
8
  };
10
9
 
11
10
  const WEIGHT_MAP: Record<string, "400" | "500" | "600" | "700"> = {
12
11
  bold: "700",
13
- medium: "500",
14
12
  normal: "400",
15
13
  };
16
14
 
package/src/ui/badge.ts CHANGED
@@ -2,10 +2,12 @@ import { z } from "zod";
2
2
  import { PROGRESS_COLOR_VALUES } from "../colors.js";
3
3
  import { ICON_NAMES } from "./icon.js";
4
4
 
5
+ export const BADGE_VARIANTS = ["default", "outline"] as const;
5
6
  export const BADGE_MAX_LABEL_CHARS = 30;
6
7
 
7
8
  export const badgeProps = z.object({
8
9
  label: z.string().min(1).max(BADGE_MAX_LABEL_CHARS),
10
+ variant: z.enum(BADGE_VARIANTS).optional(),
9
11
  color: z.enum(PROGRESS_COLOR_VALUES).optional(),
10
12
  icon: z.enum(ICON_NAMES).optional(),
11
13
  });
package/src/ui/button.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ICON_NAMES } from "./icon.js";
3
3
 
4
- export const BUTTON_VARIANTS = ["default", "secondary", "outline", "ghost"] as const;
4
+ export const BUTTON_VARIANTS = ["secondary", "primary"] as const;
5
5
  export const BUTTON_MAX_LABEL_CHARS = 30;
6
6
 
7
7
  export const buttonProps = z.object({
package/src/ui/catalog.ts CHANGED
@@ -31,7 +31,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
31
31
  badge: {
32
32
  props: badgeProps,
33
33
  description:
34
- "Inline label — variant: default | secondary | destructive | outline.",
34
+ "Inline label — variant: default (filled) or outline (bordered). Optional color and icon.",
35
35
  },
36
36
  button: {
37
37
  props: buttonProps,
@@ -95,7 +95,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
95
95
  text: {
96
96
  props: textProps,
97
97
  description:
98
- "Text block — size: lg (heading), md (body, default), sm (caption). Optional weight and align.",
98
+ "Text block — size: md (body, default), sm (caption). Optional weight and align.",
99
99
  },
100
100
  },
101
101
  actions: {
package/src/ui/image.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const IMAGE_ASPECTS = ["1:1", "16:9", "4:3", "3:4", "9:16"] as const;
3
+ export const IMAGE_ASPECTS = ["1:1", "16:9", "4:3", "9:16"] as const;
4
4
 
5
5
  export const imageProps = z.object({
6
6
  url: z.string(),
package/src/ui/item.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const ITEM_VARIANTS = ["default", "outline", "muted"] as const;
3
+ export const ITEM_VARIANTS = ["default"] as const;
4
4
  export const ITEM_MAX_TITLE_CHARS = 100;
5
5
  export const ITEM_MAX_DESCRIPTION_CHARS = 160;
6
6
 
package/src/ui/text.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const TEXT_SIZES = ["lg", "md", "sm"] as const;
4
- export const TEXT_WEIGHTS = ["bold", "medium", "normal"] as const;
3
+ export const TEXT_SIZES = ["md", "sm"] as const;
4
+ export const TEXT_WEIGHTS = ["bold", "normal"] as const;
5
5
  export const TEXT_ALIGNS = ["left", "center", "right"] as const;
6
6
  export const TEXT_MAX_CONTENT_CHARS = 320;
7
7