@farcaster/snap 1.8.0 → 1.10.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.
- package/dist/index.d.ts +4 -5
- package/dist/index.js +2 -3
- package/dist/react/components/action-button.js +2 -4
- package/dist/react/components/badge.js +2 -3
- package/dist/react/components/item.js +1 -1
- package/dist/react/components/text.js +3 -9
- package/dist/react-native/components/snap-action-button.js +6 -12
- package/dist/react-native/components/snap-badge.js +5 -3
- package/dist/react-native/components/snap-item.js +1 -5
- package/dist/react-native/components/snap-text.js +0 -2
- package/dist/schemas.d.ts +45 -3
- package/dist/schemas.js +2 -2
- package/dist/ui/badge.d.ts +5 -0
- package/dist/ui/badge.js +2 -0
- package/dist/ui/button.d.ts +2 -4
- package/dist/ui/button.js +1 -1
- package/dist/ui/catalog.d.ts +5 -8
- package/dist/ui/catalog.js +2 -2
- package/dist/ui/image.d.ts +1 -2
- package/dist/ui/image.js +1 -1
- package/dist/ui/item.d.ts +1 -3
- package/dist/ui/item.js +1 -1
- package/dist/ui/text.d.ts +2 -4
- package/dist/ui/text.js +2 -2
- package/llms.txt +172 -0
- package/package.json +3 -2
- package/src/index.ts +8 -16
- package/src/react/components/action-button.tsx +3 -5
- package/src/react/components/badge.tsx +3 -3
- package/src/react/components/item.tsx +2 -2
- package/src/react/components/text.tsx +5 -17
- package/src/react-native/components/snap-action-button.tsx +7 -13
- package/src/react-native/components/snap-badge.tsx +5 -3
- package/src/react-native/components/snap-item.tsx +1 -6
- package/src/react-native/components/snap-text.tsx +0 -2
- package/src/schemas.ts +37 -11
- package/src/ui/badge.ts +2 -0
- package/src/ui/button.ts +1 -1
- package/src/ui/catalog.ts +2 -2
- package/src/ui/image.ts +1 -1
- package/src/ui/item.ts +1 -1
- package/src/ui/text.ts +2 -2
- package/dist/dataStore.d.ts +0 -9
- package/dist/dataStore.js +0 -22
- package/src/dataStore.ts +0 -38
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
export type { Spec as SnapSpec, UIElement as SnapUIElement } from "@json-render/core";
|
|
2
|
-
export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES
|
|
1
|
+
export type { Spec as SnapSpec, UIElement as SnapUIElement, } from "@json-render/core";
|
|
2
|
+
export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES } from "./constants.js";
|
|
3
3
|
export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, type PaletteColor, } from "./colors.js";
|
|
4
|
-
export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, type SnapAction, type SnapContext, type SnapResponse, type SnapHandlerResult, type SnapFunction, type SnapPayload, } from "./schemas.js";
|
|
5
|
-
export { validateSnapResponse, type ValidationResult
|
|
6
|
-
export { type DataStoreValue, type SnapDataStore, createDefaultDataStore, createInMemoryDataStore, } from "./dataStore.js";
|
|
4
|
+
export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, type SnapAction, type SnapContext, type SnapResponse, type SnapHandlerResult, type SnapElementInput, type SnapSpecInput, type SnapFunction, type SnapPayload, } from "./schemas.js";
|
|
5
|
+
export { validateSnapResponse, type ValidationResult } from "./validator.js";
|
|
7
6
|
export { type Middleware, useMiddleware } from "./middleware.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES
|
|
1
|
+
export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES } from "./constants.js";
|
|
2
2
|
export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "./colors.js";
|
|
3
3
|
export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, } from "./schemas.js";
|
|
4
|
-
export { validateSnapResponse
|
|
5
|
-
export { createDefaultDataStore, createInMemoryDataStore, } from "./dataStore.js";
|
|
4
|
+
export { validateSnapResponse } from "./validator.js";
|
|
6
5
|
export { useMiddleware } from "./middleware.js";
|
|
@@ -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
|
-
|
|
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 ?? "
|
|
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:
|
|
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:
|
|
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
|
|
3
|
+
import { Text } from "@neynar/ui/typography";
|
|
4
4
|
const SIZE_MAP = {
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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 ?? "
|
|
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 "
|
|
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 === "
|
|
31
|
-
const iconColor = variant === "
|
|
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 === "
|
|
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
|
-
|
|
17
|
+
isFilled
|
|
16
18
|
? { backgroundColor: resolvedColor, borderColor: resolvedColor }
|
|
17
19
|
: { borderColor: resolvedColor },
|
|
18
|
-
], children: [Icon && (_jsx(Icon, { size: 12, color:
|
|
20
|
+
], children: [Icon && (_jsx(Icon, { size: 12, color: isFilled ? "#fff" : resolvedColor })), _jsx(Text, { style: [
|
|
19
21
|
styles.label,
|
|
20
|
-
{ color:
|
|
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 =
|
|
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 }, }) {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { Spec } from "@json-render/core";
|
|
3
|
-
import {
|
|
3
|
+
import { SPEC_VERSION } from "./constants.js";
|
|
4
|
+
declare const themeAccentSchema: z.ZodEnum<{
|
|
5
|
+
gray: "gray";
|
|
6
|
+
blue: "blue";
|
|
7
|
+
red: "red";
|
|
8
|
+
amber: "amber";
|
|
9
|
+
green: "green";
|
|
10
|
+
teal: "teal";
|
|
11
|
+
purple: "purple";
|
|
12
|
+
pink: "pink";
|
|
13
|
+
}>;
|
|
4
14
|
export declare const snapResponseSchema: z.ZodObject<{
|
|
5
15
|
version: z.ZodLiteral<"1.0">;
|
|
6
16
|
theme: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
@@ -21,7 +31,40 @@ export declare const snapResponseSchema: z.ZodObject<{
|
|
|
21
31
|
ui: z.ZodCustom<Spec, Spec>;
|
|
22
32
|
}, z.core.$strict>;
|
|
23
33
|
export type SnapResponse = z.infer<typeof snapResponseSchema>;
|
|
24
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Permissive element input type for snap handler authors.
|
|
36
|
+
* Allows dynamic element construction without requiring exact UIElement types.
|
|
37
|
+
*/
|
|
38
|
+
export type SnapElementInput = {
|
|
39
|
+
type: string;
|
|
40
|
+
props?: Record<string, unknown>;
|
|
41
|
+
children?: string[];
|
|
42
|
+
on?: Record<string, unknown>;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Permissive input type for the `ui` field in snap handler return values.
|
|
47
|
+
* Accepts dynamically-built element maps (e.g. `Record<string, SnapElementInput>`)
|
|
48
|
+
* without requiring exact UIElement types.
|
|
49
|
+
*/
|
|
50
|
+
export type SnapSpecInput = {
|
|
51
|
+
root: string;
|
|
52
|
+
elements: Record<string, SnapElementInput>;
|
|
53
|
+
state?: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Return type for snap handler functions.
|
|
57
|
+
* Uses permissive input types so handlers can build elements dynamically
|
|
58
|
+
* without type casts. Runtime validation via the Zod schema still catches invalid shapes.
|
|
59
|
+
*/
|
|
60
|
+
export type SnapHandlerResult = {
|
|
61
|
+
version: typeof SPEC_VERSION;
|
|
62
|
+
theme?: {
|
|
63
|
+
accent?: z.input<typeof themeAccentSchema>;
|
|
64
|
+
};
|
|
65
|
+
effects?: z.input<typeof snapResponseSchema>["effects"];
|
|
66
|
+
ui: SnapSpecInput;
|
|
67
|
+
};
|
|
25
68
|
export declare const payloadSchema: z.ZodObject<{
|
|
26
69
|
fid: z.ZodNumber;
|
|
27
70
|
inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
|
|
@@ -56,7 +99,6 @@ export type SnapAction = z.infer<typeof snapActionSchema>;
|
|
|
56
99
|
export type SnapContext = {
|
|
57
100
|
action: SnapAction;
|
|
58
101
|
request: Request;
|
|
59
|
-
data: SnapDataStore;
|
|
60
102
|
};
|
|
61
103
|
export type SnapFunction = (ctx: SnapContext) => Promise<SnapHandlerResult>;
|
|
62
104
|
export {};
|
package/dist/schemas.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { EFFECT_VALUES, SPEC_VERSION
|
|
3
|
-
import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES
|
|
2
|
+
import { EFFECT_VALUES, SPEC_VERSION } from "./constants.js";
|
|
3
|
+
import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES } from "./colors.js";
|
|
4
4
|
// ─── Theme ─────────────────────────────────────────────
|
|
5
5
|
const themeAccentSchema = z.enum(PALETTE_COLOR_VALUES, {
|
|
6
6
|
message: `accent must be a palette color: ${PALETTE_COLOR_VALUES.join(", ")}`,
|
package/dist/ui/badge.d.ts
CHANGED
|
@@ -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
|
});
|
package/dist/ui/button.d.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const BUTTON_VARIANTS: readonly ["
|
|
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
|
-
|
|
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 = ["
|
|
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),
|
package/dist/ui/catalog.d.ts
CHANGED
|
@@ -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
|
-
|
|
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<{
|
package/dist/ui/catalog.js
CHANGED
|
@@ -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
|
|
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:
|
|
83
|
+
description: "Text block — size: md (body, default), sm (caption). Optional weight and align.",
|
|
84
84
|
},
|
|
85
85
|
},
|
|
86
86
|
actions: {
|
package/dist/ui/image.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const IMAGE_ASPECTS: readonly ["1:1", "16:9", "4:3", "
|
|
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
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"
|
|
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
package/dist/ui/text.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const TEXT_SIZES: readonly ["
|
|
3
|
-
export declare const TEXT_WEIGHTS: readonly ["bold", "
|
|
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 = ["
|
|
3
|
-
export const TEXT_WEIGHTS = ["bold", "
|
|
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 { withTursoServerless, createInMemoryDataStore } from "@farcaster/snap-turso";
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `@farcaster/snap` — schemas, types, validation
|
|
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` — `withTursoServerless`, `DataStore` / `DataStoreValue`, in-memory and Turso helpers
|
|
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.
|
|
3
|
+
"version": "1.10.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"
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "./constants";
|
|
1
|
+
export type {
|
|
2
|
+
Spec as SnapSpec,
|
|
3
|
+
UIElement as SnapUIElement,
|
|
4
|
+
} from "@json-render/core";
|
|
5
|
+
export { SPEC_VERSION, MEDIA_TYPE, EFFECT_VALUES } from "./constants";
|
|
7
6
|
export {
|
|
8
7
|
DEFAULT_THEME_ACCENT,
|
|
9
8
|
PALETTE_COLOR,
|
|
@@ -22,17 +21,10 @@ export {
|
|
|
22
21
|
type SnapContext,
|
|
23
22
|
type SnapResponse,
|
|
24
23
|
type SnapHandlerResult,
|
|
24
|
+
type SnapElementInput,
|
|
25
|
+
type SnapSpecInput,
|
|
25
26
|
type SnapFunction,
|
|
26
27
|
type SnapPayload,
|
|
27
28
|
} from "./schemas";
|
|
28
|
-
export {
|
|
29
|
-
validateSnapResponse,
|
|
30
|
-
type ValidationResult,
|
|
31
|
-
} from "./validator";
|
|
32
|
-
export {
|
|
33
|
-
type DataStoreValue,
|
|
34
|
-
type SnapDataStore,
|
|
35
|
-
createDefaultDataStore,
|
|
36
|
-
createInMemoryDataStore,
|
|
37
|
-
} from "./dataStore";
|
|
29
|
+
export { validateSnapResponse, type ValidationResult } from "./validator";
|
|
38
30
|
export { type Middleware, useMiddleware } from "./middleware";
|
|
@@ -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" | "
|
|
9
|
-
|
|
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 ?? "
|
|
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={
|
|
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"
|
|
22
|
+
(props.variant as "default") ?? "default";
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<Item variant={variant} className=
|
|
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
|
|
3
|
+
import { Text } from "@neynar/ui/typography";
|
|
4
4
|
|
|
5
5
|
const SIZE_MAP = {
|
|
6
|
-
|
|
7
|
-
|
|
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 "
|
|
24
|
-
const weight = props.weight ? String(props.weight) as "bold" | "
|
|
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, "
|
|
10
|
-
|
|
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 ?? "
|
|
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 "
|
|
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 === "
|
|
40
|
-
const iconColor = variant === "
|
|
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 === "
|
|
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
|
-
|
|
24
|
+
isFilled
|
|
23
25
|
? { backgroundColor: resolvedColor, borderColor: resolvedColor }
|
|
24
26
|
: { borderColor: resolvedColor },
|
|
25
27
|
]}
|
|
26
28
|
>
|
|
27
29
|
{Icon && (
|
|
28
|
-
<Icon size={12} color={
|
|
30
|
+
<Icon size={12} color={isFilled ? "#fff" : resolvedColor} />
|
|
29
31
|
)}
|
|
30
32
|
<Text
|
|
31
33
|
style={[
|
|
32
34
|
styles.label,
|
|
33
|
-
{ color:
|
|
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/schemas.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { Spec } from "@json-render/core";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
SPEC_VERSION,
|
|
6
|
-
} from "./constants";
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_THEME_ACCENT,
|
|
9
|
-
PALETTE_COLOR_VALUES,
|
|
10
|
-
} from "./colors";
|
|
11
|
-
import { type SnapDataStore } from "./dataStore";
|
|
3
|
+
import { EFFECT_VALUES, SPEC_VERSION } from "./constants";
|
|
4
|
+
import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES } from "./colors";
|
|
12
5
|
|
|
13
6
|
// ─── Theme ─────────────────────────────────────────────
|
|
14
7
|
|
|
@@ -43,7 +36,41 @@ export const snapResponseSchema = z
|
|
|
43
36
|
.strict();
|
|
44
37
|
|
|
45
38
|
export type SnapResponse = z.infer<typeof snapResponseSchema>;
|
|
46
|
-
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Permissive element input type for snap handler authors.
|
|
42
|
+
* Allows dynamic element construction without requiring exact UIElement types.
|
|
43
|
+
*/
|
|
44
|
+
export type SnapElementInput = {
|
|
45
|
+
type: string;
|
|
46
|
+
props?: Record<string, unknown>;
|
|
47
|
+
children?: string[];
|
|
48
|
+
on?: Record<string, unknown>;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Permissive input type for the `ui` field in snap handler return values.
|
|
54
|
+
* Accepts dynamically-built element maps (e.g. `Record<string, SnapElementInput>`)
|
|
55
|
+
* without requiring exact UIElement types.
|
|
56
|
+
*/
|
|
57
|
+
export type SnapSpecInput = {
|
|
58
|
+
root: string;
|
|
59
|
+
elements: Record<string, SnapElementInput>;
|
|
60
|
+
state?: Record<string, unknown>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return type for snap handler functions.
|
|
65
|
+
* Uses permissive input types so handlers can build elements dynamically
|
|
66
|
+
* without type casts. Runtime validation via the Zod schema still catches invalid shapes.
|
|
67
|
+
*/
|
|
68
|
+
export type SnapHandlerResult = {
|
|
69
|
+
version: typeof SPEC_VERSION;
|
|
70
|
+
theme?: { accent?: z.input<typeof themeAccentSchema> };
|
|
71
|
+
effects?: z.input<typeof snapResponseSchema>["effects"];
|
|
72
|
+
ui: SnapSpecInput;
|
|
73
|
+
};
|
|
47
74
|
|
|
48
75
|
// ─── POST payload ──────────────────────────────────────
|
|
49
76
|
|
|
@@ -92,7 +119,6 @@ export type SnapAction = z.infer<typeof snapActionSchema>;
|
|
|
92
119
|
export type SnapContext = {
|
|
93
120
|
action: SnapAction;
|
|
94
121
|
request: Request;
|
|
95
|
-
data: SnapDataStore;
|
|
96
122
|
};
|
|
97
123
|
|
|
98
124
|
export type SnapFunction = (ctx: SnapContext) => Promise<SnapHandlerResult>;
|
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 = ["
|
|
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
|
|
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:
|
|
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
package/src/ui/item.ts
CHANGED
package/src/ui/text.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
export const TEXT_SIZES = ["
|
|
4
|
-
export const TEXT_WEIGHTS = ["bold", "
|
|
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
|
|
package/dist/dataStore.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export type DataStoreValue = string | number | boolean | null | DataStoreValue[] | {
|
|
2
|
-
[key: string]: DataStoreValue;
|
|
3
|
-
};
|
|
4
|
-
export type SnapDataStore = {
|
|
5
|
-
get(key: string): Promise<DataStoreValue | null>;
|
|
6
|
-
set(key: string, value: DataStoreValue): Promise<void>;
|
|
7
|
-
};
|
|
8
|
-
export declare function createDefaultDataStore(): SnapDataStore;
|
|
9
|
-
export declare function createInMemoryDataStore(): SnapDataStore;
|
package/dist/dataStore.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export function createDefaultDataStore() {
|
|
2
|
-
const err = new Error("Data store is not configured. Use withTursoServerless() from @farcaster/snap-turso or provide a data store implementation.");
|
|
3
|
-
return {
|
|
4
|
-
get(_key) {
|
|
5
|
-
return Promise.reject(err);
|
|
6
|
-
},
|
|
7
|
-
set(_key, _value) {
|
|
8
|
-
return Promise.reject(err);
|
|
9
|
-
},
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
export function createInMemoryDataStore() {
|
|
13
|
-
const data = new Map();
|
|
14
|
-
return {
|
|
15
|
-
async get(key) {
|
|
16
|
-
return data.get(key) ?? null;
|
|
17
|
-
},
|
|
18
|
-
async set(key, value) {
|
|
19
|
-
data.set(key, value);
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
}
|
package/src/dataStore.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export type DataStoreValue =
|
|
2
|
-
| string
|
|
3
|
-
| number
|
|
4
|
-
| boolean
|
|
5
|
-
| null
|
|
6
|
-
| DataStoreValue[]
|
|
7
|
-
| { [key: string]: DataStoreValue };
|
|
8
|
-
|
|
9
|
-
export type SnapDataStore = {
|
|
10
|
-
get(key: string): Promise<DataStoreValue | null>;
|
|
11
|
-
set(key: string, value: DataStoreValue): Promise<void>;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function createDefaultDataStore(): SnapDataStore {
|
|
15
|
-
const err = new Error(
|
|
16
|
-
"Data store is not configured. Use withTursoServerless() from @farcaster/snap-turso or provide a data store implementation.",
|
|
17
|
-
);
|
|
18
|
-
return {
|
|
19
|
-
get(_key: string): Promise<never> {
|
|
20
|
-
return Promise.reject(err);
|
|
21
|
-
},
|
|
22
|
-
set(_key: string, _value: DataStoreValue): Promise<never> {
|
|
23
|
-
return Promise.reject(err);
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createInMemoryDataStore(): SnapDataStore {
|
|
29
|
-
const data = new Map<string, DataStoreValue>();
|
|
30
|
-
return {
|
|
31
|
-
async get(key: string): Promise<DataStoreValue | null> {
|
|
32
|
-
return data.get(key) ?? null;
|
|
33
|
-
},
|
|
34
|
-
async set(key: string, value: DataStoreValue): Promise<void> {
|
|
35
|
-
data.set(key, value);
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|